diff --git a/apps/web/src/app/pricing/page.tsx b/apps/web/src/app/pricing/page.tsx
index abdab754f..507b41b26 100644
--- a/apps/web/src/app/pricing/page.tsx
+++ b/apps/web/src/app/pricing/page.tsx
@@ -17,8 +17,8 @@ const CORE_CAPABILITIES = [
'Evidence-gated YouTube analysis',
'Timed caption transcript',
'Visible source provenance',
- 'Durable workflow run history',
- 'Review-only action preparation',
+ 'Workflow run history',
+ 'Proposed work from a Video Pack',
'Export after quality verification',
];
@@ -32,7 +32,7 @@ const PRO_CAPABILITIES = [
const COMPARISON = [
['Verified video analysis', true, true, true],
['Timed transcript and provenance', true, true, true],
- ['Review-only action plans', true, true, true],
+ ['Proposed work from a Video Pack', true, true, true],
['External agent dispatch', false, true, 'User-managed'],
['Knowledge-store writes', false, true, 'User-managed'],
['Infrastructure and provider keys', 'Hosted', 'Hosted', 'User-managed'],
diff --git a/apps/web/src/components/OneLoopStudio.tsx b/apps/web/src/components/OneLoopStudio.tsx
index eebb07129..44ce07a17 100644
--- a/apps/web/src/components/OneLoopStudio.tsx
+++ b/apps/web/src/components/OneLoopStudio.tsx
@@ -570,6 +570,15 @@ export default function OneLoopStudio({
artifactCount: packFormation.artifacts.length,
toolCount: packFormation.tools.length,
});
+ const eventsEmptyMessage = studioEventsEmptyMessage({
+ busy: transcriptWorking,
+ hasCompletedRun: Boolean(selected && selected.status !== 'processing'),
+ eventCount,
+ hasTranscript: Boolean(selected?.transcript?.trim()),
+ hasArchitecture: Boolean(packFormation.architecture),
+ artifactCount: packFormation.artifacts.length,
+ toolCount: packFormation.tools.length,
+ });
const hasPayload = studioCanExport({
transcript: selected?.transcript,
eventCount,
@@ -619,6 +628,7 @@ export default function OneLoopStudio({
const canAttemptBuildLive = Boolean(
getYouTubeId(url || selected?.url || '') && (selected || url.trim()),
);
+ const canAct = Boolean(getYouTubeId(url || selected?.url || ''));
const act = async () => {
const next = (selected?.url || url).trim();
@@ -1044,6 +1054,20 @@ export default function OneLoopStudio({
Ship this
+
{card.title}
{card.detail}
+ {card.kind === 'review' && onAct ? (
+
+ ) : null}
diff --git a/apps/web/src/components/studio/__tests__/StudioIdeOutput.playflow.test.tsx b/apps/web/src/components/studio/__tests__/StudioIdeOutput.playflow.test.tsx
index fa67b7c34..e828dcca0 100644
--- a/apps/web/src/components/studio/__tests__/StudioIdeOutput.playflow.test.tsx
+++ b/apps/web/src/components/studio/__tests__/StudioIdeOutput.playflow.test.tsx
@@ -41,6 +41,20 @@ describe('StudioIdeOutput playflow', () => {
expect(row.textContent).not.toMatch(/production is held/i);
});
+ it('uses the honest Events empty copy and can host a pack workbench', () => {
+ render(
+ Pack workbench}
+ />,
+ );
+ const events = screen.getByTestId('studio-ide-output-events');
+ expect(events.textContent).toMatch(/no extracted events/i);
+ expect(events.textContent).not.toMatch(/INSIGHT/i);
+ expect(screen.getByTestId('studio-pack-workbench')).toBeTruthy();
+ });
+
it('renders review_action results as cards with status, title, and detail', () => {
render(
{
reviewActions={[
{ tool: 'review_action', status: 'proposed', result: 'Review evidence' },
]}
+ onAct={() => undefined}
+ canAct
/>,
);
fireEvent.click(screen.getByTestId('studio-ide-output-tab-intent'));
@@ -55,5 +71,6 @@ describe('StudioIdeOutput playflow', () => {
expect(card.textContent).toMatch(/review this result/i);
expect(card.textContent).toMatch(/needs review/i);
expect(card.textContent).toMatch(/Review evidence/);
+ expect(screen.getByTestId('studio-review-act-button').textContent).toMatch(/Act on this run/);
});
});
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..87cb4d02d 100644
--- a/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts
+++ b/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts
@@ -546,6 +546,10 @@ describe('studio-pipeline-status', () => {
expect(shell).toContain('data-testid="studio-ide-toolbar"');
expect(studio).toContain('StudioIdeShell');
expect(pricing).not.toMatch(/reviewed plan dispatches backend agents/);
+ expect(pricing).not.toContain('Durable workflow run history');
+ expect(pricing).not.toContain('Review-only action preparation');
+ expect(pricing).toContain('Workflow run history');
+ expect(pricing).toContain('Proposed work from a Video Pack');
expect(apiDocs).not.toMatch(/durable Studio analysis workflow/);
});
@@ -683,6 +687,41 @@ describe('studio-pipeline-status', () => {
expect(studio).not.toMatch(/mapKeyframes|fakeEvents|invent.*events/i);
});
+ it('rewires playflow B–G into the IDE (retry, Events≠INSIGHTs, stack skip, review cards)', () => {
+ const studio = readFileSync(join(process.cwd(), 'src/components/OneLoopStudio.tsx'), 'utf8');
+ const output = readFileSync(join(process.cwd(), 'src/components/studio/StudioIdeOutput.tsx'), 'utf8');
+
+ // B — Retry transcript is a visible control, not only a computed flag.
+ expect(studio).toContain('data-testid="studio-transcript-retry"');
+ expect(studio).toMatch(/Retry transcript/);
+ expect(studio).toContain('showTranscriptRetry');
+
+ // API events[] vs UI label: Events tab, never INSIGHTs.
+ expect(output).toContain("{ id: 'events', label: 'Events' }");
+ expect(output).not.toMatch(/label:\s*['"]INSIGHT/i);
+ expect(output).not.toMatch(/label:\s*['"]Insights['"]/);
+ expect(output).toContain('eventsEmptyMessage');
+ expect(studio).toMatch(/studioEventsEmptyMessage\(/);
+ expect(studio).toMatch(/ {
const studio = readFileSync(join(process.cwd(), 'src/components/OneLoopStudio.tsx'), 'utf8');
// The header/transcript follow a resolved selection, and pack identity is
diff --git a/apps/web/src/lib/studio-playflow-PLAN.md b/apps/web/src/lib/studio-playflow-PLAN.md
index ab68c22ec..992df26f9 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: Rewire leftover Studio playflow onto 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 **Ship this** and re-render the leftover B/E/G helpers the IDE still left unused: honest Events empty copy + pack workbench when `events[]` is empty.
+* **Context:** A/#1692 and B–G helpers landed in September. #2437 restored Ship this, transcript retry, stack-check skip/unknown, and review_action cards. Live pack GET for pBsT6v-ciO8 has transcript + architecture + artifacts + stack.tools and **no `events` field**. UI must label that as **Events**, never INSIGHTs. No G.A.T.E. No fake events. No ClipToAction.
* **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` — pass `studioEventsEmptyMessage` + `PackWorkbench` when `studioPromotePackWorkbench`
+ * `apps/web/src/components/studio/StudioIdeOutput.tsx` — Events empty honesty + pack workbench; keep Ship this / reviewActions / stackCheckViews from #2437
+ * `apps/web/src/lib/__tests__/studio-pipeline-status.test.ts` — lock Events≠INSIGHTs + workbench wiring
+ * *Initial check:* Do not restore `studio-deploy-button`, origin deploy kickoff, fake events, or G.A.T.E. contract changes.
## 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] Rebase onto origin/main after #2437 (clears DIRTY + stale required closer)
+- [x] Events tab uses `studioEventsEmptyMessage` + pack workbench
+- [x] Keep Ship this, transcript retry, skip/unknown stack checks, review_action cards
+- [x] Focused Vitest green
## 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.
+* **Expected Outcome:** Anonymous Studio can click **Ship this**. Events tab says this pack has no extracted events (not INSIGHTs) and can show the pack workbench. Stack checks stay skip/unknown and do not hold ship. Transcript retry and review cards remain 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.
+* **Proof Artifact:** `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` → **5 files, 51 passed**.
## 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:** Rebased #2436 onto #2437. Wired honest Events empty copy and PackWorkbench into the IDE without dropping Ship this.
+* **Why it was needed:** The required closer check was a stale FAILURE on the old SHA, and main had moved (CONFLICTING/DIRTY).
+* **How it was tested:** Focused Vitest after rebase. Required closer then failed because #2435 closed when #2437 merged; remaining E/G lives on open #2440 (Act CTA + pricing copy).