import { getMarkdownTheme, keyHint, type ExtensionAPI, type ExtensionContext, type Theme, } from "@earendil-works/pi-coding-agent"; import { Box, Markdown, Spacer, Text, truncateToWidth, visibleWidth, type Component, } from "@earendil-works/pi-tui"; import { Result } from "effect"; import type { WorkerOutcome, WorkerRecord, WorkerStatus, } from "../orchestration/model.ts"; import type { OwnerSnapshot } from "../orchestration/service.ts"; import { disposeComponent, formatElapsed, resultAppearance, WidthBoundComponent, } from "./tui.ts"; import { decodePersistedWorkerSettlement, type WorkerSettlement, } from "../orchestration/settlement.ts"; export const ORCHESTRATION_PRESENTATION_KEY = "pi-orchestrate"; export const MAX_RESULT_PREVIEW_LINES = 6; export const MAX_WIDGET_WORKERS = 8; const WORKER_ANIMATIONS = { starting: { frames: ["⠂", "⠌", "⡑", "⢕", "⣫", "⣿", "⣫", "⢕"], color: "muted", }, running: { frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], color: "accent", }, stopping: { frames: ["⣿", "⣶", "⣤", "⣀", "⠠", "⠐", "⠈", "⠁"], color: "dim", }, } as const; const ANIMATION_CYCLE_TICKS = 40; const SPINNER_INTERVAL_MS = 140; const TURN_USAGE_MIN_WIDTH = 12; const CONTEXT_USAGE_MIN_WIDTH = 28; const WIDE_WORKER_ROW_MIN_WIDTH = 72; const WORKER_NAME_SLACK_COLUMNS = 10; const ACTIVE_STATUSES: ReadonlySet = new Set(["starting", "running", "stopping"]); /** Owner-scoped worker state feed consumed by the parent's status presentation. */ export interface WorkerStateSource { subscribeState( ownerSessionId: string, listener: (snapshot: OwnerSnapshot) => void, ): () => void; } interface StatusBinding { readonly ownerSessionId: string; readonly ctx: ExtensionContext; } interface RenderRequester { requestRender(): void } export function registerOrchestrationPresentation(pi: ExtensionAPI): void { pi.registerMessageRenderer( "pi-orchestrate-worker-result", (message, { expanded }, theme) => new WorkerResultComponent(messageText(message.content), message.details, expanded, theme), ); } export function formatFooterStatus(snapshot: OwnerSnapshot): string | undefined { const ready = snapshot.workers.filter((worker) => worker.status === "ready").length; return ready > 0 ? `${ready} interactive ready` : undefined; } export class StatusController { private binding: StatusBinding | undefined; private disposed = false; private unsubscribeState: (() => void) | undefined; private widget: WorkerStatusComponent | undefined; constructor(private readonly workerState: WorkerStateSource) {} bind(ownerSessionId: string, ctx: ExtensionContext): void { if (this.disposed) return; this.clearBinding(); const binding = { ownerSessionId, ctx }; this.binding = binding; this.unsubscribeState = this.workerState.subscribeState(ownerSessionId, (snapshot) => { if (this.binding !== binding) return; this.present(binding.ctx, snapshot); }); } unbind(ownerSessionId?: string): void { if (ownerSessionId !== undefined && ownerSessionId !== this.binding?.ownerSessionId) return; this.clearBinding(); } dispose(): void { if (this.disposed) return; this.disposed = true; this.clearBinding(); } private present(ctx: ExtensionContext, snapshot: OwnerSnapshot): void { ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, formatFooterStatus(snapshot)); if (ctx.mode !== "tui") return; const active = activeWorkers(snapshot); if (active.length === 0) { if (this.widget !== undefined) { ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined); this.widget = undefined; } return; } if (this.widget !== undefined) { this.widget.update(snapshot); return; } ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, (tui, theme) => { const widget = new WorkerStatusComponent(snapshot, theme, tui); this.widget = widget; return widget; }, { placement: "aboveEditor" }); } private clearBinding(): void { const unsubscribeState = this.unsubscribeState; this.unsubscribeState = undefined; unsubscribeState?.(); const current = this.binding; if (!current) return; current.ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, undefined); if (current.ctx.mode === "tui" && this.widget !== undefined) { current.ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined); this.widget = undefined; } this.binding = undefined; } } export function createStatusController( workerState: WorkerStateSource, ): StatusController { return new StatusController(workerState); } export class WorkerStatusComponent implements Component { private frameIndex = 0; private snapshot: OwnerSnapshot; private timer: ReturnType | undefined; constructor(snapshot: OwnerSnapshot, private readonly theme: Theme, private readonly tui?: RenderRequester) { this.snapshot = snapshot; this.startTimer(); } update(snapshot: OwnerSnapshot): void { this.snapshot = snapshot; if (activeWorkers(snapshot).length > 0) this.startTimer(); else this.stopTimer(); this.tui?.requestRender(); } render(width: number): string[] { const boundedWidth = Math.max(1, width); const active = activeWorkers(this.snapshot); if (active.length === 0) return []; const oldest = Math.min(...active.map((worker) => worker.startedAt)); const elapsed = formatElapsed(Math.max(0, Date.now() - oldest)); const lines = [this.theme.fg("toolTitle", this.theme.bold(`Workers · ${active.length} active · ${elapsed}`))]; for (const worker of active.slice(0, MAX_WIDGET_WORKERS)) lines.push(this.workerLine(worker, boundedWidth)); if (active.length > MAX_WIDGET_WORKERS) lines.push(this.theme.fg("dim", `… ${active.length - MAX_WIDGET_WORKERS} more active`)); return lines.map((line) => truncateToWidth(line, boundedWidth, "…")); } invalidate(): void {} dispose(): void { this.stopTimer(); } private startTimer(): void { if (this.timer || activeWorkers(this.snapshot).length === 0) return; this.timer = setInterval(() => { this.frameIndex = (this.frameIndex + 1) % ANIMATION_CYCLE_TICKS; this.tui?.requestRender(); }, SPINNER_INTERVAL_MS); const timer = this.timer; if (typeof timer === "object" && timer !== null && "unref" in timer && typeof timer.unref === "function") timer.unref(); } private stopTimer(): void { if (!this.timer) return; clearInterval(this.timer); this.timer = undefined; } private workerLine(worker: WorkerRecord, width: number): string { const animation = workerAnimation(worker.status); const glyph = this.theme.fg( animation.color, animation.frames[this.frameIndex % animation.frames.length] ?? animation.frames[0], ); const turns = formatTurnMarker(worker); const context = `${formatContextTokens(numberOrZero(worker.usage?.contextTokens))} ctx`; const usageFields = width >= CONTEXT_USAGE_MIN_WIDTH ? [turns, context] : width >= TURN_USAGE_MIN_WIDTH ? [turns] : []; const workerName = this.theme.fg("muted", this.theme.italic(worker.worker)); const workerNameFits = visibleWidth( `⠋ · ${worker.worker} · ${usageFields.join(" · ")}`, ) + WORKER_NAME_SLACK_COLUMNS <= width; const suffixFields = width >= WIDE_WORKER_ROW_MIN_WIDTH && workerNameFits ? [workerName, ...usageFields] : usageFields; const prefix = `${glyph} `; const suffix = suffixFields.length ? ` · ${suffixFields.join(" · ")}` : ""; const titleWidth = Math.max(1, width - visibleWidth(prefix) - visibleWidth(suffix)); const title = truncateToWidth( this.theme.fg("text", this.theme.bold(worker.title)), titleWidth, "…", ); return `${prefix}${title}${suffix}`; } } export class WorkerResultComponent implements Component { private child: Component; constructor( private readonly content: string, private readonly rawDetails: unknown, private readonly expanded: boolean, private readonly theme: Theme, ) { this.child = this.build(); } render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); } invalidate(): void { disposeComponent(this.child); this.child = this.build(); } dispose(): void { disposeComponent(this.child); } private build(): Component { const details = readSettlement(this.rawDetails); const box = new Box(1, 1, (text) => this.theme.bg("customMessageBg", text)); if (!details) { box.addChild(new Text(this.theme.fg("warning", this.theme.bold("Worker result details unavailable")), 0, 0)); box.addChild(new WidthBoundComponent(new Markdown(this.content, 0, 0, getMarkdownTheme()), this.expanded ? undefined : MAX_RESULT_PREVIEW_LINES)); if (!this.expanded) box.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to expand")), 0, 0)); return box; } const appearance = resultAppearance( details.status, "interactive ready", details.failureStage === "startup" ? "could not start" : "failed", ); const elapsed = formatElapsed(details.settledAt - details.startedAt); const title = this.theme.bold(details.title); const workerName = this.theme.fg("muted", this.theme.italic(details.worker)); const suffix = [appearance.qualifier, elapsed].filter(Boolean).join(" · "); const header = [ this.theme.fg(appearance.color, `${appearance.icon} ${title}`), workerName, ...(suffix ? [this.theme.fg(appearance.color, suffix)] : []), ].join(" · "); const outcome = presentedOutcome(details); box.addChild(new Text(header, 0, 0)); if (outcome) { box.addChild(new Spacer(1)); box.addChild(new WidthBoundComponent( new Markdown(outcome, 0, 0, getMarkdownTheme()), this.expanded ? undefined : MAX_RESULT_PREVIEW_LINES, )); } if (!this.expanded) { box.addChild(new Spacer(1)); box.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to expand")), 0, 0)); return box; } box.addChild(new Spacer(1)); box.addChild(new Text(this.theme.fg("toolTitle", this.theme.bold("Worker details")), 0, 0)); for (const line of settlementMetadata(details)) box.addChild(new Text(this.theme.fg("dim", line), 0, 0)); return box; } } function readSettlement(value: unknown): WorkerSettlement | undefined { const decoded = decodePersistedWorkerSettlement(value); return Result.isSuccess(decoded) ? decoded.success : undefined; } function workerAnimation(status: WorkerStatus) { if (status === "starting") return WORKER_ANIMATIONS.starting; if (status === "stopping") return WORKER_ANIMATIONS.stopping; return WORKER_ANIMATIONS.running; } function outcomeText(outcome: WorkerOutcome): string { if (outcome.status === "completed" || outcome.status === "ready") return outcome.assistantText; if (outcome.status === "failed") return outcome.assistantText ? `${outcome.message}\n\n${outcome.assistantText}` : outcome.message; if (outcome.status === "aborted") return [outcome.message || "Worker was aborted.", outcome.assistantText].filter(Boolean).join("\n\n"); return "Worker session closed."; } function presentedOutcome(result: WorkerSettlement): string { const body = outcomeText(result.outcome); if (result.status !== "completed" && result.status !== "ready") return body; const lines = body.split("\n"); const headingIndex = lines.findIndex((line) => line.trim() !== ""); if (headingIndex < 0 || !/^#{1,6}\s+(?:completed|complete|done)\s*#*\s*$/i.test(lines[headingIndex]!)) { return body; } lines.splice(headingIndex, 1); while (lines[headingIndex]?.trim() === "") lines.splice(headingIndex, 1); return lines.join("\n").trimEnd(); } function settlementMetadata(result: WorkerSettlement): string[] { return [ `worker ID ${result.workerId} · run ID ${result.runId}`, `status ${result.status} · generation ${result.generation}`, `turns ${result.usage.turns} · current context ${formatCompactNumber(result.usage.contextTokens)}`, `input ${result.usage.input} · output ${result.usage.output} · cache read ${result.usage.cacheRead} · cache write ${result.usage.cacheWrite} · cost $${result.usage.cost.toFixed(4)}`, `session ${result.sessionFile ?? "unavailable"}`, ]; } function messageText(content: unknown): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n"); } function activeWorkers(snapshot: OwnerSnapshot): WorkerRecord[] { return snapshot.workers.filter((worker) => ACTIVE_STATUSES.has(worker.status)); } function formatTurnMarker(worker: Pick): string { const direction = worker.messageDirection === "from-model" ? "↓" : "↑"; return `${numberOrZero(worker.usage?.turns)}${direction}`; } function numberOrZero(value: unknown): number { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0; } function formatContextTokens(value: number): string { if (value < 1_000) return String(Math.round(value)); return `${Math.round(value / 1_000)}k`; } function formatCompactNumber(value: number): string { if (value >= 1_000_000) return `${trimDecimal(value / 1_000_000)}m`; if (value >= 1_000) return `${trimDecimal(value / 1_000)}k`; return String(Math.round(value)); } function trimDecimal(value: number): string { return value.toFixed(1).replace(/\.0$/, ""); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; }