nexus/docs/plans/2026-04-11-nexus-phase-14-voice-cmdk-globalization.md
Nexus Dev 87b45c730c docs(nexus): wave 3 plans (phases 12-16) + phase 11.5 backlog
Six plans drafted after Wave 2 completed:

Phase 11.5 — per-project scoping follow-up (BACKLOG)
  The 5 BLOCKED tabs from Phase 11 (Agents, Gates, Costs, Activity,
  Org) each need projectId scoping in the corresponding backend types
  and list components. Not time-critical — shipped as TabPlaceholder
  components in Phase 11 with honest data-gap badges. Each of the 5
  tickets is self-contained and can run independently when the
  placeholder UX hurts or when Wave 3 completes. Recommends dropping
  the Org tab entirely rather than building per-project scoping for it.

Phase 12 — Promote-to-project transition (WAVE 3)
  Replaces Phase 9's synchronous assistantHandoff() with the animated
  700ms compress-and-rise from spec 5.6. New files: PromoteTransition,
  BrainstormerPanel, usePromoteToProject state machine. CSS-first
  animation, not Motion library. Respects prefers-reduced-motion.
  Depends on Phase 9 (Assistant) and Phase 11 (Projects).

Phase 13 — Settings consolidation (WAVE 3)
  Collapses the nested instance-settings tree into a single-column
  scroll with 8 section cards (Workspace, LocalAI, Cloud, Skills,
  Routines, Telegram, About, Danger). Phase 13 is the one phase
  where routing changes ARE phase-owned: it strips nested
  /instance/settings sub-routes and replaces with Navigate redirects.

Phase 14 — Voice + CmdK globalization (WAVE 3)
  Lifts voice state out of ChatInput's internal VoiceMicButton into
  a shared VoiceContext so the top-strip GlobalMicButton becomes
  functional from any route, with speech queued to the Assistant
  inbox per spec 5.5. Replaces the CmdKButton synthetic-keydown shim
  with a real CommandPaletteContext. Extends CommandPalette search
  to cover conversations, projects, issues, agents, settings,
  workshops. Also fixes the pre-existing useKeyboardShortcuts.ts
  destructure bug caught during Phase 6/11 reviews.

Phase 15 — Mobile parity (WAVE 3)
  4-destination MobileTabBar replacing MobileBottomNav. History +
  Memory sheets become full-screen on mobile. Promote transition
  uses a full-screen takeover instead of split layout. BuilderTabStrip
  scrolls horizontally with edge fades. Single 768px breakpoint.

Phase 16 — Cleanup pass (WAVE 4, sequential)
  Deletes dead chrome files (ChatPanel, ChatPanelContext, Sidebar,
  InstanceSidebar, BreadcrumbBar, PropertiesPanel, MobileBottomNav).
  Migrates ChatMessageList off ChatPanelContext first (flagged
  hazard from Phase 9). Vocabulary sweep company to workspace in
  UI strings only (backend keeps company identifiers per upstream
  sync constraint). Fixes the minor issues accumulated across
  Phase 8-15 reviews (useKeyboardShortcuts destructure bug,
  GlobalMicButton test double-render, Projects umbrella regex
  duplication, PersonalAssistant brittle fixed height). Visual
  QA pass.

Wave structure for Wave 3 dispatch:
  Wave 3A: Phase 12, Phase 13, Phase 14, Phase 15 in parallel
           (4 subagents, disjoint file ownership)
  Wave 3B: Phase 16 sequential (intentionally touches everything)

Phase 11.5 stays in backlog — not in any wave. It activates when
the TabPlaceholder UX becomes a friction point or when a milestone
explicitly pulls one of its tickets into scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:11:12 +00:00

179 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 813 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 1217. 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