Skip to content

feat: dictate from any app, with a widget that sends to a session (core 0.43.0) - #246

Merged
Maxaubert merged 18 commits into
mainfrom
feat/dictate-anywhere
Oct 17, 2026
Merged

Maxaubert merged 18 commits into
mainfrom
feat/dictate-anywhere

Conversation

@Maxaubert

@Maxaubert Maxaubert commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #244 (the orchestrator): merges after it; retarget to main before #244 merges with its branch deleted.

Owner, 2026-10-16: "you can start the dictation while not having the app in focus ... you should still see like a sort of widget ... if you don't have an orchestrator ... the widget will show like a list of sessions and you essentially just click or pick the session ... if you have the orchestrator the widget will just show something like completed with a check mark and sent ... only if the orchestrator is actually active"; "one dictation key ... control plus the windows key"; number keys in the list: "an excellent idea". Spec and plan (approved): docs/superpowers/specs/2026-10-16-dictate-anywhere.md.

What the owner gets

  • One dictation key, Ctrl+Win by default (right Alt is AltGr on a Norwegian keyboard). A saved key is kept. Inside the app nothing changes.
  • With another app in front the key starts dictation and a widget shows on the screen the pointer is on: never takes focus, above every window, sized for 225% zoom, in the theme's inks.
  • Then: an active orchestrator (switched on, promoted, its Claude running per Windows, not asking) gets the words, the widget says Sent to with a check mark; otherwise the widget lists the Claude sessions numbered 1 to 9 (click or press the number; Esc keeps it unsent; 30 s and it lapses). A tab that is asking is listed but disabled.
  • Settings > Dictation > Dictate from any app, on by default when dictation is on.

Core (Prism gets the key default)

  • DEFAULT_HOTKEY Ctrl+Win; a chord of modifiers follows the solo rules (either key first; Ctrl+Win+Left is no dictation; the settings field binds it).
  • dictationOutside / setDictationRoute: a host starts a recording that belongs to no tab and takes what was heard (or why not: no model, nothing heard, cancelled).
  • DictationPage takes after rows.

App

  • src/main/hook/globalKeyHook.ps1 + globalDictKey.ts: a WH_KEYBOARD_LL hook in a long-lived Windows PowerShell (like the DWM helper). MEASURED spike: 0.3 to 1.2 ms key to event, ~0.35 s to start, no CPU at rest, no Defender detection. Reads only the binding's keys, "another key" (never which), and digits/Esc only while the list is open (swallowed then). Masks the Start menu after Ctrl+Win (VK 0xE8). Never runs under --e2e.
  • dictateAnywhere.ts: the widget window (sandboxed, never navigates) and the routing, through the same checked send path as the orchestrator (sessionsWire.sendInto: Windows' process check, decided again before typing and before Enter). The rule 2 exception's entry now names it.
  • src/renderer/overlay.html, src/preload/overlay.ts: the widget page (impeccable) and its bridge.

Tests: unit 2288 pass (key chord 4, the hook helper on a real F24, the feed, the route and the rows). e2e dictateAnywhere 9/9: no orchestrator lists the session and "1" sends and enters it; an active orchestrator gets it with no list; the widget is unfocusable and on top; Ctrl+Win from another app opens it. Screenshots: .e2e-shots/dictation-widget-pick.png, -sent.png. Full e2e: every scenario passed but the two existing Right Alt dictation scenarios, which assumed the old default; they now set their key and pass (dictation 21/21, dictationParakeet 11/11). "The real clipboard: untouched by the run".

Owner's hands-on list (needs a real keyboard): Ctrl+Win hold from another app; the Start menu staying shut after it; Ctrl+Win+arrows still switching desktops; picking with a number key.

Since the owner tried it (2026-10-17)

  • Two copies both took every press (MEASURED in both diag logs: Stable and the installed app each ran the hook; the second failed in 8 ms with "No microphone found" and its widget covered the first's). One copy now holds the key (a lock file; a named Mutex hung the waiting helper, MEASURED), and a saved mic that is gone falls back to the system default.
  • One dictation (owner: "they should be the same thing ... one dictation engine for both"; spec docs/superpowers/specs/2026-10-17-one-dictation.md, approved): the hook reads the key with the app in front too, one widget everywhere, and the copy holding the key runs the dictation for every copy over a local pipe (dictationLink). Words for a copy whose window was in front are pasted at its cursor, never Enter; otherwise the orchestrator or the list, which holds every copy's sessions. e2e dictateTwoCopies.
  • The link is authenticated (security review): both ends prove they hold a per-user key (PrismDictation\link.key, HMAC over fresh nonces); the pipe is named by the key's hash.
  • Performance, startup first (owner: "it's mainly startup performance"; report research/prism-terminal/2026-10-17-dictation-performance.md):
    • Parakeet stays loaded in a resident host (core/dictation-host/parakeetHost.ps1, P/Invoke into the engine's own parakeet.dll): 24 to 170 ms a pass against parakeet-cli's 0.72 to 0.86 s on the owner's RTX 5090, the text identical; ready 0.8 s after launch.
    • One helper process at launch (src/main/hook/appHelper.ps1) for the DWM attributes and the key hook, where there were two; compiled C# cached (up 320 -> 220 ms).
  • The resume skeleton is no longer drawn over by xterm's link decorations (the owner's "thin blue lines"): the terminal region is isolate. e2e launchSkeleton fails without it.

Full e2e: every scenario passed; updateWindow was re-run alone after a version file changed under the run, and passes. Unit 2302.

Core 0.42.0 -> 0.43.0, app 0.56.0 -> 0.57.0.

🤖 Generated with Claude Code

https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

Maxaubert and others added 13 commits October 16, 2026 13:43
…ve not seen (core 0.42.0)

Owner, 2026-10-16: a report of what the Claude sessions did while away and
what needs them next, based on whether they have SEEN each session, with a
tab left in front while away not counted as seen. Spec and plan:
docs/superpowers/specs/2026-10-16-catch-up-report.md (reviewed before
building; 25 findings folded in).

Core: the turn's own hooks (UserPromptSubmit, Stop) add session=<id>
(hook.cmd reads CLAUDE_CODE_SESSION_ID with delayed expansion, only a
UUID-shaped value); the parser carries it; setSessionsFile + an always
replaced PRISM_SESSIONS_FILE in ptyEnv; the plugin's catch-up skill and its
Windows PowerShell 5.1 script.

App: sessionLedger (pure): sessions kept apart from tabs; seen = in front
10 s while present (focused, shown, not locked or asleep, input within
120 s) or typed into, capped at last input + 10 s, restarted after any time
away. sessionsWire writes sessions.json, flushed with the tabs in close,
session-end and will-quit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
The digest quotes other sessions (and whatever web pages or files they
read), so a blanket powershell grant would let injected text run any
command without a prompt. The two script calls now ask like any other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
…role

Owner, 2026-10-16: others who download the terminal should be able to
use it "if they want ... if they don't want it, it's like already in
there"; an orchestrator tab "promoted in the tab bar ... first in the list
and there's a divider line"; "right click and choose make orchestrator it
already knows".

- Settings > Agents > Orchestrator (beta), off by default. Off: no session
  list, no file, no PRISM_SESSIONS_FILE, no plugin, no menu entry.
- core/claude-plugin-orchestrator: a second Claude plugin given to new
  tabs only when it is on, with the catch-up and orchestrator skills and
  role.cmd, which adds a fixed additionalContext in the promoted tab only
  (MEASURED: a plugin hook's additionalContext reaches the model).
- Make orchestrator in a tab's menu: one tab, kept first, an accent
  outline and rule; saved in tabs.json; orchestrator-tab.txt names its
  PRISM_TAB_ID for the role hook.
- The script gains -List and -Only for the orchestrator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
Owner, 2026-10-16: "the terminal needs to be able to send it, and it needs
to be able to send it to any session, not just whichever is in focus.
That is very important." An owner exception to rule 2, recorded in
regression-rules.md and CLAUDE.md.

- orchestratorSend.ts (pure): only from the promoted tab, into the tab
  named by label, a word of its label or folder, its session or its
  PRISM_TAB_ID; never into a bare shell (it would run), never while that
  tab asks a question (it would answer it); one sanitised bracketed paste,
  then Enter after 300 ms (MEASURED on Claude Code 2.1.301: one prompt,
  multi-line kept).
- The orchestrator's send.ps1 drops a request in the app's outbox and
  waits for its answer: SENT to <label>, or NOT SENT: <why>.
- An agent arriving in a tab is now news to the ledger (the e2e caught the
  list lagging).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
Security review of the sends: a tab's Claude mark rests on in-band
signals any program in the tab can print (or a stale one), so it was
enough to make the app type into a shell. Now, at the moment of typing,
agentUnder (core/main/agentPoll.ts) asks the process tree itself; no
Claude under that tab's shell, no send. The e2e proves both: a shell that
only claims to be Claude is refused, a stand-in claude.exe in the tree
gets the message.

A name now resolves to a tab's own ids first, then an exact label, a word
only when nothing else matched and only when it names one tab; -List
prints each tab's id and the skill sends by it.

The process-spawning unit tests get 60 s: 5.8 s on a busy PC failed the
5 s default in a full run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
Security review: the process-tree check takes about a second, in which
the switch, the tab or its state can change. The send is decided again,
synchronously, right before the paste (the same tab and session must
win), and once more before the Enter: a question that opened in between
would take it as its answer, so the text then waits unsent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
…re 0.43.0)

Owner, 2026-10-16: start dictation with the app not in focus, a widget
on screen while dictating, then the words go to the orchestrator when it
is ACTIVE, or to a session picked from the widget (a click or its
number); one dictation key, Ctrl+Win. Spec and plan:
docs/superpowers/specs/2026-10-16-dictate-anywhere.md.

Core: DEFAULT_HOTKEY Ctrl+Win; a chord of modifiers follows the solo
rules (either key first, Ctrl+Win+Left never dictates); a saved key is
kept; dictationOutside / setDictationRoute; DictationPage takes `after`.

App: a WH_KEYBOARD_LL helper (MEASURED: 0.3 to 1.2 ms, no Defender
detection; reads only the binding's keys, "another key", and digits/Esc
only while picking; masks Start after Ctrl+Win), the reducer fed from it
(globalDictKey), the widget window (unfocusable, on top, sized for 225%),
routing through the checked send path, "Dictate from any app" in
Settings. Never runs under --e2e: the suite feeds lines through a seam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
The default is Ctrl+Win now; these hold Right Alt to keep the AltGr-era
rules proven on a real key. dictation 21/21, dictationParakeet 11/11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
…alls back to the default

Owner, 2026-10-17: "very buggy and unresponsive, it says now it cant find
the mic ... the widget doesnt always show ... i have to press multiple
times". MEASURED in both diag logs: Stable and the installed app both ran
the keyboard hook, so every press went to both; the installed copy failed
in 8 ms ("No microphone found", a stale device id) and its widget covered
Stable's.

- The hook helper holds a lock file while it hooks: the first copy owns the
  key, a later one waits and takes over within 500 ms of the first exiting
  (a named Mutex hung the waiting helper on PowerShell's STA thread).
- A key down says when a Prism Terminal window is in front; another copy
  in front reads the key itself, so this one stays out.
- A saved mic that no longer matches records from the system default.
- Diag crumbs for the hook (ready/waiting/failed) and the key's actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
…nd again

MEASURED 2026-10-17: a closing copy started a fresh network service child
after the first force; it outlived its parent, the wait ran out and the
refresh changed nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
… copy for every copy

Owner, 2026-10-17: "the dictation in the app and outside seem like two
different things ... they should be the same thing"; "there should be one
dictation engine for both ... it should show all sessions open in one ui
from both instances"; "It needs to be ready at all times". Spec and plan
(approved): docs/superpowers/specs/2026-10-17-one-dictation.md.

- One key path: the hook reads the key with the app in front too; the page
  stops reading it whenever a service is in reach. One widget everywhere.
- The copy holding the hook's lock is the service; every other copy is a
  client on a local pipe (dictationLink): it answers with its tabs, and
  types or pastes into them through its own checked path.
- The words go to the copy whose window was in front at the press (pasted
  at its front tab's cursor, never Enter), else to the active orchestrator
  or the list, which holds every copy's Claude sessions, named by copy.
- The engine never stands down in Prism Terminal (core: idleMs), and only
  the service's copy warms one.
- Core (Prism unchanged): setDictationPageKeys, setDictationAutoWarm,
  keepDictationWarm, pasteIntoFront, registerDictationIpc idleMs.
- The mic opens on each press (owner's choice).

Tests: dictationLink (messages, a real pipe), the engine's Infinity idle,
the key with this copy in front, mergeTabs; e2e dictateTwoCopies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
Owner, 2026-10-17: thin blue horizontal lines over the skeleton while the
sessions load. They were the link decorations (termLinkPaint, xterm's
z-index 6) of the paths Claude prints as it resumes, in the same stacking
context as the skeleton (z-index 1). The terminal's region is now a
stacking context of its own (`isolate`). e2e launchSkeleton checks it,
and fails without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
Maxaubert and others added 3 commits October 17, 2026 05:30
Owner, 2026-10-17: "look into the performance part ... it's mainly startup
performance"; chose the resident Parakeet process. MEASURED on the owner's
RTX 5090: parakeet-cli took 0.72 to 0.86 s a pass whatever the clip's
length (nearly all model load and CUDA start); a resident host answers in
24 ms (1 s clip) to 170 ms (60 s clip), the text identical, ready 0.8 s
after spawn, paid once in the background after launch. Report:
research/prism-terminal/2026-10-17-dictation-performance.md.

- core/dictation-host/parakeetHost.ps1: a long-lived Windows PowerShell
  (the app's helper pattern, no new binary) calling the engine's own
  parakeet.dll; its compiled C# is cached by hash (0.86 -> 0.78 s start).
- The engine uses it when the host passes `parakeetHost`; one that never
  gets ready falls back to parakeet-cli for the session. stop(), the idle
  stand-down and a Whisper model end it. Prism passes none: unchanged.
- Shipped as resources/dictation-host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
…e key hook

Owner, 2026-10-17: merge the helpers; "it's mainly startup performance".
The DWM helper and the dictation key's hook each started their own Windows
PowerShell (about 65 MB each, MEASURED). Now one, src/main/hook/appHelper.ps1
(was globalKeyHook.ps1), started with the app: it answers `dwm` at once and
runs the hook on its own thread between `hook 1` and `hook 0`, letting the
lock go when it is turned off. Its compiled C# is cached by hash in the
profile: spawn to up 320 -> 220 ms; spawn() on main 3 to 4 ms (-File, never
-EncodedCommand, #180). A helper that dies is started and armed again.
Media pause stays its own, lazy process: it costs nothing at launch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
… key

Security review 2026-10-17: the pipe between copies had no authentication,
so a process that took its name first could pose as the service and have a
copy type into its Claude. Now the user's link key (32 random bytes in
PrismDictation\link.key under %LOCALAPPDATA%, made once with an exclusive
create) names the pipe by its hash, and each side answers the other's fresh
nonce with an HMAC before anything else is said; a peer that fails is
dropped, and a client answers, types and pastes for a proven service only.
Tests: a client and a service without the key are never trusted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
…ks first

Owner, 2026-10-17: "The dictation only opens inside of Prism Terminal, not
outside it." MEASURED in both installed 0.58.0 copies: no hook crumb at all,
and the key's lock free. The page says its dictation key before the window
is shown, and the helper's path was set only then, so `hook 1` went nowhere
and was never sent again. Lines sent before the helper is configured are now
kept and sent once it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA
@Maxaubert
Maxaubert changed the base branch from feat/catch-up-report to main October 17, 2026 09:22
@Maxaubert
Maxaubert merged commit 33fd023 into main Oct 17, 2026
2 checks passed
@Maxaubert
Maxaubert deleted the feat/dictate-anywhere branch October 17, 2026 09:25
github-actions Bot pushed a commit that referenced this pull request Oct 17, 2026
…re 0.43.0) (#246)

* feat: catch-up report: which session is in which tab, and what you have not seen (core 0.42.0)

Owner, 2026-10-16: a report of what the Claude sessions did while away and
what needs them next, based on whether they have SEEN each session, with a
tab left in front while away not counted as seen. Spec and plan:
docs/superpowers/specs/2026-10-16-catch-up-report.md (reviewed before
building; 25 findings folded in).

Core: the turn's own hooks (UserPromptSubmit, Stop) add session=<id>
(hook.cmd reads CLAUDE_CODE_SESSION_ID with delayed expansion, only a
UUID-shaped value); the parser carries it; setSessionsFile + an always
replaced PRISM_SESSIONS_FILE in ptyEnv; the plugin's catch-up skill and its
Windows PowerShell 5.1 script.

App: sessionLedger (pure): sessions kept apart from tabs; seen = in front
10 s while present (focused, shown, not locked or asleep, input within
120 s) or typed into, capped at last input + 10 s, restarted after any time
away. sessionsWire writes sessions.json, flushed with the tabs in close,
session-end and will-quit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* fix(catch-up): no tool allowlist in the skill

The digest quotes other sessions (and whatever web pages or files they
read), so a blanket powershell grant would let injected text run any
command without a prompt. The two script calls now ask like any other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* feat: the orchestrator is opt-in, with a promoted tab that knows its role

Owner, 2026-10-16: others who download the terminal should be able to
use it "if they want ... if they don't want it, it's like already in
there"; an orchestrator tab "promoted in the tab bar ... first in the list
and there's a divider line"; "right click and choose make orchestrator it
already knows".

- Settings > Agents > Orchestrator (beta), off by default. Off: no session
  list, no file, no PRISM_SESSIONS_FILE, no plugin, no menu entry.
- core/claude-plugin-orchestrator: a second Claude plugin given to new
  tabs only when it is on, with the catch-up and orchestrator skills and
  role.cmd, which adds a fixed additionalContext in the promoted tab only
  (MEASURED: a plugin hook's additionalContext reaches the model).
- Make orchestrator in a tab's menu: one tab, kept first, an accent
  outline and rule; saved in tabs.json; orchestrator-tab.txt names its
  PRISM_TAB_ID for the role hook.
- The script gains -List and -Only for the orchestrator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* feat: the orchestrator sends a message into any tab's Claude

Owner, 2026-10-16: "the terminal needs to be able to send it, and it needs
to be able to send it to any session, not just whichever is in focus.
That is very important." An owner exception to rule 2, recorded in
regression-rules.md and CLAUDE.md.

- orchestratorSend.ts (pure): only from the promoted tab, into the tab
  named by label, a word of its label or folder, its session or its
  PRISM_TAB_ID; never into a bare shell (it would run), never while that
  tab asks a question (it would answer it); one sanitised bracketed paste,
  then Enter after 300 ms (MEASURED on Claude Code 2.1.301: one prompt,
  multi-line kept).
- The orchestrator's send.ps1 drops a request in the app's outbox and
  waits for its answer: SENT to <label>, or NOT SENT: <why>.
- An agent arriving in a tab is now news to the ledger (the e2e caught the
  list lagging).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* fix(orchestrator): ask Windows before typing into a tab; send by tab id

Security review of the sends: a tab's Claude mark rests on in-band
signals any program in the tab can print (or a stale one), so it was
enough to make the app type into a shell. Now, at the moment of typing,
agentUnder (core/main/agentPoll.ts) asks the process tree itself; no
Claude under that tab's shell, no send. The e2e proves both: a shell that
only claims to be Claude is refused, a stand-in claude.exe in the tree
gets the message.

A name now resolves to a tab's own ids first, then an exact label, a word
only when nothing else matched and only when it names one tab; -List
prints each tab's id and the skill sends by it.

The process-spawning unit tests get 60 s: 5.8 s on a busy PC failed the
5 s default in a full run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* fix(orchestrator): decide again right before typing, and before Enter

Security review: the process-tree check takes about a second, in which
the switch, the tab or its state can change. The send is decided again,
synchronously, right before the paste (the same tab and session must
win), and once more before the Enter: a question that opened in between
would take it as its answer, so the text then waits unsent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* docs: spec and plan for dictating from any app

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* feat: dictate from any app, with a widget that sends to a session (core 0.43.0)

Owner, 2026-10-16: start dictation with the app not in focus, a widget
on screen while dictating, then the words go to the orchestrator when it
is ACTIVE, or to a session picked from the widget (a click or its
number); one dictation key, Ctrl+Win. Spec and plan:
docs/superpowers/specs/2026-10-16-dictate-anywhere.md.

Core: DEFAULT_HOTKEY Ctrl+Win; a chord of modifiers follows the solo
rules (either key first, Ctrl+Win+Left never dictates); a saved key is
kept; dictationOutside / setDictationRoute; DictationPage takes `after`.

App: a WH_KEYBOARD_LL helper (MEASURED: 0.3 to 1.2 ms, no Defender
detection; reads only the binding's keys, "another key", and digits/Esc
only while picking; masks Start after Ctrl+Win), the reducer fed from it
(globalDictKey), the widget window (unfocusable, on top, sized for 225%),
routing through the checked send path, "Dictate from any app" in
Settings. Never runs under --e2e: the suite feeds lines through a seam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* test(e2e): the Right Alt dictation scenarios set their key

The default is Ctrl+Win now; these hold Right Alt to keep the AltGr-era
rules proven on a real key. dictation 21/21, dictationParakeet 11/11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* fix(dictation): one copy of the app owns the global key; a gone mic falls back to the default

Owner, 2026-10-17: "very buggy and unresponsive, it says now it cant find
the mic ... the widget doesnt always show ... i have to press multiple
times". MEASURED in both diag logs: Stable and the installed app both ran
the keyboard hook, so every press went to both; the installed copy failed
in 8 ms ("No microphone found", a stale device id) and its widget covered
Stable's.

- The hook helper holds a lock file while it hooks: the first copy owns the
  key, a later one waits and takes over within 500 ms of the first exiting
  (a named Mutex hung the waiting helper on PowerShell's STA thread).
- A key down says when a Prism Terminal window is in front; another copy
  in front reads the key itself, so this one stays out.
- A saved mic that no longer matches records from the system default.
- Diag crumbs for the hook (ready/waiting/failed) and the key's actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* fix(tools): the stable refresh forces a leftover child closed again and again

MEASURED 2026-10-17: a closing copy started a fresh network service child
after the first force; it outlived its parent, the wait ran out and the
refresh changed nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* feat(dictation): one dictation for the app and outside it, run by one copy for every copy

Owner, 2026-10-17: "the dictation in the app and outside seem like two
different things ... they should be the same thing"; "there should be one
dictation engine for both ... it should show all sessions open in one ui
from both instances"; "It needs to be ready at all times". Spec and plan
(approved): docs/superpowers/specs/2026-10-17-one-dictation.md.

- One key path: the hook reads the key with the app in front too; the page
  stops reading it whenever a service is in reach. One widget everywhere.
- The copy holding the hook's lock is the service; every other copy is a
  client on a local pipe (dictationLink): it answers with its tabs, and
  types or pastes into them through its own checked path.
- The words go to the copy whose window was in front at the press (pasted
  at its front tab's cursor, never Enter), else to the active orchestrator
  or the list, which holds every copy's Claude sessions, named by copy.
- The engine never stands down in Prism Terminal (core: idleMs), and only
  the service's copy warms one.
- Core (Prism unchanged): setDictationPageKeys, setDictationAutoWarm,
  keepDictationWarm, pasteIntoFront, registerDictationIpc idleMs.
- The mic opens on each press (owner's choice).

Tests: dictationLink (messages, a real pipe), the engine's Infinity idle,
the key with this copy in front, mergeTabs; e2e dictateTwoCopies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* fix(terminal): nothing of the terminal paints over the resume skeleton

Owner, 2026-10-17: thin blue horizontal lines over the skeleton while the
sessions load. They were the link decorations (termLinkPaint, xterm's
z-index 6) of the paths Claude prints as it resumes, in the same stacking
context as the skeleton (z-index 1). The terminal's region is now a
stacking context of its own (`isolate`). e2e launchSkeleton checks it,
and fails without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* perf(dictation): Parakeet stays loaded in a resident host

Owner, 2026-10-17: "look into the performance part ... it's mainly startup
performance"; chose the resident Parakeet process. MEASURED on the owner's
RTX 5090: parakeet-cli took 0.72 to 0.86 s a pass whatever the clip's
length (nearly all model load and CUDA start); a resident host answers in
24 ms (1 s clip) to 170 ms (60 s clip), the text identical, ready 0.8 s
after spawn, paid once in the background after launch. Report:
research/prism-terminal/2026-10-17-dictation-performance.md.

- core/dictation-host/parakeetHost.ps1: a long-lived Windows PowerShell
  (the app's helper pattern, no new binary) calling the engine's own
  parakeet.dll; its compiled C# is cached by hash (0.86 -> 0.78 s start).
- The engine uses it when the host passes `parakeetHost`; one that never
  gets ready falls back to parakeet-cli for the session. stop(), the idle
  stand-down and a Whisper model end it. Prism passes none: unchanged.
- Shipped as resources/dictation-host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* perf(app): one helper process at launch for the DWM attributes and the key hook

Owner, 2026-10-17: merge the helpers; "it's mainly startup performance".
The DWM helper and the dictation key's hook each started their own Windows
PowerShell (about 65 MB each, MEASURED). Now one, src/main/hook/appHelper.ps1
(was globalKeyHook.ps1), started with the app: it answers `dwm` at once and
runs the hook on its own thread between `hook 1` and `hook 0`, letting the
lock go when it is turned off. Its compiled C# is cached by hash in the
profile: spawn to up 320 -> 220 ms; spawn() on main 3 to 4 ms (-File, never
-EncodedCommand, #180). A helper that dies is started and armed again.
Media pause stays its own, lazy process: it costs nothing at launch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* fix(dictation): both ends of the link prove they hold the user's link key

Security review 2026-10-17: the pipe between copies had no authentication,
so a process that took its name first could pose as the service and have a
copy type into its Claude. Now the user's link key (32 random bytes in
PrismDictation\link.key under %LOCALAPPDATA%, made once with an exclusive
create) names the pipe by its hash, and each side answers the other's fresh
nonce with an HMAC before anything else is said; a peer that fails is
dropped, and a client answers, types and pastes for a proven service only.
Tests: a client and a service without the key are never trusted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

* fix(dictation): the key from any app is armed even when the page speaks first

Owner, 2026-10-17: "The dictation only opens inside of Prism Terminal, not
outside it." MEASURED in both installed 0.58.0 copies: no hook crumb at all,
and the key's lock free. The page says its dictation key before the window
is shown, and the helper's path was set only then, so `hook 1` went nowhere
and was never sent again. Lines sent before the helper is configured are now
kept and sent once it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYVRPzvhLpPCr75uB6uaA

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant