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>
20 KiB
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/orpackages/. 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.tsxreferences for its tabs. ReadContentStudio.tsxfirst 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/convertstill 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:
- Studio home rewrite — replace the 7-tab layout in
ContentStudio.tsxwith 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-cardnear-black on hover, 24px padding - Title in Inter 700 uppercase 24px, subtitle in silver 14px, Lucide icon in volt top-right
- Convert workshop fold-in — the 8th card routes to the existing ConvertPage body rendered inside a Studio workshop detail shell. The
/converttop-level route must continue to work (because users might have it bookmarked) but the IconRail's Studio destination is the canonical path forward. - 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-NAMEas 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
- Two-column layout on
- 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). - 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.tsxroutes — 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
// 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:
// 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:
- Extract the inner content of
ConvertPage.tsxinto a reusable componentConvertPageBody. Do this by readingConvertPage.tsxand identifying the JSX that's the actual convert UI (the file uploader, format selector, conversion runner) vs. the page-level shell. - If
ConvertPage.tsxis already structured so the inner content is a single child component, just import and reuse that component — no refactor needed. - If
ConvertPage.tsxis a monolithic component and extractingConvertPageBodyrequires 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. - Leave
ConvertPage.tsxin place; the/convertroute 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:
- Navigate to
/<companyPrefix>/assistant?prompt=<urlencoded prompt>. - The Assistant page (Phase 9's work) reads the
promptquery 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.
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
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.
// 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:
- Loading
/NEX/content-studiorenders an 8-card grid with the new workshop definitions — no tab strip visible anywhere on the page. - Each card navigates to
/NEX/content-studio/{slug}when clicked. - 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. - Loading
/NEX/content-studio/convertrenders the ConvertPage body inside the workshop detail shell. - Loading
/NEX/convert(the legacy route) still works — backwards compat. - 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)
- All new components have tests using the Phase 8 manual createRoot + act pattern.
npx vitest run src/components/studio/ src/pages/StudioWorkshopDetail.test.tsxpasses.npx tsc --noEmit 2>&1 | grep -E "studio/|ContentStudio\.tsx|StudioWorkshopDetail\.tsx"returns no errors.- The
/converttop-level route still renders the ConvertPage. - No file outside the declared ownership is modified.
Commit scheme
Suggested atomic commits (one per logical unit):
feat(nexus): add WORKSHOPS data + classifyIntent helper (phase 10)— workshops.ts, classifyIntent.ts, their testsfeat(nexus): add WorkshopCard and WorkshopGrid components (phase 10)— cards + grid + testsfeat(nexus): add StudioPromptBar with intent routing (phase 10)— prompt bar + testsfeat(nexus): add StudioWorkshopDetail page (phase 10)— detail view + testsrefactor(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.tsxis 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
/convertroute - 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