Skip to content

Managed voice in VS Code - #1000

Open
nedtwigg wants to merge 9 commits into
mainfrom
vscode-managed-voice
Open

nedtwigg wants to merge 9 commits into
mainfrom
vscode-managed-voice

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

A Hosted VS Code build now offers managed voice, as the standalone app does: Settings → Notifications enables it by its own Hosted approval, and alarms speak in the member's ElevenLabs voice. The Marketplace is the main install channel.

  • Storage: createManagedVoiceHost takes a settings store. Standalone keeps managed-voice.json; VS Code keeps the token and the chosen voice in SecretStorage, each under its own key.
  • Windows: every VS Code window runs its own voice host, with its own clip cache and "no plan" state. An approval runs in the window that asked; the token it saves, a voice choice, or a Disconnect reaches every other window, which re-reads SecretStorage on each change. A re-read waits behind any save in progress, so a stale save can't put a token back after Disconnect.
  • Failure cases across windows: a re-read never re-arms a token this window's failed Disconnect left in the store, and a 401 forgets only the token it refused, never one a sibling saved before this window re-read.
  • Network policy: the window running the Burrow service asks it before every speak and approval request. Other windows read the saved policy, and a failed read counts as Nothing.
  • Shared webview code: the webview half of the voice port moved into lib, so Tauri, the dev harness and VS Code use the same code. The extension tells the webview at startup whether the build is Hosted.
  • CSP: the VS Code webview now allows media-src blob:.
  • Specs: vscode.md has a new "Managed voice" section. alert.md (the 401 rule), transport.md, security-local, security-remote (self-host builds never get the voice flag) and remote-network are updated.

To check

  • Autoplay of an alarm nobody clicked, in VS Code's webview, is unverified. If it is refused, the alarm falls back to the system voice.
  • The chosen voice is in SecretStorage although it isn't secret. It is the only store whose changes every window hears.

Test plan

  • All package suites; typechecks for lib, standalone and vscode-ext; spec and md lints
  • A real alarm in VS Code with two windows open, against a Hosted preview

🤖 Generated with Claude Code

nedtwigg and others added 4 commits October 4, 2026 00:28
…tStorage

The host's store is now pluggable: the sidecar keeps managed-voice.json,
and VS Code keeps the token and the chosen voice under two SecretStorage
keys, whose onDidChange reaches every window, so a sign-in or sign-out
the broker's Burrow service writes invalidates each window's status,
clips, and refusal latch. Each extension host answers its own webviews'
voice:command and broadcasts voice:status; networkAllowed reads the
service in the broker and the stored policy elsewhere.

The webview port moves to lib (createManagedVoicePortClient), shared by
Tauri, the harness, and VS Code, whose adapter has it when the host's
boot global says the build is Hosted. The webview CSP gains media-src
blob: for the clip.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
vscode.md gains a Managed voice section (per-window host, SecretStorage
keys, cross-window re-read, the boot global) and media-src blob: in its
CSP; transport.md names VS Code's carriers and drops "standalone only";
security-local, security-remote, and remote-network follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
invalidate() compares the fresh read with what the window held: its own
write heard back changes nothing, and a voice choice keeps the clips and
the refusal latch, which only a token change forgets. The host leaves
credential undefined in a self-host build, so neither caller gates it;
the StoredConfig alias goes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
invalidate() re-reads on the serialize queue, so a save in flight cannot
land an older copy over a sibling's sign-out, and a window's own write
heard back finds nothing changed. vscode.md says a non-broker window
writes the token only to forget it on a 401, and that only a token change
drops the clips and latch. The VS Code command timeout derives from the
host's shared MANAGED_VOICE_REQUEST_TIMEOUT_MS.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1c14f42
Status: ✅  Deploy successful!
Preview URL: https://dfb459d8.mouseterm.pages.dev
Branch Preview URL: https://vscode-managed-voice.mouseterm.pages.dev

View logs

Brings main and the no-device-limit change down the stack. Main now reads a
build with no relay define as self-host, so the VS Code managed-voice tests
mock a Hosted build, and the webview test also checks a self-host build gets
no managed voice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A 401 in one window can delete a token another window has just saved. In request, the 401 branch's generation === tokenGeneration guard works when there is a single process, because setToken bumps the generation synchronously. Across VS Code windows, a sign-in reaches a sibling only after its secrets.onDidChange → invalidate runs. The race goes like this:

  1. The user removes the computer at the account page, which revokes T1 with its Burrow (relay-api.ts, "revoked with it").
  2. They sign in again from the broker window, which stores T2.
  3. Window B still has a speak with T1 in flight. It gets the 401 before its invalidate runs, so it sees the old generation and calls setToken(null).
  4. secretManagedVoiceStore.write then runs secrets.delete on the token key without checking what is there, which removes T2 in every window.

The sign-in is silently undone. The window is narrow and the user can recover by signing in again, but it is the reverse of the race the PR body says is closed ("a stale save can't put a token back after sign-out").

A compare-before-clear would close it without adding anything new. On the 401 path, inside serialize, drop loaded, re-read the store, and clear only if the stored token still equals the config.token that was sent. That turns the 401 into "forget this token" rather than "forget whatever is stored".

Base automatically changed from hosted-signin to main October 19, 2026 07:49
nedtwigg and others added 2 commits October 19, 2026 00:54
Main's #999 moved managed voice off the Burrow service: the voice host runs
its own approval and Disconnect. VS Code now runs that host in every window
over SecretStorage, so an approval or Disconnect in one window reaches the
rest; the Burrow service no longer hands a token to managed voice. A re-read
never re-arms a token this window's failed Disconnect left in the store.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Across VS Code windows, a sibling's approval reaches this window only after
its SecretStorage re-read. A speak still out with a revoked token could get
its 401 first and clear the sibling's new token for every window. The 401
path now re-reads the store and clears only when it still holds the token
that was refused.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extraction left a stale JSDoc block behind. /** A changed token forgets every clip and every refusal the old one earned. */ used to describe setToken. It now sits directly above clearToken's own doc comment, and the new setToken comment repeats it. Editors attach only the nearest block, so the line is dead text that misdescribes clearToken to anyone reading the source.

Comment thread lib/src/host/managed-voice-host.ts Outdated
@nedtwigg
nedtwigg marked this pull request as ready for review October 19, 2026 08:02
Comment thread vscode-ext/src/extension.ts Outdated

This branch is waiting to be deployed

1 waiting (outdated) deployment
hosted-preview — 07430fde Waiting Oct 4, 2026 by nedtwigg via deploy #1035
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.

2 participants