From 72837c7d7ca9fd61146822a851d816bc895c4cea Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 05:17:18 +0000 Subject: [PATCH] Fix Cutroom edge cases from repo review --- .../app/api/walkthroughs/[id]/ask/route.ts | 45 +++++++++++++++---- .../src/app/api/walkthroughs/import/route.ts | 3 +- apps/cutroom/src/lib/walkthrough-mutate.ts | 18 ++++++-- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/apps/cutroom/src/app/api/walkthroughs/[id]/ask/route.ts b/apps/cutroom/src/app/api/walkthroughs/[id]/ask/route.ts index 52fe5d5..8f90375 100644 --- a/apps/cutroom/src/app/api/walkthroughs/[id]/ask/route.ts +++ b/apps/cutroom/src/app/api/walkthroughs/[id]/ask/route.ts @@ -25,6 +25,42 @@ interface PostBody { question: string; } +function parseAskEnvelope(stdout: string): { answer: string; citations: string[] } { + const lines = stdout + .trim() + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + + for (let i = lines.length - 1; i >= 0; i--) { + try { + const parsed = JSON.parse(lines[i]) as unknown; + if ( + parsed && + typeof parsed === "object" && + "answer" in parsed && + "citations" in parsed + ) { + const envelope = parsed as { answer: unknown; citations: unknown }; + if ( + typeof envelope.answer === "string" && + Array.isArray(envelope.citations) && + envelope.citations.every((c) => typeof c === "string") + ) { + return { + answer: envelope.answer, + citations: envelope.citations, + }; + } + } + } catch { + /* keep scanning older log lines */ + } + } + + throw new Error("director ask returned no JSON answer envelope"); +} + export async function POST( req: NextRequest, { params }: { params: { id: string } }, @@ -70,14 +106,7 @@ export async function POST( ], { cwd: REPO_ROOT, env, timeout: 60_000, maxBuffer: 4 * 1024 * 1024 }, ); - // CLI prints a single-line JSON envelope after a logfire status line; - // grab the last JSON object. - const lastBrace = stdout.lastIndexOf("{"); - const trailing = lastBrace >= 0 ? stdout.slice(lastBrace) : stdout; - const parsed = JSON.parse(trailing) as { - answer: string; - citations: string[]; - }; + const parsed = parseAskEnvelope(stdout); return NextResponse.json({ ok: true, ...parsed }); } catch (err) { return directorErrorResponse(err, "ask_failed"); diff --git a/apps/cutroom/src/app/api/walkthroughs/import/route.ts b/apps/cutroom/src/app/api/walkthroughs/import/route.ts index 01e78bd..a72e5a4 100644 --- a/apps/cutroom/src/app/api/walkthroughs/import/route.ts +++ b/apps/cutroom/src/app/api/walkthroughs/import/route.ts @@ -99,8 +99,9 @@ export async function POST(req: Request) { await mkdir(stepsDir, { recursive: true }); const stepIdMap = new Map(); // captured id -> stable slug + const usedStepIds = new Set(); payload.steps.forEach((s, i) => { - stepIdMap.set(s.id, uniqueSlug(slugForIndex(i, s), new Set())); + stepIdMap.set(s.id, uniqueSlug(slugForIndex(i, s), usedStepIds)); }); for (const captured of payload.steps) { diff --git a/apps/cutroom/src/lib/walkthrough-mutate.ts b/apps/cutroom/src/lib/walkthrough-mutate.ts index f73e391..065dea6 100644 --- a/apps/cutroom/src/lib/walkthrough-mutate.ts +++ b/apps/cutroom/src/lib/walkthrough-mutate.ts @@ -117,6 +117,11 @@ export async function appendStep( ): Promise { const raw = await readRaw(id); const newId = opts.step_id ?? nextStepId(raw.steps); + assertStepId(newId); + const durationMs = opts.duration_ms ?? 5000; + if (!Number.isInteger(durationMs) || durationMs < 500 || durationMs > 30_000) { + throw new Error("duration_ms must be an integer between 500 and 30000"); + } if (raw.steps.some((s) => s.id === newId)) { return raw.steps.find((s) => s.id === newId)!; } @@ -124,10 +129,10 @@ export async function appendStep( id: newId, title: opts.title ?? "New step", narration: opts.narration ?? "Describe what's on screen for this step.", - duration_ms: opts.duration_ms ?? 5000, + duration_ms: durationMs, actions: [ { kind: "goto", url: "/" }, - { kind: "wait", ms: opts.duration_ms ?? 4500 }, + { kind: "wait", ms: Math.max(0, durationMs - 500) }, ], }; raw.steps.push(step); @@ -141,10 +146,17 @@ export async function reorderSteps( id: string, orderedIds: string[], ): Promise { + for (const orderedId of orderedIds) { + assertStepId(orderedId); + } const raw = await readRaw(id); const have = new Set(raw.steps.map((s) => s.id)); const wanted = new Set(orderedIds); - if (have.size !== wanted.size || [...have].some((s) => !wanted.has(s))) { + if ( + have.size !== wanted.size || + orderedIds.length !== wanted.size || + [...have].some((s) => !wanted.has(s)) + ) { throw new Error( `reorder ids must match: have [${[...have].sort().join(",")}], wanted [${[...wanted].sort().join(",")}]`, );