From a186ac1a120ac62c4d925539b870861882aab879 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 19:28:35 +0000 Subject: [PATCH] remotion: add 20s agent-bundle demo composition Casual terminal-first demo for moving Codex / Claude / OpenCode setup between machines via root agent-bundle export, apply, and verify. Co-authored-by: Sergio Rovira --- remotion/CompositionAgentBundle.tsx | 42 +++ remotion/Root.tsx | 15 + remotion/components/GhosttyTerminal.tsx | 290 ++++++++++++++++++ remotion/data/script-agent-bundle.ts | 22 ++ remotion/package.json | 5 +- remotion/scenes/agent-bundle/ApplyScene.tsx | 58 ++++ remotion/scenes/agent-bundle/CloseScene.tsx | 82 +++++ remotion/scenes/agent-bundle/ExportScene.tsx | 54 ++++ remotion/scenes/agent-bundle/IntroScene.tsx | 78 +++++ remotion/scenes/agent-bundle/SceneCaption.tsx | 67 ++++ .../scenes/agent-bundle/TransferScene.tsx | 150 +++++++++ remotion/scenes/agent-bundle/VerifyScene.tsx | 90 ++++++ 12 files changed, 951 insertions(+), 2 deletions(-) create mode 100644 remotion/CompositionAgentBundle.tsx create mode 100644 remotion/components/GhosttyTerminal.tsx create mode 100644 remotion/data/script-agent-bundle.ts create mode 100644 remotion/scenes/agent-bundle/ApplyScene.tsx create mode 100644 remotion/scenes/agent-bundle/CloseScene.tsx create mode 100644 remotion/scenes/agent-bundle/ExportScene.tsx create mode 100644 remotion/scenes/agent-bundle/IntroScene.tsx create mode 100644 remotion/scenes/agent-bundle/SceneCaption.tsx create mode 100644 remotion/scenes/agent-bundle/TransferScene.tsx create mode 100644 remotion/scenes/agent-bundle/VerifyScene.tsx diff --git a/remotion/CompositionAgentBundle.tsx b/remotion/CompositionAgentBundle.tsx new file mode 100644 index 0000000..f368e83 --- /dev/null +++ b/remotion/CompositionAgentBundle.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Sequence } from "remotion"; +import { AB_SCENES } from "./data/script-agent-bundle"; +import { IntroScene } from "./scenes/agent-bundle/IntroScene"; +import { ExportScene } from "./scenes/agent-bundle/ExportScene"; +import { TransferScene } from "./scenes/agent-bundle/TransferScene"; +import { ApplyScene } from "./scenes/agent-bundle/ApplyScene"; +import { VerifyScene } from "./scenes/agent-bundle/VerifyScene"; +import { CloseScene } from "./scenes/agent-bundle/CloseScene"; + +const SCENES = [ + IntroScene, + ExportScene, + TransferScene, + ApplyScene, + VerifyScene, + CloseScene, +]; + +export const RootAgentBundle: React.FC = () => { + const getFrom = (sceneIndex: number) => { + let from = 0; + for (let i = 0; i < sceneIndex; i++) { + from += AB_SCENES[i].durationInFrames; + } + return from; + }; + + return ( + <> + {SCENES.map((SceneComponent, i) => ( + + + + ))} + + ); +}; diff --git a/remotion/Root.tsx b/remotion/Root.tsx index a603c41..387787e 100644 --- a/remotion/Root.tsx +++ b/remotion/Root.tsx @@ -6,6 +6,13 @@ import { TOTAL_DURATION, FPS, WIDTH, HEIGHT } from "./data/script"; import { RootLaunchV2 } from "./CompositionV2"; import { RootLaunchV2Short } from "./CompositionV2Short"; import { V2_TOTAL_DURATION, V2_SHORT_TOTAL_DURATION } from "./data/script-v2"; +import { RootAgentBundle } from "./CompositionAgentBundle"; +import { + AB_TOTAL_DURATION, + AB_FPS, + AB_WIDTH, + AB_HEIGHT, +} from "./data/script-agent-bundle"; export const RemotionRoot: React.FC = () => { return ( @@ -42,6 +49,14 @@ export const RemotionRoot: React.FC = () => { width={WIDTH} height={HEIGHT} /> + ); }; diff --git a/remotion/components/GhosttyTerminal.tsx b/remotion/components/GhosttyTerminal.tsx new file mode 100644 index 0000000..69401b4 --- /dev/null +++ b/remotion/components/GhosttyTerminal.tsx @@ -0,0 +1,290 @@ +import React from "react"; +import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; + +export type TermPart = { + text: string; + color?: string; +}; + +export type TermLine = { + delay: number; // frame when this line starts + type: "command" | "output" | "success" | "blank"; + text: string; + parts?: TermPart[]; // optional colored segments for output/success +}; + +export interface GhosttyTerminalProps { + lines: TermLine[]; + width?: number; // default 1080 + height?: number; // default 560 + title?: string; // default "root-demo" + user?: string; + host?: string; + typingSpeed?: number; // frames per character for commands, default 1 + showCursor?: boolean; // default true + fontSize?: number; // default 22 +} + +const TRAFFIC_LIGHTS = ["#ff5f57", "#febc2e", "#28c840"] as const; + +const COLOR = { + user: "#d97706", + host: "#60a5fa", + muted: "#737373", + command: "#f5f5f5", + output: "#a3a3a3", + success: "#e5e5e5", + check: "#4ade80", + cursor: "#fafafa", +} as const; + +const FONT_MONO = "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace"; +const FONT_TITLE = "'JetBrains Mono', 'SF Mono', monospace"; + +const Prompt: React.FC<{ user: string; host: string }> = ({ user, host }) => ( + <> + {user} + @ + {host} + ~ % + +); + +const CursorBlock: React.FC<{ fontSize: number }> = ({ fontSize }) => ( + +); + +const renderParts = (parts: TermPart[], fallback: string) => + parts.map((part, i) => ( + + {part.text} + + )); + +const commandDoneFrame = (line: TermLine, typingSpeed: number): number => + line.delay + line.text.length * typingSpeed; + +const lineDoneFrame = (line: TermLine, typingSpeed: number): number => + line.type === "command" ? commandDoneFrame(line, typingSpeed) : line.delay; + +export const GhosttyTerminal: React.FC = ({ + lines, + width = 1080, + height = 560, + title = "root-demo", + user = "sergio", + host = "mbp-old", + typingSpeed = 1, + showCursor = true, + fontSize = 22, +}) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const progress = spring({ + frame, + fps, + config: { damping: 20, stiffness: 90, mass: 0.5 }, + }); + + const opacity = interpolate(progress, [0, 1], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + const scale = interpolate(progress, [0, 1], [0.97, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + const blinkOn = Math.floor(frame / 10) % 2 === 0; + const lineHeightPx = fontSize * 1.65; + + const cursorLineIndex = ((): number | null => { + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.type !== "command" || frame < line.delay) { + continue; + } + const typingDone = frame >= commandDoneFrame(line, typingSpeed); + if (!typingDone) { + return i; + } + const nextVisible = lines + .slice(i + 1) + .find((candidate) => candidate.type !== "blank"); + if (nextVisible && frame < nextVisible.delay) { + return i; + } + } + return null; + })(); + + const last = lines.at(-1); + const allLinesDone = + last === undefined || frame >= lineDoneFrame(last, typingSpeed); + const showTrailingPrompt = allLinesDone && cursorLineIndex === null; + + const typedCommandText = (line: TermLine): string => { + const elapsed = frame - line.delay; + const charCount = Math.floor(elapsed / typingSpeed); + return line.text.slice(0, Math.max(0, charCount)); + }; + + const renderSuccess = (line: TermLine) => { + if (line.parts) { + return renderParts(line.parts, COLOR.success); + } + if (line.text.startsWith("✓")) { + return ( + <> + ✓ + {line.text.slice(1)} + + ); + } + return {line.text}; + }; + + const renderLineBody = (line: TermLine, index: number) => { + const cursorHere = + showCursor && blinkOn && cursorLineIndex === index ? ( + + ) : null; + + switch (line.type) { + case "command": + return ( + <> + + {typedCommandText(line)} + {cursorHere} + + ); + case "output": + return line.parts ? ( + renderParts(line.parts, COLOR.output) + ) : ( + {line.text} + ); + case "success": + return renderSuccess(line); + case "blank": + return null; + } + }; + + return ( +
+
+
+
+ {TRAFFIC_LIGHTS.map((color) => ( +
+ ))} +
+
+ {title} +
+
+ +
+ {lines.map((line, i) => { + if (frame < line.delay) { + return null; + } + return ( +
+ {renderLineBody(line, i)} +
+ ); + })} + {showTrailingPrompt ? ( +
+ + {showCursor && blinkOn ? : null} +
+ ) : null} +
+
+
+ ); +}; diff --git a/remotion/data/script-agent-bundle.ts b/remotion/data/script-agent-bundle.ts new file mode 100644 index 0000000..075d5ae --- /dev/null +++ b/remotion/data/script-agent-bundle.ts @@ -0,0 +1,22 @@ +export interface AgentBundleSceneConfig { + id: string; + durationInFrames: number; +} + +export const AB_FPS = 30; +export const AB_WIDTH = 1920; +export const AB_HEIGHT = 1080; + +export const AB_SCENES: AgentBundleSceneConfig[] = [ + { id: "Intro", durationInFrames: 75 }, + { id: "Export", durationInFrames: 150 }, + { id: "Transfer", durationInFrames: 60 }, + { id: "Apply", durationInFrames: 165 }, + { id: "Verify", durationInFrames: 75 }, + { id: "Close", durationInFrames: 75 }, +]; + +export const AB_TOTAL_DURATION = AB_SCENES.reduce( + (sum, scene) => sum + scene.durationInFrames, + 0 +); diff --git a/remotion/package.json b/remotion/package.json index ca95834..7e4e95a 100644 --- a/remotion/package.json +++ b/remotion/package.json @@ -1,12 +1,13 @@ { "name": "root-launch-video", "version": "0.1.0", - "description": "Root v0.1.9 launch video", + "description": "Root launch and agent-bundle demo videos", "private": true, "type": "module", "scripts": { "studio": "remotion studio index.ts", - "render": "remotion render index.ts" + "render": "remotion render index.ts", + "render:agent-bundle": "remotion render index.ts RootAgentBundle out/agent-bundle.mp4" }, "dependencies": { "@remotion/cli": "^4.0.0", diff --git a/remotion/scenes/agent-bundle/ApplyScene.tsx b/remotion/scenes/agent-bundle/ApplyScene.tsx new file mode 100644 index 0000000..8005f9f --- /dev/null +++ b/remotion/scenes/agent-bundle/ApplyScene.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { AbsoluteFill } from "remotion"; +import { theme } from "../../styles/theme"; +import { GhosttyTerminal, TermLine } from "../../components/GhosttyTerminal"; +import { MachineTag, SceneCaption } from "./SceneCaption"; + +const LINES: TermLine[] = [ + { + delay: 8, + type: "command", + text: "root agent-bundle apply ./root-agent-bundle", + }, + { delay: 40, type: "blank", text: "" }, + { delay: 44, type: "output", text: "→ unpacking agent environment..." }, + { delay: 56, type: "output", text: "→ restoring:" }, + { delay: 64, type: "output", text: " • Codex config" }, + { delay: 72, type: "output", text: " • Claude config" }, + { delay: 80, type: "output", text: " • OpenCode config" }, + { delay: 92, type: "blank", text: "" }, + { delay: 96, type: "output", text: "→ verifying restored files..." }, + { delay: 112, type: "success", text: "✓ apply complete" }, +]; + +export const ApplyScene: React.FC = () => { + return ( + +
+ + + +
+
+ ); +}; diff --git a/remotion/scenes/agent-bundle/CloseScene.tsx b/remotion/scenes/agent-bundle/CloseScene.tsx new file mode 100644 index 0000000..c9c2b07 --- /dev/null +++ b/remotion/scenes/agent-bundle/CloseScene.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { AbsoluteFill } from "remotion"; +import { theme } from "../../styles/theme"; +import { FadeIn } from "../../components/FadeIn"; + +export const CloseScene: React.FC = () => { + return ( + +
+ +

+ Root +

+
+ + +

+ move your agent setup between machines +

+
+ + +

+ github.com/BoringInfraCo/Root +

+
+ + +

+ Codex / Claude / OpenCode +

+
+
+
+ ); +}; diff --git a/remotion/scenes/agent-bundle/ExportScene.tsx b/remotion/scenes/agent-bundle/ExportScene.tsx new file mode 100644 index 0000000..b9798d8 --- /dev/null +++ b/remotion/scenes/agent-bundle/ExportScene.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { AbsoluteFill } from "remotion"; +import { theme } from "../../styles/theme"; +import { GhosttyTerminal, TermLine } from "../../components/GhosttyTerminal"; +import { MachineTag, SceneCaption } from "./SceneCaption"; + +const LINES: TermLine[] = [ + { delay: 8, type: "command", text: "root agent-bundle export" }, + { delay: 34, type: "blank", text: "" }, + { delay: 38, type: "output", text: "→ scanning local agent configs..." }, + { delay: 50, type: "output", text: "→ found:" }, + { delay: 56, type: "output", text: " • Codex" }, + { delay: 62, type: "output", text: " • Claude" }, + { delay: 68, type: "output", text: " • OpenCode" }, + { delay: 80, type: "blank", text: "" }, + { delay: 90, type: "output", text: "→ bundling agent environment..." }, + { delay: 114, type: "success", text: "✓ created ./root-agent-bundle" }, +]; + +export const ExportScene: React.FC = () => { + return ( + +
+ + + +
+
+ ); +}; diff --git a/remotion/scenes/agent-bundle/IntroScene.tsx b/remotion/scenes/agent-bundle/IntroScene.tsx new file mode 100644 index 0000000..b99dd35 --- /dev/null +++ b/remotion/scenes/agent-bundle/IntroScene.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion"; +import { theme } from "../../styles/theme"; +import { FadeIn } from "../../components/FadeIn"; + +export const IntroScene: React.FC = () => { + const frame = useCurrentFrame(); + const blinkOn = Math.floor(frame / 12) % 2 === 0; + const drift = interpolate(frame, [0, 75], [8, -4], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + return ( + +
+ +

+ new machine. same agent setup. + +

+
+ + +

+ move your codex / claude / opencode setup with Root +

+
+
+
+ ); +}; diff --git a/remotion/scenes/agent-bundle/SceneCaption.tsx b/remotion/scenes/agent-bundle/SceneCaption.tsx new file mode 100644 index 0000000..2c36fe7 --- /dev/null +++ b/remotion/scenes/agent-bundle/SceneCaption.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { theme } from "../../styles/theme"; +import { FadeIn } from "../../components/FadeIn"; + +interface SceneCaptionProps { + text: string; + delay?: number; + size?: "sm" | "md"; +} + +export const SceneCaption: React.FC = ({ + text, + delay = 0, + size = "md", +}) => ( + +

+ {text} +

+
+); + +interface MachineTagProps { + label: string; + delay?: number; +} + +export const MachineTag: React.FC = ({ label, delay = 0 }) => ( + +
+ + {label} +
+
+); diff --git a/remotion/scenes/agent-bundle/TransferScene.tsx b/remotion/scenes/agent-bundle/TransferScene.tsx new file mode 100644 index 0000000..a0840d7 --- /dev/null +++ b/remotion/scenes/agent-bundle/TransferScene.tsx @@ -0,0 +1,150 @@ +import React from "react"; +import { + AbsoluteFill, + interpolate, + spring, + useCurrentFrame, + useVideoConfig, +} from "remotion"; +import { theme } from "../../styles/theme"; +import { FadeIn } from "../../components/FadeIn"; + +export const TransferScene: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const slide = spring({ + frame: frame - 6, + fps, + config: { damping: 16, stiffness: 70, mass: 0.8 }, + }); + + const cardLeft = interpolate(slide, [0, 1], [40, 700], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + const cardOpacity = interpolate(slide, [0, 0.12, 1], [0, 1, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + return ( + +
+ +

+ move bundle to new machine +

+
+ +
+
+ + + old machine + + + + + new machine + + +
+ +
+
+
+ + + ./root-agent-bundle + +
+
+
+
+
+
+ ); +}; diff --git a/remotion/scenes/agent-bundle/VerifyScene.tsx b/remotion/scenes/agent-bundle/VerifyScene.tsx new file mode 100644 index 0000000..58dcd7d --- /dev/null +++ b/remotion/scenes/agent-bundle/VerifyScene.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import { AbsoluteFill } from "remotion"; +import { theme } from "../../styles/theme"; +import { GhosttyTerminal, TermLine } from "../../components/GhosttyTerminal"; +import { SceneCaption } from "./SceneCaption"; + +const LINES: TermLine[] = [ + { delay: 4, type: "command", text: "root agent-bundle verify" }, + { delay: 29, type: "blank", text: "" }, + { + delay: 32, + type: "output", + text: "Agent environments", + parts: [{ text: "Agent environments", color: "#e5e5e5" }], + }, + { + delay: 35, + type: "output", + text: "──────────────────", + parts: [{ text: "──────────────────", color: "#737373" }], + }, + { + delay: 38, + type: "output", + text: "Codex ✓ configured", + parts: [ + { text: "Codex ", color: "#a3a3a3" }, + { text: "✓", color: "#4ade80" }, + { text: " configured", color: "#e5e5e5" }, + ], + }, + { + delay: 42, + type: "output", + text: "Claude ✓ configured", + parts: [ + { text: "Claude ", color: "#a3a3a3" }, + { text: "✓", color: "#4ade80" }, + { text: " configured", color: "#e5e5e5" }, + ], + }, + { + delay: 46, + type: "output", + text: "OpenCode ✓ configured", + parts: [ + { text: "OpenCode ", color: "#a3a3a3" }, + { text: "✓", color: "#4ade80" }, + { text: " configured", color: "#e5e5e5" }, + ], + }, +]; + +export const VerifyScene: React.FC = () => { + return ( + +
+ + +
+
+ ); +};