import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { selectWorkingPhrase } from "./activity.js"; import { aggregateMetrics, type UsageMessage } from "./metrics.js"; import type { ActivityState, BarConfig, BarState } from "./types.js"; export interface RuntimeDependencies { pi: ExtensionAPI; ctx: ExtensionContext; config: BarConfig; autoCompact: boolean | null; random?: () => number; requestRender(): void; } export class BarRuntime { readonly #pi: ExtensionAPI; readonly #ctx: ExtensionContext; readonly #autoCompact: boolean | null; readonly #random: () => number; readonly #requestRender: () => void; #config: BarConfig; #disposed = false; #state: BarState; constructor(dependencies: RuntimeDependencies) { this.#pi = dependencies.pi; this.#ctx = dependencies.ctx; this.#config = dependencies.config; this.#autoCompact = dependencies.autoCompact; this.#random = dependencies.random ?? Math.random; this.#requestRender = dependencies.requestRender; const context = this.#ctx.getContextUsage(); this.#state = { activity: "ready", dirty: false, metrics: aggregateMetrics([], { subscription: false, autoCompact: this.#autoCompact, ...(context ? { context } : {}), }), extensionStatuses: [], }; this.refreshUsage(); } getState(): BarState { return this.#state; } getConfig(): BarConfig { return this.#config; } setConfig(config: BarConfig): void { this.#config = config; this.#invalidate(); } setActivity(activity: ActivityState): void { if (this.#state.activity === activity) return; this.#state = activity === "working" ? { ...this.#state, activity, workingLabel: selectWorkingPhrase(this.#random()) } : { ...this.#state, activity }; this.#invalidate(); } refreshUsage(): void { if (this.#disposed) return; const messages: UsageMessage[] = []; for (const entry of this.#ctx.sessionManager.getEntries()) { if (entry.type === "message" && entry.message.role === "assistant") { messages.push(entry.message as UsageMessage); } } const model = this.#ctx.model; const context = this.#ctx.getContextUsage(); const subscription = model ? this.#ctx.modelRegistry.isUsingOAuth(model) : false; const { modelId: _modelId, provider: _provider, ...stateWithoutModel } = this.#state; this.#state = { ...stateWithoutModel, ...(model ? { modelId: model.id, provider: model.provider } : {}), thinkingLevel: this.#pi.getThinkingLevel?.(), metrics: aggregateMetrics(messages, { subscription, autoCompact: this.#autoCompact, ...(context ? { context } : {}), }), }; this.#invalidate(); } async refreshGitDirty(): Promise { if (this.#disposed) return; let dirty = false; try { const result = await this.#pi.exec("git", ["status", "--porcelain", "--untracked-files=no"], { timeout: 2_000, }); dirty = result.code === 0 && result.stdout.trim().length > 0; } catch { dirty = false; } if (this.#state.dirty !== dirty) { this.#state = { ...this.#state, dirty }; this.#invalidate(); } } dispose(): void { this.#disposed = true; } #invalidate(): void { if (!this.#disposed) this.#requestRender(); } }