nexus/docs/plans/2026-04-11-nexus-phase-12-promote-transition.md
Nexus Dev 87b45c730c 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>
2026-04-11 13:11:12 +00:00

230 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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