import { getAgentDir } from "@earendil-works/pi-coding-agent"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; const BUNDLED_QUOTES_FILE = new URL("./quotes.txt", import.meta.url); const FALLBACK_QUOTE = "Optimism is an occupational hazard of programming. Feedback is the treatment. — Kent Beck"; type QuoteMode = "multiline" | "inline"; interface QuotesConfig { mode: QuoteMode; enabled: boolean; } interface LoadedQuotes { quotes: string[]; source: "user" | "bundled"; } function getExtensionDir(): string { return join(getAgentDir(), "pi-quotes"); } function getUserQuotesFile(): string { return join(getExtensionDir(), "quotes.txt"); } function getConfigFile(): string { return join(getExtensionDir(), "config.json"); } function loadConfig(): QuotesConfig { try { const raw = JSON.parse(readFileSync(getConfigFile(), "utf8")) as Partial; return { mode: raw.mode === "multiline" ? "multiline" : "inline", enabled: raw.enabled !== false, }; } catch { return { mode: "inline", enabled: true }; } } function saveConfig(config: QuotesConfig): void { try { mkdirSync(getExtensionDir(), { recursive: true }); writeFileSync(getConfigFile(), `${JSON.stringify(config, null, 2)}\n`, "utf8"); } catch { // Persistence is best-effort; the in-memory state still applies this session. } } function parseQuotes(text: string): string[] { return text .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line.length > 0 && !line.startsWith("#")); } function readBundledQuotesText(): string { try { return readFileSync(BUNDLED_QUOTES_FILE, "utf8"); } catch { return `${FALLBACK_QUOTE}\n`; } } function ensureUserQuotesFile(): string { const userQuotesFile = getUserQuotesFile(); if (!existsSync(userQuotesFile)) { mkdirSync(dirname(userQuotesFile), { recursive: true }); writeFileSync(userQuotesFile, readBundledQuotesText(), "utf8"); } return userQuotesFile; } function loadQuotes(): LoadedQuotes { const userQuotesFile = getUserQuotesFile(); try { if (existsSync(userQuotesFile)) { const userQuotes = parseQuotes(readFileSync(userQuotesFile, "utf8")); if (userQuotes.length > 0) return { quotes: userQuotes, source: "user" }; } const bundledQuotes = parseQuotes(readBundledQuotesText()); return { quotes: bundledQuotes.length > 0 ? bundledQuotes : [FALLBACK_QUOTE], source: "bundled", }; } catch { return { quotes: [FALLBACK_QUOTE], source: "bundled" }; } } function describeQuotes(loaded: LoadedQuotes): string { return loaded.source === "user" ? `${loaded.quotes.length} Quotes` : `${loaded.quotes.length} Bundled Quotes`; } function pickQuote(loaded: LoadedQuotes): string { return loaded.quotes[Math.floor(Math.random() * loaded.quotes.length)] ?? FALLBACK_QUOTE; } function isWideCodePoint(code: number): boolean { return ( (code >= 0x1100 && code <= 0x115f) || // Hangul Jamo (code >= 0x2e80 && code <= 0xa4cf) || // CJK radicals through Yi (code >= 0xac00 && code <= 0xd7a3) || // Hangul syllables (code >= 0xf900 && code <= 0xfaff) || // CJK compatibility ideographs (code >= 0xfe30 && code <= 0xfe4f) || // CJK compatibility forms (code >= 0xff00 && code <= 0xff60) || // Fullwidth forms (code >= 0xffe0 && code <= 0xffe6) || // Fullwidth signs (code >= 0x1f300 && code <= 0x1faff) || // Emoji and symbols (code >= 0x20000 && code <= 0x3fffd) // CJK extensions B+ ); } function displayWidth(text: string): number { let width = 0; for (const char of text) { const code = char.codePointAt(0) ?? 0; // Skip zero-width joiners/spaces, combining marks, and variation selectors. if ( code === 0x200b || code === 0x200d || code === 0xfe0f || (code >= 0x0300 && code <= 0x036f) ) { continue; } width += isWideCodePoint(code) ? 2 : 1; } return width; } function terminalContentWidth(): number { // The loader's Text component adds one column of padding on each side. return Math.max(20, (process.stdout.columns ?? 80) - 2); } function centerText(text: string): string { const width = terminalContentWidth(); const padding = Math.max(0, Math.floor((width - displayWidth(text)) / 2)); return `${" ".repeat(padding)}${text}`; } function formatWorkingMessage(ctx: ExtensionContext, quote: string, mode: QuoteMode): string { if (mode === "inline") { const styledQuote = ctx.ui.theme.italic(ctx.ui.theme.fg("dim", quote)); return `Working... ${styledQuote}`; } const centeredQuote = centerText(quote); const styledQuote = ctx.ui.theme.italic(ctx.ui.theme.fg("dim", centeredQuote)); return `Working...\n${styledQuote}`; } function formatMode(mode: QuoteMode): string { return mode === "inline" ? "Inline" : "Multiline"; } export default function quotesExtension(pi: ExtensionAPI) { const config = loadConfig(); let loaded = loadQuotes(); let mode = config.mode; let enabled = config.enabled; const applyQuote = (ctx: ExtensionContext) => { if (ctx.mode !== "tui" || !enabled) return; ctx.ui.setWorkingMessage(formatWorkingMessage(ctx, pickQuote(loaded), mode)); }; pi.on("session_start", (_event, ctx) => { applyQuote(ctx); }); pi.on("agent_start", (_event, ctx) => { loaded = loadQuotes(); applyQuote(ctx); }); pi.registerCommand("quotes", { description: "Configure the quote shown with the Working... loading message.", handler: async (args, ctx) => { const next = args.trim().toLowerCase(); if (next === "inline" || next === "multiline") { mode = next; saveConfig({ mode, enabled }); applyQuote(ctx); ctx.ui.notify(`Quotes Mode • ${formatMode(mode)}`, "info"); return; } if (next === "reload") { loaded = loadQuotes(); applyQuote(ctx); ctx.ui.notify(`${describeQuotes(loaded)} Reloaded`, "info"); return; } if (next === "edit") { const userQuotesFile = ensureUserQuotesFile(); const currentText = readFileSync(userQuotesFile, "utf8"); const editedText = await ctx.ui.editor("Edit quotes", currentText); if (editedText === undefined) { ctx.ui.notify("Quotes Edit Cancelled", "info"); return; } writeFileSync(userQuotesFile, editedText, "utf8"); loaded = loadQuotes(); applyQuote(ctx); if (loaded.source === "user") { ctx.ui.notify(`${loaded.quotes.length} Quotes Saved • ${formatMode(mode)}`, "info"); } else { ctx.ui.notify( `Quotes File Empty • Using ${loaded.quotes.length} Bundled Quotes`, "warning", ); } return; } if (next === "path") { ctx.ui.notify(getUserQuotesFile(), "info"); return; } if (next === "off") { enabled = false; saveConfig({ mode, enabled }); if (ctx.mode === "tui") ctx.ui.setWorkingMessage(); ctx.ui.notify("Quotes Disabled", "info"); return; } if (next === "on" || next === "") { enabled = true; saveConfig({ mode, enabled }); applyQuote(ctx); ctx.ui.notify(`${describeQuotes(loaded)} Enabled • ${formatMode(mode)}`, "info"); return; } ctx.ui.notify("Usage: /quotes [on|off|inline|multiline|reload|edit|path]", "error"); }, }); }