Skip to content

Instantly share code, notes, and snippets.

@EzraWolf
Created May 24, 2026 17:09
Show Gist options
  • Select an option

  • Save EzraWolf/c7970f88247812801cf917b6d62093c3 to your computer and use it in GitHub Desktop.

Select an option

Save EzraWolf/c7970f88247812801cf917b6d62093c3 to your computer and use it in GitHub Desktop.
pi agent lean /btw command
/**
* /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));
},
});
}
@EzraWolf
Copy link
Copy Markdown
Author

to use globally, paste into .pi/agent/extensions/btw.ts

run /reload or quit, reopen pi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment