@zachwill/pi-orchestrate 0.2.0 → 0.3.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 +9 -9
- package/extension/catalog.ts +40 -22
- package/extension/contract.ts +4 -4
- package/extension/delivery.ts +72 -16
- package/extension/domain.ts +21 -55
- package/extension/host.ts +1 -37
- package/extension/index.ts +39 -11
- package/extension/presentation.ts +62 -158
- package/extension/runtime.ts +241 -332
- package/extension/tools.ts +206 -214
- package/extension/worker-session.ts +190 -46
- package/extension/worker-settlement.ts +106 -0
- package/package.json +3 -2
package/extension/index.ts
CHANGED
|
@@ -22,14 +22,19 @@ import {
|
|
|
22
22
|
registerOrchestrationPresentation,
|
|
23
23
|
type StatusController,
|
|
24
24
|
} from "./presentation.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
registerOrchestrationTools,
|
|
27
|
+
type DispatchDecision,
|
|
28
|
+
} from "./tools.js";
|
|
26
29
|
|
|
27
30
|
const DISPATCH_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
28
31
|
"orchestrate",
|
|
29
32
|
"worker_send",
|
|
30
33
|
]);
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
interface StoredDispatchDecision extends DispatchDecision {
|
|
36
|
+
readonly ownerSessionId: string;
|
|
37
|
+
}
|
|
33
38
|
|
|
34
39
|
interface OwnerBinding {
|
|
35
40
|
readonly ownerSessionId: string;
|
|
@@ -52,7 +57,7 @@ export function createOrchestrationExtension(
|
|
|
52
57
|
const statusController =
|
|
53
58
|
dependencies.createStatusController?.(host.runtime) ??
|
|
54
59
|
createStatusController(host.runtime);
|
|
55
|
-
const
|
|
60
|
+
const dispatchDecisions = new Map<string, StoredDispatchDecision>();
|
|
56
61
|
let hostAttachment: ProcessHostAttachment | undefined;
|
|
57
62
|
let activeBinding: OwnerBinding | undefined;
|
|
58
63
|
let cachedCatalog: WorkerCatalog | undefined;
|
|
@@ -69,7 +74,8 @@ export function createOrchestrationExtension(
|
|
|
69
74
|
registerOrchestrationTools(pi, {
|
|
70
75
|
runtime: host.runtime,
|
|
71
76
|
getCatalog: catalogFor,
|
|
72
|
-
|
|
77
|
+
getDispatchDecision: (toolCallId) =>
|
|
78
|
+
dispatchDecisions.get(toolCallId) ?? { mode: "inline" },
|
|
73
79
|
});
|
|
74
80
|
registerOrchestrationPresentation(pi);
|
|
75
81
|
|
|
@@ -83,7 +89,7 @@ export function createOrchestrationExtension(
|
|
|
83
89
|
statusController.unbind(activeBinding.ownerSessionId);
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
dispatchDecisions.clear();
|
|
87
93
|
cachedCatalog = undefined;
|
|
88
94
|
const binding: OwnerBinding = {
|
|
89
95
|
ownerSessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -117,17 +123,39 @@ export function createOrchestrationExtension(
|
|
|
117
123
|
const toolCalls = event.message.content.filter(
|
|
118
124
|
(part) => part.type === "toolCall",
|
|
119
125
|
);
|
|
120
|
-
const
|
|
126
|
+
const ownerSessionId = activeBinding?.ownerSessionId;
|
|
127
|
+
if (!ownerSessionId) return;
|
|
128
|
+
const groupedOrchestration =
|
|
129
|
+
toolCalls.length > 1 &&
|
|
130
|
+
toolCalls.every((toolCall) => toolCall.name === "orchestrate");
|
|
131
|
+
const synthesisGroup = groupedOrchestration
|
|
132
|
+
? { id: `orchestrate:${toolCalls[0]?.id ?? "group"}`, size: toolCalls.length }
|
|
133
|
+
: undefined;
|
|
121
134
|
|
|
122
135
|
for (const toolCall of toolCalls) {
|
|
123
|
-
if (DISPATCH_TOOL_NAMES.has(toolCall.name))
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
if (!DISPATCH_TOOL_NAMES.has(toolCall.name)) continue;
|
|
137
|
+
const mode = groupedOrchestration || toolCalls.length === 1
|
|
138
|
+
? "async"
|
|
139
|
+
: "inline";
|
|
140
|
+
dispatchDecisions.set(toolCall.id, {
|
|
141
|
+
mode,
|
|
142
|
+
ownerSessionId,
|
|
143
|
+
...(toolCall.name === "orchestrate" && synthesisGroup
|
|
144
|
+
? { synthesisGroup }
|
|
145
|
+
: {}),
|
|
146
|
+
});
|
|
126
147
|
}
|
|
127
148
|
});
|
|
128
149
|
|
|
129
150
|
pi.on("tool_execution_end", (event) => {
|
|
130
|
-
|
|
151
|
+
const decision = dispatchDecisions.get(event.toolCallId);
|
|
152
|
+
dispatchDecisions.delete(event.toolCallId);
|
|
153
|
+
if (!event.isError || !decision?.synthesisGroup) return;
|
|
154
|
+
host.delivery.skipSynthesisGroupMember(
|
|
155
|
+
decision.ownerSessionId,
|
|
156
|
+
decision.synthesisGroup.id,
|
|
157
|
+
decision.synthesisGroup.size,
|
|
158
|
+
);
|
|
131
159
|
});
|
|
132
160
|
|
|
133
161
|
pi.on("agent_start", () => {
|
|
@@ -152,7 +180,7 @@ export function createOrchestrationExtension(
|
|
|
152
180
|
const binding = activeBinding;
|
|
153
181
|
activeBinding = undefined;
|
|
154
182
|
cachedCatalog = undefined;
|
|
155
|
-
|
|
183
|
+
dispatchDecisions.clear();
|
|
156
184
|
|
|
157
185
|
if (binding) {
|
|
158
186
|
host.delivery.unbind(binding.ownerSessionId, binding.generation);
|
|
@@ -14,10 +14,14 @@ import {
|
|
|
14
14
|
visibleWidth,
|
|
15
15
|
type Component,
|
|
16
16
|
} from "@earendil-works/pi-tui";
|
|
17
|
-
import { Result
|
|
17
|
+
import { Result } from "effect";
|
|
18
18
|
import type { WorkerDeliveryDetails } from "./delivery.js";
|
|
19
|
-
import type { WorkerOutcome, WorkerRecord, WorkerStatus
|
|
19
|
+
import type { WorkerOutcome, WorkerRecord, WorkerStatus } from "./domain.js";
|
|
20
20
|
import type { OrchestratorRuntime, RuntimeSnapshot } from "./runtime.js";
|
|
21
|
+
import {
|
|
22
|
+
decodePersistedWorkerSettlementDetails,
|
|
23
|
+
type WorkerSettlementDetails,
|
|
24
|
+
} from "./worker-settlement.js";
|
|
21
25
|
|
|
22
26
|
export const ORCHESTRATION_PRESENTATION_KEY = "pi-orchestrate";
|
|
23
27
|
export const MAX_RESULT_PREVIEW_LINES = 6;
|
|
@@ -43,77 +47,7 @@ const ACTIVE_STATUSES: ReadonlySet<WorkerStatus> = new Set(["starting", "running
|
|
|
43
47
|
|
|
44
48
|
export type PresentationRuntime = Pick<OrchestratorRuntime, "snapshot" | "subscribeState">;
|
|
45
49
|
|
|
46
|
-
|
|
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
|
-
]);
|
|
85
|
-
|
|
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>;
|
|
50
|
+
type SafeSettlement = WorkerSettlementDetails;
|
|
117
51
|
|
|
118
52
|
interface StatusBinding {
|
|
119
53
|
readonly ownerSessionId: string;
|
|
@@ -130,40 +64,6 @@ export function registerOrchestrationPresentation(pi: ExtensionAPI): void {
|
|
|
130
64
|
);
|
|
131
65
|
}
|
|
132
66
|
|
|
133
|
-
export function formatResultStatusSummary(details: unknown): string {
|
|
134
|
-
const result = readSettlement(details);
|
|
135
|
-
return result ? statusHeading(result) : "Worker result details unavailable";
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function formatResultPreviews(
|
|
139
|
-
details: unknown,
|
|
140
|
-
fallbackContent = "",
|
|
141
|
-
limit = MAX_RESULT_PREVIEW_LINES,
|
|
142
|
-
): string[] {
|
|
143
|
-
if (limit <= 0) return [];
|
|
144
|
-
const result = readSettlement(details);
|
|
145
|
-
const body = result ? outcomeText(result.outcome) : fallbackContent;
|
|
146
|
-
return firstNonEmptyLines(body, limit);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function formatWorkerUsage(usage: Partial<WorkerUsage> | undefined): string | undefined {
|
|
150
|
-
if (!usage) return undefined;
|
|
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(" · ");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function formatWorkerStatusLine(worker: WorkerRecord): string {
|
|
164
|
-
return `${worker.worker} → ${worker.title} · ${workerStateLabel(worker)} · ${formatTurnMarker(worker)} · ${formatCompactNumber(numberOrZero(worker.usage?.contextTokens))} ctx`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
67
|
export function formatFooterStatus(snapshot: RuntimeSnapshot): string | undefined {
|
|
168
68
|
const ready = snapshot.workers.filter((worker) => worker.status === "ready").length;
|
|
169
69
|
return ready > 0 ? `${ready} available for follow-up` : undefined;
|
|
@@ -308,24 +208,29 @@ export class WorkerStatusComponent implements Component {
|
|
|
308
208
|
}
|
|
309
209
|
|
|
310
210
|
private workerLine(worker: WorkerRecord, width: number): string {
|
|
311
|
-
const animation = worker.status
|
|
312
|
-
? WORKER_ANIMATIONS.starting
|
|
313
|
-
: worker.status === "stopping"
|
|
314
|
-
? WORKER_ANIMATIONS.stopping
|
|
315
|
-
: WORKER_ANIMATIONS.running;
|
|
211
|
+
const animation = workerAnimation(worker.status);
|
|
316
212
|
const glyph = this.theme.fg(
|
|
317
213
|
animation.color,
|
|
318
214
|
animation.frames[this.frameIndex % animation.frames.length] ?? animation.frames[0],
|
|
319
215
|
);
|
|
320
216
|
const turns = formatTurnMarker(worker);
|
|
321
217
|
const context = `${formatContextTokens(numberOrZero(worker.usage?.contextTokens))} ctx`;
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
218
|
+
const usageFields = width >= 28 ? [turns, context] : width >= 12 ? [turns] : [];
|
|
219
|
+
const workerType = this.theme.fg("muted", this.theme.italic(worker.worker));
|
|
220
|
+
const workerTypeFits = visibleWidth(
|
|
221
|
+
`⠋ · ${worker.worker} · ${usageFields.join(" · ")}`,
|
|
222
|
+
) + 10 <= width;
|
|
223
|
+
const suffixFields = width >= 72 && workerTypeFits
|
|
224
|
+
? [workerType, ...usageFields]
|
|
225
|
+
: usageFields;
|
|
226
|
+
const prefix = `${glyph} `;
|
|
326
227
|
const suffix = suffixFields.length ? ` · ${suffixFields.join(" · ")}` : "";
|
|
327
228
|
const titleWidth = Math.max(1, width - visibleWidth(prefix) - visibleWidth(suffix));
|
|
328
|
-
const title = truncateToWidth(
|
|
229
|
+
const title = truncateToWidth(
|
|
230
|
+
this.theme.fg("text", this.theme.bold(worker.title)),
|
|
231
|
+
titleWidth,
|
|
232
|
+
"…",
|
|
233
|
+
);
|
|
329
234
|
return `${prefix}${title}${suffix}`;
|
|
330
235
|
}
|
|
331
236
|
}
|
|
@@ -371,24 +276,23 @@ export class WorkerResultComponent implements Component {
|
|
|
371
276
|
return box;
|
|
372
277
|
}
|
|
373
278
|
|
|
374
|
-
const color = details.status
|
|
279
|
+
const color = resultColor(details.status);
|
|
375
280
|
const elapsed = elapsedBetween(details.startedAt, details.settledAt);
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
281
|
+
const qualifier = resultQualifier(details);
|
|
282
|
+
const title = this.theme.bold(details.title);
|
|
283
|
+
const workerType = this.theme.fg("muted", this.theme.italic(details.worker));
|
|
284
|
+
const suffix = [qualifier, elapsed].filter(Boolean).join(" · ");
|
|
285
|
+
const header = [
|
|
286
|
+
this.theme.fg(color, `${statusIcon(details)} ${title}`),
|
|
287
|
+
workerType,
|
|
288
|
+
...(suffix ? [this.theme.fg(color, suffix)] : []),
|
|
289
|
+
].join(" · ");
|
|
382
290
|
const outcome = presentedOutcome(details);
|
|
383
|
-
box.addChild(new Text(
|
|
384
|
-
|
|
385
|
-
|
|
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) {
|
|
291
|
+
box.addChild(new Text(header, 0, 0));
|
|
292
|
+
if (outcome) {
|
|
293
|
+
box.addChild(new Spacer(1));
|
|
390
294
|
box.addChild(new WidthBoundComponent(
|
|
391
|
-
new Markdown(outcome
|
|
295
|
+
new Markdown(outcome, 0, 0, getMarkdownTheme()),
|
|
392
296
|
this.expanded ? undefined : MAX_RESULT_PREVIEW_LINES,
|
|
393
297
|
));
|
|
394
298
|
}
|
|
@@ -405,16 +309,30 @@ export class WorkerResultComponent implements Component {
|
|
|
405
309
|
}
|
|
406
310
|
|
|
407
311
|
function readSettlement(value: unknown): SafeSettlement | undefined {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
312
|
+
const decoded = decodePersistedWorkerSettlementDetails(value);
|
|
313
|
+
return Result.isSuccess(decoded) ? decoded.success : undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function workerAnimation(status: WorkerStatus) {
|
|
317
|
+
if (status === "starting") return WORKER_ANIMATIONS.starting;
|
|
318
|
+
if (status === "stopping") return WORKER_ANIMATIONS.stopping;
|
|
319
|
+
return WORKER_ANIMATIONS.running;
|
|
411
320
|
}
|
|
412
321
|
|
|
413
|
-
function
|
|
414
|
-
if (
|
|
322
|
+
function resultColor(status: SafeSettlement["status"]): "success" | "error" | "warning" {
|
|
323
|
+
if (status === "failed") return "error";
|
|
324
|
+
if (status === "aborted") return "warning";
|
|
325
|
+
return "success";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function resultQualifier(result: SafeSettlement): string | undefined {
|
|
415
329
|
if (result.status === "aborted") return "aborted";
|
|
416
|
-
if (result.status === "
|
|
417
|
-
|
|
330
|
+
if (result.status === "failed" && result.failureStage === "startup") {
|
|
331
|
+
return "could not start";
|
|
332
|
+
}
|
|
333
|
+
if (result.status === "failed") return "failed";
|
|
334
|
+
if (result.status === "ready") return "ready for follow-up";
|
|
335
|
+
return undefined;
|
|
418
336
|
}
|
|
419
337
|
|
|
420
338
|
function statusIcon(result: SafeSettlement): string {
|
|
@@ -430,27 +348,24 @@ function outcomeText(outcome: WorkerOutcome): string {
|
|
|
430
348
|
return "Worker session closed.";
|
|
431
349
|
}
|
|
432
350
|
|
|
433
|
-
function presentedOutcome(result: SafeSettlement):
|
|
351
|
+
function presentedOutcome(result: SafeSettlement): string {
|
|
434
352
|
const body = outcomeText(result.outcome);
|
|
435
|
-
if (result.status !== "completed" && result.status !== "ready") return
|
|
353
|
+
if (result.status !== "completed" && result.status !== "ready") return body;
|
|
436
354
|
|
|
437
355
|
const lines = body.split("\n");
|
|
438
356
|
const headingIndex = lines.findIndex((line) => line.trim() !== "");
|
|
439
357
|
if (headingIndex < 0 || !/^#{1,6}\s+(?:completed|complete|done)\s*#*\s*$/i.test(lines[headingIndex]!)) {
|
|
440
|
-
return
|
|
358
|
+
return body;
|
|
441
359
|
}
|
|
442
360
|
|
|
443
361
|
lines.splice(headingIndex, 1);
|
|
444
362
|
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
|
-
};
|
|
363
|
+
return lines.join("\n").trimEnd();
|
|
449
364
|
}
|
|
450
365
|
|
|
451
366
|
function settlementMetadata(result: SafeSettlement): string[] {
|
|
452
367
|
return [
|
|
453
|
-
`worker ID ${result.workerId} ·
|
|
368
|
+
`worker ID ${result.workerId} · run ID ${result.runId}`,
|
|
454
369
|
`status ${result.status} · generation ${result.generation}`,
|
|
455
370
|
`turns ${numberOrZero(result.usage.turns)} · current context ${formatCompactNumber(numberOrZero(result.usage.contextTokens))}`,
|
|
456
371
|
`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)}`,
|
|
@@ -468,14 +383,6 @@ function activeWorkers(snapshot: RuntimeSnapshot): WorkerRecord[] {
|
|
|
468
383
|
return snapshot.workers.filter((worker) => ACTIVE_STATUSES.has(worker.status));
|
|
469
384
|
}
|
|
470
385
|
|
|
471
|
-
const TOOL_ACTIVITY: Readonly<Record<string, string>> = { read: "reading", grep: "searching", find: "finding files", ls: "listing", bash: "running command", edit: "editing", write: "writing" };
|
|
472
|
-
function workerStateLabel(worker: Pick<WorkerRecord, "status" | "activity">): string {
|
|
473
|
-
if (worker.status === "starting" || worker.status === "stopping") return worker.status;
|
|
474
|
-
if (worker.status !== "running") return worker.status;
|
|
475
|
-
if (!worker.activity?.trim()) return "working";
|
|
476
|
-
return TOOL_ACTIVITY[worker.activity] ?? worker.activity;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
386
|
function formatTurnMarker(worker: Pick<WorkerRecord, "messageDirection" | "usage">): string {
|
|
480
387
|
const direction = worker.messageDirection === "from-model" ? "↓" : "↑";
|
|
481
388
|
return `${numberOrZero(worker.usage?.turns)}${direction}`;
|
|
@@ -489,9 +396,6 @@ function formatElapsed(milliseconds: number): string {
|
|
|
489
396
|
const minutes = Math.floor(seconds / 60);
|
|
490
397
|
return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m ${seconds % 60}s`;
|
|
491
398
|
}
|
|
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
399
|
function numberOrZero(value: unknown): number { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0; }
|
|
496
400
|
function formatContextTokens(value: number): string {
|
|
497
401
|
if (value < 1_000) return String(Math.round(value));
|