@narumitw/pi-subagents 3.4.2 โ 3.4.4
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 +16 -0
- package/README.md +19 -13
- package/dist/index.ts +350 -39
- package/dist/index.ts.map +4 -4
- package/docs/conversation-display.md +10 -0
- package/package.json +4 -4
- package/src/conversation-view.ts +169 -0
- package/src/message-renderers.ts +90 -40
- package/src/tool-renderers.ts +156 -0
- package/src/tools.ts +6 -0
- package/src/widget-renderer.ts +33 -2
- package/src/widget.ts +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @narumitw/pi-subagents
|
|
2
2
|
|
|
3
|
+
## 3.4.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 49c68d4: Show readable tool summaries and job notifications in the conversation. Keep job identity, state, and message direction visible when collapsed, and render expanded subagent results as Markdown without changing model data or widget behavior.
|
|
8
|
+
|
|
9
|
+
## 3.4.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- cf7b63d: Put editor widget names and short current count/state summaries in their divider lines. Remove duplicate body titles while retaining safety text, detail rows, privacy modes, and standalone RPC/tool output. In fullscreen Pi, click anywhere in a widget to collapse it to one named line, then click that line to restore the latest content. Preserve independent, session-local disclosure through updates without taking editor focus, changing domain data, or stopping background work. Existing owner controls provide keyboard restoration; regular-mode mouse input remains terminal-owned.
|
|
14
|
+
|
|
15
|
+
The Kit minor release supplies the named widget state and publisher disclosure APIs. Changesets raises all adopting consumers' Kit dependency floors in the same Version PR. Publish and verify the exact Kit artifact before its consumers.
|
|
16
|
+
- Updated dependencies [cf7b63d]
|
|
17
|
+
- @narumitw/pi-tui-kit@0.70.0
|
|
18
|
+
|
|
3
19
|
## 3.4.2
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -22,18 +22,19 @@ Pi Subagents runs Pi jobs in separate child processes and supports authenticated
|
|
|
22
22
|
|
|
23
23
|
## ๐ฆ Install
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
The npm package still contains the legacy 2.x runtime and does not provide the tools below.
|
|
27
|
-
|
|
28
|
-
Install the repository source as one Pi package:
|
|
25
|
+
Install from npm:
|
|
29
26
|
|
|
30
27
|
```bash
|
|
31
|
-
pi install
|
|
28
|
+
pi install npm:@narumitw/pi-subagents
|
|
32
29
|
```
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
Try it for one invocation:
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
```bash
|
|
34
|
+
pi -e npm:@narumitw/pi-subagents
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
To install from a local checkout, clone the repository, install dependencies, build its generated runtime, and install its package directory:
|
|
37
38
|
|
|
38
39
|
```bash
|
|
39
40
|
git clone https://github.com/narumiruna/pi-extensions.git
|
|
@@ -72,10 +73,8 @@ Use messaging only when needed:
|
|
|
72
73
|
- If `subagent_wait` returns `reason: "subagent_message"`, handle the visible request or response and wait for the job again only when needed.
|
|
73
74
|
- Answer a child-originated request by calling `subagent_send` with its `requestId`.
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
The hint is omitted when the action is unbound or an older host does not supply renderer key formatting.
|
|
78
|
-
Expanded messages show the full content; this display state does not change the content sent to the model.
|
|
76
|
+
Expand tool results and notifications with Pi's `app.tools.expand` binding (`Ctrl+O` by default).
|
|
77
|
+
See [Conversation presentation](./docs/conversation-display.md) for tool summaries, notification details, and expansion behavior.
|
|
79
78
|
|
|
80
79
|
## ๐ฌ Commands
|
|
81
80
|
|
|
@@ -94,7 +93,7 @@ The widget is TUI-only and contains only queued or running jobs. Each row starts
|
|
|
94
93
|
For example, with a main-agent thinking level of `high` at spawn:
|
|
95
94
|
|
|
96
95
|
```text
|
|
97
|
-
Subagents ยท
|
|
96
|
+
โ โพ Subagents ยท 1 running ยท 1 queued โโโโโโโ
|
|
98
97
|
โถ Review widget tests ยท 12s
|
|
99
98
|
โ Audit settings ยท thinking: medium ยท queued
|
|
100
99
|
```
|
|
@@ -107,9 +106,13 @@ The task preview removes terminal controls, collapses whitespace, and uses at mo
|
|
|
107
106
|
Task text is visible in this local widget, but `subagent_inspect` continues to omit it. Optional tool names include selected core and extension tools, not fixed communication tools.
|
|
108
107
|
The widget omits attachment paths and resource totals, disappears when no jobs remain active, and clears when the session ends.
|
|
109
108
|
|
|
109
|
+
The separator shows **Subagents** and current running and queued counts, including while collapsed. In fullscreen TUI mode, click anywhere in the widget, including blank columns, to collapse it to that separator. Click the separator to restore the latest jobs without moving keyboard focus.
|
|
110
|
+
Disclosure survives job updates, temporary absence, and host remounts. Select a visible view in `/subagents` to restore disclosure with the keyboard. Session start and tree navigation reset it.
|
|
111
|
+
Collapse does not change jobs, settings, item views, pins, or automatic rules.
|
|
112
|
+
|
|
110
113
|
Ordinary updates refresh the mounted widget without changing its position among other widgets. Hiding and showing it again can change that position.
|
|
111
114
|
The widget uses at most ten rows, including one separator, and reserves eight terminal rows for the editor and footer. This is a local height limit, not a calculation of space left by other widgets.
|
|
112
|
-
With
|
|
115
|
+
With no available rows it renders nothing. One available row shows only the separator with current counts and omitted-row information. Summary view also uses only the separator, but does not count its intentionally absent job rows as omitted. Narrow layouts clip each line to the terminal width.
|
|
113
116
|
|
|
114
117
|
In `/subagents`, **Widget view** provides details, summary, hide/show, pin/unpin, automatic compaction, and item-view reset controls.
|
|
115
118
|
These controls are session-local and do not write settings. Manual hide remains in effect across job changes until you show the widget again.
|
|
@@ -332,6 +335,9 @@ packages/pi-subagents/
|
|
|
332
335
|
โโโ src/ # Authoritative implementation and helpers
|
|
333
336
|
โ โโโ index.ts # Thin Pi entrypoint
|
|
334
337
|
โ โโโ subagents.ts # Commands and session lifecycle
|
|
338
|
+
โ โโโ tool-renderers.ts # Conversation tool summaries and expanded details
|
|
339
|
+
โ โโโ message-renderers.ts # Conversation completion and broker notifications
|
|
340
|
+
โ โโโ conversation-view.ts # Shared bounded, sanitized conversation rendering
|
|
335
341
|
โ โโโ menu.ts # Job manager and session-local display controls
|
|
336
342
|
โ โโโ widget.ts # Retained widget ownership and event policies
|
|
337
343
|
โ โโโ widget-renderer.ts # Bounded rows and active-job summaries
|
package/dist/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import {
|
|
39
39
|
CHILD_CORE_TOOL_NAMES,
|
|
40
40
|
DEFAULT_SUBAGENT_TOOLS,
|
|
41
|
+
JOB_STATES,
|
|
41
42
|
SUBAGENT_THINKING_LEVELS,
|
|
42
43
|
TERMINAL_JOB_STATES
|
|
43
44
|
} from "./chunks/chunk-5OXDVXXO.ts";
|
|
@@ -53,49 +54,206 @@ function sanitizeSubagentDisplayText(value) {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
// src/message-renderers.ts
|
|
57
|
+
import { Box } from "@earendil-works/pi-tui";
|
|
58
|
+
|
|
59
|
+
// src/conversation-view.ts
|
|
56
60
|
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
57
|
-
import {
|
|
61
|
+
import { Markdown, truncateToWidth } from "@earendil-works/pi-tui";
|
|
62
|
+
import { hardWrapTerminalDocument, sanitizeTerminalDocument } from "@narumitw/pi-tui-kit/terminal-document";
|
|
63
|
+
function boundedView(render, invalidate = () => {
|
|
64
|
+
}) {
|
|
65
|
+
return {
|
|
66
|
+
render(width) {
|
|
67
|
+
if (!Number.isFinite(width) || width < 1) return [];
|
|
68
|
+
const columns = Math.floor(width);
|
|
69
|
+
return render(columns).map((line) => truncateToWidth(line, columns, ""));
|
|
70
|
+
},
|
|
71
|
+
invalidate
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function conversationView(children) {
|
|
75
|
+
return boundedView(
|
|
76
|
+
(width) => children.flatMap((child) => child.render(width)),
|
|
77
|
+
() => {
|
|
78
|
+
for (const child of children) child.invalidate();
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
function singleLine(text) {
|
|
83
|
+
return sanitizeTerminalDocument(text).replace(/\s+/gu, " ").trim();
|
|
84
|
+
}
|
|
85
|
+
function displayLine(theme, text, role = "toolOutput") {
|
|
86
|
+
const safe = singleLine(text);
|
|
87
|
+
return boundedView((width) => [theme.fg(role, truncateToWidth(safe, width, "\u2026"))]);
|
|
88
|
+
}
|
|
89
|
+
function displayHeading(theme, title, detail = "", role = "toolTitle") {
|
|
90
|
+
const safeTitle = singleLine(title);
|
|
91
|
+
const safeDetail = singleLine(detail);
|
|
92
|
+
return boundedView((width) => [
|
|
93
|
+
truncateToWidth(
|
|
94
|
+
theme.fg(role, theme.bold(safeTitle)) + (safeDetail ? theme.fg("muted", ` \xB7 ${safeDetail}`) : ""),
|
|
95
|
+
width,
|
|
96
|
+
"\u2026"
|
|
97
|
+
)
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
function displayDocument(theme, text, role = "toolOutput") {
|
|
101
|
+
const safe = sanitizeTerminalDocument(text);
|
|
102
|
+
return boundedView((width) => hardWrapTerminalDocument(safe, width).map((line) => theme.fg(role, line)));
|
|
103
|
+
}
|
|
104
|
+
function displayJson(theme, value) {
|
|
105
|
+
const text = JSON.stringify(
|
|
106
|
+
value,
|
|
107
|
+
(_key, item) => typeof item === "string" ? sanitizeTerminalDocument(item) : item,
|
|
108
|
+
2
|
|
109
|
+
);
|
|
110
|
+
return displayDocument(theme, text ?? "");
|
|
111
|
+
}
|
|
112
|
+
function displayMarkdown(theme, text, role = "toolOutput") {
|
|
113
|
+
const safe = sanitizeTerminalDocument(text);
|
|
114
|
+
const markdown = new Markdown(safe, 0, 0, getMarkdownTheme(), { color: (value) => theme.fg(role, value) });
|
|
115
|
+
return boundedView(
|
|
116
|
+
(width) => markdown.render(width),
|
|
117
|
+
() => markdown.invalidate()
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
function expansionHint(theme, hints) {
|
|
121
|
+
const key = hints.keyText?.("app.tools.expand");
|
|
122
|
+
return conversationView(key ? [displayLine(theme, `(${key} to expand)`, "muted")] : []);
|
|
123
|
+
}
|
|
124
|
+
function textContent(content) {
|
|
125
|
+
return typeof content === "string" ? content : content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
|
|
126
|
+
}
|
|
127
|
+
function asRecord(value) {
|
|
128
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
129
|
+
}
|
|
130
|
+
function parseRecord(text) {
|
|
131
|
+
try {
|
|
132
|
+
return asRecord(JSON.parse(text));
|
|
133
|
+
} catch {
|
|
134
|
+
return void 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function jobPayload(value) {
|
|
138
|
+
const record = asRecord(value);
|
|
139
|
+
if (!record || typeof record.jobId !== "string" || !JOB_STATES.some((state) => state === record.state))
|
|
140
|
+
return void 0;
|
|
141
|
+
if (["result", "error"].some((key) => record[key] !== void 0 && typeof record[key] !== "string")) return void 0;
|
|
142
|
+
if (record.limitations !== void 0 && (!Array.isArray(record.limitations) || record.limitations.some((value2) => typeof value2 !== "string")))
|
|
143
|
+
return void 0;
|
|
144
|
+
if (["timedOut", "interrupted"].some((key) => record[key] !== void 0 && typeof record[key] !== "boolean"))
|
|
145
|
+
return void 0;
|
|
146
|
+
return record;
|
|
147
|
+
}
|
|
148
|
+
function stateLabel(state) {
|
|
149
|
+
return state === "timed_out" ? "timed out" : state;
|
|
150
|
+
}
|
|
151
|
+
function stateRole(state) {
|
|
152
|
+
switch (state) {
|
|
153
|
+
case "completed":
|
|
154
|
+
return "success";
|
|
155
|
+
case "failed":
|
|
156
|
+
case "timed_out":
|
|
157
|
+
return "error";
|
|
158
|
+
case "partial":
|
|
159
|
+
case "cancelled":
|
|
160
|
+
return "warning";
|
|
161
|
+
case "running":
|
|
162
|
+
return "accent";
|
|
163
|
+
case "queued":
|
|
164
|
+
return "muted";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function jobSummary(theme, payload) {
|
|
168
|
+
const waitStatus = payload.timedOut === true ? " \xB7 wait timed out (job not cancelled)" : "";
|
|
169
|
+
const interrupted = payload.interrupted === true ? " \xB7 interrupted by subagent message" : "";
|
|
170
|
+
const error = typeof payload.error === "string" && payload.error ? ` \xB7 ${payload.error}` : "";
|
|
171
|
+
const limitations = Array.isArray(payload.limitations) ? payload.limitations.length : 0;
|
|
172
|
+
return displayLine(
|
|
173
|
+
theme,
|
|
174
|
+
`${payload.jobId} \xB7 ${stateLabel(payload.state)}${waitStatus}${interrupted}${error}${limitations ? ` \xB7 ${limitations} limitation${limitations === 1 ? "" : "s"}` : ""}`,
|
|
175
|
+
payload.state === "failed" || payload.state === "timed_out" ? "error" : payload.timedOut === true || payload.interrupted === true ? "warning" : stateRole(payload.state)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
function jobBody(theme, payload) {
|
|
179
|
+
const children = [];
|
|
180
|
+
if (typeof payload.error === "string" && payload.error) {
|
|
181
|
+
children.push(displayDocument(theme, `Error: ${payload.error}`, "error"));
|
|
182
|
+
}
|
|
183
|
+
if (Array.isArray(payload.limitations)) {
|
|
184
|
+
for (const limitation of payload.limitations) {
|
|
185
|
+
if (typeof limitation === "string") children.push(displayDocument(theme, `Limitation: ${limitation}`, "warning"));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (typeof payload.result === "string" && payload.result) children.push(displayMarkdown(theme, payload.result));
|
|
189
|
+
return conversationView(children);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/message-renderers.ts
|
|
58
193
|
var COMPLETION_MESSAGE_TYPE = "pi-subagents-completion";
|
|
59
194
|
var BROKER_MESSAGE_TYPE = "pi-subagents-message";
|
|
195
|
+
var COMPLETION_PREFIX = "Subagent job completion:\n";
|
|
196
|
+
function brokerDetails(value) {
|
|
197
|
+
const details = asRecord(value);
|
|
198
|
+
return details && (details.kind === "request" || details.kind === "response") && typeof details.jobId === "string" && typeof details.requestId === "string" ? details : void 0;
|
|
199
|
+
}
|
|
200
|
+
function legacyBrokerDetails(content) {
|
|
201
|
+
const [type, protocol, request, job] = content.split("\n", 4);
|
|
202
|
+
if (protocol !== "Protocol: pi-subagents:main-message:v1" || !request?.startsWith("Request ID: ") || !job?.startsWith("Job ID: "))
|
|
203
|
+
return void 0;
|
|
204
|
+
const kind = type === "Message Type: SUBAGENT_REQUEST" ? "request" : type === "Message Type: SUBAGENT_RESPONSE" ? "response" : void 0;
|
|
205
|
+
return brokerDetails({ kind, requestId: request.slice("Request ID: ".length), jobId: job.slice("Job ID: ".length) });
|
|
206
|
+
}
|
|
60
207
|
function createMessageBox(children, outputPad, background) {
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const box = new Box(Math.min(Math.max(0, outputPad), maxPadding), 1, background);
|
|
208
|
+
return boundedView(
|
|
209
|
+
(width) => {
|
|
210
|
+
const padding = Number.isFinite(outputPad) ? Math.max(0, Math.floor(outputPad)) : 0;
|
|
211
|
+
const box = new Box(Math.min(padding, Math.floor((width - 1) / 2)), 1, background);
|
|
66
212
|
for (const child of children) box.addChild(child);
|
|
67
213
|
return box.render(width);
|
|
68
214
|
},
|
|
69
|
-
|
|
215
|
+
() => {
|
|
70
216
|
for (const child of children) child.invalidate();
|
|
71
217
|
}
|
|
72
|
-
|
|
218
|
+
);
|
|
73
219
|
}
|
|
74
220
|
function registerMessageRenderers(pi) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
221
|
+
pi.registerMessageRenderer(COMPLETION_MESSAGE_TYPE, (message, options, theme) => {
|
|
222
|
+
const content = textContent(message.content);
|
|
223
|
+
const payload = jobPayload(message.details) ?? jobPayload(
|
|
224
|
+
parseRecord(content.startsWith(COMPLETION_PREFIX) ? content.slice(COMPLETION_PREFIX.length) : content)
|
|
225
|
+
);
|
|
226
|
+
const children = [displayHeading(theme, "Subagent job completion", "", "customMessageLabel")];
|
|
227
|
+
if (payload) children.push(jobSummary(theme, payload));
|
|
228
|
+
if (options.expanded) {
|
|
229
|
+
children.push(payload ? jobBody(theme, payload) : displayDocument(theme, content, "customMessageText"));
|
|
230
|
+
} else {
|
|
231
|
+
children.push(expansionHint(theme, options));
|
|
232
|
+
}
|
|
233
|
+
return createMessageBox(children, options.outputPad, (text) => theme.bg("customMessageBg", text));
|
|
234
|
+
});
|
|
235
|
+
pi.registerMessageRenderer(BROKER_MESSAGE_TYPE, (message, options, theme) => {
|
|
236
|
+
const content = textContent(message.content);
|
|
237
|
+
const details = brokerDetails(message.details) ?? legacyBrokerDetails(content);
|
|
238
|
+
const title = details ? `Subagent ${details.kind}` : "Subagent message";
|
|
239
|
+
const source = details ? `from ${details.jobId}` : "";
|
|
240
|
+
const children = [displayHeading(theme, title, source, "customMessageLabel")];
|
|
241
|
+
if (details) {
|
|
242
|
+
children.push(
|
|
243
|
+
displayLine(
|
|
244
|
+
theme,
|
|
245
|
+
`${details.requestId} \xB7 ${details.kind === "request" ? "needs reply" : "response received"}`,
|
|
246
|
+
"muted"
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (options.expanded) {
|
|
251
|
+
children.push(displayDocument(theme, content, "customMessageText"));
|
|
252
|
+
} else {
|
|
253
|
+
children.push(expansionHint(theme, options));
|
|
254
|
+
}
|
|
255
|
+
return createMessageBox(children, options.outputPad, (text) => theme.bg("customMessageBg", text));
|
|
256
|
+
});
|
|
99
257
|
}
|
|
100
258
|
|
|
101
259
|
// src/settings.ts
|
|
@@ -2227,6 +2385,126 @@ function abortError2(message) {
|
|
|
2227
2385
|
return error;
|
|
2228
2386
|
}
|
|
2229
2387
|
|
|
2388
|
+
// src/tool-renderers.ts
|
|
2389
|
+
var LABELS = { spawn: "Spawn", inspect: "Inspect", cancel: "Cancel", wait: "Wait", send: "Send" };
|
|
2390
|
+
var PENDING = {
|
|
2391
|
+
spawn: "Starting subagent\u2026",
|
|
2392
|
+
inspect: "Inspecting jobs\u2026",
|
|
2393
|
+
cancel: "Cancelling job\u2026",
|
|
2394
|
+
wait: "Waiting for job or subagent message\u2026",
|
|
2395
|
+
send: "Sending message\u2026"
|
|
2396
|
+
};
|
|
2397
|
+
var INSPECT_PREVIEW_JOBS = 5;
|
|
2398
|
+
var STATE_PRIORITY = { failed: 0, timed_out: 1, partial: 2, running: 3, queued: 4, cancelled: 5, completed: 6 };
|
|
2399
|
+
function subagentToolRenderers(action) {
|
|
2400
|
+
return {
|
|
2401
|
+
renderCall(input, theme, context) {
|
|
2402
|
+
const args = asRecord(input) ?? {};
|
|
2403
|
+
const target = action === "send" ? typeof args.requestId === "string" ? `reply to ${args.requestId}` : typeof args.recipient === "string" ? `to ${args.recipient}` : "" : typeof args.jobId === "string" ? args.jobId : "";
|
|
2404
|
+
const children = [displayHeading(theme, `Subagent \xB7 ${LABELS[action]}`, target)];
|
|
2405
|
+
if (context.expanded) {
|
|
2406
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2407
|
+
if (typeof value === "string") {
|
|
2408
|
+
children.push(displayDocument(theme, `${key}: ${value}`, "muted"));
|
|
2409
|
+
} else if (value !== void 0) {
|
|
2410
|
+
children.push(displayLine(theme, `${key}:`, "muted"), displayJson(theme, value));
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
} else {
|
|
2414
|
+
const preview = action === "spawn" ? args.task : action === "send" ? args.message : void 0;
|
|
2415
|
+
if (typeof preview === "string") children.push(displayLine(theme, preview, "muted"));
|
|
2416
|
+
}
|
|
2417
|
+
return conversationView(children);
|
|
2418
|
+
},
|
|
2419
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
2420
|
+
const raw = textContent(result.content);
|
|
2421
|
+
const children = [];
|
|
2422
|
+
if (context.isError) {
|
|
2423
|
+
children.push(
|
|
2424
|
+
displayLine(theme, `${LABELS[action]} failed`, "error"),
|
|
2425
|
+
expanded ? displayDocument(theme, raw, "error") : displayLine(theme, raw, "error")
|
|
2426
|
+
);
|
|
2427
|
+
} else if (isPartial) {
|
|
2428
|
+
return displayLine(theme, PENDING[action], "muted");
|
|
2429
|
+
} else {
|
|
2430
|
+
const payload = toolPayload(action, result);
|
|
2431
|
+
if (!payload) {
|
|
2432
|
+
children.push(
|
|
2433
|
+
expanded ? displayDocument(theme, raw || "No result returned.") : displayLine(theme, raw || "No result returned.")
|
|
2434
|
+
);
|
|
2435
|
+
} else if (action === "inspect") {
|
|
2436
|
+
children.push(...inspectSummary(theme, payload, expanded));
|
|
2437
|
+
if (expanded) children.push(displayJson(theme, payload));
|
|
2438
|
+
} else if (action === "send") {
|
|
2439
|
+
const accepted = payload.accepted === true;
|
|
2440
|
+
const reply = typeof asRecord(context.args)?.requestId === "string";
|
|
2441
|
+
children.push(
|
|
2442
|
+
displayLine(
|
|
2443
|
+
theme,
|
|
2444
|
+
`${reply ? "Reply" : "Request"} ${accepted ? "accepted" : "not accepted"}${payload.duplicate === true ? " \xB7 duplicate" : ""} \xB7 ${payload.requestId}`,
|
|
2445
|
+
accepted ? "success" : "warning"
|
|
2446
|
+
)
|
|
2447
|
+
);
|
|
2448
|
+
if (expanded) children.push(displayJson(theme, payload));
|
|
2449
|
+
} else {
|
|
2450
|
+
const job = jobPayload(payload);
|
|
2451
|
+
if (job) children.push(jobSummary(theme, job));
|
|
2452
|
+
if (expanded) {
|
|
2453
|
+
if (action === "wait" && job) {
|
|
2454
|
+
const { result: _result, error: _error, limitations: _limitations, ...metadata } = job;
|
|
2455
|
+
children.push(displayJson(theme, metadata), jobBody(theme, job));
|
|
2456
|
+
} else {
|
|
2457
|
+
children.push(displayJson(theme, payload));
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
if (!expanded) children.push(expansionHint(theme, context));
|
|
2463
|
+
return conversationView(children);
|
|
2464
|
+
}
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
function toolPayload(action, result) {
|
|
2468
|
+
for (const value of [result.details, parseRecord(textContent(result.content))]) {
|
|
2469
|
+
const record = asRecord(value);
|
|
2470
|
+
if (!record) continue;
|
|
2471
|
+
if (action === "inspect") {
|
|
2472
|
+
if (Array.isArray(record.jobs) && record.jobs.every((job) => jobPayload(job))) return record;
|
|
2473
|
+
} else if (action === "send") {
|
|
2474
|
+
if (typeof record.requestId === "string" && typeof record.accepted === "boolean") return record;
|
|
2475
|
+
} else if (jobPayload(record)) {
|
|
2476
|
+
return record;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return void 0;
|
|
2480
|
+
}
|
|
2481
|
+
function inspectSummary(theme, payload, expanded) {
|
|
2482
|
+
const jobs = payload.jobs.map(jobPayload).filter((job) => job !== void 0);
|
|
2483
|
+
const counts = JOB_STATES.map((state) => {
|
|
2484
|
+
const count = jobs.filter((job) => job.state === state).length;
|
|
2485
|
+
return count ? `${count} ${stateLabel(state)}` : void 0;
|
|
2486
|
+
}).filter(Boolean);
|
|
2487
|
+
const children = [
|
|
2488
|
+
displayLine(
|
|
2489
|
+
theme,
|
|
2490
|
+
jobs.length ? `${jobs.length} job${jobs.length === 1 ? "" : "s"} \xB7 ${counts.join(" \xB7 ")}` : "No retained jobs.",
|
|
2491
|
+
"muted"
|
|
2492
|
+
)
|
|
2493
|
+
];
|
|
2494
|
+
if (!expanded) {
|
|
2495
|
+
const sorted = [...jobs].sort((left, right) => STATE_PRIORITY[left.state] - STATE_PRIORITY[right.state]);
|
|
2496
|
+
for (const job of sorted.slice(0, INSPECT_PREVIEW_JOBS)) children.push(jobSummary(theme, job));
|
|
2497
|
+
if (jobs.length > INSPECT_PREVIEW_JOBS) {
|
|
2498
|
+
children.push(displayLine(theme, `${jobs.length - INSPECT_PREVIEW_JOBS} more jobs`, "muted"));
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
const omitted = asRecord(payload.omitted)?.jobs;
|
|
2502
|
+
if (typeof omitted === "number" && omitted > 0) {
|
|
2503
|
+
children.push(displayLine(theme, `${omitted} expired or removed jobs not retained`, "muted"));
|
|
2504
|
+
}
|
|
2505
|
+
return children;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2230
2508
|
// src/tools.ts
|
|
2231
2509
|
var MAX_TASK_BYTES = 50 * 1024;
|
|
2232
2510
|
var MAX_TOOLS = MAX_SELECTED_TOOLS;
|
|
@@ -2367,6 +2645,7 @@ function registerSubagentTools(pi, dependencies = {}) {
|
|
|
2367
2645
|
pi.registerTool({
|
|
2368
2646
|
name: "subagent_spawn",
|
|
2369
2647
|
label: "Subagent \xB7 Spawn",
|
|
2648
|
+
...subagentToolRenderers("spawn"),
|
|
2370
2649
|
description: "Use subagent_spawn to start one Pi subagent job and return its jobId immediately. The task defines the child's specialization; selected core tools and explicit trusted local skills or extensions define its initial capabilities. The job may ask the main agent questions and publishes one asynchronous completion when terminal.",
|
|
2371
2650
|
promptSnippet: "Use subagent_spawn to start one Pi subagent job",
|
|
2372
2651
|
parameters: SpawnParameters,
|
|
@@ -2432,6 +2711,7 @@ function registerSubagentTools(pi, dependencies = {}) {
|
|
|
2432
2711
|
pi.registerTool({
|
|
2433
2712
|
name: "subagent_inspect",
|
|
2434
2713
|
label: "Subagent \xB7 Inspect",
|
|
2714
|
+
...subagentToolRenderers("inspect"),
|
|
2435
2715
|
description: "Use subagent_inspect to return one privacy-filtered snapshot of retained jobs without exposing task text, complete child output, prompts, selected tools, context, credentials, or broker messages.",
|
|
2436
2716
|
promptSnippet: "Use subagent_inspect to inspect retained subagent jobs",
|
|
2437
2717
|
parameters: InspectParameters,
|
|
@@ -2444,6 +2724,7 @@ function registerSubagentTools(pi, dependencies = {}) {
|
|
|
2444
2724
|
pi.registerTool({
|
|
2445
2725
|
name: "subagent_cancel",
|
|
2446
2726
|
label: "Subagent \xB7 Cancel",
|
|
2727
|
+
...subagentToolRenderers("cancel"),
|
|
2447
2728
|
description: "Use subagent_cancel to idempotently cancel one queued or running job and release its process, timer, broker credentials, and temporary resources. Terminal jobs remain unchanged.",
|
|
2448
2729
|
promptSnippet: "Use subagent_cancel to cancel one active subagent job",
|
|
2449
2730
|
parameters: CancelParameters,
|
|
@@ -2455,6 +2736,7 @@ function registerSubagentTools(pi, dependencies = {}) {
|
|
|
2455
2736
|
pi.registerTool({
|
|
2456
2737
|
name: "subagent_wait",
|
|
2457
2738
|
label: "Subagent \xB7 Wait",
|
|
2739
|
+
...subagentToolRenderers("wait"),
|
|
2458
2740
|
description: "Use subagent_wait to wait for one job to become terminal. An incoming child request or response interrupts the wait without cancelling the job. A timeout or caller cancellation stops only this wait.",
|
|
2459
2741
|
promptSnippet: "Use subagent_wait to wait for one subagent job or incoming message",
|
|
2460
2742
|
parameters: WaitParameters,
|
|
@@ -2467,6 +2749,7 @@ function registerSubagentTools(pi, dependencies = {}) {
|
|
|
2467
2749
|
pi.registerTool({
|
|
2468
2750
|
name: "subagent_send",
|
|
2469
2751
|
label: "Subagent \xB7 Send",
|
|
2752
|
+
...subagentToolRenderers("send"),
|
|
2470
2753
|
description: "Use subagent_send to send one request to an active job or answer one pending child request. For a new request, provide recipient and omit requestId. To answer a request, provide requestId and omit recipient. Provide exactly one of recipient or requestId. An accepted new request interrupts any active child response wait so delivery can proceed without consuming the child's original request.",
|
|
2471
2754
|
promptSnippet: "Use subagent_send to send or answer one subagent message",
|
|
2472
2755
|
parameters: SendParameters,
|
|
@@ -2655,7 +2938,7 @@ import {
|
|
|
2655
2938
|
} from "@narumitw/pi-tui-kit/editor-status-view";
|
|
2656
2939
|
|
|
2657
2940
|
// src/widget-renderer.ts
|
|
2658
|
-
import { stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2941
|
+
import { stripTerminalSequences, truncateToWidth as truncateToWidth2, visibleWidth } from "@earendil-works/pi-tui";
|
|
2659
2942
|
import { EditorStatusWidget } from "@narumitw/pi-tui-kit/editor-status-widget";
|
|
2660
2943
|
var TASK_PREVIEW_WIDTH = 40;
|
|
2661
2944
|
var SUBAGENT_WIDGET_RESERVED_ROWS = 8;
|
|
@@ -2663,10 +2946,34 @@ var SUBAGENT_WIDGET_MAX_ROWS = 10;
|
|
|
2663
2946
|
function subagentWidgetRowBudget(rows) {
|
|
2664
2947
|
return Number.isFinite(rows) ? Math.min(SUBAGENT_WIDGET_MAX_ROWS, Math.max(0, Math.floor(rows) - SUBAGENT_WIDGET_RESERVED_ROWS)) : 0;
|
|
2665
2948
|
}
|
|
2666
|
-
function
|
|
2949
|
+
function subagentWidgetSummary(snapshot, maxRows) {
|
|
2950
|
+
const { jobs, items, mode } = snapshot;
|
|
2951
|
+
const budget = normalizeRowBudget(maxRows);
|
|
2952
|
+
if (!isVisible(snapshot) || budget === 0) return "";
|
|
2953
|
+
const shown = mode === "compact" ? items.length : Math.min(items.length, budget - 1);
|
|
2954
|
+
const omitted = jobs.length - shown;
|
|
2955
|
+
const removed = jobs.length - items.length;
|
|
2956
|
+
const running = jobs.filter((job) => job.state === "running").length;
|
|
2957
|
+
const counts = `${running} running \xB7 ${jobs.length - running} queued`;
|
|
2958
|
+
return `${counts}${omitted > 0 ? ` \xB7 ${omitted} not shown${removed > 0 ? ` (${removed} removed)` : ""}` : ""}`;
|
|
2959
|
+
}
|
|
2960
|
+
function normalizeRowBudget(maxRows) {
|
|
2961
|
+
return Number.isFinite(maxRows) ? Math.max(0, Math.floor(maxRows)) : 0;
|
|
2962
|
+
}
|
|
2963
|
+
function isVisible({ jobs, items, settings, mode }) {
|
|
2964
|
+
return settings.showWidget && mode !== "hidden" && jobs.length > 0 && items.length > 0;
|
|
2965
|
+
}
|
|
2966
|
+
function renderSubagentWidgetSnapshot(snapshot, theme, width, maxRows, includeHeader = true) {
|
|
2667
2967
|
const { jobs, items, settings, mode } = snapshot;
|
|
2668
|
-
const budget =
|
|
2669
|
-
if (!
|
|
2968
|
+
const budget = normalizeRowBudget(maxRows);
|
|
2969
|
+
if (!isVisible(snapshot) || budget < (includeHeader ? 2 : 1)) return [];
|
|
2970
|
+
if (!includeHeader) {
|
|
2971
|
+
const shown = mode === "compact" ? [] : items.slice(0, budget - 1);
|
|
2972
|
+
return new EditorStatusWidget({
|
|
2973
|
+
theme,
|
|
2974
|
+
renderBody: () => shown.map(({ value, mode: itemMode }) => renderJob(value, theme, settings, itemMode === "compact"))
|
|
2975
|
+
}).render(width);
|
|
2976
|
+
}
|
|
2670
2977
|
const running = jobs.filter((job) => job.state === "running").length;
|
|
2671
2978
|
const counts = `Subagents \xB7 ${jobs.length} active \xB7 ${running} running \xB7 ${jobs.length - running} queued`;
|
|
2672
2979
|
const removed = jobs.length - items.length;
|
|
@@ -2690,7 +2997,7 @@ function renderJob(job, theme, settings, compact) {
|
|
|
2690
2997
|
const state = theme.fg(running ? "accent" : "muted", job.state);
|
|
2691
2998
|
const jobId = sanitizeSubagentDisplayText(job.jobId);
|
|
2692
2999
|
const taskText = sanitizeSubagentDisplayText(job.task);
|
|
2693
|
-
const task = stripTerminalSequences(
|
|
3000
|
+
const task = stripTerminalSequences(truncateToWidth2(taskText, TASK_PREVIEW_WIDTH, "\u2026"));
|
|
2694
3001
|
const labels = [
|
|
2695
3002
|
...settings.showJobId && visibleWidth(jobId) > 0 ? [theme.fg("text", jobId)] : [],
|
|
2696
3003
|
...visibleWidth(task) > 0 ? [theme.fg("text", task)] : []
|
|
@@ -2842,9 +3149,11 @@ function createSubagentWidgetController(runtime) {
|
|
|
2842
3149
|
view = createEditorStatusView({ onChange: publish, canCompact: () => settings.showWidget && jobs.length > 0 });
|
|
2843
3150
|
publisher = createEditorStatusPublisher({
|
|
2844
3151
|
key: SUBAGENT_WIDGET_KEY,
|
|
3152
|
+
name: "Subagents",
|
|
2845
3153
|
context: ctx,
|
|
2846
3154
|
isCurrent,
|
|
2847
|
-
|
|
3155
|
+
summary: (snapshot, _width, rows) => subagentWidgetSummary(snapshot, subagentWidgetRowBudget(rows)),
|
|
3156
|
+
render: (snapshot, theme, width, rows) => renderSubagentWidgetSnapshot(snapshot, theme, width, subagentWidgetRowBudget(rows), false)
|
|
2848
3157
|
});
|
|
2849
3158
|
if (phase === "maintenance") cyclePhase = phase;
|
|
2850
3159
|
else if (phase !== "idle") startCycle(phase);
|
|
@@ -2853,7 +3162,9 @@ function createSubagentWidgetController(runtime) {
|
|
|
2853
3162
|
controls = {
|
|
2854
3163
|
state: () => ({ mode: ownedView.mode, pinned: ownedView.pinned, trigger: ownedView.trigger }),
|
|
2855
3164
|
setMode: (mode) => {
|
|
2856
|
-
if (isCurrent())
|
|
3165
|
+
if (!isCurrent()) return;
|
|
3166
|
+
if (mode !== "hidden") publisher?.setCollapsed(false);
|
|
3167
|
+
ownedView.setMode(mode);
|
|
2857
3168
|
},
|
|
2858
3169
|
setPinned: (pinned) => {
|
|
2859
3170
|
if (isCurrent()) ownedView.setPinned(pinned);
|