docs(nexus): wave 3 plans (phases 12-16) + phase 11.5 backlog

Six plans drafted after Wave 2 completed:

Phase 11.5 — per-project scoping follow-up (BACKLOG)
  The 5 BLOCKED tabs from Phase 11 (Agents, Gates, Costs, Activity,
  Org) each need projectId scoping in the corresponding backend types
  and list components. Not time-critical — shipped as TabPlaceholder
  components in Phase 11 with honest data-gap badges. Each of the 5
  tickets is self-contained and can run independently when the
  placeholder UX hurts or when Wave 3 completes. Recommends dropping
  the Org tab entirely rather than building per-project scoping for it.

Phase 12 — Promote-to-project transition (WAVE 3)
  Replaces Phase 9's synchronous assistantHandoff() with the animated
  700ms compress-and-rise from spec 5.6. New files: PromoteTransition,
  BrainstormerPanel, usePromoteToProject state machine. CSS-first
  animation, not Motion library. Respects prefers-reduced-motion.
  Depends on Phase 9 (Assistant) and Phase 11 (Projects).

Phase 13 — Settings consolidation (WAVE 3)
  Collapses the nested instance-settings tree into a single-column
  scroll with 8 section cards (Workspace, LocalAI, Cloud, Skills,
  Routines, Telegram, About, Danger). Phase 13 is the one phase
  where routing changes ARE phase-owned: it strips nested
  /instance/settings sub-routes and replaces with Navigate redirects.

Phase 14 — Voice + CmdK globalization (WAVE 3)
  Lifts voice state out of ChatInput's internal VoiceMicButton into
  a shared VoiceContext so the top-strip GlobalMicButton becomes
  functional from any route, with speech queued to the Assistant
  inbox per spec 5.5. Replaces the CmdKButton synthetic-keydown shim
  with a real CommandPaletteContext. Extends CommandPalette search
  to cover conversations, projects, issues, agents, settings,
  workshops. Also fixes the pre-existing useKeyboardShortcuts.ts
  destructure bug caught during Phase 6/11 reviews.

Phase 15 — Mobile parity (WAVE 3)
  4-destination MobileTabBar replacing MobileBottomNav. History +
  Memory sheets become full-screen on mobile. Promote transition
  uses a full-screen takeover instead of split layout. BuilderTabStrip
  scrolls horizontally with edge fades. Single 768px breakpoint.

Phase 16 — Cleanup pass (WAVE 4, sequential)
  Deletes dead chrome files (ChatPanel, ChatPanelContext, Sidebar,
  InstanceSidebar, BreadcrumbBar, PropertiesPanel, MobileBottomNav).
  Migrates ChatMessageList off ChatPanelContext first (flagged
  hazard from Phase 9). Vocabulary sweep company to workspace in
  UI strings only (backend keeps company identifiers per upstream
  sync constraint). Fixes the minor issues accumulated across
  Phase 8-15 reviews (useKeyboardShortcuts destructure bug,
  GlobalMicButton test double-render, Projects umbrella regex
  duplication, PersonalAssistant brittle fixed height). Visual
  QA pass.

Wave structure for Wave 3 dispatch:
  Wave 3A: Phase 12, Phase 13, Phase 14, Phase 15 in parallel
           (4 subagents, disjoint file ownership)
  Wave 3B: Phase 16 sequential (intentionally touches everything)

Phase 11.5 stays in backlog — not in any wave. It activates when
the TabPlaceholder UX becomes a friction point or when a milestone
explicitly pulls one of its tickets into scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nexus Dev 2026-04-11 13:11:12 +00:00
parent 428f033690
commit 87b45c730c
6 changed files with 1066 additions and 0 deletions

View file

@ -0,0 +1,184 @@
# Nexus Phase 11.5 — Per-Project Scoping Follow-Up
**Status:** Drafted 2026-04-11. Not yet started. Queued after Wave 3 completes, unless the `TabPlaceholder` UX pain surfaces earlier.
**Why this exists:** Phase 11's subagent discovered that 5 of the 6 new Project Detail "Builder" tabs have **no existing projectId-aware list component** to reuse, and the underlying type system doesn't carry project association on the relevant entities. Rather than halt Phase 11, the subagent shipped visible `TabPlaceholder` components that render a "PHASE 11 DATA GAP" badge with a precise description of what each tab needs. Those placeholders work as honest diagnostic UI but are not the final product.
**Source of truth:** `docs/plans/2026-04-11-nexus-phase-11-projects-builder.md` (original phase 11 plan), Phase 11's final subagent report, and `docs/specs/2026-04-11-nexus-layout-overhaul.md` §7.2 for the target UX.
---
## 1. The five gaps
Each gap is a **self-contained ticket** that can be worked independently. Ordering is flexible but the agents/gates/costs trio share some backend patterns and are best done together; activity and org are their own work.
### 1.1 `AGENTS` tab — per-project agent list
**Goal:** Render the agents assigned to or actively working on a specific project, in the `AgentsTab` component currently showing a `TabPlaceholder`.
**Current state:**
- `ui/src/pages/Agents.tsx` — 100+ lines of inline list + filter UI, not extracted as a reusable component
- `Agent` type in `@paperclipai/shared` has **no `projectId` field** and no reverse index to projects
- `agentsApi.list(companyId)` returns all agents for a company, no filter options
**Scope:**
1. **Define the agent ↔ project relationship.** Two options:
- (a) Add `projectId: string | null` to the `Agent` type + DB column, backfilled by looking at the agents' currently-assigned issues. Simplest, requires a migration.
- (b) Derive the relationship from assigned issues: an agent is "on project X" if it has any open issue in X. No schema change; frontend join; more expensive to query.
- (c) Add a `project_agents` join table for explicit assignment; requires migration and a separate assignment UI.
2. **Extract `AgentList` as a reusable component** from `Agents.tsx`, with a `projectId?: string | null` prop.
3. **Wire into `AgentsTab`** — replace the `TabPlaceholder`.
**Recommendation:** start with (b) — pure frontend derivation from the issue query already in memory (Phase 11.5 already fetches all issues for ProjectCard derivatives; piggyback on that cache). If the UX turns out to need explicit assignment semantics (e.g., "assign this agent to project X even though they have no issues yet"), escalate to (a) or (c) later.
**Files to touch:**
- `ui/src/components/agents/AgentList.tsx` (new — extract from Agents.tsx)
- `ui/src/components/projects/tabs/AgentsTab.tsx` (update — remove TabPlaceholder, render `<AgentList projectId={projectId} />`)
- `ui/src/pages/Agents.tsx` (small refactor — consume the new AgentList for the unscoped case)
**Acceptance:** AgentsTab renders the subset of company agents that have any open issue whose `projectId` matches the current project. Hover/click behavior matches the existing Agents page.
---
### 1.2 `GATES` tab — per-project pending approvals
**Goal:** Render the approvals (display: "gates") for a specific project in the `GatesTab`.
**Current state:**
- `Approval.payload` is `Record<string, unknown>`. Some approval types carry `payload.projectId` by convention; most don't.
- `ApprovalsList` or equivalent doesn't exist as a standalone component; `ui/src/pages/Approvals.tsx` is inline.
- `approvalsApi.list(companyId, status)` returns all approvals scoped by status, no project filter.
**Scope:**
1. **Audit approval types for `projectId` coverage.** Grep the server and UI for approval payload shapes; list which types carry `projectId` and which don't. Types that don't need either:
- (a) have a `projectId` added to their payload going forward (backward-compat default: `null`)
- (b) be filtered out of the per-project view and only surfaced in the global `/approvals` route
2. **Lift the frontend filter** already used by Projects.tsx (`buildProjectDerivatives` probes `payload.projectId`) into a shared `filterApprovalsByProject(approvals, projectId)` helper.
3. **Extract `ApprovalsList`** as a reusable component with a `projectId?: string | null` prop and a display-mode toggle for "Gates" vs "Approvals" copy.
4. **Wire into `GatesTab`** — replace TabPlaceholder, use "Gates" copy.
**Files to touch:**
- `ui/src/lib/approval-filters.ts` (new — shared filter helper)
- `ui/src/components/approvals/ApprovalsList.tsx` (new — extract from Approvals.tsx)
- `ui/src/components/projects/tabs/GatesTab.tsx` (update)
- `ui/src/pages/Approvals.tsx` (refactor — consume new component)
**Acceptance:** GatesTab renders pending+historical approvals whose `payload.projectId` matches. Copy uses "Gate" / "Gates" everywhere. Underlying API calls remain on `/api/approvals`.
---
### 1.3 `COSTS` tab — per-project cost burn
**Goal:** Render cost breakdown for a specific project in `CostsTab`.
**Current state:**
- `ui/src/pages/Costs.tsx` is ~1100 lines of inline content — aggregation, charting, filters, and layout all in one file
- No `CostsBreakdown` standalone component
- `costsApi` probably has per-company totals; per-project breakdown depends on the cost event records carrying `projectId` (likely yes because costs roll up to projects in the existing UI somewhere)
**Scope:**
1. **Read `Costs.tsx` and identify the breakdown section** (the chart/table that shows costs by project or by agent). If it exists, extract to `CostsBreakdown.tsx` with a `projectId?: string | null` prop.
2. **If the existing costs API doesn't support per-project filtering**, add a server-side filter to the endpoint (small backend change) or filter on the client (acceptable if cost events are bounded).
3. **Wire into `CostsTab`** — replace TabPlaceholder.
**Files to touch:**
- `ui/src/components/costs/CostsBreakdown.tsx` (new — extract)
- `ui/src/components/projects/tabs/CostsTab.tsx` (update)
- `ui/src/pages/Costs.tsx` (refactor)
- Possibly `server/src/routes/costs.ts` (add projectId filter param)
**Acceptance:** CostsTab renders cost events for the current project with an agent-by-agent breakdown. The global `/costs` page still works.
---
### 1.4 `ACTIVITY` tab — per-project activity feed
**Goal:** Render an activity feed scoped to a single project in `ActivityTab`.
**Current state:**
- `activityApi.list(entityType, entityId)` **already supports** filtering by entity — this is the easiest of the five gaps
- `ui/src/pages/Activity.tsx` renders the unscoped feed inline, no standalone component
**Scope:**
1. **Extract `ActivityFeed` as a reusable component** from `Activity.tsx` with a `{ entityType: string; entityId: string | null; companyId: string }` prop shape.
2. **Wire into `ActivityTab`** — call `<ActivityFeed entityType="project" entityId={projectId} companyId={companyId} />`.
**Files to touch:**
- `ui/src/components/activity/ActivityFeed.tsx` (new — extract)
- `ui/src/components/projects/tabs/ActivityTab.tsx` (update)
- `ui/src/pages/Activity.tsx` (refactor)
**Acceptance:** ActivityTab renders a filtered feed for the current project. Global `/activity` still works. The proxy in Wave 2.5's ProjectCard (newest issue updatedAt) can optionally be replaced with this feed's latest event.
**This is the lowest-effort ticket of the five.** Start here if you want a quick win.
---
### 1.5 `ORG` tab — per-project org chart
**Goal:** Render the subset of the org chart relevant to a specific project in `OrgTab`, hidden entirely for single-agent projects.
**Current state:**
- `agentsApi.org(companyId)` returns a company-wide tree
- `ui/src/pages/Org.tsx` (or similarly named) renders the full tree inline
- No way to query "which agents and what reporting structure are relevant to project X"
**Scope:**
1. **Define what "project-scoped org chart" means.** Options:
- (a) Show the subset of agents assigned to the project (from ticket 1.1) with their existing reporting relationships preserved.
- (b) Show the full company org with non-project agents visually dimmed.
- (c) Drop the tab entirely for Phase 11.5 — the ORG tab is already hidden for single-agent projects, and multi-agent projects might be better served by the global /org page than a per-project subview.
2. **If (a) or (b)**, extract `OrgChart` as a standalone component with a `highlightAgentIds?: string[]` or `agentIdFilter?: string[]` prop.
**Recommendation:** **(c) — drop the ORG tab entirely for Phase 11.5** unless you actively miss it. The single-agent auto-hide already reduces the surface; multi-agent projects are rare enough that the global org page suffices. If the tab is dropped, delete `OrgTab.tsx` and update `BuilderTabStrip.tsx` to unconditionally hide it.
**Files to touch (if not dropped):**
- `ui/src/components/agents/OrgChart.tsx` (new — extract)
- `ui/src/components/projects/tabs/OrgTab.tsx` (update)
- `ui/src/pages/Org.tsx` (refactor)
**Acceptance (if not dropped):** OrgTab renders a subtree of agents relevant to the project. Still hidden for single-agent projects.
---
## 2. Dependency order and parallelism
```
Ticket 1.4 (Activity) ← lowest effort, start here
Ticket 1.1 (Agents) ← unblocks Ticket 1.5 if we keep Org
Ticket 1.2 (Gates) ← independent
Ticket 1.3 (Costs) ← independent, backend work TBD
Ticket 1.5 (Org) ← drop it, or wait on Ticket 1.1
```
Tickets 1.2, 1.3, 1.4 are pure-frontend extractions and can run in parallel. Ticket 1.1 may involve a schema decision that blocks parallelization. Ticket 1.5 is suggested to drop.
## 3. Non-scope (things Phase 11.5 does NOT do)
- Adding real `milestoneProgress`, `phasesCompleted`, `phasesTotal` fields to the `Project` record. That's a Phase 11.6 backend phase if wanted, or accepted as permanent client-side proxy.
- Adding `originConversationId` to the `Project` record. Same disposition.
- Adding `costBurnedCents` aggregate to the `Project` record. Same.
- Adding explicit `project_agents` join table. That's (c) in Ticket 1.1 and deferred unless issue-derivation proves insufficient.
- Phase 14's ⌘K globalization (that's its own phase in Wave 3).
- Phase 16 cleanup of dead top-level routes like `/approvals`, `/agents`, `/activity`, `/org`, `/costs`. Those pages stay for backwards compat until Phase 16 decides their fate.
## 4. Triggering condition
This plan sits in the backlog until either:
1. **The `TabPlaceholder` UX hurts** — user hits one of the 5 blocked tabs frequently and the "data gap" badge becomes a friction point. At that moment, promote whichever tab is hurting most to active work.
2. **Wave 3 completes** and the team has bandwidth for backend-adjacent tickets.
3. **A v1.8+ milestone** explicitly pulls one of these scoping stories into scope (e.g., "project-level cost budgeting" would pull in Ticket 1.3).
None of these tickets is time-critical. The placeholders are honest and diagnosable — users can see "PHASE 11 DATA GAP: project-scoped costs not yet implemented" and navigate to the global `/costs` page if they need the data.
## 5. Execution
When ready to start, write a narrow implementation plan per ticket (one plan per ticket), using the same format as Phase 8's task-by-task TDD plan. Dispatch via subagent-driven-development for tickets 1.21.5 and in-session for ticket 1.1 (because it requires an architectural decision).
Do not dispatch all five tickets as one subagent — they touch disjoint subsystems and deserve independent review cycles.

View file

@ -0,0 +1,230 @@
# Nexus Phase 12 — Promote-to-Project Transition
> **For agentic workers:** Use `superpowers:test-driven-development` per component. Commit atomically per logical unit.
**Goal:** Replace the current synchronous `assistantHandoff()` call on the ActionStrip's "Promote to project" button with the signature animated transition from `docs/specs/2026-04-11-nexus-layout-overhaul.md` §5.6 — the 700ms compress-and-rise where the chat thread compresses to a 30% ribbon at the top while the brainstormer panel rises into the bottom 70%, with an inset shadow ripple along the boundary.
**Source of truth:** spec §5.6 (promote-to-project transition) and §5.7 (origin chat preservation). Phase 9 components under `ui/src/components/assistant/` are the consumption context.
**Branch:** `nexus/design-system-migration`. Commit directly.
---
## Ownership boundaries
**You may create or modify ONLY:**
| Path | Action |
|---|---|
| `ui/src/components/assistant/PromoteTransition.tsx` | Create — the animation-owning component |
| `ui/src/components/assistant/PromoteTransition.test.tsx` | Create |
| `ui/src/components/assistant/BrainstormerPanel.tsx` | Create — the rising brainstormer form (goal, acceptance, gates, agents) |
| `ui/src/components/assistant/BrainstormerPanel.test.tsx` | Create |
| `ui/src/components/assistant/ActionStrip.tsx` | Modify — replace synchronous handoff with a `promoteState` flip that opens the PromoteTransition |
| `ui/src/pages/PersonalAssistant.tsx` | Modify minimally — render the PromoteTransition overlay when active |
| `ui/src/hooks/usePromoteToProject.ts` | Create — state machine + create-project mutation |
| `ui/src/hooks/usePromoteToProject.test.ts` | Create |
**You MUST NOT touch:**
- Any other `ui/src/components/assistant/**` file besides the two listed above
- `ui/src/components/frame/**`, `ui/src/components/Layout.tsx`
- `ui/src/pages/Projects.tsx`, `ui/src/pages/ProjectDetail.tsx`, `ui/src/components/projects/**`
- `ui/src/pages/ContentStudio.tsx`, `ui/src/pages/StudioWorkshopDetail.tsx`, `ui/src/components/studio/**`
- `ui/src/App.tsx`
- Any backend code
---
## Scope (strictly)
**In Phase 12:**
1. **Replace the existing synchronous promote handler** on `ActionStrip`'s "Promote to project" button with a state-machine-backed handler that transitions:
- `idle` → (click) → `prompting` (the transition animation running, brainstormer rising)
- `prompting` → (user confirms) → `creating` (API call to create project)
- `creating``done` (project created, banner appears linking to new project, transition collapses, chat restores)
- Any error → `error` (transition collapses, error toast, state resets to `idle`)
2. **Animate the transition** per spec §5.6:
- 0200ms: chat thread height compresses from 100% to 30% via CSS transform/grid or max-height + cubic-bezier(0.22, 1, 0.36, 1). Inset shadow (DESIGN.md Level 4) fades in along the bottom edge of the ribbon.
- 200500ms: BrainstormerPanel slides up from the bottom into the lower 70%
- 500700ms: small `SOURCE CONVERSATION` uppercase 1.4px-tracked silver label fades in above the compressed thread
3. **BrainstormerPanel form** — spec §5.6:
- Goal (textarea)
- Acceptance criteria (textarea, one per line)
- Gates (list of checkboxes with default gate suggestions)
- Agents (PM auto-assigned; Engineer template picker)
- `[ Create project ]` volt-outline submit button at the bottom
- Cancel button (returns to `idle` state)
4. **On successful create:**
- Animate the collapse: brainstormer slides down, chat restores to full height (reverse of steps 2.12.2 but faster, ~300ms)
- A persistent banner at the top of the chat reads `→ Project: <PROJECT-NAME>` with a link to `/<companyPrefix>/projects/<slug>/overview`
- The user stays in Assistant — does NOT auto-navigate
5. **Implementation must be CSS-first, not a motion library.** Use CSS keyframes + transitions, not `motion/react`. Reasoning: one-off transition, no need for a new dependency. If you genuinely can't hand-roll the timing, report BLOCKED and the controller will reconsider adding Motion.
**NOT in Phase 12:**
- Server-side brainstormer changes. Reuse existing create-project endpoint.
- Changes to the project create flow semantics.
- Any changes to the Projects list or Project Detail pages — they just receive the new project normally.
- Mobile full-screen takeover for the transition — Phase 15 handles mobile variant.
- Reduced-motion fallback more elaborate than `prefers-reduced-motion: no animation`.
---
## File plan
| File | Responsibility | Est. lines |
|---|---|---|
| `ui/src/hooks/usePromoteToProject.ts` | State machine: `{state, startPrompting, confirm, cancel, error}`. `confirm(form)` calls `projectsApi.create` and transitions through states. | ~120 |
| `ui/src/hooks/usePromoteToProject.test.ts` | Tests each state transition, mocks `projectsApi.create` | ~140 |
| `ui/src/components/assistant/BrainstormerPanel.tsx` | The form that appears in the bottom 70% during `prompting`. Controlled form, `onConfirm(payload)` → calls hook. | ~180 |
| `ui/src/components/assistant/BrainstormerPanel.test.tsx` | Tests: renders fields, validation, submit payload | ~160 |
| `ui/src/components/assistant/PromoteTransition.tsx` | The animation container. Props: `state`, `children` (the chat thread ribbon), `panelChildren` (BrainstormerPanel). CSS-driven. | ~150 |
| `ui/src/components/assistant/PromoteTransition.test.tsx` | Tests: renders in idle/prompting/creating/done states, aria-live polite for status | ~140 |
Modifications:
- `ui/src/components/assistant/ActionStrip.tsx`: swap sync `onPromote` for `usePromoteToProject().startPrompting`, pass the hook's state down
- `ui/src/pages/PersonalAssistant.tsx`: mount `<PromoteTransition>` as an overlay that wraps the chat thread when `promoteState !== "idle"`, render `<BrainstormerPanel>` inside it
---
## Implementation notes
### State machine shape
```ts
type PromoteState =
| { kind: "idle" }
| { kind: "prompting"; conversationId: string }
| { kind: "creating"; conversationId: string; payload: BrainstormerPayload }
| { kind: "done"; projectSlug: string; projectName: string }
| { kind: "error"; message: string };
interface BrainstormerPayload {
goal: string;
acceptanceCriteria: string[];
gates: string[];
engineerTemplateId?: string;
}
```
### Animation — target keyframes
```css
/* Spec §5.6 — 700ms total. Prefers-reduced-motion: instant swap. */
.promote-chat-ribbon {
/* idle → prompting: collapses from 100% to 30% over 200ms */
transition: max-height 200ms cubic-bezier(0.22, 1, 0.36, 1);
max-height: 100%;
overflow: hidden;
}
.promote-chat-ribbon[data-state="prompting"],
.promote-chat-ribbon[data-state="creating"] {
max-height: 30vh;
box-shadow: inset 0 -16px 24px -12px rgba(0, 0, 0, 0.55);
}
.promote-panel {
/* 200ms delay, 300ms rise */
transition: transform 300ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 300ms ease-out;
transform: translateY(100%);
opacity: 0;
}
.promote-panel[data-state="prompting"],
.promote-panel[data-state="creating"] {
transform: translateY(0);
opacity: 1;
transition-delay: 200ms;
}
.source-conversation-label {
/* 500ms delay, 200ms fade */
transition: opacity 200ms ease-out;
opacity: 0;
}
.source-conversation-label[data-state="prompting"],
.source-conversation-label[data-state="creating"] {
opacity: 1;
transition-delay: 500ms;
}
@media (prefers-reduced-motion: reduce) {
.promote-chat-ribbon,
.promote-panel,
.source-conversation-label {
transition: none !important;
}
}
```
Inline these via Tailwind arbitrary values where possible. If keyframes need to live in a global stylesheet, add them to `ui/src/index.css` under a clearly-scoped block — but prefer inline Tailwind.
### Integration with Phase 9's ActionStrip
Phase 9 wired `onPromote` to call `assistantHandoff()` synchronously. Phase 12 replaces that:
```tsx
// ActionStrip.tsx — change
const promote = usePromoteToProject(activeConversationId);
// ...
<button onClick={promote.startPrompting} disabled={!canPromote || promote.state.kind !== "idle"}>
⊕ Promote to project
</button>
```
PersonalAssistant.tsx renders the transition overlay:
```tsx
{promote.state.kind !== "idle" && promote.state.kind !== "done" && (
<PromoteTransition state={promote.state.kind}>
<BrainstormerPanel
conversationId={promote.state.conversationId}
onConfirm={promote.confirm}
onCancel={promote.cancel}
isCreating={promote.state.kind === "creating"}
/>
</PromoteTransition>
)}
{promote.state.kind === "done" && (
<div className="border-l-2 border-primary bg-card p-3 mb-4">
→ Project: <Link to={`.../${promote.state.projectSlug}/overview`}>{promote.state.projectName}</Link>
</div>
)}
```
### Accessibility
- `aria-live="polite"` on the transition container so screen readers announce state changes
- The BrainstormerPanel form fields have labels
- ESC while in `prompting` or `creating` triggers `promote.cancel()`
- Focus traps inside the BrainstormerPanel while active
---
## Acceptance criteria
1. Clicking `⊕ Promote to project` on an active Assistant conversation triggers the 700ms animation per spec §5.6
2. The chat thread compresses to 30%, brainstormer rises into 70%, source-conversation label fades in — in that order
3. The BrainstormerPanel accepts Goal / Acceptance Criteria / Gates / Engineer Template, validates the Goal field as non-empty, submits via the existing project-create endpoint
4. On success: transition collapses, chat restores, a `→ Project: <NAME>` banner appears at the top of the chat, user stays in Assistant
5. On error: transition collapses, error toast shown, chat restores, promote button becomes clickable again
6. ESC cancels the prompting state and restores chat
7. `prefers-reduced-motion: reduce` disables the transition — brainstormer snaps in and out instead of animating
8. Tests pass: `npx vitest run src/components/assistant/PromoteTransition.test.tsx src/components/assistant/BrainstormerPanel.test.tsx src/hooks/usePromoteToProject.test.ts`
9. Typecheck clean on Phase 12 files
10. No file outside declared ownership modified
---
## Report format
- Status
- Commit SHAs
- Files created / modified
- Tests added / passing
- Typecheck result
- Animation approach (CSS-only vs keyframes-file)
- Concerns, deviations, self-review findings

View file

@ -0,0 +1,181 @@
# Nexus Phase 13 — Settings Consolidation
> Use `superpowers:test-driven-development`. Commit atomically per section.
**Goal:** Collapse the current Paperclip nested instance-settings tree (`/instance/settings/general`, `/instance/settings/integrations`, …) into a **single-column scroll page** at `/instance/settings/general` (the URL stays so existing bookmarks still work) with ~8 section cards per `docs/specs/2026-04-11-nexus-layout-overhaul.md` §8. Move the Skill Aggregator, Routines, Telegram bridge, and re-run onboarding link into section cards. Drop the nested sub-routes from App.tsx (they redirect to the single page).
**Source of truth:** spec §8 (Mode 4 — Settings). Phase 8 established the frame; the Settings destination in the IconRail already points at `/instance/settings/general`.
**Branch:** `nexus/design-system-migration`.
---
## Ownership boundaries
**You may create or modify ONLY:**
| Path | Action |
|---|---|
| `ui/src/pages/InstanceSettings.tsx` (or equivalent single page) | Major rewrite into single-column scroll |
| `ui/src/pages/instance/settings/**` or sub-pages | Modify — kill the child sub-pages, fold into sections |
| `ui/src/components/settings/**` | Create (new subdir for section cards) |
| `ui/src/App.tsx` | Routes may be removed (nested settings sub-routes) — see note |
**Exception on App.tsx:** Phase 13 is the one phase where routing changes ARE owned by the phase, because the instance-settings tree is being collapsed. You may remove nested `/instance/settings/integrations`, `/instance/settings/adapters`, etc. routes and replace them with `<Navigate to="/instance/settings/general" replace />` redirects so existing bookmarks still work. Do not touch any other App.tsx route.
**You MUST NOT touch:**
- `ui/src/pages/PersonalAssistant.tsx`, `ui/src/components/assistant/**` (Phase 9/12)
- `ui/src/pages/Projects.tsx`, `ui/src/pages/ProjectDetail.tsx`, `ui/src/components/projects/**` (Phase 11)
- `ui/src/pages/ContentStudio.tsx`, `ui/src/pages/StudioWorkshopDetail.tsx`, `ui/src/components/studio/**` (Phase 10)
- `ui/src/components/Layout.tsx`, `ui/src/components/frame/**` (Phase 8)
- Any backend code
---
## Scope (strictly)
### Section cards per spec §8.1
1. **WORKSPACE** — root directory, theme toggle (dark/light), re-run onboarding link
2. **LOCAL AI** — Hermes provider, Whisper STT model, Piper TTS voice
3. **CLOUD PROVIDERS** — Anthropic / OpenAI API keys (masked), Puter.js status
4. **SKILLS** — Skill Aggregator browse/install/assign (currently wherever it lives; Phase 13 folds it in)
5. **ROUTINES** — list of workspace routines with edit/pause (currently the top-level `/routines` route — folded in; that route stays for backwards compat)
6. **TELEGRAM BRIDGE** — bot token, allowed chat IDs
7. **ABOUT** — Nexus version, fork info, MIT license
8. **DANGER ZONE** — reset workspace, delete all conversations
Each card: 1px charcoal border, 8px radius, 24px padding, transparent fill. Uppercase 1.4px-tracked title in silver + hairline rule. Sections listed vertically with 24px gap.
### What to preserve from the existing nested pages
- All existing form state, field validation, save handlers
- The API endpoints each section uses (likely `instanceSettingsApi`, `skillsApi`, `routinesApi`, etc.)
- Access control checks (though single-user self-hosted, some checks may still exist)
### What to kill from the chrome
- The old left-rail `InstanceSidebar` that showed nested settings sub-links — already unmounted by Phase 8 but the component file may linger; delete it in Phase 13 along with its test file
- Any `/instance/settings/*` sub-routes other than `/general`
---
## File plan
| File | Action |
|---|---|
| `ui/src/pages/InstanceSettings.tsx` (or the existing main settings page file) | Rewrite into single-column scroll that renders 8 section cards |
| `ui/src/components/settings/WorkspaceSection.tsx` | Create |
| `ui/src/components/settings/LocalAISection.tsx` | Create |
| `ui/src/components/settings/CloudProvidersSection.tsx` | Create |
| `ui/src/components/settings/SkillsSection.tsx` | Create (embeds Skill Aggregator) |
| `ui/src/components/settings/RoutinesSection.tsx` | Create (reuses routines list components) |
| `ui/src/components/settings/TelegramSection.tsx` | Create |
| `ui/src/components/settings/AboutSection.tsx` | Create |
| `ui/src/components/settings/DangerZoneSection.tsx` | Create |
| Each section + `.test.tsx` for tests | Create tests |
| `ui/src/components/InstanceSidebar.tsx` | Delete (was a Phase 8 legacy candidate) |
| `ui/src/App.tsx` | Strip nested `/instance/settings/*` sub-routes except `/general`; add redirect stubs |
---
## Implementation notes
### Single-page layout
```tsx
export function InstanceSettings() {
return (
<div className="mx-auto w-full max-w-[960px] px-6 py-8 space-y-6">
<h1 className="text-[14px] font-semibold uppercase tracking-[0.1em] text-primary">
Settings
</h1>
<WorkspaceSection />
<LocalAISection />
<CloudProvidersSection />
<SkillsSection />
<RoutinesSection />
<TelegramSection />
<AboutSection />
<DangerZoneSection />
</div>
);
}
```
### Section card skeleton
Every section uses the same shell:
```tsx
export function SettingsSection({ title, children }: { title: string; children: ReactNode }) {
return (
<section
aria-label={title}
className="rounded-[8px] border border-border bg-transparent p-6"
>
<header className="mb-4">
<h2 className="text-[12px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
{title}
</h2>
<hr className="mt-2 border-border" />
</header>
<div className="space-y-4">{children}</div>
</section>
);
}
```
Consider extracting `SettingsSection` as a shared primitive in `components/settings/SettingsSection.tsx` and importing it from all 8 section files — that's 9 files total but keeps each section shell 1-line and eliminates drift.
### Routines fold-in strategy
The existing `/routines` top-level route and its page stay. The Routines section in Settings either:
- (a) renders a compact read-only list of routines with an "open full routines page" link that navigates to `/routines`, OR
- (b) renders the full interactive routines UI inline
**Recommendation (a)** — single responsibility per page. Phase 16 decides whether to delete `/routines`.
### Skills fold-in
Same pattern: the Skills section embeds the Skill Aggregator UI inline (browse + installed count + assignments) OR links out to a Skill Aggregator page if one exists. Read the codebase to find where Skills currently live. If there's an existing route, link to it from the section; if it's unrouted, embed the component.
---
## Acceptance criteria
1. `/instance/settings/general` renders a single-column scroll page with 8 section cards
2. Any previous `/instance/settings/<sub>` URL (e.g., `/instance/settings/integrations`) redirects to `/instance/settings/general`
3. The theme toggle in the WORKSPACE section switches between light/dark and persists
4. API key fields mask their values and never log them
5. All new components have tests using the Phase 8 pattern
6. `npx vitest run src/components/settings/` passes
7. `npx tsc --noEmit 2>&1 | grep -E "(settings/|InstanceSettings\.tsx)"` clean
8. `InstanceSidebar.tsx` deleted (and any other dead settings chrome files)
9. No file outside declared ownership touched
10. Existing settings functionality preserved — nothing breaks
---
## Before you begin
Read:
- `ui/src/pages/InstanceSettings.tsx` (or whatever the main settings page is named)
- Any nested settings sub-pages (grep for `InstanceSettings*.tsx` or look under `ui/src/pages/instance/settings/`)
- `ui/src/api/instanceSettings.ts` for the API shape
- The current routines list component and skills aggregator UI locations
---
## Report format
- Status
- Commit SHAs
- Files created / modified / deleted
- Tests added / passing
- Typecheck result
- Nested sub-routes removed and their redirect targets
- Skill Aggregator fold-in strategy (inline vs link-out)
- Routines section strategy (compact list vs full inline)
- Concerns, deviations, self-review

View file

@ -0,0 +1,179 @@
# Nexus Phase 14 — Voice + ⌘K Globalization
> Use `superpowers:test-driven-development`. Commit atomically.
**Goal:** Lift the voice capture state out of `ChatInput`'s internal `VoiceMicButton` into a shared context so the top-strip `GlobalMicButton` becomes functional from any route, with all speech queued to the Assistant inbox per spec §5.5. Replace the `CmdKButton` shim (which dispatches synthetic Meta+K keydowns) with a real `CommandPaletteContext` that exposes an imperative open method, and expand the command palette's search index to cover conversations, projects, issues, agents, recipes, settings, and workshops per spec §10.1.
**Source of truth:** spec §4.2 (GlobalMicButton states), §5.5 (voice routing from non-Assistant modes), §10.1 (⌘K palette), §10.3 (voice as global affordance), §10.4 (single notification surface).
**Branch:** `nexus/design-system-migration`.
---
## Ownership boundaries
**You may create or modify ONLY:**
| Path | Action |
|---|---|
| `ui/src/context/VoiceContext.tsx` | Create — new context |
| `ui/src/context/VoiceContext.test.tsx` | Create |
| `ui/src/context/CommandPaletteContext.tsx` | Create — new context |
| `ui/src/context/CommandPaletteContext.test.tsx` | Create |
| `ui/src/components/frame/GlobalMicButton.tsx` | Modify — wire to VoiceContext |
| `ui/src/components/frame/GlobalMicButton.test.tsx` | Modify — add functional tests |
| `ui/src/components/frame/CmdKButton.tsx` | Modify — wire to CommandPaletteContext, remove synthetic keydown |
| `ui/src/components/frame/CmdKButton.test.tsx` | Modify |
| `ui/src/components/CommandPalette.tsx` | Modify — consume CommandPaletteContext, extend search index |
| `ui/src/components/ChatInput.tsx` | Modify — lift voice state to VoiceContext |
| `ui/src/pages/PersonalAssistant.tsx` | Modify — read voice queue from VoiceContext when navigating to assistant |
| `ui/src/main.tsx` or equivalent | Modify — mount the new providers in the provider stack |
| `ui/src/hooks/useKeyboardShortcuts.ts` | Modify — fix the pre-existing destructure bug (onSearch missing from destructure) AND wire onSearch to CommandPaletteContext |
**You MUST NOT touch:**
- Any other Phase 813 owned files
- `ui/src/App.tsx` routes
- Backend voice pipeline or STT/TTS endpoints (v1.6 already ships them — consume as-is)
- `@/lib/router`
---
## Scope
### 1. VoiceContext
**Responsibilities:**
- Ownership of the current `MediaStream | null` for the microphone
- Recording state: `idle` / `listening` / `speaking` (matching the GlobalMicButton states)
- Transcription buffer (latest transcript from Whisper STT)
- Queue: when voice is captured from a non-Assistant route, append to a pending queue; when the user navigates to `/assistant`, drain the queue into a new user message
- Emits a `hasQueuedVoice` boolean that other components (e.g., the Assistant icon volt dot) can read
**Consumers:**
- `GlobalMicButton` — renders idle/listening/speaking per context state; tap cycles through record → stop → queue
- `ChatInput` inside PersonalAssistant — subscribes to the VoiceContext stream instead of owning its own
- `PersonalAssistant` — on mount, drains `VoiceContext.queue`
**What lifts out of ChatInput:**
Currently `VoiceMicButton` inside `ChatInput` owns:
- `navigator.mediaDevices.getUserMedia` call
- MediaStream state
- MediaRecorder and audio chunk buffer
- Whisper transcription invocation
- Silence-detection / auto-send behavior
All of this moves to VoiceContext. ChatInput's internal mic button becomes a thin consumer that reads `VoiceContext.state` and calls `VoiceContext.startListening()` / `stopListening()`.
### 2. CommandPaletteContext
**Responsibilities:**
- `open: boolean`, `setOpen(next: boolean)`, `toggle()`
- Keyboard listener for Cmd+K / Ctrl+K registered once at the provider level (replaces the existing one inside `CommandPalette.tsx`'s `useEffect`)
**Consumers:**
- `CmdKButton` — calls `setOpen(true)` on click. No more synthetic keydown dispatch.
- `CommandPalette` — reads `open` from context, `setOpen(false)` to close
- `useKeyboardShortcuts.onSearch` — calls `setOpen(true)` instead of synthetic keydown
**Extending the search index (spec §10.1):**
- Conversations (by title, by recent message text snippet)
- Projects (by name)
- Issues (by title, by project)
- Agents (by name, by role)
- Recipes (by name, by tag — **stubbed if recipe API doesn't exist yet; that's a v1.8 feature**)
- Settings (by section name — map the 8 Phase 13 section titles to `/instance/settings/general#<section>` anchors)
- Studio workshops (by name — map the 9 WorkshopSlugs to `/content-studio/<slug>`)
- Commands: `New project`, `New conversation`, `Re-run onboarding`, etc.
Grouped with uppercase 1.4px-tracked category headers. Selected result has a 2px volt left border + pale-yellow text. Keyboard nav: arrow keys + enter + escape.
### 3. Fix `useKeyboardShortcuts.ts:12-17` destructure bug
Phase 6/11 reviews flagged that `onSearch` is referenced at line 25 but never destructured at lines 1217. Fix: add `onSearch` to the destructure. Once fixed, wire it to `commandPalette.setOpen(true)` through a context consumer (the shortcut hook itself may not consume contexts directly — escalate and resolve).
### 4. IconRail dot wiring — keep Phase 11's integration
The volt dot on the Assistant icon (from Phase 11's `useGateIndicator`) already exists and works. Phase 14 optionally extends it to also light up when there's queued voice (`VoiceContext.hasQueuedVoice`). If you add this, make the aria-label more precise: "Assistant (pending gates and queued voice)" or split into two overlays.
---
## Implementation notes
### Provider stack order
```
<QueryClientProvider>
<ThemeProvider>
<DialogProvider>
<CompanyProvider>
<VoiceProvider> <-- NEW (Phase 14)
<CommandPaletteProvider> <-- NEW (Phase 14)
<Router>
<App />
```
VoiceContext must be above CompanyContext because voice state is user-level, not company-level. CommandPaletteContext is above Router because the shortcut handler is global.
### CmdKButton shim removal
Before:
```tsx
const handleClick = () => {
const event = new KeyboardEvent("keydown", { key: "k", metaKey: true, bubbles: true });
document.dispatchEvent(event);
};
```
After:
```tsx
const { setOpen } = useCommandPalette();
const handleClick = () => setOpen(true);
```
Delete the shim comment block at the top of the file; replace with a note that Phase 14 landed the real wiring.
### GlobalMicButton real wiring
Before (Phase 8 scaffold):
```tsx
export function GlobalMicButton({ state = "idle", onClick }: GlobalMicButtonProps) {
```
After:
```tsx
export function GlobalMicButton() {
const { state, toggleListening } = useVoice();
// render idle/listening/speaking based on state
}
```
Remove the `state` prop and `onClick` prop — they were Phase 8 scaffolding for when no real voice pipeline was available. Tests need updating.
---
## Acceptance criteria
1. Tapping the `GlobalMicButton` from ANY route starts listening and transitions through states
2. Speech captured on non-Assistant routes queues to `VoiceContext.queue` and surfaces as queued-voice indicator
3. Navigating to `/assistant` drains the queue into a new user message that streams through the existing chat pipeline
4. `Cmd+K` / `Ctrl+K` from anywhere opens the command palette via context, not synthetic keydown
5. Clicking the `CmdKButton` opens the palette via context
6. The palette searches across conversations, projects, issues, agents, settings sections, and workshops (recipes stubbed if no API)
7. `useKeyboardShortcuts.onSearch` is wired correctly (destructure bug fixed)
8. `ChatInput`'s internal voice button consumes `VoiceContext` instead of owning its own state
9. All new tests pass; all existing frame tests still pass
10. Typecheck clean on Phase 14 files
---
## Report format
- Status
- Commit SHAs
- Files created / modified
- Tests added / passing
- Voice pipeline integration notes — where did the `getUserMedia` and MediaRecorder calls end up? What backend endpoints are consumed?
- Command palette search extensions — which sources are live and which are stubbed?
- IconRail dot extension — did you extend it for queued voice, or only gates?
- Concerns, deviations, self-review

View file

@ -0,0 +1,131 @@
# Nexus Phase 15 — Mobile Parity
> Use `superpowers:test-driven-development`. Commit atomically.
**Goal:** Bring the new frame to mobile breakpoint `< 768px`. Replace the legacy `MobileBottomNav` with a 4-destination bottom tab bar matching the desktop IconRail destinations (Assistant, Studio, Projects, Settings). Collapse the desktop slide-overs (History, Memory) to full-screen sheets on mobile. Make the project tab strip scroll horizontally without shrinking. Adapt the promote-to-project transition to a full-screen takeover on mobile instead of the split layout.
**Source of truth:** spec §9 (Mobile).
**Branch:** `nexus/design-system-migration`.
---
## Ownership boundaries
**You may create or modify ONLY:**
| Path | Action |
|---|---|
| `ui/src/components/frame/MobileTabBar.tsx` | Create |
| `ui/src/components/frame/MobileTabBar.test.tsx` | Create |
| `ui/src/components/Layout.tsx` | Modify — swap `MobileBottomNav` for `MobileTabBar` |
| `ui/src/components/assistant/HistorySheet.tsx` | Modify — add mobile full-screen variant |
| `ui/src/components/assistant/MemorySheet.tsx` | Modify — add mobile full-screen variant |
| `ui/src/components/assistant/PromoteTransition.tsx` | Modify — mobile full-screen takeover variant |
| `ui/src/components/projects/BuilderTabStrip.tsx` | Modify — horizontal scroll on mobile |
| `ui/src/components/frame/TopStrip.tsx` | Modify if needed — mobile tweaks to cluster spacing |
**You MUST NOT touch:**
- Phase 8 icon rail semantics (desktop behavior stays)
- Phase 9 Assistant thread layout (desktop stays)
- Phase 10 Studio grid (mobile already collapses to 1-col)
- Phase 11 Project card layout (desktop stays)
- `ui/src/App.tsx`
---
## Scope
### 1. MobileTabBar component
Replacement for `MobileBottomNav`. Renders 4 tab links at the bottom of the viewport:
- Assistant (MessageCircle)
- Studio (Sparkles)
- Projects (FolderKanban)
- Settings (Settings)
Specs:
- `fixed bottom-0 left-0 right-0 h-14` (56px tall)
- `border-t border-border bg-background` — charcoal top border, pure black fill
- Each tab is a flex column: icon on top (20×20), label below (10px uppercase 1.4px tracking)
- Silver default, volt active with a 2px volt bar above the icon
- Focus-visible styles per DESIGN.md
- Safe-area aware: `pb-[env(safe-area-inset-bottom)]`
- Active state derivation: same regex patterns as IconRail's `isActive` functions — factor out if possible
Delete the existing `MobileBottomNav.tsx` after confirming the new bar covers its behavior. If `MobileBottomNav` has non-destination features (e.g., a "new issue" FAB), report them and the controller will decide whether to keep or drop.
### 2. Mobile slide-over → full-screen sheet
Phase 9's `HistorySheet` and `MemorySheet` are desktop slide-overs at 320px / 340px. On mobile:
- Take the full viewport width
- Full viewport height below the top strip (48px)
- Close button in the top-right of the sheet
- Swipe-down-to-close gesture (optional — acceptable if CSS-only)
Implementation: add a `useMediaQuery("(min-width: 768px)")` check; when mobile, render different class names that go full-screen. Do NOT duplicate the component — same file, conditional rendering.
### 3. Promote-to-project mobile takeover
Phase 12's `PromoteTransition` split-screen layout (30% chat ribbon / 70% brainstormer) doesn't fit on mobile. On mobile, the brainstormer **completely covers the chat** — slides up to 100% viewport height instead of 70%.
Implementation: same `PromoteTransition.tsx` file, conditional max-height and translate values based on `useMediaQuery`. Tests need a mobile case.
### 4. BuilderTabStrip horizontal scroll
Phase 11's Project tab strip has 7 tabs that don't fit on narrow mobile. Spec §9.1 says "Project sub-tabs become a horizontally-scrolling strip under the header, no shrinking."
Implementation: on mobile, wrap the tab list in a `flex overflow-x-auto` container with `scroll-snap-type: x mandatory` for tab-sized snapping. Add edge fades (`mask-image` or left/right gradient overlays) so users see there's more content. Tests need a mobile case.
### 5. TopStrip mobile polish
On mobile, the top strip should:
- Keep the 48px height
- Drop the `⌘K` text label and show only the kbd glyph (already minimal — verify)
- Keep the mic button
- Keep the mode breadcrumb but allow it to truncate if too long
- Consider whether to hide the breadcrumb entirely on very narrow screens
---
## Implementation notes
### `useMediaQuery("(min-width: 768px)")` already exists
`ui/src/hooks/useMediaQuery.ts` (or similar) is used throughout the codebase. Consume it rather than adding a new media-query hook.
### Mobile breakpoint is single — 768px
Per spec §9.2: "Single breakpoint: `>= 768px` is desktop frame, `< 768px` is mobile frame." Don't introduce intermediate breakpoints; everything is binary.
### Tailwind responsive classes vs runtime media query
For simple show/hide behavior, use Tailwind's `hidden md:block` / `md:hidden`. For layout variants that need JS logic (like the promote transition's translate values), use `useMediaQuery`. Prefer CSS when possible.
---
## Acceptance criteria
1. On a viewport `< 768px`, the new MobileTabBar renders with 4 destinations matching the IconRail; clicking each navigates correctly
2. The 56px desktop IconRail is hidden on mobile (already `hidden md:flex` from Phase 8 — verify)
3. HistorySheet and MemorySheet render full-screen on mobile
4. The promote-to-project transition completely covers the chat on mobile
5. BuilderTabStrip scrolls horizontally on mobile with no tabs cropped
6. TopStrip remains 48px and functional on mobile
7. Safe-area insets (notch, home indicator) respected
8. `MobileBottomNav` file deleted or clearly documented as dead
9. All existing frame + assistant + projects tests pass
10. Mobile-specific tests added
---
## Report format
- Status
- Commit SHAs
- Files created / modified / deleted
- Mobile tests added
- Features carried over from MobileBottomNav (if any)
- Gesture support (swipe-down-to-close etc.)
- Concerns, deviations, self-review

View file

@ -0,0 +1,161 @@
# Nexus Phase 16 — Cleanup Pass
> **Sequential, not parallel.** This phase touches everything. Use `superpowers:test-driven-development` only for any non-trivial logic change; most of Phase 16 is deletion and find-replace.
**Goal:** Delete the dead chrome code that Phase 8 unmounted, sweep the UI copy for "company" → "workspace" (or deletion), fix accumulated minor issues surfaced by review cycles across Phases 815, and perform a final visual QA pass.
**Source of truth:** spec §2 "Killed list" and the list of known minor issues logged across Phase 815 reviews.
**Branch:** `nexus/design-system-migration`. This is the LAST phase of the structural overhaul.
---
## Ownership boundaries
**Phase 16 is intentionally broad.** You may touch any file in `ui/src/` that contains dead chrome code, legacy vocabulary, or accumulated cruft. However:
- Do NOT touch `server/`, `packages/`, or `cli/` — the backend vocabulary stays per PROJECT.md's upstream-sync constraint
- Do NOT touch any test file unless its assertions reference dead code you're deleting
- Do NOT introduce new features — this is strictly subtraction
- Do NOT touch the `.planning/` directory
If in doubt whether a file is dead, run `grep -r "<SymbolName>" ui/src/` — if there are zero remaining consumers, it's dead. If there's any consumer, escalate before deleting.
---
## Scope
### 1. Delete dead chrome files (spec §2 "Killed list")
Files Phase 8 unmounted but didn't delete. Verify each has zero consumers, then delete:
- `ui/src/components/ChatPanel.tsx` — the old 380px slide-in chat panel
- **HAZARD:** `ChatMessageList` is hard-bound to `ChatPanelContext` (Phase 9 note). Before deleting `ChatPanelContext.tsx`, migrate `ChatMessageList` off it. See "ChatMessageList migration" below.
- `ui/src/context/ChatPanelContext.tsx` — only after `ChatMessageList` migration
- `ui/src/components/PropertiesPanel.tsx` — unless a page-level usage still exists (grep first)
- `ui/src/components/Sidebar.tsx` — the 280px left sidebar
- `ui/src/components/InstanceSidebar.tsx` — already slated by Phase 13; confirm deleted
- `ui/src/components/BreadcrumbBar.tsx` — replaced by ModeBreadcrumb
- `ui/src/components/MobileBottomNav.tsx` — replaced by MobileTabBar (Phase 15)
For each deletion, also delete the corresponding `.test.tsx` / `.stories.tsx` file and search the codebase for any remaining imports. If any import survives, fix it in the same commit.
### 2. ChatMessageList migration
Phase 9 flagged: `ChatMessageList.tsx` reads `activeConversationId` / `scrollToMessageId` from `ChatPanelContext`. Phase 16 must migrate these reads OFF the legacy context before deleting it.
**Migration strategy:** introduce `AssistantChatContext` in `ui/src/context/AssistantChatContext.tsx` that owns `activeConversationId` and `scrollToMessageId`. Update `ChatMessageList` to consume it. Update `PersonalAssistant` and any other consumers to provide the new context. Delete `ChatPanelContext` only AFTER all consumers moved.
Alternative: if the state is better owned by URL params (`/:conversationId` + a query param for scroll target), refactor that way. Cleaner but more invasive.
### 3. Vocabulary sweep — "company" → "workspace"
Per spec §2: "Any text containing 'company', 'companies', 'workspace member', 'tenant' → vocabulary cleanup."
Scope: **display-only strings** in `ui/src/`. Do NOT rename code symbols. The UI says "workspace" while the backend keeps `company` / `companyId` identifiers forever (upstream sync constraint from PROJECT.md).
- Find all UI strings with "company" / "Company" / "companies" / "Companies" / "Tenant" / "tenant"
- Replace with "workspace" / "Workspace" / "workspaces" / "Workspaces"
- Exception: if `@paperclipai/branding` already has a `VOCAB.company` token that resolves to "Workspace", just use that — don't hand-type the replacement
- Exception: error messages from the backend that include "company" — leave alone, they come from the server
### 4. Minor issues accumulated across Phase 815 reviews
These were flagged during reviews but deferred to Phase 16. Fix each in a clearly-labelled commit:
1. **`useKeyboardShortcuts.ts:12-17` destructure bug** — the type declares `onSearch?: () => void` but the destructure omits it, so the call at line 25 always resolves to undefined. If Phase 14 hasn't already fixed this, fix it here.
2. **`GlobalMicButton.test.tsx` double-render warning** — the test's `render(state)` helper is called 3 times in one test without unmounting between. Cosmetic but noisy. Refactor to unmount between renders or split into 3 separate tests.
3. **`IconRail.tsx` Projects umbrella regex repetition** — 10 regexes with `||`. Extract to a constant list + `.some()` helper. Coordinate with Phase 11's note about duplicated-umbrella-list-drift.
4. **`PersonalAssistant.tsx` message-thread fixed height** `calc(100vh - 320px)` — brittle. Investigate replacing with a flex layout that derives height from flex parent, now that the surrounding chrome is stable.
5. **`Projects.tsx` phase / costBurnedCents / activeAgentsCount gaps** — still rendering as null. Phase 16 doesn't fix the data gaps (that's Phase 11.5), but it removes the `// TODO` comments that were added by Wave 2.5 IF Phase 11.5 has already landed.
6. **Spec drift from Phase 10 commit 5 missing body** — optional: amend the commit or just leave it. Low priority.
7. **Phase 10 `PresentationPanel` fold-in** — Wave 2.5 already handled this. If Phase 16 finds `PresentationPanel` is dead code for any reason (e.g. spec changed), delete it.
### 5. Top-level route deletion decisions
Phase 11 demoted several routes to per-project tabs but kept them at the top level for backwards compat:
- `/issues` → now a per-project tab. Delete?
- `/agents` → now a per-project tab. Delete?
- `/approvals` → now a per-project `/gates` tab. Delete?
- `/costs` → now a per-project tab. Delete?
- `/activity` → now a per-project tab. Delete?
- `/inbox` → replaced by Assistant dot + ⌘K. Delete?
- `/goals` → folded into Overview milestone checklist. Delete?
- `/routines` → moved to Settings in Phase 13. Delete?
- `/org` → now a per-project tab. Delete?
- `/dashboard` → replaced by Assistant home greeting. Delete?
- `/convert` → folded into Studio. Delete?
**Decision rule:** delete IF the corresponding Phase 11 / 13 / 15 replacement fully covers the use case AND there are no external bookmarks / docs pointing at the route. If in doubt, keep and flag for user decision.
**Safer default:** keep all of them for now as backwards-compat redirects. If Phase 16 finds clear dead code (component exists but its route is already a `<Navigate>` to a new home), delete. If the page still renders real UI, keep.
### 6. Visual QA pass
Walk every top-level route in the dev server, compare against DESIGN.md principles and the layout spec. Capture issues as a short list and either fix inline or file follow-up tickets. No code changes unless issues found.
Routes to walk:
- `/assistant` (with and without active conversation)
- `/content-studio` + each workshop
- `/projects` (populated and empty states)
- `/projects/<slug>/overview` + each tab
- `/instance/settings/general`
- `/convert` (legacy)
- `/approvals` (legacy)
---
## Sequential commits (suggested)
This phase is naturally sequential. Each commit is reviewable independently.
1. `refactor(nexus): migrate ChatMessageList off ChatPanelContext (phase 16)`
2. `refactor(nexus): delete legacy chrome files (phase 16)` — Sidebar, InstanceSidebar, ChatPanel, ChatPanelContext, PropertiesPanel, BreadcrumbBar, MobileBottomNav
3. `refactor(nexus): vocabulary sweep company -> workspace (phase 16)`
4. `fix(nexus): useKeyboardShortcuts destructure bug (phase 16)`
5. `refactor(nexus): fix GlobalMicButton test double-render warning (phase 16)`
6. `refactor(nexus): extract shared PROJECTS_UMBRELLA constant (phase 16)`
7. `refactor(nexus): PersonalAssistant message-thread flex layout (phase 16)`
8. `refactor(nexus): delete dead top-level routes (phase 16)` — only routes whose components are pure redirects
9. `docs(nexus): visual QA findings (phase 16)` — any issues found during QA, either fixed inline or filed
Group commits together only when they touch the same file for the same reason.
---
## Acceptance criteria
1. All files listed in §1 are deleted or clearly documented as intentionally kept
2. `ChatMessageList` no longer consumes `ChatPanelContext`
3. Zero UI strings say "company" or "Company" (except via `VOCAB.company` token)
4. Known minor issues from §4 are fixed or tickets filed
5. Full test suite passes: `npx vitest run src/components/frame/ src/components/assistant/ src/components/studio/ src/components/projects/ src/components/settings/ src/hooks/`
6. Typecheck clean on all Wave 13 files
7. Visual QA pass findings logged (either fixed inline or filed)
8. The commit chain on `nexus/design-system-migration` is complete and ready for merge / PR review
---
## Non-scope
- Backend vocabulary changes (`company` identifiers stay — upstream sync)
- Phase 11.5 per-project-scoping work (separate plan)
- Phase 47 visual migration polish (can run before or after Phase 16, independent)
- New features
- Performance optimization
- Test coverage expansion beyond what's needed to protect deletions
---
## Report format
- Status (DONE / DONE_WITH_CONCERNS / BLOCKED)
- Commit SHAs with one-line summaries
- Files deleted (with confirmation of zero consumers)
- Files moved / refactored (e.g. ChatMessageList migration)
- Vocabulary sweep count (how many strings changed)
- Minor issues fixed vs deferred
- Visual QA findings
- Final typecheck + test suite result
- Concerns