|
| 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