/* Director Console (hero) — transcript + command engine + autocomplete. */ import { useEffect, useRef, useState, type MutableRefObject } from "react"; import { Icon } from "./Icon"; import { boldHtml, highlightArgs } from "../state/util"; import { VERBS, parse } from "../state/sceneTool"; import { parseCliResult, runSceneCommand } from "../state/backend"; import type { ComposerMode, CmdTurn, Turn } from "../state/types"; /** Imperative handle so the palette can run a command in the console. */ export interface ConsoleApi { /** Run a command; `forceCommand` runs it as a raw scene-tool command regardless of mode. */ run?: (text: string, forceCommand?: boolean) => void; } // Quick-insert examples — each one is the REAL CLI grammar (animation-scene.ts) and runs as-is. const CHIPS = [ 'cast add --id pip --query "robot kid"', "shot retime --id shot-2 --duration 30", "camera --shot shot-1 --preset close-up", "gesture --character pip --shot shot-1 --clip wave" ]; /** The portion of a command after its (possibly two-word) verb — the card's args line. */ function argTail(raw: string, verb: string): string { const t = raw.trim(); return verb && t.toLowerCase().startsWith(verb.toLowerCase()) ? t.slice(verb.length).trim() : ""; } function CmdCard({ turn }: { turn: CmdTurn }) { return (
aura {turn.verb} {turn.state === "run" ? ( validating ) : turn.state === "bad" ? ( rejected ) : ( committed )}
{turn.state !== "run" && turn.diffs && (
{turn.diffs.map((d, i) => (
{d.op}
))}
)} {turn.state === "ok" && (
{"ran in " + turn.dur} {"doc @ " + turn.hash}
)}
); } function TurnView({ turn }: { turn: Turn }) { if (turn.type === "you") return (
{turn.text}
); if (turn.type === "dir") return (
Aura
); if (turn.type === "cmd") return (
); if (turn.type === "render") return (
{turn.label}
{turn.shot} {"· " + turn.meta}
); return null; } export interface ConsoleProps { transcript: Turn[]; setTranscript: React.Dispatch>; selShot: string | null; onRender: (scope: "shot" | "sequence") => void; /** Called after a committed Scene-Tool mutation so the app re-syncs with the real document. */ onSceneCommit: () => void; api: MutableRefObject; } export function Console({ transcript, setTranscript, selShot, onRender, onSceneCommit, api }: ConsoleProps) { const [mode, setMode] = useState("Prompt"); const [val, setVal] = useState(""); const [focus, setFocus] = useState(false); const [acIdx, setAcIdx] = useState(0); const scrollRef = useRef(null); const inputRef = useRef(null); useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [transcript]); const suggestions = (() => { const q = val.trim().toLowerCase(); if (!q) return VERBS.slice(0, 6); return VERBS.filter((v) => v.verb.startsWith(q.split(" ")[0]) || v.verb.includes(q)); })(); const push = (t: Turn) => setTranscript((p) => [...p, t]); const update = (id: string, patch: Partial) => setTranscript((p) => p.map((t) => (t.id === id && t.type === "cmd" ? { ...t, ...patch } : t))); // Command mode runs the raw scene-tool command against the REAL CLI (POST /api/scene); // Prompt mode shows the user's intent — their own coding agent drives the actual commands. // `render`/`render --shot` short-circuits to the real render pipeline. const run = (text: string, forceCommand = false) => { const raw = text.trim(); if (!raw) return; push({ type: "you", id: "u" + Date.now(), text: raw }); setVal(""); if (inputRef.current) inputRef.current.style.height = "auto"; const p = parse(raw); // Render verb → the real render pipeline (also reachable from the Render button). if (p.verb === "render") { const cmdId = "c" + Date.now(); const scope = p.flags.shot ? "shot" : "sequence"; push({ type: "cmd", id: cmdId, verb: "render", args: p.flags.shot ? "--shot " + p.flags.shot : "--scope sequence", state: "run" }); onRender(scope); update(cmdId, { state: "ok", diffs: [{ op: "~", k: "mod", t: "render queued · " + scope }], dur: "—", hash: "" }); return; } // Prompt mode: do NOT mutate. The directing agent (the user's coding agent) reads this // intent and runs the concrete scene-tool commands itself. We only echo the intent. // (The palette forces command execution, so it bypasses this.) if (mode === "Prompt" && !forceCommand) { window.setTimeout( () => push({ type: "dir", id: "d" + Date.now(), think: "Noted. Your coding agent is the director — it will translate this into validated **Scene-Tool** commands. " + "Switch to **Command** mode to run a raw command here against the working document." }), 300 ); return; } // Command mode: run the REAL CLI and render the committed / rejected card. const cmdId = "c" + Date.now(); push({ type: "cmd", id: cmdId, verb: p.verb || raw.split(/\s+/)[0], args: argTail(raw, p.verb), state: "run" }); void runSceneCommand(raw).then((res) => { const { diffs } = parseCliResult(raw, res); if (!res.ok || res.rejected) { update(cmdId, { state: "bad", diffs }); return; } update(cmdId, { state: "ok", diffs, dur: res.ms ? (res.ms / 1000).toFixed(1) + "s" : "—", hash: res.hash || "—" }); // Re-sync the panels with the now-mutated working document. onSceneCommit(); }); }; useEffect(() => { api.current.run = run; }); const onKey = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); run(val); setAcIdx(0); return; } if (focus && suggestions.length) { if (e.key === "ArrowDown") { e.preventDefault(); setAcIdx((i) => Math.min(suggestions.length - 1, i + 1)); } if (e.key === "ArrowUp") { e.preventDefault(); setAcIdx((i) => Math.max(0, i - 1)); } if (e.key === "Tab") { e.preventDefault(); const s = suggestions[acIdx]; setVal(s.verb + " "); inputRef.current?.focus(); } } }; const showAc = focus && mode === "Command" && suggestions.length > 0; return (
Edit this scene
ask the AI in plain English, or type an exact command
{transcript.map((t) => ( ))}
{CHIPS.map((c) => ( ))}
{showAc && (
Scene-Tool commands
{suggestions.map((s, i) => (
setAcIdx(i)} onMouseDown={(e) => { e.preventDefault(); setVal(s.verb + " "); inputRef.current?.focus(); }} > {s.verb} {s.tail} {s.desc} {i === acIdx && tab}
))}
)}
{(["Prompt", "Command"] as ComposerMode[]).map((m) => ( ))}
{mode === "Prompt" ? "plain English · your AI agent runs it" : "exact command · runs now"}
{mode === "Command" && (
Top commands — click one to use it
{VERBS.map((v) => ( ))}
)}