anomalyco / anomalyco/opencode
NVDA cannot read streaming assistant replies or counter numbers (desktop app)
@Hona is already working on this.
Since Aug 9, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
NVDA cannot read streaming assistant replies or counter numbers (desktop app)
Environment
- App: opencode desktop app v1.18.15 (Windows 10/11 x64)
- Screen reader: NVDA (browse mode)
- Renderer: Electron
Problem 1 — Streaming assistant replies are invisible to NVDA
While the assistant is generating a reply, NVDA in browse mode cannot read the new content with the virtual cursor (arrow keys). Pressing Tab once "refreshes" the page and only then the reply becomes readable. The new content is effectively missing from the accessibility tree until the turn finishes.
Root cause (message timeline):
MessageTimelinerenders rows through a virtualized list (createVirtualizer/VirtualTimelineRow), so only viewport rows are in the DOM.- In
renderTimelineRow, theAssistantPartbranch setsaria-hidden=trueon the whole assistant content while the turn is "working":
During streaming the entire reply is hidden from assistive tech; it only reappears (and NVDA only notices) after the turn ends.const workingTurn = (userMessageID) => sessionStatus().type !== "idle" && activeMessageID() === userMessageID; // ... createRenderEffect(() => setAttribute(_el, "aria-hidden", workingTurn(assistantPartRow().userMessageID)) );
Verified locally: removing the aria-hidden binding keeps the streaming content in the accessibility tree and NVDA reads it as it grows.
Problem 2 — Counter numbers are read as a long digit string
The context summary (N read / N search / N list via AnimatedCountList) and the todo progress (X of Y todos completed via SessionTodoDock) use the AnimatedNumber component. NVDA reads something like 0123456789... instead of the actual number.
Root cause:
AnimatedNumberrenders each digit as a vertical strip of 30 cells (data-slot=animated-number-cell, digits 0-9 repeated), with only one visible via CSS transform. The hidden cells have noaria-hidden, so their text is exposed to the accessibility tree.- The outer element is a bare
<span data-component=animated-number>that setsaria-labeldynamically, butaria-labelon a roleless<span>is invalid ARIA (axereportsaria-prohibited-attr), so it is not a reliable accessible name.
Verified locally: adding aria-hidden=true to <span data-slot=animated-number-value> (hiding the strip) makes NVDA read the number correctly.
Local workaround (validated)
Patching the two spots above in the desktop app bundle fixes both problems, confirming the root causes.
Suggested upstream fixes
- Streaming replies: do not
aria-hiddenthe assistant content while streaming. Announce new content via a proper live region (role=status/aria-live=polite, with debounce), and consider exposing the virtualized list so screen-reader users can reach off-screen/new rows (higher overscan or a non-virtualized fallback,aria-busyon the scroll container). - Numbers: hide the animated digit strip from assistive tech (
aria-hidden=trueondata-slot=animated-number-value/ the strip) and expose the value as real text or via a valid accessible name (a visually-hidden text node, or an element/role that supportsaria-label).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.