import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; /** * Host-compatibility shims for the `ExtensionContext["ui"]` surface. * * RETENTION POLICY (W4 2026-10-03): these typeof-based capability guards are * deliberate no-ops, NOT version guards, and must be kept. pi-crew declares * `"@earendil-works/pi-coding-agent": "*"` and `"@earendil-works/pi-tui": "*"` * as optional peerDependencies with no minimum-version floor * (package.json peerDependencies/peerDependenciesMeta), so host installs older * than any SDK floor we verify against must keep working — retiring these * shims would crash hosts whose UI predates * `setWidget`/`setFooter`/`custom`/`setStatus`/`setWorkingIndicator`. * Verified live: each guard is a pure feature check; there is no version * constant or gate in this file. New host UI APIs should be added here * (feature-detected), not called directly from src/. */ export interface WorkingIndicatorOptions { frames?: string[]; intervalMs?: number; } type UiContext = Pick; type ExtensionUi = ExtensionContext["ui"]; type WidgetContent = string[] | ((tui: unknown, theme: unknown) => unknown); type WidgetOptions = Parameters[2]; type CustomOptions = Parameters[1]; type CustomFactory = (tui: unknown, theme: unknown, keybindings: unknown, done: (result: T) => void) => unknown; type GenericCustom = (factory: CustomFactory, options?: CustomOptions) => Promise; function maybeRecord(value: unknown): Record | undefined { return value && typeof value === "object" ? (value as Record) : undefined; } export function requestRender(ctx: UiContext): void { requestRenderTarget(ctx.ui); } export function requestRenderTarget(target: unknown): void { const record = maybeRecord(target); const fn = record?.requestRender; if (typeof fn === "function") fn.call(target); } export function setWorkingIndicator(ctx: UiContext, options?: WorkingIndicatorOptions): void { const record = maybeRecord(ctx.ui); const fn = record?.setWorkingIndicator; if (typeof fn === "function") fn.call(ctx.ui, options); } /** Install or update an extension widget slot. * No-op when the host UI predates the `setWidget` API. */ export function setExtensionWidget(ctx: UiContext, key: string, content: WidgetContent | undefined, options?: WidgetOptions): void { const record = maybeRecord(ctx.ui); const fn = record?.setWidget; if (typeof fn === "function") fn.call(ctx.ui, key, content as never, options); } /** * Map the crew widget placement onto pi's `WidgetPlacement`. `"bottom"` is a * crew-only placement (rendered inside the crew-vibes footer, below the quota * lines); when a raw widget slot is involved it falls back to `belowEditor`. */ export function toPiWidgetPlacement(placement: "aboveEditor" | "belowEditor" | "bottom"): "aboveEditor" | "belowEditor" { return placement === "bottom" ? "belowEditor" : placement; } type FooterFactory = (tui: unknown, theme: unknown, footerData: unknown) => unknown; /** Install a custom footer component, or pass `undefined` to restore pi's built-in footer. * No-op when the host UI predates the `setFooter` API. */ export function setFooter(ctx: UiContext | undefined, factory: FooterFactory | undefined): void { if (!ctx) return; const record = maybeRecord(ctx.ui); const fn = record?.setFooter; if (typeof fn === "function") fn.call(ctx.ui, factory as never); } /** Show a custom focused component. * When the host UI predates (or mangles) the `custom` API, resolves `undefined` * instead of rejecting or hanging: the sole caller today * (src/extension/registration/ui.ts) already fire-and-forgets via * `void showCustom(...)`, so a rejected or never-settling promise would only * surface as an unhandled rejection. */ export function showCustom(ctx: UiContext, factory: CustomFactory, options?: CustomOptions): Promise { const record = maybeRecord(ctx.ui); const fn = record?.custom; if (typeof fn !== "function") return Promise.resolve(undefined as T); const custom = fn as unknown as GenericCustom; return custom(factory, options); } /** Set a status fallback line. * No-op when the host UI predates the `setStatus` API. */ export function setStatusFallback(ctx: UiContext, key: string, lines: string | readonly string[] | undefined, segment?: string): void { const text = typeof lines === "string" ? lines : lines ? [...lines].join("\n") : undefined; const record = maybeRecord(ctx.ui); const fn = record?.setStatus; if (typeof fn === "function") fn.call(ctx.ui, segment ? `${key}:${segment}` : key, text); }