import { useCurrentFrame } from "remotion";
// ─── Design tokens (from DESIGN.md dark mode) ───
const C = {
bg: "#09090B",
surface: "#18181B",
border: "#27272A",
text: "#FAFAFA",
dim: "#71717A",
secondary: "#A1A1AA",
accent: "#22D3EE",
success: "#22C55E",
warning: "#EAB308",
error: "#EF4444",
orange: "#F97316",
} as const;
const FONT = "'Geist Mono', 'SF Mono', 'Cascadia Code', monospace";
// ─── Event types ───
export type TerminalEvent =
| { type: "type"; text: string; at: number; duration: number }
| { type: "print"; text: string; at: number; color?: string }
| { type: "blank"; at: number };
// ─── Auto-scroll container ───
const LINE_H = 26; // matches lineHeight
function AutoScroll({ lineCount, children }: { lineCount: number; children: React.ReactNode }) {
// Estimate visible lines from a reference height (1080 - 60*2 padding - 40 title bar - 48 body pad)
// For split-screen the container is smaller, but overflow: hidden on parent clips it anyway.
// We use a generous max visible count; the translateY just pins the bottom.
const MAX_VISIBLE = 30;
const overflow = lineCount - MAX_VISIBLE;
const scrollY = overflow > 0 ? overflow * LINE_H : 0;
return
{children}
;
}
// ─── Helpers ───
function Prompt() {
return $ ;
}
function Cursor({ visible }: { visible: boolean }) {
return (
);
}
function colorize(text: string, color?: string): React.ReactNode {
if (color) return {text};
// Auto-colorize patterns
return text.split(/(\s+)/).map((token, i) => {
// Checkmark
if (token === "\u2713")
return (
{token}
);
// Priority tags
if (token === "[urgent]")
return (
{token}
);
if (token === "[high]")
return (
{token}
);
if (token === "[medium]")
return (
{token}
);
if (token === "[low]")
return (
{token}
);
// Arrow for agent assignment
if (token.startsWith("\u2192"))
return (
{token}
);
// Log level tags
if (token === "[info]")
return (
{token}
);
// IDs and special values
if (/^[bmatrs]_/.test(token))
return (
{token}
);
return {token};
});
}
// ─── Terminal ───
interface TerminalProps {
events: TerminalEvent[];
title?: string;
}
export function Terminal({ events, title }: TerminalProps) {
const frame = useCurrentFrame();
const lines: React.ReactNode[] = [];
for (const ev of events) {
if (ev.at > frame) continue;
if (ev.type === "blank") {
lines.push();
continue;
}
if (ev.type === "print") {
lines.push(
{colorize(ev.text, ev.color)}
,
);
continue;
}
if (ev.type === "type") {
const elapsed = frame - ev.at;
const progress = Math.min(elapsed / ev.duration, 1);
const chars = Math.floor(progress * ev.text.length);
const visible = ev.text.slice(0, chars);
const isDone = progress >= 1;
lines.push(
,
);
}
}
// If no active typing, show blinking cursor on last line
const lastEvent = events.filter((e) => e.at <= frame).at(-1);
const shouldShowIdleCursor = lastEvent && lastEvent.type !== "type" && frame - lastEvent.at > 5;
return (
{/* Title bar */}
{/* Terminal body — auto-scrolls when content exceeds viewport */}
{lines}
{shouldShowIdleCursor && (
)}
);
}