Status: WORK IN PROGRESS. This add-on is functional but still being polished. Hotkeys, foreground routing, and the Hermes
app.asarpatcher all work, but expect rough edges: the session picker dialog is unstyled, the diagnostic dump is verbose, the speech filter regex set is not exhaustive, and the auto-read behavior in OpenCode may stutter on long messages. Do not rely on this for production screen-reader use without testing your specific workflow first. Please file issues for anything that gets in your way.
An NVDA screen-reader add-on that improves accessibility of two desktop apps:
- Hermes Agent (Electron)
- OpenCode Desktop (Electron)
The add-on is foreground-aware: the same hotkey does the right thing in each app, with no double-bindings and no conflicts. It is a merge of the previous hermesAccessibility and opencodeAccessibility add-ons.
Grab the latest .nvda-addon from the Releases page. The current build is v2.1.0.
Two equivalent ways to install the add-on on NVDA 2024.1 or later:
Option A — open the file directly
Double-click agentDesktopAccessibility-2.1.0.nvda-addon in your file manager (or open it from your browser's downloads). NVDA will detect the add-on and offer to install it.
Option B — from the Add-on Store
- In NVDA: NVDA+N → Tools → Add-on Store.
- Open the add-on store menu and choose Install from External Source.
- Select the downloaded
.nvda-addonfile. - Restart NVDA when prompted.
Upgrade note: if you have the legacy
hermesAccessibilityoropencodeAccessibilityadd-ons installed, disable them first. Both can be uninstalled once you've confirmed the merged add-on works.
Every shared gesture in the tables below checks which app is currently focused:
- Hermes focused — calls the Hermes backend (
state.db,hermes://session/<id>deep links, status suppression,@picker). - OpenCode focused — calls the OpenCode backend (
opencode.db,opencode://open-project?directory=...deep links, auto-read, thinking trace). - Neither focused — the gesture passes through (
gesture.send()), so it can be handled by other apps or NVDA itself.
App-specific gestures (e.g. NVDA+Alt+T for the OpenCode thinking trace) only fire when that app is the foreground. They pass through everywhere else.
| Gesture | Action |
|---|---|
| NVDA+Alt+Down | Next message |
| NVDA+Alt+Up | Previous message |
| NVDA+Alt+Home | First message (force refresh) |
| NVDA+Alt+End | Last message (force refresh) |
| NVDA+Alt+R | Re-read current message |
| NVDA+Alt+S | Open session switcher dialog |
| NVDA+Alt+Shift+N | Next session (cycle) |
| NVDA+Alt+Shift+P | Previous session (cycle) |
| Ctrl+N | New session. In OpenCode this fires the add-on's 5-method fallback chain (button → API → bridge → clipboard → keystroke). In Hermes it passes through to the OS — Hermes handles Ctrl+N natively. In any other app it passes through normally. |
| NVDA+Alt+D | Diagnostic dump |
| NVDA+Alt+Shift+D | Foreground window metadata (always on) |
These pass through when Hermes is the foreground app.
| Gesture | Action |
|---|---|
| NVDA+Alt+T | Read thinking trace for current assistant message |
| NVDA+Alt+A | Toggle auto-read of new assistant messages |
These fire only when Hermes is the foreground app. In OpenCode (or any other app) they pass through to the OS or to NVDA's default handling. Preserved from hermesAccessibility 1.7.2.
| Gesture | Action |
|---|---|
| NVDA+Alt+Space | Open the Hermes @ reference picker |
| NVDA+Shift+H | Toggle Hermes speech filter (silence status spam) |
| NVDA+Shift+J | Hermes speech filter status + suppression count |
Press NVDA+Alt+Space when focused in Hermes to open a two-pane dialog. The left pane lists reference types — Folder is first (most common). The right pane shows recent entries formatted as name — full path so two folders with the same basename are easy to tell apart.
@folder:— browse for a folder, or pick from recent. Inserts@folder:full/path/to/folder.@file:— browse for a file, or pick from recent. Inserts@file:full/path/to/file.@url:— type or pick a URL. Inserts@url:https://....@diff— inserts immediately (Git working-tree diff).@staged— inserts immediately (Git staged diff).@git:5/@git:10/@git:20— prompts for commit count, then inserts.
Paths with whitespace are automatically wrapped in backticks on the wire, mirroring the desktop's formatRefValue cascade, so a folder like C:/Users/willb/programs/Hermes accessibility arrives intact and the agent's filesystem lookup succeeds.
Hermes repeatedly announces thinking / running / spinner characters / timers (1:13, 5m 30s) while the agent is working. The add-on hooks the synth driver's speak() method (the only Python-level interception point that catches Electron IA2 live-region announcements) and drops any utterance that matches a known status pattern.
- Toggle with NVDA+Shift+H.
- Check the current state and suppression count with NVDA+Shift+J.
- Hermes — uses the
hermes://session/<id>deep-link protocol, auto-patched intoapp.asarthe first time you use it, and re-applied automatically (with audible failure announcements) if Hermes updates and overwrites the patch. - OpenCode — uses the
opencode://open-project?directory=...deep link.
Both protocols route through the running app's existing IPC — no second process is spawned, no keystroke simulation is needed.
The Hermes desktop app's built-in deep-link handler only routes kind=blueprint links to the renderer. The add-on's patch_app_asar.js injects a 3-line branch that also routes kind=session to the renderer's existing hermes:focus-session listener.
- Self-healing — re-checks the patch on every session-pick (60s TTL cache), and re-applies if a Hermes update overwrote it.
- Audible failure — if the patch cannot be applied, NVDA announces "Hermes session patch failed: <reason>. Session picker will not work." — you'll never be left wondering why picking does nothing.
- Pattern-based matching — the patcher locates the target line by function structure, not exact text, so cosmetic upstream reformatting doesn't break it.
- Diagnostic — pressing NVDA+Alt+D while Hermes is foreground reports "Patcher: asar OK/MISSING, marker found/not-found".
- Manual audit — run
node patch_app_asar.js --auditfor a JSON status report with no side effects.
The proper long-term fix is for Hermes' handleDeepLink to route kind=session natively — a one-line change. Until that lands upstream, the patcher is the binding solution.
- NVDA 2024.1 or later (tested on 2026.1)
- Hermes Agent desktop app (Electron) — speech filter, message nav, session switching,
@picker - OpenCode Desktop — message nav, session switching, auto-read, thinking trace
addon/ # NVDA add-on source (manifest.ini + Python modules)
manifest.ini
appModules/Hermes.py
globalPlugins/agentDesktopAccessibility.py
globalPlugins/addtl/ # backends, router, completion, speech filter
buildVars.py # build metadata (name + version)
build_addon.py # builds the .nvda-addon zip from addon/
patch_app_asar.js # Hermes app.asar patcher (bundled in the .nvda-addon)
readme.html # in-NVDA documentation (referenced by manifest.ini)
COPYING # GPL v2+
build_addon.py reads buildVars.py, walks addon/, drops __pycache__ and .pyc, and produces agentDesktopAccessibility-<version>.nvda-addon in the repo root. Built artifacts are .gitignored; releases are attached via GitHub Releases.
Free software. Modify and redistribute under the terms of the GNU GPL v2 or later. See COPYING.