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 { WorkerDeliveryDetails } from "./delivery.js"; import type { WorkerOutcome, WorkerRecord, WorkerStatus } from "./domain.js"; import type { OrchestratorRuntime, RuntimeSnapshot } from "./runtime.js"; import { decodePersistedWorkerSettlementDetails, type WorkerSettlementDetails, } from "./worker-settlement.js"; 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 ACTIVE_STATUSES: ReadonlySet = new Set(["starting", "running", "stopping"]); export type PresentationRuntime = Pick; type SafeSettlement = WorkerSettlementDetails; 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: RuntimeSnapshot): string | undefined { const ready = snapshot.workers.filter((worker) => worker.status === "ready").length; return ready > 0 ? `${ready} available for follow-up` : undefined; } export class StatusController { private binding: StatusBinding | undefined; private bindingGeneration = 0; private refreshGeneration = 0; private disposed = false; private unsubscribeState: (() => void) | undefined; private widget: WorkerStatusComponent | undefined; private widgetInstalled = false; private pendingSnapshot: RuntimeSnapshot | undefined; constructor(private readonly runtime: PresentationRuntime) {} bind(ownerSessionId: string, ctx: ExtensionContext): void { if (this.disposed) return; this.clearBinding(); this.bindingGeneration += 1; this.binding = { ownerSessionId, ctx }; this.unsubscribeState = this.runtime.subscribeState((changedOwner) => { if (changedOwner === this.binding?.ownerSessionId) void this.refresh(); }); void this.refresh(); } unbind(ownerSessionId?: string): void { if (ownerSessionId !== undefined && ownerSessionId !== this.binding?.ownerSessionId) return; this.clearBinding(); this.bindingGeneration += 1; this.refreshGeneration += 1; } async refresh(): Promise { const binding = this.binding; if (!binding || this.disposed) return; const bindingGeneration = this.bindingGeneration; const refreshGeneration = ++this.refreshGeneration; let snapshot: RuntimeSnapshot; try { snapshot = await this.runtime.snapshot(binding.ownerSessionId); } catch { return; } if (this.disposed || this.binding !== binding || this.bindingGeneration !== bindingGeneration || this.refreshGeneration !== refreshGeneration) return; this.present(binding.ctx, snapshot); } dispose(): void { if (this.disposed) return; this.disposed = true; this.clearBinding(); this.bindingGeneration += 1; this.refreshGeneration += 1; } private present(ctx: ExtensionContext, snapshot: RuntimeSnapshot): void { ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, formatFooterStatus(snapshot)); if (ctx.mode !== "tui") return; const active = activeWorkers(snapshot); this.pendingSnapshot = snapshot; if (active.length === 0) { if (this.widgetInstalled) ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined); this.widget = undefined; this.widgetInstalled = false; return; } if (this.widget) { this.widget.update(snapshot); return; } if (this.widgetInstalled) return; ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, (tui, theme) => { this.widget = new WorkerStatusComponent(this.pendingSnapshot ?? snapshot, theme, tui); return this.widget; }, { placement: "aboveEditor" }); this.widgetInstalled = true; } private clearBinding(): void { this.unsubscribeState?.(); this.unsubscribeState = undefined; this.widget = undefined; this.pendingSnapshot = undefined; const current = this.binding; if (!current) return; current.ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, undefined); if (current.ctx.mode === "tui" && this.widgetInstalled) current.ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined); this.widgetInstalled = false; this.binding = undefined; } } export function createStatusController(runtime: PresentationRuntime): StatusController { return new StatusController(runtime); } export class WorkerStatusComponent implements Component { private frameIndex = 0; private snapshot: RuntimeSnapshot; private timer: ReturnType | undefined; constructor(snapshot: RuntimeSnapshot, private readonly theme: Theme, private readonly tui?: RenderRequester) { this.snapshot = snapshot; this.startTimer(); } update(snapshot: RuntimeSnapshot): 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 >= 28 ? [turns, context] : width >= 12 ? [turns] : []; const workerType = this.theme.fg("muted", this.theme.italic(worker.worker)); const workerTypeFits = visibleWidth( `⠋ · ${worker.worker} · ${usageFields.join(" · ")}`, ) + 10 <= width; const suffixFields = width >= 72 && workerTypeFits ? [workerType, ...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}`; } } class WidthBoundComponent implements Component { constructor(private readonly child: Component, private readonly maxLines?: number) {} render(width: number): string[] { const bounded = Math.max(1, Math.floor(width)); const lines = this.child.render(bounded); const selected = this.maxLines === undefined ? lines : lines.slice(0, this.maxLines); return selected.map((line) => truncateToWidth(line, bounded, "…")); } invalidate(): void { this.child.invalidate(); } dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); } } 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 { (this.child as Component & { dispose?: () => void }).dispose?.(); this.child = this.build(); } dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); } 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 color = resultColor(details.status); const elapsed = elapsedBetween(details.startedAt, details.settledAt); const qualifier = resultQualifier(details); const title = this.theme.bold(details.title); const workerType = this.theme.fg("muted", this.theme.italic(details.worker)); const suffix = [qualifier, elapsed].filter(Boolean).join(" · "); const header = [ this.theme.fg(color, `${statusIcon(details)} ${title}`), workerType, ...(suffix ? [this.theme.fg(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): SafeSettlement | undefined { const decoded = decodePersistedWorkerSettlementDetails(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 resultColor(status: SafeSettlement["status"]): "success" | "error" | "warning" { if (status === "failed") return "error"; if (status === "aborted") return "warning"; return "success"; } function resultQualifier(result: SafeSettlement): string | undefined { if (result.status === "aborted") return "aborted"; if (result.status === "failed" && result.failureStage === "startup") { return "could not start"; } if (result.status === "failed") return "failed"; if (result.status === "ready") return "ready for follow-up"; return undefined; } function statusIcon(result: SafeSettlement): string { if (result.status === "failed") return "✗"; if (result.status === "aborted") return "■"; return "✓"; } 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: SafeSettlement): 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: SafeSettlement): string[] { return [ `worker ID ${result.workerId} · run ID ${result.runId}`, `status ${result.status} · generation ${result.generation}`, `turns ${numberOrZero(result.usage.turns)} · current context ${formatCompactNumber(numberOrZero(result.usage.contextTokens))}`, `input ${numberOrZero(result.usage.input)} · output ${numberOrZero(result.usage.output)} · cache read ${numberOrZero(result.usage.cacheRead)} · cache write ${numberOrZero(result.usage.cacheWrite)} · cost $${numberOrZero(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: RuntimeSnapshot): 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 elapsedBetween(start?: number, end?: number): string | undefined { return start !== undefined && end !== undefined && end >= start ? formatElapsed(end - start) : undefined; } function formatElapsed(milliseconds: number): string { const seconds = Math.floor(milliseconds / 1000); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m ${seconds % 60}s`; } 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; }