@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 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
- The version 3 runtime documented here is not yet published to npm.
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 git:github.com/narumiruna/pi-extensions
28
+ pi install npm:@narumitw/pi-subagents
32
29
  ```
33
30
 
34
- This Git installation enables every extension listed in the repository root manifest, including Pi Subagents.
31
+ Try it for one invocation:
35
32
 
36
- To install only Pi Subagents, clone the repository, install dependencies, build its generated runtime, and install its package directory:
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
- Completion messages and child requests and responses follow Pi's global tool-output expansion state and the `app.tools.expand` binding (`Ctrl+O` by default).
76
- Collapsed messages show a summary and the host's configured expansion keys.
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 ยท 2 active
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 fewer than two available rows it renders nothing. Short layouts use a summary with active, running, queued, and omitted-row counts. Narrow layouts clip each line to the terminal width.
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 { Box, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
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
- render(width) {
63
- if (width <= 0) return [];
64
- const maxPadding = Math.floor((width - 1) / 2);
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
- invalidate() {
215
+ () => {
70
216
  for (const child of children) child.invalidate();
71
217
  }
72
- };
218
+ );
73
219
  }
74
220
  function registerMessageRenderers(pi) {
75
- for (const [messageType, summary] of [
76
- [COMPLETION_MESSAGE_TYPE, "Subagent job completion"],
77
- [BROKER_MESSAGE_TYPE, "Subagent message"]
78
- ]) {
79
- pi.registerMessageRenderer(messageType, (message, options, theme) => {
80
- const children = [
81
- new Text(theme.fg("customMessageLabel", theme.bold(`[${messageType}]`)), 0, 0),
82
- new Spacer(1)
83
- ];
84
- if (!options.expanded) {
85
- const expansionKey = options.keyText?.("app.tools.expand");
86
- const hint = expansionKey ? theme.fg("muted", ` (${expansionKey} to expand)`) : "";
87
- children.push(new Text(theme.fg("customMessageText", summary) + hint, 0, 0));
88
- } else {
89
- const content = typeof message.content === "string" ? message.content : message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
90
- children.push(
91
- new Markdown(sanitizeTerminalText(content), 0, 0, getMarkdownTheme(), {
92
- color: (text) => theme.fg("customMessageText", text)
93
- })
94
- );
95
- }
96
- return createMessageBox(children, options.outputPad, (text) => theme.bg("customMessageBg", text));
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 renderSubagentWidgetSnapshot(snapshot, theme, width, maxRows) {
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 = Number.isFinite(maxRows) ? Math.max(0, Math.floor(maxRows)) : 0;
2669
- if (!settings.showWidget || mode === "hidden" || jobs.length === 0 || items.length === 0 || budget < 2) return [];
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(truncateToWidth(taskText, TASK_PREVIEW_WIDTH, "\u2026"));
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
- render: (snapshot, theme, width, rows) => renderSubagentWidgetSnapshot(snapshot, theme, width, subagentWidgetRowBudget(rows))
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()) ownedView.setMode(mode);
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);