Skip to content

bug(session-ui): V2 still hides streaming assistant content from screen readers #54652

Open

Description

@aimansour

Description

V2 still hides visible assistant text, reasoning and tool steps from the accessibility tree while the active response is generating. Screen-reader users cannot browse this content until the response finishes.

The current renderer wraps assistant rows in aria-hidden={workingTurn(current().userMessageID)}. workingTurn is true for the active message while session status is not idle: condition, assistant wrapper.

OpenCode version

2.0.26 source build, latest v2 commit 4617210822bdbb31e5749751afbfb4584ccedac8 (audited 2026-10-12). The affected file is also unchanged at current dev commit 707de5832656e89cc0f04436aa1cd5f4f91c5763.

Steps to reproduce

  1. Open a Desktop/Web session with a screen reader.
  2. Submit a prompt that produces streamed text and tool calls.
  3. Browse the visible assistant output before generation completes.

Expected: visible content remains available to assistive technology during generation. Actual: the entire assistant subtree is excluded by aria-hidden.

Related reports and fix

This is the V2 continuation of #33137 and #46396, and Problem 1 of #41408. They were closed by the V1 migration triage, not by a merged fix.

Existing PR #51084 removes this barrier and includes an incomplete-response accessibility regression covering text and a tool step. Earlier PR #48597 was closed in favor of #51084. The AnimatedNumber problem from #41408 is tracked separately in #54653; this issue tracks streaming-content exposure.

Activity

  1. opencode-agent commented on Oct 12, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I was able to reproduce this, and it still happens on the latest dev (707de58).

    I used the app's Playwright e2e setup to show a session with an unfinished assistant message: a completed shell step plus some text. The session status is busy.

    • While the response is unfinished: both session-turn-assistant-content wrappers have aria-hidden="true". The text is visible on screen, but it is missing from the accessibility tree (getByRole("paragraph") finds nothing).
    • Once the response is finished: aria-hidden is false and the content is exposed as expected.
    • Without the attribute: removing aria-hidden={workingTurn(...)} from session-timeline-row.tsx makes both cases pass. That is the same change as in fix(session-ui): expose streaming assistant steps #51084.

    The script below reproduces it. Related history: #33137, #46396 and #41408 (Problem 1) were closed without a merged fix, and open PR #51084 addresses this.

    Reproduction script (issue-54652.spec.ts)
    // Reproduction for anomalyco/opencode#54652: streaming assistant content is aria-hidden.
    // Run from packages/app: bunx playwright test e2e/reproduction/issue-54652.spec.ts
    import { expect, test } from "@playwright/test"
    import { assistantMessage, setupTimeline, shell, textPart, userMessage } from "../utils/timeline"
    
    const text = "Streaming assistant output"
    
    for (const completed of [false, true]) {
      test(`assistant content accessibility (completed=${completed})`, async ({ page }) => {
        await setupTimeline(page, {
          messages: [
            userMessage(),
            assistantMessage([shell("prt_a11y_shell", "completed", "done"), textPart("prt_a11y_text", text)], {
              completed,
            }),
          ],
        })
        const assistant = page.locator('[data-slot="session-turn-assistant-content"]').filter({ hasText: text })
        await expect(assistant).toBeVisible()
        const hidden = await page
          .locator('[data-slot="session-turn-assistant-content"]')
          .evaluateAll((els) => els.map((e) => e.getAttribute("aria-hidden")))
        console.log(`completed=${completed} aria-hidden values:`, JSON.stringify(hidden))
        await page.screenshot({ path: `/workspace/repro/issue-54652-completed-${completed}.png` })
        // Visible text must be exposed in the accessibility tree.
        await expect(page.getByRole("paragraph").filter({ hasText: text })).toHaveCount(1, { timeout: 3000 })
      })
    }

    Run it: bun issue-54652.spec.ts

  2. pranam-s commented on Oct 20, 2026

    @pranam-s

    I'm a screen reader user on the web UI (NVDA on Windows) and hit this.

    Removing aria-hidden puts the content in the accessibility tree, but it stays silent. There is no live region in the transcript. The only role="status" is the "Working" chip at packages/app/src/session/timeline/message-timeline.tsx:411, and that one cannot announce either:

    • it sits inside a <Show>, so the region is created along with its text
    • the text is in aria-hidden spans (TextShimmer), and Chrome ignores aria-label on a role=generic span, so the live region computes to empty text

    I don't want to open a second PR for the aria-hidden part, @aimansour has one open in #51084. What I would like to add is one polite live region outside the virtualized rows that announces the turn status ("Thinking", "Running commands", and so on). The ui.sessionTurn.status.* strings in packages/ui/src/i18n/en.ts are already translated in every locale, but only one of the twelve is used anywhere.

    Would you want this tracked here, or in a separate issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions