diff --git a/apps/web/src/components/OneLoopStudio.tsx b/apps/web/src/components/OneLoopStudio.tsx index eebb07129..3b79e074a 100644 --- a/apps/web/src/components/OneLoopStudio.tsx +++ b/apps/web/src/components/OneLoopStudio.tsx @@ -503,6 +503,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), @@ -570,6 +575,17 @@ 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 hasPayload = studioCanExport({ transcript: selected?.transcript, eventCount, @@ -1033,6 +1049,27 @@ export default function OneLoopStudio({ ) : null}
+ {showTranscriptRetry ? ( + + ) : null} + {showAgentWorkflowUi ? ( + + ) : null} - {showTranscriptRetry ? ( - - ) : null}

{statusText} @@ -1145,6 +1172,19 @@ export default function OneLoopStudio({ outputPane={ void exportPkg()} + canExport={hasPayload} + /> + ) : undefined + } + showTranscriptRetry={showTranscriptRetry} + onRetryTranscript={retryTranscript} stackChecks={stackCheckViews} reviewActions={(workflowActions?.actions ?? []).map((action) => { const card = studioActionCard(action); diff --git a/apps/web/src/components/__tests__/OneLoopStudio.playflow.test.tsx b/apps/web/src/components/__tests__/OneLoopStudio.playflow.test.tsx new file mode 100644 index 000000000..492cdaa21 --- /dev/null +++ b/apps/web/src/components/__tests__/OneLoopStudio.playflow.test.tsx @@ -0,0 +1,148 @@ +// @vitest-environment jsdom +import React from 'react'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import OneLoopStudio from '@/components/OneLoopStudio'; +import { useDashboardStore, type Video } from '@/store/dashboard-store'; + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), + useSearchParams: () => new URLSearchParams(), + usePathname: () => '/studio', +})); +vi.mock('@/components/Nav', () => ({ default: () => null })); +vi.mock('@/app/studio/actions', () => ({ openGitHubPrsForApprovedSpecs: vi.fn() })); +vi.mock('@/lib/use-youtube-player', () => ({ + useYouTubePlayer: () => ({ + containerRef: { current: null }, + ready: true, + failed: false, + seekTo: vi.fn(), + }), +})); + +const dogfoodVideo: Video = { + id: 'pBsT6v-ciO8', + title: 'AI gold rushes', + url: 'https://www.youtube.com/watch?v=pBsT6v-ciO8', + status: 'complete', + progress: 100, + transcript: + 'What do you think are the three mini gold rushes happening inside AI right now that no one is talking about? SeaDance 2.5 and Cluely Desktop.', + events: [], +}; + +beforeEach(() => { + vi.stubGlobal('React', React); + vi.spyOn(useDashboardStore.persist, 'rehydrate').mockImplementation(() => undefined); + useDashboardStore.setState({ videos: [], selectedVideoId: null }); +}); + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe('OneLoopStudio playflow CTAs after the IDE rebuild', () => { + it('shows Retry transcript when analysis failed and does not leave a silent wait', async () => { + const { reviewPackFixture } = await import('@/test/grounded-spec-fixture'); + const pack = reviewPackFixture(); + useDashboardStore.setState({ + videos: [ + { + ...dogfoodVideo, + status: 'failed', + transcript: '', + failure: { + stage: 'analysis', + message: 'Captions were not available.', + retryable: true, + failedAt: '2026-10-17T00:00:00.000Z', + }, + videoPack: { + packId: pack.id, + videoId: 'pBsT6v-ciO8', + sourceUrl: dogfoodVideo.url, + sourceHash: pack.provenance.source_hash, + version: pack.version, + pack: { ...pack, video_id: 'pBsT6v-ciO8', source_url: dogfoodVideo.url }, + }, + }, + ], + selectedVideoId: dogfoodVideo.id, + }); + + render(); + + expect(screen.getByTestId('studio-transcript-retry').textContent).toMatch(/retry transcript/i); + expect(screen.getByTestId('studio-ide-status').textContent.toLowerCase()).not.toMatch( + /reviewed actions|durable workflows/, + ); + }); + + it('keeps Export and Build live enabled for anonymous SeaDance/Cluely packs with empty events[]', async () => { + const { reviewPackFixture } = await import('@/test/grounded-spec-fixture'); + const pack = reviewPackFixture(); + pack.architecture = { + summary: 'Five-stage monetization pipeline.', + stages: [{ id: 'stage_1', name: 'Target Lead Extraction' }], + mermaid: null, + }; + pack.artifacts = [ + { + path_hint: 'interfaces/agency_pipeline.ts', + purpose: 'Lead and retainer contracts.', + interface: 'export interface AgencyClientPipeline {}', + }, + { + path_hint: 'interfaces/cluely_architecture.ts', + purpose: 'Micro-app factory.', + interface: 'export interface ProductSuiteManager {}', + }, + ]; + pack.stack = { + tools: [ + { name: 'SeaDance 2.5', check: 'seadance --version' }, + { name: 'Cluely Desktop', check: 'curl -I https://cluely.com' }, + ], + }; + + useDashboardStore.setState({ + videos: [ + { + ...dogfoodVideo, + videoPack: { + packId: pack.id, + videoId: 'pBsT6v-ciO8', + sourceUrl: dogfoodVideo.url, + sourceHash: pack.provenance.source_hash, + version: pack.version, + pack: { ...pack, video_id: 'pBsT6v-ciO8', source_url: dogfoodVideo.url }, + }, + }, + ], + selectedVideoId: dogfoodVideo.id, + }); + + render(); + + expect(screen.getByTestId('studio-ide-ship').hasAttribute('disabled')).toBe(false); + expect(screen.getByTestId('studio-ide-ship').textContent).toMatch(/ship this/i); + expect(screen.getByTestId('studio-build-live-button').hasAttribute('disabled')).toBe(false); + expect(screen.getByTestId('studio-act-button').textContent).toMatch(/act on this run/i); + + 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')).toBeTruthy(); + expect(screen.getByTestId('pack-architecture').textContent).toMatch(/five-stage/i); + + 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); + }); +}); diff --git a/apps/web/src/components/__tests__/StudioIdeOutput.playflow.test.tsx b/apps/web/src/components/__tests__/StudioIdeOutput.playflow.test.tsx new file mode 100644 index 000000000..62fe7c77d --- /dev/null +++ b/apps/web/src/components/__tests__/StudioIdeOutput.playflow.test.tsx @@ -0,0 +1,129 @@ +// @vitest-environment jsdom +import React from 'react'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import StudioIdeOutput from '@/components/studio/StudioIdeOutput'; +import type { Video } from '@/store/dashboard-types'; +import { studioEventsEmptyMessage } from '@/lib/studio-pipeline-status'; + +const emptyEventsVideo: Video = { + id: 'pBsT6v-ciO8', + title: 'AI gold rushes', + url: 'https://www.youtube.com/watch?v=pBsT6v-ciO8', + status: 'complete', + progress: 100, + transcript: 'SeaDance 2.5 and Cluely Desktop from the video.', + events: [], +}; + +afterEach(() => { + cleanup(); +}); + +describe('StudioIdeOutput playflow (B/E/F + events vs Insights)', () => { + it('labels the Events tab Events, never Insights, and tells the truth when events[] is empty', () => { + const emptyMessage = studioEventsEmptyMessage({ + busy: false, + hasCompletedRun: true, + eventCount: 0, + hasArchitecture: true, + artifactCount: 2, + toolCount: 4, + }); + + render( + Architecture from this pack} + />, + ); + + 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', () => { + 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 ae4cc5b42..16c40bf22 100644 --- a/apps/web/src/components/studio/StudioIdeOutput.tsx +++ b/apps/web/src/components/studio/StudioIdeOutput.tsx @@ -1,6 +1,6 @@ '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'; import { studioActionCard } from '@/lib/studio-pipeline-status'; @@ -42,11 +42,19 @@ const SECTIONS: Array<{ id: OutputSection; label: string }> = [ export default function StudioIdeOutput({ video, specReview, + eventsEmptyMessage, + packWorkbench, + showTranscriptRetry, + onRetryTranscript, stackChecks = [], reviewActions = [], }: { video: Video | undefined; - specReview?: React.ReactNode; + specReview?: ReactNode; + eventsEmptyMessage?: string; + packWorkbench?: ReactNode; + showTranscriptRetry?: boolean; + onRetryTranscript?: () => void; stackChecks?: StudioStackCheckView[]; reviewActions?: StudioReviewActionInput[]; }) { @@ -56,6 +64,10 @@ export default function StudioIdeOutput({ video?.videoPack || video?.events?.length || video?.insights || + video?.transcript?.trim() || + video?.failure || + eventsEmptyMessage || + packWorkbench || stackChecks.length > 0 || reviewActions.length > 0, ); @@ -110,29 +122,39 @@ export default function StudioIdeOutput({ ) : ( <> {active === 'events' && ( -

)} - + )} {active === 'lingo' && ( @@ -319,16 +341,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 05167ecba..4927f6d63 100644 --- a/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts +++ b/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts @@ -532,6 +532,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 2bd869c7e..9dc5acfef 100644 --- a/apps/web/src/lib/studio-pipeline-status.ts +++ b/apps/web/src/lib/studio-pipeline-status.ts @@ -276,7 +276,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 ab68c22ec..feeb4c26c 100644 --- a/apps/web/src/lib/studio-playflow-PLAN.md +++ b/apps/web/src/lib/studio-playflow-PLAN.md @@ -1,29 +1,26 @@ -# TASK: Restore Studio Ship this playflow after IDE shell +# TASK: Restore remaining Studio IDE playflow after Ship this ## 1. Goal & Scope -* **Objective:** Get UVAI Studio playflow back toward ≥9/10 by restoring a discoverable **Ship this** path and rewiring leftover B/E/G helpers the IDE shell left unused. -* **Context:** Kill A (#1692) merged. B–G helpers exist (`studioTranscriptStage`, `studioActionCard`, `deployHoldReason` skip/unknown, export toast, player overlay). The 2026-10-07 IDE refactor deleted `studio-deploy-button` and stopped rendering Ship this, stack-check honesty, review_action cards, and transcript retry. Dogfood kill F still stands: no Ship this; seadance/cluely-class checks must not hold anonymous ship. +* **Objective:** Keep #2437’s **Ship this** ZIP CTA, and restore the leftover dogfood rails this branch still owns: honest empty Events (never Insights), pack workbench, Act on this run, transcript-tab retry, and softened “run tools” copy. +* **Context:** #2437 (`560f07120`) already closed #2435 with Ship this + skip/unknown stack rows + review cards + toolbar retry. This branch layers Events honesty and the flag-gated Act CTA on that landing. C (player) and D (export toast at `bottom-4`) stay. No Origin G.A.T.E., ClipToAction, fake events, or Ship checkout. * **Scope:** - * `apps/web/src/lib/studio-pipeline-status.ts` — `studioShipEnabled` / `studioShipCta` (exportability gates ship; holdReason never does) - * `apps/web/src/lib/official-templates.ts` — stack-check row view (skip/unknown) - * `apps/web/src/components/OneLoopStudio.tsx` — Ship this CTA, transcript retry, toast position, pass checks/actions - * `apps/web/src/components/studio/StudioIdeOutput.tsx` — stack checks + `studioActionCard` - * *Initial check:* Extend existing helpers. Do not restore `studio-deploy-button`, origin deploy kickoff, fake events, or G.A.T.E. contract changes. + * `apps/web/src/components/OneLoopStudio.tsx` — Act on this run, honest empty-events + pack workbench into output; keep Ship this + * `apps/web/src/components/studio/StudioIdeOutput.tsx` — Events empty copy, pack workbench, transcript-tab retry on main’s stackChecks/reviewActions API + * Focused Vitest for the dogfood path + * *Initial check:* Do not restore `studio-deploy-button`. Do not replace Ship this with a second Export button. ## 2. Execution Plan -- [x] Failing tests for Ship this enablement, skip/unknown checks, review cards, retry, toast position -- [x] Ship this CTA enabled by `studioCanExport`, never by non-runnable stack checks -- [x] Tools tab shows honest skip/unknown; Intent tab renders `review_action` cards -- [x] Transcript retry control when `studioCanRetryTranscript` -- [x] Export toast no longer uses sticky-footer `bottom-20` -- [x] Focused Vitest +- [x] Merge origin/main (#2437 Ship this) and keep complementary Events/Act wiring +- [x] Honest Events empty + pack workbench; Tools skip/unknown; Intent review cards +- [x] Soften remaining “run tools” paste-ready copy +- [x] Verify focused Vitest after the merge ## 3. Definition of Done (Success Verification) -* **Expected Outcome:** Anonymous Studio on a seadance/cluely pack can click **Ship this** (ZIP export). Stack checks read skip/unknown and do not disable the button. No live hostname claim. Transcript retry and review cards are visible. -* **Verification Method:** `cd apps/web && npx vitest run src/lib/__tests__/studio-pipeline-status.test.ts src/lib/__tests__/official-templates.test.ts src/lib/__tests__/uvai-surface-unification.test.ts src/components/__tests__/OneLoopStudio.three-panel.test.tsx src/components/studio/__tests__/StudioIdeOutput.playflow.test.tsx` -* **Proof Artifact:** `cd apps/web && npm test -- src/lib/__tests__/studio-pipeline-status.test.ts src/lib/__tests__/official-templates.test.ts src/lib/__tests__/uvai-surface-unification.test.ts src/components/__tests__/OneLoopStudio.three-panel.test.tsx src/components/studio/__tests__/StudioIdeOutput.playflow.test.tsx` → **5 files, 49 passed**. Related: OneLoopStudio.build-live + job-strip + linked-sop → **3 files, 16 passed**. `tsc --noEmit` and eslint on touched files passed. +* **Expected Outcome:** On `/studio?video=…pBsT6v-ciO8` Events says no extracted events (not Insights), architecture/artifacts visible, SeaDance/Cluely Unknown, **Ship this** enabled, 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/studio/__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:** 9 files, 62 passed after merging `origin/main` (#2437). ## 4. Post-Task Reflection -* **What was done:** Restored a discoverable **Ship this** toolbar CTA that downloads the pack ZIP. Anonymous/unknown SeaDance/Cluely stack checks render as skip/unknown and never disable the button. Intent tab renders `review_action` cards. Transcript retry is visible on retryable failure. Export toast sits at `bottom-4` (no sticky footer). Origin deploy button was not restored. -* **Why it was needed:** IDE shell left playflow helpers unwired; dogfood kill F was a disabled Deploy with no Ship this. -* **How it was tested:** Focused Vitest (49 + 16), type-check, eslint. No production live claim. GitHub issue create was 403 so this cut has no new issue number. +* **What was done:** Merged #2437’s Ship this landing and kept the remaining playflow: Events honesty, pack workbench, Act on this run (`agentWorkflowUi`), transcript-tab retry, and “act, export, or save” paste copy. +* **Why it was needed:** #2437 restored Ship this / stack rows / review cards; empty `events[]` packs still needed an honest Events pane and the flag-gated Act CTA. +* **How it was tested:** Focused Vitest after merge: 9 files, 62 passed.