Created
May 24, 2026 17:09
-
-
Save EzraWolf/c7970f88247812801cf917b6d62093c3 to your computer and use it in GitHub Desktop.
pi agent lean /btw command
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * /btw <msg> spawns a throwaway LLM chat | |
| * /btw:settings to configure model/thinking | |
| */ | |
| import * as fs from "node:fs"; | |
| import * as path from "node:path"; | |
| import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; | |
| import { | |
| createAgentSession, DynamicBorder, getMarkdownTheme, | |
| getSettingsListTheme, SessionManager, | |
| } from "@earendil-works/pi-coding-agent"; | |
| import type { AgentSession } from "@earendil-works/pi-coding-agent"; | |
| import { | |
| Container, Key, Markdown, matchesKey, type SettingItem, | |
| SettingsList, Spacer, Text, type Theme, truncateToWidth, | |
| } from "@earendil-works/pi-tui"; | |
| import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; | |
| import type { Model } from "@earendil-works/pi-ai"; | |
| // state (persisted to settings.json under extensions.btw) | |
| let prefModelProvider: string | null = null; | |
| let prefModelId: string | null = null; | |
| let prefThinking: ThinkingLevel | null = null; | |
| const LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"]; | |
| const SETTINGS_FILE = path.join(process.env.HOME ?? "~", ".pi", "agent", "extensions", "settings.json"); | |
| function readExtSettings(): any { | |
| try { return JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8")); } catch { return {}; } | |
| } | |
| function loadGlobalPrefs(): void { | |
| const btw = readExtSettings()?.btw; | |
| if (btw) { | |
| prefModelProvider = btw.modelProvider ?? null; | |
| prefModelId = btw.modelId ?? null; | |
| prefThinking = btw.thinking ?? null; | |
| } | |
| } | |
| function saveGlobalPrefs(): void { | |
| try { | |
| fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true }); | |
| const s = readExtSettings(); | |
| s.btw = { modelProvider: prefModelProvider, modelId: prefModelId, thinking: prefThinking }; | |
| fs.writeFileSync(SETTINGS_FILE, JSON.stringify(s, null, 2) + "\n"); | |
| } catch {} | |
| } | |
| // overlay | |
| const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; | |
| class BtwOverlay { | |
| private messages: string[] = []; | |
| private input = ""; | |
| private busy = false; | |
| private cw?: number; | |
| private cl?: string[]; | |
| private spinnerIndex = 0; | |
| private spinnerTimer?: ReturnType<typeof setTimeout>; | |
| private stallTimer?: ReturnType<typeof setTimeout>; | |
| constructor( | |
| private s: AgentSession, | |
| private t: Theme, | |
| private d: () => void, | |
| private modelId: string, | |
| private thinkingLevel: ThinkingLevel, | |
| init?: string, | |
| private tui?: { requestRender: () => void }, | |
| ) { | |
| this.s.subscribe((e) => { | |
| if (e.type === "message_update" && e.assistantMessageEvent.type === "text_delta") { | |
| const last = this.messages[this.messages.length - 1]; | |
| if (last?.startsWith("LLM: ")) this.messages[this.messages.length - 1] += e.assistantMessageEvent.delta; | |
| else this.messages.push("LLM: " + e.assistantMessageEvent.delta); | |
| } else if (e.type === "tool_execution_start") { | |
| this.messages.push(`\u{1F527} ${e.toolName}`); | |
| } else if (e.type === "tool_execution_end") { | |
| for (const b of e.result?.content ?? []) if (b.type === "text") this.messages.push(...b.text.split("\n")); | |
| this.messages.push(`\u{1F527} ${e.toolName} ${e.isError ? "\u274C" : "\u2713"}`); | |
| } else if (e.type === "agent_end") { | |
| this.clearTimers(); | |
| this.busy = false; | |
| } | |
| this.invalidate(); | |
| this.tui?.requestRender(); | |
| }); | |
| if (init) this.send(init); | |
| } | |
| handleInput(data: string): void { | |
| if (matchesKey(data, Key.escape)) { this.clearTimers(); this.s.dispose(); this.d(); return; } | |
| if (this.busy) return; | |
| if (matchesKey(data, Key.enter) && this.input.trim()) { this.send(this.input.trim()); return; } | |
| if (matchesKey(data, Key.backspace)) { this.input = this.input.slice(0, -1); this.invalidate(); return; } | |
| if (data.length === 1 && data >= " ") { this.input += data; this.invalidate(); } | |
| } | |
| private clearTimers(): void { | |
| if (this.stallTimer) { clearTimeout(this.stallTimer); this.stallTimer = undefined; } | |
| this.stopSpinner(); | |
| } | |
| private stopSpinner(): void { | |
| if (this.spinnerTimer) { clearInterval(this.spinnerTimer); this.spinnerTimer = undefined; } | |
| } | |
| private startSpinner(): void { | |
| this.stopSpinner(); | |
| this.spinnerIndex = 0; | |
| this.spinnerTimer = setInterval(() => { | |
| this.spinnerIndex = (this.spinnerIndex + 1) % SPINNER_FRAMES.length; | |
| this.invalidate(); | |
| this.tui?.requestRender(); | |
| }, 80); | |
| } | |
| private send(text: string): void { | |
| this.messages.push("You: " + text); | |
| this.input = ""; this.busy = true; this.invalidate(); | |
| this.clearTimers(); | |
| this.startSpinner(); | |
| this.stallTimer = setTimeout(() => { | |
| if (!this.busy) return; | |
| this.busy = false; | |
| this.stopSpinner(); | |
| this.messages.push("\u26A0 No response received within 60s. The model may be stuck."); | |
| this.invalidate(); | |
| }, 60_000); | |
| this.s.prompt(text).catch((e: Error) => { | |
| this.clearTimers(); | |
| this.messages.push("\u26A0 " + e.message); this.busy = false; this.invalidate(); | |
| }); | |
| } | |
| render(w: number): string[] { | |
| if (this.cl && this.cw === w) return this.cl; | |
| const t = this.t, bar = t.fg("accent", "\u2500".repeat(w)); | |
| const out: string[] = []; | |
| const think = this.thinkingLevel === "off" ? "thinking off" : this.thinkingLevel; | |
| const hdr = t.fg("toolTitle", t.bold(" BTW")) + t.fg("muted", ` · ${this.modelId} · ${think}`); | |
| out.push(bar, hdr + " ".repeat(Math.max(0, w - hdr.length - 11)), bar); | |
| if (this.messages.length === 0) out.push("", t.fg("muted", " Type a message and press Enter.")); | |
| else for (const m of this.messages) { | |
| if (m.startsWith("LLM: ")) { | |
| const mdTheme = getMarkdownTheme(); | |
| const md = new Markdown(m.slice(5), 2, 0, mdTheme); | |
| out.push(...md.render(w)); | |
| } else if (m.startsWith("You: ")) { | |
| const lines = m.slice(5).split("\n"); | |
| out.push("", t.bg("userMessageBg", " ".repeat(w))); | |
| for (const line of lines) out.push(t.bg("userMessageBg", " " + line + " ".repeat(Math.max(0, w - 2 - line.length)))); | |
| out.push(t.bg("userMessageBg", " ".repeat(w)), ""); | |
| } else { | |
| for (const line of m.split("\n")) out.push(truncateToWidth(" " + line, w)); | |
| } | |
| } | |
| const hint = this.busy ? t.fg("accent", SPINNER_FRAMES[this.spinnerIndex]) + t.fg("muted", " Working...") : this.input ? "" : t.fg("dim", "type here..."); | |
| out.push("", bar, truncateToWidth(t.fg("accent", " > ") + t.fg("text", this.input) + hint, w, "")); | |
| this.cw = w; this.cl = out; return out; | |
| } | |
| invalidate(): void { this.cw = undefined; this.cl = undefined; } | |
| } | |
| // settings command | |
| async function showSettings(ctx: ExtensionCommandContext) { | |
| const modelValues = ["inherit", ...(await ctx.modelRegistry.getAvailable()).map((m) => `${m.provider}/${m.id}`)]; | |
| const items: SettingItem[] = [ | |
| { | |
| id: "model", label: "model", | |
| currentValue: prefModelProvider && prefModelId ? `${prefModelProvider}/${prefModelId}` : "inherit", | |
| values: modelValues, descriptions: { inherit: `current (${ctx.model?.id ?? "none"})` }, | |
| }, | |
| { | |
| id: "think", label: "think", | |
| currentValue: prefThinking ?? "inherit", | |
| values: ["inherit", ...LEVELS], | |
| }, | |
| ]; | |
| await ctx.ui.custom((_tui, theme, _kb, done) => { | |
| const container = new Container(); | |
| const border = new DynamicBorder(s => theme.fg("accent", s)); | |
| container.addChild(border); | |
| container.addChild(new Spacer(1)); | |
| container.addChild(new Text(theme.fg("accent", theme.bold("BTW Settings")), 1, 0)); | |
| container.addChild(new Spacer(1)); | |
| const close = () => { | |
| const mv = items[0]!.currentValue; | |
| prefModelProvider = mv === "inherit" ? null : mv.split("/")[0]!; | |
| prefModelId = mv === "inherit" ? null : mv.split("/").slice(1).join("/"); | |
| prefThinking = items[1]!.currentValue === "inherit" ? null : items[1]!.currentValue as ThinkingLevel; | |
| saveGlobalPrefs(); | |
| ctx.ui.notify(`BTW: ${prefModelProvider ? `${prefModelProvider}/${prefModelId}` : "inherit"}, ${prefThinking ?? "inherit"} thinking`, "info"); | |
| done(undefined); | |
| }; | |
| const list = new SettingsList(items, Math.min(items.length + 4, 15), | |
| { ...getSettingsListTheme(), hint: () => theme.fg("dim", "↑↓ navigate · Enter/Space to change · Esc to cancel") }, | |
| () => {}, close, { enableSearch: false }); | |
| container.addChild(list); | |
| container.addChild(border); | |
| return { | |
| render: (w: number) => container.render(w), | |
| invalidate: () => container.invalidate(), | |
| handleInput: (d: string) => { list.handleInput(d); _tui.requestRender(); }, | |
| }; | |
| }); | |
| } | |
| export default function (pi: ExtensionAPI) { | |
| loadGlobalPrefs(); | |
| pi.registerCommand("btw:settings", { | |
| description: "Configure /btw", | |
| handler: async (_args, ctx) => { if (ctx.hasUI) await showSettings(ctx); }, | |
| }); | |
| pi.registerCommand("btw", { | |
| description: "Open a throwaway session", | |
| handler: async (_args, ctx) => { | |
| if (!ctx.hasUI || !ctx.model) return; | |
| let model: Model<any> = ctx.model; | |
| if (prefModelProvider && prefModelId) { | |
| const found = ctx.modelRegistry.find(prefModelProvider, prefModelId); | |
| if (found) model = found; | |
| } | |
| const thinking = prefThinking ?? pi.getThinkingLevel(); | |
| const { session } = await createAgentSession({ | |
| model, thinkingLevel: thinking, | |
| sessionManager: SessionManager.inMemory(ctx.cwd), | |
| }); | |
| await ctx.ui.custom<null>((tui, theme, _kb, done) => | |
| new BtwOverlay(session, theme, done, model.id, thinking, _args?.trim() || undefined, tui)); | |
| }, | |
| }); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
to use globally, paste into
.pi/agent/extensions/btw.tsrun
/reloador quit, reopen pi