import { useCurrentFrame } from "remotion"; // ─── Design tokens ─── const C = { bg: "#09090B", surface: "#111113", border: "#27272A", text: "#FAFAFA", dim: "#71717A", secondary: "#A1A1AA", accent: "#22D3EE", success: "#22C55E", orange: "#F97316", warning: "#EAB308", error: "#EF4444", purple: "#C084FC", inputBg: "#18181B", } as const; const FONT = "'Geist', system-ui, sans-serif"; const MONO = "'Geist Mono', 'SF Mono', monospace"; const LINE_H = 26; // ─── Event types ─── export type ClaudeEvent = | { type: "user"; text: string; at: number; duration?: number } | { type: "thinking"; text: string; at: number } | { type: "text"; text: string; at: number; color?: string } | { type: "tool"; command: string; output?: string; at: number } | { type: "blank"; at: number }; // ─── Claude Code Terminal ─── export function ClaudeTerminal({ events }: { events: ClaudeEvent[] }) { const frame = useCurrentFrame(); // Separate user input (goes to bottom bar) from conversation (goes to main area) // Find the active user input event (the latest "user" event being typed or just finished) const activeUserEvents = events.filter((e) => e.type === "user" && e.at <= frame); const latestUserEvent = activeUserEvents.at(-1) as (ClaudeEvent & { type: "user" }) | undefined; // Is the user currently typing? let inputText = ""; let inputCursorVisible = false; if (latestUserEvent) { const duration = latestUserEvent.duration ?? 30; const elapsed = frame - latestUserEvent.at; const progress = Math.min(elapsed / duration, 1); const chars = Math.floor(progress * latestUserEvent.text.length); inputText = latestUserEvent.text.slice(0, chars); inputCursorVisible = progress < 1; // After typing is done + a short delay, input was "sent" — clear it if (elapsed > duration + 15) { inputText = ""; inputCursorVisible = false; } } // Build conversation lines (everything except user typing state) const lines: React.ReactNode[] = []; for (const ev of events) { if (ev.at > frame) continue; if (ev.type === "blank") { lines.push(
); continue; } if (ev.type === "user") { // Show as sent message once typing is complete + delay const duration = ev.duration ?? 30; const elapsed = frame - ev.at; if (elapsed <= duration + 15) continue; // still typing, shown in input bar lines.push(
Y
{ev.text}
, ); continue; } if (ev.type === "thinking") { lines.push(
{ev.text}
, ); continue; } if (ev.type === "text") { lines.push(
{colorize(ev.text, ev.color)}
, ); continue; } if (ev.type === "tool") { lines.push(
{ev.command}
{ev.output &&
{ev.output}
}
, ); } } // Auto-scroll const totalHeight = lines.length * (LINE_H + 2); const viewportHeight = 820; // approximate: 1080 - padding - header - input bar const scrollY = Math.max(0, totalHeight - viewportHeight); return (
{/* Header */}
Claude Code
opus
vtit-agent-coding.dev
{/* Conversation area */}
{lines}
{/* Input bar at bottom */}
{inputText ? ( <> {inputText} {inputCursorVisible && } ) : ( Type a message... )}
); } // ─── Helpers ─── function InputCursor() { return ( ); } function Spinner({ frame }: { frame: number }) { const chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; const idx = Math.floor(frame / 3) % chars.length; return {chars[idx]}; } function ClaudeIcon() { return (
C
); } function ToolIcon() { return ( ); } function colorize(text: string, color?: string): React.ReactNode { if (color) return {text}; return text.split(/(\s+)/).map((token, i) => { if (token === "✓") return ( {token} ); if (token === "[urgent]") return ( {token} ); if (token === "[high]") return ( {token} ); if (token === "[medium]") return ( {token} ); if (token === "[low]") return ( {token} ); if (token.startsWith("→")) return ( {token} ); if (/^[bmatrs]_/.test(token)) return ( {token} ); return {token}; }); }