Repository navigation
Announce replies, stops and approvals to screen readers - #393
Open
coolestnick wants to merge 1 commit into
Open
coolestnick wants to merge 1 commit into
coolestnick wants to merge 1 commit into
Conversation
The running footer (role="status") and the thread error banner (role="alert") already tell screen reader users when a turn is running or has failed, but nothing announced a finished reply, a user stop, or a newly waiting approval or question. Closes useagenthq#350. Add one visually hidden polite live region to the conversation that speaks a short sentence for each of those events, once. What to say is a pure function (agent-announcement.ts) picked from a before/after snapshot, so it never streams tokens and is unit tested without a DOM. The region (agent-announcer.tsx) re-reads an identical sentence via a keyed span, and conversation-announcer.tsx maps thread state to it and keys it on the thread root so switching threads resets it. Real failures stay silent here so the error banner remains their sole announcement. Both approval cards already route their error through the shared ApprovalCard, so one role="alert" there covers both lanes. A /lab section drives the announcer so it can be heard with VoiceOver or NVDA.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #350
What this changes
Screen reader users were told when a turn is running (the running footer has
role="status") and when one fails (the thread error banner hasrole="alert"), but nothing announced a finished reply, a user stop, or a newly waiting approval or question. So they had no way to know the agent was done or waiting on them.This adds one visually hidden polite live region to the conversation that announces exactly those three events, once each, and nothing else.
Approach
components/chat/agent-announcement.tsis a pure function,agentAnnouncement(prev, next), that picks the single thing to announce (or nothing) from a before/after snapshot. It never sees streamed tokens and is unit tested without a DOM. A real failure returnsnullon purpose so the error banner stays the only failure announcement.components/chat/agent-announcer.tsxis the live region (role="status",aria-live="polite",sr-only). It holds only a short sentence, never the answer text. A keyed<span>remounts per event so two identical sentences in a row (e.g. a second approval) are each read out.components/chat/conversation-announcer.tsxmaps the thread's state to that snapshot: a completed turn is a reply, a user cancel is a stop, a real failure stays silent. It keys the announcer on the thread root so switching threads resets it instead of replaying the new thread's last event.components/ai/approval-card.tsx: both the gateway and native approval cards already send their error through the sharedApprovalCard, so a singlerole="alert"on that error line covers both lanes.Why
role="status"and notrole="log"The issue points at the
role="log"region inmessage-scroller.tsxas a starting point.logis for append-only histories where only new additions are read; here each event is a single current message, sorole="status"(implicitly polite and atomic) is the right primitive. The keyed-span remount handles re-announcing an identical message.Done when
Tests
components/chat/agent-announcement.test.ts- the pure rule: every transition, reply vs stop, failure stays silent, priority, baseline on first snapshot.components/chat/agent-announcer.interaction.test.tsx- the live region under happy-dom: polite status, announce once, no re-announce on resolve, keyed re-announce.components/chat/conversation-announcer.interaction.test.tsx- the mapping end to end: completed turn to reply, user cancel to stop, real failure silent, new gateway approval announced.components/ai/approval-card.test.tsx- asserts the error line is arole="alert".Checks
bun run typecheck(repo root) - passcd frontend && bun run test- 1787 pass, 0 failcd frontend && bun run lint- cleancd frontend && bun run build- passbun test scripts/agent-docs.test.ts- passHow to see it
A
/labsection ("Chat - Screen-reader announcements") drives the real announcer: fire each event and see what it speaks, or turn on VoiceOver / NVDA to hear it. Since the region is invisible, the screenshots below show that lab, and the recording shows it being read out.Need help on this PR? Tag
@codesmith-botwith what you need.