nexus/docs/plans/2026-04-11-nexus-phase-9-assistant.md
Nexus Dev 3f0d3377e7 docs(nexus): wave 2 implementation plans (phases 9, 10, 11)
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>
2026-04-11 12:09:34 +00:00

19 KiB
Raw Blame History

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.3v1.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)

// 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 13 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:

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 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