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