@pi-archimedes/subagent 2.10.3 → 2.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.
- package/README.md +1 -1
- package/package.json +10 -10
- package/src/agent-panel.mcp.test.ts +76 -0
- package/src/agent-panel.tools.test.ts +162 -0
- package/src/agent-panel.ts +37 -8
- package/src/agent-panel.width-audit.test.ts +258 -0
- package/src/agents-parse.test.ts +252 -0
- package/src/agents.ts +72 -5
- package/src/compact.test.ts +136 -57
- package/src/compact.ts +57 -37
- package/src/dispatch.test.ts +73 -1
- package/src/dispatch.ts +20 -2
- package/src/execute.test.ts +9 -1
- package/src/execute.ts +26 -29
- package/src/expanded.test.ts +61 -0
- package/src/expanded.ts +96 -80
- package/src/format.test.ts +71 -29
- package/src/format.ts +71 -28
- package/src/frontmatter-io.test.ts +62 -0
- package/src/frontmatter-io.ts +16 -2
- package/src/handlers.test.ts +21 -3
- package/src/handlers.ts +22 -3
- package/src/index.output-schema.test.ts +317 -0
- package/src/index.test.ts +486 -0
- package/src/index.ts +168 -8
- package/src/render.ts +46 -36
- package/src/row-block.test.ts +195 -0
- package/src/row-block.ts +186 -0
- package/src/run-outcome.test.ts +71 -0
- package/src/run-outcome.ts +84 -0
- package/src/spawn.test.ts +204 -19
- package/src/spawn.ts +49 -17
- package/src/stream.test.ts +215 -17
- package/src/stream.ts +47 -9
- package/src/thinking-validation.test.ts +96 -0
- package/src/thinking-validation.ts +78 -0
- package/src/tool-declarations.test.ts +58 -0
- package/src/tool-row.width-audit.test.ts +720 -0
- package/src/tool-schema.ts +85 -0
- package/src/tool-validation.test.ts +321 -0
- package/src/tool-validation.ts +262 -0
- package/src/types.ts +86 -0
- package/src/usage.test.ts +95 -0
- package/src/usage.ts +87 -0
- package/src/cost.test.ts +0 -62
- package/src/cost.ts +0 -8
package/src/dispatch.ts
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
import { dispatch, BridgeTransportError, BridgeCancelledError } from "@pi-archimedes/core/bridge";
|
|
34
34
|
import { resolveModel } from "./spawn.js";
|
|
35
|
+
import { resolveThinkingLevel } from "./thinking-validation.js";
|
|
35
36
|
import type { ExecuteOptions } from "./execute.js";
|
|
36
37
|
import type { SubagentProgress, SubagentResult } from "./types.js";
|
|
37
38
|
|
|
@@ -101,7 +102,9 @@ export function dispatchViaBridge(
|
|
|
101
102
|
task: options.task,
|
|
102
103
|
systemPrompt: options.agentConfig?.systemPrompt?.trim() || null,
|
|
103
104
|
model: model ?? null,
|
|
104
|
-
|
|
105
|
+
// The same rule the fork path applies: a level pi's `--thinking` would warn
|
|
106
|
+
// about and drop is not handed to the runner either (`thinking-validation.ts`).
|
|
107
|
+
thinking: resolveThinkingLevel(options.agentConfig?.thinking).level ?? null,
|
|
105
108
|
tools: options.agentConfig?.tools?.length ? options.agentConfig.tools : null,
|
|
106
109
|
};
|
|
107
110
|
|
|
@@ -114,6 +117,11 @@ export function dispatchViaBridge(
|
|
|
114
117
|
const progress: SubagentProgress = {
|
|
115
118
|
agent: agentName,
|
|
116
119
|
status: "completed",
|
|
120
|
+
// The Client ran the session and reported it finished; that is a
|
|
121
|
+
// completed run in this vocabulary. It has no stopReason to expose and
|
|
122
|
+
// no cancellation of its own to report — a local cancel settles below
|
|
123
|
+
// with BridgeCancelledError, which is a failure by contract.
|
|
124
|
+
outcome: "completed",
|
|
117
125
|
task: options.task,
|
|
118
126
|
currentTool: undefined,
|
|
119
127
|
currentToolArgs: undefined,
|
|
@@ -134,6 +142,7 @@ export function dispatchViaBridge(
|
|
|
134
142
|
agent: agentName,
|
|
135
143
|
task: options.task,
|
|
136
144
|
exitCode: 0,
|
|
145
|
+
outcome: "completed",
|
|
137
146
|
usage: {
|
|
138
147
|
input: res.metrics.inputTokens,
|
|
139
148
|
output: res.metrics.outputTokens,
|
|
@@ -142,6 +151,9 @@ export function dispatchViaBridge(
|
|
|
142
151
|
cost: res.metrics.cost,
|
|
143
152
|
turns: 0,
|
|
144
153
|
},
|
|
154
|
+
// The Client ran this as a session of its own and books its usage there;
|
|
155
|
+
// this session must not also report it. See `SubagentResult`.
|
|
156
|
+
usageAccountedElsewhere: true,
|
|
145
157
|
model,
|
|
146
158
|
finalOutput: res.output,
|
|
147
159
|
error: undefined,
|
|
@@ -169,7 +181,13 @@ export function dispatchViaBridge(
|
|
|
169
181
|
const isLocalCancel = err instanceof BridgeCancelledError;
|
|
170
182
|
const errorMessage = isLocalCancel ? "cancelled" : err instanceof Error ? err.message : String(err);
|
|
171
183
|
const durationMs = Date.now() - startTime;
|
|
172
|
-
|
|
184
|
+
// Same ownership rule as the success path above: this settled from a
|
|
185
|
+
// desktop-side session (or a cancel of one), so its usage is that
|
|
186
|
+
// session's to report even if it ended badly.
|
|
187
|
+
return {
|
|
188
|
+
...buildFailedResult(agentName, options.task, errorMessage, model, durationMs),
|
|
189
|
+
usageAccountedElsewhere: true,
|
|
190
|
+
};
|
|
173
191
|
},
|
|
174
192
|
);
|
|
175
193
|
}
|
package/src/execute.test.ts
CHANGED
|
@@ -43,7 +43,9 @@ function activateBridge(socketPath: string): void {
|
|
|
43
43
|
process.env.PI_ARCHIMEDES_BRIDGE_SESSION = "test-desktop-session";
|
|
44
44
|
process.env.PI_ARCHIMEDES_BRIDGE_SERVER_PID = "12345";
|
|
45
45
|
const onSpy = vi.fn();
|
|
46
|
-
|
|
46
|
+
// The handle the relay subscribes on — inert here (these tests are about fork
|
|
47
|
+
// vs dispatch, not about the relay), but a real session always hands one over.
|
|
48
|
+
registerBridge({ on: onSpy, events: { emit: () => {}, on: () => () => {} } } as unknown as ExtensionAPI);
|
|
47
49
|
const calls = onSpy.mock.calls as Array<[string, (e: unknown, ctx: unknown) => void]>;
|
|
48
50
|
const startCall = calls.find((c) => c[0] === "session_start");
|
|
49
51
|
expect(startCall).toBeTruthy();
|
|
@@ -75,6 +77,9 @@ function makeOptions(overrides: Partial<ExecuteOptions> = {}): ExecuteOptions {
|
|
|
75
77
|
cwd: undefined,
|
|
76
78
|
signal: undefined,
|
|
77
79
|
onUpdate: undefined,
|
|
80
|
+
// These tests never fork, so the handle is inert — present because a real
|
|
81
|
+
// ExecuteOptions always carries one.
|
|
82
|
+
events: { emit: () => {}, on: () => () => {} },
|
|
78
83
|
toolCallId: "tc-1",
|
|
79
84
|
...overrides,
|
|
80
85
|
};
|
|
@@ -168,6 +173,9 @@ describe("executeSubagent (bridge branch)", () => {
|
|
|
168
173
|
expect(result.error).toBe("cancelled");
|
|
169
174
|
expect(result.progress!.status).toBe("failed");
|
|
170
175
|
expect(result.progress!.error).toBe("cancelled");
|
|
176
|
+
// Bridge-mode bookkeeping: whatever the desktop did with the task, its
|
|
177
|
+
// session owns the usage — the parent must not fold it into its own totals.
|
|
178
|
+
expect(result.usageAccountedElsewhere).toBe(true);
|
|
171
179
|
expect(spawnSpy).not.toHaveBeenCalled();
|
|
172
180
|
// Exactly ONE update: the final-failure progress (no "dispatched"
|
|
173
181
|
// placeholder — a cancelled task does not claim it was dispatched).
|
package/src/execute.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getBridge } from "@pi-archimedes/core/bridge";
|
|
2
|
+
import type { EventBus } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { spawnSubagent, resolveModel } from "./spawn.js";
|
|
3
4
|
import { dispatchViaBridge, buildFailedResult } from "./dispatch.js";
|
|
4
5
|
import { streamEvents } from "./stream.js";
|
|
5
|
-
import { emitCostUpdate } from "./cost.js";
|
|
6
6
|
import type { AgentConfig } from "./agents.js";
|
|
7
7
|
import type { SubagentProgress, SubagentResult } from "./types.js";
|
|
8
8
|
|
|
@@ -15,6 +15,10 @@ export interface ExecuteOptions {
|
|
|
15
15
|
cwd: string | undefined;
|
|
16
16
|
signal: AbortSignal | undefined;
|
|
17
17
|
onUpdate: ((progress: SubagentProgress) => void) | undefined;
|
|
18
|
+
/** The parent's host handle (`pi.events`) — forwarded to the ask bridge and to
|
|
19
|
+
* the todo announcements in `streamEvents`. Neither is an extension, so
|
|
20
|
+
* neither can reach a handle of its own. */
|
|
21
|
+
events: EventBus;
|
|
18
22
|
/** The subagent tool-call id (the tool's `_id`) — rides the dispatch frame
|
|
19
23
|
* for Client correlation; `undefined` → the frame's `toolCallId` is absent.
|
|
20
24
|
* In `executeParallel` all N tasks share the single subagent tool-call's id. */
|
|
@@ -49,7 +53,11 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
49
53
|
// so executeParallel's progress slot updates from the pending placeholder
|
|
50
54
|
// to "failed" instead of a stale "Starting...".
|
|
51
55
|
if (options.signal?.aborted) {
|
|
52
|
-
const result =
|
|
56
|
+
const result = {
|
|
57
|
+
...buildFailedResult(agentName, options.task, "cancelled", model, Date.now() - startTime),
|
|
58
|
+
// Bridge mode: any usage this run ever had belongs to a desktop session.
|
|
59
|
+
usageAccountedElsewhere: true,
|
|
60
|
+
};
|
|
53
61
|
options.onUpdate?.(result.progress!);
|
|
54
62
|
return result;
|
|
55
63
|
}
|
|
@@ -81,23 +89,21 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
81
89
|
if (!("fallback" in outcome)) {
|
|
82
90
|
// Bridge used (success or a response-carrying error, incl. the terminal
|
|
83
91
|
// "cancelled" frame — the desktop is alive, its outcome is authoritative):
|
|
84
|
-
// return it
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
92
|
+
// return it, with no cost report of its own. The PARENT never reports a
|
|
93
|
+
// child's usage for a run it did not perform: the subagent process emits
|
|
94
|
+
// its OWN usage to its OWN bridge (the core self-usage emitter), which is a
|
|
95
|
+
// different session's total. Every result `dispatchViaBridge` settles
|
|
96
|
+
// carries `usageAccountedElsewhere`, which is how the tool knows to leave
|
|
97
|
+
// it off the tool result — pi folds that `usage` into THIS session's
|
|
98
|
+
// totals, so copying it across would bill the parent twice. The fork path
|
|
99
|
+
// below does report, through the tool result's native `usage`.
|
|
91
100
|
return outcome;
|
|
92
101
|
}
|
|
93
102
|
// { fallback: true } — unreachable bridge: fall through to the fork path.
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
let lastEmittedOutput = 0;
|
|
99
|
-
let lastEmittedCost = 0;
|
|
100
|
-
|
|
105
|
+
// Progress is passed straight through: cost is reported once, on the tool
|
|
106
|
+
// result's native `usage`, so there is no delta bookkeeping left to do here.
|
|
101
107
|
try {
|
|
102
108
|
const child = spawnSubagent({
|
|
103
109
|
task: options.task,
|
|
@@ -106,26 +112,14 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
106
112
|
cwd: options.cwd,
|
|
107
113
|
signal: options.signal,
|
|
108
114
|
agent: options.agentConfig,
|
|
115
|
+
events: options.events,
|
|
109
116
|
});
|
|
110
117
|
|
|
111
118
|
const result = await streamEvents(child, {
|
|
112
119
|
agent: agentName,
|
|
113
120
|
task: options.task,
|
|
121
|
+
events: options.events,
|
|
114
122
|
onProgress: (progress: SubagentProgress) => {
|
|
115
|
-
// Emit only deltas to avoid double-counting in CostAccumulator
|
|
116
|
-
const deltaInput = progress.inputTokens - lastEmittedInput;
|
|
117
|
-
const deltaOutput = progress.outputTokens - lastEmittedOutput;
|
|
118
|
-
const deltaCost = progress.cost - lastEmittedCost;
|
|
119
|
-
if (deltaInput > 0 || deltaOutput > 0 || deltaCost > 0) {
|
|
120
|
-
emitCostUpdate(agentName, {
|
|
121
|
-
inputTokens: deltaInput,
|
|
122
|
-
outputTokens: deltaOutput,
|
|
123
|
-
cost: deltaCost,
|
|
124
|
-
});
|
|
125
|
-
lastEmittedInput = progress.inputTokens;
|
|
126
|
-
lastEmittedOutput = progress.outputTokens;
|
|
127
|
-
lastEmittedCost = progress.cost;
|
|
128
|
-
}
|
|
129
123
|
options.onUpdate?.(progress);
|
|
130
124
|
},
|
|
131
125
|
});
|
|
@@ -142,7 +136,8 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
142
136
|
// synthesize one so the parallel renderer stays aligned with results.
|
|
143
137
|
{
|
|
144
138
|
agent: agentName,
|
|
145
|
-
status: result.exitCode === 0 ? "completed" : "failed",
|
|
139
|
+
status: result.outcome || result.exitCode === 0 ? "completed" : "failed",
|
|
140
|
+
...(result.outcome ? { outcome: result.outcome } : {}),
|
|
146
141
|
task: options.task,
|
|
147
142
|
currentTool: undefined,
|
|
148
143
|
currentToolArgs: undefined,
|
|
@@ -187,6 +182,7 @@ export async function executeParallel(options: {
|
|
|
187
182
|
tasks: Array<{ agent: string | undefined; agentConfig: AgentConfig | undefined; task: string; model: string | undefined; activeModel: string | undefined; cwd: string | undefined; toolCallId?: string }>;
|
|
188
183
|
signal: AbortSignal | undefined;
|
|
189
184
|
onUpdate: ((progress: SubagentProgress[]) => void) | undefined;
|
|
185
|
+
events: EventBus;
|
|
190
186
|
}): Promise<SubagentResult[]> {
|
|
191
187
|
// Pre-fill one pending slot per task, keyed by task index (NOT agent name).
|
|
192
188
|
// This keeps all N lines stacked from t=0 in stable task order, with no
|
|
@@ -218,6 +214,7 @@ export async function executeParallel(options: {
|
|
|
218
214
|
executeSubagent({
|
|
219
215
|
...taskDef,
|
|
220
216
|
signal: options.signal,
|
|
217
|
+
events: options.events,
|
|
221
218
|
onUpdate: (progress: SubagentProgress) => {
|
|
222
219
|
// Store latest progress in this task's stable slot (by index).
|
|
223
220
|
latestProgress[index] = progress;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildExpandedText } from "./expanded.js";
|
|
3
|
+
import type { RowSegment } from "./row-block.js";
|
|
4
|
+
import type { SubagentResult } from "./types.js";
|
|
5
|
+
|
|
6
|
+
// The expanded row is what the user reads after `ctrl+o`; the compact row is
|
|
7
|
+
// what they see while it runs. Both have to carry the *Run outcome* vocabulary
|
|
8
|
+
// (CONTEXT.md) rather than collapsing it into done/failed.
|
|
9
|
+
|
|
10
|
+
const theme = { fg: (token: string, text: string) => `[${token}]${text}`, bold: (t: string) => t };
|
|
11
|
+
|
|
12
|
+
/** The content these segments carry, ignoring how each is laid out at render time. */
|
|
13
|
+
const drawnText = (segments: RowSegment[]): string => segments.map((s) => s.text).join("\n");
|
|
14
|
+
|
|
15
|
+
function makeResult(overrides: Partial<SubagentResult> = {}): SubagentResult {
|
|
16
|
+
return {
|
|
17
|
+
agent: "reviewer",
|
|
18
|
+
task: "review the diff",
|
|
19
|
+
exitCode: 0,
|
|
20
|
+
usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: 0.01, turns: 1 },
|
|
21
|
+
model: "anthropic/claude-opus-4.5",
|
|
22
|
+
finalOutput: "the answer",
|
|
23
|
+
error: undefined,
|
|
24
|
+
progress: undefined,
|
|
25
|
+
progressSummary: { toolCount: 2, tokens: 15, durationMs: 1000 },
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("expanded view: run outcomes", () => {
|
|
31
|
+
it("still says Done for a completed run", () => {
|
|
32
|
+
expect(drawnText(buildExpandedText(makeResult({ outcome: "completed" }), undefined, theme))).toContain("✓ Done");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("says Truncated — and not Failed — for a cut-off answer", () => {
|
|
36
|
+
const out = drawnText(buildExpandedText(
|
|
37
|
+
makeResult({ outcome: "truncated", exitCode: 1, finalOutput: "partial" }),
|
|
38
|
+
undefined,
|
|
39
|
+
theme,
|
|
40
|
+
));
|
|
41
|
+
expect(out).toMatch(/truncated/i);
|
|
42
|
+
expect(out).not.toMatch(/Failed/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("says Aborted for a cancelled run", () => {
|
|
46
|
+
const out = drawnText(buildExpandedText(
|
|
47
|
+
makeResult({ outcome: "aborted", exitCode: 143, error: undefined }),
|
|
48
|
+
undefined,
|
|
49
|
+
theme,
|
|
50
|
+
));
|
|
51
|
+
expect(out).toMatch(/aborted/i);
|
|
52
|
+
expect(out).not.toMatch(/Failed/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("keeps reporting a real error as an error", () => {
|
|
56
|
+
const failed = makeResult({ exitCode: 1, error: "401 unauthorized" });
|
|
57
|
+
delete failed.outcome; // a failed run claims no outcome
|
|
58
|
+
const out = drawnText(buildExpandedText(failed, undefined, theme));
|
|
59
|
+
expect(out).toContain("401 unauthorized");
|
|
60
|
+
});
|
|
61
|
+
});
|
package/src/expanded.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import type { RunOutcome, SubagentDetails, SubagentProgress, SubagentResult, SubagentToolCall } from "./types.js";
|
|
2
|
+
import { formatDuration, buildStatsLine, buildAgentLabel, buildOutcomeLine } from "./format.js";
|
|
3
|
+
import { block, row, type RowBlock, type RowSegment } from "./row-block.js";
|
|
4
4
|
import { renderToolCallLine } from "@pi-archimedes/core/tool-render";
|
|
5
5
|
|
|
6
6
|
type Theme = { fg: (token: string, text: string) => string; bold: (text: string) => string };
|
|
7
7
|
|
|
8
|
+
// The expanded view mixes two kinds of line and says which is which (see
|
|
9
|
+
// `row-block.ts`): a **row** (label, activity, outcome) stays one line and fills
|
|
10
|
+
// the frame; a **block** (the stats line, the task text, the tool-call history,
|
|
11
|
+
// the output) keeps wrapping, because the expanded view is where all of it is
|
|
12
|
+
// meant to be readable (ADR 0020). The stats line is a block and not a row
|
|
13
|
+
// because nothing else repeats its numbers: cut at the frame, its tail — tokens,
|
|
14
|
+
// duration, cost — would simply be gone on a narrow terminal.
|
|
15
|
+
|
|
8
16
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
9
17
|
|
|
10
18
|
// A completed tool call renders with a status glyph (✓ ok / ✗ error) and its
|
|
@@ -19,17 +27,31 @@ function formatToolCall(call: SubagentToolCall | string, theme: Theme): string {
|
|
|
19
27
|
return renderToolCallLine(status, call.name, suffix, theme);
|
|
20
28
|
}
|
|
21
29
|
|
|
30
|
+
// The bottom row of a finished view. The *Run outcome* wins over the done/failed
|
|
31
|
+
// pair: a truncated answer is not a failed run, and labelling it "✗ Failed" is
|
|
32
|
+
// how a useful answer gets thrown away by whoever reads it.
|
|
33
|
+
function outcomeRow(
|
|
34
|
+
data: { outcome?: RunOutcome | undefined; error: string | undefined },
|
|
35
|
+
theme: Theme,
|
|
36
|
+
fallback: { failed: string },
|
|
37
|
+
): string {
|
|
38
|
+
const line = buildOutcomeLine({ outcome: data.outcome, error: data.error }, theme);
|
|
39
|
+
if (line) return line;
|
|
40
|
+
if (data.error) return theme.fg("error", "✗ " + data.error);
|
|
41
|
+
return theme.fg("error", "✗ " + fallback.failed);
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
// ── Expanded completed result ───────────────────────────────────────────────
|
|
23
45
|
|
|
24
46
|
export function buildExpandedText(
|
|
25
47
|
result: SubagentResult,
|
|
26
48
|
progress: SubagentProgress | undefined,
|
|
27
49
|
theme: Theme,
|
|
28
|
-
):
|
|
29
|
-
const lines:
|
|
50
|
+
): RowSegment[] {
|
|
51
|
+
const lines: RowSegment[] = [];
|
|
30
52
|
|
|
31
|
-
lines.push(buildAgentLabel(result.agent, result.task, theme));
|
|
32
|
-
lines.push("");
|
|
53
|
+
lines.push(row(buildAgentLabel(result.agent, result.task, theme)));
|
|
54
|
+
lines.push(row(""));
|
|
33
55
|
|
|
34
56
|
// Stats line (same as compact view)
|
|
35
57
|
const modelName = progress?.model ?? result.model;
|
|
@@ -43,69 +65,73 @@ export function buildExpandedText(
|
|
|
43
65
|
}, theme);
|
|
44
66
|
const statsParts = [modelLabel, statsLine].filter(Boolean);
|
|
45
67
|
if (statsParts.length > 0) {
|
|
46
|
-
|
|
68
|
+
// A block, not a row: this is the only place the run's numbers appear, and
|
|
69
|
+
// the expanded view is where they are meant to be readable however narrow
|
|
70
|
+
// the frame is (ADR 0020).
|
|
71
|
+
lines.push(block(statsParts.join(" ")));
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
// Task
|
|
50
75
|
if (result.task) {
|
|
51
|
-
lines.push("");
|
|
52
|
-
lines.push(theme.fg("dim", "Task: " + result.task));
|
|
76
|
+
lines.push(row(""));
|
|
77
|
+
lines.push(block(theme.fg("dim", "Task: " + result.task)));
|
|
53
78
|
}
|
|
54
79
|
|
|
55
80
|
// Tool calls history
|
|
56
81
|
const toolCalls = progress?.toolCalls;
|
|
57
82
|
if (toolCalls && toolCalls.length > 0) {
|
|
58
|
-
lines.push("");
|
|
83
|
+
lines.push(row(""));
|
|
59
84
|
for (const call of toolCalls) {
|
|
60
|
-
lines.push(formatToolCall(call, theme));
|
|
85
|
+
lines.push(block(formatToolCall(call, theme)));
|
|
61
86
|
}
|
|
62
87
|
}
|
|
63
88
|
|
|
64
89
|
// Output - show live streaming output or final output
|
|
65
90
|
const outputText = progress?.output ?? result.finalOutput;
|
|
66
91
|
if (outputText) {
|
|
67
|
-
lines.push("");
|
|
68
|
-
lines.push(outputText);
|
|
92
|
+
lines.push(row(""));
|
|
93
|
+
lines.push(block(outputText));
|
|
69
94
|
}
|
|
70
95
|
|
|
71
|
-
// Error
|
|
72
|
-
if (result.error) {
|
|
73
|
-
lines.push("");
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
// Error / outcome at the bottom
|
|
97
|
+
if (result.error || result.outcome || result.exitCode === 0) {
|
|
98
|
+
lines.push(row(""));
|
|
99
|
+
if (result.outcome || result.error) {
|
|
100
|
+
lines.push(row(outcomeRow({ outcome: result.outcome, error: result.error }, theme, { failed: "Failed" })));
|
|
101
|
+
} else {
|
|
102
|
+
lines.push(row(theme.fg("success", "✓ Done")));
|
|
103
|
+
}
|
|
78
104
|
} else {
|
|
79
|
-
lines.push("");
|
|
80
|
-
lines.push(theme.fg("error", "✗ Failed"));
|
|
105
|
+
lines.push(row(""));
|
|
106
|
+
lines.push(row(theme.fg("error", "✗ Failed")));
|
|
81
107
|
}
|
|
82
108
|
|
|
83
|
-
return lines
|
|
109
|
+
return lines;
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
export function renderExpanded(
|
|
87
|
-
|
|
113
|
+
rows: RowBlock,
|
|
88
114
|
result: SubagentResult,
|
|
89
115
|
progress: SubagentProgress | undefined,
|
|
90
116
|
theme: Theme,
|
|
91
|
-
):
|
|
92
|
-
|
|
93
|
-
return
|
|
117
|
+
): RowBlock {
|
|
118
|
+
rows.setSegments(buildExpandedText(result, progress, theme));
|
|
119
|
+
return rows;
|
|
94
120
|
}
|
|
95
121
|
|
|
96
122
|
// ── Expanded streaming progress ─────────────────────────────────────────────
|
|
97
123
|
|
|
98
124
|
export function renderProgressExpanded(
|
|
99
|
-
|
|
125
|
+
rows: RowBlock,
|
|
100
126
|
progress: SubagentProgress,
|
|
101
127
|
theme: Theme,
|
|
102
|
-
):
|
|
103
|
-
const lines:
|
|
128
|
+
): RowBlock {
|
|
129
|
+
const lines: RowSegment[] = [];
|
|
104
130
|
|
|
105
|
-
lines.push(buildAgentLabel(progress.agent, progress.task, theme));
|
|
106
|
-
lines.push("");
|
|
131
|
+
lines.push(row(buildAgentLabel(progress.agent, progress.task, theme)));
|
|
132
|
+
lines.push(row(""));
|
|
107
133
|
|
|
108
|
-
// Stats line (same as compact view)
|
|
134
|
+
// Stats line (same as compact view) — a block: see buildExpandedText.
|
|
109
135
|
const modelLabel = progress.model ? theme.fg("accent", progress.model) : "";
|
|
110
136
|
const statsLine = buildStatsLine({
|
|
111
137
|
turns: undefined,
|
|
@@ -116,51 +142,46 @@ export function renderProgressExpanded(
|
|
|
116
142
|
}, theme);
|
|
117
143
|
const statsParts = [modelLabel, statsLine].filter(Boolean);
|
|
118
144
|
if (statsParts.length > 0) {
|
|
119
|
-
lines.push(statsParts.join(" "));
|
|
145
|
+
lines.push(block(statsParts.join(" ")));
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
// Task
|
|
123
149
|
if (progress.task) {
|
|
124
|
-
lines.push("");
|
|
125
|
-
lines.push(theme.fg("dim", "Task: " + progress.task));
|
|
150
|
+
lines.push(row(""));
|
|
151
|
+
lines.push(block(theme.fg("dim", "Task: " + progress.task)));
|
|
126
152
|
}
|
|
127
153
|
|
|
128
154
|
// Tool calls history
|
|
129
155
|
if (progress.toolCalls && progress.toolCalls.length > 0) {
|
|
130
|
-
lines.push("");
|
|
156
|
+
lines.push(row(""));
|
|
131
157
|
for (const call of progress.toolCalls) {
|
|
132
|
-
lines.push(formatToolCall(call, theme));
|
|
158
|
+
lines.push(block(formatToolCall(call, theme)));
|
|
133
159
|
}
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
// Activity (current tool, marked with the running glyph ▸)
|
|
137
163
|
if (progress.currentTool) {
|
|
138
|
-
lines.push("");
|
|
139
|
-
const argsPreview = progress.currentToolArgs
|
|
140
|
-
? truncLine(progress.currentToolArgs, 60)
|
|
141
|
-
: "";
|
|
164
|
+
lines.push(row(""));
|
|
165
|
+
const argsPreview = progress.currentToolArgs ?? "";
|
|
142
166
|
const durationPart = progress.currentToolStartedAt
|
|
143
167
|
? " | " + formatDuration(Date.now() - progress.currentToolStartedAt)
|
|
144
168
|
: "";
|
|
145
169
|
const suffix = (argsPreview ? ": " + argsPreview : "") + durationPart;
|
|
146
|
-
lines.push(renderToolCallLine("running", progress.currentTool, suffix, theme));
|
|
170
|
+
lines.push(row(renderToolCallLine("running", progress.currentTool, suffix, theme)));
|
|
147
171
|
}
|
|
148
172
|
|
|
149
173
|
// Status at bottom
|
|
150
|
-
if (progress.status === "completed") {
|
|
151
|
-
lines.push("");
|
|
152
|
-
|
|
174
|
+
if (progress.status === "completed" || progress.outcome) {
|
|
175
|
+
lines.push(row(""));
|
|
176
|
+
const outcomeLine = buildOutcomeLine({ outcome: progress.outcome, error: progress.error }, theme);
|
|
177
|
+
lines.push(row(outcomeLine ?? theme.fg("success", "✓ Done")));
|
|
153
178
|
} else if (progress.status === "failed") {
|
|
154
|
-
lines.push("");
|
|
155
|
-
|
|
156
|
-
lines.push(theme.fg("error", "✗ " + progress.error));
|
|
157
|
-
} else {
|
|
158
|
-
lines.push(theme.fg("error", "✗ Failed"));
|
|
159
|
-
}
|
|
179
|
+
lines.push(row(""));
|
|
180
|
+
lines.push(row(outcomeRow({ outcome: undefined, error: progress.error }, theme, { failed: "Failed" })));
|
|
160
181
|
}
|
|
161
182
|
|
|
162
|
-
|
|
163
|
-
return
|
|
183
|
+
rows.setSegments(lines);
|
|
184
|
+
return rows;
|
|
164
185
|
}
|
|
165
186
|
|
|
166
187
|
// ── Expanded parallel streaming progress ────────────────────────────────────
|
|
@@ -168,13 +189,13 @@ export function renderProgressExpanded(
|
|
|
168
189
|
export function buildProgressExpandedText(
|
|
169
190
|
progress: SubagentProgress,
|
|
170
191
|
theme: Theme,
|
|
171
|
-
):
|
|
172
|
-
const lines:
|
|
192
|
+
): RowSegment[] {
|
|
193
|
+
const lines: RowSegment[] = [];
|
|
173
194
|
|
|
174
|
-
lines.push(buildAgentLabel(progress.agent, progress.task, theme));
|
|
175
|
-
lines.push("");
|
|
195
|
+
lines.push(row(buildAgentLabel(progress.agent, progress.task, theme)));
|
|
196
|
+
lines.push(row(""));
|
|
176
197
|
|
|
177
|
-
// Stats line (same as compact view)
|
|
198
|
+
// Stats line (same as compact view) — a block: see buildExpandedText.
|
|
178
199
|
const modelLabel = progress.model ? theme.fg("accent", progress.model) : "";
|
|
179
200
|
const statsLine = buildStatsLine({
|
|
180
201
|
turns: undefined,
|
|
@@ -185,28 +206,26 @@ export function buildProgressExpandedText(
|
|
|
185
206
|
}, theme);
|
|
186
207
|
const statsParts = [modelLabel, statsLine].filter(Boolean);
|
|
187
208
|
if (statsParts.length > 0) {
|
|
188
|
-
lines.push(statsParts.join(" "));
|
|
209
|
+
lines.push(block(statsParts.join(" ")));
|
|
189
210
|
}
|
|
190
211
|
|
|
191
212
|
// Task
|
|
192
213
|
if (progress.task) {
|
|
193
|
-
lines.push("");
|
|
194
|
-
lines.push(theme.fg("dim", "Task: " + progress.task));
|
|
214
|
+
lines.push(row(""));
|
|
215
|
+
lines.push(block(theme.fg("dim", "Task: " + progress.task)));
|
|
195
216
|
}
|
|
196
217
|
|
|
197
218
|
// Tool calls history
|
|
198
219
|
if (progress.toolCalls && progress.toolCalls.length > 0) {
|
|
199
|
-
lines.push("");
|
|
220
|
+
lines.push(row(""));
|
|
200
221
|
for (const call of progress.toolCalls) {
|
|
201
|
-
lines.push(formatToolCall(call, theme));
|
|
222
|
+
lines.push(block(formatToolCall(call, theme)));
|
|
202
223
|
}
|
|
203
224
|
}
|
|
204
225
|
|
|
205
226
|
// Activity
|
|
206
227
|
if (progress.currentTool) {
|
|
207
|
-
const argsPreview = progress.currentToolArgs
|
|
208
|
-
? truncLine(progress.currentToolArgs, 60)
|
|
209
|
-
: "";
|
|
228
|
+
const argsPreview = progress.currentToolArgs ?? "";
|
|
210
229
|
const durationPart = progress.currentToolStartedAt
|
|
211
230
|
? " | " + formatDuration(Date.now() - progress.currentToolStartedAt)
|
|
212
231
|
: "";
|
|
@@ -217,22 +236,19 @@ export function buildProgressExpandedText(
|
|
|
217
236
|
if (durationPart) {
|
|
218
237
|
line += theme.fg("dim", durationPart);
|
|
219
238
|
}
|
|
220
|
-
lines.push("");
|
|
221
|
-
lines.push(line);
|
|
239
|
+
lines.push(row(""));
|
|
240
|
+
lines.push(row(line));
|
|
222
241
|
}
|
|
223
242
|
|
|
224
243
|
// Status at bottom
|
|
225
|
-
if (progress.status === "completed") {
|
|
226
|
-
lines.push("");
|
|
227
|
-
|
|
244
|
+
if (progress.status === "completed" || progress.outcome) {
|
|
245
|
+
lines.push(row(""));
|
|
246
|
+
const outcomeLine = buildOutcomeLine({ outcome: progress.outcome, error: progress.error }, theme);
|
|
247
|
+
lines.push(row(outcomeLine ?? theme.fg("success", "✓ Done")));
|
|
228
248
|
} else if (progress.status === "failed") {
|
|
229
|
-
lines.push("");
|
|
230
|
-
|
|
231
|
-
lines.push(theme.fg("error", "✗ " + progress.error));
|
|
232
|
-
} else {
|
|
233
|
-
lines.push(theme.fg("error", "✗ Failed"));
|
|
234
|
-
}
|
|
249
|
+
lines.push(row(""));
|
|
250
|
+
lines.push(row(outcomeRow({ outcome: undefined, error: progress.error }, theme, { failed: "Failed" })));
|
|
235
251
|
}
|
|
236
252
|
|
|
237
|
-
return lines
|
|
253
|
+
return lines;
|
|
238
254
|
}
|