/* Terminals: a generic shell that boots an (original-styled) AI agent session via `claude`. */ const wait = (ms) => new Promise((r) => setTimeout(r, ms)); let _lid = 0; const lid = () => ++_lid; function Terminal({ kind, seed }) { const [lines, setLines] = useState(seed.lines); const [mode, setMode] = useState(seed.mode); const [input, setInput] = useState(""); const [busy, setBusy] = useState(false); const bodyRef = useRef(null); const inputRef = useRef(null); const hist = useRef([]); const histIx = useRef(-1); useEffect(() => { const el = bodyRef.current; if (el) el.scrollTop = el.scrollHeight; }, [lines, busy]); useEffect(() => { if (kind !== "agent") return; const h = (e) => { if (mode !== "agent") { pushMany(bootAgent()); setMode("agent"); } // bracketed-paste semantics: append as a new, UNSUBMITTED line; leave caret on a fresh line setInput((prev) => { const base = prev.replace(/\n+$/, ""); return (base ? base + "\n" : "") + e.detail + "\n"; }); requestAnimationFrame(() => { const el = inputRef.current; if (el) { el.focus(); const v = el.value.length; el.setSelectionRange(v, v); } }); }; window.addEventListener("agentPaste", h); return () => window.removeEventListener("agentPaste", h); }, [kind, mode]); const push = (line) => setLines((p) => [...p, { id: lid(), ...line }]); const pushMany = (arr) => setLines((p) => [...p, ...arr.map((l) => ({ id: lid(), ...l }))]); async function runAgent(prompt) { setBusy(true); push({ kind: "agent-think", html: ` thinking…` }); await wait(520); setLines((p) => p.slice(0, -1)); // drop the thinking line push({ kind: "t", cls: "", html: ` I'll take a look at the relevant files first.` }); await wait(380); push({ kind: "t", cls: "dim", html: ` read src/Http/Controller/UserController.php` }); await wait(300); push({ kind: "t", cls: "dim", html: ` grep "balanceFor" → 2 matches` }); await wait(420); push({ kind: "t", cls: "", html: ` Adding the field and a fillable allow-list. Editing now.` }); await wait(360); push({ kind: "card", ch: `edit · src/Http/Controller/UserController.php`, rows: [ { cls: "del", t: "- 'plan' => $user->plan," }, { cls: "add", t: "+ 'plan' => $user->plan->value," }, { cls: "add", t: "+ 'currency' => $user->currency," }, ], }); await wait(450); push({ kind: "t", cls: "ok", html: ` Done. Updated UserController.php. Run tests with composer test.` }); setBusy(false); requestAnimationFrame(() => inputRef.current && inputRef.current.focus()); } function shell(cmd) { const [c, ...rest] = cmd.split(/\s+/); const arg = rest.join(" "); switch (c) { case "": return; case "claude": pushMany(bootAgent()); setMode("agent"); return; case "clear": setLines([]); return; case "help": pushMany([ { kind: "t", cls: "dim", html: "commands: claude ls pwd cat <file> git status git diff echo clear" }, ]); return; case "pwd": push({ kind: "t", html: "/Users/dev/console" }); return; case "ls": push({ kind: "t", html: "config public scripts src templates composer.json package.json README.md" }); return; case "echo": push({ kind: "t", html: HL.escapeHtml(arg) }); return; case "git": if (rest[0] === "status") { pushMany([ { kind: "t", cls: "dim", html: `On branch ${PROJECT.branch}` }, { kind: "t", cls: "dim", html: "Changes to be committed:" }, ...PROJECT.changes.map((ch) => ({ kind: "t", html: ` ${ch.status === 'A' ? 'new file' : ch.status === 'D' ? 'deleted ' : 'modified'} ${ch.path}`, })), ]); } else if (rest[0] === "diff") { pushMany([ { kind: "t", cls: "dim", html: "diff --git a/public/assets/app.js b/public/assets/app.js" }, { kind: "t", cls: "err", html: "- const res = await fetch('/api/session');" }, { kind: "t", cls: "ok", html: "+ const res = await fetch('/api/session', { credentials: 'include' });" }, ]); } else push({ kind: "t", cls: "dim", html: `git: '${rest[0] || ""}' is not handled in this demo` }); return; case "cat": { const f = PROJECT.files[arg] || Object.entries(PROJECT.files).find(([p]) => p.endsWith(arg))?.[1]; if (!f) { push({ kind: "t", cls: "err", html: `cat: ${HL.escapeHtml(arg)}: No such file` }); return; } pushMany(f.replace(/\n$/, "").split("\n").slice(0, 24).map((l) => ({ kind: "t", cls: "dim", html: HL.escapeHtml(l) || " " }))); return; } default: push({ kind: "t", cls: "err", html: `${HL.escapeHtml(c)}: command not found` }); } } function submit() { const cmd = input.trim(); if (busy) return; if (cmd) { hist.current.unshift(cmd); } histIx.current = -1; if (mode === "agent") { if (cmd === "/exit" || cmd === "exit") { push({ kind: "t", cls: "dim", html: " Session ended." }); setMode("shell"); setInput(""); return; } if (cmd === "/clear" || cmd === "clear") { setLines([]); setInput(""); return; } push({ kind: "t", html: `> ${HL.escapeHtml(cmd).replace(/\n/g, "
  ") || " "}` }); setInput(""); if (cmd) runAgent(cmd); return; } push({ kind: "t", html: `console % ${HL.escapeHtml(cmd) || " "}` }); setInput(""); shell(cmd); } function onKey(e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); return; } if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !input.includes("\n")) { e.preventDefault(); if (e.key === "ArrowUp") { if (hist.current.length) { histIx.current = Math.min(histIx.current + 1, hist.current.length - 1); setInput(hist.current[histIx.current]); } } else { if (histIx.current > 0) { histIx.current--; setInput(hist.current[histIx.current]); } else { histIx.current = -1; setInput(""); } } } } const live = mode === "agent"; return (
inputRef.current && inputRef.current.focus()}>
{live ? "claude" : kind === "agent" ? "claude" : "zsh"} {live ? "agent session" : "— bash · ~/console"}
{lines.map((l) => { if (l.kind === "card") { return (
{Icon.diff({ width: 11, height: 11 })}{l.ch}
{l.rows.map((r, i) => (
{r.t}
))}
); } if (l.kind === "welcome") { return (
◇ agent session
{l.text}
); } return
; })} {busy &&
working
} {!busy && (
{live ? ">" : console %}