@zachwill/pi-orchestrate 0.1.0 → 0.1.1
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.
- package/README.md +5 -3
- package/extension/contract.ts +3 -1
- package/extension/delivery.ts +137 -89
- package/extension/domain.ts +2 -0
- package/extension/host.ts +41 -5
- package/extension/presentation.ts +304 -462
- package/extension/runtime.ts +190 -27
- package/extension/tools.ts +296 -19
- package/extension/worker-session.ts +7 -2
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type Theme,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
Box,
|
|
10
10
|
Markdown,
|
|
11
11
|
Spacer,
|
|
12
12
|
Text,
|
|
@@ -14,39 +14,40 @@ import {
|
|
|
14
14
|
visibleWidth,
|
|
15
15
|
type Component,
|
|
16
16
|
} from "@earendil-works/pi-tui";
|
|
17
|
-
import type {
|
|
18
|
-
import {
|
|
19
|
-
isWorkerCompleteForWave,
|
|
20
|
-
type WaveRecord,
|
|
21
|
-
type WorkerOutcome,
|
|
22
|
-
type WorkerRecord,
|
|
23
|
-
type WorkerStatus,
|
|
24
|
-
type WorkerUsage,
|
|
25
|
-
} from "./domain.js";
|
|
17
|
+
import type { WorkerDeliveryDetails } from "./delivery.js";
|
|
18
|
+
import type { WorkerOutcome, WorkerRecord, WorkerStatus, WorkerUsage } from "./domain.js";
|
|
26
19
|
import type { OrchestratorRuntime, RuntimeSnapshot } from "./runtime.js";
|
|
27
20
|
|
|
28
21
|
export const ORCHESTRATION_PRESENTATION_KEY = "pi-orchestrate";
|
|
29
|
-
export const MAX_RESULT_PREVIEW_LINES =
|
|
22
|
+
export const MAX_RESULT_PREVIEW_LINES = 6;
|
|
30
23
|
export const MAX_WIDGET_WORKERS = 8;
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
25
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
26
|
+
const SPINNER_INTERVAL_MS = 140;
|
|
27
|
+
const ACTIVE_STATUSES: ReadonlySet<WorkerStatus> = new Set(["starting", "running", "stopping"]);
|
|
28
|
+
|
|
29
|
+
export type PresentationRuntime = Pick<OrchestratorRuntime, "snapshot" | "subscribeState">;
|
|
30
|
+
|
|
31
|
+
interface SafeSettlement {
|
|
32
|
+
eventId?: string;
|
|
33
|
+
sequence?: number;
|
|
34
|
+
ownerSessionId: string;
|
|
35
|
+
waveId: string;
|
|
36
|
+
workerId: string;
|
|
37
|
+
generation: number;
|
|
38
|
+
mode: "async" | "inline";
|
|
39
|
+
worker: string;
|
|
40
|
+
title: string;
|
|
41
|
+
lifecycle: "one-shot" | "reusable";
|
|
42
|
+
status: "completed" | "ready" | "failed" | "aborted";
|
|
43
|
+
outcome: Exclude<WorkerOutcome, { status: "closed" }>;
|
|
44
|
+
usage: Partial<WorkerUsage>;
|
|
45
|
+
startedAt?: number;
|
|
46
|
+
settledAt?: number;
|
|
47
|
+
remainingActive?: number;
|
|
48
|
+
waveComplete?: boolean;
|
|
49
|
+
sessionFile?: string;
|
|
50
|
+
failureStage?: "startup" | "prompt" | "workflow" | "cancellation";
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
interface StatusBinding {
|
|
@@ -54,52 +55,19 @@ interface StatusBinding {
|
|
|
54
55
|
readonly ctx: ExtensionContext;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
|
|
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
|
-
]);
|
|
58
|
+
interface RenderRequester { requestRender(): void }
|
|
70
59
|
|
|
71
60
|
export function registerOrchestrationPresentation(pi: ExtensionAPI): void {
|
|
72
|
-
pi.registerMessageRenderer<
|
|
73
|
-
"pi-orchestrate-
|
|
74
|
-
(message, { expanded }, theme) =>
|
|
75
|
-
|
|
76
|
-
const content = messageText(message.content);
|
|
77
|
-
|
|
78
|
-
if (expanded) return expandedResult(content, details, theme);
|
|
79
|
-
return new BoundedLines(collapsedResultLines(content, details, theme));
|
|
80
|
-
},
|
|
61
|
+
pi.registerMessageRenderer<WorkerDeliveryDetails>(
|
|
62
|
+
"pi-orchestrate-worker-result",
|
|
63
|
+
(message, { expanded }, theme) =>
|
|
64
|
+
new WorkerResultComponent(messageText(message.content), message.details, expanded, theme),
|
|
81
65
|
);
|
|
82
66
|
}
|
|
83
67
|
|
|
84
68
|
export function formatResultStatusSummary(details: unknown): string {
|
|
85
|
-
const
|
|
86
|
-
|
|
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(" · ")}`;
|
|
69
|
+
const result = readSettlement(details);
|
|
70
|
+
return result ? statusHeading(result) : "Worker result details unavailable";
|
|
103
71
|
}
|
|
104
72
|
|
|
105
73
|
export function formatResultPreviews(
|
|
@@ -108,52 +76,32 @@ export function formatResultPreviews(
|
|
|
108
76
|
limit = MAX_RESULT_PREVIEW_LINES,
|
|
109
77
|
): string[] {
|
|
110
78
|
if (limit <= 0) return [];
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
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;
|
|
79
|
+
const result = readSettlement(details);
|
|
80
|
+
const body = result ? outcomeText(result.outcome) : fallbackContent;
|
|
81
|
+
return firstNonEmptyLines(body, limit);
|
|
127
82
|
}
|
|
128
83
|
|
|
129
84
|
export function formatWorkerUsage(usage: Partial<WorkerUsage> | undefined): string | undefined {
|
|
130
85
|
if (!usage) return undefined;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
86
|
+
const parts = [
|
|
87
|
+
`${numberOrZero(usage.turns)}t`,
|
|
88
|
+
`${formatCompactNumber(numberOrZero(usage.contextTokens))} ctx`,
|
|
89
|
+
`↑${formatCompactNumber(numberOrZero(usage.input))}`,
|
|
90
|
+
`↓${formatCompactNumber(numberOrZero(usage.output))}`,
|
|
91
|
+
`R${formatCompactNumber(numberOrZero(usage.cacheRead))}`,
|
|
92
|
+
`W${formatCompactNumber(numberOrZero(usage.cacheWrite))}`,
|
|
93
|
+
`$${numberOrZero(usage.cost).toFixed(4)}`,
|
|
94
|
+
];
|
|
95
|
+
return parts.join(" · ");
|
|
139
96
|
}
|
|
140
97
|
|
|
141
98
|
export function formatWorkerStatusLine(worker: WorkerRecord): string {
|
|
142
|
-
|
|
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}`;
|
|
99
|
+
return `${worker.worker} → ${worker.title} · ${workerStateLabel(worker)} · ${compactLiveUsage(worker.usage)}`;
|
|
149
100
|
}
|
|
150
101
|
|
|
151
102
|
export function formatFooterStatus(snapshot: RuntimeSnapshot): string | undefined {
|
|
152
|
-
const active = snapshot.workers.filter((worker) => ACTIVE_STATUSES.has(worker.status)).length;
|
|
153
103
|
const ready = snapshot.workers.filter((worker) => worker.status === "ready").length;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return `Orchestrate: ${active} active · ${ready} ready`;
|
|
104
|
+
return ready > 0 ? `${ready} available for follow-up` : undefined;
|
|
157
105
|
}
|
|
158
106
|
|
|
159
107
|
export class StatusController {
|
|
@@ -162,6 +110,9 @@ export class StatusController {
|
|
|
162
110
|
private refreshGeneration = 0;
|
|
163
111
|
private disposed = false;
|
|
164
112
|
private unsubscribeState: (() => void) | undefined;
|
|
113
|
+
private widget: WorkerStatusComponent | undefined;
|
|
114
|
+
private widgetInstalled = false;
|
|
115
|
+
private pendingSnapshot: RuntimeSnapshot | undefined;
|
|
165
116
|
|
|
166
117
|
constructor(private readonly runtime: PresentationRuntime) {}
|
|
167
118
|
|
|
@@ -170,9 +121,8 @@ export class StatusController {
|
|
|
170
121
|
this.clearBinding();
|
|
171
122
|
this.bindingGeneration += 1;
|
|
172
123
|
this.binding = { ownerSessionId, ctx };
|
|
173
|
-
this.unsubscribeState = this.runtime.subscribeState((
|
|
174
|
-
if (
|
|
175
|
-
void this.refresh();
|
|
124
|
+
this.unsubscribeState = this.runtime.subscribeState((changedOwner) => {
|
|
125
|
+
if (changedOwner === this.binding?.ownerSessionId) void this.refresh();
|
|
176
126
|
});
|
|
177
127
|
void this.refresh();
|
|
178
128
|
}
|
|
@@ -187,25 +137,11 @@ export class StatusController {
|
|
|
187
137
|
async refresh(): Promise<void> {
|
|
188
138
|
const binding = this.binding;
|
|
189
139
|
if (!binding || this.disposed) return;
|
|
190
|
-
|
|
191
140
|
const bindingGeneration = this.bindingGeneration;
|
|
192
141
|
const refreshGeneration = ++this.refreshGeneration;
|
|
193
142
|
let snapshot: RuntimeSnapshot;
|
|
194
|
-
try {
|
|
195
|
-
|
|
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
|
-
|
|
143
|
+
try { snapshot = await this.runtime.snapshot(binding.ownerSessionId); } catch { return; }
|
|
144
|
+
if (this.disposed || this.binding !== binding || this.bindingGeneration !== bindingGeneration || this.refreshGeneration !== refreshGeneration) return;
|
|
209
145
|
this.present(binding.ctx, snapshot);
|
|
210
146
|
}
|
|
211
147
|
|
|
@@ -218,33 +154,38 @@ export class StatusController {
|
|
|
218
154
|
}
|
|
219
155
|
|
|
220
156
|
private present(ctx: ExtensionContext, snapshot: RuntimeSnapshot): void {
|
|
221
|
-
|
|
222
|
-
ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, footer);
|
|
223
|
-
|
|
157
|
+
ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, formatFooterStatus(snapshot));
|
|
224
158
|
if (ctx.mode !== "tui") return;
|
|
225
|
-
|
|
226
|
-
|
|
159
|
+
const active = activeWorkers(snapshot);
|
|
160
|
+
this.pendingSnapshot = snapshot;
|
|
161
|
+
if (active.length === 0) {
|
|
162
|
+
if (this.widgetInstalled) ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined);
|
|
163
|
+
this.widget = undefined;
|
|
164
|
+
this.widgetInstalled = false;
|
|
227
165
|
return;
|
|
228
166
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)
|
|
167
|
+
if (this.widget) {
|
|
168
|
+
this.widget.update(snapshot);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (this.widgetInstalled) return;
|
|
172
|
+
ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, (tui, theme) => {
|
|
173
|
+
this.widget = new WorkerStatusComponent(this.pendingSnapshot ?? snapshot, theme, tui);
|
|
174
|
+
return this.widget;
|
|
175
|
+
}, { placement: "aboveEditor" });
|
|
176
|
+
this.widgetInstalled = true;
|
|
235
177
|
}
|
|
236
178
|
|
|
237
179
|
private clearBinding(): void {
|
|
238
180
|
this.unsubscribeState?.();
|
|
239
181
|
this.unsubscribeState = undefined;
|
|
240
|
-
|
|
182
|
+
this.widget = undefined;
|
|
183
|
+
this.pendingSnapshot = undefined;
|
|
241
184
|
const current = this.binding;
|
|
242
185
|
if (!current) return;
|
|
243
|
-
|
|
244
186
|
current.ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, undefined);
|
|
245
|
-
if (current.ctx.mode === "tui")
|
|
246
|
-
|
|
247
|
-
}
|
|
187
|
+
if (current.ctx.mode === "tui" && this.widgetInstalled) current.ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined);
|
|
188
|
+
this.widgetInstalled = false;
|
|
248
189
|
this.binding = undefined;
|
|
249
190
|
}
|
|
250
191
|
}
|
|
@@ -253,377 +194,278 @@ export function createStatusController(runtime: PresentationRuntime): StatusCont
|
|
|
253
194
|
return new StatusController(runtime);
|
|
254
195
|
}
|
|
255
196
|
|
|
256
|
-
class
|
|
257
|
-
|
|
197
|
+
export class WorkerStatusComponent implements Component {
|
|
198
|
+
private frameIndex = 0;
|
|
199
|
+
private snapshot: RuntimeSnapshot;
|
|
200
|
+
private timer: ReturnType<typeof setInterval> | undefined;
|
|
258
201
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
202
|
+
constructor(snapshot: RuntimeSnapshot, private readonly theme: Theme, private readonly tui?: RenderRequester) {
|
|
203
|
+
this.snapshot = snapshot;
|
|
204
|
+
this.startTimer();
|
|
262
205
|
}
|
|
263
206
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
readonly settled: number;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
class WorkerStatusComponent implements Component {
|
|
274
|
-
constructor(
|
|
275
|
-
private readonly snapshot: RuntimeSnapshot,
|
|
276
|
-
private readonly theme: Theme,
|
|
277
|
-
) {}
|
|
207
|
+
update(snapshot: RuntimeSnapshot): void {
|
|
208
|
+
this.snapshot = snapshot;
|
|
209
|
+
if (activeWorkers(snapshot).length > 0) this.startTimer();
|
|
210
|
+
else this.stopTimer();
|
|
211
|
+
this.tui?.requestRender();
|
|
212
|
+
}
|
|
278
213
|
|
|
279
214
|
render(width: number): string[] {
|
|
280
215
|
const boundedWidth = Math.max(1, width);
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
const lines
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
}
|
|
216
|
+
const active = activeWorkers(this.snapshot);
|
|
217
|
+
if (active.length === 0) return [];
|
|
218
|
+
const oldest = Math.min(...active.map((worker) => worker.startedAt));
|
|
219
|
+
const elapsed = formatElapsed(Math.max(0, Date.now() - oldest));
|
|
220
|
+
const lines = [this.theme.fg("toolTitle", this.theme.bold(`Workers · ${active.length} active · ${elapsed}`))];
|
|
221
|
+
for (const worker of active.slice(0, MAX_WIDGET_WORKERS)) lines.push(this.workerLine(worker, boundedWidth));
|
|
222
|
+
if (active.length > MAX_WIDGET_WORKERS) lines.push(this.theme.fg("dim", `… ${active.length - MAX_WIDGET_WORKERS} more active`));
|
|
311
223
|
return lines.map((line) => truncateToWidth(line, boundedWidth, "…"));
|
|
312
224
|
}
|
|
313
225
|
|
|
314
226
|
invalidate(): void {}
|
|
227
|
+
dispose(): void { this.stopTimer(); }
|
|
228
|
+
|
|
229
|
+
private startTimer(): void {
|
|
230
|
+
if (this.timer || activeWorkers(this.snapshot).length === 0) return;
|
|
231
|
+
this.timer = setInterval(() => {
|
|
232
|
+
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
233
|
+
this.tui?.requestRender();
|
|
234
|
+
}, SPINNER_INTERVAL_MS);
|
|
235
|
+
const timer = this.timer;
|
|
236
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer && typeof timer.unref === "function") timer.unref();
|
|
237
|
+
}
|
|
315
238
|
|
|
316
|
-
private
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
`Wave ${group.wave.id} · ${group.settled}/${group.wave.workerIds.length} settled`,
|
|
321
|
-
),
|
|
322
|
-
);
|
|
239
|
+
private stopTimer(): void {
|
|
240
|
+
if (!this.timer) return;
|
|
241
|
+
clearInterval(this.timer);
|
|
242
|
+
this.timer = undefined;
|
|
323
243
|
}
|
|
324
244
|
|
|
325
245
|
private workerLine(worker: WorkerRecord, width: number): string {
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
|
|
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, "…");
|
|
246
|
+
const glyph = this.theme.fg("warning", SPINNER_FRAMES[this.frameIndex] ?? SPINNER_FRAMES[0]);
|
|
247
|
+
const turns = `${numberOrZero(worker.usage?.turns)}t`;
|
|
248
|
+
const context = `${formatCompactNumber(numberOrZero(worker.usage?.contextTokens))} ctx`;
|
|
249
|
+
const suffixFields = width >= 28 ? [turns, context] : width >= 12 ? [turns] : [];
|
|
250
|
+
const activity = workerStateLabel(worker);
|
|
251
|
+
const requiredWidth = (fields: readonly string[]) => visibleWidth(`⠋ · ${fields.join(" · ")}`) + 10;
|
|
252
|
+
if (width >= 42 && requiredWidth([activity, ...suffixFields]) <= width) suffixFields.unshift(activity);
|
|
253
|
+
const showWorker = width >= 72 && requiredWidth([worker.worker, ...suffixFields]) <= width;
|
|
254
|
+
const prefix = showWorker ? `${glyph} ${this.theme.fg("muted", worker.worker)} → ` : `${glyph} `;
|
|
255
|
+
const suffix = suffixFields.length ? ` · ${suffixFields.join(" · ")}` : "";
|
|
256
|
+
const titleWidth = Math.max(1, width - visibleWidth(prefix) - visibleWidth(suffix));
|
|
257
|
+
const title = truncateToWidth(this.theme.fg("text", this.theme.bold(worker.title)), titleWidth, "…");
|
|
258
|
+
return `${prefix}${title}${suffix}`;
|
|
350
259
|
}
|
|
351
260
|
}
|
|
352
261
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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));
|
|
262
|
+
class WidthBoundComponent implements Component {
|
|
263
|
+
constructor(private readonly child: Component, private readonly maxLines?: number) {}
|
|
264
|
+
render(width: number): string[] {
|
|
265
|
+
const bounded = Math.max(1, Math.floor(width));
|
|
266
|
+
const lines = this.child.render(bounded);
|
|
267
|
+
const selected = this.maxLines === undefined ? lines : lines.slice(0, this.maxLines);
|
|
268
|
+
return selected.map((line) => truncateToWidth(line, bounded, "…"));
|
|
407
269
|
}
|
|
408
|
-
|
|
270
|
+
invalidate(): void { this.child.invalidate(); }
|
|
271
|
+
dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
|
|
409
272
|
}
|
|
410
273
|
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
}
|
|
274
|
+
export class WorkerResultComponent implements Component {
|
|
275
|
+
private child: Component;
|
|
429
276
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
outcome,
|
|
438
|
-
usage: candidate.usage as Partial<WorkerUsage>,
|
|
439
|
-
...(typeof candidate.sessionFile === "string" ? { sessionFile: candidate.sessionFile } : {}),
|
|
440
|
-
});
|
|
277
|
+
constructor(
|
|
278
|
+
private readonly content: string,
|
|
279
|
+
private readonly rawDetails: unknown,
|
|
280
|
+
private readonly expanded: boolean,
|
|
281
|
+
private readonly theme: Theme,
|
|
282
|
+
) {
|
|
283
|
+
this.child = this.build();
|
|
441
284
|
}
|
|
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
285
|
|
|
449
|
-
return
|
|
450
|
-
|
|
451
|
-
|
|
286
|
+
render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
|
|
287
|
+
invalidate(): void {
|
|
288
|
+
(this.child as Component & { dispose?: () => void }).dispose?.();
|
|
289
|
+
this.child = this.build();
|
|
290
|
+
}
|
|
291
|
+
dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
|
|
292
|
+
|
|
293
|
+
private build(): Component {
|
|
294
|
+
const details = readSettlement(this.rawDetails);
|
|
295
|
+
const box = new Box(1, 1, (text) => this.theme.bg("customMessageBg", text));
|
|
296
|
+
if (!details) {
|
|
297
|
+
box.addChild(new Text(this.theme.fg("warning", this.theme.bold("Worker result details unavailable")), 0, 0));
|
|
298
|
+
box.addChild(new WidthBoundComponent(new Markdown(this.content, 0, 0, getMarkdownTheme()), this.expanded ? undefined : MAX_RESULT_PREVIEW_LINES));
|
|
299
|
+
if (!this.expanded) box.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to expand")), 0, 0));
|
|
300
|
+
return box;
|
|
452
301
|
}
|
|
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
302
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
return
|
|
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;
|
|
303
|
+
const color = details.status === "failed" ? "error" : details.status === "aborted" ? "warning" : "success";
|
|
304
|
+
const elapsed = elapsedBetween(details.startedAt, details.settledAt);
|
|
305
|
+
const status = details.status === "aborted"
|
|
306
|
+
? " · aborted"
|
|
307
|
+
: details.status === "failed" && details.failureStage === "startup"
|
|
308
|
+
? " · could not start"
|
|
309
|
+
: "";
|
|
310
|
+
const header = `${statusIcon(details)} ${details.worker} · ${details.title}${status}${elapsed ? ` · ${elapsed}` : ""}`;
|
|
311
|
+
box.addChild(new Text(this.theme.fg(color, this.theme.bold(header)), 0, 0));
|
|
312
|
+
box.addChild(new Spacer(1));
|
|
313
|
+
box.addChild(new WidthBoundComponent(new Markdown(outcomeText(details.outcome), 0, 0, getMarkdownTheme()), this.expanded ? undefined : MAX_RESULT_PREVIEW_LINES));
|
|
314
|
+
if (!this.expanded) {
|
|
315
|
+
box.addChild(new Spacer(1));
|
|
316
|
+
box.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to expand")), 0, 0));
|
|
317
|
+
return box;
|
|
488
318
|
}
|
|
489
|
-
|
|
490
|
-
|
|
319
|
+
box.addChild(new Spacer(1));
|
|
320
|
+
box.addChild(new Text(this.theme.fg("toolTitle", this.theme.bold("Worker details")), 0, 0));
|
|
321
|
+
for (const line of settlementMetadata(details)) box.addChild(new Text(this.theme.fg("dim", line), 0, 0));
|
|
322
|
+
return box;
|
|
491
323
|
}
|
|
492
324
|
}
|
|
493
325
|
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
326
|
+
function readSettlement(value: unknown): SafeSettlement | undefined {
|
|
327
|
+
const candidate = isRecord(value) && isRecord(value.settlement) ? value.settlement : value;
|
|
328
|
+
if (!isRecord(candidate)) return undefined;
|
|
329
|
+
const mode = enumField(candidate.mode, ["async", "inline"]);
|
|
330
|
+
const lifecycle = enumField(candidate.lifecycle, ["one-shot", "reusable"]);
|
|
331
|
+
const status = enumField(candidate.status, ["completed", "ready", "failed", "aborted"]);
|
|
332
|
+
const outcome = readOutcome(candidate.outcome);
|
|
333
|
+
const usage = readUsage(candidate.usage);
|
|
334
|
+
const generation = nonnegativeInteger(candidate.generation);
|
|
335
|
+
if (!mode || !lifecycle || !status || !outcome || outcome.status !== status || !usage || generation === undefined) return undefined;
|
|
336
|
+
const ownerSessionId = requiredString(candidate.ownerSessionId);
|
|
337
|
+
const waveId = requiredString(candidate.waveId);
|
|
338
|
+
const workerId = requiredString(candidate.workerId);
|
|
339
|
+
const worker = requiredString(candidate.worker);
|
|
340
|
+
const title = requiredString(candidate.title);
|
|
341
|
+
if (ownerSessionId === undefined || waveId === undefined || workerId === undefined || worker === undefined || title === undefined) return undefined;
|
|
342
|
+
const eventId = optionalString(candidate.eventId);
|
|
343
|
+
const sessionFile = optionalString(candidate.sessionFile);
|
|
344
|
+
const sequence = optionalInteger(candidate.sequence);
|
|
345
|
+
const startedAt = optionalInteger(candidate.startedAt);
|
|
346
|
+
const settledAt = optionalInteger(candidate.settledAt);
|
|
347
|
+
const remainingActive = optionalInteger(candidate.remainingActive);
|
|
348
|
+
const waveComplete = optionalBoolean(candidate.waveComplete);
|
|
349
|
+
const failureStage = optionalEnum(candidate.failureStage, ["startup", "prompt", "workflow", "cancellation"]);
|
|
350
|
+
if (eventId === INVALID || sessionFile === INVALID || sequence === INVALID || startedAt === INVALID || settledAt === INVALID ||
|
|
351
|
+
remainingActive === INVALID || waveComplete === INVALID || failureStage === INVALID) return undefined;
|
|
352
|
+
if (startedAt === undefined || settledAt === undefined || settledAt < startedAt) return undefined;
|
|
353
|
+
if (failureStage !== undefined && status !== "failed" && status !== "aborted") return undefined;
|
|
354
|
+
return {
|
|
355
|
+
ownerSessionId, waveId, workerId, generation, mode, worker, title, lifecycle, status, outcome, usage,
|
|
356
|
+
...(eventId === undefined ? {} : { eventId }),
|
|
357
|
+
...(sequence === undefined ? {} : { sequence }),
|
|
358
|
+
...(startedAt === undefined ? {} : { startedAt }),
|
|
359
|
+
...(settledAt === undefined ? {} : { settledAt }),
|
|
360
|
+
...(remainingActive === undefined ? {} : { remainingActive }),
|
|
361
|
+
...(waveComplete === undefined ? {} : { waveComplete }),
|
|
362
|
+
...(sessionFile === undefined ? {} : { sessionFile }),
|
|
363
|
+
...(failureStage === undefined ? {} : { failureStage }),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function readOutcome(value: unknown): WorkerOutcome | undefined {
|
|
368
|
+
if (!isRecord(value) || typeof value.status !== "string") return undefined;
|
|
369
|
+
if ((value.status === "completed" || value.status === "ready") && typeof value.assistantText === "string") return { status: value.status, assistantText: value.assistantText };
|
|
370
|
+
const message = optionalString(value.message);
|
|
371
|
+
const assistantText = optionalString(value.assistantText);
|
|
372
|
+
if (message === INVALID || assistantText === INVALID) return undefined;
|
|
373
|
+
if (value.status === "failed" && message !== undefined) return { status: "failed", message, ...(assistantText === undefined ? {} : { assistantText }) };
|
|
374
|
+
if (value.status === "aborted") return { status: "aborted", ...(message === undefined ? {} : { message }), ...(assistantText === undefined ? {} : { assistantText }) };
|
|
517
375
|
return undefined;
|
|
518
376
|
}
|
|
519
377
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
378
|
+
const INVALID = Symbol("invalid");
|
|
379
|
+
type Invalid = typeof INVALID;
|
|
380
|
+
function requiredString(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; }
|
|
381
|
+
function optionalString(value: unknown): string | undefined | Invalid { return value === undefined ? undefined : typeof value === "string" ? value : INVALID; }
|
|
382
|
+
function nonnegativeInteger(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0 ? value : undefined; }
|
|
383
|
+
function optionalInteger(value: unknown): number | undefined | Invalid { return value === undefined ? undefined : nonnegativeInteger(value) ?? INVALID; }
|
|
384
|
+
function optionalBoolean(value: unknown): boolean | undefined | Invalid { return value === undefined ? undefined : typeof value === "boolean" ? value : INVALID; }
|
|
385
|
+
function enumField<const T extends string>(value: unknown, values: readonly T[]): T | undefined { return typeof value === "string" && values.includes(value as T) ? value as T : undefined; }
|
|
386
|
+
function optionalEnum<const T extends string>(value: unknown, values: readonly T[]): T | undefined | Invalid { return value === undefined ? undefined : enumField(value, values) ?? INVALID; }
|
|
387
|
+
function readUsage(value: unknown): WorkerUsage | undefined {
|
|
388
|
+
if (!isRecord(value)) return undefined;
|
|
389
|
+
const input = nonnegativeNumber(value.input);
|
|
390
|
+
const output = nonnegativeNumber(value.output);
|
|
391
|
+
const cacheRead = nonnegativeNumber(value.cacheRead);
|
|
392
|
+
const cacheWrite = nonnegativeNumber(value.cacheWrite);
|
|
393
|
+
const cost = nonnegativeNumber(value.cost);
|
|
394
|
+
const contextTokens = nonnegativeNumber(value.contextTokens);
|
|
395
|
+
const turns = nonnegativeInteger(value.turns);
|
|
396
|
+
if (input === undefined || output === undefined || cacheRead === undefined || cacheWrite === undefined || cost === undefined || contextTokens === undefined || turns === undefined) return undefined;
|
|
397
|
+
return { input, output, cacheRead, cacheWrite, cost, contextTokens, turns };
|
|
398
|
+
}
|
|
399
|
+
function nonnegativeNumber(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined; }
|
|
400
|
+
|
|
401
|
+
function statusHeading(result: SafeSettlement): string {
|
|
402
|
+
if (result.status === "failed") return result.failureStage === "startup" ? "could not start" : "failed";
|
|
403
|
+
if (result.status === "aborted") return "aborted";
|
|
404
|
+
if (result.status === "ready") return "response complete";
|
|
405
|
+
return "completed";
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function statusIcon(result: SafeSettlement): string {
|
|
409
|
+
if (result.status === "failed") return "✗";
|
|
410
|
+
if (result.status === "aborted") return "■";
|
|
411
|
+
return "✓";
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function outcomeText(outcome: WorkerOutcome): string {
|
|
415
|
+
if (outcome.status === "completed" || outcome.status === "ready") return outcome.assistantText;
|
|
416
|
+
if (outcome.status === "failed") return outcome.assistantText ? `${outcome.message}\n\n${outcome.assistantText}` : outcome.message;
|
|
417
|
+
if (outcome.status === "aborted") return [outcome.message || "Worker was aborted.", outcome.assistantText].filter(Boolean).join("\n\n");
|
|
418
|
+
return "Worker session closed.";
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function settlementMetadata(result: SafeSettlement): string[] {
|
|
422
|
+
return [
|
|
423
|
+
`worker ID ${result.workerId} · wave ID ${result.waveId}`,
|
|
424
|
+
`status ${result.status} · generation ${result.generation}`,
|
|
425
|
+
`turns ${numberOrZero(result.usage.turns)} · current context ${formatCompactNumber(numberOrZero(result.usage.contextTokens))}`,
|
|
426
|
+
`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)}`,
|
|
427
|
+
`session ${result.sessionFile ?? "unavailable"}`,
|
|
428
|
+
];
|
|
543
429
|
}
|
|
544
430
|
|
|
545
|
-
function
|
|
546
|
-
|
|
431
|
+
function messageText(content: unknown): string {
|
|
432
|
+
if (typeof content === "string") return content;
|
|
433
|
+
if (!Array.isArray(content)) return "";
|
|
434
|
+
return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n");
|
|
547
435
|
}
|
|
548
436
|
|
|
549
|
-
function
|
|
550
|
-
|
|
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;
|
|
437
|
+
function activeWorkers(snapshot: RuntimeSnapshot): WorkerRecord[] {
|
|
438
|
+
return snapshot.workers.filter((worker) => ACTIVE_STATUSES.has(worker.status));
|
|
554
439
|
}
|
|
555
440
|
|
|
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
|
-
|
|
441
|
+
const TOOL_ACTIVITY: Readonly<Record<string, string>> = { read: "reading", grep: "searching", find: "finding files", ls: "listing", bash: "running command", edit: "editing", write: "writing" };
|
|
566
442
|
function workerStateLabel(worker: Pick<WorkerRecord, "status" | "activity">): string {
|
|
567
443
|
if (worker.status === "starting" || worker.status === "stopping") return worker.status;
|
|
568
444
|
if (worker.status !== "running") return worker.status;
|
|
569
|
-
if (!worker.activity?.trim()) return "
|
|
445
|
+
if (!worker.activity?.trim()) return "working";
|
|
570
446
|
return TOOL_ACTIVITY[worker.activity] ?? worker.activity;
|
|
571
447
|
}
|
|
572
448
|
|
|
573
|
-
function
|
|
574
|
-
|
|
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;
|
|
449
|
+
function compactLiveUsage(usage: Partial<WorkerUsage> | undefined): string {
|
|
450
|
+
return `${numberOrZero(usage?.turns)}t · ${formatCompactNumber(numberOrZero(usage?.contextTokens))} ctx`;
|
|
579
451
|
}
|
|
580
|
-
|
|
581
|
-
|
|
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;
|
|
452
|
+
function elapsedBetween(start?: number, end?: number): string | undefined {
|
|
453
|
+
return start !== undefined && end !== undefined && end >= start ? formatElapsed(end - start) : undefined;
|
|
588
454
|
}
|
|
589
|
-
|
|
455
|
+
function formatElapsed(milliseconds: number): string {
|
|
456
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
457
|
+
if (seconds < 60) return `${seconds}s`;
|
|
458
|
+
const minutes = Math.floor(seconds / 60);
|
|
459
|
+
return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m ${seconds % 60}s`;
|
|
460
|
+
}
|
|
461
|
+
function firstNonEmptyLines(text: string, limit: number): string[] {
|
|
462
|
+
return text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, limit);
|
|
463
|
+
}
|
|
464
|
+
function numberOrZero(value: unknown): number { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0; }
|
|
590
465
|
function formatCompactNumber(value: number): string {
|
|
591
466
|
if (value >= 1_000_000) return `${trimDecimal(value / 1_000_000)}m`;
|
|
592
467
|
if (value >= 1_000) return `${trimDecimal(value / 1_000)}k`;
|
|
593
468
|
return String(Math.round(value));
|
|
594
469
|
}
|
|
595
|
-
|
|
596
|
-
function
|
|
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
|
-
}
|
|
470
|
+
function trimDecimal(value: number): string { return value.toFixed(1).replace(/\.0$/, ""); }
|
|
471
|
+
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null; }
|