From df2e65782ac45c9de77eb8a10c1be42df85886c0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 8 Sep 2026 22:55:07 +0000
Subject: [PATCH 1/4] Initial plan
From 2ca23edaabedf0c23b33bb95ec93508461bb6a72 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 8 Sep 2026 23:12:09 +0000
Subject: [PATCH 2/4] fix: make studio video autostart strict-mode safe
Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com>
---
apps/web/src/app/docs/api/page.tsx | 6 ++++++
.../src/lib/__tests__/studio-handoff.test.ts | 20 +++++++++++++++++++
apps/web/src/lib/studio-handoff.ts | 9 ++++++++-
3 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/apps/web/src/app/docs/api/page.tsx b/apps/web/src/app/docs/api/page.tsx
index 08c7070ea..f61b809ff 100644
--- a/apps/web/src/app/docs/api/page.tsx
+++ b/apps/web/src/app/docs/api/page.tsx
@@ -110,6 +110,12 @@ export default function ApiDocsPage() {
Pack via /api/video/pack, then{' '}
/api/workflows/video-to-actions.
+
+ - Paste a YouTube URL on Home.
+ - Home redirects to
/studio?video=....
+ - Studio auto-starts analysis once for that handoff URL.
+ - Transcript and events flow into actions and output publishing.
+
diff --git a/apps/web/src/lib/__tests__/studio-handoff.test.ts b/apps/web/src/lib/__tests__/studio-handoff.test.ts
index 995c9aa4b..ee957ca79 100644
--- a/apps/web/src/lib/__tests__/studio-handoff.test.ts
+++ b/apps/web/src/lib/__tests__/studio-handoff.test.ts
@@ -72,6 +72,26 @@ describe('submitHomePaste kicks pack emit then hands off to Studio', () => {
});
describe('applyStudioQueryAutoStart (?video= one-shot, Strict Mode safe)', () => {
+ it('suppresses duplicate start when a Strict Mode remount gets a fresh ref object', () => {
+ const remountWatchUrl = 'https://www.youtube.com/watch?v=pBsT6v-ciO8';
+ const firstMountKey = { current: null as string | null };
+ const remountKey = { current: null as string | null };
+ const start = vi.fn();
+ const first = applyStudioQueryAutoStart({
+ query: remountWatchUrl,
+ startedKey: firstMountKey,
+ start,
+ });
+ const remount = applyStudioQueryAutoStart({
+ query: remountWatchUrl,
+ startedKey: remountKey,
+ start,
+ });
+ expect(first).toBe('started');
+ expect(remount).toBe('already');
+ expect(start).toHaveBeenCalledTimes(1);
+ });
+
it('starts once with the canonical watch URL across a Strict Mode double effect', () => {
const startedKey = { current: null as string | null };
const start = vi.fn();
diff --git a/apps/web/src/lib/studio-handoff.ts b/apps/web/src/lib/studio-handoff.ts
index 08be98249..390b49ed7 100644
--- a/apps/web/src/lib/studio-handoff.ts
+++ b/apps/web/src/lib/studio-handoff.ts
@@ -43,6 +43,7 @@ export function submitHomePaste(raw: string): string | null {
}
export type StudioQueryStartedKey = { current: string | null };
+let strictModeAutoStartedVideoId: string | null = null;
/**
* One-shot ?video= / ?url= kick. Safe under Strict Mode: the same startedKey
@@ -94,8 +95,14 @@ export function applyStudioQueryAutoStart(input: {
return 'invalid';
}
input.onResolved?.(handoff.watchUrl);
- if (input.startedKey.current === handoff.videoId) return 'already';
+ if (
+ input.startedKey.current === handoff.videoId ||
+ strictModeAutoStartedVideoId === handoff.videoId
+ ) {
+ return 'already';
+ }
input.startedKey.current = handoff.videoId;
+ strictModeAutoStartedVideoId = handoff.videoId;
input.start(handoff.watchUrl);
return 'started';
}
From cb84e8d3a9ffd22d0a76b4742e16f0ac19d2808f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 8 Sep 2026 23:13:48 +0000
Subject: [PATCH 3/4] docs: detail studio youtube handoff flow
Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com>
---
apps/web/src/lib/studio-handoff.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/lib/studio-handoff.ts b/apps/web/src/lib/studio-handoff.ts
index 390b49ed7..1a7626f47 100644
--- a/apps/web/src/lib/studio-handoff.ts
+++ b/apps/web/src/lib/studio-handoff.ts
@@ -46,8 +46,8 @@ export type StudioQueryStartedKey = { current: string | null };
let strictModeAutoStartedVideoId: string | null = null;
/**
- * One-shot ?video= / ?url= kick. Safe under Strict Mode: the same startedKey
- * ref suppresses a second start() for the same video id.
+ * One-shot ?video= / ?url= kick. Safe under Strict Mode remounts: both the
+ * caller ref and a module-level key suppress duplicate start() calls.
*/
export type StudioSearchParams = {
get(name: string): string | null;
From 05a3da92cb8317306133d428934ab1f2b040a8bb Mon Sep 17 00:00:00 2001
From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com>
Date: Sat, 12 Sep 2026 09:03:28 +0000
Subject: [PATCH 4/4] Fix: The module-level `strictModeAutoStartedVideoId`
guard is never reset, permanently suppressing auto-start for the last-started
video id across remounts within an SPA session.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This commit fixes the issue reported at apps/web/src/lib/studio-handoff.ts:46
## Bug
`apps/web/src/lib/studio-handoff.ts` has a module-level global `strictModeAutoStartedVideoId` used to swallow React's Strict Mode double-mount:
```ts
let strictModeAutoStartedVideoId: string | null = null;
// ...
if (
input.startedKey.current === handoff.videoId ||
strictModeAutoStartedVideoId === handoff.videoId
) {
return 'already';
}
input.startedKey.current = handoff.videoId;
strictModeAutoStartedVideoId = handoff.videoId; // set, but never cleared
```
A grep confirms the global is only ever **set/compared, never reset**. The per-mount `autoStartedKey` `useRef` in `OneLoopStudio.tsx` resets to `null` on a fresh mount — which is what let the previous ref-only guard correctly re-trigger auto-start on a genuine remount. The new global lives for the entire module/page lifetime.
### Concrete failure trigger
1. User pastes video `A` on Home → navigates to `/studio?video=A`.
2. `OneLoopStudio` mounts, the auto-start effect runs, `runAnalysis(A)` fires, and the global is set to `A`.
3. Analysis fails (transient network error) **or** the user leaves Studio and later re-pastes the *same* video `A`.
4. `OneLoopStudio` remounts → `autoStartedKey.current` is a fresh `null`, but `strictModeAutoStartedVideoId` still equals `A`.
5. `applyStudioQueryAutoStart` returns `'already'`, so `start()` / `runAnalysis()` is **never called**. Studio opens but analysis silently never auto-starts.
This is a regression versus the prior ref-only guard, where a fresh mount always re-triggered auto-start for the same video.
## Fix
The guard is only needed to survive React's *synchronous* Strict Mode unmount→remount; it must be released once the component truly leaves the tree.
- Added `resetStudioQueryAutoStart()` to `studio-handoff.ts` that clears the module global.
- Added a dedicated `[]`-deps unmount effect in `OneLoopStudio.tsx` whose cleanup schedules the reset via `window.setTimeout(..., 0)`.
The deferral discriminates the two remount kinds:
- **Strict Mode** (dev): unmount + remount happen synchronously in the same tick, so the remount effect runs *before* the `setTimeout(0)` callback — the guard is still set → correctly returns `'already'`, no double-start. The reset then fires harmlessly.
- **Genuine navigation**: the user leaves Studio and returns later; the timeout has long since cleared the guard, so the fresh mount (with a fresh `null` ref) re-triggers auto-start.
Also added a regression test and an `afterEach(resetStudioQueryAutoStart)` to keep the suite deterministic.
## Rebase note
Rebased onto commit `cb84e8d`. The docs commit only reworded the comment above `StudioSearchParams`; the guard code and the absence of any reset site are unchanged, so the bug and fix stand as-is.
Co-authored-by: Vercel
Co-authored-by: groupthinking
---
apps/web/src/components/OneLoopStudio.tsx | 11 +++++++
.../src/lib/__tests__/studio-handoff.test.ts | 30 +++++++++++++++++++
apps/web/src/lib/studio-handoff.ts | 11 +++++++
3 files changed, 52 insertions(+)
diff --git a/apps/web/src/components/OneLoopStudio.tsx b/apps/web/src/components/OneLoopStudio.tsx
index f8e92fe98..d3a326814 100644
--- a/apps/web/src/components/OneLoopStudio.tsx
+++ b/apps/web/src/components/OneLoopStudio.tsx
@@ -57,6 +57,7 @@ import { useYouTubePlayer } from '@/lib/use-youtube-player';
import { buildSameRunActInput, MIN_ACT_TRANSCRIPT_CHARS } from '@/lib/video-to-actions-input';
import {
applyStudioQueryAutoStart,
+ resetStudioQueryAutoStart,
resolveStudioHandoff,
studioQueryFromSearchParams,
} from '@/lib/studio-handoff';
@@ -352,6 +353,16 @@ export default function OneLoopStudio() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams]);
+ useEffect(() => {
+ // Release the module-level Strict Mode guard when Studio truly unmounts so
+ // re-entering /studio?video= with the same id (re-paste, or retry after a
+ // failed run) auto-starts again. Deferred so React's synchronous Strict
+ // Mode unmount/remount still sees the guard and does not double-start.
+ return () => {
+ window.setTimeout(() => resetStudioQueryAutoStart(), 0);
+ };
+ }, []);
+
const analyze = (event?: FormEvent) => {
event?.preventDefault();
void runAnalysis(url);
diff --git a/apps/web/src/lib/__tests__/studio-handoff.test.ts b/apps/web/src/lib/__tests__/studio-handoff.test.ts
index ee957ca79..b28482fb3 100644
--- a/apps/web/src/lib/__tests__/studio-handoff.test.ts
+++ b/apps/web/src/lib/__tests__/studio-handoff.test.ts
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
applyStudioQueryAutoStart,
+ resetStudioQueryAutoStart,
resolveStudioHandoff,
studioQueryFromSearchParams,
studioVideoHref,
@@ -72,6 +73,35 @@ describe('submitHomePaste kicks pack emit then hands off to Studio', () => {
});
describe('applyStudioQueryAutoStart (?video= one-shot, Strict Mode safe)', () => {
+ afterEach(() => {
+ // Release the module-level Strict Mode guard so tests stay isolated.
+ resetStudioQueryAutoStart();
+ });
+
+ it('re-starts the same video after a genuine unmount clears the guard', () => {
+ const start = vi.fn();
+ const firstMountKey = { current: null as string | null };
+ const first = applyStudioQueryAutoStart({
+ query: FIXTURE_WATCH,
+ startedKey: firstMountKey,
+ start,
+ });
+ expect(first).toBe('started');
+
+ // Genuine unmount releases the module-level guard.
+ resetStudioQueryAutoStart();
+
+ // Fresh mount (fresh ref) re-navigates to the same video.
+ const remountKey = { current: null as string | null };
+ const remount = applyStudioQueryAutoStart({
+ query: FIXTURE_WATCH,
+ startedKey: remountKey,
+ start,
+ });
+ expect(remount).toBe('started');
+ expect(start).toHaveBeenCalledTimes(2);
+ });
+
it('suppresses duplicate start when a Strict Mode remount gets a fresh ref object', () => {
const remountWatchUrl = 'https://www.youtube.com/watch?v=pBsT6v-ciO8';
const firstMountKey = { current: null as string | null };
diff --git a/apps/web/src/lib/studio-handoff.ts b/apps/web/src/lib/studio-handoff.ts
index 1a7626f47..71b2f4f6a 100644
--- a/apps/web/src/lib/studio-handoff.ts
+++ b/apps/web/src/lib/studio-handoff.ts
@@ -45,6 +45,17 @@ export function submitHomePaste(raw: string): string | null {
export type StudioQueryStartedKey = { current: string | null };
let strictModeAutoStartedVideoId: string | null = null;
+/**
+ * Clear the module-level Strict Mode auto-start guard. Studio calls this on a
+ * genuine unmount so re-navigating to the same ?video= later in the same SPA
+ * session (or retrying after a failed run) can auto-start again. The guard only
+ * exists to swallow React's synchronous Strict Mode double-mount, so it must be
+ * released once the component truly leaves the tree.
+ */
+export function resetStudioQueryAutoStart(): void {
+ strictModeAutoStartedVideoId = null;
+}
+
/**
* One-shot ?video= / ?url= kick. Safe under Strict Mode remounts: both the
* caller ref and a module-level key suppress duplicate start() calls.