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>
19 KiB
Nexus Phase 9 — Assistant Mode Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:test-driven-developmentfor 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:
- Full-bleed chat surface at
/assistant— no inner conversation-list column, max-width 760px centered in canvas, messages alternate left/right. - AssistantInputBar — focal element at the bottom. Voice waveform visible above the text input, hairline divider between, generous 24px vertical padding, near-black
#141414fill, charcoal border, sharp 8px radius. - 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). - HistorySheet — left slide-over, 320px wide, butts against the icon rail. Lists conversations grouped by today/yesterday/this-week/older. Uses existing
ChatConversationList.tsxas the inner content; wraps it in a slide-over container with close-on-outside-click and ESC-key-to-close. - 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.
- 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
useAssistantHomeStatushook 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. - 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)
// 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-backgroundfor canvas (pure black)bg-cardfor input-bar fill (#141414near-black)border-borderfor charcoal borderstext-primary/bg-primaryfor volt accentstext-muted-foregroundfor silverrounded-[8px]for input barrounded-[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-backgroundon all interactive elements
Slide-over pattern (reusable mental model for HistorySheet and MemorySheet)
Use a simple portal-free pattern:
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
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
agentsApior similar — filter bystatus === "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 bystatus === "pending" - Recent completions: from existing
activityApi— last 24h, filter bytype === "completion"or similar - Stale projects: from existing
projectsApi— filter bylastActivity > 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:
- Loading
/NEX/assistantwith no active conversation renders the AssistantHomeGreeting inside a max-w-760px centered column, with no inner left conversation list. - Loading
/NEX/assistantwith an active conversation renders the existing message thread usingChatMessageList, with no inner left conversation list. - The AssistantInputBar shows the voice waveform above the text input with a hairline divider between them, anchored at the bottom of the canvas.
- The ActionStrip renders 4 buttons below the input bar: Promote (volt outline, disabled when no conversation), Attach, Memory, History.
- 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.
- Clicking Memory opens a right slide-over at 340px wide. ESC closes. Backdrop click closes.
- Streaming, voice I/O, agent switching, and handoff-to-PM from the existing PersonalAssistant still work — Phase 9 rewires composition but preserves functionality.
- All new components have tests using the Phase 8 pattern (manual createRoot + act,
// @vitest-environment jsdom,vi.mock("@/context/CompanyContext", ...)whereLinkis used). npx vitest run src/components/assistant/ src/hooks/useAssistantHomeStatus.test.tspasses.npx tsc --noEmit 2>&1 | grep -E "assistant/|PersonalAssistant\.tsx|useAssistantHomeStatus"returns no errors in Phase 9 files.- Typecheck on the full UI may still show pre-existing errors in other files — do not try to fix them.
- 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):
feat(nexus): add useAssistantHomeStatus hook (phase 9)— the hook + its testsfeat(nexus): add AssistantHomeGreeting component (phase 9)— greeting + testsfeat(nexus): add AssistantInputBar composite (phase 9)— input bar + testsfeat(nexus): add ActionStrip for assistant actions (phase 9)— action strip + testsfeat(nexus): add HistorySheet slide-over (phase 9)— history sheet + testsfeat(nexus): add MemorySheet slide-over (phase 9)— memory sheet + testsrefactor(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/ChatMarkdownMessageexist 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.tsoutput summary - Typecheck result: grep output for Phase 9 files
- Routing needs: any new routes the controller needs to add to
App.tsxafter 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