From b9f78b44418e9470f3f2e564288f95e6107936e3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 17 Oct 2026 16:43:06 +0000 Subject: [PATCH] fix(web): restore Studio IDE playflow CTAs after rebuild Wire Retry transcript, honest empty Events, skip/unknown stack checks, review_action cards, and Act on this run into the IDE shell so pBsT6v-ciO8 is no longer a dead rail. Co-authored-by: KK --- apps/web/src/components/OneLoopStudio.tsx | 62 +++++++ .../__tests__/OneLoopStudio.playflow.test.tsx | 147 ++++++++++++++++ .../StudioIdeOutput.playflow.test.tsx | 127 ++++++++++++++ .../src/components/studio/StudioIdeOutput.tsx | 162 ++++++++++++++---- .../__tests__/studio-pipeline-status.test.ts | 3 + apps/web/src/lib/studio-pipeline-status.ts | 2 +- apps/web/src/lib/studio-playflow-PLAN.md | 35 ++-- 7 files changed, 485 insertions(+), 53 deletions(-) create mode 100644 apps/web/src/components/__tests__/OneLoopStudio.playflow.test.tsx create mode 100644 apps/web/src/components/__tests__/StudioIdeOutput.playflow.test.tsx diff --git a/apps/web/src/components/OneLoopStudio.tsx b/apps/web/src/components/OneLoopStudio.tsx index c418a40d1..d0971d0b7 100644 --- a/apps/web/src/components/OneLoopStudio.tsx +++ b/apps/web/src/components/OneLoopStudio.tsx @@ -493,6 +493,11 @@ export default function OneLoopStudio({ setMessage('Cancelling analysis…'); }, []); + const retryTranscript = () => { + const next = (selected?.url || url).trim(); + if (next) void runAnalysis(next); + }; + useEffect(() => { applyStudioQueryAutoStart({ query: studioQueryFromSearchParams(searchParams), @@ -560,6 +565,27 @@ export default function OneLoopStudio({ artifactCount: packFormation.artifacts.length, toolCount: packFormation.tools.length, }); + const eventsEmptyMessage = selected + ? studioEventsEmptyMessage({ + busy: transcriptWorking, + hasCompletedRun: selected.status !== 'processing', + eventCount, + hasTranscript: Boolean(selected.transcript?.trim()), + hasArchitecture: Boolean(packFormation.architecture), + artifactCount: packFormation.artifacts.length, + toolCount: packFormation.tools.length, + }) + : undefined; + const outputStackChecks = stackChecks.map((item) => { + const status = stackCheckStatus(item, completedChecks, 'anonymous'); + return { + id: item.id, + title: item.title, + status, + statusLabel: stackCheckStatusLabel(status), + }; + }); + const actionCards = (workflowActions?.actions ?? []).map((action) => studioActionCard(action)); const hasPayload = studioCanExport({ transcript: selected?.transcript, eventCount, @@ -1022,6 +1048,27 @@ export default function OneLoopStudio({ ) : null}
+ {showTranscriptRetry ? ( + + ) : null} + {showAgentWorkflowUi ? ( + + ) : null}
} + />, + ); + + expect(screen.getByTestId('studio-ide-output-tab-events').textContent).toBe('Events'); + expect(screen.queryByRole('tab', { name: /insights/i })).toBeNull(); + expect(screen.getByTestId('studio-ide-output-events').textContent).toMatch(/no extracted events/i); + expect(screen.getByTestId('studio-ide-output-events').textContent.toLowerCase()).not.toContain( + 'insights', + ); + expect(screen.getByTestId('pack-workbench').textContent).toMatch(/architecture/i); + }); + + it('shows skip/unknown stack-check labels so SeaDance/Cluely do not look like a production hold', () => { + render( + , + ); + + fireEvent.click(screen.getByTestId('studio-ide-output-tab-tools')); + const tools = screen.getByTestId('studio-ide-output-tools'); + expect(tools.textContent).toMatch(/SeaDance/i); + expect(tools.textContent).toMatch(/Cluely/i); + expect(tools.textContent).toMatch(/cannot run this check/i); + expect(tools.textContent.toLowerCase()).not.toMatch(/held until/); + }); + + it('renders review_action as a card with a clear CTA, not Run tools → review_action', () => { + const review = studioActionCard({ + tool: 'review_action', + status: 'proposed', + result: 'Check the pack evidence before you ship.', + }); + + render( + , + ); + + fireEvent.click(screen.getByTestId('studio-ide-output-tab-intent')); + const intent = screen.getByTestId('studio-ide-output-intent'); + expect(intent.textContent).toMatch(/review this result/i); + expect(intent.textContent).toMatch(/needs review/i); + expect(intent.textContent).toContain('Check the pack evidence before you ship.'); + expect(intent.textContent.toLowerCase()).not.toContain('run tools'); + expect(intent.textContent).not.toMatch(/review_action/); + }); + + it('offers Retry transcript in the Transcript tab when the run failed', () => { + const onRetry = vi.fn(); + render( + , + ); + + fireEvent.click(screen.getByTestId('studio-ide-output-tab-transcript')); + fireEvent.click(screen.getByTestId('studio-transcript-retry')); + expect(onRetry).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/web/src/components/studio/StudioIdeOutput.tsx b/apps/web/src/components/studio/StudioIdeOutput.tsx index 6c5ad6ddd..c072acb69 100644 --- a/apps/web/src/components/studio/StudioIdeOutput.tsx +++ b/apps/web/src/components/studio/StudioIdeOutput.tsx @@ -1,11 +1,25 @@ 'use client'; -import { useState } from 'react'; +import { useState, type ReactNode } from 'react'; import type { Video } from '@/store/dashboard-types'; import type { ExtractedEvent } from '@/lib/types'; type OutputSection = 'events' | 'lingo' | 'tools' | 'intent' | 'signals' | 'spec' | 'transcript'; +export type StudioOutputActionCard = { + title: string; + statusLabel: string; + detail: string; + kind: 'review' | 'tool'; +}; + +export type StudioOutputStackCheck = { + id: string; + title: string; + status: string; + statusLabel: string; +}; + const SECTIONS: Array<{ id: OutputSection; label: string }> = [ { id: 'events', label: 'Events' }, { id: 'lingo', label: 'Lingo' }, @@ -34,13 +48,35 @@ const SECTIONS: Array<{ id: OutputSection; label: string }> = [ export default function StudioIdeOutput({ video, specReview, + eventsEmptyMessage, + packWorkbench, + showTranscriptRetry, + onRetryTranscript, + actionCards, + stackChecks, }: { video: Video | undefined; - specReview?: React.ReactNode; + specReview?: ReactNode; + eventsEmptyMessage?: string; + packWorkbench?: ReactNode; + showTranscriptRetry?: boolean; + onRetryTranscript?: () => void; + actionCards?: StudioOutputActionCard[]; + stackChecks?: StudioOutputStackCheck[]; }) { const [active, setActive] = useState('events'); - const hasContent = Boolean(video?.videoPack || video?.events?.length || video?.insights); + const hasContent = Boolean( + video?.videoPack || + video?.events?.length || + video?.insights || + video?.transcript?.trim() || + video?.failure || + eventsEmptyMessage || + packWorkbench || + actionCards?.length || + stackChecks?.length, + ); const pack = video?.videoPack?.pack; const events: ExtractedEvent[] = video?.events ?? []; @@ -92,29 +128,39 @@ export default function StudioIdeOutput({ ) : ( <> {active === 'events' && ( -
    +
    {events.length === 0 ? ( - + <> + + {packWorkbench} + ) : ( - events.map((e) => ( -
  • -
    - - - {e.type} - - {e.timestamp ? ( - {e.timestamp} +
      + {events.map((e) => ( +
    • +
      + + + {e.type} + + {e.timestamp ? ( + {e.timestamp} + ) : null} +
      +

      {e.title}

      + {e.description ? ( +

      {e.description}

      ) : null} -
    -

    {e.title}

    - {e.description ? ( -

    {e.description}

    - ) : null} -
  • - )) + + ))} +
)} - + )} {active === 'lingo' && ( @@ -141,7 +187,7 @@ export default function StudioIdeOutput({ {active === 'tools' && (
- {toolNames.length === 0 && !linkedSop?.steps.length ? ( + {toolNames.length === 0 && !linkedSop?.steps.length && !stackChecks?.length ? ( ) : ( <> @@ -176,6 +222,26 @@ export default function StudioIdeOutput({
) : null} + {stackChecks && stackChecks.length > 0 ? ( +
+

Stack checks

+
    + {stackChecks.map((check) => ( +
  • +
    +

    {check.title}

    +

    {check.statusLabel}

    +
    +
  • + ))} +
+
+ ) : null} )} @@ -183,7 +249,23 @@ export default function StudioIdeOutput({ {active === 'intent' && (
    - {actionItems.length === 0 ? ( + {actionCards?.map((card, i) => ( +
  • +
    +

    {card.title}

    + + {card.statusLabel} + +
    +

    {card.detail}

    +
  • + ))} + {actionItems.length === 0 && !actionCards?.length ? ( ) : ( actionItems.map((a, i) => ( @@ -254,16 +336,28 @@ export default function StudioIdeOutput({ )} {active === 'transcript' && ( -
    - {video?.transcript?.trim() ? ( -

    - {video.transcript} -

    - ) : video?.failure?.message ? ( -

    {video.failure.message}

    - ) : ( -

    Nothing yet.

    - )} +
    +
    + {video?.transcript?.trim() ? ( +

    + {video.transcript} +

    + ) : video?.failure?.message ? ( +

    {video.failure.message}

    + ) : ( +

    Nothing yet.

    + )} +
    + {showTranscriptRetry && onRetryTranscript ? ( + + ) : null}
    )} diff --git a/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts b/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts index 119c49e9a..4210aa622 100644 --- a/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts +++ b/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts @@ -506,6 +506,9 @@ describe('studio-pipeline-status', () => { it('softens reviewed/durable overclaim copy and pads the sticky footer', () => { expect(STUDIO_PRODUCT_TAGLINE.toLowerCase()).not.toMatch(/reviewed actions|durable workflows/); expect(STUDIO_PRODUCT_TAGLINE).toMatch(/video pack/i); + expect( + studioPasteOutcomeMessage({ hasUsableTranscript: true }).toLowerCase(), + ).not.toContain('run tools'); const footer = readFileSync(join(process.cwd(), 'src/components/Footer.tsx'), 'utf8'); const studio = readFileSync(join(process.cwd(), 'src/components/OneLoopStudio.tsx'), 'utf8'); diff --git a/apps/web/src/lib/studio-pipeline-status.ts b/apps/web/src/lib/studio-pipeline-status.ts index c43fd886c..049cf7b2d 100644 --- a/apps/web/src/lib/studio-pipeline-status.ts +++ b/apps/web/src/lib/studio-pipeline-status.ts @@ -250,7 +250,7 @@ export function studioPasteOutcomeMessage(input: { packCitation?: string | null; }): string { if (input.hasUsableTranscript) { - return 'Ready — run tools, export, or save from this page.'; + return 'Ready — act, export, or save from this page.'; } if (input.packCitation) { return `Identity pack ${input.packCitation}. No usable transcript.`; diff --git a/apps/web/src/lib/studio-playflow-PLAN.md b/apps/web/src/lib/studio-playflow-PLAN.md index 8e94b8fc6..53536aea3 100644 --- a/apps/web/src/lib/studio-playflow-PLAN.md +++ b/apps/web/src/lib/studio-playflow-PLAN.md @@ -1,28 +1,27 @@ -# TASK: Studio playflow for empty events[] packs +# TASK: Restore Studio IDE playflow CTAs (B/E/F/G) ## 1. Goal & Scope -* **Objective:** Remove Studio dead ends when a live pack has transcript + architecture + artifacts + stack.tools but `events[]` is empty (`pBsT6v-ciO8` and similar). -* **Context:** Hayden flagged `https://uvai.io/studio?video=https://www.youtube.com/watch?v=pBsT6v-ciO8`. Pack 200: transcript ~1767 chars, architecture + artifacts[2] + stack.tools present, events count = 0. Do not invent fake events from keyframes/concepts. +* **Objective:** Restore user-visible playflow after the three-pane IDE rebuild: Retry transcript, honest empty Events (never Insights), skip/unknown stack checks, review_action cards + Act CTA. +* **Context:** Closes #2435. Dogfood `pBsT6v-ciO8`: pack 200, no `events`, no `insights`, architecture + artifacts + SeaDance/Cluely tools. Helpers already exist on main; IDE does not render them. C (player) and D (export toast) stay. * **Scope:** - * `apps/web/src/lib/studio-handoff.ts` — reconstruct unencoded `?video=https://www.youtube.com/watch?v=ID` - * `apps/web/src/lib/studio-pipeline-status.ts` — honest empty-events copy; promote pack workbench - * `apps/web/src/lib/action-surface.ts` — export architecture/artifacts when events are empty - * `apps/web/src/components/OneLoopStudio.tsx` — consume helpers; no fake events - * *Initial check:* Extend existing helpers. No Origin G.A.T.E., ClipToAction, forge, or Ship checkout. + * `apps/web/src/components/OneLoopStudio.tsx` — Retry transcript, Act on this run, pass empty-events + stack + action cards into output + * `apps/web/src/components/studio/StudioIdeOutput.tsx` — Events empty copy, pack workbench, stack skip/unknown, review_action cards, transcript retry + * Focused Vitest for the dogfood path + * *Initial check:* Extend existing helpers (`studioCanRetryTranscript`, `studioEventsEmptyMessage`, `studioActionCard`, `stackCheckStatusLabel`). No Origin G.A.T.E., ClipToAction, fake events, Ship checkout. ## 2. Execution Plan -- [x] Failing tests: unencoded query, empty-events copy, export without events, no keyframe→event mapping -- [x] Reconstruct `video` + sibling `v` for Home/share handoff -- [x] Honest Events empty state + promote architecture/artifacts/stack -- [x] Export pack files from formation when events[] is empty +- [x] Failing tests: retry button, empty Events ≠ Insights, SeaDance Unknown, review_action card, Act CTA +- [x] Wire Retry transcript in toolbar + transcript tab +- [x] Honest Events empty + pack workbench; Tools skip/unknown; Intent review cards +- [x] Soften remaining “run tools” / durable overclaim copy - [x] Verify focused Vitest ## 3. Definition of Done (Success Verification) -* **Expected Outcome:** Studio for `pBsT6v-ciO8` loads the player from `?video=`, shows transcript, honest empty Events, prominent architecture/artifacts/stack, and Export is usable. No fake events. -* **Verification Method:** `cd apps/web && npx vitest run src/lib/__tests__/studio-handoff.test.ts src/lib/__tests__/studio-pipeline-status.test.ts src/lib/__tests__/action-surface.test.ts` -* **Proof Artifact:** `npx vitest run src/lib/__tests__/studio-handoff.test.ts src/lib/__tests__/studio-pipeline-status.test.ts src/lib/__tests__/action-surface.test.ts src/lib/__tests__/uvai-surface-unification.test.ts src/lib/__tests__/home-sell-surface.test.ts` → 5 files, 43 passed. Live GET `https://uvai.io/api/video/pack?url=https://www.youtube.com/watch?v=pBsT6v-ciO8` → 200; transcript 1767 chars; architecture present; artifacts 2; stack.tools 4; no `events` field; keyframes/concepts not mapped into Events. +* **Expected Outcome:** On `/studio?video=…pBsT6v-ciO8` Events says no extracted events (not Insights), architecture/artifacts visible, SeaDance/Cluely Unknown, failed transcript has Retry, Act cards say Review this result. +* **Verification Method:** `npm --workspace=apps/web exec vitest run src/components/__tests__/OneLoopStudio.playflow.test.tsx src/components/__tests__/StudioIdeOutput.playflow.test.tsx src/components/__tests__/OneLoopStudio.three-panel.test.tsx src/components/__tests__/OneLoopStudio.build-live.test.tsx src/components/__tests__/OneLoopStudio.job-strip.test.tsx src/lib/__tests__/studio-pipeline-status.test.ts src/lib/__tests__/official-templates.test.ts src/lib/__tests__/uvai-surface-unification.test.ts` +* **Proof Artifact:** 8 files, 58 passed. ## 4. Post-Task Reflection -* **What was done:** Honest empty Events copy after a completed run; promoted architecture/artifacts workbench when `events[]` is empty; Export writes ARCHITECTURE.md + artifacts.json; `?video=` handoff reads unencoded watch URLs and reconstructs split `video`+`v` params. -* **Why it was needed:** Packs like `pBsT6v-ciO8` have useful formation but zero events. Studio previously left a dead “Events show up after Run.” rail and could refuse export. -* **How it was tested:** RED then GREEN Vitest for handoff, empty-state copy, export without events, and OneLoopStudio source guards (no keyframe→event mapping). Live pack GET confirmed the empty-events shape. +* **What was done:** Re-wired B/E/F/G helpers into the IDE: Retry transcript in toolbar + Transcript tab, honest Events empty copy + pack workbench, skip/unknown stack checks, review_action cards, Act on this run when `agentWorkflowUi` is on. Softened paste-ready copy from “run tools” to “act, export, or save”. +* **Why it was needed:** The IDE rebuild left those CTAs computed but unrendered, so AXIOM’s dogfood pack still looked stuck (empty Events, no retry, SeaDance looking like a hold). +* **How it was tested:** RED playflow component tests, then GREEN (`58 passed` across 8 files, including job-strip / three-panel / build-live regressions).