@esso0428/pi-subagents 0.15.0 → 0.15.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/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/dist/agent-manager.d.ts +8 -0
- package/dist/agent-manager.js +87 -19
- package/dist/agent-runner.js +85 -66
- package/dist/agent-types.js +45 -27
- package/dist/context.js +6 -2
- package/dist/cross-extension-rpc.js +9 -5
- package/dist/custom-agents.js +18 -15
- package/dist/default-agents.js +4 -1
- package/dist/enabled-models.js +16 -11
- package/dist/env.js +4 -1
- package/dist/group-join.js +5 -1
- package/dist/index.js +280 -221
- package/dist/invocation-config.js +6 -2
- package/dist/memory.js +34 -24
- package/dist/model-resolver.js +4 -1
- package/dist/nico-overrides.js +20 -14
- package/dist/output-file.js +21 -15
- package/dist/prompts.js +4 -1
- package/dist/schedule-store.js +21 -16
- package/dist/schedule.js +12 -8
- package/dist/settings.js +23 -15
- package/dist/skill-loader.js +23 -20
- package/dist/status-note.js +4 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.js +4 -1
- package/dist/ui/agent-widget.js +37 -23
- package/dist/ui/conversation-viewer.js +43 -39
- package/dist/ui/fleet-list.js +30 -24
- package/dist/ui/markdown-result.d.ts +3 -0
- package/dist/ui/markdown-result.js +53 -0
- package/dist/ui/schedule-menu.js +4 -1
- package/dist/ui/viewer-keys.js +10 -7
- package/dist/usage.js +10 -4
- package/dist/worktree.js +31 -26
- package/package.json +1 -1
- package/src/agent-manager.ts +72 -0
- package/src/agent-runner.ts +11 -3
- package/src/index.ts +39 -16
- package/src/types.ts +1 -0
- package/src/ui/markdown-result.ts +56 -0
- package/test/agent-manager-history.test.ts +84 -0
- package/test/ui/markdown-result.test.ts +45 -0
package/src/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
type UICtx,
|
|
49
49
|
} from "./ui/agent-widget.js";
|
|
50
50
|
import { FleetList, type FleetUICtx } from "./ui/fleet-list.js";
|
|
51
|
+
import { createMarkdownResult } from "./ui/markdown-result.js";
|
|
51
52
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
52
53
|
import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
|
|
53
54
|
|
|
@@ -240,6 +241,9 @@ function buildDetails(
|
|
|
240
241
|
};
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
/** Maximum expanded notification body retained in message details. */
|
|
245
|
+
const MAX_NOTIFICATION_RESULT_TEXT_LENGTH = 20_000;
|
|
246
|
+
|
|
243
247
|
/** Build notification details for the custom message renderer. */
|
|
244
248
|
function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, activity?: AgentActivity): NotificationDetails {
|
|
245
249
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
@@ -255,6 +259,11 @@ function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, act
|
|
|
255
259
|
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
256
260
|
outputFile: record.outputFile,
|
|
257
261
|
error: record.error,
|
|
262
|
+
resultText: record.result
|
|
263
|
+
? record.result.length > MAX_NOTIFICATION_RESULT_TEXT_LENGTH
|
|
264
|
+
? record.result.slice(0, MAX_NOTIFICATION_RESULT_TEXT_LENGTH) + "…"
|
|
265
|
+
: record.result
|
|
266
|
+
: undefined,
|
|
258
267
|
resultPreview: record.result
|
|
259
268
|
? record.result.length > resultMaxLen
|
|
260
269
|
? record.result.slice(0, resultMaxLen) + "…"
|
|
@@ -271,7 +280,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
271
280
|
const d = message.details;
|
|
272
281
|
if (!d) return undefined;
|
|
273
282
|
|
|
274
|
-
function renderOne(d: NotificationDetails): string {
|
|
283
|
+
function renderOne(d: NotificationDetails): string | Container {
|
|
275
284
|
const isError = d.status === "error" || d.status === "stopped" || d.status === "aborted";
|
|
276
285
|
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
277
286
|
const statusText = isError ? d.status
|
|
@@ -291,25 +300,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
291
300
|
line += "\n " + parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
292
301
|
}
|
|
293
302
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const lines = d.resultPreview.split("\n").slice(0, 30);
|
|
297
|
-
for (const l of lines) line += "\n" + theme.fg("dim", ` ${l}`);
|
|
298
|
-
} else {
|
|
303
|
+
if (!expanded) {
|
|
304
|
+
// Collapsed previews intentionally remain compact plain text.
|
|
299
305
|
const preview = d.resultPreview.split("\n")[0]?.slice(0, 80) ?? "";
|
|
300
306
|
line += "\n " + theme.fg("dim", `⎿ ${preview}`);
|
|
307
|
+
if (d.outputFile) {
|
|
308
|
+
line += "\n " + theme.fg("muted", `transcript: ${d.outputFile}`);
|
|
309
|
+
}
|
|
310
|
+
return line;
|
|
301
311
|
}
|
|
302
312
|
|
|
303
|
-
|
|
313
|
+
const container = new Container();
|
|
314
|
+
container.addChild(new Text(line, 0, 0));
|
|
315
|
+
container.addChild(createMarkdownResult(d.resultText ?? d.resultPreview, theme, 30));
|
|
304
316
|
if (d.outputFile) {
|
|
305
|
-
|
|
317
|
+
container.addChild(new Text(theme.fg("muted", ` transcript: ${d.outputFile}`), 0, 0));
|
|
306
318
|
}
|
|
307
|
-
|
|
308
|
-
return line;
|
|
319
|
+
return container;
|
|
309
320
|
}
|
|
310
321
|
|
|
311
322
|
const all = [d, ...(d.others ?? [])];
|
|
312
|
-
return new Text(all.map(renderOne).join("\n"), 0, 0);
|
|
323
|
+
if (!expanded) return new Text(all.map(renderOne).join("\n"), 0, 0);
|
|
324
|
+
|
|
325
|
+
const container = new Container();
|
|
326
|
+
all.forEach((item, index) => {
|
|
327
|
+
if (index > 0) container.addChild(new Spacer(1));
|
|
328
|
+
container.addChild(renderOne(item) as Container);
|
|
329
|
+
});
|
|
330
|
+
return container;
|
|
313
331
|
}
|
|
314
332
|
);
|
|
315
333
|
|
|
@@ -553,6 +571,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
553
571
|
pi.on("session_start", async (_event, ctx) => {
|
|
554
572
|
currentCtx = ctx;
|
|
555
573
|
manager.clearCompleted(true);
|
|
574
|
+
const historicalRecords = ctx.sessionManager
|
|
575
|
+
.getBranch()
|
|
576
|
+
.filter((entry: any) => entry?.type === "custom" && entry.customType === "subagents:record")
|
|
577
|
+
.map((entry: any) => entry.data);
|
|
578
|
+
manager.restoreCompleted(historicalRecords);
|
|
556
579
|
// Guard mirrors the `!scheduler.isActive()` pattern below: session_start
|
|
557
580
|
// fires once per activation, but a double-bind must not leak listeners.
|
|
558
581
|
if (!rpcHandle) {
|
|
@@ -1024,15 +1047,15 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1024
1047
|
|
|
1025
1048
|
if (expanded) {
|
|
1026
1049
|
const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
1050
|
+
const container = new Container();
|
|
1051
|
+
container.addChild(new Text(line, 0, 0));
|
|
1027
1052
|
if (resultText) {
|
|
1028
|
-
|
|
1029
|
-
for (const l of lines) {
|
|
1030
|
-
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
1031
|
-
}
|
|
1053
|
+
container.addChild(createMarkdownResult(resultText, theme, 50));
|
|
1032
1054
|
if (resultText.split("\n").length > 50) {
|
|
1033
|
-
|
|
1055
|
+
container.addChild(new Text(theme.fg("muted", " ... (use get_subagent_result with verbose for full output)"), 0, 0));
|
|
1034
1056
|
}
|
|
1035
1057
|
}
|
|
1058
|
+
return container;
|
|
1036
1059
|
} else {
|
|
1037
1060
|
const doneText = isSteered ? "Wrapped up (turn limit)" : "Done";
|
|
1038
1061
|
line += "\n" + theme.fg("dim", ` ⎿ ${doneText}`);
|
package/src/types.ts
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Markdown, Text, type Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { Theme } from "./agent-widget.js";
|
|
4
|
+
|
|
5
|
+
const TRUNCATION_MARKER = "… (more output available)";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render a child result as Markdown while keeping notification output bounded.
|
|
9
|
+
*
|
|
10
|
+
* Markdown is rendered at paint time so the TUI's current width controls wrapping.
|
|
11
|
+
* If the host's Markdown implementation is unavailable or throws, the original
|
|
12
|
+
* text is rendered through Text instead of making the result disappear.
|
|
13
|
+
*/
|
|
14
|
+
class CappedMarkdownResult implements Component {
|
|
15
|
+
private readonly text: string;
|
|
16
|
+
private readonly theme: Theme;
|
|
17
|
+
private readonly maxLines: number;
|
|
18
|
+
private markdown: Component | undefined;
|
|
19
|
+
|
|
20
|
+
constructor(text: string, theme: Theme, maxLines: number) {
|
|
21
|
+
this.text = text;
|
|
22
|
+
this.theme = theme;
|
|
23
|
+
this.maxLines = Math.max(0, Math.floor(maxLines));
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
this.markdown = new Markdown(text, 0, 0, getMarkdownTheme());
|
|
27
|
+
} catch {
|
|
28
|
+
// A mismatched host Pi version should still leave the child output visible.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
invalidate(): void {
|
|
33
|
+
this.markdown?.invalidate();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render(width: number): string[] {
|
|
37
|
+
let lines: string[];
|
|
38
|
+
try {
|
|
39
|
+
lines = this.markdown?.render(width) ?? new Text(this.text, 0, 0).render(width);
|
|
40
|
+
} catch {
|
|
41
|
+
lines = new Text(this.text, 0, 0).render(width);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (lines.length <= this.maxLines) return lines;
|
|
45
|
+
if (this.maxLines === 0) return [];
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
...lines.slice(0, this.maxLines - 1),
|
|
49
|
+
this.theme.fg("muted", TRUNCATION_MARKER),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createMarkdownResult(text: string, theme: Theme, maxLines: number): Component {
|
|
55
|
+
return new CappedMarkdownResult(text, theme, maxLines);
|
|
56
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { AgentManager } from "../src/agent-manager.js";
|
|
3
|
+
|
|
4
|
+
type PersistedRecord = {
|
|
5
|
+
id: string;
|
|
6
|
+
type: string;
|
|
7
|
+
description: string;
|
|
8
|
+
status: string;
|
|
9
|
+
result?: unknown;
|
|
10
|
+
error?: unknown;
|
|
11
|
+
startedAt: number;
|
|
12
|
+
completedAt: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function persisted(overrides: Partial<PersistedRecord> = {}): PersistedRecord {
|
|
16
|
+
return {
|
|
17
|
+
id: "agent-1",
|
|
18
|
+
type: "general-purpose",
|
|
19
|
+
description: "history test",
|
|
20
|
+
status: "completed",
|
|
21
|
+
result: "restored result",
|
|
22
|
+
startedAt: 100,
|
|
23
|
+
completedAt: 200,
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("AgentManager.restoreCompleted", () => {
|
|
29
|
+
let manager: AgentManager | undefined;
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
manager?.dispose();
|
|
33
|
+
manager = undefined;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("restores terminal records without creating live runtime state", () => {
|
|
37
|
+
manager = new AgentManager();
|
|
38
|
+
|
|
39
|
+
manager.restoreCompleted([
|
|
40
|
+
persisted(),
|
|
41
|
+
persisted({ id: "agent-error", status: "error", error: "failed" }),
|
|
42
|
+
persisted({ id: "agent-running", status: "running" }),
|
|
43
|
+
persisted({ id: "agent-queued", status: "queued" }),
|
|
44
|
+
persisted({ id: "agent-invalid", completedAt: Number.NaN }),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
expect(manager.listAgents().map((record) => record.id).sort()).toEqual([
|
|
48
|
+
"agent-1",
|
|
49
|
+
"agent-error",
|
|
50
|
+
]);
|
|
51
|
+
const restored = manager.getRecord("agent-1");
|
|
52
|
+
expect(restored).toMatchObject({
|
|
53
|
+
status: "completed",
|
|
54
|
+
result: "restored result",
|
|
55
|
+
toolUses: 0,
|
|
56
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
57
|
+
compactionCount: 0,
|
|
58
|
+
isBackground: true,
|
|
59
|
+
});
|
|
60
|
+
expect(restored?.session).toBeUndefined();
|
|
61
|
+
expect(restored?.promise).toBeUndefined();
|
|
62
|
+
expect(restored?.abortController).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("uses the newest branch entry when an id appears more than once", () => {
|
|
66
|
+
manager = new AgentManager();
|
|
67
|
+
|
|
68
|
+
manager.restoreCompleted([
|
|
69
|
+
persisted({ result: "old", completedAt: 200 }),
|
|
70
|
+
persisted({ result: "new", completedAt: 300 }),
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
expect(manager.getRecord("agent-1")?.result).toBe("new");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not overwrite a record already held by the manager", () => {
|
|
77
|
+
manager = new AgentManager();
|
|
78
|
+
|
|
79
|
+
manager.restoreCompleted([persisted({ result: "first" })]);
|
|
80
|
+
manager.restoreCompleted([persisted({ result: "second" })]);
|
|
81
|
+
|
|
82
|
+
expect(manager.getRecord("agent-1")?.result).toBe("first");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMarkdownResult } from "../../src/ui/markdown-result.js";
|
|
3
|
+
|
|
4
|
+
const theme = {
|
|
5
|
+
fg: (_name: string, text: string) => text,
|
|
6
|
+
bold: (text: string) => text,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
describe("createMarkdownResult", () => {
|
|
10
|
+
it("renders headings, lists, inline code, and fenced code", () => {
|
|
11
|
+
const component = createMarkdownResult(
|
|
12
|
+
[
|
|
13
|
+
"# Result",
|
|
14
|
+
"",
|
|
15
|
+
"- first",
|
|
16
|
+
"- `inline()`",
|
|
17
|
+
"",
|
|
18
|
+
"```ts",
|
|
19
|
+
"const answer: number = 42;",
|
|
20
|
+
"```",
|
|
21
|
+
].join("\n"),
|
|
22
|
+
theme,
|
|
23
|
+
50,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const rendered = component.render(100).join("\n");
|
|
27
|
+
expect(rendered).toContain("Result");
|
|
28
|
+
expect(rendered).toContain("first");
|
|
29
|
+
expect(rendered).toContain("inline()");
|
|
30
|
+
expect(rendered).toContain("answer");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("caps rendered lines and adds a truncation marker", () => {
|
|
34
|
+
const component = createMarkdownResult("line\n".repeat(20), theme, 5);
|
|
35
|
+
const rendered = component.render(80);
|
|
36
|
+
|
|
37
|
+
expect(rendered).toHaveLength(5);
|
|
38
|
+
expect(rendered.at(-1)).toContain("more");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("keeps unlabelled code blocks safe without language guessing", () => {
|
|
42
|
+
const component = createMarkdownResult("```\nplain text\n```", theme, 20);
|
|
43
|
+
expect(component.render(80).join("\n")).toContain("plain text");
|
|
44
|
+
});
|
|
45
|
+
});
|