import { basename } from "node:path"; import type { Api, Model } from "@earendil-works/pi-ai"; import type { ExtensionAPI, SessionEntry, } from "@earendil-works/pi-coding-agent"; import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; import { getAgentRuntimeComposition, MAIN_AGENT_CONTRIBUTION_CHANGE_EVENT, } from "../../shared/agent-runtime-composition"; import { isChildAgentProcess } from "../../shared/child-agent-environment"; import { CODEX_FAST_ENABLED_STATUS, CODEX_FAST_STATUS_KEY, } from "../../shared/codex-fast-status"; import { getProjectionAwareContextUsage } from "../../shared/context-projection"; import { CONTEXT_PROJECTION_STATUS_KEY } from "../../shared/context-projection-status"; import { sliceTextByWidth, sliceTextSuffixByWidth, truncateTextByWidth, } from "../../shared/display-width"; import { type FooterConfig, readFooterConfig, } from "../../shared/footer-config"; import { type NativeCompactionSettings, readNativeCompactionSettings, } from "../../shared/native-compaction-settings"; import { formatUsageTokenCount } from "../../shared/usage-format"; import { requestUsageRootTotals, type UsageSessionTotals, } from "../../shared/usage-read-broker"; import { readFooterProcessEnvironment } from "./environment"; /** Footer label shown when no main-agent runtime contribution is active. */ const NO_AGENT_LABEL = "No agent"; /** Legacy status key superseded by the main-agent runtime contribution. */ const LEGACY_AGENT_STATUS_KEY = "agent"; /** Status key used by the Codex quota extension for quota text. */ const CODEX_QUOTA_STATUS_KEY = "codex-quota"; /** Status keys already represented by dedicated primary-line segments. */ const PRIMARY_LINE_STATUS_KEYS = new Set([ LEGACY_AGENT_STATUS_KEY, CODEX_FAST_STATUS_KEY, CODEX_QUOTA_STATUS_KEY, CONTEXT_PROJECTION_STATUS_KEY, ]); /** Separator between footer segments in the current minimal renderer. */ export const SEGMENT_SEPARATOR = " · "; /** Minimum useful width for a shortened label with visible start, ellipsis, and visible end. */ const MIN_SHORTENED_LABEL_WIDTH = 5; /** Context usage percentage where the footer switches from plain text to warning. */ const CONTEXT_WARNING_USED_PERCENT = 50; /** Context usage percentage where the footer switches from warning to error. */ const CONTEXT_ERROR_USED_PERCENT = 80; /** Converts a token ratio into a percentage for threshold checks. */ const PERCENT_FACTOR = 100; /** Token count where the footer switches from raw numbers to a compact thousands label. */ const TOKEN_COMPACT_THRESHOLD = 1000; /** Number of decimal places used by pi's standard footer for API cost. */ const API_COST_DECIMAL_PLACES = 3; /** Informational usage-store refresh cadence for the active root footer. */ const USAGE_REFRESH_INTERVAL_MS = 10_000; /** Warning shown when complete root-family usage is unavailable. */ const USAGE_UNAVAILABLE_WARNING = "[footer] Usage is unavailable; API cost and tokens are hidden."; const FOOTER_PROCESS_STATE_KEY = Symbol.for( "pi-agent-suite.footer.process-state.v1", ); /** Matches MCP status keys that pi exposes for MCP server state. */ const MCP_STATUS_KEY_PATTERN = /^mcp(?:-|$)/i; /** Footer data provided by pi for runtime values owned by the interactive host. */ interface FooterData { getExtensionStatuses(): ReadonlyMap; getGitBranch(): string | null; } /** TUI surface used by the footer to request render after external footer data changes. */ interface FooterTui { requestRender(): void; } interface FooterEventBus { on( eventName: typeof MAIN_AGENT_CONTRIBUTION_CHANGE_EVENT, listener: () => void, ): () => void; } /** Minimal theme surface needed by the footer renderer. */ interface FooterTheme { fg(color: "accent" | "warning" | "error", value: string): string; } /** Context usage fields that the footer displays without owning context calculation. */ interface FooterContextUsageState { readonly tokens: number | null; readonly contextWindow: number; readonly percent: number | null; } /** Mutable session state updated by pi events and read by the footer renderer. */ interface FooterSessionState { projectName: string | undefined; model: FooterModelState | undefined; requestRender: (() => void) | undefined; readonly warnedUsageRootSessionIds: Set; } type FooterModelState = Model; type FooterCompactionSettings = Extract< NativeCompactionSettings, { readonly status: "enabled" } >; /** Render input assembled from session-owned state. */ interface FooterRenderState { readonly agentLabel: string; readonly thinkingLevel: string | undefined; readonly contextUsage: FooterContextUsageState | undefined; } /** Input needed to build one footer render. */ interface FooterRenderOptions { readonly config: FooterConfig; readonly compactionSettings: FooterCompactionSettings | undefined; readonly footerData: FooterData; readonly usageTotals: UsageSessionTotals | undefined; readonly ctx: FooterSessionContext; readonly renderState: FooterRenderState; readonly sessionState: FooterSessionState; readonly theme: FooterTheme; readonly width: number; } /** Session context surface that the footer reads during rendering. */ interface FooterSessionContext { readonly cwd: string; readonly hasUI?: boolean; readonly model: FooterModelState | undefined; readonly sessionManager: { getSessionId(): string; getEntries(): SessionEntry[]; }; readonly modelRegistry: { isUsingOAuth(model: FooterModelState): boolean; }; getContextUsage(): FooterContextUsageState | undefined; readonly ui: { notify(message: string, type: "warning"): void; setFooter( footerFactory: ( tui: FooterTui, theme: FooterTheme, footerData: FooterData, ) => FooterComponent, ): void; }; } /** Footer component contract used by the pi session UI. */ interface FooterComponent { render(width: number): string[]; invalidate(): void; dispose?(): void; } /** Formats token counts into compact footer labels. */ function formatTokens(count: number): string { if (count < TOKEN_COMPACT_THRESHOLD) { return count.toString(); } return `${Math.round(count / TOKEN_COMPACT_THRESHOLD)}k`; } /** Resolves the project label from the git root and uses the working directory name outside git repositories. */ async function resolveProjectName( pi: ExtensionAPI, cwd: string, ): Promise { try { const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd, }); const gitRoot = result.stdout.trim(); if (result.code === 0 && gitRoot.length > 0) { return basename(gitRoot); } } catch { // The footer still needs a stable project label when git metadata is unavailable. } return basename(cwd); } /** Shortens a plain label from the middle so both repository label ends remain visible. */ function truncateMiddleToWidth(label: string, width: number): string { if (width <= 0) { return ""; } if (visibleWidth(label) <= width) { return label; } if (width < MIN_SHORTENED_LABEL_WIDTH) { return sliceTextByWidth(label, width); } const ellipsis = "…"; const leftWidth = Math.ceil((width - visibleWidth(ellipsis)) / 2); const rightWidth = width - visibleWidth(ellipsis) - leftWidth; return `${sliceTextByWidth(label, leftWidth)}${ellipsis}${sliceTextSuffixByWidth(label, rightWidth)}`; } /** Builds a width-bounded project label with an optional git branch suffix. */ function formatProjectLabel( projectName: string, gitBranch: string | undefined, width: number, ): string | undefined { if (width <= 0) { return undefined; } const label = gitBranch ? `${projectName}(${gitBranch})` : projectName; return truncateMiddleToWidth(label, width); } /** Builds the project segment from the session working directory and optional branch. */ function buildProjectSegment( state: FooterSessionState, gitBranch: string | undefined, width: number, ): string | undefined { if (!state.projectName) { return undefined; } return formatProjectLabel(state.projectName, gitBranch, width); } /** Builds the thinking-level label with colors for exceptional thinking levels. */ function formatThinkingLevel( thinkingLevel: string | undefined, theme: FooterTheme, ): string | undefined { if (!thinkingLevel) { return undefined; } if (thinkingLevel === "xhigh") { return theme.fg("error", thinkingLevel); } if ( thinkingLevel === "low" || thinkingLevel === "minimal" || thinkingLevel === "off" ) { return theme.fg("warning", thinkingLevel); } return thinkingLevel; } /** Builds the slash-delimited provider, model, and thinking-level segment. */ function buildModelDisplaySegment( config: FooterConfig, renderState: FooterRenderState, sessionState: FooterSessionState, theme: FooterTheme, ): string | undefined { const showProvider = config.showProvider || (config.showModel && config.showThinkingLevel); const parts = [ showProvider ? sessionState.model?.provider : undefined, config.showModel ? sessionState.model?.id : undefined, config.showThinkingLevel ? formatThinkingLevel(renderState.thinkingLevel, theme) : undefined, ].filter((part): part is string => Boolean(part)); return parts.length === 0 ? undefined : parts.join("/"); } /** Builds the fast-mode suffix shown in the model segment. */ function buildCodexFastSuffix( footerData: FooterData, theme: FooterTheme, ): string { return footerData.getExtensionStatuses().get(CODEX_FAST_STATUS_KEY) === CODEX_FAST_ENABLED_STATUS ? `-${theme.fg("accent", "F")}` : ""; } /** Selects the context usage color from used context percentage. */ function getContextUsageColor( usedTokens: number, contextWindow: number, ): "warning" | "error" | undefined { const usedPercent = (usedTokens / contextWindow) * PERCENT_FACTOR; if (usedPercent >= CONTEXT_ERROR_USED_PERCENT) { return "error"; } if (usedPercent >= CONTEXT_WARNING_USED_PERCENT) { return "warning"; } return undefined; } /** Builds token context usage without owning context calculation. */ function buildContextSegment( state: FooterRenderState, theme: FooterTheme, compactionSettings: FooterCompactionSettings | undefined, ): string | undefined { const contextWindow = state.contextUsage?.contextWindow; if (!contextWindow) { return undefined; } const compactionLimit = calculateCompactionLimit( contextWindow, compactionSettings, ); const contextWindowParts = [ compactionLimit === undefined ? undefined : formatTokens(compactionLimit), formatTokens(contextWindow), ].filter((part): part is string => Boolean(part)); const usedTokens = state.contextUsage?.tokens; if (usedTokens === undefined || usedTokens === null) { return ["?", ...contextWindowParts].join("/"); } const segment = [formatTokens(usedTokens), ...contextWindowParts].join("/"); const color = getContextUsageColor(usedTokens, contextWindow); return color ? theme.fg(color, segment) : segment; } /** Converts native compaction reserve into the used-token limit shown in the footer. */ function calculateCompactionLimit( contextWindow: number, settings: FooterCompactionSettings | undefined, ): number | undefined { if (settings === undefined) { return undefined; } return Math.max(0, contextWindow - settings.reserveTokens); } /** Normalizes status text because footer statuses must stay on one terminal row. */ function sanitizeStatusText(text: string): string { return text .replace(/[\r\n\t]/g, " ") .replace(/ +/g, " ") .trim(); } /** Identifies an MCP wrapper status that belongs on the primary footer line. */ function isPrimaryLineMcpStatus(key: string): boolean { return MCP_STATUS_KEY_PATTERN.test(key); } /** Identifies extension status entries already represented on the primary line. */ function isStatusConsumedByPrimaryLine(key: string): boolean { return PRIMARY_LINE_STATUS_KEYS.has(key) || isPrimaryLineMcpStatus(key); } /** Reads session-owned state at render time so footer output follows active session changes. */ function readFooterRenderState( pi: ExtensionAPI, ctx: FooterSessionContext, ): FooterRenderState { return { agentLabel: getAgentRuntimeComposition(pi).getMainAgentContribution()?.agent?.id ?? NO_AGENT_LABEL, thinkingLevel: pi.getThinkingLevel(), contextUsage: getProjectionAwareContextUsage( ctx.sessionManager.getSessionId(), ctx.getContextUsage(), ), }; } /** Reads one allowed extension status by key and normalizes it for one-row footer rendering. */ function buildStatusSegmentByKey( footerData: FooterData, key: string, ): string | undefined { const value = footerData.getExtensionStatuses().get(key); if (!value) { return undefined; } const sanitizedValue = sanitizeStatusText(value); return sanitizedValue || undefined; } /** Builds the API cost segment from the cached complete root-family total. */ function buildApiCostSegment( config: FooterConfig, ctx: FooterSessionContext, sessionState: FooterSessionState, apiCost: number | undefined, ): string | undefined { if (!config.showApiCost || apiCost === undefined) { return undefined; } const usingSubscription = sessionState.model ? ctx.modelRegistry.isUsingOAuth(sessionState.model) : false; if (!apiCost && !usingSubscription) { return undefined; } return `$${apiCost.toFixed(API_COST_DECIMAL_PLACES)}`; } /** Builds the processed-token segment from the cached root-family total. */ function buildApiTokensSegment( config: FooterConfig, usageTotals: UsageSessionTotals | undefined, ): string | undefined { return config.showApiTokens && usageTotals !== undefined ? `T${formatUsageTokenCount(usageTotals.tokens)}` : undefined; } /** Builds enabled root-family usage segments in display order. */ function buildUsageSegments( config: FooterConfig, ctx: FooterSessionContext, sessionState: FooterSessionState, usageTotals: UsageSessionTotals | undefined, ): string[] { return [ buildApiCostSegment(config, ctx, sessionState, usageTotals?.cost), buildApiTokensSegment(config, usageTotals), ].filter((segment): segment is string => segment !== undefined); } /** Builds the latest prompt cache hit rate using pi's normalized assistant usage. */ function buildCacheHitRateSegment( config: FooterConfig, ctx: FooterSessionContext, ): string | undefined { if (!config.showCacheHitRate) { return undefined; } let hasCacheActivity = false; let latestCacheHitRate: number | undefined; for (const entry of ctx.sessionManager.getEntries()) { if (entry.type !== "message" || entry.message.role !== "assistant") { continue; } const { input, cacheRead, cacheWrite } = entry.message.usage; hasCacheActivity ||= cacheRead > 0 || cacheWrite > 0; const promptTokens = input + cacheRead + cacheWrite; latestCacheHitRate = promptTokens > 0 ? (cacheRead / promptTokens) * PERCENT_FACTOR : undefined; } return hasCacheActivity && latestCacheHitRate !== undefined ? `CH${Math.round(latestCacheHitRate)}` : undefined; } /** Builds the agent segment from the runtime contribution used for prompt composition. */ function buildAgentSegment(renderState: FooterRenderState): string { return sanitizeStatusText(renderState.agentLabel) || NO_AGENT_LABEL; } /** Builds MCP status segments that report user-visible errors. */ function buildMcpStatusSegments(footerData: FooterData): string[] { const segments: string[] = []; for (const [key, value] of footerData.getExtensionStatuses().entries()) { const sanitizedValue = sanitizeStatusText(value); if (sanitizedValue && isPrimaryLineMcpStatus(key)) { segments.push(sanitizedValue); } } return segments; } /** Builds one line from statuses that have no representation on the primary line. */ function buildAdditionalStatusLine( footerData: FooterData, width: number, ): string | undefined { const statuses = [...footerData.getExtensionStatuses().entries()] .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)) .map(([key, value]) => [key, sanitizeStatusText(value)] as const) .filter( ([key, value]) => value.length > 0 && !isStatusConsumedByPrimaryLine(key), ) .map(([, value]) => value); if (statuses.length === 0) { return undefined; } return truncateToWidth(statuses.join(" "), width) || undefined; } /** Calculates the remaining width that the project segment may use without hiding runtime status segments. */ function calculateProjectSegmentWidth( width: number, prioritySegments: readonly string[], ): number { if (prioritySegments.length === 0) { return width; } return ( width - visibleWidth(prioritySegments.join(SEGMENT_SEPARATOR)) - visibleWidth(SEGMENT_SEPARATOR) ); } /** Fits the model segment after all fixed-priority footer segments. */ function buildBoundedModelDisplaySegment( rawSegment: string | undefined, fastSuffix: string, width: number, fixedPrioritySegments: readonly string[], ): string | undefined { if (rawSegment === undefined) { return undefined; } const fixedWidth = visibleWidth( fixedPrioritySegments.join(SEGMENT_SEPARATOR), ); return truncateTextByWidth( `${rawSegment}${fastSuffix}`, width - fixedWidth - (fixedPrioritySegments.length > 0 ? visibleWidth(SEGMENT_SEPARATOR) : 0), ); } /** Builds footer lines from extension-owned status values and session-owned display state. */ function renderFooterLines({ config, compactionSettings, footerData, usageTotals, ctx, renderState, sessionState, theme, width, }: FooterRenderOptions): string[] { const cacheHitRateSegment = buildCacheHitRateSegment(config, ctx); const usageSegments = buildUsageSegments( config, ctx, sessionState, usageTotals, ); const fixedPrioritySegments = [ buildStatusSegmentByKey(footerData, CODEX_QUOTA_STATUS_KEY), ...usageSegments, buildAgentSegment(renderState), cacheHitRateSegment, buildStatusSegmentByKey(footerData, CONTEXT_PROJECTION_STATUS_KEY), ...buildMcpStatusSegments(footerData), buildContextSegment(renderState, theme, compactionSettings), ].filter((part): part is string => Boolean(part)); const modelDisplaySegment = buildBoundedModelDisplaySegment( buildModelDisplaySegment(config, renderState, sessionState, theme), buildCodexFastSuffix(footerData, theme), width, fixedPrioritySegments, ); const prioritySegments = [ buildStatusSegmentByKey(footerData, CODEX_QUOTA_STATUS_KEY), ...usageSegments, buildAgentSegment(renderState), modelDisplaySegment, cacheHitRateSegment, buildStatusSegmentByKey(footerData, CONTEXT_PROJECTION_STATUS_KEY), ...buildMcpStatusSegments(footerData), buildContextSegment(renderState, theme, compactionSettings), ].filter((part): part is string => Boolean(part)); const projectSegment = buildProjectSegment( sessionState, config.showGitBranch ? (footerData.getGitBranch() ?? undefined) : undefined, calculateProjectSegmentWidth(width, prioritySegments), ); const parts = [projectSegment, ...prioritySegments].filter( (part): part is string => Boolean(part), ); const lines = parts.length === 0 ? [] : [truncateTextByWidth(parts.join(SEGMENT_SEPARATOR), width)]; const additionalStatusLine = config.showAdditionalStatusLine ? buildAdditionalStatusLine(footerData, width) : undefined; if (additionalStatusLine) { lines.push(additionalStatusLine); } return lines; } /** Warns once when complete usage becomes unavailable for one root session. */ function usageTotalsEqual( left: UsageSessionTotals | undefined, right: UsageSessionTotals | undefined, ): boolean { if (left === undefined || right === undefined) { return left === right; } return left.cost === right.cost && left.tokens === right.tokens; } function notifyUsageUnavailable( ctx: FooterSessionContext, state: FooterSessionState, rootSessionId: string, ): void { if (state.warnedUsageRootSessionIds.has(rootSessionId)) { return; } state.warnedUsageRootSessionIds.add(rootSessionId); ctx.ui.notify(USAGE_UNAVAILABLE_WARNING, "warning"); } interface CreateFooterComponentOptions { readonly config: FooterConfig; readonly compactionSettings: FooterCompactionSettings | undefined; readonly initialUsageTotals: UsageSessionTotals | undefined; readonly rootSessionId: string | undefined; readonly pi: ExtensionAPI; readonly ctx: FooterSessionContext; readonly footerData: FooterData; readonly state: FooterSessionState; readonly theme: FooterTheme; readonly tui: FooterTui; } /** Creates the footer component installed into the active pi session. */ function createFooterComponent({ config, compactionSettings, initialUsageTotals, rootSessionId, pi, ctx, footerData, state, theme, tui, }: CreateFooterComponentOptions): FooterComponent { const requestRender = () => tui.requestRender(); let agentLabel = readFooterRenderState(pi, ctx).agentLabel; const unsubscribeFromAgentChanges = (pi.events as FooterEventBus).on( MAIN_AGENT_CONTRIBUTION_CHANGE_EVENT, () => { const nextAgentLabel = readFooterRenderState(pi, ctx).agentLabel; if (nextAgentLabel === agentLabel) { return; } agentLabel = nextAgentLabel; requestRender(); }, ); state.requestRender = requestRender; let usageTotals = initialUsageTotals; let disposed = false; const refreshTimer = rootSessionId === undefined || initialUsageTotals === undefined ? undefined : setInterval(() => { if (disposed) { return; } const refreshedUsageTotals = requestUsageRootTotals( pi, rootSessionId, ); if (usageTotalsEqual(usageTotals, refreshedUsageTotals)) { return; } usageTotals = refreshedUsageTotals; if (usageTotals === undefined) { notifyUsageUnavailable(ctx, state, rootSessionId); } requestRender(); }, USAGE_REFRESH_INTERVAL_MS); return { dispose() { disposed = true; if (refreshTimer !== undefined) { clearInterval(refreshTimer); } unsubscribeFromAgentChanges(); if (state.requestRender === requestRender) { state.requestRender = undefined; } }, render(width: number) { return renderFooterLines({ config, compactionSettings, footerData, usageTotals, ctx, renderState: readFooterRenderState(pi, ctx), sessionState: state, theme, width, }); }, invalidate() {}, }; } /** Installs the footer component for one active session. */ async function installSessionFooter( pi: ExtensionAPI, ctx: FooterSessionContext, state: FooterSessionState, ): Promise { if (ctx.hasUI === false) { return; } const config = await readFooterConfig(); if (config.kind !== "enabled") { return; } const nativeCompactionSettings = readNativeCompactionSettings(ctx.cwd); const compactionSettings = nativeCompactionSettings.status === "enabled" ? nativeCompactionSettings : undefined; state.projectName = await resolveProjectName(pi, ctx.cwd); state.model = ctx.model; const rootSessionId = (config.config.showApiCost || config.config.showApiTokens) && !isChildAgentProcess(readFooterProcessEnvironment()) ? ctx.sessionManager.getSessionId().trim() : undefined; const initialUsageTotals = rootSessionId ? requestUsageRootTotals(pi, rootSessionId) : undefined; if (rootSessionId && initialUsageTotals === undefined) { notifyUsageUnavailable(ctx, state, rootSessionId); } ctx.ui.setFooter((tui, theme, footerData) => createFooterComponent({ config: config.config, compactionSettings, initialUsageTotals, rootSessionId, pi, ctx, footerData, state, theme, tui, }), ); } /** Keeps unavailable-usage warnings unique across cache-free footer reloads. */ function getWarnedUsageRootSessionIds(): Set { const lifetime = process as unknown as Record; const existing = lifetime[FOOTER_PROCESS_STATE_KEY]; if (existing instanceof Set) { return existing as Set; } const warnedRootSessionIds = new Set(); lifetime[FOOTER_PROCESS_STATE_KEY] = warnedRootSessionIds; return warnedRootSessionIds; } /** Extension entry point for custom footer runtime behavior. */ export default function footer(pi: ExtensionAPI): void { const state: FooterSessionState = { projectName: undefined, model: undefined, requestRender: undefined, warnedUsageRootSessionIds: getWarnedUsageRootSessionIds(), }; pi.on("model_select", async (event) => { state.model = event.model; state.requestRender?.(); }); pi.on("session_start", async (_event, ctx) => { await installSessionFooter(pi, ctx, state); }); }