@zachwill/pi-orchestrate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,629 @@
1
+ import {
2
+ getMarkdownTheme,
3
+ keyHint,
4
+ type ExtensionAPI,
5
+ type ExtensionContext,
6
+ type Theme,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ Container,
10
+ Markdown,
11
+ Spacer,
12
+ Text,
13
+ truncateToWidth,
14
+ visibleWidth,
15
+ type Component,
16
+ } from "@earendil-works/pi-tui";
17
+ import type { WaveDeliveryDetails } from "./delivery.js";
18
+ import {
19
+ isWorkerCompleteForWave,
20
+ type WaveRecord,
21
+ type WorkerOutcome,
22
+ type WorkerRecord,
23
+ type WorkerStatus,
24
+ type WorkerUsage,
25
+ } from "./domain.js";
26
+ import type { OrchestratorRuntime, RuntimeSnapshot } from "./runtime.js";
27
+
28
+ export const ORCHESTRATION_PRESENTATION_KEY = "pi-orchestrate";
29
+ export const MAX_RESULT_PREVIEW_LINES = 5;
30
+ export const MAX_WIDGET_WORKERS = 8;
31
+
32
+ export type PresentationRuntime = Pick<
33
+ OrchestratorRuntime,
34
+ "snapshot" | "subscribeState"
35
+ >;
36
+
37
+ interface SafeResult {
38
+ readonly workerId: string;
39
+ readonly worker: string;
40
+ readonly title: string;
41
+ readonly status: string;
42
+ readonly outcome: WorkerOutcome;
43
+ readonly usage: Partial<WorkerUsage>;
44
+ readonly sessionFile?: string;
45
+ }
46
+
47
+ interface SafeDetails {
48
+ readonly id: string;
49
+ readonly results: readonly SafeResult[];
50
+ }
51
+
52
+ interface StatusBinding {
53
+ readonly ownerSessionId: string;
54
+ readonly ctx: ExtensionContext;
55
+ }
56
+
57
+ const ACTIVE_STATUSES: ReadonlySet<WorkerStatus> = new Set([
58
+ "starting",
59
+ "running",
60
+ "stopping",
61
+ ]);
62
+
63
+ const KNOWN_RESULT_STATUSES = new Set([
64
+ "completed",
65
+ "failed",
66
+ "aborted",
67
+ "ready",
68
+ "closed",
69
+ ]);
70
+
71
+ export function registerOrchestrationPresentation(pi: ExtensionAPI): void {
72
+ pi.registerMessageRenderer<WaveDeliveryDetails>(
73
+ "pi-orchestrate-wave",
74
+ (message, { expanded }, theme) => {
75
+ const details = readDeliveryDetails(message.details);
76
+ const content = messageText(message.content);
77
+
78
+ if (expanded) return expandedResult(content, details, theme);
79
+ return new BoundedLines(collapsedResultLines(content, details, theme));
80
+ },
81
+ );
82
+ }
83
+
84
+ export function formatResultStatusSummary(details: unknown): string {
85
+ const parsed = readDeliveryDetails(details);
86
+ if (!parsed) return "Result details unavailable";
87
+
88
+ const resultWord = parsed.results.length === 1 ? "result" : "results";
89
+ if (parsed.results.length === 0) return `0 ${resultWord}`;
90
+
91
+ const counts = new Map<string, number>();
92
+ for (const result of parsed.results) {
93
+ const status = KNOWN_RESULT_STATUSES.has(result.status) ? result.status : "unknown";
94
+ counts.set(status, (counts.get(status) ?? 0) + 1);
95
+ }
96
+
97
+ const statusOrder = ["completed", "ready", "failed", "aborted", "closed", "unknown"];
98
+ const statuses = statusOrder.flatMap((status) => {
99
+ const count = counts.get(status);
100
+ return count === undefined ? [] : [`${count} ${status}`];
101
+ });
102
+ return `${parsed.results.length} ${resultWord} · ${statuses.join(" · ")}`;
103
+ }
104
+
105
+ export function formatResultPreviews(
106
+ details: unknown,
107
+ fallbackContent = "",
108
+ limit = MAX_RESULT_PREVIEW_LINES,
109
+ ): string[] {
110
+ if (limit <= 0) return [];
111
+ const parsed = readDeliveryDetails(details);
112
+ if (!parsed || parsed.results.length === 0) {
113
+ return firstNonEmptyLines(fallbackContent, limit);
114
+ }
115
+
116
+ const visibleResults = parsed.results.slice(0, limit);
117
+ const previews = visibleResults.map((result) => {
118
+ const label = `${statusIcon(result.status)} ${result.worker} — ${result.title} · ${result.status}`;
119
+ const outcome = outcomePreview(result);
120
+ return outcome ? `${label}: ${outcome}` : label;
121
+ });
122
+
123
+ if (parsed.results.length > limit && previews.length > 0) {
124
+ previews[previews.length - 1] = `… ${parsed.results.length - limit + 1} more results`;
125
+ }
126
+ return previews;
127
+ }
128
+
129
+ export function formatWorkerUsage(usage: Partial<WorkerUsage> | undefined): string | undefined {
130
+ if (!usage) return undefined;
131
+
132
+ const parts = contextTurnUsageParts(usage);
133
+ if (isPositiveNumber(usage.input)) parts.push(`↑${formatCompactNumber(usage.input)}`);
134
+ if (isPositiveNumber(usage.output)) parts.push(`↓${formatCompactNumber(usage.output)}`);
135
+ if (isPositiveNumber(usage.cacheRead)) parts.push(`R${formatCompactNumber(usage.cacheRead)}`);
136
+ if (isPositiveNumber(usage.cacheWrite)) parts.push(`W${formatCompactNumber(usage.cacheWrite)}`);
137
+ if (isPositiveNumber(usage.cost)) parts.push(`$${usage.cost.toFixed(4)}`);
138
+ return parts.length > 0 ? parts.join(" · ") : undefined;
139
+ }
140
+
141
+ export function formatWorkerStatusLine(worker: WorkerRecord): string {
142
+ const metadata = [
143
+ worker.worker,
144
+ workerStateLabel(worker),
145
+ formatContextCost(worker.usage),
146
+ ].filter((part): part is string => Boolean(part));
147
+
148
+ return `${worker.title} · ${metadata.join(" · ")} · ${worker.id}`;
149
+ }
150
+
151
+ export function formatFooterStatus(snapshot: RuntimeSnapshot): string | undefined {
152
+ const active = snapshot.workers.filter((worker) => ACTIVE_STATUSES.has(worker.status)).length;
153
+ const ready = snapshot.workers.filter((worker) => worker.status === "ready").length;
154
+ if (active === 0 && ready === 0) return undefined;
155
+
156
+ return `Orchestrate: ${active} active · ${ready} ready`;
157
+ }
158
+
159
+ export class StatusController {
160
+ private binding: StatusBinding | undefined;
161
+ private bindingGeneration = 0;
162
+ private refreshGeneration = 0;
163
+ private disposed = false;
164
+ private unsubscribeState: (() => void) | undefined;
165
+
166
+ constructor(private readonly runtime: PresentationRuntime) {}
167
+
168
+ bind(ownerSessionId: string, ctx: ExtensionContext): void {
169
+ if (this.disposed) return;
170
+ this.clearBinding();
171
+ this.bindingGeneration += 1;
172
+ this.binding = { ownerSessionId, ctx };
173
+ this.unsubscribeState = this.runtime.subscribeState((changedOwnerSessionId) => {
174
+ if (changedOwnerSessionId !== this.binding?.ownerSessionId) return;
175
+ void this.refresh();
176
+ });
177
+ void this.refresh();
178
+ }
179
+
180
+ unbind(ownerSessionId?: string): void {
181
+ if (ownerSessionId !== undefined && ownerSessionId !== this.binding?.ownerSessionId) return;
182
+ this.clearBinding();
183
+ this.bindingGeneration += 1;
184
+ this.refreshGeneration += 1;
185
+ }
186
+
187
+ async refresh(): Promise<void> {
188
+ const binding = this.binding;
189
+ if (!binding || this.disposed) return;
190
+
191
+ const bindingGeneration = this.bindingGeneration;
192
+ const refreshGeneration = ++this.refreshGeneration;
193
+ let snapshot: RuntimeSnapshot;
194
+ try {
195
+ snapshot = await this.runtime.snapshot(binding.ownerSessionId);
196
+ } catch {
197
+ return;
198
+ }
199
+
200
+ if (
201
+ this.disposed ||
202
+ this.binding !== binding ||
203
+ this.bindingGeneration !== bindingGeneration ||
204
+ this.refreshGeneration !== refreshGeneration
205
+ ) {
206
+ return;
207
+ }
208
+
209
+ this.present(binding.ctx, snapshot);
210
+ }
211
+
212
+ dispose(): void {
213
+ if (this.disposed) return;
214
+ this.disposed = true;
215
+ this.clearBinding();
216
+ this.bindingGeneration += 1;
217
+ this.refreshGeneration += 1;
218
+ }
219
+
220
+ private present(ctx: ExtensionContext, snapshot: RuntimeSnapshot): void {
221
+ const footer = formatFooterStatus(snapshot);
222
+ ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, footer);
223
+
224
+ if (ctx.mode !== "tui") return;
225
+ if (widgetWorkerCount(snapshot) === 0) {
226
+ ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined);
227
+ return;
228
+ }
229
+
230
+ ctx.ui.setWidget(
231
+ ORCHESTRATION_PRESENTATION_KEY,
232
+ (_tui, theme) => new WorkerStatusComponent(snapshot, theme),
233
+ { placement: "aboveEditor" },
234
+ );
235
+ }
236
+
237
+ private clearBinding(): void {
238
+ this.unsubscribeState?.();
239
+ this.unsubscribeState = undefined;
240
+
241
+ const current = this.binding;
242
+ if (!current) return;
243
+
244
+ current.ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, undefined);
245
+ if (current.ctx.mode === "tui") {
246
+ current.ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined);
247
+ }
248
+ this.binding = undefined;
249
+ }
250
+ }
251
+
252
+ export function createStatusController(runtime: PresentationRuntime): StatusController {
253
+ return new StatusController(runtime);
254
+ }
255
+
256
+ class BoundedLines implements Component {
257
+ constructor(private readonly lines: readonly string[]) {}
258
+
259
+ render(width: number): string[] {
260
+ const boundedWidth = Math.max(1, width);
261
+ return this.lines.map((line) => truncateToWidth(line, boundedWidth, "…"));
262
+ }
263
+
264
+ invalidate(): void {}
265
+ }
266
+
267
+ interface WaveGroup {
268
+ readonly wave: WaveRecord;
269
+ readonly workers: readonly WorkerRecord[];
270
+ readonly settled: number;
271
+ }
272
+
273
+ class WorkerStatusComponent implements Component {
274
+ constructor(
275
+ private readonly snapshot: RuntimeSnapshot,
276
+ private readonly theme: Theme,
277
+ ) {}
278
+
279
+ render(width: number): string[] {
280
+ const boundedWidth = Math.max(1, width);
281
+ const groups = activeWaveGroups(this.snapshot);
282
+ const groupedWorkerIds = new Set(groups.flatMap((group) => group.workers.map((worker) => worker.id)));
283
+ const ready = readyWorkers(this.snapshot).filter((worker) => !groupedWorkerIds.has(worker.id));
284
+ const totalWorkers = groups.reduce((total, group) => total + group.workers.length, 0) + ready.length;
285
+ const lines: string[] = [];
286
+ let remaining = MAX_WIDGET_WORKERS;
287
+ let shown = 0;
288
+
289
+ for (const group of groups) {
290
+ if (remaining === 0) break;
291
+ const visibleWorkers = group.workers.slice(0, remaining);
292
+ if (visibleWorkers.length === 0) continue;
293
+ lines.push(this.waveHeader(group));
294
+ for (const worker of visibleWorkers) lines.push(this.workerLine(worker, boundedWidth));
295
+ shown += visibleWorkers.length;
296
+ remaining -= visibleWorkers.length;
297
+ }
298
+
299
+ if (remaining > 0) {
300
+ const visibleReady = ready.slice(0, remaining);
301
+ if (visibleReady.length > 0) {
302
+ lines.push(this.theme.fg("toolTitle", this.theme.bold(`Ready · ${ready.length}`)));
303
+ for (const worker of visibleReady) lines.push(this.workerLine(worker, boundedWidth));
304
+ shown += visibleReady.length;
305
+ }
306
+ }
307
+
308
+ if (totalWorkers > shown) {
309
+ lines.push(this.theme.fg("dim", `… ${totalWorkers - shown} more workers`));
310
+ }
311
+ return lines.map((line) => truncateToWidth(line, boundedWidth, "…"));
312
+ }
313
+
314
+ invalidate(): void {}
315
+
316
+ private waveHeader(group: WaveGroup): string {
317
+ return this.theme.fg(
318
+ "toolTitle",
319
+ this.theme.bold(
320
+ `Wave ${group.wave.id} · ${group.settled}/${group.wave.workerIds.length} settled`,
321
+ ),
322
+ );
323
+ }
324
+
325
+ private workerLine(worker: WorkerRecord, width: number): string {
326
+ const state = workerStateLabel(worker);
327
+ const title = `${statusIcon(worker.status)} ${this.theme.fg("text", this.theme.bold(worker.title))}`;
328
+ const workerType = this.theme.fg("muted", worker.worker);
329
+ const activity = this.theme.fg("text", state);
330
+ const context = isPositiveNumber(worker.usage.contextTokens)
331
+ ? this.theme.fg("dim", `${formatCompactNumber(worker.usage.contextTokens)} ctx`)
332
+ : undefined;
333
+ const cost = isPositiveNumber(worker.usage.cost)
334
+ ? this.theme.fg("dim", `$${worker.usage.cost.toFixed(4)}`)
335
+ : undefined;
336
+ const id = this.theme.fg("muted", String(worker.id));
337
+ const variants = [
338
+ [title, workerType, activity, context, cost, id],
339
+ [title, workerType, activity, context, id],
340
+ [title, workerType, activity, id],
341
+ [title, activity, id],
342
+ ].map((parts) => parts.filter((part): part is string => Boolean(part)).join(" · "));
343
+ const fitting = variants.find((line) => visibleWidth(line) <= width);
344
+ if (fitting) return fitting;
345
+
346
+ const suffix = ` · ${activity} · ${id}`;
347
+ const titleWidth = Math.max(1, width - visibleWidth(suffix));
348
+ if (titleWidth > 1) return `${truncateToWidth(title, titleWidth, "…")}${suffix}`;
349
+ return truncateToWidth(`${statusIcon(worker.status)} ${id}`, width, "…");
350
+ }
351
+ }
352
+
353
+ function collapsedResultLines(
354
+ content: string,
355
+ details: SafeDetails | undefined,
356
+ theme: Theme,
357
+ ): string[] {
358
+ const waveId = details?.id ?? "unknown wave";
359
+ const heading = `${theme.fg("toolTitle", theme.bold("Worker results"))} ${theme.fg("dim", `· ${waveId}`)}`;
360
+ const summary = theme.fg("muted", formatResultStatusSummary(details));
361
+ const previews = formatResultPreviews(details, content).map((line) => theme.fg("customMessageText", line));
362
+ const hint = theme.fg("dim", keyHint("app.tools.expand", "to expand results"));
363
+ return [heading, summary, ...previews, hint];
364
+ }
365
+
366
+ function expandedResult(
367
+ content: string,
368
+ details: SafeDetails | undefined,
369
+ theme: Theme,
370
+ ): Component {
371
+ const container = new Container();
372
+ const waveId = details?.id ?? "unknown wave";
373
+ container.addChild(
374
+ new Text(
375
+ `${theme.fg("toolTitle", theme.bold("Worker results"))} ${theme.fg("dim", `· ${waveId}`)}`,
376
+ 0,
377
+ 0,
378
+ ),
379
+ );
380
+ container.addChild(new Text(theme.fg("muted", formatResultStatusSummary(details)), 0, 0));
381
+ container.addChild(new Spacer(1));
382
+
383
+ if (!details) {
384
+ container.addChild(new Markdown(content, 0, 0, getMarkdownTheme()));
385
+ container.addChild(new Spacer(1));
386
+ container.addChild(new Text(theme.fg("dim", "Structured worker metadata unavailable"), 0, 0));
387
+ return container;
388
+ }
389
+
390
+ container.addChild(new Markdown(reconstructOutcomeMarkdown(details), 0, 0, getMarkdownTheme()));
391
+ container.addChild(new Spacer(1));
392
+ container.addChild(new Text(theme.fg("toolTitle", theme.bold("Worker details")), 0, 0));
393
+
394
+ for (const result of details.results) {
395
+ const usage = formatWorkerUsage(result.usage) ?? "unavailable";
396
+ const session = result.sessionFile ?? "unavailable";
397
+ container.addChild(new Text(theme.fg("text", `${result.worker} — ${result.title}`), 0, 0));
398
+ container.addChild(
399
+ new Text(
400
+ `${theme.fg("muted", "ID")} ${result.workerId} · ${theme.fg("muted", "status")} ${result.status}`,
401
+ 0,
402
+ 0,
403
+ ),
404
+ );
405
+ container.addChild(new Text(theme.fg("dim", `usage ${usage}`), 0, 0));
406
+ container.addChild(new Text(theme.fg("dim", `session ${session}`), 0, 0));
407
+ }
408
+ return container;
409
+ }
410
+
411
+ function readDeliveryDetails(value: unknown): SafeDetails | undefined {
412
+ if (!isRecord(value) || typeof value.id !== "string" || !Array.isArray(value.results)) {
413
+ return undefined;
414
+ }
415
+
416
+ const results: SafeResult[] = [];
417
+ for (const candidate of value.results) {
418
+ if (
419
+ !isRecord(candidate) ||
420
+ typeof candidate.workerId !== "string" ||
421
+ typeof candidate.worker !== "string" ||
422
+ typeof candidate.title !== "string" ||
423
+ typeof candidate.status !== "string" ||
424
+ !KNOWN_RESULT_STATUSES.has(candidate.status) ||
425
+ !isRecord(candidate.usage)
426
+ ) {
427
+ return undefined;
428
+ }
429
+
430
+ const outcome = readOutcome(candidate.outcome, candidate.status);
431
+ if (!outcome) return undefined;
432
+ results.push({
433
+ workerId: candidate.workerId,
434
+ worker: candidate.worker,
435
+ title: candidate.title,
436
+ status: candidate.status,
437
+ outcome,
438
+ usage: candidate.usage as Partial<WorkerUsage>,
439
+ ...(typeof candidate.sessionFile === "string" ? { sessionFile: candidate.sessionFile } : {}),
440
+ });
441
+ }
442
+ return { id: value.id, results };
443
+ }
444
+
445
+ function messageText(content: unknown): string {
446
+ if (typeof content === "string") return content;
447
+ if (!Array.isArray(content)) return "";
448
+
449
+ return content.flatMap((part) => {
450
+ if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
451
+ return [part.text];
452
+ }
453
+ return [];
454
+ }).join("\n");
455
+ }
456
+
457
+ function outcomePreview(result: SafeResult): string | undefined {
458
+ const outcome = result.outcome;
459
+ const assistantText = "assistantText" in outcome
460
+ ? firstNonEmptyLines(outcome.assistantText ?? "", 1)[0]
461
+ : undefined;
462
+ const message = "message" in outcome ? outcome.message?.trim() : undefined;
463
+ if (result.status === "failed") return message;
464
+ if (result.status === "aborted") return message;
465
+ return assistantText;
466
+ }
467
+
468
+ function reconstructOutcomeMarkdown(details: SafeDetails): string {
469
+ return details.results.map((result) => {
470
+ const heading = `### ${result.worker} — ${result.title}`;
471
+ const outcome = renderOutcomeMarkdown(result.outcome);
472
+ return outcome ? `${heading}\n\n${outcome}` : heading;
473
+ }).join("\n\n");
474
+ }
475
+
476
+ function renderOutcomeMarkdown(outcome: WorkerOutcome): string {
477
+ switch (outcome.status) {
478
+ case "completed":
479
+ case "ready":
480
+ return outcome.assistantText;
481
+ case "failed":
482
+ return outcome.assistantText
483
+ ? `Failed: ${outcome.message}\n\n${outcome.assistantText}`
484
+ : `Failed: ${outcome.message}`;
485
+ case "aborted": {
486
+ const reason = outcome.message ? `Aborted: ${outcome.message}` : "Aborted";
487
+ return outcome.assistantText ? `${reason}\n\n${outcome.assistantText}` : reason;
488
+ }
489
+ case "closed":
490
+ return "Closed";
491
+ }
492
+ }
493
+
494
+ function readOutcome(value: unknown, status: string): WorkerOutcome | undefined {
495
+ if (!isRecord(value) || value.status !== status) return undefined;
496
+ if (status === "completed" || status === "ready") {
497
+ return typeof value.assistantText === "string"
498
+ ? { status, assistantText: value.assistantText }
499
+ : undefined;
500
+ }
501
+ if (status === "failed") {
502
+ if (typeof value.message !== "string") return undefined;
503
+ return {
504
+ status: "failed",
505
+ message: value.message,
506
+ ...(typeof value.assistantText === "string" ? { assistantText: value.assistantText } : {}),
507
+ };
508
+ }
509
+ if (status === "aborted") {
510
+ return {
511
+ status: "aborted",
512
+ ...(typeof value.message === "string" ? { message: value.message } : {}),
513
+ ...(typeof value.assistantText === "string" ? { assistantText: value.assistantText } : {}),
514
+ };
515
+ }
516
+ if (status === "closed") return { status: "closed" };
517
+ return undefined;
518
+ }
519
+
520
+ function firstNonEmptyLines(text: string, limit: number): string[] {
521
+ const lines: string[] = [];
522
+ let start = 0;
523
+ for (let index = 0; index <= text.length && lines.length < limit; index += 1) {
524
+ if (index !== text.length && text[index] !== "\n") continue;
525
+ const line = text.slice(start, index).replace(/\r$/, "").trim();
526
+ if (line) lines.push(line);
527
+ start = index + 1;
528
+ }
529
+ return lines;
530
+ }
531
+
532
+ function activeWaveGroups(snapshot: RuntimeSnapshot): WaveGroup[] {
533
+ const workersById = new Map(snapshot.workers.map((worker) => [worker.id, worker]));
534
+ return snapshot.waves.flatMap((wave) => {
535
+ if (wave.state !== "running") return [];
536
+ const workers = wave.workerIds.flatMap((workerId) => {
537
+ const worker = workersById.get(workerId);
538
+ return worker?.waveId === wave.id ? [worker] : [];
539
+ });
540
+ const settled = workers.filter((worker) => isWorkerCompleteForWave(worker.status)).length;
541
+ return [{ wave, workers, settled }];
542
+ });
543
+ }
544
+
545
+ function readyWorkers(snapshot: RuntimeSnapshot): WorkerRecord[] {
546
+ return snapshot.workers.filter((worker) => worker.status === "ready");
547
+ }
548
+
549
+ function widgetWorkerCount(snapshot: RuntimeSnapshot): number {
550
+ const groups = activeWaveGroups(snapshot);
551
+ const groupedWorkerIds = new Set(groups.flatMap((group) => group.workers.map((worker) => worker.id)));
552
+ return groups.reduce((total, group) => total + group.workers.length, 0) +
553
+ readyWorkers(snapshot).filter((worker) => !groupedWorkerIds.has(worker.id)).length;
554
+ }
555
+
556
+ const TOOL_ACTIVITY: Readonly<Record<string, string>> = {
557
+ read: "reading",
558
+ grep: "searching",
559
+ find: "finding files",
560
+ ls: "listing",
561
+ bash: "running command",
562
+ edit: "editing",
563
+ write: "writing",
564
+ };
565
+
566
+ function workerStateLabel(worker: Pick<WorkerRecord, "status" | "activity">): string {
567
+ if (worker.status === "starting" || worker.status === "stopping") return worker.status;
568
+ if (worker.status !== "running") return worker.status;
569
+ if (!worker.activity?.trim()) return "thinking";
570
+ return TOOL_ACTIVITY[worker.activity] ?? worker.activity;
571
+ }
572
+
573
+ function formatContextCost(usage: Partial<WorkerUsage> | undefined): string | undefined {
574
+ if (!usage) return undefined;
575
+ const parts: string[] = [];
576
+ if (isPositiveNumber(usage.contextTokens)) parts.push(`${formatCompactNumber(usage.contextTokens)} ctx`);
577
+ if (isPositiveNumber(usage.cost)) parts.push(`$${usage.cost.toFixed(4)}`);
578
+ return parts.length > 0 ? parts.join(" · ") : undefined;
579
+ }
580
+
581
+ function contextTurnUsageParts(usage: Partial<WorkerUsage>): string[] {
582
+ const parts: string[] = [];
583
+ if (isPositiveNumber(usage.contextTokens)) parts.push(`${formatCompactNumber(usage.contextTokens)} ctx`);
584
+ if (isPositiveNumber(usage.turns)) {
585
+ parts.push(`${usage.turns} ${usage.turns === 1 ? "turn" : "turns"}`);
586
+ }
587
+ return parts;
588
+ }
589
+
590
+ function formatCompactNumber(value: number): string {
591
+ if (value >= 1_000_000) return `${trimDecimal(value / 1_000_000)}m`;
592
+ if (value >= 1_000) return `${trimDecimal(value / 1_000)}k`;
593
+ return String(Math.round(value));
594
+ }
595
+
596
+ function trimDecimal(value: number): string {
597
+ return value.toFixed(1).replace(/\.0$/, "");
598
+ }
599
+
600
+ function statusIcon(status: string): string {
601
+ switch (status) {
602
+ case "starting":
603
+ return "◌";
604
+ case "running":
605
+ return "●";
606
+ case "stopping":
607
+ return "◍";
608
+ case "ready":
609
+ return "○";
610
+ case "completed":
611
+ return "✓";
612
+ case "failed":
613
+ return "✗";
614
+ case "aborted":
615
+ return "■";
616
+ case "closed":
617
+ return "×";
618
+ default:
619
+ return "·";
620
+ }
621
+ }
622
+
623
+ function isPositiveNumber(value: unknown): value is number {
624
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
625
+ }
626
+
627
+ function isRecord(value: unknown): value is Record<string, unknown> {
628
+ return typeof value === "object" && value !== null;
629
+ }