import { createPiAgent, DEFAULT_LOCAL_MODEL_KEY, DEFAULT_MAX_NEW_TOKENS, LOCAL_MODELS } from "./piAgent.js"; import { PI_BUILTIN_TOOLS, PI_CLI_VERSION, PI_DEFAULT_TOOLS, PI_HELP_TEXT, PI_SLASH_COMMANDS, formatHotkeys } from "./piCliContract.js"; const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g; function stripAnsi(text) { return String(text).replace(ANSI_PATTERN, ""); } function textFromContent(content) { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content .filter((part) => part.type === "text") .map((part) => part.text) .join("\n"); } function toolCallsFromMessage(message) { return Array.isArray(message.content) ? message.content.filter((part) => part.type === "toolCall") : []; } function formatToolCall(call) { const args = call.arguments || {}; switch (call.name) { case "bash": return `$ ${args.command || ""}${args.timeout ? ` (timeout ${args.timeout}s)` : ""}`; case "write": return `write ${args.path || ""} (${String(args.content || "").length} bytes)`; case "read": return `read ${args.path || ""}${args.offset ? `:${args.offset}` : ""}${args.limit ? ` limit=${args.limit}` : ""}`; case "edit": return `edit ${args.path || ""} (${Array.isArray(args.edits) ? args.edits.length : 0} replacement(s))`; case "grep": return `grep ${args.pattern || ""} ${args.path || "."}`; case "find": return `find ${args.pattern || ""} ${args.path || "."}`; case "ls": return `ls ${args.path || "."}`; default: return `${call.name} ${JSON.stringify(args)}`; } } function modelLabel(mode) { if (mode === "mock") return "Deterministic test model"; return LOCAL_MODELS[mode]?.id || LOCAL_MODELS[DEFAULT_LOCAL_MODEL_KEY].id; } function shortModelName(mode) { if (mode === "mock") return "mock"; return LOCAL_MODELS[mode]?.name || LOCAL_MODELS[DEFAULT_LOCAL_MODEL_KEY].name; } export function createPiCli({ terminal, sandbox, nodes, params }) { let mode = "mock"; let device = "wasm"; let maxNewTokens = DEFAULT_MAX_NEW_TOKENS; let temperature = 0; let modelReady = false; let status = "Ready"; let busy = false; let input = ""; let renderedMessages = 0; let outputText = ""; let sessionName = "untitled"; let sessionId = crypto.randomUUID(); let queuedLines = []; let agent = null; if (params.get("device")) device = params.get("device"); if (params.get("tokens")) maxNewTokens = Number(params.get("tokens")) || DEFAULT_MAX_NEW_TOKENS; if (params.get("max_new_tokens")) maxNewTokens = Number(params.get("max_new_tokens")) || DEFAULT_MAX_NEW_TOKENS; if (LOCAL_MODELS[params.get("model")]) mode = params.get("model"); if (LOCAL_MODELS[params.get("mode")]) mode = params.get("mode"); if (params.get("mode") === "mock") mode = "mock"; if (mode !== "mock" && !LOCAL_MODELS[mode]) mode = DEFAULT_LOCAL_MODEL_KEY; if (!navigator.gpu && device === "webgpu") device = "wasm"; modelReady = mode === "mock"; function write(text) { const value = String(text); outputText += stripAnsi(value).replace(/\r/g, ""); if (outputText.length > 250000) outputText = outputText.slice(-200000); terminal.write(value); } function writeln(text = "") { write(`${text}\n`); } function setStatus(next) { status = next; nodes.status.textContent = next; } function setModelStatus(next) { nodes.modelStatus.textContent = next; nodes.modelLabel.textContent = modelLabel(mode); } function setSandboxStatus(next) { nodes.sandboxStatus.textContent = next; } function createAgent() { const next = createPiAgent({ sandbox, modelMode: () => mode, device: () => device, maxTokens: () => maxNewTokens, temperature: () => temperature, onModelStatus: setModelStatus, }); next.subscribe((event) => { switch (event.type) { case "agent_start": setStatus("Agent running"); break; case "tool_execution_start": writeln(`\x1b[2m[running ${event.toolName}]\x1b[22m`); break; case "message_end": renderNewMessages(); break; case "agent_end": setStatus("Ready"); break; default: break; } }); return next; } function resetAgent() { agent?.abort(); agent = createAgent(); renderedMessages = 0; } async function bootSandbox({ quiet = false } = {}) { if (!quiet) writeln("\x1b[2mBooting WebContainer sandbox...\x1b[22m"); await sandbox.boot(); if (!quiet) writeln("\x1b[2mSandbox ready.\x1b[22m"); } function showGate(message = "Ready.") { nodes.gateStatus.textContent = message; nodes.modelGate.classList.remove("hidden"); } function hideGate() { nodes.modelGate.classList.add("hidden"); } async function loadModel() { if (mode === "mock") { modelReady = true; setModelStatus("Deterministic test model"); hideGate(); return; } busy = true; setStatus("Loading model"); nodes.gateStatus.textContent = "Booting sandbox..."; writeln(`\x1b[2mPreparing ${modelLabel(mode)} on ${device}...\x1b[22m`); try { await bootSandbox({ quiet: true }); nodes.gateStatus.textContent = "Downloading model..."; await agent.preloadModel(); modelReady = true; setModelStatus("Model ready"); nodes.gateStatus.textContent = "Model ready."; hideGate(); writeln("\x1b[32mModel ready.\x1b[0m"); } catch (error) { const message = error.stack || error.message || String(error); setModelStatus("Model error"); nodes.gateStatus.textContent = message; writeln(`\x1b[31m${message}\x1b[0m`); } finally { busy = false; setStatus("Ready"); writePrompt(); } } function writeStartup() { writeln(`pi - AI coding assistant with read, bash, edit, write tools`); writeln(`version ${PI_CLI_VERSION} | web terminal ${terminal.engine} | cwd /workspace`); writeln(`shortcuts: /hotkeys | /model | /settings | /session | /help`); writeln(`loaded AGENTS.md: none | prompt templates: none | skills: none | extensions: none`); writeln(`tools: ${PI_DEFAULT_TOOLS.join(", ")} (read-only extras available: grep, find, ls)`); if (!modelReady) { writeln(`model: ${modelLabel(mode)} is not downloaded. Use the setup dialog or /model mock.`); } else { writeln(`model: ${modelLabel(mode)}`); } writeln(); } function footer() { return `cwd /workspace | session ${sessionName} | model ${shortModelName(mode)} | ${maxNewTokens} max tokens | ${status}`; } function writePrompt() { if (busy) return; writeln(`\x1b[2m${footer()}\x1b[22m`); write("> "); terminal.focus(); } function renderNewMessages() { const messages = agent.state.messages; while (renderedMessages < messages.length) { const message = messages[renderedMessages]; renderedMessages += 1; if (message.role === "user") continue; if (message.role === "toolResult") { const text = textFromContent(message.content); const label = message.isError ? `[tool error: ${message.toolName}]` : `[tool: ${message.toolName}]`; writeln(`\x1b[36m${label}\x1b[0m`); if (text) writeln(text); writeln(); continue; } const toolCalls = toolCallsFromMessage(message); if (toolCalls.length > 0) { writeln("\x1b[35mUsing pi tools:\x1b[0m"); for (const call of toolCalls) writeln(` ${formatToolCall(call)}`); writeln(); continue; } const text = textFromContent(message.content); if (text) { writeln("\x1b[32mpi\x1b[0m"); writeln(text); writeln(); } } } async function runBashLine(command, sendToModel) { busy = true; setStatus("Running command"); try { await bootSandbox({ quiet: true }); writeln(`$ ${command}`); const result = await sandbox.bash(command, 120); if (result.output) writeln(result.output); writeln(`exit ${result.exitCode}`); if (sendToModel) { await promptAgent(`Command output from \`${command}\`:\n${result.output || "(no output)"}\nexit ${result.exitCode}`); } } catch (error) { writeln(`\x1b[31m${error.stack || error.message || String(error)}\x1b[0m`); } finally { busy = false; setStatus("Ready"); if (!sendToModel) writePrompt(); } } async function promptAgent(prompt) { if (mode !== "mock" && !modelReady) { writeln("Download the selected local model before sending, or use /model mock."); showGate("Download the selected model before sending, or use the test model."); writePrompt(); return; } busy = true; setStatus("Agent running"); try { await bootSandbox({ quiet: true }); await agent.prompt(prompt); } catch (error) { writeln(`\x1b[31m${error.stack || error.message || String(error)}\x1b[0m`); } finally { busy = false; setStatus("Ready"); const next = queuedLines.shift(); if (next) { writeln(`\x1b[2m[dequeued] ${next}\x1b[22m`); await handleLine(next, { fromQueue: true }); } else { writePrompt(); } } } function printModels() { writeln("Available browser-local models:"); for (const [key, value] of Object.entries(LOCAL_MODELS)) { writeln(` ${key.padEnd(12)} ${value.id}`); } writeln(" mock deterministic test model"); writeln("Use /model to switch. Non-mock models run in this browser through Transformers.js."); } function printSession() { writeln(`Session`); writeln(` id: ${sessionId}`); writeln(` name: ${sessionName}`); writeln(` messages: ${agent.state.messages.length}`); writeln(` cwd: /workspace`); writeln(` storage: browser memory`); } async function handleSlash(line) { const [command, ...rest] = line.trim().split(/\s+/); const argText = rest.join(" "); switch (command) { case "/help": writeln(PI_HELP_TEXT); break; case "/hotkeys": writeln(formatHotkeys()); break; case "/model": if (!argText) { printModels(); break; } if (argText === "mock" || LOCAL_MODELS[argText]) { mode = argText; modelReady = mode === "mock"; resetAgent(); setModelStatus(modelReady ? "Deterministic test model" : "Model idle"); writeln(`model: ${modelLabel(mode)}`); if (!modelReady) showGate("Download the selected model before sending."); else hideGate(); } else { writeln(`Unknown model: ${argText}`); printModels(); } break; case "/settings": if (argText.startsWith("device=")) { device = argText.slice("device=".length) || device; modelReady = mode === "mock"; resetAgent(); } else if (argText.startsWith("tokens=")) { maxNewTokens = Number(argText.slice("tokens=".length)) || maxNewTokens; } else if (argText.startsWith("temperature=")) { temperature = Number(argText.slice("temperature=".length)) || 0; } writeln(`Settings`); writeln(` device: ${device}`); writeln(` max tokens: ${maxNewTokens}`); writeln(` temperature: ${temperature}`); writeln(` transport: browser-local Transformers.js`); break; case "/session": printSession(); break; case "/new": resetAgent(); await sandbox.reset(); sessionId = crypto.randomUUID(); sessionName = "untitled"; writeln("Started a new browser session and reset the WebContainer workspace."); break; case "/name": sessionName = argText || "untitled"; writeln(`Session name: ${sessionName}`); break; case "/tools": writeln(`Default tools: ${PI_DEFAULT_TOOLS.join(", ")}`); writeln(`Built-in tools: ${PI_BUILTIN_TOOLS.join(", ")}`); break; case "/clear": terminal.clear(); outputText = ""; writeStartup(); break; case "/login": case "/logout": writeln("This web port runs a local browser model, so provider login is not required. Use /model to switch local planners."); break; case "/resume": case "/tree": case "/fork": case "/clone": case "/compact": case "/copy": case "/export": case "/share": case "/reload": case "/changelog": case "/scoped-models": writeln(`${command} is recognized from Pi CLI. In this static browser build it is represented in-memory; full filesystem-backed session management is a follow-up target.`); break; case "/quit": writeln("Close this browser tab to quit the web terminal."); break; case "/commands": writeln(PI_SLASH_COMMANDS.join("\n")); break; default: writeln(`Unknown command: ${command}`); writeln("Type /help or /commands."); break; } } async function handleLine(line, { fromQueue = false } = {}) { const trimmed = line.trim(); if (!trimmed) { writePrompt(); return; } if (busy && !fromQueue) { queuedLines.push(line); writeln(`\x1b[2m[queued] ${line}\x1b[22m`); return; } if (trimmed.startsWith("/")) { await handleSlash(trimmed); writePrompt(); return; } if (trimmed.startsWith("!!")) { await runBashLine(trimmed.slice(2).trim(), false); return; } if (trimmed.startsWith("!")) { await runBashLine(trimmed.slice(1).trim(), true); return; } await promptAgent(line); } function redrawInput(nextInput) { write("\r\x1b[2K> "); input = nextInput; write(input); } function handleData(data) { for (const char of data) { if (char === "\r") { const submitted = input; input = ""; writeln(); void handleLine(submitted); } else if (char === "\u007f") { if (input.length > 0) { input = input.slice(0, -1); write("\b \b"); } } else if (char === "\x03") { if (busy) { agent.abort(); writeln("^C"); setStatus("Aborted"); } else if (input) { redrawInput(""); } else { writeln("^C"); writeln("Use /quit or close this browser tab to exit."); writePrompt(); } } else if (char === "\x1b") { if (busy) agent.abort(); } else if (char >= " " && char !== "\x7f") { input += char; write(char); } } } nodes.confirmLoadModel.addEventListener("click", async () => { device = nodes.gateDevice.value; await loadModel(); }); nodes.useTestModel.addEventListener("click", () => { mode = "mock"; modelReady = true; resetAgent(); setModelStatus("Deterministic test model"); hideGate(); writeln("model: deterministic test model"); writePrompt(); }); resetAgent(); setStatus("Ready"); setSandboxStatus("Not booted"); setModelStatus(modelReady ? "Deterministic test model" : "Model idle"); nodes.gateDevice.value = device; if (modelReady || params.get("setup") === "skip") hideGate(); else showGate("Ready."); terminal.onData(handleData); writeStartup(); writePrompt(); return { loadModel, handleLine, get outputText() { return outputText; }, get transcript() { return agent.state.messages .map((message) => { if (message.role === "toolResult") return `TOOL ${message.toolName}\n${textFromContent(message.content)}`; return `${message.role.toUpperCase()}\n${textFromContent(message.content)}`; }) .join("\n\n"); }, get modelReady() { return modelReady; }, get status() { return status; }, get mode() { return mode; }, }; }