# Nexus Phase 14 — Voice + ⌘K Globalization > Use `superpowers:test-driven-development`. Commit atomically. **Goal:** Lift the voice capture state out of `ChatInput`'s internal `VoiceMicButton` into a shared context so the top-strip `GlobalMicButton` becomes functional from any route, with all speech queued to the Assistant inbox per spec §5.5. Replace the `CmdKButton` shim (which dispatches synthetic Meta+K keydowns) with a real `CommandPaletteContext` that exposes an imperative open method, and expand the command palette's search index to cover conversations, projects, issues, agents, recipes, settings, and workshops per spec §10.1. **Source of truth:** spec §4.2 (GlobalMicButton states), §5.5 (voice routing from non-Assistant modes), §10.1 (⌘K palette), §10.3 (voice as global affordance), §10.4 (single notification surface). **Branch:** `nexus/design-system-migration`. --- ## Ownership boundaries **You may create or modify ONLY:** | Path | Action | |---|---| | `ui/src/context/VoiceContext.tsx` | Create — new context | | `ui/src/context/VoiceContext.test.tsx` | Create | | `ui/src/context/CommandPaletteContext.tsx` | Create — new context | | `ui/src/context/CommandPaletteContext.test.tsx` | Create | | `ui/src/components/frame/GlobalMicButton.tsx` | Modify — wire to VoiceContext | | `ui/src/components/frame/GlobalMicButton.test.tsx` | Modify — add functional tests | | `ui/src/components/frame/CmdKButton.tsx` | Modify — wire to CommandPaletteContext, remove synthetic keydown | | `ui/src/components/frame/CmdKButton.test.tsx` | Modify | | `ui/src/components/CommandPalette.tsx` | Modify — consume CommandPaletteContext, extend search index | | `ui/src/components/ChatInput.tsx` | Modify — lift voice state to VoiceContext | | `ui/src/pages/PersonalAssistant.tsx` | Modify — read voice queue from VoiceContext when navigating to assistant | | `ui/src/main.tsx` or equivalent | Modify — mount the new providers in the provider stack | | `ui/src/hooks/useKeyboardShortcuts.ts` | Modify — fix the pre-existing destructure bug (onSearch missing from destructure) AND wire onSearch to CommandPaletteContext | **You MUST NOT touch:** - Any other Phase 8–13 owned files - `ui/src/App.tsx` routes - Backend voice pipeline or STT/TTS endpoints (v1.6 already ships them — consume as-is) - `@/lib/router` --- ## Scope ### 1. VoiceContext **Responsibilities:** - Ownership of the current `MediaStream | null` for the microphone - Recording state: `idle` / `listening` / `speaking` (matching the GlobalMicButton states) - Transcription buffer (latest transcript from Whisper STT) - Queue: when voice is captured from a non-Assistant route, append to a pending queue; when the user navigates to `/assistant`, drain the queue into a new user message - Emits a `hasQueuedVoice` boolean that other components (e.g., the Assistant icon volt dot) can read **Consumers:** - `GlobalMicButton` — renders idle/listening/speaking per context state; tap cycles through record → stop → queue - `ChatInput` inside PersonalAssistant — subscribes to the VoiceContext stream instead of owning its own - `PersonalAssistant` — on mount, drains `VoiceContext.queue` **What lifts out of ChatInput:** Currently `VoiceMicButton` inside `ChatInput` owns: - `navigator.mediaDevices.getUserMedia` call - MediaStream state - MediaRecorder and audio chunk buffer - Whisper transcription invocation - Silence-detection / auto-send behavior All of this moves to VoiceContext. ChatInput's internal mic button becomes a thin consumer that reads `VoiceContext.state` and calls `VoiceContext.startListening()` / `stopListening()`. ### 2. CommandPaletteContext **Responsibilities:** - `open: boolean`, `setOpen(next: boolean)`, `toggle()` - Keyboard listener for Cmd+K / Ctrl+K registered once at the provider level (replaces the existing one inside `CommandPalette.tsx`'s `useEffect`) **Consumers:** - `CmdKButton` — calls `setOpen(true)` on click. No more synthetic keydown dispatch. - `CommandPalette` — reads `open` from context, `setOpen(false)` to close - `useKeyboardShortcuts.onSearch` — calls `setOpen(true)` instead of synthetic keydown **Extending the search index (spec §10.1):** - Conversations (by title, by recent message text snippet) - Projects (by name) - Issues (by title, by project) - Agents (by name, by role) - Recipes (by name, by tag — **stubbed if recipe API doesn't exist yet; that's a v1.8 feature**) - Settings (by section name — map the 8 Phase 13 section titles to `/instance/settings/general#
` anchors) - Studio workshops (by name — map the 9 WorkshopSlugs to `/content-studio/`) - Commands: `New project`, `New conversation`, `Re-run onboarding`, etc. Grouped with uppercase 1.4px-tracked category headers. Selected result has a 2px volt left border + pale-yellow text. Keyboard nav: arrow keys + enter + escape. ### 3. Fix `useKeyboardShortcuts.ts:12-17` destructure bug Phase 6/11 reviews flagged that `onSearch` is referenced at line 25 but never destructured at lines 12–17. Fix: add `onSearch` to the destructure. Once fixed, wire it to `commandPalette.setOpen(true)` through a context consumer (the shortcut hook itself may not consume contexts directly — escalate and resolve). ### 4. IconRail dot wiring — keep Phase 11's integration The volt dot on the Assistant icon (from Phase 11's `useGateIndicator`) already exists and works. Phase 14 optionally extends it to also light up when there's queued voice (`VoiceContext.hasQueuedVoice`). If you add this, make the aria-label more precise: "Assistant (pending gates and queued voice)" or split into two overlays. --- ## Implementation notes ### Provider stack order ``` <-- NEW (Phase 14) <-- NEW (Phase 14) ``` VoiceContext must be above CompanyContext because voice state is user-level, not company-level. CommandPaletteContext is above Router because the shortcut handler is global. ### CmdKButton shim removal Before: ```tsx const handleClick = () => { const event = new KeyboardEvent("keydown", { key: "k", metaKey: true, bubbles: true }); document.dispatchEvent(event); }; ``` After: ```tsx const { setOpen } = useCommandPalette(); const handleClick = () => setOpen(true); ``` Delete the shim comment block at the top of the file; replace with a note that Phase 14 landed the real wiring. ### GlobalMicButton real wiring Before (Phase 8 scaffold): ```tsx export function GlobalMicButton({ state = "idle", onClick }: GlobalMicButtonProps) { ``` After: ```tsx export function GlobalMicButton() { const { state, toggleListening } = useVoice(); // render idle/listening/speaking based on state } ``` Remove the `state` prop and `onClick` prop — they were Phase 8 scaffolding for when no real voice pipeline was available. Tests need updating. --- ## Acceptance criteria 1. Tapping the `GlobalMicButton` from ANY route starts listening and transitions through states 2. Speech captured on non-Assistant routes queues to `VoiceContext.queue` and surfaces as queued-voice indicator 3. Navigating to `/assistant` drains the queue into a new user message that streams through the existing chat pipeline 4. `Cmd+K` / `Ctrl+K` from anywhere opens the command palette via context, not synthetic keydown 5. Clicking the `CmdKButton` opens the palette via context 6. The palette searches across conversations, projects, issues, agents, settings sections, and workshops (recipes stubbed if no API) 7. `useKeyboardShortcuts.onSearch` is wired correctly (destructure bug fixed) 8. `ChatInput`'s internal voice button consumes `VoiceContext` instead of owning its own state 9. All new tests pass; all existing frame tests still pass 10. Typecheck clean on Phase 14 files --- ## Report format - Status - Commit SHAs - Files created / modified - Tests added / passing - Voice pipeline integration notes — where did the `getUserMedia` and MediaRecorder calls end up? What backend endpoints are consumed? - Command palette search extensions — which sources are live and which are stubbed? - IconRail dot extension — did you extend it for queued voice, or only gates? - Concerns, deviations, self-review