Three phase plans for Wave 2 of the Nexus layout overhaul. Each plan
is self-contained with ownership boundaries, scope fence, file inventory,
implementation notes, acceptance criteria, and commit scheme — designed
for parallel subagent dispatch per MIGRATION-PLAN.md section 11.
Phase 9 (Assistant mode):
- Full-bleed chat at /assistant, no inner conversation list
- History slide-over (left) + Memory slide-over (right)
- Conversational home-state greeting replaces Dashboard
- ActionStrip with Promote/Attach/Memory/History
- New components: AssistantInputBar, ActionStrip, HistorySheet,
MemorySheet, AssistantHomeGreeting + useAssistantHomeStatus hook
- Owns: PersonalAssistant.tsx + components/assistant/**
Phase 10 (Studio mode):
- 8-card workshop grid replaces 7-tab ContentStudio
- ConvertPage folds in as 8th workshop (legacy /convert route kept)
- StudioPromptBar freeform input with keyword-based classifier
- Two-column workshop detail view (params left, preview right)
- Owns: ContentStudio.tsx + pages/StudioWorkshopDetail.tsx
+ components/studio/**
Phase 11 (Projects + Builder mode):
- ProjectCard with 72px Inter Black volt hero percentage
- 7-tab BuilderTabStrip (Overview/Issues/Agents/Gates/Costs/Activity/Org)
- Approvals -> Gates display-only rename
- OverviewTab with milestone checklist, origin chat card, activity
- Thin per-project wrappers reuse existing list components scoped
by projectId (escalate if not supported)
- useGateIndicator hook for the future Assistant dot notification
- Owns: Projects.tsx + ProjectDetail.tsx + components/projects/**
Ownership boundaries prevent parallel-dispatch file conflicts:
- App.tsx routing changes are controller-owned (post-Wave wiring)
- Each phase declares its file ownership and must not cross into others
- Existing list components reused as-is; escalate if not scope-compatible
- IconRail dot wiring for phase 11's gate indicator deferred to
post-Wave controller step
Dispatch pattern: three general-purpose subagents dispatched in parallel,
each handed the full plan text + the spec + ownership rules. Each
subagent implements its phase end-to-end with atomic commits per
logical unit. Controller reviews outputs after all three complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
334 lines
19 KiB
Markdown
334 lines
19 KiB
Markdown
# 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
|