@mystilleef/pi-subagent 0.8.0 → 0.10.1
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 +76 -27
- package/package.json +9 -9
- package/src/agent/agent-cache.ts +1 -0
- package/src/agent/agents.ts +25 -4
- package/src/child/child-events.ts +22 -20
- package/src/child/process.ts +288 -125
- package/src/child/termination.ts +320 -5
- package/src/env.d.ts +15 -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/run-registry.ts +1 -2
- package/src/orchestration/subagent-orchestrator.ts +57 -82
- 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 +55 -140
- package/src/progress/progress.ts +40 -31
- package/src/progress/result-details.ts +146 -39
- package/src/shared/types.ts +18 -16
- package/src/shared/utils.ts +60 -11
- package/tsconfig.json +9 -11
|
@@ -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,12 +78,15 @@ 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,
|
|
78
84
|
) => SubagentDetails;
|
|
79
85
|
|
|
86
|
+
function isDebugDetailsAuthorized(debugRequested: boolean): boolean {
|
|
87
|
+
return debugRequested && process.env.PI_SUBAGENT_DEBUG_ENABLED === "1";
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
interface LifecycleContext {
|
|
81
91
|
pi: ExtensionAPI;
|
|
82
92
|
ctx: ExtensionContext;
|
|
@@ -90,7 +100,7 @@ interface LifecycleContext {
|
|
|
90
100
|
task: string;
|
|
91
101
|
parentModel: { provider: string; id: string } | undefined;
|
|
92
102
|
parentThinking: ThinkingLevel;
|
|
93
|
-
hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails
|
|
103
|
+
hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails> | undefined;
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
function createDetailsBuilder(
|
|
@@ -108,63 +118,6 @@ function createDetailsBuilder(
|
|
|
108
118
|
});
|
|
109
119
|
}
|
|
110
120
|
|
|
111
|
-
function sanitizeResultDetails(
|
|
112
|
-
result: SingleResult,
|
|
113
|
-
includeDebugMessages: boolean,
|
|
114
|
-
options: DetailsOptions | undefined,
|
|
115
|
-
): SingleResult {
|
|
116
|
-
const includeMessages =
|
|
117
|
-
includeDebugMessages && (options?.includeMessages ?? true);
|
|
118
|
-
const { messages, termination, progress, stderr, usage, ...core } = result;
|
|
119
|
-
const { contextWindowTokens, ...usageBase } = usage;
|
|
120
|
-
const sanitized: Record<string, unknown> = {
|
|
121
|
-
...core,
|
|
122
|
-
stderr: includeDebugMessages ? stderr : "",
|
|
123
|
-
usage: { ...usageBase },
|
|
124
|
-
};
|
|
125
|
-
if (contextWindowTokens !== undefined) {
|
|
126
|
-
(sanitized.usage as Record<string, unknown>).contextWindowTokens =
|
|
127
|
-
contextWindowTokens;
|
|
128
|
-
}
|
|
129
|
-
if (progress !== undefined) {
|
|
130
|
-
const {
|
|
131
|
-
activityText,
|
|
132
|
-
activeToolActivity,
|
|
133
|
-
lastToolPreview,
|
|
134
|
-
toolResultCompleted,
|
|
135
|
-
...progBase
|
|
136
|
-
} = progress;
|
|
137
|
-
sanitized.progress = {
|
|
138
|
-
toolCalls: progBase.toolCalls.map((tc) => ({
|
|
139
|
-
id: tc.id,
|
|
140
|
-
preview: tc.preview,
|
|
141
|
-
})),
|
|
142
|
-
...(activityText !== undefined && { activityText }),
|
|
143
|
-
...(activeToolActivity !== undefined && { activeToolActivity }),
|
|
144
|
-
...(lastToolPreview !== undefined && { lastToolPreview }),
|
|
145
|
-
...(toolResultCompleted !== undefined && { toolResultCompleted }),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
if (includeMessages) {
|
|
149
|
-
sanitized.messages = options?.recentMessages
|
|
150
|
-
? [...options.recentMessages]
|
|
151
|
-
: messages !== undefined
|
|
152
|
-
? [...messages]
|
|
153
|
-
: undefined;
|
|
154
|
-
if (includeDebugMessages && termination !== undefined) {
|
|
155
|
-
const { cancelReason, terminationSignal, fallbackCause, ...termBase } =
|
|
156
|
-
termination;
|
|
157
|
-
sanitized.termination = {
|
|
158
|
-
...termBase,
|
|
159
|
-
...(cancelReason !== undefined && { cancelReason }),
|
|
160
|
-
...(terminationSignal !== undefined && { terminationSignal }),
|
|
161
|
-
...(fallbackCause !== undefined && { fallbackCause }),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return sanitized as unknown as SingleResult;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
121
|
function createProgressRenderRequester(
|
|
169
122
|
ctx: ExtensionContext,
|
|
170
123
|
requestId: string,
|
|
@@ -194,11 +147,26 @@ function sendSubagentResultMessage(
|
|
|
194
147
|
});
|
|
195
148
|
}
|
|
196
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
|
+
|
|
197
158
|
export function emitCompletionAlert(
|
|
198
159
|
state: ReturnType<typeof getProgressState>,
|
|
199
160
|
): void {
|
|
200
161
|
if (!state) return;
|
|
201
162
|
if (state.status === "cancelled") return;
|
|
163
|
+
if (
|
|
164
|
+
isDesktopNotificationsEnabled() &&
|
|
165
|
+
!isPerJobNotificationEnabled() &&
|
|
166
|
+
getSubagentDepth() === 0
|
|
167
|
+
) {
|
|
168
|
+
deliverDesktopCompletionNotification(state);
|
|
169
|
+
}
|
|
202
170
|
const tty = (process.stdout as { isTTY?: boolean }).isTTY;
|
|
203
171
|
if (!tty) return;
|
|
204
172
|
process.stdout.write("\x07");
|
|
@@ -268,10 +236,11 @@ function createPayloadFingerprint(payload: {
|
|
|
268
236
|
const contentText = payload.content[0]?.text ?? "";
|
|
269
237
|
const latestResult = payload.details.results[0];
|
|
270
238
|
const activityText = latestResult?.progress?.activityText ?? "";
|
|
271
|
-
const toolCallIds =
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
239
|
+
const toolCallIds = [
|
|
240
|
+
...new Set(latestResult?.progress?.toolCalls?.map((tc) => tc.id)),
|
|
241
|
+
]
|
|
242
|
+
.sort()
|
|
243
|
+
.join(",");
|
|
275
244
|
const exitCode = latestResult?.exitCode ?? 0;
|
|
276
245
|
const stopReason = latestResult?.stopReason ?? "";
|
|
277
246
|
return `${contentText}|${activityText}|${toolCallIds}|${exitCode}|${stopReason}`;
|
|
@@ -318,6 +287,7 @@ async function runSubagentLifecycle(
|
|
|
318
287
|
lc.makeDetails,
|
|
319
288
|
lc.parentModel,
|
|
320
289
|
lc.parentThinking,
|
|
290
|
+
lc.debug,
|
|
321
291
|
);
|
|
322
292
|
return finishLifecycleResult(lc, result);
|
|
323
293
|
} catch (error) {
|
|
@@ -332,15 +302,25 @@ async function runSubagentLifecycle(
|
|
|
332
302
|
clearInterval(timerTick);
|
|
333
303
|
requestProgressRender();
|
|
334
304
|
removeRunJob(lc.requestId);
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (state) {
|
|
338
|
-
emitCompletionAlert(state);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
305
|
+
const state = getProgressState(lc.requestId);
|
|
306
|
+
if (state) deliverLifecycleCompletionNotification(state);
|
|
341
307
|
}
|
|
342
308
|
}
|
|
343
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
|
+
|
|
344
324
|
type StartJobResult =
|
|
345
325
|
| {
|
|
346
326
|
kind: "started";
|
|
@@ -358,11 +338,10 @@ type PrepareSubagentJobResult =
|
|
|
358
338
|
lc: LifecycleContext;
|
|
359
339
|
instanceName: string;
|
|
360
340
|
requestProgressRender: () => void;
|
|
361
|
-
hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails
|
|
341
|
+
hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails> | undefined;
|
|
362
342
|
}
|
|
363
343
|
| { kind: "not_found"; makeDetails: DetailsBuilder }
|
|
364
|
-
| { kind: "cancelled"; makeDetails: DetailsBuilder }
|
|
365
|
-
| { kind: "aborted"; makeDetails: DetailsBuilder };
|
|
344
|
+
| { kind: "cancelled"; makeDetails: DetailsBuilder };
|
|
366
345
|
|
|
367
346
|
export function formatSubagentToolResult(
|
|
368
347
|
agentName: string,
|
|
@@ -409,7 +388,7 @@ async function prepareSubagentJob(
|
|
|
409
388
|
const agentScope: AgentScope = params.agentScope ?? "both";
|
|
410
389
|
const discovery = await getCachedAgentDiscovery(ctx.cwd, agentScope);
|
|
411
390
|
const agents = discovery.agents;
|
|
412
|
-
const debug = params.debug === true;
|
|
391
|
+
const debug = isDebugDetailsAuthorized(params.debug === true);
|
|
413
392
|
const makeDetails = createDetailsBuilder(
|
|
414
393
|
agentScope,
|
|
415
394
|
discovery.projectAgentsDir,
|
|
@@ -474,7 +453,7 @@ async function prepareSubagentJob(
|
|
|
474
453
|
if (mergedSignal.aborted) {
|
|
475
454
|
cancelStartedJob(job, job.cancelReason ?? "Aborted");
|
|
476
455
|
requestProgressRender();
|
|
477
|
-
return { kind: "
|
|
456
|
+
return { kind: "cancelled", makeDetails: makeStartedDetails };
|
|
478
457
|
}
|
|
479
458
|
return {
|
|
480
459
|
kind: "ready",
|
|
@@ -513,11 +492,7 @@ export async function startSubagentJob(
|
|
|
513
492
|
hostSignal,
|
|
514
493
|
hostOnUpdate,
|
|
515
494
|
);
|
|
516
|
-
if (prepared.kind !== "ready")
|
|
517
|
-
if (prepared.kind === "aborted")
|
|
518
|
-
return { kind: "cancelled", makeDetails: prepared.makeDetails };
|
|
519
|
-
return prepared;
|
|
520
|
-
}
|
|
495
|
+
if (prepared.kind !== "ready") return prepared;
|
|
521
496
|
const { lc, instanceName, requestProgressRender } = prepared;
|
|
522
497
|
if (getSubagentDepth() > 0) {
|
|
523
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
|
+
}
|