@jameslovespancakes/pi-plus 1.0.19 → 1.0.21

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.
Files changed (45) hide show
  1. package/README.md +133 -246
  2. package/package.json +3 -2
  3. package/src/core/claude-remote/LICENSE.md +22 -0
  4. package/src/core/claude-remote/UPSTREAM.md +40 -0
  5. package/src/core/claude-remote/bridge.ts +392 -0
  6. package/src/core/claude-remote/protocol.ts +83 -0
  7. package/src/core/env.ts +7 -1
  8. package/src/domains/claude-remote/auth.ts +25 -0
  9. package/src/domains/claude-remote/index.ts +183 -0
  10. package/src/domains/claude-remote/picker.ts +36 -0
  11. package/src/domains/models/provider-picker.ts +3 -46
  12. package/src/domains/setup/index.ts +12 -1
  13. package/src/domains/workflows/index.ts +56 -104
  14. package/src/domains/workflows/runtime/advisory-challenge.ts +3 -3
  15. package/src/domains/workflows/runtime/agent-attempt.ts +3 -3
  16. package/src/domains/workflows/runtime/agent-options.ts +18 -0
  17. package/src/domains/workflows/runtime/agent-runner-types.ts +7 -3
  18. package/src/domains/workflows/runtime/agent-runner.ts +14 -6
  19. package/src/domains/workflows/runtime/agent-session.ts +34 -3
  20. package/src/domains/workflows/runtime/cancellation.ts +5 -0
  21. package/src/domains/workflows/runtime/engine.ts +19 -40
  22. package/src/domains/workflows/runtime/journal.ts +4 -4
  23. package/src/domains/workflows/runtime/live-agent.ts +37 -0
  24. package/src/domains/workflows/runtime/model-profiles.ts +2 -6
  25. package/src/domains/workflows/runtime/progress-types.ts +3 -1
  26. package/src/domains/workflows/runtime/progress.ts +69 -42
  27. package/src/domains/workflows/runtime/review/review-fix-workflow.ts +3 -3
  28. package/src/domains/workflows/runtime/types.ts +16 -15
  29. package/src/domains/workflows/runtime/ui/agent-transcript.ts +59 -0
  30. package/src/domains/workflows/runtime/ui/workflow-format.ts +6 -2
  31. package/src/domains/workflows/runtime/ui/workflow-inspector.ts +75 -61
  32. package/src/domains/workflows/runtime/ui/workflow-widget.ts +26 -66
  33. package/src/domains/workflows/runtime/workflow-advisory-utils.ts +5 -5
  34. package/src/domains/workflows/runtime/{background-workflows.ts → workflow-lifecycle.ts} +91 -75
  35. package/src/domains/workflows/runtime/workflow-management.ts +66 -0
  36. package/src/domains/workflows/runtime/workflow-run-controller.ts +16 -20
  37. package/src/domains/workflows/runtime/{workflow-run-background.ts → workflow-run-delivery.ts} +3 -3
  38. package/src/domains/workflows/runtime/workflow-run-record.ts +7 -4
  39. package/src/domains/workflows/workflows/code-review.ts +1 -1
  40. package/src/domains/workflows/workflows/diagnose.ts +1 -1
  41. package/src/domains/workflows/workflows/perf-review.ts +1 -1
  42. package/src/domains/workflows/workflows/refactor-scout.ts +1 -1
  43. package/src/domains/workflows/workflows/research.ts +4 -4
  44. package/src/ui/settings-picker.ts +26 -0
  45. package/src/domains/workflows/runtime/background-workflow-tool.ts +0 -75
@@ -1,9 +1,10 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { AgentTranscript } from "./live-agent.ts";
2
3
  import type { WorkflowProgressEvent } from "./types.ts";
3
4
  import type { AgentChatMessage, AgentChatRole, AgentRowStatus, WorkflowLaneItemStatus, WorkflowProgressSnapshot } from "./progress-types.ts";
4
- import { formatWorkflowUsageLine, type WorkflowUsageSnapshot } from "./usage.ts";
5
+ import type { WorkflowUsageSnapshot } from "./usage.ts";
5
6
  import { unknownErrorMessage } from "./unknown-error.ts";
6
- import { statusTextFromCounts, type WorkflowStatusCounts } from "./ui/workflow-format.ts";
7
+ import type { WorkflowStatusCounts } from "./ui/workflow-format.ts";
7
8
  import { toDisplayLine, toDisplayText } from "./ui/display-text.ts";
8
9
  import { renderWorkflowWidgetLines } from "./ui/workflow-widget.ts";
9
10
 
@@ -23,6 +24,8 @@ interface AgentRow {
23
24
  id: number;
24
25
  label: string;
25
26
  model?: string;
27
+ modelName?: string;
28
+ thinkingLevel?: string;
26
29
  status: AgentRowStatus;
27
30
  startedAt?: number;
28
31
  doneAt?: number;
@@ -61,7 +64,7 @@ const WIDGET_REFRESH_INTERVAL_MS = 1_000;
61
64
  export const DEFAULT_LANE_ITEM_LIMIT = 200;
62
65
 
63
66
  /**
64
- * Tracks live workflow state for widgets, footer/status text, result renderers,
67
+ * Tracks live workflow state for widgets, result renderers,
65
68
  * and headless stderr breadcrumbs.
66
69
  */
67
70
  export class ProgressTracker {
@@ -73,25 +76,34 @@ export class ProgressTracker {
73
76
  private readonly laneOverflow = new Map<string, number>();
74
77
  private readonly rowsById = new Map<number, AgentRow>();
75
78
  private readonly agentChats = new Map<number, AgentChatMessage[]>();
76
- private readonly agentFollowUps = new Map<number, (message: string) => Promise<void>>();
79
+ private readonly agentTranscripts = new Map<number, () => AgentTranscript>();
80
+ private readonly agentStops = new Map<number, () => void>();
81
+ private readonly agentFollowUps = new Map<number, (message: string, steer?: boolean) => Promise<void>>();
77
82
  private readonly listeners = new Set<() => void>();
78
- private readonly agentCounts: Record<AgentRowStatus, number> = { queued: 0, running: 0, done: 0, failed: 0 };
83
+ private readonly agentCounts: Record<AgentRowStatus, number> = { queued: 0, running: 0, stopping: 0, stopped: 0, done: 0, failed: 0 };
79
84
  private readonly startedAt = Date.now();
80
85
  private readonly laneItemLimit = laneItemLimitFromEnv();
81
86
  private doneAt: number | undefined;
82
87
  private currentPhase = "Workflow";
83
88
  private nextAgentId = 1;
84
- private lastStatusText: string | undefined;
85
89
  private usageSnapshot: WorkflowUsageSnapshot | undefined;
86
90
  private widgetRefreshInterval: ReturnType<typeof setInterval> | undefined;
87
91
  private readonly surfaceKey: string;
92
+ private readonly ctx: ExtensionContext;
93
+ private readonly title: string;
94
+ private readonly runId: string;
95
+ private readonly onSnapshot?: (snapshot: WorkflowProgressSnapshot) => void;
88
96
 
89
97
  constructor(
90
- private readonly ctx: ExtensionContext,
91
- private readonly title: string,
92
- private readonly runId: string,
93
- private readonly onSnapshot?: (snapshot: WorkflowProgressSnapshot) => void,
98
+ ctx: ExtensionContext,
99
+ title: string,
100
+ runId: string,
101
+ onSnapshot?: (snapshot: WorkflowProgressSnapshot) => void,
94
102
  ) {
103
+ this.ctx = ctx;
104
+ this.title = title;
105
+ this.runId = runId;
106
+ this.onSnapshot = onSnapshot;
95
107
  this.surfaceKey = `workflow:${runId}`;
96
108
  this.ensurePhase(this.currentPhase);
97
109
  }
@@ -154,9 +166,9 @@ export class ProgressTracker {
154
166
  this.publish();
155
167
  }
156
168
 
157
- agentQueued(phase: string | undefined, label: string, model?: string): number {
169
+ agentQueued(phase: string | undefined, label: string, model?: string, modelName?: string, thinkingLevel?: string): number {
158
170
  const id = this.nextAgentId++;
159
- const row = { label, model, id, status: "queued" as const, toolUses: 0 };
171
+ const row = { label, model, modelName, thinkingLevel, id, status: "queued" as const, toolUses: 0 };
160
172
  this.ensurePhase(phase ?? this.currentPhase).agents.push(row);
161
173
  this.rowsById.set(id, row);
162
174
  this.agentCounts.queued++;
@@ -201,7 +213,27 @@ export class ProgressTracker {
201
213
  this.publish();
202
214
  }
203
215
 
204
- bindAgentFollowUp(id: number, send: (message: string) => Promise<void>): () => void {
216
+ bindAgentTranscript(id: number, read: () => AgentTranscript): () => void {
217
+ this.agentTranscripts.set(id, read);
218
+ return () => {
219
+ const last = read();
220
+ const snapshot = { ...last, messages: [...last.messages], streaming: undefined };
221
+ this.agentTranscripts.set(id, () => snapshot);
222
+ };
223
+ }
224
+
225
+ transcript(id: number): AgentTranscript | undefined {
226
+ return this.agentTranscripts.get(id)?.();
227
+ }
228
+
229
+ agentChanged(id: number, model?: string, modelName?: string, thinkingLevel?: string): void {
230
+ const row = this.rowsById.get(id);
231
+ if (row) Object.assign(row, { model, modelName, thinkingLevel });
232
+ // Stream updates are UI-only; do not rewrite the durable run on every token.
233
+ for (const listener of this.listeners) listener();
234
+ }
235
+
236
+ bindAgentFollowUp(id: number, send: (message: string, steer?: boolean) => Promise<void>): () => void {
205
237
  this.agentFollowUps.set(id, send);
206
238
  this.publish();
207
239
  return () => {
@@ -210,16 +242,32 @@ export class ProgressTracker {
210
242
  };
211
243
  }
212
244
 
245
+ bindAgentStop(id: number, stop: () => void): () => void {
246
+ this.agentStops.set(id, stop);
247
+ return () => this.agentStops.delete(id);
248
+ }
249
+
250
+ stopAgent(id: number): void {
251
+ const row = this.rowsById.get(id);
252
+ if (!row) throw new Error(`Unknown agent ${id}.`);
253
+ if (row.status !== "running" && row.status !== "queued") return;
254
+ const stop = this.agentStops.get(id);
255
+ if (!stop) throw new Error("Agent cannot be stopped right now.");
256
+ this.transitionAgentStatus(row, "stopping");
257
+ stop();
258
+ this.publish();
259
+ }
260
+
213
261
  conversation(id: number): readonly AgentChatMessage[] {
214
262
  return (this.agentChats.get(id) ?? []).map((message) => ({ ...message }));
215
263
  }
216
264
 
217
- async followUp(id: number, message: string): Promise<void> {
218
- const text = toDisplayLine(message, AGENT_CHAT_TEXT_LIMIT);
265
+ async followUp(id: number, message: string, steer = false): Promise<void> {
266
+ const text = message.trim();
219
267
  if (!text) throw new Error("Enter a follow-up message.");
220
268
  const send = this.agentFollowUps.get(id);
221
269
  if (!send) throw new Error("This agent is no longer accepting follow-ups.");
222
- await send(text);
270
+ await send(text, steer);
223
271
  this.appendAgentChat(id, "user", text);
224
272
  this.publish();
225
273
  }
@@ -231,7 +279,7 @@ export class ProgressTracker {
231
279
 
232
280
  agentDone(label: string, id?: number): void {
233
281
  const row = this.findRow(label, id);
234
- if (row && row.status !== "failed") {
282
+ if (row && (row.status === "running" || row.status === "queued")) {
235
283
  this.transitionAgentStatus(row, "done");
236
284
  row.doneAt = Date.now();
237
285
  }
@@ -241,7 +289,7 @@ export class ProgressTracker {
241
289
  agentFailed(label: string, error: unknown, id?: number): void {
242
290
  const row = this.findRow(label, id);
243
291
  if (row) {
244
- this.transitionAgentStatus(row, "failed");
292
+ this.transitionAgentStatus(row, row.status === "stopping" ? "stopped" : "failed");
245
293
  row.doneAt = Date.now();
246
294
  row.error = toDisplayLine(unknownErrorMessage(error), AGENT_ERROR_DISPLAY_LIMIT) || "agent failed";
247
295
  this.appendAgentChat(row.id, "status", row.error);
@@ -281,10 +329,10 @@ export class ProgressTracker {
281
329
  private statusCountsSnapshot(): WorkflowStatusCounts {
282
330
  return {
283
331
  queued: this.agentCounts.queued,
284
- running: this.agentCounts.running,
332
+ running: this.agentCounts.running + this.agentCounts.stopping,
285
333
  done: this.agentCounts.done,
286
- failed: this.agentCounts.failed,
287
- total: this.agentCounts.queued + this.agentCounts.running + this.agentCounts.done + this.agentCounts.failed,
334
+ failed: this.agentCounts.failed + this.agentCounts.stopped,
335
+ total: Object.values(this.agentCounts).reduce((sum, count) => sum + count, 0),
288
336
  };
289
337
  }
290
338
 
@@ -336,7 +384,6 @@ export class ProgressTracker {
336
384
  if (!this.ctx.hasUI) return;
337
385
  this.publishWidget();
338
386
  this.startWidgetRefresh();
339
- this.publishStatus();
340
387
  }
341
388
 
342
389
  private publishWidget(): void {
@@ -358,24 +405,6 @@ export class ProgressTracker {
358
405
  this.widgetRefreshInterval = undefined;
359
406
  }
360
407
 
361
- private publishStatus(): void {
362
- const status = statusTextFromCounts(
363
- {
364
- title: this.title,
365
- doneAt: this.doneAt,
366
- currentPhase: this.currentPhase,
367
- counters: [...this.counters.values()].map((counter) => ({ ...counter })),
368
- },
369
- this.statusCountsSnapshot(),
370
- this.ctx.ui.theme,
371
- );
372
- const usage = formatWorkflowUsageLine(this.usageSnapshot);
373
- const next = usage ? `${status} · ${usage}` : status;
374
- if (next === this.lastStatusText) return;
375
- this.ctx.ui.setStatus(this.surfaceKey, next);
376
- this.lastStatusText = next;
377
- }
378
-
379
408
  /** Clear this run's live workflow surfaces. Final feedback is delivered by the result surface. */
380
409
  done(): void {
381
410
  this.doneAt = Date.now();
@@ -383,8 +412,6 @@ export class ProgressTracker {
383
412
  this.publishSnapshot();
384
413
  if (!this.ctx.hasUI) return;
385
414
  this.ctx.ui.setWidget(this.surfaceKey, undefined);
386
- this.ctx.ui.setStatus(this.surfaceKey, undefined);
387
- this.lastStatusText = undefined;
388
415
  }
389
416
 
390
417
  private publishSnapshot(): void {
@@ -32,7 +32,7 @@ export interface ReviewFixWorkflowResult {
32
32
  readonly fixes: readonly ReviewFixOutcome[];
33
33
  }
34
34
 
35
- export type ReviewFixWorkflowApi = Pick<WorkflowApi, "agent" | "parallel" | "phase" | "cwd" | "signal">;
35
+ export type ReviewFixWorkflowApi = Pick<WorkflowApi, "agent" | "modelProfile" | "parallel" | "phase" | "cwd" | "signal">;
36
36
 
37
37
  /** Build an ephemeral workflow that generates one isolated patch preview per finding. */
38
38
  export function createReviewFixWorkflow(
@@ -89,7 +89,7 @@ export async function runReviewFixWorkflow(
89
89
  isolation: "worktree",
90
90
  label: `fix:${issue.id}`,
91
91
  phase: REVIEW_FIX_PHASE,
92
- profile: "medium",
92
+ ...api.modelProfile("medium"),
93
93
  cacheKey: `review-fix:${issue.id}`,
94
94
  tools: [...REVIEW_FIX_TOOLS],
95
95
  toolHints: ["search"],
@@ -138,7 +138,7 @@ async function evaluateReviewFix(api: ReviewFixWorkflowApi, input: {
138
138
  const evaluated = await api.agent(
139
139
  `Independently evaluate a candidate repair. Your fresh worktree contains the exact reviewed baseline plus the captured patch. The implementer's report is not validation evidence. Inspect the finding, callers and tests. Reject incorrect repairs; return blocked if required validation is unavailable. Select at most six focused deterministic checks with executable and argument arrays. Require at least one meaningful behavior check. If a regression test is applicable, supply a test-only baselinePatch and specific expectedFailure so the engine can prove it fails before the repair and passes after. Do not edit, install dependencies, commit or change branches. The engine will execute checks independently.\nFinding: ${JSON.stringify(serializeReviewIssue(issue))}\nBaseline: ${isolated.baselineOid}\nPatch SHA-256: ${validation.patchHash}\nPatch:\n${isolated.patch}`,
140
140
  { isolation: "worktree", candidatePatch: { baselineOid: isolated.baselineOid, patch: isolated.patch },
141
- label: `evaluate:${issue.id}`, phase: "Validate patch previews", profile: "medium", resume: "off",
141
+ label: `evaluate:${issue.id}`, phase: "Validate patch previews", ...api.modelProfile("medium"), resume: "off",
142
142
  tools: ["read", "bash", "grep", "find", "ls"], toolHints: ["search"], schema: PatchEvaluationSchema },
143
143
  );
144
144
  if (evaluated.baselineOid !== isolated.baselineOid || evaluated.patch !== isolated.patch) {
@@ -1,5 +1,6 @@
1
1
  import type { Static, TSchema } from "typebox";
2
2
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
3
+ import type { AgentTranscript } from "./live-agent.ts";
3
4
  import type { WorkflowBudget } from "./budget.ts";
4
5
  import type { Pipeline, WorkflowParallel } from "./concurrency.ts";
5
6
  import type { PerfSink, PerfSnapshot } from "./perf.ts";
@@ -35,14 +36,13 @@ export interface WorkflowRunMetadata {
35
36
  readonly recordPath: string;
36
37
  }
37
38
 
38
- export interface WorkflowBackgroundOrigin {
39
+ export interface WorkflowOrigin {
39
40
  /** Stable pi session id used to route and deduplicate completion delivery. */
40
41
  readonly sessionId: string;
41
42
  readonly requestedAt: number;
42
43
  }
43
44
 
44
45
  export interface WorkflowRunOptions {
45
- inspect?: boolean;
46
46
  perf?: boolean;
47
47
  concurrency?: number;
48
48
  parallelSubmissionLimit?: number;
@@ -64,8 +64,8 @@ export interface WorkflowRunOptions {
64
64
  budget?: number;
65
65
  /** Internal/test override for the generated run id. Omit to generate a new id. */
66
66
  runId?: string;
67
- /** Internal origin metadata for an explicitly backgrounded tool invocation. */
68
- background?: WorkflowBackgroundOrigin;
67
+ /** Owning session for durable completion delivery. */
68
+ origin?: WorkflowOrigin;
69
69
  /** Replay completed agent results from this prior run id when call and execution context still match. */
70
70
  resumeFromRunId?: string;
71
71
  /** Explicitly allow resume to ignore only a workflow-source fingerprint mismatch. */
@@ -92,7 +92,9 @@ export interface WorkflowRunOptions {
92
92
  export interface WorkflowProgressSource {
93
93
  snapshot(): WorkflowProgressSnapshot;
94
94
  conversation(agentId: number): readonly AgentChatMessage[];
95
- followUp(agentId: number, message: string): Promise<void>;
95
+ stopAgent?(agentId: number): void;
96
+ transcript?(agentId: number): AgentTranscript | undefined;
97
+ followUp(agentId: number, message: string, steer?: boolean): Promise<void>;
96
98
  subscribe(listener: () => void): () => void;
97
99
  }
98
100
 
@@ -122,19 +124,16 @@ export type WorkflowProgressEvent =
122
124
 
123
125
  export interface AgentOptions<S extends TSchema = TSchema> {
124
126
  /** Label shown in the progress tree (e.g. "find:logic-bugs"). */
125
- label?: string;
127
+ label: string;
126
128
  /** Phase to group this agent under in the progress tree. */
127
129
  phase?: string;
128
130
  /**
129
- * Optional model id. Overrides profile routing; when both model and profile are
130
- * omitted, inherit the host model. Explicit refs are strict: bare ids resolve as
131
- * Anthropic shorthand; use "provider/id" for other providers.
131
+ * Required model id. Bare ids resolve as Anthropic shorthand;
132
+ * use "provider/id" for other providers. No implicit host fallback.
132
133
  */
133
- model?: string;
134
- /** Reasoning effort for this agent. Overrides the profile's configured effort. */
135
- thinkingLevel?: ThinkingLevel;
136
- /** Exact configured model route to use when model/thinkingLevel do not override it. */
137
- profile?: WorkflowModelProfileName;
134
+ model: string;
135
+ /** Required reasoning effort for this agent. */
136
+ thinkingLevel: ThinkingLevel;
138
137
  /**
139
138
  * Stable identity hint for resume replay. Use this for repeated logical calls
140
139
  * with identical prompts/options, e.g. `${stage}:${item.id}`.
@@ -187,6 +186,8 @@ export interface AgentOptions<S extends TSchema = TSchema> {
187
186
  * exports `meta` plus a default `async (api: WorkflowApi) => result`.
188
187
  */
189
188
  export interface WorkflowApi {
189
+ /** Resolve an explicitly configured route; throws instead of inheriting the host. */
190
+ modelProfile(name: WorkflowModelProfileName): Pick<AgentOptions, "model" | "thinkingLevel">;
190
191
  /** Run a schema subagent in an isolated worktree and return its structured result plus patch. */
191
192
  agent<S extends TSchema>(
192
193
  prompt: string,
@@ -197,7 +198,7 @@ export interface WorkflowApi {
197
198
  /** Run a subagent and return validated structured output; rejects with a recoverable typed error on repair exhaustion. */
198
199
  agent<S extends TSchema>(prompt: string, opts: AgentOptions<S> & { schema: S }): Promise<Static<S>>;
199
200
  /** Run a subagent and return its final assistant text. */
200
- agent(prompt: string, opts?: AgentOptions): Promise<string>;
201
+ agent(prompt: string, opts: AgentOptions): Promise<string>;
201
202
  /**
202
203
  * Run another registered workflow inline as a sub-step and return its result. The child shares
203
204
  * this run's concurrency cap, abort signal, and perf sink. Nests one level only: calling
@@ -0,0 +1,59 @@
1
+ import { AssistantMessageComponent, ToolExecutionComponent, UserMessageComponent } from "@earendil-works/pi-coding-agent";
2
+ import type { Component, TUI } from "@earendil-works/pi-tui";
3
+ import type { AgentTranscript } from "../live-agent.ts";
4
+
5
+ /** Native pi transcript components, driven by the child session's own messages. */
6
+ export class AgentTranscriptView {
7
+ private readonly components = new WeakMap<object, Component>();
8
+ private readonly tools = new Map<string, ToolExecutionComponent>();
9
+ private readonly results = new Map<string, object>();
10
+
11
+ render(transcript: AgentTranscript, width: number, tui: TUI, cwd: string): string[] {
12
+ const messages = transcript.streaming && !transcript.messages.includes(transcript.streaming)
13
+ ? [...transcript.messages, transcript.streaming] : transcript.messages;
14
+ const rows: string[] = [];
15
+ for (const message of messages) {
16
+ if (message.role === "user") {
17
+ let component = this.components.get(message);
18
+ if (!component) {
19
+ const text = typeof message.content === "string" ? message.content
20
+ : message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
21
+ component = new UserMessageComponent(text);
22
+ this.components.set(message, component);
23
+ }
24
+ rows.push(...component.render(width));
25
+ } else if (message.role === "assistant") {
26
+ // Streaming messages can be mutated by pi; update the native component every render.
27
+ let component = this.components.get(message) as AssistantMessageComponent | undefined;
28
+ if (!component) {
29
+ component = new AssistantMessageComponent(message, false);
30
+ this.components.set(message, component);
31
+ }
32
+ component.updateContent(message, message === transcript.streaming);
33
+ rows.push(...component.render(width));
34
+ for (const part of message.content) {
35
+ if (part.type !== "toolCall") continue;
36
+ let tool = this.tools.get(part.id);
37
+ if (!tool) {
38
+ tool = new ToolExecutionComponent(part.name, part.id, part.arguments, { showImages: false }, undefined, tui, cwd);
39
+ this.tools.set(part.id, tool);
40
+ }
41
+ tool.updateArgs(part.arguments);
42
+ if (message !== transcript.streaming) tool.setArgsComplete();
43
+ const update = transcript.toolUpdates?.get(part.id);
44
+ if (update && this.results.get(part.id) !== update) {
45
+ tool.updateResult({ ...update.result, isError: update.isError }, update.isPartial);
46
+ this.results.set(part.id, update);
47
+ }
48
+ const result = messages.find((candidate) => candidate.role === "toolResult" && candidate.toolCallId === part.id);
49
+ if (result?.role === "toolResult" && this.results.get(part.id) !== result) {
50
+ tool.updateResult(result);
51
+ this.results.set(part.id, result);
52
+ }
53
+ rows.push(...tool.render(width));
54
+ }
55
+ }
56
+ }
57
+ return rows;
58
+ }
59
+ }
@@ -4,7 +4,7 @@ import type { AgentRowSnapshot, WorkflowLaneItemStatus, WorkflowProgressSnapshot
4
4
  import { toDisplayLine } from "./display-text.ts";
5
5
  import { formatWorkflowUsageLine } from "../usage.ts";
6
6
 
7
- export type WorkflowDisplayStatus = WorkflowLaneItemStatus | "queued" | "done" | "failed";
7
+ export type WorkflowDisplayStatus = WorkflowLaneItemStatus | "queued" | "done" | "failed" | "stopping" | "stopped";
8
8
  export type WorkflowThemeColor = Parameters<Theme["fg"]>[0];
9
9
 
10
10
  export function formatDuration(ms: number): string {
@@ -42,6 +42,10 @@ export function statusIcon(status: WorkflowDisplayStatus, theme: Theme): string
42
42
  case "error":
43
43
  case "failed":
44
44
  return theme.fg("error", "✗");
45
+ case "stopped":
46
+ return theme.fg("dim", "■");
47
+ case "stopping":
48
+ return theme.fg("warning", "◌");
45
49
  case "running":
46
50
  return theme.fg("accent", "●");
47
51
  case "queued":
@@ -151,7 +155,7 @@ function countSnapshotAgents(snapshot: WorkflowProgressSnapshot): WorkflowStatus
151
155
  const counts = { queued: 0, running: 0, done: 0, failed: 0, total: 0 };
152
156
  for (const phase of snapshot.phases) {
153
157
  for (const agent of phase.agents) {
154
- counts[agent.status]++;
158
+ counts[agent.status === "stopped" ? "failed" : agent.status === "stopping" ? "running" : agent.status]++;
155
159
  counts.total++;
156
160
  }
157
161
  }