nexus/docs/plans/2026-04-11-nexus-phase-10-studio.md
Nexus Dev 3f0d3377e7 docs(nexus): wave 2 implementation plans (phases 9, 10, 11)
Three phase plans for Wave 2 of the Nexus layout overhaul. Each plan
is self-contained with ownership boundaries, scope fence, file inventory,
implementation notes, acceptance criteria, and commit scheme — designed
for parallel subagent dispatch per MIGRATION-PLAN.md section 11.

Phase 9 (Assistant mode):
  - Full-bleed chat at /assistant, no inner conversation list
  - History slide-over (left) + Memory slide-over (right)
  - Conversational home-state greeting replaces Dashboard
  - ActionStrip with Promote/Attach/Memory/History
  - New components: AssistantInputBar, ActionStrip, HistorySheet,
    MemorySheet, AssistantHomeGreeting + useAssistantHomeStatus hook
  - Owns: PersonalAssistant.tsx + components/assistant/**

Phase 10 (Studio mode):
  - 8-card workshop grid replaces 7-tab ContentStudio
  - ConvertPage folds in as 8th workshop (legacy /convert route kept)
  - StudioPromptBar freeform input with keyword-based classifier
  - Two-column workshop detail view (params left, preview right)
  - Owns: ContentStudio.tsx + pages/StudioWorkshopDetail.tsx
    + components/studio/**

Phase 11 (Projects + Builder mode):
  - ProjectCard with 72px Inter Black volt hero percentage
  - 7-tab BuilderTabStrip (Overview/Issues/Agents/Gates/Costs/Activity/Org)
  - Approvals -> Gates display-only rename
  - OverviewTab with milestone checklist, origin chat card, activity
  - Thin per-project wrappers reuse existing list components scoped
    by projectId (escalate if not supported)
  - useGateIndicator hook for the future Assistant dot notification
  - Owns: Projects.tsx + ProjectDetail.tsx + components/projects/**

Ownership boundaries prevent parallel-dispatch file conflicts:
- App.tsx routing changes are controller-owned (post-Wave wiring)
- Each phase declares its file ownership and must not cross into others
- Existing list components reused as-is; escalate if not scope-compatible
- IconRail dot wiring for phase 11's gate indicator deferred to
  post-Wave controller step

Dispatch pattern: three general-purpose subagents dispatched in parallel,
each handed the full plan text + the spec + ownership rules. Each
subagent implements its phase end-to-end with atomic commits per
logical unit. Controller reviews outputs after all three complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:09:34 +00:00

357 lines
20 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 10 — Studio Mode Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:test-driven-development`. Commit atomically per logical unit.
**Goal:** Refactor ContentStudio from a 7-tab page into an **8-card workshop grid** that folds in the existing ConvertPage as the 8th workshop. Add a freeform prompt input at the bottom of the Studio home that routes to a workshop based on intent. Build a workshop detail layout (two columns: params left, preview right) for when a user selects a workshop.
**Source of truth:** `docs/specs/2026-04-11-nexus-layout-overhaul.md` **§6** (Mode 2 — Studio). Read §6.1§6.5 before starting. DESIGN.md governs visuals.
**Branch:** `nexus/design-system-migration`. Commit directly.
---
## Ownership boundaries
**You may create or modify ONLY:**
| Path | Action |
|---|---|
| `ui/src/pages/ContentStudio.tsx` | Modify (major rewrite: from tabs to workshop grid) |
| `ui/src/pages/StudioWorkshopDetail.tsx` | Create (new routed view for a single workshop) |
| `ui/src/components/studio/**` | Create (new subdir for Phase 10 components) |
| `ui/src/pages/ConvertPage.tsx` | MAY read and reference, but DO NOT DELETE. Phase 16 handles dead-file deletion. Leave the file in place; it just stops being linked from the rail. |
**You MUST NOT touch:**
- `ui/src/App.tsx` — routing is controller-owned. If you need new sub-routes for workshops (e.g. `/content-studio/:workshopSlug`), report them in your final report; the controller wires them up after Wave 2.
- Existing content generator backends under `server/` or `packages/`. Phase 10 is purely a frontend IA reshape.
- Any file outside `ui/src/` except as noted.
- `ui/src/components/Layout.tsx`, `ui/src/components/frame/*` — Phase 8 territory, do not touch.
- Other phases' owned paths (`ui/src/pages/PersonalAssistant.tsx`, `ui/src/pages/Projects.tsx`, `ui/src/pages/ProjectDetail.tsx`, `ui/src/components/assistant/**`, `ui/src/components/projects/**`).
**Existing code you may reuse (read-only):**
- The 7 existing content-generator subcomponents that the current `ContentStudio.tsx` references for its tabs. Read `ContentStudio.tsx` first to find them. You'll reuse them as the body content of each workshop's detail view, unchanged.
- `ui/src/pages/ConvertPage.tsx` — you'll embed its main content (not the whole page shell) as the body of the Convert workshop detail view. Do not delete the file; the route path `/convert` still works in Phase 10 for backwards compatibility.
- `ui/src/components/frame/*` — Phase 8 patterns (test shape, semantic tokens, focus-visible styles, cn helper, slide-overs if you need any).
---
## Scope (strictly)
**In Phase 10:**
1. **Studio home rewrite** — replace the 7-tab layout in `ContentStudio.tsx` with a grid of **8 workshop cards**:
- `diagrams`, `icons`, `themes`, `wallpapers`, `documents`, `brand-kits`, `social`, `convert`
- 3-column grid on `>= 1024px`, 2-column on `>= 640px`, 1-column below
- Card: 1px charcoal border, 8px radius, transparent fill on idle, `bg-card` near-black on hover, 24px padding
- Title in Inter 700 uppercase 24px, subtitle in silver 14px, Lucide icon in volt top-right
2. **Convert workshop fold-in** — the 8th card routes to the existing ConvertPage body rendered inside a Studio workshop detail shell. The `/convert` top-level route must continue to work (because users might have it bookmarked) but the IconRail's Studio destination is the canonical path forward.
3. **Workshop detail view** at `StudioWorkshopDetail.tsx` — a new routed view that renders one workshop at a time:
- Two-column layout on `>= 1024px`: left = params/prompt input (40% width), right = preview (60% width)
- Mobile stacks to a single column
- Top of page shows `STUDIO / WORKSHOP-NAME` as the page title (the actual mode-breadcrumb is in TopStrip from Phase 8; this is an in-page heading)
- Action bar at the bottom of the right column: `Save`, `Export`, `Send to Assistant`
4. **StudioPromptBar** — freeform text input at the bottom of the Studio home. Placeholder: `Or just describe it: "I need a 1920×1080 wallpaper of …"`. Submits to a classifier (§6.5 of the spec) that maps intent to a workshop slug. If routing succeeds, navigate to `/content-studio/{workshopSlug}` with the prompt pre-filled. If routing fails, the prompt falls through to the Assistant as a new conversation message (see §Implementation notes).
5. **Workshop definitions** — a single source-of-truth data structure that both the grid and the detail view consume.
**NOT in Phase 10:**
- Any backend changes to content generators.
- The freeform prompt classifier itself — use a simple regex/keyword matching stub for Phase 10 (see §Implementation notes). A real LLM classifier is out of scope.
- Recipe integration (v1.8 planned; not part of Wave 2).
- Deleting `ConvertPage.tsx`. Phase 16 cleanup handles dead files.
- Editing `App.tsx` routes — report needed routes to the controller.
- Mobile polish beyond "grid collapses to 1 column" — Phase 15 handles mobile.
---
## File plan
### Create
| File | Responsibility | Est. lines |
|---|---|---|
| `ui/src/components/studio/workshops.ts` | Single source-of-truth data: array of 8 workshops with `slug`, `title`, `subtitle`, `icon`, `componentKey`. Exported for consumption by both the grid and the detail view. | ~80 |
| `ui/src/components/studio/workshops.test.ts` | Tests: all 8 workshops present, slugs unique, icons are valid Lucide imports, each `componentKey` maps to a real component | ~60 |
| `ui/src/components/studio/WorkshopCard.tsx` | Single card for the grid. Props: `workshop`, `onClick`. Renders title, subtitle, icon, hover state. | ~70 |
| `ui/src/components/studio/WorkshopCard.test.tsx` | Tests: renders workshop data, click fires callback, hover state applies correctly | ~90 |
| `ui/src/components/studio/WorkshopGrid.tsx` | Responsive grid of WorkshopCards. Takes `workshops` array and `onSelect` callback. | ~60 |
| `ui/src/components/studio/WorkshopGrid.test.tsx` | Tests: renders N cards, onSelect fires with correct workshop slug, responsive class application | ~80 |
| `ui/src/components/studio/StudioPromptBar.tsx` | Freeform text input + submit button. Calls `classifyIntent` helper and either navigates or calls `onFallbackToAssistant`. | ~80 |
| `ui/src/components/studio/StudioPromptBar.test.tsx` | Tests: renders input, classifier routing, fallback path | ~110 |
| `ui/src/components/studio/classifyIntent.ts` | Pure function. Given a prompt string, returns `{ slug: string; prefilledPrompt: string } \| null`. Keyword-matching only in Phase 10. | ~70 |
| `ui/src/components/studio/classifyIntent.test.ts` | Parameterized tests: "diagram of X" → diagrams, "wallpaper of Y" → wallpapers, "convert pdf to docx" → convert, "make me a logo" → brand-kits, "random chat" → null | ~80 |
| `ui/src/pages/StudioWorkshopDetail.tsx` | Routed view. Reads `:workshopSlug` from params, looks up in workshops data, renders two-column layout with the workshop's existing generator component on the right and a params/prompt input on the left. Action bar at the bottom: Save / Export / Send to Assistant. | ~180 |
| `ui/src/pages/StudioWorkshopDetail.test.tsx` | Tests: renders workshop by slug, 404 fallback for unknown slug, action bar renders | ~120 |
### Modify
| File | Change |
|---|---|
| `ui/src/pages/ContentStudio.tsx` | Replace the 7-tab layout with the new WorkshopGrid + StudioPromptBar. Remove tab state and tab container. Preserve any cross-cutting concerns (e.g. error boundaries, query client usage) that the existing page has. |
**Do not create or modify any other files.**
---
## Implementation notes
### Workshop data structure
```ts
// ui/src/components/studio/workshops.ts
import { Sparkles, Layers, Palette, Image, FileText, Award, Share2, Repeat } from "lucide-react";
import type { ComponentType } from "react";
export type WorkshopSlug =
| "diagrams" | "icons" | "themes" | "wallpapers"
| "documents" | "brand-kits" | "social" | "convert";
export interface WorkshopDefinition {
slug: WorkshopSlug;
title: string; // Inter 700 uppercase, e.g. "DIAGRAMS"
subtitle: string; // Inter 400 silver, single line
icon: ComponentType<{ className?: string }>; // Lucide icon
/**
* Key for looking up the generator component. The detail view maps
* this to an existing component from ContentStudio's current tab
* implementations. Phase 10 does NOT rewrite any generator internals.
*/
componentKey: string;
}
export const WORKSHOPS: WorkshopDefinition[] = [
{ slug: "diagrams", title: "DIAGRAMS", subtitle: "Mermaid → rendered SVG", icon: Sparkles, componentKey: "diagram-renderer" },
{ slug: "icons", title: "ICONS", subtitle: "SVG sets from description", icon: Layers, componentKey: "icon-renderer" },
{ slug: "themes", title: "THEMES", subtitle: "Color → full palette", icon: Palette, componentKey: "theme-renderer" },
{ slug: "wallpapers", title: "WALLPAPERS", subtitle: "Desktop, mobile, banners", icon: Image, componentKey: "wallpaper-renderer" },
{ slug: "documents", title: "DOCUMENTS", subtitle: "PDF reports, invoices", icon: FileText, componentKey: "document-renderer" },
{ slug: "brand-kits", title: "BRAND KITS", subtitle: "Full brand identity", icon: Award, componentKey: "brand-renderer" },
{ slug: "social", title: "SOCIAL", subtitle: "Platform-ready posts", icon: Share2, componentKey: "social-renderer" },
{ slug: "convert", title: "CONVERT", subtitle: "File format conversion", icon: Repeat, componentKey: "convert" },
];
```
Pick Lucide icons that fit. If a more appropriate icon exists for a slug, use it. The above is a sensible default.
### The `componentKey` → real component mapping
Phase 10 does NOT rewrite any generator. For each workshop, the detail view renders the existing generator component as-is. You must read the current `ContentStudio.tsx` to find those components. They'll be something like `<DiagramGeneratorTab />`, `<IconGeneratorTab />`, etc. Build a map:
```ts
// In StudioWorkshopDetail.tsx (or a small helper module within studio/)
const WORKSHOP_COMPONENTS: Record<string, ComponentType> = {
"diagram-renderer": DiagramGeneratorTab,
"icon-renderer": IconGeneratorTab,
// ... etc.
"convert": ConvertPageBody, // see Convert fold-in notes below
};
```
If a generator doesn't currently exist in `ContentStudio.tsx`'s tabs (e.g. if the current ContentStudio has only 6 tabs and we're adding Social as a future extension), render a `<WorkshopPlaceholder />` inline-component that shows `"Coming soon"` for that slug. Do NOT stub out a fake generator.
### Convert fold-in
`ui/src/pages/ConvertPage.tsx` currently exists and is routed via `/convert`. Phase 10 needs the Convert workshop body rendered inside the Studio workshop detail shell, but the existing `/convert` route MUST continue to work (backwards compat).
Strategy:
1. Extract the inner content of `ConvertPage.tsx` into a reusable component `ConvertPageBody`. Do this by **reading** `ConvertPage.tsx` and identifying the JSX that's the actual convert UI (the file uploader, format selector, conversion runner) vs. the page-level shell.
2. If `ConvertPage.tsx` is already structured so the inner content is a single child component, just import and reuse that component — no refactor needed.
3. If `ConvertPage.tsx` is a monolithic component and extracting `ConvertPageBody` requires a real refactor, take the smallest possible subset: wrap the whole `<ConvertPage />` in the workshop detail and accept that there's a small amount of redundant chrome. Flag as a concern.
4. Leave `ConvertPage.tsx` in place; the `/convert` route keeps rendering it directly.
### StudioPromptBar fallback to Assistant
When the classifier returns `null`, the prompt should fall through to the Assistant as a new conversation. The cleanest implementation is:
1. Navigate to `/<companyPrefix>/assistant?prompt=<urlencoded prompt>`.
2. The Assistant page (Phase 9's work) reads the `prompt` query param on mount and pre-fills the input.
**BUT:** Phase 9's `PersonalAssistant.tsx` rewrite may not include query-param pre-fill logic. Coordinate: add a note to your final report saying "StudioPromptBar fallback assumes Phase 9 will honor `?prompt=` query param on the Assistant route". Phase 9's plan should also have this noted; if it doesn't, the controller will coordinate.
For Phase 10's implementation, go ahead and navigate with the query param. If Phase 9 doesn't honor it yet, the worst case is the prompt is dropped and the user lands on an empty assistant — acceptable degradation.
### classifyIntent — Phase 10 rules
Simple keyword-based classifier. Pure function, no state, no async.
```ts
export function classifyIntent(prompt: string): { slug: WorkshopSlug; prefilledPrompt: string } | null {
const lower = prompt.toLowerCase().trim();
if (!lower) return null;
// Convert intent — "convert X to Y", "from X to Y", explicit formats
if (/\bconvert\b|from\s+\w+\s+to\s+\w+|\bpdf\b|\bdocx?\b|\bmd\b/i.test(lower)) {
return { slug: "convert", prefilledPrompt: prompt };
}
// Wallpaper intent
if (/wallpaper|background\s+image|desktop\s+bg/i.test(lower)) {
return { slug: "wallpapers", prefilledPrompt: prompt };
}
// Diagram intent
if (/diagram|flowchart|sequence|mermaid|architecture/i.test(lower)) {
return { slug: "diagrams", prefilledPrompt: prompt };
}
// Icon intent
if (/\bicon\b|icon\s+set|svg\s+icon/i.test(lower)) {
return { slug: "icons", prefilledPrompt: prompt };
}
// Theme intent
if (/\btheme\b|color\s+palette|palette/i.test(lower)) {
return { slug: "themes", prefilledPrompt: prompt };
}
// Document intent
if (/pdf|document|invoice|report|one-?pager/i.test(lower)) {
return { slug: "documents", prefilledPrompt: prompt };
}
// Brand intent
if (/brand|logo|identity|style\s+guide/i.test(lower)) {
return { slug: "brand-kits", prefilledPrompt: prompt };
}
// Social intent
if (/social|tweet|post|instagram|linkedin|twitter|x\.com/i.test(lower)) {
return { slug: "social", prefilledPrompt: prompt };
}
return null; // unclassified → fall through to Assistant
}
```
Test exhaustively with the parameterized test pattern from Phase 8's `ModeBreadcrumb.test.tsx`.
### ContentStudio.tsx target structure
```tsx
export function ContentStudio() {
const navigate = useNavigate();
const { selectedCompanyId, selectedCompany } = useCompany();
const prefix = selectedCompany?.issuePrefix ?? "";
const handleSelectWorkshop = (slug: WorkshopSlug) => {
navigate(`/${prefix}/content-studio/${slug}`);
};
const handleFallbackToAssistant = (prompt: string) => {
navigate(`/${prefix}/assistant?prompt=${encodeURIComponent(prompt)}`);
};
return (
<div className="mx-auto w-full max-w-[1200px] px-6 py-8">
<header className="mb-8">
<h1 className="text-[14px] font-semibold uppercase tracking-[0.1em] text-primary">STUDIO</h1>
<p className="mt-2 text-[14px] text-muted-foreground">
Eight workshops. Pick one or describe what you need.
</p>
</header>
<WorkshopGrid workshops={WORKSHOPS} onSelect={handleSelectWorkshop} />
<div className="mt-12">
<StudioPromptBar
onClassified={(slug, prefilled) => navigate(`/${prefix}/content-studio/${slug}?prompt=${encodeURIComponent(prefilled)}`)}
onFallbackToAssistant={handleFallbackToAssistant}
/>
</div>
</div>
);
}
```
Preserve any existing cross-cutting concerns (error boundaries, query client, auth checks) from the current ContentStudio.
### StudioWorkshopDetail routing
**You are NOT allowed to edit App.tsx.** You'll need sub-routes for Studio workshops, e.g. `/<prefix>/content-studio/:workshopSlug`. The existing App.tsx currently only has `/<prefix>/content-studio` without a sub-route param.
**Workaround for Phase 10:** render StudioWorkshopDetail as a child of ContentStudio, not as a separate route. ContentStudio checks for a `:workshopSlug` path segment via `useLocation()` (not `useParams()`, because the App.tsx routes don't define the param). If there's a slug, render StudioWorkshopDetail; otherwise render the grid.
```tsx
// ContentStudio.tsx pseudocode
export function ContentStudio() {
const location = useLocation();
const match = location.pathname.match(/\/content-studio\/([^/]+)/);
const workshopSlug = match?.[1];
if (workshopSlug) {
return <StudioWorkshopDetail slug={workshopSlug} />;
}
return <StudioHomeGrid ... />;
}
```
This is hacky but correct for Phase 10. Report to the controller: "New App.tsx routes needed: `content-studio/:workshopSlug`". The controller will replace the pathname-matching with a proper route param after Wave 2.
---
## Acceptance criteria
Phase 10 is complete when:
1. Loading `/NEX/content-studio` renders an 8-card grid with the new workshop definitions — no tab strip visible anywhere on the page.
2. Each card navigates to `/NEX/content-studio/{slug}` when clicked.
3. Loading `/NEX/content-studio/{slug}` renders the workshop detail view with the existing generator component on the right and a params panel on the left.
4. Loading `/NEX/content-studio/convert` renders the ConvertPage body inside the workshop detail shell.
5. Loading `/NEX/convert` (the legacy route) still works — backwards compat.
6. The StudioPromptBar at the bottom of the grid view accepts text input and routes correctly:
- `"diagram of the auth flow"` → navigates to `/NEX/content-studio/diagrams?prompt=...`
- `"convert this pdf to markdown"` → navigates to `/NEX/content-studio/convert?prompt=...`
- `"random musing"` → navigates to `/NEX/assistant?prompt=...` (fallback)
7. All new components have tests using the Phase 8 manual createRoot + act pattern.
8. `npx vitest run src/components/studio/ src/pages/StudioWorkshopDetail.test.tsx` passes.
9. `npx tsc --noEmit 2>&1 | grep -E "studio/|ContentStudio\.tsx|StudioWorkshopDetail\.tsx"` returns no errors.
10. The `/convert` top-level route still renders the ConvertPage.
11. No file outside the declared ownership is modified.
---
## Commit scheme
Suggested atomic commits (one per logical unit):
1. `feat(nexus): add WORKSHOPS data + classifyIntent helper (phase 10)` — workshops.ts, classifyIntent.ts, their tests
2. `feat(nexus): add WorkshopCard and WorkshopGrid components (phase 10)` — cards + grid + tests
3. `feat(nexus): add StudioPromptBar with intent routing (phase 10)` — prompt bar + tests
4. `feat(nexus): add StudioWorkshopDetail page (phase 10)` — detail view + tests
5. `refactor(nexus): rewire ContentStudio to workshop grid (phase 10)` — ContentStudio.tsx rewrite
Each commit must build and pass tests. Include `Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>` on every commit.
---
## Before you begin
Ask the controller if any of these are unclear:
- The component names and import paths of the current ContentStudio's 7 tabs (read the file first and report what's there)
- Whether `ConvertPage.tsx` is extractable or if you should wrap the whole thing
- Whether the new sub-routes the controller needs to add warrant a pause and App.tsx edit, or if the hacky path-matching workaround is acceptable
- Anything in §6 of the spec that reads as ambiguous
If you find that the current ContentStudio has fewer than 7 tabs (e.g. only 5 generators are actually built), report it — the spec assumes 7 existing + 1 new (convert). The workshop grid can still show 8 cards with placeholder content for missing generators.
---
## When you're in over your head
Escalate early if:
- ConvertPage body extraction breaks tests in the legacy `/convert` route
- The current ContentStudio has a global state or context that doesn't compose cleanly with the new grid → detail flow
- Any generator component has hardcoded dependencies on the tab layout you're removing
- classifyIntent is pulling in too many edge cases (stop at ~10 keyword patterns; more is Phase 14's classifier)
---
## Report format (final)
- **Status**
- **Commits produced**
- **Files created / modified**
- **Tests added and passing count**
- **Typecheck result** for Phase 10 files
- **Routing needs** for the controller to add to App.tsx
- **ConvertPage fold-in notes** — did you extract a body component or wrap the whole page?
- **Open concerns**
- **Deviations from the plan**
- **Self-review findings**