/* 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 (