@mystilleef/pi-subagent 0.3.1 → 0.4.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 +169 -11
- package/package.json +30 -12
- package/src/cancel-command.ts +3 -1
- package/src/index.ts +5 -0
- package/src/instance-name.ts +164 -0
- package/src/jobs-command.ts +41 -0
- package/src/progress-state.ts +80 -0
- package/src/progress.ts +100 -111
- package/src/run-registry.ts +1 -0
- package/src/subagent-orchestrator.ts +74 -16
- package/src/termination.ts +7 -6
- package/src/types.ts +1 -0
- package/src/ui.ts +198 -15
package/src/ui.ts
CHANGED
|
@@ -12,13 +12,20 @@ import {
|
|
|
12
12
|
extractSemanticToolTarget,
|
|
13
13
|
normalizeSummaryValue,
|
|
14
14
|
} from "./normalize.js";
|
|
15
|
+
import {
|
|
16
|
+
formatContextPercent,
|
|
17
|
+
formatElapsed,
|
|
18
|
+
type ProgressStatus,
|
|
19
|
+
STATUS_BG,
|
|
20
|
+
STATUS_COLOR,
|
|
21
|
+
STATUS_ICON,
|
|
22
|
+
type SubagentProgressState,
|
|
23
|
+
type ThemeBg,
|
|
24
|
+
} from "./progress-state.js";
|
|
15
25
|
import { hasSubagentFailed } from "./result-details.js";
|
|
16
26
|
import type { SubagentDetails, UsageStats } from "./types.js";
|
|
17
27
|
|
|
18
|
-
|
|
19
|
-
* Background theme keys for subagent tool status.
|
|
20
|
-
*/
|
|
21
|
-
export type ThemeBg = "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
28
|
+
export type { ThemeBg };
|
|
22
29
|
|
|
23
30
|
/**
|
|
24
31
|
* Abstraction for theme-aware text formatting.
|
|
@@ -27,8 +34,35 @@ export type SubagentTheme = {
|
|
|
27
34
|
fg: (color: ThemeColor, text: string) => string;
|
|
28
35
|
bg: (color: ThemeBg, text: string) => string;
|
|
29
36
|
bold: (text: string) => string;
|
|
37
|
+
italic?: (text: string) => string;
|
|
30
38
|
};
|
|
31
39
|
|
|
40
|
+
const ANSI_ITALIC_ON = "\x1b[3m";
|
|
41
|
+
const ANSI_ITALIC_OFF = "\x1b[23m";
|
|
42
|
+
const ANSI_STRIKETHROUGH_ON = "\x1b[9m";
|
|
43
|
+
const ANSI_STRIKETHROUGH_OFF = "\x1b[29m";
|
|
44
|
+
const ANSI_UNDERLINE_ON = "\x1b[4m";
|
|
45
|
+
const ANSI_UNDERLINE_OFF = "\x1b[24m";
|
|
46
|
+
|
|
47
|
+
function italicText(text: string, theme: SubagentTheme): string {
|
|
48
|
+
return theme.italic
|
|
49
|
+
? theme.italic(text)
|
|
50
|
+
: `${ANSI_ITALIC_ON}${text}${ANSI_ITALIC_OFF}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Formats the shared subagent title from agent and optional instance name.
|
|
55
|
+
*/
|
|
56
|
+
export function formatSubagentTitle(
|
|
57
|
+
agent: string,
|
|
58
|
+
instanceName: string | undefined,
|
|
59
|
+
theme: SubagentTheme,
|
|
60
|
+
): string {
|
|
61
|
+
const agentSegment = theme.fg("toolTitle", theme.bold(agent));
|
|
62
|
+
if (!instanceName) return agentSegment;
|
|
63
|
+
return `${agentSegment} ${theme.fg("accent", italicText(instanceName, theme))}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
32
66
|
/**
|
|
33
67
|
* Formats token counts into human-readable strings (e.g., "1.2k", "1.5M").
|
|
34
68
|
*/
|
|
@@ -96,7 +130,7 @@ export function formatResultFooter(
|
|
|
96
130
|
parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
97
131
|
if (typeof durationMs === "number") parts.push(formatDuration(durationMs));
|
|
98
132
|
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
99
|
-
return
|
|
133
|
+
return parts.join(" · ");
|
|
100
134
|
}
|
|
101
135
|
|
|
102
136
|
/**
|
|
@@ -149,9 +183,10 @@ function makeMarkdownTheme(theme: SubagentTheme): MarkdownTheme {
|
|
|
149
183
|
hr: fg("mdHr"),
|
|
150
184
|
listBullet: fg("mdListBullet"),
|
|
151
185
|
bold: (text) => theme.bold(text),
|
|
152
|
-
italic: (text) =>
|
|
153
|
-
strikethrough: (text) =>
|
|
154
|
-
|
|
186
|
+
italic: (text) => `${ANSI_ITALIC_ON}${text}${ANSI_ITALIC_OFF}`,
|
|
187
|
+
strikethrough: (text) =>
|
|
188
|
+
`${ANSI_STRIKETHROUGH_ON}${text}${ANSI_STRIKETHROUGH_OFF}`,
|
|
189
|
+
underline: (text) => `${ANSI_UNDERLINE_ON}${text}${ANSI_UNDERLINE_OFF}`,
|
|
155
190
|
};
|
|
156
191
|
}
|
|
157
192
|
|
|
@@ -199,20 +234,168 @@ export function renderSubagentResult(
|
|
|
199
234
|
);
|
|
200
235
|
}
|
|
201
236
|
const failed = hasSubagentFailed(r);
|
|
237
|
+
const cancelled = r.stopReason === "aborted";
|
|
238
|
+
const resultStatus: ProgressStatus = cancelled
|
|
239
|
+
? "cancelled"
|
|
240
|
+
: failed
|
|
241
|
+
? "error"
|
|
242
|
+
: "success";
|
|
202
243
|
const finalOutput = r.finalOutput ?? getFinalOutput(r.messages ?? []);
|
|
203
|
-
const
|
|
204
|
-
const box = new Box(1, 1, (line) => theme.bg(bg, line));
|
|
244
|
+
const title = formatSubagentTitle(r.agent, r.instanceName, theme);
|
|
205
245
|
const bodyText = stripOutcomeLineForResultUi(bodyOverride ?? finalOutput);
|
|
206
|
-
|
|
207
|
-
|
|
246
|
+
const usageStr = formatResultFooter(r.usage, r.model, r.durationMs);
|
|
247
|
+
return renderStatusCard(
|
|
248
|
+
{
|
|
249
|
+
status: resultStatus,
|
|
250
|
+
title,
|
|
251
|
+
variant: "full",
|
|
252
|
+
body: bodyText,
|
|
253
|
+
footer: usageStr,
|
|
254
|
+
},
|
|
255
|
+
theme,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
type StatusCardVariant = "full" | "abridged";
|
|
260
|
+
|
|
261
|
+
type StatusCardOptions = {
|
|
262
|
+
status: ProgressStatus;
|
|
263
|
+
title: string;
|
|
264
|
+
variant: StatusCardVariant;
|
|
265
|
+
metadata?: string;
|
|
266
|
+
body?: string;
|
|
267
|
+
footer?: string;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
function renderStatusCard(
|
|
271
|
+
options: StatusCardOptions,
|
|
272
|
+
theme: SubagentTheme,
|
|
273
|
+
): Box {
|
|
274
|
+
const box = new Box(1, 1, (line) =>
|
|
275
|
+
theme.bg(STATUS_BG[options.status], line),
|
|
276
|
+
);
|
|
277
|
+
const icon = theme.fg(
|
|
278
|
+
STATUS_COLOR[options.status],
|
|
279
|
+
STATUS_ICON[options.status],
|
|
280
|
+
);
|
|
281
|
+
const status = theme.fg("dim", `[${options.status}]`);
|
|
282
|
+
const metadata = options.metadata
|
|
283
|
+
? ` ${theme.fg("muted", options.metadata)}`
|
|
284
|
+
: "";
|
|
285
|
+
box.addChild(new Text(`${icon} ${options.title} ${status}${metadata}`, 0, 0));
|
|
286
|
+
box.addChild(makeStatusCardBody(options, theme));
|
|
287
|
+
if (options.variant === "full" && options.footer)
|
|
288
|
+
box.addChild(new Text(theme.fg("dim", options.footer), 0, 0));
|
|
289
|
+
return box;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function makeStatusCardBody(
|
|
293
|
+
options: StatusCardOptions,
|
|
294
|
+
theme: SubagentTheme,
|
|
295
|
+
): Box {
|
|
296
|
+
const body = new Box(2, options.variant === "full" ? 1 : 0);
|
|
297
|
+
const bodyText = options.body ?? "";
|
|
298
|
+
if (bodyText && options.variant === "full") {
|
|
299
|
+
body.addChild(
|
|
208
300
|
new Markdown(bodyText, 0, 0, makeMarkdownTheme(theme), {
|
|
209
301
|
color: (text) => theme.fg("toolOutput", text),
|
|
210
302
|
}),
|
|
211
303
|
);
|
|
304
|
+
} else if (bodyText) {
|
|
305
|
+
body.addChild(new Text(theme.fg("toolOutput", bodyText), 0, 0));
|
|
212
306
|
} else {
|
|
213
|
-
|
|
307
|
+
body.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
|
|
214
308
|
}
|
|
215
|
-
|
|
216
|
-
|
|
309
|
+
return body;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const BODY_PREVIEW_MAX = 120;
|
|
313
|
+
|
|
314
|
+
function selectRunsBoardBody(state: SubagentProgressState): string {
|
|
315
|
+
return (
|
|
316
|
+
[state.finalOutput, state.errorText, state.taskPreview].find(
|
|
317
|
+
(c): c is string => typeof c === "string" && c.trim().length > 0,
|
|
318
|
+
) ?? ""
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function renderJobCard(
|
|
323
|
+
state: SubagentProgressState,
|
|
324
|
+
theme: SubagentTheme,
|
|
325
|
+
): Box {
|
|
326
|
+
const title = formatSubagentTitle(state.agent, state.instanceName, theme);
|
|
327
|
+
const elapsed = formatElapsed(
|
|
328
|
+
state.durationMs ?? Date.now() - state.startTime,
|
|
329
|
+
);
|
|
330
|
+
const ctxPercent = formatContextPercent(state);
|
|
331
|
+
const toolLabel = state.toolCount === 1 ? "tool" : "tools";
|
|
332
|
+
const metadata = `${state.toolCount} ${toolLabel} · ${ctxPercent} ctx · ${elapsed}`;
|
|
333
|
+
const bodyText = selectRunsBoardBody(state);
|
|
334
|
+
const preview =
|
|
335
|
+
bodyText.length > BODY_PREVIEW_MAX
|
|
336
|
+
? `${bodyText.slice(0, BODY_PREVIEW_MAX - 1)}…`
|
|
337
|
+
: bodyText;
|
|
338
|
+
return renderStatusCard(
|
|
339
|
+
{
|
|
340
|
+
status: state.status,
|
|
341
|
+
title,
|
|
342
|
+
variant: "abridged",
|
|
343
|
+
metadata,
|
|
344
|
+
body: preview,
|
|
345
|
+
},
|
|
346
|
+
theme,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Sort states by startTime descending (newest first). */
|
|
351
|
+
function sortByStartTimeDesc(
|
|
352
|
+
a: SubagentProgressState,
|
|
353
|
+
b: SubagentProgressState,
|
|
354
|
+
): number {
|
|
355
|
+
return b.startTime - a.startTime;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Ordered section definitions: label → status filter for the runs board. */
|
|
359
|
+
const BOARD_SECTIONS: [string, ProgressStatus][] = [
|
|
360
|
+
["ACTIVE", "running"],
|
|
361
|
+
["FAILED", "error"],
|
|
362
|
+
["CANCELLED", "cancelled"],
|
|
363
|
+
["SUCCEEDED", "success"],
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Renders a unified job board for the `/jobs` command.
|
|
368
|
+
* Jobs render in status-specific sections, each sorted by `startTime` descending.
|
|
369
|
+
* Status icons preserve the existing /jobs contract for running and cancelled jobs.
|
|
370
|
+
*/
|
|
371
|
+
export function renderRunsBoard(
|
|
372
|
+
states: SubagentProgressState[],
|
|
373
|
+
theme: SubagentTheme,
|
|
374
|
+
width = 80,
|
|
375
|
+
): Component {
|
|
376
|
+
if (states.length === 0) {
|
|
377
|
+
return new Text(theme.fg("muted", "No /run jobs in this session."), 0, 0);
|
|
378
|
+
}
|
|
379
|
+
const grouped = new Map<ProgressStatus, SubagentProgressState[]>();
|
|
380
|
+
for (const s of states) {
|
|
381
|
+
const bucket = grouped.get(s.status);
|
|
382
|
+
if (bucket) bucket.push(s);
|
|
383
|
+
else grouped.set(s.status, [s]);
|
|
384
|
+
}
|
|
385
|
+
for (const bucket of grouped.values()) bucket.sort(sortByStartTimeDesc);
|
|
386
|
+
const box = new Box(0, 0);
|
|
387
|
+
const addSection = (
|
|
388
|
+
label: string,
|
|
389
|
+
sectionStates: SubagentProgressState[],
|
|
390
|
+
) => {
|
|
391
|
+
if (sectionStates.length === 0) return;
|
|
392
|
+
const sectionHeader = `${label} (${sectionStates.length})`;
|
|
393
|
+
const ruler = "─".repeat(Math.max(0, width - sectionHeader.length - 1));
|
|
394
|
+
box.addChild(new Text(theme.fg("dim", `${sectionHeader} ${ruler}`), 0, 0));
|
|
395
|
+
for (const state of sectionStates)
|
|
396
|
+
box.addChild(renderJobCard(state, theme));
|
|
397
|
+
};
|
|
398
|
+
for (const [label, status] of BOARD_SECTIONS)
|
|
399
|
+
addSection(label, grouped.get(status) ?? []);
|
|
217
400
|
return box;
|
|
218
401
|
}
|