# Nexus Phase 9 — Assistant Mode Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:test-driven-development` for each unit. Commit atomically per logical unit (see §Commit scheme). **Goal:** Rebuild the Assistant experience at `/assistant` as the canonical home screen of Nexus — full-bleed chat with no inner conversation-list column, History + Memory slide-overs, a conversational "home greeting" state when no conversation is active, and an action strip that includes Promote-to-Project (the transition is Phase 12's job, not Phase 9's — Phase 9 just wires the button). **Source of truth:** `docs/specs/2026-04-11-nexus-layout-overhaul.md` **§5** (Mode 1 — Assistant). Read §5.1–§5.7 end-to-end before starting. DESIGN.md governs all visuals. Phase 8 components at `ui/src/components/frame/*` are the reference pattern for new UI pieces. **Branch:** `nexus/design-system-migration`. Commit directly; do not create a worktree. --- ## Ownership boundaries **You may create or modify ONLY the following paths:** | Path | Action | |---|---| | `ui/src/pages/PersonalAssistant.tsx` | Modify (major rewrite) | | `ui/src/components/assistant/**` | Create (new subdir for Phase 9 components) | | `ui/src/hooks/useAssistantHomeStatus.ts` | Create (new hook for home-state greeting data) | **You MUST NOT touch:** - `ui/src/App.tsx` — routing is controller-owned. If your rewrite needs a new route, STOP and report it in your final report; the controller will wire it up after Wave 2 completes. - `ui/src/components/ChatPanel.tsx` — the old global slide-in chat panel. Phase 8 killed it from the Layout chrome; Phase 16 deletes the file. Do not edit it in Phase 9. - `ui/src/components/MobileChatView.tsx` — Phase 9 is desktop only. Mobile parity is Phase 15. - `ui/src/context/ChatPanelContext.tsx` — legacy. Phase 9's Assistant route does not consume this context. - `ui/src/components/Layout.tsx` — already rewritten in Phase 8, do not touch. - Any file under `server/`, `packages/`, `cli/`. Phase 9 is UI-only; the backend chat streaming, voice pipeline, and memory APIs are already built (v1.3–v1.6). - Any file outside `ui/src/` except as noted in this plan. **Existing code you may reuse (read-only dependencies):** - `ui/src/components/ChatMessageList.tsx` — existing message thread renderer. Consume it as-is. - `ui/src/components/ChatInput.tsx` — existing text input with Enter/Shift-Enter handling. Consume it as-is inside the new AssistantInputBar wrapper. - `ui/src/components/VoiceWaveform.tsx` — existing visualizer. Lift it into the new layout. - `ui/src/components/ChatAgentSelector.tsx` — agent selector popover. Use it as-is in the conversation header. - `ui/src/hooks/useStreamingChat.ts` — SSE streaming hook. Reuse as-is. - `ui/src/api/chat.ts` (or whatever the chat API client is named) — do not modify. - `ui/src/components/frame/*` — Phase 8 patterns (createRoot test pattern, semantic tokens, focus-visible styles, cn helper). If you need a component that already exists in `ui/src/components/` but isn't listed above, read it first to understand its contract, then reuse it. Do not duplicate functionality. --- ## Scope (strictly) **In Phase 9:** 1. **Full-bleed chat surface** at `/assistant` — no inner conversation-list column, max-width 760px centered in canvas, messages alternate left/right. 2. **AssistantInputBar** — focal element at the bottom. Voice waveform visible above the text input, hairline divider between, generous 24px vertical padding, near-black `#141414` fill, charcoal border, sharp 8px radius. 3. **ActionStrip** — 4 actions below the input bar: `⊕ Promote to project` (volt outline, eligibility TBD but render button always for Phase 9 — Phase 12 wires the transition), `📎 Attach` (reuses existing upload flow), `🧠 Memory` (opens MemorySheet), `📁 History` (opens HistorySheet). 4. **HistorySheet** — left slide-over, 320px wide, butts against the icon rail. Lists conversations grouped by today/yesterday/this-week/older. Uses existing `ChatConversationList.tsx` as the inner content; wraps it in a slide-over container with close-on-outside-click and ESC-key-to-close. 5. **MemorySheet** — right slide-over, 340px wide. Displays what the assistant remembers about the user, with simple edit controls (reuses existing memory API if one exists; if not, render a read-only view with a "coming soon" placeholder for edit). Close-on-outside-click and ESC. 6. **AssistantHomeGreeting** — when there is no active conversation, render a synthesized greeting message at the top of the empty thread. Lists: active agents (count), pending gates (count), recent completions (list of up to 3), stale projects (list of up to 3). Data comes from a new `useAssistantHomeStatus` hook that reads existing endpoints (projects list, agents list, approvals/gates list, activity feed). Rendered as an assistant-turn bubble, NOT as a grid of widgets. 7. **PersonalAssistant.tsx rewrite** — rewire to compose the new pieces. Kill the 160px inner conversation-list column. Kill any hardcoded right-panel logic. Preserve existing streaming, voice I/O, agent switching, handoff-to-PM. **NOT in Phase 9 (explicitly deferred):** - The promote-to-project animated transition (Phase 12). Phase 9 renders the button; Phase 12 wires the 700ms compress-and-rise animation. - Voice routing from non-Assistant modes to the Assistant inbox (Phase 14). - The globalized ⌘K palette searching across conversations (Phase 14). - Mobile full-screen chat (Phase 15 — keep existing MobileChatView untouched for now; Phase 9 is desktop-only). - Any visual edge-polish beyond what the semantic tokens already produce. - Memory API changes or new persistence. If no memory API exists, render MemorySheet as read-only "coming soon" stub. - Editing the home-greeting data source or the underlying endpoints. Compose from what exists. - Deletion of `ChatPanel.tsx` / `ChatPanelContext.tsx` — Phase 16 cleanup. --- ## File plan ### Create | File | Responsibility | Est. lines | |---|---|---| | `ui/src/components/assistant/AssistantInputBar.tsx` | Voice waveform + text input composite with hairline divider. Wraps existing `ChatInput` and `VoiceWaveform`. | ~80 | | `ui/src/components/assistant/AssistantInputBar.test.tsx` | Tests: renders waveform + input, forwards onSend, shows correct state when streaming | ~100 | | `ui/src/components/assistant/ActionStrip.tsx` | 4-action row: Promote, Attach, Memory, History. Icon + label buttons, volt outline on Promote when eligible. | ~90 | | `ui/src/components/assistant/ActionStrip.test.tsx` | Tests: all 4 buttons render, onClick callbacks fire, Promote disabled when `canPromote={false}` | ~120 | | `ui/src/components/assistant/HistorySheet.tsx` | Left slide-over (320px), close-on-outside-click, ESC-to-close. Wraps existing `ChatConversationList`. | ~90 | | `ui/src/components/assistant/HistorySheet.test.tsx` | Tests: opens when `open`, closes on ESC, closes on backdrop click, renders conversation list inside | ~100 | | `ui/src/components/assistant/MemorySheet.tsx` | Right slide-over (340px). Read-only memory view with placeholder edit hook. | ~90 | | `ui/src/components/assistant/MemorySheet.test.tsx` | Tests: open/close, renders memory content, read-only mode disables edit | ~80 | | `ui/src/components/assistant/AssistantHomeGreeting.tsx` | Synthesized home-state message rendered as an assistant-turn bubble when no active conversation. | ~100 | | `ui/src/components/assistant/AssistantHomeGreeting.test.tsx` | Tests: renders greeting when no conversation, lists active agents/pending gates/etc., hidden when conversation active | ~110 | | `ui/src/hooks/useAssistantHomeStatus.ts` | Data hook: aggregates projects, agents, gates, activity from existing APIs. Returns `{ activeAgents, pendingGates, recentCompletions, staleProjects, loading }`. | ~80 | | `ui/src/hooks/useAssistantHomeStatus.test.ts` | Tests: aggregates correctly, handles loading state, handles errors | ~100 | ### Modify | File | Change | |---|---| | `ui/src/pages/PersonalAssistant.tsx` | Major rewrite: compose new pieces, remove inner conversation list column, remove any right-panel logic, preserve streaming + voice + handoff | **Do not create or modify any other files.** --- ## Implementation notes ### Layout skeleton (the target JSX) ```tsx // ui/src/pages/PersonalAssistant.tsx (simplified target structure) export function PersonalAssistant() { const { conversationId } = useParams(); const [historyOpen, setHistoryOpen] = useState(false); const [memoryOpen, setMemoryOpen] = useState(false); // ... existing chat state, streaming hook, voice state ... return (
{/* conversation thread: max-w-[760px] centered */}
{!activeConversation && } {activeConversation && }
{/* input bar + action strip anchored at bottom */}
{/* Phase 12 wires the transition */}} onAttach={...} onOpenMemory={() => setMemoryOpen(true)} onOpenHistory={() => setHistoryOpen(true)} />
{/* slide-overs */} setHistoryOpen(false)} /> setMemoryOpen(false)} />
); } ``` ### Design tokens to use (from Phase 1–3 migration and DESIGN.md) - `bg-background` for canvas (pure black) - `bg-card` for input-bar fill (`#141414` near-black) - `border-border` for charcoal borders - `text-primary` / `bg-primary` for volt accents - `text-muted-foreground` for silver - `rounded-[8px]` for input bar - `rounded-[4px]` for small buttons/badges - Inter weights via Tailwind defaults; no custom font classes needed - Uppercase labels with `tracking-[0.1em]` for small section titles (matches ModeBreadcrumb) - Focus-visible: `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background` on all interactive elements ### Slide-over pattern (reusable mental model for HistorySheet and MemorySheet) Use a simple portal-free pattern: ```tsx export function Sheet({ open, onClose, side, // "left" | "right" width, // e.g. 320 or 340 children, "aria-label": ariaLabel, }: SheetProps) { // Close on ESC useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, onClose]); if (!open) return null; return ( <> {/* backdrop */}