@mystilleef/pi-subagent 0.9.0 → 0.10.2
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 +68 -40
- package/package.json +8 -8
- package/src/agent/agent-cache.ts +1 -0
- package/src/agent/agents.ts +22 -1
- package/src/child/child-events.ts +2 -0
- package/src/child/process.ts +196 -84
- package/src/child/termination.ts +316 -1
- package/src/env.d.ts +2 -0
- package/src/notification/delivery.ts +231 -0
- package/src/notification/desktop-notification.ts +73 -0
- package/src/orchestration/run-command.ts +1 -1
- package/src/orchestration/subagent-orchestrator.ts +49 -79
- package/src/output/normalize.ts +2 -2
- package/src/output/ui.ts +17 -28
- package/src/progress/progress-format.ts +111 -0
- package/src/progress/progress-state.ts +20 -108
- package/src/progress/progress.ts +29 -27
- package/src/progress/result-details.ts +96 -33
- package/src/shared/types.ts +2 -0
- package/src/shared/utils.ts +27 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import {
|
|
2
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
3
3
|
import type {
|
|
4
4
|
ExtensionAPI,
|
|
5
5
|
ExtensionContext,
|
|
@@ -12,6 +12,12 @@ import type {
|
|
|
12
12
|
ThinkingLevel,
|
|
13
13
|
} from "../agent/agents.js";
|
|
14
14
|
import { runSingleAgent, SubagentAbortError } from "../child/process.js";
|
|
15
|
+
import { deliverNotification } from "../notification/delivery.js";
|
|
16
|
+
import {
|
|
17
|
+
buildNotificationRequest,
|
|
18
|
+
isDesktopNotificationsEnabled,
|
|
19
|
+
isPerJobNotificationEnabled,
|
|
20
|
+
} from "../notification/desktop-notification.js";
|
|
15
21
|
import { formatSubagentResultForParent } from "../output/summary.js";
|
|
16
22
|
import {
|
|
17
23
|
cancelProgressState,
|
|
@@ -22,12 +28,13 @@ import {
|
|
|
22
28
|
} from "../progress/progress.js";
|
|
23
29
|
import {
|
|
24
30
|
createSubagentError,
|
|
31
|
+
type DetailsOptions,
|
|
25
32
|
getFeedbackSummaryText,
|
|
26
33
|
getLatestResult,
|
|
27
34
|
getResultDisplayText,
|
|
28
|
-
hasSubagentFailed,
|
|
29
35
|
patchProgressFromDetails,
|
|
30
36
|
sanitizeDetailsForDisplay,
|
|
37
|
+
sanitizeResultDetails,
|
|
31
38
|
} from "../progress/result-details.js";
|
|
32
39
|
import { generateSubagentInstanceName } from "../shared/instance-name.js";
|
|
33
40
|
import type {
|
|
@@ -36,7 +43,7 @@ import type {
|
|
|
36
43
|
SubagentDetails,
|
|
37
44
|
SubagentToolResult,
|
|
38
45
|
} from "../shared/types.js";
|
|
39
|
-
import { getSubagentDepth } from "../shared/utils.js";
|
|
46
|
+
import { getSubagentDepth, hasSubagentFailed } from "../shared/utils.js";
|
|
40
47
|
import {
|
|
41
48
|
listRunJobs,
|
|
42
49
|
type RunJob,
|
|
@@ -71,7 +78,6 @@ export const SubagentParams = Type.Object({
|
|
|
71
78
|
|
|
72
79
|
export type { SubagentToolResult };
|
|
73
80
|
|
|
74
|
-
type DetailsOptions = { includeMessages?: boolean; recentMessages?: Message[] };
|
|
75
81
|
type DetailsBuilder = (
|
|
76
82
|
results: SingleResult[],
|
|
77
83
|
options?: DetailsOptions,
|
|
@@ -112,63 +118,6 @@ function createDetailsBuilder(
|
|
|
112
118
|
});
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
function sanitizeResultDetails(
|
|
116
|
-
result: SingleResult,
|
|
117
|
-
includeDebugMessages: boolean,
|
|
118
|
-
options: DetailsOptions | undefined,
|
|
119
|
-
): SingleResult {
|
|
120
|
-
const includeMessages =
|
|
121
|
-
includeDebugMessages && (options?.includeMessages ?? true);
|
|
122
|
-
const { messages, termination, progress, stderr, usage, ...core } = result;
|
|
123
|
-
const { contextWindowTokens, ...usageBase } = usage;
|
|
124
|
-
const sanitized: Record<string, unknown> = {
|
|
125
|
-
...core,
|
|
126
|
-
stderr: includeDebugMessages ? stderr : "",
|
|
127
|
-
usage: { ...usageBase },
|
|
128
|
-
};
|
|
129
|
-
if (contextWindowTokens !== undefined) {
|
|
130
|
-
(sanitized["usage"] as Record<string, unknown>)["contextWindowTokens"] =
|
|
131
|
-
contextWindowTokens;
|
|
132
|
-
}
|
|
133
|
-
if (progress !== undefined) {
|
|
134
|
-
const {
|
|
135
|
-
activityText,
|
|
136
|
-
activeToolActivity,
|
|
137
|
-
lastToolPreview,
|
|
138
|
-
toolResultCompleted,
|
|
139
|
-
...progBase
|
|
140
|
-
} = progress;
|
|
141
|
-
sanitized["progress"] = {
|
|
142
|
-
toolCalls: progBase.toolCalls.map((tc) => ({
|
|
143
|
-
id: tc.id,
|
|
144
|
-
preview: tc.preview,
|
|
145
|
-
})),
|
|
146
|
-
...(activityText !== undefined && { activityText }),
|
|
147
|
-
...(activeToolActivity !== undefined && { activeToolActivity }),
|
|
148
|
-
...(lastToolPreview !== undefined && { lastToolPreview }),
|
|
149
|
-
...(toolResultCompleted !== undefined && { toolResultCompleted }),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
if (includeMessages) {
|
|
153
|
-
sanitized["messages"] = options?.recentMessages
|
|
154
|
-
? [...options.recentMessages]
|
|
155
|
-
: messages !== undefined
|
|
156
|
-
? [...messages]
|
|
157
|
-
: undefined;
|
|
158
|
-
if (includeDebugMessages && termination !== undefined) {
|
|
159
|
-
const { cancelReason, terminationSignal, fallbackCause, ...termBase } =
|
|
160
|
-
termination;
|
|
161
|
-
sanitized["termination"] = {
|
|
162
|
-
...termBase,
|
|
163
|
-
...(cancelReason !== undefined && { cancelReason }),
|
|
164
|
-
...(terminationSignal !== undefined && { terminationSignal }),
|
|
165
|
-
...(fallbackCause !== undefined && { fallbackCause }),
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return sanitized as unknown as SingleResult;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
121
|
function createProgressRenderRequester(
|
|
173
122
|
ctx: ExtensionContext,
|
|
174
123
|
requestId: string,
|
|
@@ -198,11 +147,26 @@ function sendSubagentResultMessage(
|
|
|
198
147
|
});
|
|
199
148
|
}
|
|
200
149
|
|
|
150
|
+
function deliverDesktopCompletionNotification(
|
|
151
|
+
state: NonNullable<ReturnType<typeof getProgressState>>,
|
|
152
|
+
): void {
|
|
153
|
+
if (state.status === "cancelled") return;
|
|
154
|
+
const request = buildNotificationRequest(state);
|
|
155
|
+
deliverNotification(request).catch(() => {});
|
|
156
|
+
}
|
|
157
|
+
|
|
201
158
|
export function emitCompletionAlert(
|
|
202
159
|
state: ReturnType<typeof getProgressState>,
|
|
203
160
|
): void {
|
|
204
161
|
if (!state) return;
|
|
205
162
|
if (state.status === "cancelled") return;
|
|
163
|
+
if (
|
|
164
|
+
isDesktopNotificationsEnabled() &&
|
|
165
|
+
!isPerJobNotificationEnabled() &&
|
|
166
|
+
getSubagentDepth() === 0
|
|
167
|
+
) {
|
|
168
|
+
deliverDesktopCompletionNotification(state);
|
|
169
|
+
}
|
|
206
170
|
const tty = (process.stdout as { isTTY?: boolean }).isTTY;
|
|
207
171
|
if (!tty) return;
|
|
208
172
|
process.stdout.write("\x07");
|
|
@@ -272,10 +236,11 @@ function createPayloadFingerprint(payload: {
|
|
|
272
236
|
const contentText = payload.content[0]?.text ?? "";
|
|
273
237
|
const latestResult = payload.details.results[0];
|
|
274
238
|
const activityText = latestResult?.progress?.activityText ?? "";
|
|
275
|
-
const toolCallIds =
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
239
|
+
const toolCallIds = [
|
|
240
|
+
...new Set(latestResult?.progress?.toolCalls?.map((tc) => tc.id)),
|
|
241
|
+
]
|
|
242
|
+
.sort()
|
|
243
|
+
.join(",");
|
|
279
244
|
const exitCode = latestResult?.exitCode ?? 0;
|
|
280
245
|
const stopReason = latestResult?.stopReason ?? "";
|
|
281
246
|
return `${contentText}|${activityText}|${toolCallIds}|${exitCode}|${stopReason}`;
|
|
@@ -337,15 +302,25 @@ async function runSubagentLifecycle(
|
|
|
337
302
|
clearInterval(timerTick);
|
|
338
303
|
requestProgressRender();
|
|
339
304
|
removeRunJob(lc.requestId);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (state) {
|
|
343
|
-
emitCompletionAlert(state);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
305
|
+
const state = getProgressState(lc.requestId);
|
|
306
|
+
if (state) deliverLifecycleCompletionNotification(state);
|
|
346
307
|
}
|
|
347
308
|
}
|
|
348
309
|
|
|
310
|
+
function deliverLifecycleCompletionNotification(
|
|
311
|
+
state: NonNullable<ReturnType<typeof getProgressState>>,
|
|
312
|
+
): void {
|
|
313
|
+
if (
|
|
314
|
+
isDesktopNotificationsEnabled() &&
|
|
315
|
+
isPerJobNotificationEnabled() &&
|
|
316
|
+
getSubagentDepth() === 0
|
|
317
|
+
) {
|
|
318
|
+
deliverDesktopCompletionNotification(state);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (listRunJobs().length === 0) emitCompletionAlert(state);
|
|
322
|
+
}
|
|
323
|
+
|
|
349
324
|
type StartJobResult =
|
|
350
325
|
| {
|
|
351
326
|
kind: "started";
|
|
@@ -366,8 +341,7 @@ type PrepareSubagentJobResult =
|
|
|
366
341
|
hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails> | undefined;
|
|
367
342
|
}
|
|
368
343
|
| { kind: "not_found"; makeDetails: DetailsBuilder }
|
|
369
|
-
| { kind: "cancelled"; makeDetails: DetailsBuilder }
|
|
370
|
-
| { kind: "aborted"; makeDetails: DetailsBuilder };
|
|
344
|
+
| { kind: "cancelled"; makeDetails: DetailsBuilder };
|
|
371
345
|
|
|
372
346
|
export function formatSubagentToolResult(
|
|
373
347
|
agentName: string,
|
|
@@ -479,7 +453,7 @@ async function prepareSubagentJob(
|
|
|
479
453
|
if (mergedSignal.aborted) {
|
|
480
454
|
cancelStartedJob(job, job.cancelReason ?? "Aborted");
|
|
481
455
|
requestProgressRender();
|
|
482
|
-
return { kind: "
|
|
456
|
+
return { kind: "cancelled", makeDetails: makeStartedDetails };
|
|
483
457
|
}
|
|
484
458
|
return {
|
|
485
459
|
kind: "ready",
|
|
@@ -518,11 +492,7 @@ export async function startSubagentJob(
|
|
|
518
492
|
hostSignal,
|
|
519
493
|
hostOnUpdate,
|
|
520
494
|
);
|
|
521
|
-
if (prepared.kind !== "ready")
|
|
522
|
-
if (prepared.kind === "aborted")
|
|
523
|
-
return { kind: "cancelled", makeDetails: prepared.makeDetails };
|
|
524
|
-
return prepared;
|
|
525
|
-
}
|
|
495
|
+
if (prepared.kind !== "ready") return prepared;
|
|
526
496
|
const { lc, instanceName, requestProgressRender } = prepared;
|
|
527
497
|
if (getSubagentDepth() > 0) {
|
|
528
498
|
const result = await runSubagentLifecycle(lc);
|
package/src/output/normalize.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const TERMINAL_SENTENCE_MAX_CHARS = 100;
|
|
2
2
|
|
|
3
3
|
export function normalizeSummaryValue(value: string): string {
|
|
4
4
|
const normalized = value.trim().replace(/\s+/g, " ");
|
|
@@ -67,7 +67,7 @@ export function normalizeTerminalSentence(
|
|
|
67
67
|
return truncateText(collapsed, limit);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
const TOOL_PREVIEW_MAX_CHARS = 120;
|
|
71
71
|
|
|
72
72
|
export function normalizeAndTruncate(
|
|
73
73
|
text: string,
|
package/src/output/ui.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type { AgentScope } from "../agent/agents.js";
|
|
|
11
11
|
import {
|
|
12
12
|
formatContextPercent,
|
|
13
13
|
formatElapsed,
|
|
14
|
+
} from "../progress/progress-format.js";
|
|
15
|
+
import {
|
|
14
16
|
type ProgressStatus,
|
|
15
17
|
STATUS_BG,
|
|
16
18
|
STATUS_COLOR,
|
|
@@ -18,8 +20,8 @@ import {
|
|
|
18
20
|
type SubagentProgressState,
|
|
19
21
|
type ThemeBg,
|
|
20
22
|
} from "../progress/progress-state.js";
|
|
21
|
-
import { hasSubagentFailed } from "../progress/result-details.js";
|
|
22
23
|
import type { SubagentDetails, UsageStats } from "../shared/types.js";
|
|
24
|
+
import { hasSubagentFailed } from "../shared/utils.js";
|
|
23
25
|
import {
|
|
24
26
|
extractSemanticToolTarget,
|
|
25
27
|
normalizeSummaryValue,
|
|
@@ -115,9 +117,6 @@ export function formatResultFooter(usage: UsageStats, model?: string): string {
|
|
|
115
117
|
return parts.join(" · ");
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
/**
|
|
119
|
-
* Formats a tool call for the UI, optionally extracting a semantic target for clarity.
|
|
120
|
-
*/
|
|
121
120
|
export function formatToolCall(
|
|
122
121
|
toolName: string,
|
|
123
122
|
args: Record<string, unknown>,
|
|
@@ -131,9 +130,6 @@ export function formatToolCall(
|
|
|
131
130
|
return themeFg("accent", toolName) + themeFg("dim", ` ${target}`);
|
|
132
131
|
}
|
|
133
132
|
|
|
134
|
-
/**
|
|
135
|
-
* Extracts the final text response from an array of assistant messages.
|
|
136
|
-
*/
|
|
137
133
|
export function getFinalOutput(messages: Message[]): string {
|
|
138
134
|
const lastAsst = messages.findLast((m) => m.role === "assistant");
|
|
139
135
|
const lastText = lastAsst?.content.findLast((p) => p.type === "text");
|
|
@@ -148,9 +144,6 @@ function stripOutcomeLineForResultUi(output: string): string {
|
|
|
148
144
|
return stripped.trim() ? stripped : output;
|
|
149
145
|
}
|
|
150
146
|
|
|
151
|
-
/**
|
|
152
|
-
* Maps subagent theme colors to Markdown rendering components.
|
|
153
|
-
*/
|
|
154
147
|
function makeMarkdownTheme(theme: SubagentTheme): MarkdownTheme {
|
|
155
148
|
const fg = (c: ThemeColor) => (text: string) => theme.fg(c, text);
|
|
156
149
|
return {
|
|
@@ -172,9 +165,6 @@ function makeMarkdownTheme(theme: SubagentTheme): MarkdownTheme {
|
|
|
172
165
|
};
|
|
173
166
|
}
|
|
174
167
|
|
|
175
|
-
/**
|
|
176
|
-
* Renders the pending subagent call UI component.
|
|
177
|
-
*/
|
|
178
168
|
export function renderSubagentCall(
|
|
179
169
|
args: { agent?: string; task?: string; agentScope?: AgentScope },
|
|
180
170
|
theme: SubagentTheme,
|
|
@@ -205,7 +195,7 @@ export function renderSubagentToolResult(
|
|
|
205
195
|
export function renderSubagentResult(
|
|
206
196
|
result: { content: { type: string; text?: string }[]; details?: unknown },
|
|
207
197
|
theme: SubagentTheme,
|
|
208
|
-
|
|
198
|
+
display?: { isPartial?: boolean },
|
|
209
199
|
bodyOverride?: string,
|
|
210
200
|
): Component {
|
|
211
201
|
const details = result.details as SubagentDetails | undefined;
|
|
@@ -228,7 +218,7 @@ export function renderSubagentResult(
|
|
|
228
218
|
const finalOutput = r.finalOutput ?? getFinalOutput(r.messages ?? []);
|
|
229
219
|
const title = formatSubagentTitle(r.agent, r.instanceName, theme);
|
|
230
220
|
let effectiveBody = bodyOverride ?? finalOutput;
|
|
231
|
-
if (
|
|
221
|
+
if (display?.isPartial && !finalOutput?.trim() && !bodyOverride) {
|
|
232
222
|
effectiveBody =
|
|
233
223
|
result.content[0]?.text ||
|
|
234
224
|
r.progress?.activityText ||
|
|
@@ -241,7 +231,7 @@ export function renderSubagentResult(
|
|
|
241
231
|
const ctxPercent = formatContextPercent({
|
|
242
232
|
contextTokens: r.usage.contextTokens,
|
|
243
233
|
contextWindowTokens: r.usage.contextWindowTokens,
|
|
244
|
-
}
|
|
234
|
+
});
|
|
245
235
|
const metadata = `${toolLabel} · ${ctxPercent} ctx · ${formatElapsed(r.durationMs ?? 0)}`;
|
|
246
236
|
const usageStr = formatResultFooter(r.usage, r.model);
|
|
247
237
|
return renderStatusCard(
|
|
@@ -285,7 +275,7 @@ function renderStatusCard(
|
|
|
285
275
|
: "";
|
|
286
276
|
box.addChild(new Text(`${icon} ${options.title} ${status}${metadata}`, 0, 0));
|
|
287
277
|
box.addChild(makeStatusCardBody(options, theme));
|
|
288
|
-
if (options.
|
|
278
|
+
if (options.footer)
|
|
289
279
|
box.addChild(new Text(theme.fg("dim", options.footer), 0, 0));
|
|
290
280
|
return box;
|
|
291
281
|
}
|
|
@@ -294,7 +284,7 @@ function makeStatusCardBody(
|
|
|
294
284
|
options: StatusCardOptions,
|
|
295
285
|
theme: SubagentTheme,
|
|
296
286
|
): Box {
|
|
297
|
-
const body = new Box(2, options.variant === "full" ? 1 : 0);
|
|
287
|
+
const body = new Box(2, options.variant === "full" || options.footer ? 1 : 0);
|
|
298
288
|
const bodyText = options.body ?? "";
|
|
299
289
|
if (bodyText && options.variant === "full") {
|
|
300
290
|
body.addChild(
|
|
@@ -336,16 +326,15 @@ function renderJobCard(
|
|
|
336
326
|
bodyText.length > BODY_PREVIEW_MAX
|
|
337
327
|
? `${bodyText.slice(0, BODY_PREVIEW_MAX - 1)}…`
|
|
338
328
|
: bodyText;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
);
|
|
329
|
+
const options: StatusCardOptions = {
|
|
330
|
+
status: state.status,
|
|
331
|
+
title,
|
|
332
|
+
variant: "abridged",
|
|
333
|
+
metadata,
|
|
334
|
+
body: preview,
|
|
335
|
+
};
|
|
336
|
+
if (state.modelDisplay) options.footer = state.modelDisplay;
|
|
337
|
+
return renderStatusCard(options, theme);
|
|
349
338
|
}
|
|
350
339
|
|
|
351
340
|
function sortByStartTimeDesc(
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting and rendering helpers for subagent progress display.
|
|
3
|
+
*
|
|
4
|
+
* Pure presentation logic extracted from progress-state.js. No state
|
|
5
|
+
* management or store access — only transforms data into display strings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
normalizeAndTruncate,
|
|
10
|
+
normalizeSummaryValue,
|
|
11
|
+
truncateText,
|
|
12
|
+
} from "../output/normalize.js";
|
|
13
|
+
import type { ToolActivity } from "../shared/types.js";
|
|
14
|
+
import type { SubagentProgressState } from "./progress-state.js";
|
|
15
|
+
|
|
16
|
+
export const SENSITIVE_PATTERN = /secret|token|password/i;
|
|
17
|
+
|
|
18
|
+
const REDACTED_PLACEHOLDER = "(running...)";
|
|
19
|
+
const REDACTED_PLACEHOLDER_LENGTH = REDACTED_PLACEHOLDER.length;
|
|
20
|
+
|
|
21
|
+
function redactOrTruncate(text: string, maxChars: number): string {
|
|
22
|
+
if (SENSITIVE_PATTERN.test(text))
|
|
23
|
+
return maxChars >= REDACTED_PLACEHOLDER_LENGTH ? REDACTED_PLACEHOLDER : "";
|
|
24
|
+
return truncateText(text, maxChars);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function walkActivityTree(activity: ToolActivity): string[] {
|
|
28
|
+
const parts: string[] = [];
|
|
29
|
+
let current: ToolActivity | undefined = activity;
|
|
30
|
+
while (current) {
|
|
31
|
+
if (current.inputSummary) {
|
|
32
|
+
const annotated = current.instanceName
|
|
33
|
+
? `${current.inputSummary} [${current.instanceName}]`
|
|
34
|
+
: current.inputSummary;
|
|
35
|
+
parts.push(annotated);
|
|
36
|
+
}
|
|
37
|
+
current = current.child;
|
|
38
|
+
}
|
|
39
|
+
return parts;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Format a millisecond duration for compact display.
|
|
44
|
+
* Renders sub-minute durations as decimal seconds (`45.2s`),
|
|
45
|
+
* longer durations as minutes and whole seconds (`2m 15s`).
|
|
46
|
+
*/
|
|
47
|
+
export function formatElapsed(ms: number): string {
|
|
48
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
49
|
+
const mins = Math.floor(ms / 60000);
|
|
50
|
+
const secs = Math.floor((ms % 60000) / 1000);
|
|
51
|
+
return `${mins}m ${secs}s`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatContextPercent(state: {
|
|
55
|
+
contextTokens?: number | undefined;
|
|
56
|
+
contextWindowTokens?: number | undefined;
|
|
57
|
+
}): string {
|
|
58
|
+
const windowTokens = state.contextWindowTokens;
|
|
59
|
+
if (!windowTokens || windowTokens <= 0 || !Number.isFinite(windowTokens))
|
|
60
|
+
return "--%";
|
|
61
|
+
const usedTokens = state.contextTokens;
|
|
62
|
+
if (!usedTokens || usedTokens <= 0 || !Number.isFinite(usedTokens))
|
|
63
|
+
return "0%";
|
|
64
|
+
return `${Math.round((usedTokens / windowTokens) * 100)}%`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format the one-line statistics header for a subagent progress display.
|
|
69
|
+
* Includes tool count, context window usage, and elapsed time.
|
|
70
|
+
* When the subagent is still running (`durationMs` unset), elapsed is
|
|
71
|
+
* computed live from `startTime`.
|
|
72
|
+
*
|
|
73
|
+
* @returns Single line, e.g. `"3 tools · 45% ctx · 12.3s"`
|
|
74
|
+
*/
|
|
75
|
+
export function formatHeaderStats(state: SubagentProgressState): string {
|
|
76
|
+
const elapsedMs = state.durationMs ?? Date.now() - state.startTime;
|
|
77
|
+
const toolLabel = state.toolCount === 1 ? "tool" : "tools";
|
|
78
|
+
return `${state.toolCount} ${toolLabel} · ${formatContextPercent(state)} ctx · ${formatElapsed(elapsedMs)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Renders a ToolActivity tree for storage. Each segment is
|
|
83
|
+
* independently normalized and truncated to TOOL_PREVIEW_MAX_CHARS (120).
|
|
84
|
+
*/
|
|
85
|
+
export function renderToolActivity(
|
|
86
|
+
activity: ToolActivity | undefined,
|
|
87
|
+
): string | undefined {
|
|
88
|
+
if (!activity) return undefined;
|
|
89
|
+
const parts = walkActivityTree(activity);
|
|
90
|
+
if (parts.length === 0) return activity.toolName;
|
|
91
|
+
const result = parts.map((p) => normalizeAndTruncate(p)).join(" - ");
|
|
92
|
+
if (SENSITIVE_PATTERN.test(result)) return REDACTED_PLACEHOLDER;
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Renders a ToolActivity tree for display with a caller-provided truncation
|
|
98
|
+
* budget. Segments are normalized without individual truncation so the
|
|
99
|
+
* joined result shares one post-join display budget.
|
|
100
|
+
*/
|
|
101
|
+
export function renderToolActivityForDisplay(
|
|
102
|
+
activity: ToolActivity | undefined,
|
|
103
|
+
maxChars: number,
|
|
104
|
+
): string | undefined {
|
|
105
|
+
if (!activity) return undefined;
|
|
106
|
+
if (maxChars <= 0) return "";
|
|
107
|
+
const parts = walkActivityTree(activity);
|
|
108
|
+
if (parts.length === 0) return redactOrTruncate(activity.toolName, maxChars);
|
|
109
|
+
const joined = parts.map((p) => normalizeSummaryValue(p)).join(" - ");
|
|
110
|
+
return redactOrTruncate(joined, maxChars);
|
|
111
|
+
}
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
normalizeAndTruncate,
|
|
7
7
|
normalizeSummaryValue,
|
|
8
8
|
normalizeTerminalSentence,
|
|
9
|
-
truncateText,
|
|
10
9
|
} from "../output/normalize.js";
|
|
11
10
|
import type {
|
|
12
11
|
SingleResult,
|
|
@@ -14,8 +13,6 @@ import type {
|
|
|
14
13
|
ToolActivity,
|
|
15
14
|
} from "../shared/types.js";
|
|
16
15
|
|
|
17
|
-
export const SENSITIVE_PATTERN = /secret|token|password/i;
|
|
18
|
-
|
|
19
16
|
export type ThemeBg = "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
20
17
|
|
|
21
18
|
export type ProgressStatus = "running" | "success" | "error" | "cancelled";
|
|
@@ -57,6 +54,7 @@ export interface SubagentProgressState {
|
|
|
57
54
|
outputTokens?: number | undefined;
|
|
58
55
|
contextTokens?: number | undefined;
|
|
59
56
|
contextWindowTokens?: number | undefined;
|
|
57
|
+
modelDisplay?: string | undefined;
|
|
60
58
|
finalOutput?: string | undefined;
|
|
61
59
|
errorText?: string | undefined;
|
|
62
60
|
}
|
|
@@ -97,12 +95,8 @@ type ProgressTransientFields = Pick<
|
|
|
97
95
|
function stripTransientFields(
|
|
98
96
|
merged: SubagentProgressState,
|
|
99
97
|
): Omit<SubagentProgressState, keyof ProgressTransientFields> {
|
|
100
|
-
const {
|
|
101
|
-
|
|
102
|
-
lastToolPreview: _l,
|
|
103
|
-
toolResultCompleted: _t,
|
|
104
|
-
...base
|
|
105
|
-
} = merged;
|
|
98
|
+
const { activeToolActivity, lastToolPreview, toolResultCompleted, ...base } =
|
|
99
|
+
merged;
|
|
106
100
|
return base;
|
|
107
101
|
}
|
|
108
102
|
|
|
@@ -112,11 +106,15 @@ export function patchProgressState(
|
|
|
112
106
|
): void {
|
|
113
107
|
const state = store.get(requestId);
|
|
114
108
|
if (!state) return;
|
|
109
|
+
const merged: SubagentProgressState = { ...state, ...patch };
|
|
110
|
+
if (merged.modelDisplay === undefined || merged.modelDisplay === "") {
|
|
111
|
+
delete merged.modelDisplay;
|
|
112
|
+
}
|
|
115
113
|
if (state.status !== "running") {
|
|
116
|
-
store.set(requestId, stripTransientFields(
|
|
114
|
+
store.set(requestId, stripTransientFields(merged));
|
|
117
115
|
return;
|
|
118
116
|
}
|
|
119
|
-
store.set(requestId,
|
|
117
|
+
store.set(requestId, merged);
|
|
120
118
|
}
|
|
121
119
|
|
|
122
120
|
function storeTerminalProgressState(
|
|
@@ -256,22 +254,26 @@ function extractProgressFromExistingProgress(
|
|
|
256
254
|
if (progress.activeToolActivity) {
|
|
257
255
|
state.activeToolActivity = progress.activeToolActivity;
|
|
258
256
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
257
|
+
const previewValue = progress.lastToolPreview;
|
|
258
|
+
const truncatedPreview =
|
|
259
|
+
typeof previewValue === "string" && previewValue.trim()
|
|
260
|
+
? normalizeAndTruncate(previewValue)
|
|
261
|
+
: undefined;
|
|
262
|
+
if (truncatedPreview) {
|
|
263
|
+
state.progressLastToolPreview = truncatedPreview;
|
|
266
264
|
}
|
|
267
265
|
if (progress.toolResultCompleted) {
|
|
268
266
|
state.toolResultCompleted = true;
|
|
269
267
|
}
|
|
268
|
+
const hasToolCalls = progress.toolCalls.some(isDerivedToolCall);
|
|
270
269
|
for (const toolCall of progress.toolCalls) {
|
|
271
270
|
if (!isDerivedToolCall(toolCall)) continue;
|
|
272
271
|
const preview = normalizeAndTruncate(toolCall.preview);
|
|
273
272
|
trackNewToolCall(toolCall.id, preview, seenToolCallIds, state);
|
|
274
273
|
}
|
|
274
|
+
if (truncatedPreview && !hasToolCalls) {
|
|
275
|
+
state.lastToolPreview = truncatedPreview;
|
|
276
|
+
}
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
function extractProgressFromMessages(
|
|
@@ -336,93 +338,3 @@ export function isToolCallPart(part: unknown): part is {
|
|
|
336
338
|
typeof part["name"] === "string"
|
|
337
339
|
);
|
|
338
340
|
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Format a millisecond duration for compact display.
|
|
342
|
-
* Renders sub-minute durations as decimal seconds (`45.2s`),
|
|
343
|
-
* longer durations as minutes and whole seconds (`2m 15s`).
|
|
344
|
-
*/
|
|
345
|
-
export function formatElapsed(ms: number): string {
|
|
346
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
347
|
-
const mins = Math.floor(ms / 60000);
|
|
348
|
-
const secs = Math.floor((ms % 60000) / 1000);
|
|
349
|
-
return `${mins}m ${secs}s`;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export function formatContextPercent(state: SubagentProgressState): string {
|
|
353
|
-
const d = state.contextWindowTokens;
|
|
354
|
-
if (!d || d <= 0 || !Number.isFinite(d)) return "--%";
|
|
355
|
-
const n = state.contextTokens;
|
|
356
|
-
if (!n || n <= 0 || !Number.isFinite(n)) return "0%";
|
|
357
|
-
return `${Math.round((n / d) * 100)}%`;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Format the one-line statistics header for a subagent progress display.
|
|
362
|
-
* Includes tool count, context window usage, and elapsed time.
|
|
363
|
-
* When the subagent is still running (`durationMs` unset), elapsed is
|
|
364
|
-
* computed live from `startTime`.
|
|
365
|
-
*
|
|
366
|
-
* @returns Single line ending in `\n`, e.g. `"3 tools · 45% ctx · 12.3s\n"`
|
|
367
|
-
*/
|
|
368
|
-
export function formatHeaderStats(state: SubagentProgressState): string {
|
|
369
|
-
const elapsedMs = state.durationMs ?? Date.now() - state.startTime;
|
|
370
|
-
const toolLabel = state.toolCount === 1 ? "tool" : "tools";
|
|
371
|
-
return `${state.toolCount} ${toolLabel} · ${formatContextPercent(state)} ctx · ${formatElapsed(elapsedMs)}\n`;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const REDACTED_PLACEHOLDER = "(running...)";
|
|
375
|
-
const REDACTED_PLACEHOLDER_LENGTH = REDACTED_PLACEHOLDER.length;
|
|
376
|
-
|
|
377
|
-
function redactOrTruncate(text: string, maxChars: number): string {
|
|
378
|
-
if (SENSITIVE_PATTERN.test(text))
|
|
379
|
-
return maxChars >= REDACTED_PLACEHOLDER_LENGTH ? REDACTED_PLACEHOLDER : "";
|
|
380
|
-
return truncateText(text, maxChars);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function walkActivityTree(activity: ToolActivity): string[] {
|
|
384
|
-
const parts: string[] = [];
|
|
385
|
-
let current: ToolActivity | undefined = activity;
|
|
386
|
-
while (current) {
|
|
387
|
-
if (current.inputSummary) {
|
|
388
|
-
const annotated = current.instanceName
|
|
389
|
-
? `${current.inputSummary} [${current.instanceName}]`
|
|
390
|
-
: current.inputSummary;
|
|
391
|
-
parts.push(annotated);
|
|
392
|
-
}
|
|
393
|
-
current = current.child;
|
|
394
|
-
}
|
|
395
|
-
return parts;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Renders a ToolActivity tree for storage. Each segment is
|
|
400
|
-
* independently normalized and truncated to TOOL_PREVIEW_MAX_CHARS (120).
|
|
401
|
-
*/
|
|
402
|
-
export function renderToolActivity(
|
|
403
|
-
activity: ToolActivity | undefined,
|
|
404
|
-
): string | undefined {
|
|
405
|
-
if (!activity) return undefined;
|
|
406
|
-
const parts = walkActivityTree(activity);
|
|
407
|
-
if (parts.length === 0) return activity.toolName;
|
|
408
|
-
const result = parts.map((p) => normalizeAndTruncate(p)).join(" - ");
|
|
409
|
-
if (SENSITIVE_PATTERN.test(result)) return REDACTED_PLACEHOLDER;
|
|
410
|
-
return result;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Renders a ToolActivity tree for display with a caller-provided truncation
|
|
415
|
-
* budget. Segments are normalized without individual truncation so the
|
|
416
|
-
* joined result shares one post-join display budget.
|
|
417
|
-
*/
|
|
418
|
-
export function renderToolActivityForDisplay(
|
|
419
|
-
activity: ToolActivity | undefined,
|
|
420
|
-
maxChars: number,
|
|
421
|
-
): string | undefined {
|
|
422
|
-
if (!activity) return undefined;
|
|
423
|
-
if (maxChars <= 0) return "";
|
|
424
|
-
const parts = walkActivityTree(activity);
|
|
425
|
-
if (parts.length === 0) return redactOrTruncate(activity.toolName, maxChars);
|
|
426
|
-
const joined = parts.map((p) => normalizeSummaryValue(p)).join(" - ");
|
|
427
|
-
return redactOrTruncate(joined, maxChars);
|
|
428
|
-
}
|