Spaces:
Sleeping
Sleeping
| 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 <key> 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; | |
| }, | |
| }; | |
| } | |