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>
10 KiB
Nexus Phase 12 — Promote-to-Project Transition
For agentic workers: Use
superpowers:test-driven-developmentper 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.tsxui/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:
- 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 toidle)
- 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 CONVERSATIONuppercase 1.4px-tracked silver label fades in above the compressed thread
- 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
idlestate)
- 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: <PROJECT-NAME>with a link to/<companyPrefix>/projects/<slug>/overview - The user stays in Assistant — does NOT auto-navigate
- 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 synconPromoteforusePromoteToProject().startPrompting, pass the hook's state downui/src/pages/PersonalAssistant.tsx: mount<PromoteTransition>as an overlay that wraps the chat thread whenpromoteState !== "idle", render<BrainstormerPanel>inside it
Implementation notes
State machine shape
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
/* 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:
// 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:
{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
promptingorcreatingtriggerspromote.cancel() - Focus traps inside the BrainstormerPanel while active
Acceptance criteria
- Clicking
⊕ Promote to projecton an active Assistant conversation triggers the 700ms animation per spec §5.6 - The chat thread compresses to 30%, brainstormer rises into 70%, source-conversation label fades in — in that order
- The BrainstormerPanel accepts Goal / Acceptance Criteria / Gates / Engineer Template, validates the Goal field as non-empty, submits via the existing project-create endpoint
- On success: transition collapses, chat restores, a
→ Project: <NAME>banner appears at the top of the chat, user stays in Assistant - On error: transition collapses, error toast shown, chat restores, promote button becomes clickable again
- ESC cancels the prompting state and restores chat
prefers-reduced-motion: reducedisables the transition — brainstormer snaps in and out instead of animating- Tests pass:
npx vitest run src/components/assistant/PromoteTransition.test.tsx src/components/assistant/BrainstormerPanel.test.tsx src/hooks/usePromoteToProject.test.ts - Typecheck clean on Phase 12 files
- 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