diff --git a/docs/plans/2026-04-11-nexus-phase-11-5-per-project-scoping.md b/docs/plans/2026-04-11-nexus-phase-11-5-per-project-scoping.md new file mode 100644 index 00000000..301936f5 --- /dev/null +++ b/docs/plans/2026-04-11-nexus-phase-11-5-per-project-scoping.md @@ -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 ``) +- `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`. 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 ``. + +**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.2–1.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. diff --git a/docs/plans/2026-04-11-nexus-phase-12-promote-transition.md b/docs/plans/2026-04-11-nexus-phase-12-promote-transition.md new file mode 100644 index 00000000..144f430c --- /dev/null +++ b/docs/plans/2026-04-11-nexus-phase-12-promote-transition.md @@ -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: + - 0–200ms: 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. + - 200–500ms: BrainstormerPanel slides up from the bottom into the lower 70% + - 500–700ms: 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.1–2.2 but faster, ~300ms) + - A persistent banner at the top of the chat reads `→ Project: ` with a link to `//projects//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 `` as an overlay that wraps the chat thread when `promoteState !== "idle"`, render `` 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); +// ... + +``` + +PersonalAssistant.tsx renders the transition overlay: + +```tsx +{promote.state.kind !== "idle" && promote.state.kind !== "done" && ( + + + +)} +{promote.state.kind === "done" && ( +
+ → Project: {promote.state.projectName} +
+)} +``` + +### 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: ` 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 diff --git a/docs/plans/2026-04-11-nexus-phase-13-settings-consolidation.md b/docs/plans/2026-04-11-nexus-phase-13-settings-consolidation.md new file mode 100644 index 00000000..75243cc5 --- /dev/null +++ b/docs/plans/2026-04-11-nexus-phase-13-settings-consolidation.md @@ -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 `` 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 ( +
+

+ Settings +

+ + + + + + + + +
+ ); +} +``` + +### Section card skeleton + +Every section uses the same shell: + +```tsx +export function SettingsSection({ title, children }: { title: string; children: ReactNode }) { + return ( +
+
+

+ {title} +

+
+
+
{children}
+
+ ); +} +``` + +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/` 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 diff --git a/docs/plans/2026-04-11-nexus-phase-14-voice-cmdk-globalization.md b/docs/plans/2026-04-11-nexus-phase-14-voice-cmdk-globalization.md new file mode 100644 index 00000000..71e89b29 --- /dev/null +++ b/docs/plans/2026-04-11-nexus-phase-14-voice-cmdk-globalization.md @@ -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 8–13 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#
` anchors) +- Studio workshops (by name — map the 9 WorkshopSlugs to `/content-studio/`) +- 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 12–17. 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 + +``` + + + + + <-- NEW (Phase 14) + <-- NEW (Phase 14) + + +``` + +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 diff --git a/docs/plans/2026-04-11-nexus-phase-15-mobile.md b/docs/plans/2026-04-11-nexus-phase-15-mobile.md new file mode 100644 index 00000000..9e33a9a7 --- /dev/null +++ b/docs/plans/2026-04-11-nexus-phase-15-mobile.md @@ -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 diff --git a/docs/plans/2026-04-11-nexus-phase-16-cleanup.md b/docs/plans/2026-04-11-nexus-phase-16-cleanup.md new file mode 100644 index 00000000..226d124a --- /dev/null +++ b/docs/plans/2026-04-11-nexus-phase-16-cleanup.md @@ -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 8–15, and perform a final visual QA pass. + +**Source of truth:** spec §2 "Killed list" and the list of known minor issues logged across Phase 8–15 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 "" 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 8–15 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 `` 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//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 1–3 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 4–7 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