# 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 (
    <div className="relative flex h-full flex-col">
      {/* conversation thread: max-w-[760px] centered */}
      <div className="flex-1 overflow-auto">
        <div className="mx-auto w-full max-w-[760px] px-6 py-8">
          {!activeConversation && <AssistantHomeGreeting />}
          {activeConversation && <ChatMessageList ... />}
        </div>
      </div>

      {/* input bar + action strip anchored at bottom */}
      <div className="border-t border-border bg-background">
        <div className="mx-auto w-full max-w-[760px] px-6 py-4">
          <AssistantInputBar ... />
          <ActionStrip
            canPromote={canPromoteCurrentConversation}
            onPromote={() => {/* Phase 12 wires the transition */}}
            onAttach={...}
            onOpenMemory={() => setMemoryOpen(true)}
            onOpenHistory={() => setHistoryOpen(true)}
          />
        </div>
      </div>

      {/* slide-overs */}
      <HistorySheet open={historyOpen} onClose={() => setHistoryOpen(false)} />
      <MemorySheet open={memoryOpen} onClose={() => setMemoryOpen(false)} />
    </div>
  );
}
```

### 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 */}
      <button
        type="button"
        onClick={onClose}
        aria-label="Close"
        className="fixed inset-0 z-40 bg-black/40"
      />
      {/* panel */}
      <aside
        aria-label={ariaLabel}
        className={cn(
          "fixed top-12 bottom-0 z-50 bg-background border-border flex flex-col",
          side === "left" ? "left-[56px] border-r" : "right-0 border-l",
        )}
        style={{ width }}
      >
        {children}
      </aside>
    </>
  );
}
```

(`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) <noreply@anthropic.com>` 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
