
diff --git a/public/js/dodo-chat.js b/public/js/dodo-chat.js
index 318ac6f..2cce083 100644
--- a/public/js/dodo-chat.js
+++ b/public/js/dodo-chat.js
@@ -457,26 +457,39 @@ function updateTokenSummary(state){
el.textContent='';
el.title='';
}
- const banner=$("context-warning");
- if(banner)banner.style.display="none";
+ hideContextWarning();
return;
}
- // Color code by context usage
+ // Color code by context usage — use design tokens, not hardcoded hex
let color='var(--text-subtle)';
- if(pct>80)color='#e74c3c';
- else if(pct>50)color='#e67e22';
+ if(pct>80)color='var(--text-danger)';
+ else if(pct>50)color='var(--text-warning)';
el.style.color=color;
const budgetStr=budget?`${Math.round(budget/1000)}k`:'?';
el.textContent=pct>0?`Context: ${pct}% of ${budgetStr} · ${(ti/1000).toFixed(1)}k in / ${(to/1000).toFixed(1)}k out`:`${(ti/1000).toFixed(1)}k in / ${(to/1000).toFixed(1)}k out`;
el.title=`Context window: ${state.contextWindow??0} tokens\nBudget (80%): ${budget} tokens\nUsage: ~${pct}%\nModel: ${state.model??'unknown'}`;
// Show/hide context warning banner
- const banner=$("context-warning");
- if(banner){
- if(pct>80){banner.style.display="block";banner.textContent=`Context is ${pct}% full. Consider starting a new session for a fresh topic.`}
- else{banner.style.display="none"}
+ if(pct>80){
+ showContextWarning(`Context is ${pct}% full. Consider starting a new session for a fresh topic.`);
+ } else {
+ hideContextWarning();
}
}
+// Show/hide the sticky context-usage banner. Writes only the message span so
+// the dismiss button (and its aria-label) are preserved across updates.
+function showContextWarning(message){
+ const banner=$("context-warning");
+ if(!banner)return;
+ const msg=$("context-warning-msg");
+ if(msg)msg.textContent=message;
+ banner.classList.add("visible");
+}
+function hideContextWarning(){
+ const banner=$("context-warning");
+ if(banner)banner.classList.remove("visible");
+}
+
// --- Chat actions ---
async function sendMessage(){
const content=$("msg-input").value.trim();
@@ -519,7 +532,7 @@ async function sendMessage(){
}
async function abortPrompt(){if(!currentSession)return;await jsonSafe(`/session/${currentSession}/abort`,{});setProcessing(false);hideThinking()}
async function forkSession(){if(!currentSession)return;const d=await jsonSafe(`/session/${currentSession}/fork`,{});if(!d)return;const{id}=d;currentSession=id;await selectSession(id)}
-async function deleteSession(){if(!currentSession)return;const ok=await appConfirm("Delete this session? This can\u2019t be undone.");if(!ok)return;await apiSafe(`/session/${currentSession}`,{method:"DELETE"});currentSession=null;clearPendingImages();$("chat").innerHTML="";$("session-title-display").textContent="No session";$("session-id-display").textContent="";$("token-summary").textContent="";$("token-summary").style.color="";const cw=$("context-warning");if(cw)cw.style.display="none";$("presence-bar").innerHTML="";if(typeof renderSessionTodos==='function')renderSessionTodos([]);history.replaceState(null,"",location.pathname);await loadSessions();if(window.innerWidth<=900)switchTab('chat')}
+async function deleteSession(){if(!currentSession)return;const ok=await appConfirm("Delete this session? This can\u2019t be undone.");if(!ok)return;await apiSafe(`/session/${currentSession}`,{method:"DELETE"});currentSession=null;clearPendingImages();$("chat").innerHTML="";$("session-title-display").textContent="No session";$("session-id-display").textContent="";$("token-summary").textContent="";$("token-summary").style.color="";hideContextWarning();$("presence-bar").innerHTML="";if(typeof renderSessionTodos==='function')renderSessionTodos([]);history.replaceState(null,"",location.pathname);await loadSessions();if(window.innerWidth<=900)switchTab('chat')}
// --- Session rename ---
async function renameSession(){
@@ -666,3 +679,12 @@ function clearPendingImages(){
_pendingImages.length=0;
renderImagePreviews();
}
+
+// Wire up the context-warning dismiss button. The banner returns next time
+// updateTokenSummary() decides usage is still > 80%, so this is an "until I
+// stop typing" dismissal, not a permanent suppression — which matches the
+// pre-refactor click-anywhere behaviour.
+window.addEventListener('load',()=>{
+ const btn=$("context-warning-dismiss");
+ if(btn)btn.addEventListener('click',hideContextWarning);
+});
diff --git a/public/js/dodo-sessions.js b/public/js/dodo-sessions.js
index 44a61b9..6fc767a 100644
--- a/public/js/dodo-sessions.js
+++ b/public/js/dodo-sessions.js
@@ -27,7 +27,9 @@ function connectUserEvents(){
};
}
function _sessionItemHtml(s){
- return `
${esc(s.title||s.id.slice(0,8))}
${esc(s.status)} · ${esc(new Date(s.updatedAt).toLocaleString())}
`
+ // Both Enter and Space must activate `role=button` elements (ARIA spec).
+ // preventDefault() on Space stops the page from scrolling.
+ return `
${esc(s.title||s.id.slice(0,8))}
${esc(s.status)} · ${esc(new Date(s.updatedAt).toLocaleString())}
`
}
function renderSessionList(sessions){
const el=$("session-list");
@@ -82,7 +84,7 @@ async function selectSession(id){
// to whatever the user was about to send there, not here.
if(typeof clearPendingImages==='function')clearPendingImages();
$("chat").innerHTML="";$("onboarding")?.remove();
- const cw=$("context-warning");if(cw)cw.style.display="none";
+ if(typeof hideContextWarning==='function')hideContextWarning();
showSkeleton($("chat"),5);
$("session-id-display").textContent=id.slice(0,8);
$("session-title-display").textContent=id.slice(0,8);
From 6f2bc9a677a4ecad23468133d98ef7d12798c3be Mon Sep 17 00:00:00 2001
From: jonnyparris <6400000+jonnyparris@users.noreply.github.com>
Date: Mon, 25 May 2026 13:59:24 +0100
Subject: [PATCH 2/2] fix(a11y): drop session-item aria-label to preserve meta
announcement
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
aria-label on an element hides inner text from AT (only the label is
announced). The session-item div contains both the title and a meta line
with status + timestamp; with aria-label='Open session ${title}' screen
readers would lose access to status and timestamp.
Removing aria-label lets accessible-name computation fall through to the
title + meta children, matching what sighted users see. role='button' +
the surrounding context still convey that it's actionable.
beep-boop-ruskin-agent-🤖
---
public/js/dodo-sessions.js | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/public/js/dodo-sessions.js b/public/js/dodo-sessions.js
index 6fc767a..be49877 100644
--- a/public/js/dodo-sessions.js
+++ b/public/js/dodo-sessions.js
@@ -29,7 +29,11 @@ function connectUserEvents(){
function _sessionItemHtml(s){
// Both Enter and Space must activate `role=button` elements (ARIA spec).
// preventDefault() on Space stops the page from scrolling.
- return `
${esc(s.title||s.id.slice(0,8))}
${esc(s.status)} · ${esc(new Date(s.updatedAt).toLocaleString())}
`
+ // No `aria-label` — when set, AT announces only the label and skips inner
+ // text, which would hide the `.session-meta` status + timestamp. Letting
+ // accessible-name computation fall through to the title + meta children
+ // matches what sighted users see.
+ return `
${esc(s.title||s.id.slice(0,8))}
${esc(s.status)} · ${esc(new Date(s.updatedAt).toLocaleString())}
`
}
function renderSessionList(sessions){
const el=$("session-list");