# 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#<section>` anchors)
- Studio workshops (by name — map the 9 WorkshopSlugs to `/content-studio/<slug>`)
- 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

```
<QueryClientProvider>
  <ThemeProvider>
    <DialogProvider>
      <CompanyProvider>
        <VoiceProvider>              <-- NEW (Phase 14)
          <CommandPaletteProvider>   <-- NEW (Phase 14)
            <Router>
              <App />
```

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
