From 7dfc7d5f878f3ad6ba00390a865c71fcad2e6347 Mon Sep 17 00:00:00 2001 From: jonnyparris <6400000+jonnyparris@users.noreply.github.com> Date: Mon, 25 May 2026 13:53:30 +0100 Subject: [PATCH 1/2] refactor(ui): tokenise context-warning banner + a11y fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes guided by the new frontend-ui-engineering skill, all on the context-usage warning surface plus one neighbour: 1. #context-warning was a
styled with hardcoded #e67e22 and a click-anywhere-to-dismiss handler. Replaced with a proper sticky banner using --warning / --text-warning tokens (matching the existing-but-unused .banner.warn rule), aria-live='polite' for announce-on-show, and a real
Connection lost — reconnecting…
- +
+ + +
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");