Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/web/src/app/pricing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];

Expand All @@ -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'],
Expand Down
38 changes: 38 additions & 0 deletions apps/web/src/components/OneLoopStudio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 || ''));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind Act to the URL that the button validates

When a stored pack is selected and the user types a different valid URL without first clicking Run, canAct validates the typed URL via url || selected?.url, but act() processes selected?.url || url. The enabled CTA therefore starts the workflow for the old selected video while the input and player show the new one; resolve the source once or disable Act until the typed URL is associated with the selection.

Useful? React with 👍 / 👎.


const act = async () => {
const next = (selected?.url || url).trim();
Expand Down Expand Up @@ -1044,6 +1054,20 @@ export default function OneLoopStudio({
<Rocket className="h-4 w-4" aria-hidden />
Ship this
</button>
<button
type="button"
data-testid="studio-act-button"
onClick={() => void act()}
Comment on lines +1059 to +1060

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reuse the completed workflow instead of starting it again

In the normal flow, Run already calls startVideoToActions through processVideo and waits for it to complete, but this new CTA calls act(), which posts to the same workflow again. videoToActionsWorkflow unconditionally repeats transcript acquisition and Gemini analysis before using the supplied transcript, so every Act click—and every click from a review card—duplicates the costly analysis and may produce a different second result; persist and expose the first run's actions or use a preview endpoint that skips acquisition and analysis.

Useful? React with 👍 / 👎.

disabled={actBusy || !canAct}
Comment on lines +1057 to +1061

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep Act behind the workflow feature flag

When agent-workflow-ui is false (the default in src/flags.ts), this button still renders and invokes startVideoToActions; the new Intent CTA and returned tool results likewise ignore showAgentWorkflowUi. This exposes the durable agent workflow to cohorts for which it is explicitly disabled, so gate the triggers and results with the existing flag.

AGENTS.md reference: AGENTS.md:L64-L68

Useful? React with 👍 / 👎.

title={
canAct
? 'Prepare proposed work from this Video Pack. Review before you act.'
: 'Paste a YouTube URL and run analysis first.'
}
className="uvai-btn disabled:opacity-40"
>
{actBusy ? 'Acting…' : 'Act on this run'}
</button>
<button
type="button"
data-testid="studio-build-live-button"
Expand Down Expand Up @@ -1145,6 +1169,7 @@ export default function OneLoopStudio({
outputPane={
<StudioIdeOutput
video={selected}
eventsEmptyMessage={eventsEmptyMessage}
stackChecks={stackCheckViews}
reviewActions={(workflowActions?.actions ?? []).map((action) => {
const card = studioActionCard(action);
Expand All @@ -1154,6 +1179,19 @@ export default function OneLoopStudio({
result: action.result ?? card.detail,
};
})}
onAct={() => void act()}
actBusy={actBusy}
canAct={canAct}
Comment on lines +1182 to +1184

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope Act results to the selected pack

After Act completes for one video, selecting another stored pack does not clear or associate the component-level workflowActions state, so the second pack's Intent tab displays the first video's review cards and exporting the second pack can include those stale actions. Clear Act state on selectedVideoId changes or store the originating selection ID and render/export results only when it matches.

Useful? React with 👍 / 👎.

packWorkbench={
promotePack ? (
<PackWorkbench
architecture={packFormation.architecture}
artifacts={packFormation.artifacts}
onExport={() => void exportPkg()}
Comment on lines +1187 to +1190

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass stack tools into the promoted pack workbench

For a pack with no events and only stack.tools, studioPromotePackWorkbench returns true and the empty-state message says the stack is shown below, but this JSX passes only architecture and artifacts to PackWorkbench, which renders neither the tools nor any other pack detail in that case. The Events tab therefore shows an effectively empty card despite claiming the stack is present; pass the tools through or direct the message to the Tools tab.

AGENTS.md reference: AGENTS.md:L34-L39

Useful? React with 👍 / 👎.

canExport={hasPayload}
/>
) : undefined
}
specReview={
selected?.videoPack?.pack ? (
<GroundedSpecReview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ describe('OneLoopStudio IDE shell', () => {
expect(screen.getByTestId('studio-ide-run')).toBeTruthy();
expect(screen.getByTestId('studio-ide-ship')).toBeTruthy();
expect(screen.getByTestId('studio-ide-ship').textContent).toMatch(/Ship this/);
expect(screen.getByTestId('studio-act-button')).toBeTruthy();
expect(screen.getByTestId('studio-act-button').textContent).toMatch(/Act on this run/);
expect(screen.getByTestId('studio-build-live-button')).toBeTruthy();
});

Expand Down
75 changes: 53 additions & 22 deletions apps/web/src/components/studio/StudioIdeOutput.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -42,22 +42,34 @@ const SECTIONS: Array<{ id: OutputSection; label: string }> = [
export default function StudioIdeOutput({
video,
specReview,
eventsEmptyMessage,
stackChecks = [],
reviewActions = [],
packWorkbench,
onAct,
actBusy = false,
canAct = false,
}: {
video: Video | undefined;
specReview?: React.ReactNode;
specReview?: ReactNode;
eventsEmptyMessage?: string;
stackChecks?: StudioStackCheckView[];
reviewActions?: StudioReviewActionInput[];
packWorkbench?: ReactNode;
onAct?: () => void;
actBusy?: boolean;
canAct?: boolean;
}) {
const [active, setActive] = useState<OutputSection>('events');

const hasContent = Boolean(
video?.videoPack ||
video?.events?.length ||
video?.insights ||
eventsEmptyMessage ||
stackChecks.length > 0 ||
reviewActions.length > 0,
reviewActions.length > 0 ||
packWorkbench,
);

const pack = video?.videoPack?.pack;
Expand Down Expand Up @@ -110,29 +122,37 @@ export default function StudioIdeOutput({
) : (
<>
{active === 'events' && (
<ul className="flex flex-col gap-3" data-testid="studio-ide-output-events">
<div className="flex flex-col gap-3" data-testid="studio-ide-output-events">
{events.length === 0 ? (
<EmptyNote text="No events extracted from this video yet." />
<EmptyNote
text={
eventsEmptyMessage ||
'This run has no extracted events. Pack identity stays on this page.'
}
/>
) : (
events.map((e) => (
<li key={e.id} className="uvai-card p-4">
<div className="flex items-center justify-between gap-2">
<span className="inline-flex items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-neutral-700">
<DotIcon />
{e.type}
</span>
{e.timestamp ? (
<span className="font-mono text-[11px] text-slate-400">{e.timestamp}</span>
<ul className="flex flex-col gap-3">
{events.map((e) => (
<li key={e.id} className="uvai-card p-4">
<div className="flex items-center justify-between gap-2">
<span className="inline-flex items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-neutral-700">
<DotIcon />
{e.type}
</span>
{e.timestamp ? (
<span className="font-mono text-[11px] text-slate-400">{e.timestamp}</span>
) : null}
</div>
<p className="mt-2 text-sm font-semibold text-slate-900">{e.title}</p>
{e.description ? (
<p className="mt-1 text-xs leading-relaxed text-slate-500">{e.description}</p>
) : null}
</div>
<p className="mt-2 text-sm font-semibold text-slate-900">{e.title}</p>
{e.description ? (
<p className="mt-1 text-xs leading-relaxed text-slate-500">{e.description}</p>
) : null}
</li>
))
</li>
))}
</ul>
)}
</ul>
{packWorkbench}
</div>
)}

{active === 'lingo' && (
Expand Down Expand Up @@ -245,6 +265,17 @@ export default function StudioIdeOutput({
</p>
<p className="mt-1 text-sm font-semibold text-slate-900">{card.title}</p>
<p className="mt-1 text-xs leading-relaxed text-slate-500">{card.detail}</p>
{card.kind === 'review' && onAct ? (
<button
type="button"
data-testid="studio-review-act-button"
onClick={onAct}
disabled={actBusy || !canAct}
Comment on lines +268 to +273

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match the review CTA to the actions the workflow produces

Real Act previews return registered tool names such as create_workflow_task with status pending because videoToActionsWorkflow calls runActionAgent with executeTools: false; no production producer emits a tool named review_action. Since this button is restricted to card.kind === 'review', normal proposed actions render as ordinary cards labelled pending with no review CTA, making the newly wired confirmation path unreachable. Render the CTA based on the preview status or return explicit review metadata from the workflow.

Useful? React with 👍 / 👎.

className="uvai-btn uvai-btn-primary mt-3 disabled:opacity-40"
>
{actBusy ? 'Acting…' : 'Act on this run'}
</button>
) : null}
</div>
</div>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,36 @@ 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(
<StudioIdeOutput
video={baseVideo}
eventsEmptyMessage="This pack has no extracted events. Architecture and artifacts stay on this page."
packWorkbench={<div data-testid="studio-pack-workbench">Pack workbench</div>}
/>,
);
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(
<StudioIdeOutput
video={baseVideo}
reviewActions={[
{ tool: 'review_action', status: 'proposed', result: 'Review evidence' },
]}
onAct={() => undefined}
canAct
/>,
);
fireEvent.click(screen.getByTestId('studio-ide-output-tab-intent'));
const card = screen.getByTestId('studio-review-action-card');
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/);
});
});
39 changes: 39 additions & 0 deletions apps/web/src/lib/__tests__/studio-pipeline-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});

Expand Down Expand Up @@ -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(/<PackWorkbench/);

// F — anonymous stack checks surface skip/unknown; Build live is not held.
expect(output).toContain('stackChecks');
expect(studio).toMatch(/stackCheckStatus\(/);
expect(studio).toMatch(/stackCheckStatusLabel\(/);
expect(studio).toMatch(/stackCheckStatus\([^)]*'anonymous'/);
expect(studio).not.toMatch(/!canAttemptBuildLive\s*\|\|\s*.*holdReason/);
expect(studio).not.toContain('studio-deploy-button');

// E — review_action cards render in the Intent tab, with Act CTA un-gated.
expect(output).toContain('reviewActions');
expect(studio).toMatch(/studioActionCard\(/);
expect(output).toMatch(/Needs review|statusLabel/);
expect(studio).toContain('data-testid="studio-act-button"');
expect(studio).toMatch(/Act on this run/);
expect(studio).not.toMatch(/showAgentWorkflowUi\s*&&\s*[\s\S]{0,80}studio-act-button/);
expect(output).toContain('studio-review-act-button');
});

it('selects a stored pack for the header without a Stored packs combobox click (#2244)', () => {
const studio = readFileSync(join(process.cwd(), 'src/components/OneLoopStudio.tsx'), 'utf8');
// The header/transcript follow a resolved selection, and pack identity is
Expand Down
35 changes: 16 additions & 19 deletions apps/web/src/lib/studio-playflow-PLAN.md
Original file line number Diff line number Diff line change
@@ -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).
Loading