--- status: shipped created: 2026-08-12 owner: kandev --- # Voice Mode Leaves Core ## Why Voice Mode is an optional way to put text into a composer. It is not part of what kandev is for, and carrying it in core cost every install a 100 MB browser ML dependency, a per-user settings block in the shared user-settings row, an unauthenticated transcription route, and a server-side OpenAI key whether or not anyone dictated a word. The host prerequisites for moving it out shipped separately ([voice-extraction-host](voice-extraction-host.md), ADR [composer access and authenticated webhooks](../../decisions/2026-08-11-composer-access-authenticated-webhooks.md)). This spec records the extraction itself: what the plugin owns, what core stops owning, and what an existing user has to do. ## What - Voice Mode ships as [`kdlbs/kandev-plugin-voice`](https://github.com/kdlbs/kandev-plugin-voice), installed like any other plugin. Core contains no voice code. - The plugin reaches behaviour parity with the removed feature: the same three engines (browser speech, in-browser Whisper, server-side Whisper), insertion at the current selection with the same word-boundary spacing rule, optional auto-send, hold and toggle activation with the coarse-pointer override, cancellation on task/session change, the same `Cmd/Ctrl+Shift+M` shortcut, and the same four composers on desktop and phone: task chat, Quick Chat, task creation, new-session creation. - The plugin uses only published host contracts: the composer action slots, `host.storage` for per-user preferences, the owner-scoped `plugin-settings` slot for its settings form, `registerKeybinding` for the shortcut, the `/api/plugins/{id}/ui/*` asset route for its Whisper worker, and one authenticated webhook for the server relay. It adds no core-specific coupling. - Core keeps every generic host API the extraction needed. None of them mention voice. ### Removed from core | Removed | Replacement | |---|---| | `POST /api/v1/transcribe` (unauthenticated) | the plugin's `transcribe` webhook, which requires a signed-in user | | `voice.openAIApiKey` / `KANDEV_VOICE_OPENAI_API_KEY` | the plugin's own **OpenAI API key** setting | | `userSettings.voiceMode` (backend model, DTO, store, boot payload, frontend types) | the plugin's per-user `host.storage` entry | | The Voice Mode section of **Settings > Task Behavior** | **Settings > Plugins > Voice Mode** | | The `VOICE_INPUT_TOGGLE` core shortcut | the plugin keybinding, listed and rebindable in the same **Settings > Keyboard shortcuts** page | | `@huggingface/transformers` in `apps/web` | bundled inside the plugin's Whisper worker | ### Migration Both migration steps are manual and deliberate; neither is silent. - **The OpenAI key does not move.** An operator who set `KANDEV_VOICE_OPENAI_API_KEY` re-enters the key in the plugin's settings form. Core never had a way to write into a plugin's secret storage and should not gain one for a single plugin. - **Per-user preferences do not move.** Everyone's engine, language, model, activation mode and auto-send return to the plugin's defaults, which match the previous core defaults except that the engine list now hides server transcription until an operator saves a key. Copying them across would require core to write into one named plugin's per-user storage, which is exactly the coupling the extraction exists to remove. - A stale `voice_mode` object left inside an existing `users.settings` row is inert. Nothing reads it and nothing needs to delete it. ## Scenarios - **GIVEN** kandev with no plugins installed, **WHEN** a user opens task chat, **THEN** no microphone control appears and nothing in Settings mentions voice. - **GIVEN** the Voice plugin installed and enabled, **WHEN** a user dictates into task chat, Quick Chat, task creation or new-session creation on desktop or a phone, **THEN** the transcript is inserted at the caret and the native submit path owns sending it. - **GIVEN** no operator OpenAI key, **WHEN** a user opens the plugin's settings, **THEN** server transcription is shown as unavailable and the two browser engines still work. - **GIVEN** an operator key saved in the plugin, **WHEN** a signed-in user dictates with the server engine, **THEN** the audio reaches OpenAI through the plugin's authenticated webhook and the key never reaches the browser. - **GIVEN** an anonymous caller on an install with authentication enabled, **WHEN** it posts to the transcription webhook, **THEN** the request fails before the plugin runs and cannot spend the operator's budget. - **GIVEN** a user who had Voice Mode configured before upgrading, **WHEN** they install the plugin, **THEN** dictation works at the plugin's defaults and their previous engine and language choices are not carried over. ## Out Of Scope - Publishing the plugin to the official marketplace catalog. - Localizing the plugin. Kandev has no host i18n API for plugins yet; the plugin keeps its copy in one module so a catalog can be added once that API exists. - Migrating operator or per-user configuration automatically, for the reasons above.