@narumitw/pi-subagents 3.2.1 → 3.3.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 +46 -4
- package/dist/chunks/chunk-FY5HIU6Y.ts +42 -0
- package/dist/chunks/chunk-FY5HIU6Y.ts.map +7 -0
- package/dist/chunks/menu-KAIUEWL7.ts +171 -0
- package/dist/chunks/menu-KAIUEWL7.ts.map +7 -0
- package/dist/chunks/settings-ui-PSEYB6JX.ts +196 -0
- package/dist/chunks/settings-ui-PSEYB6JX.ts.map +7 -0
- package/dist/index.ts +338 -108
- package/dist/index.ts.map +4 -4
- package/package.json +6 -5
- package/src/display-text.ts +14 -0
- package/src/menu.ts +188 -0
- package/src/message-renderers.ts +60 -0
- package/src/runtime.ts +1 -1
- package/src/settings-ui.ts +216 -0
- package/src/settings.ts +142 -0
- package/src/subagents.ts +139 -11
- package/src/tools.ts +2 -2
- package/src/widget.ts +59 -39
- package/src/completion-renderer.ts +0 -58
package/src/subagents.ts
CHANGED
|
@@ -1,30 +1,158 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { sanitizeSubagentDisplayText } from "./display-text.js";
|
|
3
|
+
import { registerMessageRenderers } from "./message-renderers.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_SUBAGENT_SETTINGS,
|
|
6
|
+
type SubagentSettings,
|
|
7
|
+
SubagentSettingsStore,
|
|
8
|
+
subagentSettingsPath,
|
|
9
|
+
} from "./settings.js";
|
|
3
10
|
import { registerSubagentTools, type SubagentToolsDependencies } from "./tools.js";
|
|
4
11
|
import { createSubagentWidgetController } from "./widget.js";
|
|
5
12
|
|
|
6
|
-
export
|
|
13
|
+
export interface SubagentsDependencies extends SubagentToolsDependencies {
|
|
14
|
+
settingsStore?: Pick<SubagentSettingsStore, "userPath" | "load" | "save" | "flush">;
|
|
15
|
+
}
|
|
7
16
|
|
|
8
17
|
export default function subagents(pi: ExtensionAPI, dependencies: SubagentsDependencies = {}): void {
|
|
9
|
-
|
|
18
|
+
registerMessageRenderers(pi);
|
|
10
19
|
const tools = registerSubagentTools(pi, dependencies);
|
|
11
20
|
const widget = createSubagentWidgetController(tools.runtime);
|
|
12
|
-
|
|
21
|
+
const store = dependencies.settingsStore ?? new SubagentSettingsStore(subagentSettingsPath());
|
|
22
|
+
let activeContext: ExtensionContext | undefined;
|
|
13
23
|
let sessionGeneration = 0;
|
|
24
|
+
let owner = new AbortController();
|
|
25
|
+
let ready = false;
|
|
26
|
+
let menuOpen = false;
|
|
27
|
+
let settings: SubagentSettings = { ...DEFAULT_SUBAGENT_SETTINGS };
|
|
28
|
+
const menuTasks = new Set<Promise<void>>();
|
|
29
|
+
const ownsSession = (ctx: ExtensionContext) => ctx.sessionManager === activeContext?.sessionManager;
|
|
30
|
+
|
|
31
|
+
const openSettings = async (
|
|
32
|
+
ctx: ExtensionCommandContext,
|
|
33
|
+
signal: AbortSignal,
|
|
34
|
+
isCurrent: () => boolean,
|
|
35
|
+
): Promise<"back" | "close" | "stale"> => {
|
|
36
|
+
if (signal.aborted || !isCurrent()) return "stale";
|
|
37
|
+
if (ctx.mode !== "tui") {
|
|
38
|
+
ctx.ui.notify(sanitizeSubagentDisplayText(`Edit user settings: ${store.userPath}. Then run /reload.`), "info");
|
|
39
|
+
return "back";
|
|
40
|
+
}
|
|
41
|
+
const { showSubagentSettings } = await import("./settings-ui.js");
|
|
42
|
+
if (signal.aborted || !isCurrent()) return "stale";
|
|
43
|
+
return showSubagentSettings(ctx, {
|
|
44
|
+
signal,
|
|
45
|
+
isCurrent,
|
|
46
|
+
get: () => ({ ...settings }),
|
|
47
|
+
save: (key, value, actionSignal) => {
|
|
48
|
+
actionSignal.throwIfAborted();
|
|
49
|
+
if (!isCurrent()) throw new Error("The subagents session changed.");
|
|
50
|
+
return store.save(key, value, actionSignal);
|
|
51
|
+
},
|
|
52
|
+
apply: (next) => {
|
|
53
|
+
if (!isCurrent()) return;
|
|
54
|
+
settings = { ...next };
|
|
55
|
+
widget.update(ctx, settings);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
pi.registerCommand("subagents", {
|
|
61
|
+
description: "Manage subagent jobs and widget settings",
|
|
62
|
+
getArgumentCompletions: (prefix) =>
|
|
63
|
+
"settings".startsWith(prefix)
|
|
64
|
+
? [{ value: "settings", label: "settings", description: "Configure the subagent widget" }]
|
|
65
|
+
: null,
|
|
66
|
+
handler: async (args, ctx) => {
|
|
67
|
+
if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) {
|
|
68
|
+
throw new Error("/subagents requires TUI or RPC mode.");
|
|
69
|
+
}
|
|
70
|
+
const route = args.trim();
|
|
71
|
+
if (route !== "" && route !== "settings") {
|
|
72
|
+
ctx.ui.notify("Usage: /subagents | /subagents settings", "warning");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!ownsSession(ctx) || !ready || menuOpen) {
|
|
76
|
+
ctx.ui.notify("Subagent controls are loading, unavailable, or already open.", "warning");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
menuOpen = true;
|
|
80
|
+
const generation = sessionGeneration;
|
|
81
|
+
const signal = owner.signal;
|
|
82
|
+
const isCurrent = () => generation === sessionGeneration && ownsSession(ctx) && !signal.aborted;
|
|
83
|
+
const task = (async () => {
|
|
84
|
+
if (route === "settings") {
|
|
85
|
+
await openSettings(ctx, signal, isCurrent);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const { showSubagentsMenu } = await import("./menu.js");
|
|
89
|
+
if (!isCurrent()) return;
|
|
90
|
+
await showSubagentsMenu(ctx, {
|
|
91
|
+
signal,
|
|
92
|
+
isCurrent,
|
|
93
|
+
getState: () => ({
|
|
94
|
+
settings: { ...settings },
|
|
95
|
+
settingsPath: store.userPath,
|
|
96
|
+
...tools.runtime.inspectJobs(),
|
|
97
|
+
}),
|
|
98
|
+
cancelJob: async (jobId, actionSignal) => {
|
|
99
|
+
actionSignal.throwIfAborted();
|
|
100
|
+
if (!isCurrent()) throw new Error("The subagents session changed.");
|
|
101
|
+
await tools.runtime.cancel(jobId);
|
|
102
|
+
},
|
|
103
|
+
showSettings: (actionSignal) => openSettings(ctx, actionSignal, isCurrent),
|
|
104
|
+
});
|
|
105
|
+
})();
|
|
106
|
+
menuTasks.add(task);
|
|
107
|
+
try {
|
|
108
|
+
await task;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (isCurrent()) {
|
|
111
|
+
ctx.ui.notify(sanitizeSubagentDisplayText(error instanceof Error ? error.message : String(error)), "error");
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
menuTasks.delete(task);
|
|
115
|
+
if (isCurrent()) menuOpen = false;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
});
|
|
14
119
|
|
|
15
120
|
pi.on("session_start", async (_event, ctx) => {
|
|
16
|
-
|
|
121
|
+
owner.abort();
|
|
122
|
+
if (activeContext) widget.shutdown(activeContext);
|
|
123
|
+
owner = new AbortController();
|
|
124
|
+
activeContext = ctx;
|
|
125
|
+
ready = false;
|
|
126
|
+
menuOpen = false;
|
|
17
127
|
const generation = ++sessionGeneration;
|
|
128
|
+
const isCurrent = () => generation === sessionGeneration && ownsSession(ctx) && !owner.signal.aborted;
|
|
129
|
+
// Stop old children and broker delivery before waiting for slow UI storage work.
|
|
18
130
|
await tools.startSession();
|
|
19
|
-
if (
|
|
20
|
-
|
|
131
|
+
if (!isCurrent()) return;
|
|
132
|
+
await Promise.allSettled([...menuTasks]);
|
|
133
|
+
if (!isCurrent()) return;
|
|
134
|
+
const loaded = await store.load();
|
|
135
|
+
if (!isCurrent()) return;
|
|
136
|
+
settings = { ...loaded.settings };
|
|
137
|
+
ready = true;
|
|
138
|
+
widget.start(ctx, settings);
|
|
139
|
+
if (loaded.diagnostics.length > 0) {
|
|
140
|
+
const message = sanitizeSubagentDisplayText(`Subagent settings: ${loaded.diagnostics.join(" ")}`);
|
|
141
|
+
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
142
|
+
else throw new Error(message);
|
|
143
|
+
}
|
|
21
144
|
});
|
|
22
145
|
|
|
23
146
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
24
|
-
if (ctx
|
|
147
|
+
if (!ownsSession(ctx)) return;
|
|
25
148
|
sessionGeneration++;
|
|
26
|
-
|
|
149
|
+
owner.abort();
|
|
150
|
+
ready = false;
|
|
151
|
+
menuOpen = false;
|
|
152
|
+
activeContext = undefined;
|
|
27
153
|
widget.shutdown(ctx);
|
|
28
|
-
await tools.shutdown();
|
|
154
|
+
const results = await Promise.allSettled([tools.shutdown(), ...menuTasks, store.flush()]);
|
|
155
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
156
|
+
if (failed?.status === "rejected") throw failed.reason;
|
|
29
157
|
});
|
|
30
158
|
}
|
package/src/tools.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
sanitizeTerminalText,
|
|
12
12
|
validateMessage,
|
|
13
13
|
} from "./message-broker.js";
|
|
14
|
+
import { BROKER_MESSAGE_TYPE } from "./message-renderers.js";
|
|
14
15
|
import { modelVisibleJson, requireBoundedModelText } from "./model-output.js";
|
|
15
16
|
import { assertChildCommandCapacity } from "./process.js";
|
|
16
17
|
import {
|
|
@@ -32,7 +33,6 @@ import {
|
|
|
32
33
|
|
|
33
34
|
const MAX_TASK_BYTES = 50 * 1024;
|
|
34
35
|
const MAX_TOOLS = MAX_SELECTED_TOOLS;
|
|
35
|
-
const MESSAGE_TYPE = "pi-subagents-message";
|
|
36
36
|
const CHILD_CORE_TOOL_SET = new Set<string>(CHILD_CORE_TOOL_NAMES);
|
|
37
37
|
const THINKING_LEVEL_SET = new Set<string>(SUBAGENT_THINKING_LEVELS);
|
|
38
38
|
|
|
@@ -347,7 +347,7 @@ function deliverMessage(pi: ExtensionAPI, message: BrokerInboundMessage): void {
|
|
|
347
347
|
);
|
|
348
348
|
pi.sendMessage(
|
|
349
349
|
{
|
|
350
|
-
customType:
|
|
350
|
+
customType: BROKER_MESSAGE_TYPE,
|
|
351
351
|
content,
|
|
352
352
|
display: true,
|
|
353
353
|
details: {
|
package/src/widget.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import { EditorStatusWidget } from "@narumitw/pi-tui-kit/editor-status-widget";
|
|
4
|
+
import { sanitizeSubagentDisplayText as sanitizeLabel } from "./display-text.js";
|
|
4
5
|
import type { ActiveJobDisplay, SubagentRuntime } from "./runtime.js";
|
|
6
|
+
import { DEFAULT_SUBAGENT_SETTINGS, type SubagentSettings } from "./settings.js";
|
|
5
7
|
|
|
6
8
|
export const SUBAGENT_WIDGET_KEY = "subagents";
|
|
7
9
|
export const SUBAGENT_WIDGET_REFRESH_INTERVAL_MS = 1_000;
|
|
@@ -10,7 +12,8 @@ const WIDGET_OPTIONS = { placement: "aboveEditor" } as const;
|
|
|
10
12
|
const TASK_PREVIEW_WIDTH = 40;
|
|
11
13
|
|
|
12
14
|
export interface SubagentWidgetController {
|
|
13
|
-
start(ctx: ExtensionContext): void;
|
|
15
|
+
start(ctx: ExtensionContext, settings?: SubagentSettings): void;
|
|
16
|
+
update(ctx: ExtensionContext, settings: SubagentSettings): void;
|
|
14
17
|
shutdown(ctx: ExtensionContext): void;
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -19,6 +22,8 @@ export function createSubagentWidgetController(runtime: SubagentRuntime): Subage
|
|
|
19
22
|
let refreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
20
23
|
let unsubscribeJobs: (() => void) | undefined;
|
|
21
24
|
let publishedValue: string | undefined;
|
|
25
|
+
let settings = { ...DEFAULT_SUBAGENT_SETTINGS };
|
|
26
|
+
let clearWidget: (() => void) | undefined;
|
|
22
27
|
|
|
23
28
|
const ownsSession = (ctx: ExtensionContext): boolean => ctx.sessionManager === activeSession;
|
|
24
29
|
|
|
@@ -31,19 +36,17 @@ export function createSubagentWidgetController(runtime: SubagentRuntime): Subage
|
|
|
31
36
|
|
|
32
37
|
const publish = (ctx: ExtensionContext): void => {
|
|
33
38
|
if (ctx.mode !== "tui" || !ownsSession(ctx)) return;
|
|
34
|
-
const jobs = runtime.activeJobsForDisplay();
|
|
35
|
-
const value = widgetValue(jobs)
|
|
39
|
+
const jobs = settings.showWidget ? runtime.activeJobsForDisplay() : [];
|
|
40
|
+
const value = `${JSON.stringify(settings)}\n${widgetValue(jobs)}`;
|
|
36
41
|
if (value === publishedValue) return;
|
|
37
42
|
if (jobs.length === 0) {
|
|
38
43
|
ctx.ui.setWidget(SUBAGENT_WIDGET_KEY, undefined);
|
|
39
44
|
} else {
|
|
40
45
|
const snapshot = jobs.map(cloneDisplayJob);
|
|
46
|
+
const preferences = { ...settings };
|
|
41
47
|
ctx.ui.setWidget(
|
|
42
48
|
SUBAGENT_WIDGET_KEY,
|
|
43
|
-
(_tui, theme) => (
|
|
44
|
-
render: (width) => renderSubagentWidget(snapshot, theme, width),
|
|
45
|
-
invalidate: () => {},
|
|
46
|
-
}),
|
|
49
|
+
(_tui, theme) => createSubagentWidget(snapshot, theme, preferences),
|
|
47
50
|
WIDGET_OPTIONS,
|
|
48
51
|
);
|
|
49
52
|
}
|
|
@@ -51,50 +54,82 @@ export function createSubagentWidgetController(runtime: SubagentRuntime): Subage
|
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
return {
|
|
54
|
-
start(ctx) {
|
|
57
|
+
start(ctx, preferences = DEFAULT_SUBAGENT_SETTINGS) {
|
|
55
58
|
stop();
|
|
59
|
+
clearWidget?.();
|
|
60
|
+
clearWidget = undefined;
|
|
61
|
+
settings = { ...preferences };
|
|
56
62
|
activeSession = ctx.sessionManager;
|
|
57
63
|
publishedValue = undefined;
|
|
58
64
|
if (ctx.mode !== "tui") return;
|
|
59
|
-
ctx.ui.setWidget(SUBAGENT_WIDGET_KEY, undefined);
|
|
65
|
+
clearWidget = () => ctx.ui.setWidget(SUBAGENT_WIDGET_KEY, undefined);
|
|
66
|
+
clearWidget();
|
|
60
67
|
unsubscribeJobs = runtime.subscribeJobs(() => publish(ctx));
|
|
61
68
|
refreshTimer = setInterval(() => publish(ctx), SUBAGENT_WIDGET_REFRESH_INTERVAL_MS);
|
|
62
69
|
refreshTimer.unref();
|
|
63
70
|
publish(ctx);
|
|
64
71
|
},
|
|
72
|
+
update(ctx, preferences) {
|
|
73
|
+
if (!ownsSession(ctx)) return;
|
|
74
|
+
settings = { ...preferences };
|
|
75
|
+
publish(ctx);
|
|
76
|
+
},
|
|
65
77
|
shutdown(ctx) {
|
|
66
78
|
if (!ownsSession(ctx)) return;
|
|
67
79
|
stop();
|
|
68
|
-
|
|
80
|
+
clearWidget?.();
|
|
81
|
+
clearWidget = undefined;
|
|
69
82
|
publishedValue = undefined;
|
|
70
83
|
activeSession = undefined;
|
|
71
84
|
},
|
|
72
85
|
};
|
|
73
86
|
}
|
|
74
87
|
|
|
75
|
-
export function renderSubagentWidget(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
];
|
|
82
|
-
return
|
|
88
|
+
export function renderSubagentWidget(
|
|
89
|
+
jobs: readonly ActiveJobDisplay[],
|
|
90
|
+
theme: Theme,
|
|
91
|
+
width: number,
|
|
92
|
+
settings: SubagentSettings = DEFAULT_SUBAGENT_SETTINGS,
|
|
93
|
+
): string[] {
|
|
94
|
+
if (!settings.showWidget) return [];
|
|
95
|
+
return createSubagentWidget(jobs, theme, settings).render(width);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createSubagentWidget(
|
|
99
|
+
jobs: readonly ActiveJobDisplay[],
|
|
100
|
+
theme: Theme,
|
|
101
|
+
settings: SubagentSettings,
|
|
102
|
+
): EditorStatusWidget {
|
|
103
|
+
return new EditorStatusWidget({
|
|
104
|
+
theme,
|
|
105
|
+
renderBody: () => [
|
|
106
|
+
theme.fg("muted", `Subagents · ${jobs.length} active`),
|
|
107
|
+
...jobs.map((job) => renderJob(job, theme, settings)),
|
|
108
|
+
],
|
|
109
|
+
});
|
|
83
110
|
}
|
|
84
111
|
|
|
85
|
-
function renderJob(job: ActiveJobDisplay, theme: Theme): string {
|
|
112
|
+
function renderJob(job: ActiveJobDisplay, theme: Theme, settings: SubagentSettings): string {
|
|
86
113
|
const running = job.state === "running";
|
|
87
114
|
const symbol = theme.fg(running ? "accent" : "dim", running ? "▶ " : "○ ");
|
|
88
115
|
const state = theme.fg(running ? "accent" : "muted", job.state);
|
|
89
116
|
const jobId = sanitizeLabel(job.jobId);
|
|
90
|
-
const taskText =
|
|
117
|
+
const taskText = sanitizeLabel(job.task);
|
|
91
118
|
// Pi's truncation adds ANSI resets; remove them before applying the callback theme.
|
|
92
119
|
const task = stripTerminalSequences(truncateToWidth(taskText, TASK_PREVIEW_WIDTH, "…"));
|
|
93
|
-
const
|
|
120
|
+
const labels = [
|
|
121
|
+
...(settings.showJobId ? [theme.fg("text", jobId)] : []),
|
|
122
|
+
...(visibleWidth(task) > 0 ? [theme.fg("text", task)] : []),
|
|
123
|
+
state,
|
|
124
|
+
];
|
|
94
125
|
const tools = job.tools.length > 0 ? job.tools.map(sanitizeLabel).join(", ") : "none";
|
|
95
126
|
const timeout = job.timeout === undefined ? "no timeout" : formatSeconds(job.timeout);
|
|
96
|
-
const
|
|
97
|
-
|
|
127
|
+
const details = [
|
|
128
|
+
...(settings.showTiming ? [`${formatSeconds(Math.floor(job.elapsedMs / 1_000))} / ${timeout}`] : []),
|
|
129
|
+
...(settings.showTools ? [`tools: ${tools}`] : []),
|
|
130
|
+
];
|
|
131
|
+
const detail = details.length > 0 ? theme.fg("muted", ` · ${details.join(" · ")}`) : "";
|
|
132
|
+
return `${symbol}${labels.join(" · ")}${detail}`;
|
|
98
133
|
}
|
|
99
134
|
|
|
100
135
|
function widgetValue(jobs: readonly ActiveJobDisplay[]): string {
|
|
@@ -110,21 +145,6 @@ function cloneDisplayJob(job: ActiveJobDisplay): ActiveJobDisplay {
|
|
|
110
145
|
return { ...job, tools: [...job.tools] };
|
|
111
146
|
}
|
|
112
147
|
|
|
113
|
-
function sanitizeTask(value: string): string {
|
|
114
|
-
// Stop sequence bodies at another control introducer so malformed input cannot
|
|
115
|
-
// repeatedly scan the rest of a maximum-size task, as Pi's escape stripper can.
|
|
116
|
-
const text = value.replace(
|
|
117
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: Remove untrusted terminal escape sequences before display.
|
|
118
|
-
/(?:\u001b\[|\u009b)[0-?]*[ -/]*[@-~]|(?:\u001b[\]PX^_]|[\u0090\u0098\u009d-\u009f])[^\u0007\u001b\u0080-\u009f]*(?:\u0007|\u001b\\|\u009c)|\u001b[ -/]*[0-~]/gu,
|
|
119
|
-
"",
|
|
120
|
-
);
|
|
121
|
-
return sanitizeLabel(text.replace(/\p{Bidi_Control}/gu, ""));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function sanitizeLabel(value: string): string {
|
|
125
|
-
return sanitizeTerminalText(value).replace(/\s+/gu, " ").trim();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
148
|
function formatSeconds(value: number): string {
|
|
129
149
|
if (value < 60) return `${formatNumber(value)}s`;
|
|
130
150
|
const wholeSeconds = Math.floor(value);
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { type ExtensionAPI, getMarkdownTheme, keyText } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Box, type Component, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
|
-
import { sanitizeTerminalText } from "./message-broker.js";
|
|
4
|
-
|
|
5
|
-
export const COMPLETION_MESSAGE_TYPE = "pi-subagents-completion";
|
|
6
|
-
|
|
7
|
-
function createCompletionBox(
|
|
8
|
-
children: Component[],
|
|
9
|
-
outputPad: number,
|
|
10
|
-
background: (text: string) => string,
|
|
11
|
-
): Component {
|
|
12
|
-
return {
|
|
13
|
-
render(width) {
|
|
14
|
-
if (width <= 0) return [];
|
|
15
|
-
const maxPadding = Math.floor((width - 1) / 2);
|
|
16
|
-
const box = new Box(Math.min(Math.max(0, outputPad), maxPadding), 1, background);
|
|
17
|
-
for (const child of children) box.addChild(child);
|
|
18
|
-
return box.render(width);
|
|
19
|
-
},
|
|
20
|
-
invalidate() {
|
|
21
|
-
for (const child of children) child.invalidate();
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function registerCompletionRenderer(pi: ExtensionAPI): void {
|
|
27
|
-
pi.registerMessageRenderer(COMPLETION_MESSAGE_TYPE, (message, options, theme) => {
|
|
28
|
-
const children: Component[] = [
|
|
29
|
-
new Text(theme.fg("customMessageLabel", theme.bold(`[${COMPLETION_MESSAGE_TYPE}]`)), 0, 0),
|
|
30
|
-
new Spacer(1),
|
|
31
|
-
];
|
|
32
|
-
if (!options.expanded) {
|
|
33
|
-
children.push(
|
|
34
|
-
new Text(
|
|
35
|
-
theme.fg("customMessageText", "Subagent job completion (") +
|
|
36
|
-
theme.fg("dim", keyText("app.tools.expand")) +
|
|
37
|
-
theme.fg("customMessageText", " to expand)"),
|
|
38
|
-
0,
|
|
39
|
-
0,
|
|
40
|
-
),
|
|
41
|
-
);
|
|
42
|
-
} else {
|
|
43
|
-
const content =
|
|
44
|
-
typeof message.content === "string"
|
|
45
|
-
? message.content
|
|
46
|
-
: message.content
|
|
47
|
-
.filter((part) => part.type === "text")
|
|
48
|
-
.map((part) => part.text)
|
|
49
|
-
.join("\n");
|
|
50
|
-
children.push(
|
|
51
|
-
new Markdown(sanitizeTerminalText(content), 0, 0, getMarkdownTheme(), {
|
|
52
|
-
color: (text) => theme.fg("customMessageText", text),
|
|
53
|
-
}),
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
return createCompletionBox(children, options.outputPad, (text) => theme.bg("customMessageBg", text));
|
|
57
|
-
});
|
|
58
|
-
}
|