/** * model-flash: when you cycle models with Ctrl+P (or shift+Ctrl+P), * flash the current model name in BIG letters above the editor for ~1s. * * Implemented as a non-capturing widget (ctx.ui.setWidget) so rapid * Ctrl+P keystrokes are never queued behind the popup — the widget just * re-renders in place as the model changes. * * Usage: * pi -e ./model-flash.ts * or add this file to your settings under extensions. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; // --------------------------------------------------------------------------- // Tiny 5-row block font. Each glyph is 5 rows tall, columns vary (mostly 4-5). // Add a 1-col gap between glyphs when rendering. // '#' = filled cell, ' ' = empty. // --------------------------------------------------------------------------- const FONT: Record = { A: [" ## ", "# #", "####", "# #", "# #"], B: ["### ", "# #", "### ", "# #", "### "], C: [" ###", "# ", "# ", "# ", " ###"], D: ["### ", "# #", "# #", "# #", "### "], E: ["####", "# ", "### ", "# ", "####"], F: ["####", "# ", "### ", "# ", "# "], G: [" ###", "# ", "# ##", "# #", " ###"], H: ["# #", "# #", "####", "# #", "# #"], I: ["###", " # ", " # ", " # ", "###"], J: ["####", " #", " #", "# #", " ## "], K: ["# #", "# # ", "## ", "# # ", "# #"], L: ["# ", "# ", "# ", "# ", "####"], M: ["# #", "## ##", "# # #", "# #", "# #"], N: ["# #", "## #", "# ##", "# #", "# #"], O: [" ## ", "# #", "# #", "# #", " ## "], P: ["### ", "# #", "### ", "# ", "# "], Q: [" ## ", "# #", "# #", "# ##", " ###"], R: ["### ", "# #", "### ", "# # ", "# #"], S: [" ###", "# ", " ## ", " #", "### "], T: ["###", " # ", " # ", " # ", " # "], U: ["# #", "# #", "# #", "# #", " ## "], V: ["# #", "# #", "# #", " ## ", " ## "], W: ["# #", "# #", "# # #", "## ##", "# #"], X: ["# #", " ## ", " ## ", " ## ", "# #"], Y: ["# #", " ## ", " ## ", " # ", " # "], Z: ["####", " #", " # ", " # ", "####"], "0": [" ## ", "# #", "# #", "# #", " ## "], "1": [" # ", "## ", " # ", " # ", "###"], "2": ["### ", " #", " ## ", "# ", "####"], "3": ["### ", " #", " ## ", " #", "### "], "4": ["# #", "# #", "####", " #", " #"], "5": ["####", "# ", "### ", " #", "### "], "6": [" ###", "# ", "### ", "# #", " ## "], "7": ["####", " #", " # ", " # ", " # "], "8": [" ## ", "# #", " ## ", "# #", " ## "], "9": [" ## ", "# #", " ###", " #", "### "], "-": [" ", " ", "####", " ", " "], "_": [" ", " ", " ", " ", "####"], ".": [" ", " ", " ", " ", " # "], "/": [" #", " # ", " # ", " # ", "# "], ":": [" ", " # ", " ", " # ", " "], " ": [" ", " ", " ", " ", " "], }; const BLOCK = "█"; function renderBig(text: string): string[] { const chars = [...text.toUpperCase()].map((c) => FONT[c] ?? FONT["-"]!); const rows: string[] = []; for (let r = 0; r < 5; r++) { let line = ""; for (let i = 0; i < chars.length; i++) { const glyph = chars[i]!; line += glyph[r]!.replace(/#/g, BLOCK); if (i < chars.length - 1) line += " "; } rows.push(line); } return rows; } function maxLineWidth(lines: string[]): number { let w = 0; for (const l of lines) if (l.length > w) w = l.length; return w; } // --------------------------------------------------------------------------- // Extension entry point. // // Renders the flash as a non-capturing widget above the editor. Each cycle // just replaces the widget content; the timer schedules removal ~1s after // the last cycle. // --------------------------------------------------------------------------- const WIDGET_KEY = "model-flash"; const FLASH_MS = 1000; export default function (pi: ExtensionAPI) { let timer: NodeJS.Timeout | null = null; pi.on("model_select", async (event, ctx) => { // Only flash on user-driven cycling. Skip /model "set" and "restore". if (event.source !== "cycle") return; const { model } = event; const theme = ctx.ui.theme; const banner = renderBig(model.id); const bannerW = maxLineWidth(banner); // Center each banner row and the subtitle within bannerW. const center = (raw: string, styled: string): string => { const leftPad = Math.max(0, Math.floor((bannerW - raw.length) / 2)); return " ".repeat(leftPad) + styled; }; const lines: string[] = [ ...banner.map((raw) => center(raw, theme.fg("accent", theme.bold(raw)))), center(model.provider, theme.fg("muted", model.provider)), ]; ctx.ui.setWidget(WIDGET_KEY, lines); if (timer) clearTimeout(timer); timer = setTimeout(() => { timer = null; ctx.ui.setWidget(WIDGET_KEY, undefined); }, FLASH_MS); }); }