/* Terminals: a generic shell that boots an (original-styled) AI agent session via `claude`. */ const { ref, computed, onMounted, onUnmounted, watch, nextTick } = Vue; const wait = (ms) => new Promise((r) => setTimeout(r, ms)); let _lid = 0; const lid = () => ++_lid; function bootAgent() { return [ { kind: "welcome", text: "model: opus · cwd: ~/console · branch: " + PROJECT.branch + " · type /exit to leave" }, { kind: "t", cls: "dim", html: " I can read, search and edit files in this project. Describe a change to get started." }, ]; } function agentSeed() { return { mode: "agent", lines: [ { id: lid(), kind: "t", html: `console % claude` }, { id: lid(), kind: "welcome", text: "model: opus · cwd: ~/console · branch: " + PROJECT.branch + " · type /exit to leave" }, { id: lid(), kind: "t", html: `> add currency + name to the user payload and gate the fillable fields` }, { id: lid(), kind: "t", html: ` Read UserController.php, edited the response array and onlyFillable().` }, { id: lid(), 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: "+ 'name' => $user->name," }, ], }, { id: lid(), kind: "t", cls: "ok", html: ` Done — updated UserController.php. Anything else?` }, ], }; } function shellSeed() { return { mode: "shell", lines: [ { id: lid(), kind: "t", html: `console % git status -s` }, ...PROJECT.changes.map((c) => ({ id: lid(), kind: "t", html: `${c.status} ${c.path}`, })), ], }; } const Terminal = { props: ["kind", "seed"], setup(props) { const lines = ref(props.seed.lines); const mode = ref(props.seed.mode); const input = ref(""); const busy = ref(false); const bodyRef = ref(null); const inputRef = ref(null); const hist = ref([]); const histIx = ref(-1); // Auto-scroll on lines/busy change watch([lines, busy], () => nextTick(() => { const el = bodyRef.value; if (el) el.scrollTop = el.scrollHeight; })); const push = (line) => { lines.value = [...lines.value, { id: lid(), ...line }]; }; const pushMany = (arr) => { lines.value = [...lines.value, ...arr.map((l) => ({ id: lid(), ...l }))]; }; async function runAgent(prompt) { busy.value = true; push({ kind: "agent-think", html: ` thinking…` }); await wait(520); lines.value = lines.value.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.` }); busy.value = false; requestAnimationFrame(() => inputRef.value && inputRef.value.focus()); } function shell(cmd) { const [c, ...rest] = cmd.split(/\s+/); const arg = rest.join(" "); switch (c) { case "": return; case "claude": pushMany(bootAgent()); mode.value = "agent"; return; case "clear": lines.value = []; 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.value.trim(); if (busy.value) return; if (cmd) { hist.value.unshift(cmd); } histIx.value = -1; if (mode.value === "agent") { if (cmd === "/exit" || cmd === "exit") { push({ kind: "t", cls: "dim", html: " Session ended." }); mode.value = "shell"; input.value = ""; return; } if (cmd === "/clear" || cmd === "clear") { lines.value = []; input.value = ""; return; } push({ kind: "t", html: `> ${HL.escapeHtml(cmd).replace(/\n/g, "
  ") || " "}` }); input.value = ""; if (cmd) runAgent(cmd); return; } push({ kind: "t", html: `console % ${HL.escapeHtml(cmd) || " "}` }); input.value = ""; shell(cmd); } function onKey(e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); return; } if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !input.value.includes("\n")) { e.preventDefault(); if (e.key === "ArrowUp") { if (hist.value.length) { histIx.value = Math.min(histIx.value + 1, hist.value.length - 1); input.value = hist.value[histIx.value]; } } else { if (histIx.value > 0) { histIx.value--; input.value = hist.value[histIx.value]; } else { histIx.value = -1; input.value = ""; } } } } // agentPaste CustomEvent listener — agent kind only let agentPasteHandler = null; onMounted(() => { if (props.kind !== "agent") return; agentPasteHandler = (e) => { if (mode.value !== "agent") { pushMany(bootAgent()); mode.value = "agent"; } // bracketed-paste semantics: append as a new, UNSUBMITTED line; leave caret on a fresh line const base = input.value.replace(/\n+$/, ""); input.value = (base ? base + "\n" : "") + e.detail + "\n"; requestAnimationFrame(() => { const el = inputRef.value; if (el) { el.focus(); const v = el.value.length; el.setSelectionRange(v, v); } }); }; window.addEventListener("agentPaste", agentPasteHandler); }); onUnmounted(() => { if (agentPasteHandler) window.removeEventListener("agentPaste", agentPasteHandler); }); const live = computed(() => mode.value === "agent"); const inputRows = computed(() => Math.min(8, Math.max(1, input.value.split("\n").length))); const inputPlaceholder = computed(() => live.value ? "Ask the agent to change something…" : props.kind === "agent" ? "type 'claude' to start a session" : "" ); return { lines, mode, input, busy, bodyRef, inputRef, live, inputRows, inputPlaceholder, onKey, submit }; }, template: `
{{ live ? 'claude' : kind === 'agent' ? 'claude' : 'zsh' }} {{ live ? 'agent session' : '— bash · ~/console' }}
working
`, }; // Expose globals for app.js and other files window.wait = wait; window.lid = lid; window.bootAgent = bootAgent; window.agentSeed = agentSeed; window.shellSeed = shellSeed; window.HelderComponents = Object.assign(window.HelderComponents || {}, { Terminal });