Skip to content

Commit 4ba9e38

Browse files
committed
feat: add sizzle reel video using Remotion
Faux Claude Code session demoing agentcrumbs workflow: - User reports a crashing pipeline - Agent adds crumb tracing to the summarize function - Runs the pipeline, sees the error - Queries the crumb trail across all 3 services - Identifies the bug from the cross-service trace - Strips crumbs before merge Built with Remotion, renders to 1080p MP4 at 30fps. Uses Trigger.dev dark theme colors and real Claude Code UI patterns (spinners, tool calls, status bar).
1 parent dff52e4 commit 4ba9e38

13 files changed

Lines changed: 3177 additions & 48 deletions

File tree

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"access": "public",
88
"baseBranch": "main",
99
"updateInternalDependencies": "patch",
10-
"ignore": ["agentcrumbs-docs"]
10+
"ignore": ["agentcrumbs-docs", "papertrail", "agentcrumbs-sizzle-reel"]
1111
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ dist
66
.env
77
.env.*
88
.claude/
9+
out/

examples/sizzle-reel/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "agentcrumbs-sizzle-reel",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"studio": "remotion studio",
7+
"render": "remotion render AgentCrumbsDemo out/agentcrumbs-demo.mp4",
8+
"render:gif": "remotion render AgentCrumbsDemo out/agentcrumbs-demo.gif"
9+
},
10+
"dependencies": {
11+
"@remotion/cli": "4.0.434",
12+
"react": "^19.0.0",
13+
"react-dom": "^19.0.0",
14+
"remotion": "4.0.434"
15+
},
16+
"devDependencies": {
17+
"@remotion/eslint-config": "4.0.434",
18+
"@types/react": "^19.0.0",
19+
"typescript": "^5.7.0"
20+
}
21+
}
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
import {
2+
AbsoluteFill,
3+
useCurrentFrame,
4+
} from "remotion";
5+
import { theme, fonts } from "./theme";
6+
import {
7+
ClaudeCodeShell,
8+
ClaudeIcon,
9+
Dim,
10+
Bright,
11+
Err,
12+
} from "./ClaudeCodeShell";
13+
import { CrumbLine } from "./CrumbLine";
14+
import { SyntaxLine, lineLength, type Token } from "./SyntaxLine";
15+
16+
// ── Spinner component ───────────────────────────────────────────────
17+
const Spinner: React.FC<{ frame: number; startFrame: number; verb: string }> = ({ frame, startFrame, verb }) => {
18+
const elapsed = frame - startFrame;
19+
const seconds = Math.floor(elapsed / 30);
20+
21+
return (
22+
<div>
23+
<span style={{ color: "#D4A574" }}>{"✻ "}</span>
24+
<span style={{ color: "#D4A574" }}>{verb}...</span>
25+
<span style={{ color: theme.comment }}>{` (${seconds}s)`}</span>
26+
</div>
27+
);
28+
};
29+
30+
// ── Code tokens from examples/papertrail/src/pipeline.ts ────────────
31+
const codeTokens: Token[][] = [
32+
[["import", "kw"], [" { ", "punct"], ["trail", "fn"], [" } ", "punct"], ["from", "kw"], [" ", "plain"], ["\"agentcrumbs\"", "str"], [";", "punct"], [" // @crumbs", "cmt"]],
33+
[],
34+
[["import", "kw"], [" ", "plain"], ["type", "kw"], [" {", "punct"]],
35+
[[" Document", "type"], [",", "punct"]],
36+
[[" DocumentMetadata", "type"], [",", "punct"]],
37+
[[" DocumentSummary", "type"], [",", "punct"]],
38+
[["} ", "punct"], ["from", "kw"], [" ", "plain"], ["\"./types.js\"", "str"], [";", "punct"]],
39+
[["import", "kw"], [" { ", "punct"], ["saveDocument", "fn"], [" } ", "punct"], ["from", "kw"], [" ", "plain"], ["\"./store.js\"", "str"], [";", "punct"]],
40+
[],
41+
[["const", "kw"], [" crumb = ", "plain"], ["trail", "fn"], ["(", "punct"], ["\"doc-pipeline\"", "str"], [")", "punct"], [";", "punct"], [" // @crumbs", "cmt"]],
42+
[],
43+
[["async", "kw"], [" ", "plain"], ["function", "kw"], [" ", "plain"], ["summarize", "fn"], ["(", "punct"]],
44+
[[" text", "var"], [": ", "punct"], ["string", "type"], [",", "punct"]],
45+
[[" metadata", "var"], [": ", "punct"], ["DocumentMetadata", "type"]],
46+
[["): ", "punct"], ["Promise", "type"], ["<", "punct"], ["DocumentSummary", "type"], ["> {", "punct"]],
47+
[[" ", "plain"], ["return", "kw"], [" crumb.", "plain"], ["scope", "fn"], ["(", "punct"], ["\"summarize\"", "str"], [", ", "punct"], ["async", "kw"], [" (ctx) => {", "punct"], [" // @crumbs", "cmt"]],
48+
[[" ctx.", "plain"], ["crumb", "fn"], ["(", "punct"], ["\"calling summarizer\"", "str"], [", {", "punct"], [" // @crumbs", "cmt"]],
49+
[[" wordCount: metadata.wordCount,", "plain"], [" // @crumbs", "cmt"]],
50+
[[" topics: metadata.topics,", "plain"], [" // @crumbs", "cmt"]],
51+
[[" }, { tags: [", "punct"], ["\"ai\"", "str"], ["] });", "punct"], [" // @crumbs", "cmt"]],
52+
[],
53+
[[" crumb.", "plain"], ["time", "fn"], ["(", "punct"], ["\"ai-summarize\"", "str"], [");", "punct"], [" // @crumbs", "cmt"]],
54+
[[" ", "plain"], ["await", "kw"], [" ", "plain"], ["simulateLatency", "fn"], ["(", "punct"], ["200", "num"], [", ", "punct"], ["500", "num"], [");", "punct"]],
55+
];
56+
57+
// ── Crumb trail output ──────────────────────────────────────────────
58+
const crumbData = [
59+
{ ns: "api-gateway", nsColor: theme.nsGreen, msg: "upload request", dt: "+0ms", data: '{ filename: "q3-report.md" }' },
60+
{ ns: "api-gateway", nsColor: theme.nsGreen, msg: "auth success", dt: "+2ms", data: '{ userId: "user_1", role: "admin" }', tags: ["auth"] as string[] },
61+
{ ns: "doc-pipeline", nsColor: theme.nsCyan, msg: "starting pipeline", dt: "+3ms", data: '{ id: "doc_0002" }', type: "session:start" as const },
62+
{ ns: "doc-pipeline", nsColor: theme.nsCyan, msg: "parse", dt: "+1ms", type: "scope:enter" as const, depth: 1 },
63+
{ ns: "doc-pipeline", nsColor: theme.nsCyan, msg: "parse", dt: "+47ms", type: "time" as const, data: "{ duration: 47.1 }", depth: 1 },
64+
{ ns: "doc-pipeline", nsColor: theme.nsCyan, msg: "parse", dt: "+1ms", type: "scope:exit" as const, depth: 1 },
65+
{ ns: "doc-pipeline", nsColor: theme.nsCyan, msg: "extract-metadata", dt: "+0ms", type: "scope:enter" as const, depth: 1 },
66+
{ ns: "doc-pipeline", nsColor: theme.nsCyan, msg: "extracted-metadata", dt: "+117ms", type: "snapshot" as const, data: '{ wordCount: 56, topics: ["engineering"] }', depth: 1 },
67+
{ ns: "doc-pipeline", nsColor: theme.nsCyan, msg: "summarize", dt: "+0ms", type: "scope:enter" as const, depth: 1 },
68+
{ ns: "doc-pipeline", nsColor: theme.nsCyan, msg: "calling summarizer", dt: "+1ms", data: "{ wordCount: 56 }", depth: 2, tags: ["ai"] as string[] },
69+
{ ns: "doc-pipeline", nsColor: theme.nsCyan, msg: "summarize", dt: "+225ms", type: "scope:error" as const, data: '{ message: "Cannot read properties of undefined" }', depth: 1 },
70+
{ ns: "doc-store", nsColor: theme.nsPink, msg: "save-document", dt: "+0ms", type: "scope:enter" as const },
71+
{ ns: "doc-store", nsColor: theme.nsPink, msg: "save-document", dt: "+1ms", type: "scope:error" as const, data: '{ message: "Cannot read properties of undefined" }' },
72+
];
73+
74+
// ── Timeline ────────────────────────────────────────────────────────
75+
// 30fps × 30s = 900 frames
76+
const userMessage = "the document processing pipeline is crashing when I upload markdown files";
77+
78+
// 76 chars at 2 chars/frame = 38 frames to type, submit at 40
79+
const T = {
80+
inputStart: 0, // User starts typing in input bar
81+
userSubmit: 40, // User hits enter as soon as typing finishes
82+
agentMsg1: 55, // "I'll add crumb tracing..."
83+
// Spinner: thinking before writing code (4s = 120 frames)
84+
spinner1: 60,
85+
writeCall: 180, // Write(src/pipeline.ts) (replaces spinner)
86+
writeContent: 182,
87+
// Spinner: thinking about next step after writing code (2.5s = 75 frames)
88+
spinner2: 190,
89+
agentMsg2: 265, // "Let me run the pipeline..." (replaces spinner)
90+
bashRun: 270,
91+
runOutput: 277,
92+
runProcessing: 287,
93+
runProcessing2: 297,
94+
runError: 315,
95+
runStack: 321,
96+
// Spinner: analyzing the error (viewer reads the error)
97+
spinner3: 330,
98+
agentMsg3: 405, // "The pipeline crashed..." (replaces spinner)
99+
bashQuery: 415,
100+
crumbStart: 423,
101+
crumbInterval: 4, // 13 lines × 4 = 52 frames → ends at ~475
102+
// Spinner: analyzing the trail (viewer scans the crumb output)
103+
spinner4: 485,
104+
agentInsight: 555, // "Found it." (replaces spinner)
105+
agentInsight2: 557,
106+
agentMsg4: 625, // "Bug fixed."
107+
bashStrip: 630,
108+
stripOutput: 637,
109+
cta: 685,
110+
ctaUrl: 705,
111+
};
112+
113+
const AllContent: React.FC = () => {
114+
const frame = useCurrentFrame();
115+
116+
// Step-function scroll — jumps instantly, no animation
117+
// With flex-end anchoring, content sticks to the bottom automatically.
118+
// scrollY is only needed to scroll UP and reveal older content that
119+
// got pushed off the top — i.e., when total content height exceeds
120+
// the viewport (~692px). We DON'T scroll — we let content grow
121+
// downward and older stuff naturally disappears off the top.
122+
// scrollY = 0 always, since flex-end handles it.
123+
const scrollY = 0;
124+
125+
// Typewriter for input bar — user is typing before submit, empty after
126+
const isTyping = frame >= T.inputStart && frame < T.userSubmit;
127+
const typedChars = Math.min(Math.floor((frame - T.inputStart) * 2), userMessage.length);
128+
const inputText = isTyping ? userMessage.slice(0, typedChars) : "";
129+
130+
return (
131+
<ClaudeCodeShell scrollY={scrollY} showInput inputText={inputText}>
132+
{/* ── User prompt (appears after submit) ── */}
133+
{frame >= T.userSubmit && (
134+
<div style={{ marginBottom: 16 }}>
135+
<span style={{ color: theme.green }}>{"❯ "}</span>
136+
<span style={{ color: theme.fgBright }}>{userMessage}</span>
137+
</div>
138+
)}
139+
140+
{/* ── Agent message 1 ── */}
141+
{frame >= T.agentMsg1 && (
142+
<div style={{ marginBottom: 6 }}>
143+
<ClaudeIcon />
144+
<span style={{ color: theme.fgBright }}>
145+
I'll add crumb tracing to the summarize function so we can see what's happening at runtime.
146+
</span>
147+
</div>
148+
)}
149+
150+
{/* ── Spinner 1: thinking before writing code ── */}
151+
{frame >= T.spinner1 && frame < T.writeCall && (
152+
<Spinner frame={frame} startFrame={T.spinner1} verb="Cerebrating" />
153+
)}
154+
{frame >= T.writeCall && (
155+
<div style={{ marginBottom: 4 }}>
156+
<ClaudeIcon />
157+
<Bright>Write(</Bright>
158+
<span style={{ color: theme.string }}>src/pipeline.ts</span>
159+
<Bright>)</Bright>
160+
</div>
161+
)}
162+
163+
{/* ── Code content ── */}
164+
{frame >= T.writeContent && (
165+
<div style={{ paddingLeft: 16, marginBottom: 6 }}>
166+
<div style={{ borderLeft: `2px solid ${theme.border}`, paddingLeft: 12 }}>
167+
{codeTokens.map((tokens, i) => (
168+
<div key={i} style={{ height: 24, whiteSpace: "pre", fontSize: 16 }}>
169+
<SyntaxLine tokens={tokens} visibleChars={lineLength(tokens)} />
170+
</div>
171+
))}
172+
</div>
173+
</div>
174+
)}
175+
176+
{/* ── Spinner 2: thinking after writing code ── */}
177+
{frame >= T.spinner2 && frame < T.agentMsg2 && (
178+
<Spinner frame={frame} startFrame={T.spinner2} verb="Gallivanting" />
179+
)}
180+
{frame >= T.agentMsg2 && (
181+
<div style={{ marginTop: 20, marginBottom: 6 }}>
182+
<ClaudeIcon />
183+
<span style={{ color: theme.fgBright }}>
184+
Let me run the pipeline to test the changes.
185+
</span>
186+
</div>
187+
)}
188+
189+
{/* ── Bash run ── */}
190+
{frame >= T.bashRun && (
191+
<div style={{ marginBottom: 4 }}>
192+
<ClaudeIcon />
193+
<Bright>Bash(</Bright>
194+
<span style={{ color: theme.string }}>AGENTCRUMBS=1 pnpm -F papertrail dev</span>
195+
<Bright>)</Bright>
196+
</div>
197+
)}
198+
199+
{/* ── Run output ── */}
200+
{frame >= T.runOutput && (
201+
<div style={{ paddingLeft: 16, marginBottom: 6 }}>
202+
<div style={{ borderLeft: `2px solid ${theme.border}`, paddingLeft: 12, fontSize: 16 }}>
203+
<div><Dim>PaperTrail Demo — Document Processing Pipeline</Dim></div>
204+
{frame >= T.runProcessing && <div><Dim>Processing q3-report.md...</Dim></div>}
205+
{frame >= T.runProcessing2 && <div><Dim>Processing incident-postmortem.txt...</Dim></div>}
206+
{frame >= T.runError && (
207+
<div><Err>Error: Cannot read properties of undefined (reading 'summary')</Err></div>
208+
)}
209+
{frame >= T.runStack && (
210+
<div><Err>{" at processDocument (src/pipeline.ts:89:24)"}</Err></div>
211+
)}
212+
</div>
213+
</div>
214+
)}
215+
216+
{/* ── Spinner 3: analyzing the error (viewer reads the error) ── */}
217+
{frame >= T.spinner3 && frame < T.agentMsg3 && (
218+
<Spinner frame={frame} startFrame={T.spinner3} verb="Discombobulating" />
219+
)}
220+
221+
{/* ── Agent message 3 ── */}
222+
{frame >= T.agentMsg3 && (
223+
<div style={{ marginTop: 20, marginBottom: 6 }}>
224+
<ClaudeIcon />
225+
<span style={{ color: theme.fgBright }}>
226+
The pipeline crashed. Let me check the crumb trail to see what happened.
227+
</span>
228+
</div>
229+
)}
230+
231+
{/* ── Query call ── */}
232+
{frame >= T.bashQuery && (
233+
<div style={{ marginBottom: 4 }}>
234+
<ClaudeIcon />
235+
<Bright>Bash(</Bright>
236+
<span style={{ color: theme.string }}>agentcrumbs query --since 5m</span>
237+
<Bright>)</Bright>
238+
</div>
239+
)}
240+
241+
{/* ── Crumb trail output ── */}
242+
{frame >= T.crumbStart && (
243+
<div style={{ paddingLeft: 16, marginBottom: 6 }}>
244+
<div style={{ borderLeft: `2px solid ${theme.border}`, paddingLeft: 12 }}>
245+
{crumbData.map((line, i) => {
246+
const lineFrame = T.crumbStart + i * T.crumbInterval;
247+
if (frame < lineFrame) return null;
248+
const isError = line.type === "scope:error";
249+
return (
250+
<div
251+
key={i}
252+
style={{
253+
backgroundColor: isError ? "#F8514915" : "transparent",
254+
borderRadius: 2,
255+
padding: isError ? "0 4px" : 0,
256+
}}
257+
>
258+
<CrumbLine {...line} />
259+
</div>
260+
);
261+
})}
262+
</div>
263+
</div>
264+
)}
265+
266+
{/* ── Spinner 4: analyzing the trail (viewer scans the crumbs) ── */}
267+
{frame >= T.spinner4 && frame < T.agentInsight && (
268+
<Spinner frame={frame} startFrame={T.spinner4} verb="Cogitating" />
269+
)}
270+
271+
{/* ── Agent insight ── */}
272+
{frame >= T.agentInsight && (
273+
<div style={{ marginTop: 16 }}>
274+
<ClaudeIcon />
275+
<Bright>Found it.</Bright>
276+
<span style={{ color: theme.fg }}>
277+
{" "}The summarize scope errors at +225ms — the AI response
278+
</span>
279+
</div>
280+
)}
281+
{frame >= T.agentInsight2 && (
282+
<div style={{ paddingLeft: 20, color: theme.fg }}>
283+
is missing the expected <Bright>summary</Bright> field. The parse
284+
and metadata extraction succeeded across all 3 services.
285+
</div>
286+
)}
287+
288+
{/* ── Agent message 4 ── */}
289+
{frame >= T.agentMsg4 && (
290+
<div style={{ marginTop: 20, marginBottom: 6 }}>
291+
<ClaudeIcon />
292+
<span style={{ color: theme.fgBright }}>
293+
Bug fixed. Stripping crumbs before merge.
294+
</span>
295+
</div>
296+
)}
297+
298+
{/* ── Strip call ── */}
299+
{frame >= T.bashStrip && (
300+
<div style={{ marginBottom: 4 }}>
301+
<ClaudeIcon />
302+
<Bright>Bash(</Bright>
303+
<span style={{ color: theme.string }}>agentcrumbs strip</span>
304+
<Bright>)</Bright>
305+
</div>
306+
)}
307+
308+
{/* ── Strip output ── */}
309+
{frame >= T.stripOutput && (
310+
<div style={{ paddingLeft: 16 }}>
311+
<div style={{ borderLeft: `2px solid ${theme.border}`, paddingLeft: 12, fontSize: 16 }}>
312+
<Dim>Stripped 23 lines from 3 files.</Dim>
313+
</div>
314+
</div>
315+
)}
316+
317+
{/* ── CTA ── */}
318+
{frame >= T.cta && (
319+
<div style={{ marginTop: 32, textAlign: "center" }}>
320+
<div
321+
style={{
322+
display: "inline-block",
323+
padding: "12px 24px",
324+
backgroundColor: `${theme.green}15`,
325+
border: `1px solid ${theme.green}40`,
326+
borderRadius: 8,
327+
fontSize: 16,
328+
}}
329+
>
330+
<span style={{ color: theme.green, fontWeight: "bold" }}>agentcrumbs</span>
331+
<span style={{ color: theme.fg }}> — debug mode for </span>
332+
<span style={{ color: theme.fgBright, fontWeight: "bold" }}>any agent</span>
333+
</div>
334+
{frame >= T.ctaUrl && (
335+
<div style={{ marginTop: 12, fontSize: 13, color: theme.comment }}>
336+
agentcrumbs.dev
337+
</div>
338+
)}
339+
</div>
340+
)}
341+
</ClaudeCodeShell>
342+
);
343+
};
344+
345+
export const AgentCrumbsDemo: React.FC = () => {
346+
return (
347+
<AbsoluteFill
348+
style={{
349+
backgroundColor: theme.termBg,
350+
fontFamily: fonts.mono,
351+
}}
352+
>
353+
<AllContent />
354+
</AbsoluteFill>
355+
);
356+
};

0 commit comments

Comments
 (0)