/** * bytefly — the package's terminal chrome and diagnostics, composed from twelve * feature folders: `header/` (the themed startup mark and version), `footer/` * (the two-line, width-adaptive status bar with its price calculation), * `message/` (the user-message nameplate), `notice/` (the shared transcript * notice renderer), `block/` (the unified tool-block frame), `separator/` (the * transcript turn rule), `working/` (the live run line on the composer's top * border), `panel/` (the unified `/bytefly` screen and its * `doctor` report), `doctor/` (the `run_doctor` tool and its session-scoped * probe runtime), `notify/` (OS notifications for unattended runs), `mode/` * (the editor's Shift+Tab plan-mode toggle and its border), and `update/` (Pi's * package list, the host suppression, and the header marker). * * The header, footer, turn rule, and working line are installed * only in interactive (`tui`) sessions, and are always on; the user-message * marker, notice, and tool blocks register at load and take effect wherever a * transcript renders. `pi config` turns bytefly's chrome off. The panel and doctor work * in every mode, and notify works wherever the environment can deliver it. The * footer is priced from DeepSeek's published rates by default and from * `bytefly.json` otherwise, the same file whose `notify` section configures * notifications. * * Load with: pi --extension ./extensions/bytefly */ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { isExtensionEnabled } from "../../lib/env.ts"; import { registerToolBlocks } from "./block/register.ts"; import { registerChrome } from "./chrome.ts"; import { type DoctorDeps, registerDoctor } from "./doctor/register.ts"; import { registerFooter } from "./footer/register.ts"; import { registerHeader } from "./header/register.ts"; import { registerUserMessage } from "./message/register.ts"; import { registerNotice } from "./notice/register.ts"; import { type NotifyDeps, registerNotify } from "./notify/register.ts"; import { createPadStore } from "./pad.ts"; import { type PanelCommandDeps, registerPanel } from "./panel/commands.ts"; import { registerTurnSeparator } from "./separator/register.ts"; import { registerPiRelease, type ReleaseDeps } from "./update/release.ts"; import { registerUpdate, type UpdateDeps } from "./update/register.ts"; import { createUpdateState } from "./update/state.ts"; import { registerWorkingTimer } from "./working/register.ts"; /** Host seams, injected by tests. */ export interface ByteflyDeps { /** Seams for the picker side of the unified panel. */ extensions?: Pick; /** Seams for the doctor surface. */ doctor?: DoctorDeps; /** Seams for the notify capability. */ notify?: NotifyDeps; /** Seams for the package-update surface. */ update?: UpdateDeps; /** Seams for Pi's own release notice. */ release?: ReleaseDeps; } /** Register every bytefly surface; the factory only registers. */ export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void { if (!isExtensionEnabled("bytefly")) return; // One store per load: the update check writes this package's new version and // the header reads it, so the marker and every re-install agree. Pi's own // release notice writes the second store the same way. const updates = createUpdateState(); const piUpdates = createUpdateState(); const header = registerHeader(pi, updates, piUpdates); // Notify first, so the footer's budget guardrail can raise one OS notification // through the same capability the triggers use instead of a second dispatch path. const notifications = registerNotify(pi, deps.notify); registerFooter(pi, { notify: notifications.notify }); registerUserMessage(pi); // One store per load: the tool frame and the notice record Pi's output pad and // the turn separator reads it, so bytefly's transcript blocks share one margin. const pad = createPadStore(); // The rail glyph is read from `bytefly.json` at session start; the tool results // and the notice read it through this one store. const rail = registerChrome(pi); registerNotice(pi, pad, rail); registerToolBlocks(pi, pad, rail); registerTurnSeparator(pi, pad); registerWorkingTimer(pi); const doctor = registerDoctor(pi, deps.doctor); // The returned handle is what `/bytefly update` re-runs, so the update surface is // registered before the panel that offers the command. const update = registerUpdate(pi, updates, { ...deps.update, onUpdate: () => header.refresh() }); // Pi's own release notice is the second source of a header marker, so it shares // the one repaint hook rather than installing a header of its own. registerPiRelease(pi, piUpdates, { ...deps.release, onUpdate: () => header.refresh() }); registerPanel(pi, { doctor, update, discover: deps.extensions?.discover }); // Shift+Tab is a chord Pi reserves for the thinking cycle, so the mode key is // an editor rather than a shortcut. It loads on the first TUI session and only // when `plan` is loaded, so a session without it keeps Pi's key and pays for // nothing; the mode module owns that availability check. let detachModeKey: (() => void) | undefined; let modeEpoch = 0; const installModeKey = async (ctx: ExtensionContext): Promise => { const epoch = ++modeEpoch; detachModeKey?.(); detachModeKey = undefined; if (ctx.mode !== "tui") return; const { attachModeKey } = await import("./mode/editor.ts"); // A shutdown or a newer session may land while the module loads; the stale // reply must not install an editor nothing will release. if (epoch !== modeEpoch) return; detachModeKey = attachModeKey(pi, ctx); }; pi.on("session_start", (_event, ctx) => installModeKey(ctx)); pi.on("session_tree", (_event, ctx) => installModeKey(ctx)); pi.on("session_shutdown", () => { modeEpoch++; detachModeKey?.(); detachModeKey = undefined; }); }