anomalyco / anomalyco/opencode

NVDA cannot read streaming assistant replies or counter numbers (desktop app)

Open
#41,408 2 comments 0 reactions 1 assignee View on GitHub

@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):

  • MessageTimeline renders rows through a virtualized list (createVirtualizer / VirtualTimelineRow), so only viewport rows are in the DOM.
  • In renderTimelineRow, the AssistantPart branch sets aria-hidden=true on the whole assistant content while the turn is "working":
    const workingTurn = (userMessageID) =>
      sessionStatus().type !== "idle" && activeMessageID() === userMessageID;
    // ...
    createRenderEffect(() =>
      setAttribute(_el, "aria-hidden", workingTurn(assistantPartRow().userMessageID))
    );
    
    During streaming the entire reply is hidden from assistive tech; it only reappears (and NVDA only notices) after the turn ends.

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:

  • AnimatedNumber renders 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 no aria-hidden, so their text is exposed to the accessibility tree.
  • The outer element is a bare <span data-component=animated-number> that sets aria-label dynamically, but aria-label on a roleless <span> is invalid ARIA (axe reports aria-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
  1. Streaming replies: do not aria-hidden the 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-busy on the scroll container).
  2. Numbers: hide the animated digit strip from assistive tech (aria-hidden=true on data-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 supports aria-label).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.