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

20 KiB
Raw Blame History

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

// 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:

  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.

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:

  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