@zachwill/pi-orchestrate 0.9.2 → 0.11.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.
@@ -15,19 +15,22 @@ import {
15
15
  type Component,
16
16
  } from "@earendil-works/pi-tui";
17
17
  import { Result } from "effect";
18
- import type { WorkerDeliveryDetails } from "./delivery.js";
19
- import type { WorkerOutcome, WorkerRecord, WorkerStatus } from "./domain.js";
20
- import type { RuntimeSnapshot } from "./runtime.js";
18
+ import type {
19
+ WorkerOutcome,
20
+ WorkerRecord,
21
+ WorkerStatus,
22
+ } from "../orchestration/model.ts";
23
+ import type { OwnerSnapshot } from "../orchestration/service.ts";
21
24
  import {
22
25
  disposeComponent,
23
26
  formatElapsed,
24
27
  resultAppearance,
25
28
  WidthBoundComponent,
26
- } from "./tui.js";
29
+ } from "./tui.ts";
27
30
  import {
28
- decodePersistedWorkerSettlementDetails,
29
- type WorkerSettlementDetails,
30
- } from "./worker-settlement.js";
31
+ decodePersistedWorkerSettlement,
32
+ type WorkerSettlement,
33
+ } from "../orchestration/settlement.ts";
31
34
 
32
35
  export const ORCHESTRATION_PRESENTATION_KEY = "pi-orchestrate";
33
36
  export const MAX_RESULT_PREVIEW_LINES = 6;
@@ -49,12 +52,17 @@ const WORKER_ANIMATIONS = {
49
52
  } as const;
50
53
  const ANIMATION_CYCLE_TICKS = 40;
51
54
  const SPINNER_INTERVAL_MS = 140;
55
+ const TURN_USAGE_MIN_WIDTH = 12;
56
+ const CONTEXT_USAGE_MIN_WIDTH = 28;
57
+ const WIDE_WORKER_ROW_MIN_WIDTH = 72;
58
+ const WORKER_NAME_SLACK_COLUMNS = 10;
52
59
  const ACTIVE_STATUSES: ReadonlySet<WorkerStatus> = new Set(["starting", "running", "stopping"]);
53
60
 
54
- export interface PresentationRuntime {
61
+ /** Owner-scoped worker state feed consumed by the parent's status presentation. */
62
+ export interface WorkerStateSource {
55
63
  subscribeState(
56
64
  ownerSessionId: string,
57
- listener: (snapshot: RuntimeSnapshot) => void,
65
+ listener: (snapshot: OwnerSnapshot) => void,
58
66
  ): () => void;
59
67
  }
60
68
 
@@ -66,14 +74,14 @@ interface StatusBinding {
66
74
  interface RenderRequester { requestRender(): void }
67
75
 
68
76
  export function registerOrchestrationPresentation(pi: ExtensionAPI): void {
69
- pi.registerMessageRenderer<WorkerDeliveryDetails>(
77
+ pi.registerMessageRenderer<WorkerSettlement>(
70
78
  "pi-orchestrate-worker-result",
71
79
  (message, { expanded }, theme) =>
72
80
  new WorkerResultComponent(messageText(message.content), message.details, expanded, theme),
73
81
  );
74
82
  }
75
83
 
76
- export function formatFooterStatus(snapshot: RuntimeSnapshot): string | undefined {
84
+ export function formatFooterStatus(snapshot: OwnerSnapshot): string | undefined {
77
85
  const ready = snapshot.workers.filter((worker) => worker.status === "ready").length;
78
86
  return ready > 0 ? `${ready} interactive ready` : undefined;
79
87
  }
@@ -84,14 +92,14 @@ export class StatusController {
84
92
  private unsubscribeState: (() => void) | undefined;
85
93
  private widget: WorkerStatusComponent | undefined;
86
94
 
87
- constructor(private readonly runtime: PresentationRuntime) {}
95
+ constructor(private readonly workerState: WorkerStateSource) {}
88
96
 
89
97
  bind(ownerSessionId: string, ctx: ExtensionContext): void {
90
98
  if (this.disposed) return;
91
99
  this.clearBinding();
92
100
  const binding = { ownerSessionId, ctx };
93
101
  this.binding = binding;
94
- this.unsubscribeState = this.runtime.subscribeState(ownerSessionId, (snapshot) => {
102
+ this.unsubscribeState = this.workerState.subscribeState(ownerSessionId, (snapshot) => {
95
103
  if (this.binding !== binding) return;
96
104
  this.present(binding.ctx, snapshot);
97
105
  });
@@ -108,7 +116,7 @@ export class StatusController {
108
116
  this.clearBinding();
109
117
  }
110
118
 
111
- private present(ctx: ExtensionContext, snapshot: RuntimeSnapshot): void {
119
+ private present(ctx: ExtensionContext, snapshot: OwnerSnapshot): void {
112
120
  ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, formatFooterStatus(snapshot));
113
121
  if (ctx.mode !== "tui") return;
114
122
  const active = activeWorkers(snapshot);
@@ -145,21 +153,23 @@ export class StatusController {
145
153
  }
146
154
  }
147
155
 
148
- export function createStatusController(runtime: PresentationRuntime): StatusController {
149
- return new StatusController(runtime);
156
+ export function createStatusController(
157
+ workerState: WorkerStateSource,
158
+ ): StatusController {
159
+ return new StatusController(workerState);
150
160
  }
151
161
 
152
162
  export class WorkerStatusComponent implements Component {
153
163
  private frameIndex = 0;
154
- private snapshot: RuntimeSnapshot;
164
+ private snapshot: OwnerSnapshot;
155
165
  private timer: ReturnType<typeof setInterval> | undefined;
156
166
 
157
- constructor(snapshot: RuntimeSnapshot, private readonly theme: Theme, private readonly tui?: RenderRequester) {
167
+ constructor(snapshot: OwnerSnapshot, private readonly theme: Theme, private readonly tui?: RenderRequester) {
158
168
  this.snapshot = snapshot;
159
169
  this.startTimer();
160
170
  }
161
171
 
162
- update(snapshot: RuntimeSnapshot): void {
172
+ update(snapshot: OwnerSnapshot): void {
163
173
  this.snapshot = snapshot;
164
174
  if (activeWorkers(snapshot).length > 0) this.startTimer();
165
175
  else this.stopTimer();
@@ -205,12 +215,14 @@ export class WorkerStatusComponent implements Component {
205
215
  );
206
216
  const turns = formatTurnMarker(worker);
207
217
  const context = `${formatContextTokens(numberOrZero(worker.usage?.contextTokens))} ctx`;
208
- const usageFields = width >= 28 ? [turns, context] : width >= 12 ? [turns] : [];
218
+ const usageFields = width >= CONTEXT_USAGE_MIN_WIDTH
219
+ ? [turns, context]
220
+ : width >= TURN_USAGE_MIN_WIDTH ? [turns] : [];
209
221
  const workerName = this.theme.fg("muted", this.theme.italic(worker.worker));
210
222
  const workerNameFits = visibleWidth(
211
223
  `⠋ · ${worker.worker} · ${usageFields.join(" · ")}`,
212
- ) + 10 <= width;
213
- const suffixFields = width >= 72 && workerNameFits
224
+ ) + WORKER_NAME_SLACK_COLUMNS <= width;
225
+ const suffixFields = width >= WIDE_WORKER_ROW_MIN_WIDTH && workerNameFits
214
226
  ? [workerName, ...usageFields]
215
227
  : usageFields;
216
228
  const prefix = `${glyph} `;
@@ -289,8 +301,8 @@ export class WorkerResultComponent implements Component {
289
301
  }
290
302
  }
291
303
 
292
- function readSettlement(value: unknown): WorkerSettlementDetails | undefined {
293
- const decoded = decodePersistedWorkerSettlementDetails(value);
304
+ function readSettlement(value: unknown): WorkerSettlement | undefined {
305
+ const decoded = decodePersistedWorkerSettlement(value);
294
306
  return Result.isSuccess(decoded) ? decoded.success : undefined;
295
307
  }
296
308
 
@@ -307,7 +319,7 @@ function outcomeText(outcome: WorkerOutcome): string {
307
319
  return "Worker session closed.";
308
320
  }
309
321
 
310
- function presentedOutcome(result: WorkerSettlementDetails): string {
322
+ function presentedOutcome(result: WorkerSettlement): string {
311
323
  const body = outcomeText(result.outcome);
312
324
  if (result.status !== "completed" && result.status !== "ready") return body;
313
325
 
@@ -322,7 +334,7 @@ function presentedOutcome(result: WorkerSettlementDetails): string {
322
334
  return lines.join("\n").trimEnd();
323
335
  }
324
336
 
325
- function settlementMetadata(result: WorkerSettlementDetails): string[] {
337
+ function settlementMetadata(result: WorkerSettlement): string[] {
326
338
  return [
327
339
  `worker ID ${result.workerId} · run ID ${result.runId}`,
328
340
  `status ${result.status} · generation ${result.generation}`,
@@ -338,7 +350,7 @@ function messageText(content: unknown): string {
338
350
  return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n");
339
351
  }
340
352
 
341
- function activeWorkers(snapshot: RuntimeSnapshot): WorkerRecord[] {
353
+ function activeWorkers(snapshot: OwnerSnapshot): WorkerRecord[] {
342
354
  return snapshot.workers.filter((worker) => ACTIVE_STATUSES.has(worker.status));
343
355
  }
344
356
 
@@ -0,0 +1,422 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import {
4
+ getMarkdownTheme,
5
+ keyHint,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import {
8
+ Container,
9
+ Markdown,
10
+ Spacer,
11
+ Text,
12
+ truncateToWidth,
13
+ wrapTextWithAnsi,
14
+ type Component,
15
+ } from "@earendil-works/pi-tui";
16
+ import { Result, Schema } from "effect";
17
+ import {
18
+ disposeComponent,
19
+ formatElapsed,
20
+ resultAppearance,
21
+ WidthBoundComponent,
22
+ } from "./tui.ts";
23
+ import {
24
+ decodeInlineWorkerToolDetails,
25
+ type InlineWorkerSettlementDetails,
26
+ WorkerSettlement,
27
+ } from "../orchestration/settlement.ts";
28
+
29
+ const MAX_INSTRUCTION_PREVIEW_LINES = 2;
30
+
31
+ const AcceptedRunRenderDetails = Schema.Struct({
32
+ mode: Schema.Literal("async"),
33
+ run_id: WorkerSettlement.fields.runId,
34
+ worker_id: WorkerSettlement.fields.workerId,
35
+ });
36
+ const UnavailableWorkerRenderDetails = Schema.Union([
37
+ Schema.Struct({ result: Schema.Unknown }),
38
+ Schema.Struct({ worker_id: Schema.Unknown }),
39
+ ]);
40
+ const decodeAcceptedRunRenderDetails = Schema.decodeUnknownResult(
41
+ AcceptedRunRenderDetails,
42
+ );
43
+ const decodeUnavailableWorkerRenderDetails = Schema.decodeUnknownResult(
44
+ UnavailableWorkerRenderDetails,
45
+ );
46
+
47
+ interface RenderOptions {
48
+ readonly isPartial: boolean;
49
+ readonly expanded: boolean;
50
+ }
51
+
52
+ interface RenderContext {
53
+ readonly isError: boolean;
54
+ readonly lastComponent?: unknown;
55
+ }
56
+
57
+ export const orchestrateToolRenderer = {
58
+ renderCall(args: unknown, theme: Theme, { expanded }: RenderOptions) {
59
+ return renderDispatchCall(theme, args, expanded);
60
+ },
61
+ renderResult(
62
+ result: AgentToolResult<unknown>,
63
+ { isPartial, expanded }: RenderOptions,
64
+ theme: Theme,
65
+ context: RenderContext,
66
+ ) {
67
+ return renderOrchestrationResult(
68
+ result,
69
+ isPartial,
70
+ expanded,
71
+ theme,
72
+ context.isError,
73
+ context.lastComponent,
74
+ );
75
+ },
76
+ };
77
+
78
+ export const workerStatusToolRenderer = {
79
+ renderCall(_args: unknown, theme: Theme) {
80
+ return new Text(theme.fg("toolTitle", theme.bold("worker_status")), 0, 0);
81
+ },
82
+ renderResult(
83
+ result: AgentToolResult<unknown>,
84
+ { isPartial }: RenderOptions,
85
+ theme: Theme,
86
+ context: RenderContext,
87
+ ) {
88
+ return renderDiagnosticsResult(result, isPartial, context.isError, theme);
89
+ },
90
+ };
91
+
92
+ export const interactiveSendToolRenderer = {
93
+ renderCall(args: unknown, theme: Theme, { expanded }: RenderOptions) {
94
+ const fields = objectFields(args);
95
+ return renderInteractiveMessageCall(
96
+ theme,
97
+ "interactive_send",
98
+ fields.worker_id,
99
+ fields.instructions,
100
+ expanded,
101
+ );
102
+ },
103
+ renderResult: orchestrateToolRenderer.renderResult,
104
+ };
105
+
106
+ export const workerAbortToolRenderer = {
107
+ renderCall(args: unknown, theme: Theme) {
108
+ const fields = objectFields(args);
109
+ const workerIds = Array.isArray(fields.worker_ids)
110
+ ? fields.worker_ids
111
+ : undefined;
112
+ const target = workerIds
113
+ ? `${workerIds.length} worker${workerIds.length === 1 ? "" : "s"}`
114
+ : fields.all === true ? "all workers" : "";
115
+ return renderCompactCall(theme, "worker_abort", target);
116
+ },
117
+ renderResult(
118
+ result: AgentToolResult<unknown>,
119
+ { isPartial }: RenderOptions,
120
+ theme: Theme,
121
+ context: RenderContext,
122
+ ) {
123
+ return renderSimpleResult(
124
+ result,
125
+ context.isError,
126
+ isPartial ? "Requesting worker stop…" : "Worker stop requested",
127
+ theme,
128
+ "warning",
129
+ );
130
+ },
131
+ };
132
+
133
+ export const interactiveCloseToolRenderer = {
134
+ renderCall(args: unknown, theme: Theme) {
135
+ return renderCompactCall(
136
+ theme,
137
+ "interactive_close",
138
+ objectFields(args).worker_id,
139
+ );
140
+ },
141
+ renderResult(
142
+ result: AgentToolResult<unknown>,
143
+ { isPartial }: RenderOptions,
144
+ theme: Theme,
145
+ context: RenderContext,
146
+ ) {
147
+ return renderSimpleResult(
148
+ result,
149
+ context.isError,
150
+ isPartial ? "Closing worker…" : "✓ Worker closed",
151
+ theme,
152
+ );
153
+ },
154
+ };
155
+
156
+ function renderDispatchCall(
157
+ theme: Theme,
158
+ task: unknown,
159
+ expanded: boolean,
160
+ ): Component {
161
+ const fields = isRecord(task) ? task : {};
162
+ const container = new Container();
163
+ container.addChild(new Text(
164
+ theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", safeTerminalText(fields.worker)),
165
+ 0, 0,
166
+ ));
167
+ container.addChild(new Text(
168
+ `${theme.fg("accent", "→")} ${theme.fg("text", theme.bold(safeTerminalText(fields.title)))}`,
169
+ 0, 0,
170
+ ));
171
+ if (expanded) {
172
+ container.addChild(new Text(safeTerminalText(fields.instructions), 2, 0));
173
+ return new WidthBoundComponent(container);
174
+ }
175
+ container.addChild(new InstructionPreview(fields.instructions, theme));
176
+ container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full instructions")), 0, 0));
177
+ return new WidthBoundComponent(container);
178
+ }
179
+
180
+ class InstructionPreview implements Component {
181
+ constructor(
182
+ private readonly instructions: unknown,
183
+ private readonly theme: Theme,
184
+ ) {}
185
+ render(width: number): string[] {
186
+ const bounded = Math.max(1, width);
187
+ const contentWidth = Math.max(1, bounded - 2);
188
+ const characterLimit = Math.max(256, Math.min(4096, contentWidth * 3));
189
+ const preview = compactInstructionPreview(this.instructions, characterLimit);
190
+ if (!preview.text) return [];
191
+
192
+ const wrapped = wrapTextWithAnsi(preview.text, contentWidth);
193
+ const previewLines = wrapped.slice(0, MAX_INSTRUCTION_PREVIEW_LINES);
194
+ if (preview.truncated || wrapped.length > MAX_INSTRUCTION_PREVIEW_LINES) {
195
+ const lastIndex = previewLines.length - 1;
196
+ previewLines[lastIndex] = truncateToWidth(`${previewLines[lastIndex] ?? ""}…`, contentWidth, "…");
197
+ }
198
+ return previewLines.map((line) =>
199
+ truncateToWidth(this.theme.fg("dim", ` ${line}`), bounded, "…")
200
+ );
201
+ }
202
+ invalidate(): void {}
203
+ }
204
+
205
+ function renderInteractiveMessageCall(
206
+ theme: Theme,
207
+ tool: string,
208
+ workerId: unknown,
209
+ instructions: unknown,
210
+ expanded: boolean,
211
+ ): Component {
212
+ const container = new Container();
213
+ container.addChild(new Text(theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(workerId)), 0, 0));
214
+ if (expanded) container.addChild(new Text(safeTerminalText(instructions), 2, 0));
215
+ else {
216
+ container.addChild(new Text(`${theme.fg("accent", "→")} ${truncateInstruction(instructions, 240)}`, 0, 0));
217
+ container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full message")), 0, 0));
218
+ }
219
+ return new WidthBoundComponent(container);
220
+ }
221
+
222
+ function safeTerminalText(value: unknown): string {
223
+ const text = typeof value === "string" ? value : value == null ? "" : String(value);
224
+ return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, (character) => {
225
+ const code = character.charCodeAt(0);
226
+ return code === 0x7f ? "␡" : String.fromCodePoint(0x2400 + code);
227
+ });
228
+ }
229
+
230
+ function compactInstructionPreview(instructions: unknown, characterLimit: number): { text: string; truncated: boolean } {
231
+ const text = typeof instructions === "string" ? instructions : instructions == null ? "" : String(instructions);
232
+ const source = text.slice(0, characterLimit);
233
+ return {
234
+ text: safeTerminalText(source).replace(/\s+/g, " ").trim(),
235
+ truncated: source.length < text.length,
236
+ };
237
+ }
238
+
239
+ function firstInstructionLine(instructions: unknown): string | undefined {
240
+ const text = typeof instructions === "string" ? instructions : instructions == null ? "" : String(instructions);
241
+ return text.split(/\r\n?|\n/).find((line) => line.trim().length > 0);
242
+ }
243
+
244
+ function truncateInstruction(instructions: unknown, limit: number): string {
245
+ const first = firstInstructionLine(instructions) ?? "";
246
+ return first.length > limit ? `${first.slice(0, limit - 1)}…` : first;
247
+ }
248
+
249
+ function renderCompactCall(theme: Theme, tool: string, target: unknown): Text {
250
+ return new Text(
251
+ theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(target)),
252
+ 0,
253
+ 0,
254
+ );
255
+ }
256
+
257
+ function renderOrchestrationResult(
258
+ result: AgentToolResult<unknown>,
259
+ isPartial: boolean,
260
+ expanded: boolean,
261
+ theme: Theme,
262
+ isError: boolean,
263
+ lastComponent: unknown,
264
+ ): Component {
265
+ if (isError) {
266
+ return new WidthBoundComponent(renderSimpleResult(
267
+ result,
268
+ true,
269
+ firstResultLine(result) || "Worker operation failed",
270
+ theme,
271
+ "warning",
272
+ ));
273
+ }
274
+ const details = result.details;
275
+ if (Result.isSuccess(decodeAcceptedRunRenderDetails(details))) {
276
+ return new WidthBoundComponent(new Text(theme.fg("success", "Sent to worker") + theme.fg("dim", " · response arrives when complete"), 0, 0));
277
+ }
278
+ const inlineResult = readInlineResult(details);
279
+ if (inlineResult) {
280
+ const component = lastComponent instanceof InlineResultComponent
281
+ ? lastComponent
282
+ : new InlineResultComponent(theme);
283
+ component.update(inlineResult, isPartial, expanded);
284
+ return component;
285
+ }
286
+ if (Result.isSuccess(decodeUnavailableWorkerRenderDetails(details))) {
287
+ return new WidthBoundComponent(new Text(theme.fg("warning", "Worker result details unavailable"), 0, 0));
288
+ }
289
+ if (isPartial) return new WidthBoundComponent(new Text(theme.fg("warning", "Sending work…"), 0, 0));
290
+ return new WidthBoundComponent(renderSimpleResult(
291
+ result,
292
+ false,
293
+ firstResultLine(result) || "Work sent",
294
+ theme,
295
+ "warning",
296
+ ));
297
+ }
298
+
299
+ interface RenderedInlineSettlement {
300
+ worker: string;
301
+ title: string;
302
+ status: InlineWorkerSettlementDetails["status"];
303
+ response: string;
304
+ elapsed?: string;
305
+ }
306
+
307
+ class InlineResultComponent implements Component {
308
+ private result: RenderedInlineSettlement | undefined;
309
+ private partial = false;
310
+ private expanded = false;
311
+ private child: Component = new Container();
312
+ constructor(private readonly theme: Theme) {}
313
+ update(result: RenderedInlineSettlement, partial: boolean, expanded: boolean): void {
314
+ this.result = result;
315
+ this.partial = partial;
316
+ this.expanded = expanded;
317
+ this.rebuild();
318
+ }
319
+ render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
320
+ invalidate(): void { this.rebuild(); }
321
+ dispose(): void { disposeComponent(this.child); }
322
+ private rebuild(): void {
323
+ disposeComponent(this.child);
324
+ const container = new Container();
325
+ const result = this.result;
326
+ if (!result) {
327
+ this.child = container;
328
+ return;
329
+ }
330
+ const appearance = resultAppearance(result.status, "ready for follow-up");
331
+ const suffix = [appearance.qualifier, result.elapsed].filter(Boolean).join(" · ");
332
+ const title = this.theme.bold(result.title);
333
+ const workerName = this.theme.fg("muted", this.theme.italic(result.worker));
334
+ const header = [
335
+ this.theme.fg(appearance.color, `${appearance.icon} ${title}`),
336
+ workerName,
337
+ ...(suffix ? [this.theme.fg(appearance.color, suffix)] : []),
338
+ ].join(" · ");
339
+ container.addChild(new WidthBoundComponent(new Text(header, 0, 0), 1));
340
+ if (result.response) {
341
+ const markdown = new Markdown(result.response, this.expanded ? 2 : 0, 0, getMarkdownTheme());
342
+ container.addChild(new WidthBoundComponent(markdown, this.expanded ? undefined : 2));
343
+ }
344
+ container.addChild(new Spacer(1));
345
+ if (this.partial) container.addChild(new Text(this.theme.fg("warning", "Receiving worker response…"), 0, 0));
346
+ else if (!this.expanded) container.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to inspect full response")), 0, 0));
347
+ this.child = container;
348
+ }
349
+ }
350
+
351
+ function readInlineResult(details: unknown): RenderedInlineSettlement | undefined {
352
+ const decoded = decodeInlineWorkerToolDetails(details);
353
+ if (Result.isFailure(decoded)) return undefined;
354
+ const settlement = decoded.success.result;
355
+ const outcome = settlement.outcome;
356
+ const message = outcome.status === "failed" || outcome.status === "aborted"
357
+ ? outcome.message
358
+ : undefined;
359
+ const assistantText = outcome.assistantText;
360
+ const response = [message, assistantText]
361
+ .filter((item): item is string => typeof item === "string" && item.length > 0)
362
+ .join("\n\n");
363
+ return {
364
+ worker: settlement.worker,
365
+ title: settlement.title,
366
+ status: settlement.status,
367
+ response,
368
+ elapsed: formatElapsed(settlement.settledAt - settlement.startedAt),
369
+ };
370
+ }
371
+
372
+ function renderDiagnosticsResult(
373
+ result: AgentToolResult<unknown>,
374
+ isPartial: boolean,
375
+ isError: boolean,
376
+ theme: Theme,
377
+ ): Text {
378
+ if (isError) {
379
+ return renderSimpleResult(
380
+ result,
381
+ true,
382
+ firstResultLine(result) || "Worker diagnostics failed",
383
+ theme,
384
+ );
385
+ }
386
+ if (isPartial) return new Text(theme.fg("muted", "Reading worker diagnostics…"), 0, 0);
387
+ const details = result.details;
388
+ if (isRecord(details) && isRecord(details.state) && Array.isArray(details.state.workers)) {
389
+ const workers = details.state.workers.filter(isRecord);
390
+ const active = workers.filter((worker) => ["starting", "running", "stopping"].includes(String(worker.status))).length;
391
+ const ready = workers.filter((worker) => worker.status === "ready").length;
392
+ const diagnostics = isRecord(details.catalog) && Array.isArray(details.catalog.diagnostics) ? details.catalog.diagnostics.length : 0;
393
+ const facts = [active ? `${active} active` : "No active workers", ready ? `${ready} available for follow-up` : undefined, diagnostics ? `${diagnostics} catalog diagnostic${diagnostics === 1 ? "" : "s"}` : undefined].filter(Boolean);
394
+ return new Text(theme.fg("muted", facts.join(" · ")), 0, 0);
395
+ }
396
+ return new Text(theme.fg("muted", firstResultLine(result) || "Diagnostics unavailable"), 0, 0);
397
+ }
398
+
399
+ function renderSimpleResult(
400
+ result: AgentToolResult<unknown>,
401
+ isError: boolean,
402
+ message: string,
403
+ theme: Theme,
404
+ normalColor: "success" | "warning" = "success",
405
+ ): Text {
406
+ const text = isError ? firstResultLine(result) || message : message;
407
+ return new Text(theme.fg(isError ? "error" : normalColor, text), 0, 0);
408
+ }
409
+
410
+ function firstResultLine(result: AgentToolResult<unknown>): string | undefined {
411
+ const first = result.content[0];
412
+ if (first?.type !== "text") return undefined;
413
+ return first.text.split("\n").find((line) => line.trim())?.trim();
414
+ }
415
+
416
+ function objectFields(value: unknown): Record<string, unknown> {
417
+ return isRecord(value) ? value : {};
418
+ }
419
+
420
+ function isRecord(value: unknown): value is Record<string, unknown> {
421
+ return typeof value === "object" && value !== null;
422
+ }