# 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 (
);
}
```
### 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 */}
{/* panel */}
>
);
}
```
(`top-12` = 48px — below the TopStrip. `left-[56px]` on the left side = butts against the IconRail.)
You don't need to extract this into a shared `Sheet` primitive — inlining the same pattern twice in HistorySheet and MemorySheet is fine. But if you choose to extract, put it at `ui/src/components/assistant/Sheet.tsx` (still within your ownership).
### ActionStrip eligibility for Promote
For Phase 9, `canPromote` should be `true` when:
- There is an active conversation
- AND the conversation has at least one user message and one assistant message
Do not implement the actual brainstormer-eligibility check — that's Phase 12 territory. Phase 9 just needs a boolean that correctly enables/disables the button.
### AssistantHomeGreeting data shape
```ts
interface HomeStatus {
activeAgents: number; // count of agents currently working
pendingGates: Array<{ projectName: string; gateName: string; href: string }>;
recentCompletions: Array<{ projectName: string; summary: string; when: string }>;
staleProjects: Array<{ name: string; lastActivity: string; href: string }>;
loading: boolean;
}
```
The `useAssistantHomeStatus` hook reads:
- Active agents: from existing `agentsApi` or similar — filter by `status === "working"`
- Pending gates: from existing `approvalsApi` (still named "approvals" in the backend; the UI label is "Gates" but the API endpoint stays as `/api/approvals`) — filter by `status === "pending"`
- Recent completions: from existing `activityApi` — last 24h, filter by `type === "completion"` or similar
- Stale projects: from existing `projectsApi` — filter by `lastActivity > 3 days ago`
Do not create new API endpoints. Compose from what exists. If any of these endpoints are missing, gracefully degrade — return empty arrays and log a console.warn once in development.
### Rendering the home greeting
Not as a grid of widgets. Render as an assistant-turn chat bubble with formatted markdown content:
```
Good morning, Mikkel.
Since we last talked:
• nexus-design-migration: 4 commits, Phase 4 ready
• personal-finance-dashboard: idle 3 days
• 1 gate awaiting approval (nexus, Phase 4 audit)
What do you want to do?
```
Reuse existing markdown rendering components (`ChatMarkdownMessage.tsx` or similar) to match the rest of the chat visual language. The greeting should look indistinguishable from a real assistant turn.
---
## Acceptance criteria
Phase 9 is complete when:
1. Loading `/NEX/assistant` with no active conversation renders the AssistantHomeGreeting inside a max-w-760px centered column, with no inner left conversation list.
2. Loading `/NEX/assistant` with an active conversation renders the existing message thread using `ChatMessageList`, with no inner left conversation list.
3. The AssistantInputBar shows the voice waveform above the text input with a hairline divider between them, anchored at the bottom of the canvas.
4. The ActionStrip renders 4 buttons below the input bar: Promote (volt outline, disabled when no conversation), Attach, Memory, History.
5. Clicking History opens a left slide-over at 320px wide, butted against the 56px icon rail, showing the existing ChatConversationList inside. ESC closes. Backdrop click closes.
6. Clicking Memory opens a right slide-over at 340px wide. ESC closes. Backdrop click closes.
7. Streaming, voice I/O, agent switching, and handoff-to-PM from the existing PersonalAssistant still work — Phase 9 rewires composition but preserves functionality.
8. All new components have tests using the Phase 8 pattern (manual createRoot + act, `// @vitest-environment jsdom`, `vi.mock("@/context/CompanyContext", ...)` where `Link` is used).
9. `npx vitest run src/components/assistant/ src/hooks/useAssistantHomeStatus.test.ts` passes.
10. `npx tsc --noEmit 2>&1 | grep -E "assistant/|PersonalAssistant\.tsx|useAssistantHomeStatus"` returns no errors in Phase 9 files.
11. Typecheck on the full UI may still show pre-existing errors in other files — do not try to fix them.
12. No file outside the declared ownership is modified.
---
## Commit scheme
One atomic commit per logical unit. Each commit is independently reviewable. Prefix with `feat(nexus):` or `refactor(nexus):` per existing repo convention. Include `Co-Authored-By: Claude Opus 4.6 (1M context) ` on every commit.
Suggested commits (adjust as needed):
1. `feat(nexus): add useAssistantHomeStatus hook (phase 9)` — the hook + its tests
2. `feat(nexus): add AssistantHomeGreeting component (phase 9)` — greeting + tests
3. `feat(nexus): add AssistantInputBar composite (phase 9)` — input bar + tests
4. `feat(nexus): add ActionStrip for assistant actions (phase 9)` — action strip + tests
5. `feat(nexus): add HistorySheet slide-over (phase 9)` — history sheet + tests
6. `feat(nexus): add MemorySheet slide-over (phase 9)` — memory sheet + tests
7. `refactor(nexus): rewire PersonalAssistant to use new frame pieces (phase 9)` — the main page rewrite
If you find a cleaner grouping, use it. Each commit must build and pass its own tests.
---
## Before you begin
Ask the controller (your dispatching session) questions if any of the following are unclear:
- The existing memory API — does it exist, and what's its shape?
- The existing `activityApi` / `approvalsApi` / `projectsApi` — what endpoints are available for the home greeting?
- Whether `ChatMessageList` / `ChatInput` / `VoiceWaveform` / `ChatConversationList` / `ChatMarkdownMessage` exist and what their props are (read their files first; if any are missing or named differently, report it)
- Anything in §5 of the spec that reads as ambiguous after a careful re-read
If you find a genuine architectural obstacle (e.g., `useStreamingChat` is tangled with the old ChatPanel context in a way that makes full-bleed rendering hard), STOP and report BLOCKED with the specifics. Do not silently invent workarounds.
---
## When you're in over your head
It is always OK to stop and say "this is too hard for me." Bad work is worse than no work. Escalate early if:
- You can't find a reference API for the home greeting data
- The existing chat state machine doesn't compose cleanly with the new layout
- Any of the reused components (ChatInput, ChatMessageList, etc.) turn out to need changes you're not allowed to make
- You hit a TypeScript error in a Phase 9 file you can't resolve
- Phase 8's Layout.tsx behavior conflicts with how PersonalAssistant wants to render
Report BLOCKED with specific details about what's stuck, what you've tried, and what kind of help you need.
---
## Report format (final)
When Phase 9 is complete or blocked, report:
- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- **Commits produced:** list of SHAs with one-line summaries
- **Files created:** list
- **Files modified:** list (with line counts)
- **Tests added:** total count + per-file breakdown
- **Tests passing:** `npx vitest run src/components/assistant/ src/hooks/useAssistantHomeStatus.test.ts` output summary
- **Typecheck result:** grep output for Phase 9 files
- **Routing needs:** any new routes the controller needs to add to `App.tsx` after Wave 2 (describe the route pattern and the page component it maps to)
- **Open concerns:** anything the reviewer should look at specially
- **Deviations from the plan:** any places where you had to adapt because of codebase reality, with justification
- **Self-review findings:** what you found when re-reading your own code