Bundle
@deepseek-ai/dsh-a11y-announcer
Accessibility plugin for the DeepSeek Harness web UI: announces tool calls as they appear and finished assistant messages through an aria-live region so screen readers read them aloud.
- Source
- slohmaier
- License
- MIT
- Updated
- Updated 2 days ago
Readme
# dsh-a11y-announcer
Accessibility plugin for the DeepSeek Harness web UI. Announces tool calls as
soon as they appear and finished assistant messages through an `aria-live`
region. NVDA/JAWS/VoiceOver read the live region automatically, without moving
the user's focus.
## Behavior
- **Tool calls** are announced shortly after they appear ("Tool: <name +
content>"), once their text is briefly stable — NOT when they finish (the
start is the interesting moment). A short debounce (TOOL_SETTLE_MS=400ms)
avoids reading the very first streaming fragment (e.g. a half token like
"ulti" from "multi-...").
- The last **finished** assistant message is announced with its full text over
the live region (not while streaming).
- Only the visible answer text is read; the agent's internal reasoning ("Think"
block) is excluded so streamed reasoning fragments are not read aloud.
- Deduplication via `data-chat-anchor-key`: each element is announced only
once; re-renders do not trigger a new announcement.
- Assistant settle is detected via the `data-streaming` attribute (dsh removes
it once streaming finishes), so only complete text is announced.
- No replay: nodes already present when loading an old session are not walked
through, only new events from now on.
## Deep-diving announcement
- While the dsh "Deep diving..." status (agent working) is active, "Deep
diving..." is announced every 5 seconds over the live region for as long as
the state persists (first announcement after 5s, then every 5s).
- Detection by content: a `role="status"` element whose text contains "Deep
diving". IMPORTANT: dsh may additionally render an EMPTY `role="status"`
element; therefore match on text content, not on the first role=status.
- Fallback: in addition to the MutationObserver, a 1s polling loop (for missed
mutations).
## How it works
- Client plugin, discovered via `dsh.client` metadata (`platform: web`) and
activated as its own bundle (`dsh.bundle.patch` = cordis.patch.yml with
`- insert`) in the web profile.
- `lib/index.js` = plain host loader entry (`apply`/`inject`), NO
`window.__ModuleLoader__` wrapper (server-side, otherwise "window is not
defined" and the plugin is not activated).
- `lib/client.js` = browser bundle (ModuleLoader wrapper). Uses a
MutationObserver on the `[data-slot="conversation.session"]` container and
selects targets via `data-chat-flow-kind` / `data-chat-anchor-key`.
- Assistant settle detection via the `data-streaming` attribute: dsh renders
the assistant markdown root with `data-streaming` while it streams and
REMOVES it once the full text is settled. The plugin observes attribute
changes (`attributes` + `attributeFilter: ["data-streaming"]`) and only
announces when the node is no longer streaming — so it never reads partial
output.
- Tool calls are announced after a short debounce (TOOL_SETTLE_MS=400ms) once
their text is briefly stable, without waiting for completion.
- Text extraction: clones the node and REMOVES the agent's internal reasoning
("Think" block, `data-variant="think"`) before reading `textContent`. This
keeps the hidden, streamed reasoning fragments from being read aloud — only
the visible answer text is announced. It also strips known model-artifact
lines (a bare "ulti" the LLM occasionally appends). For tool calls,
`textContent` also captures collapsed/hidden content (which `innerText`
would omit).
- Announcement: a hidden (but assistive-visible) `div` with
`aria-live="polite"` + `role="status"`; `textContent` is set when settled.
No `element.focus()` (moves the user's focus unnecessarily and did not
reliably read elements).
## Installation
- Package: this repository (e.g. cloned to `/opt/dsh/plugins/dsh-a11y-announcer/`).
- Add as a file dependency in `/opt/dsh/package.json` and
`/mnt/ssd/dsh/profiles/web/package.json`.
- Add as a bundle in `dsh.profile.bundles` (profiles/web/package.json).
- After changes to `lib/client.js`: restart `dsh web` (no bundle build needed,
the file is served directly).
## Files
- `lib/client.js` — browser bundle (MutationObserver + aria-live announcement)
- `lib/index.js` — host loader entry (empty)
- `cordis.patch.yml` — `- insert` of the loader entry
- `package.json` — `dsh.client` + `dsh.bundle` + `exports["./client"]`
## Tests
Logic verified in jsdom (see development test cases):
- A tool call is announced shortly after appearing ("Tool: <name>") with its
full text, without waiting for completion; streaming fragments (e.g. "ulti"
from "multi-...") are not announced thanks to the short debounce.
- A finished assistant is only announced after text stability (900ms) with the
full text, not while streaming.
- The live region has `aria-live="polite"`.
- Deduplication via `data-chat-anchor-key` works.
Install
dsh plugin --profile web add github:slohmaier/dsh-a11y-announcer#02ddba97739fe737f2b71b524392d1f265cf2684
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install deepseek-ai-dsh-a11y-announcer from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.