@pi-spice/minimal-subagents 0.1.0 → 0.1.1
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 +7 -0
- package/README.md +2 -1
- package/index.ts +18 -3
- package/package.json +1 -1
- package/panel.ts +52 -16
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @pi-spice/minimal-subagents
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 245d41f: Details panel: alt+a now toggles (also closes an open panel); panel data is seeded the moment spawn_agents starts, so alt+a works during the child-startup window and after an interrupt instead of reporting "no data"; alt+a with no run at all shows pi's notify; panel content gets inner padding and footer hints that fit narrow panels.
|
package/README.md
CHANGED
|
@@ -31,7 +31,8 @@ Quick test from this repo: `pi -e ./extensions/minimal-subagents`
|
|
|
31
31
|
|
|
32
32
|
- One tab per sub-agent (`←`/`→` or `1`-`8`; the tab bar compacts automatically on narrow panels), labeled with name and live status (⏳/✓/✗).
|
|
33
33
|
- Each tab is the agent's full timeline: task, tool calls, tool-result previews (first 10 lines), assistant output rendered as markdown, usage. Thinking is not shown.
|
|
34
|
-
- Terminal-style scrolling: `↑/↓`, `PgUp/PgDn`, `Home`/`g`, `End`/`G`, mouse wheel — pinned to the bottom while following new output, scrolling up pauses, `End` resumes. `Esc` closes.
|
|
34
|
+
- Terminal-style scrolling: `↑/↓`, `PgUp/PgDn`, `Home`/`g`, `End`/`G`, mouse wheel — pinned to the bottom while following new output, scrolling up pauses, `End` resumes. `alt+a` toggles (same key opens and closes); `Esc` also closes.
|
|
35
|
+
- Pressing `alt+a` before any `spawn_agents` run shows pi's notify message above the input instead of opening an empty panel.
|
|
35
36
|
- Shows the latest call only. Two platform limits: it is an overlay (the transcript is covered, not reflowed), and mouse wheel works only under `--tui-mode fullscreen` — the only mode where pi enables terminal mouse reporting.
|
|
36
37
|
|
|
37
38
|
## No nesting
|
package/index.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
type SubagentDetails,
|
|
29
29
|
} from "./spawn.ts";
|
|
30
30
|
import { renderSpawnCall, renderSpawnResult } from "./render.ts";
|
|
31
|
-
import { openAgentPanel, setPanelDetails } from "./panel.ts";
|
|
31
|
+
import { hasPanelDetails, openAgentPanel, setPanelDetails } from "./panel.ts";
|
|
32
32
|
|
|
33
33
|
const MAX_AGENTS = 8;
|
|
34
34
|
const MAX_CONCURRENCY = 4;
|
|
@@ -138,6 +138,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
138
138
|
}
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
// Seed the panel the moment the tool starts: alt+a can then open it and
|
|
142
|
+
// show "running" placeholders. Without this, the panel would stay
|
|
143
|
+
// "no data" until the first child event arrives — child boot plus the
|
|
144
|
+
// child's first model turn can take 5-20s, and interrupting (or just
|
|
145
|
+
// peeking) inside that window would claim there is nothing to show.
|
|
146
|
+
emitParallelUpdate();
|
|
147
|
+
|
|
141
148
|
const results = await mapWithConcurrencyLimit(params.agents, MAX_CONCURRENCY, async (spec, index) => {
|
|
142
149
|
// On abort, return what exists instead of throwing finished work away.
|
|
143
150
|
const abortPlaceholder = (): SingleResult => {
|
|
@@ -202,7 +209,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
202
209
|
});
|
|
203
210
|
|
|
204
211
|
pi.registerShortcut("alt+a", {
|
|
205
|
-
description: "
|
|
206
|
-
handler: (ctx) =>
|
|
212
|
+
description: "Toggle the sub-agent details panel (tabs per agent, full timeline); alt+a or Esc closes it",
|
|
213
|
+
handler: (ctx) => {
|
|
214
|
+
// No run yet → pi's notify message, not an empty overlay stuck in the
|
|
215
|
+
// corner.
|
|
216
|
+
if (!hasPanelDetails()) {
|
|
217
|
+
ctx.ui.notify("No sub-agent data yet — run spawn_agents first", "info");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
openAgentPanel(ctx);
|
|
221
|
+
},
|
|
207
222
|
});
|
|
208
223
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-spice/minimal-subagents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Create sub-agents dynamically and run them in parallel; single blocking tool, no orchestration, nesting prevented",
|
|
5
5
|
"keywords": ["pi-package"],
|
|
6
6
|
"license": "MIT",
|
package/panel.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* panel.ts — sub-agent details panel (overlay) for minimal-subagents
|
|
3
3
|
*
|
|
4
|
-
* Opened with alt+a (registered in index.ts)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Opened with alt+a (registered in index.ts), closed with alt+a or Esc —
|
|
5
|
+
* while the panel is focused the host routes all input here, so the open
|
|
6
|
+
* shortcut never fires; the panel must recognize alt+a itself to toggle.
|
|
7
|
+
* With no spawn_agents data yet, the shortcut shows a notify message
|
|
8
|
+
* instead of opening an empty overlay (guarded in index.ts via
|
|
9
|
+
* hasPanelDetails).
|
|
10
|
+
* Shows the latest spawn_agents call: one tab per sub-agent, a full scrollable
|
|
11
|
+
* timeline per tab (task, tool calls, tool-result previews, assistant output
|
|
12
|
+
* rendered as markdown, usage), live-updating while agents run.
|
|
8
13
|
*
|
|
9
14
|
* Rendering is line-based: the timeline is flattened into styled lines and
|
|
10
15
|
* windowed by a hand-rolled viewport (offset math) — the overlay contract is
|
|
@@ -13,7 +18,7 @@
|
|
|
13
18
|
* while the overlay is focused.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
|
-
import { Markdown, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
21
|
+
import { Markdown, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
17
22
|
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
18
23
|
import { type Message } from "@earendil-works/pi-ai";
|
|
19
24
|
import { formatToolCall, formatUsageStats, type RenderTheme } from "./render.ts";
|
|
@@ -32,6 +37,11 @@ export function setPanelDetails(details: SubagentDetails): void {
|
|
|
32
37
|
for (const listener of listeners) listener();
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
/** True once at least one spawn_agents result (even still running) exists. */
|
|
41
|
+
export function hasPanelDetails(): boolean {
|
|
42
|
+
return currentDetails !== null && currentDetails.results.length > 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
// ---------------------------------------------------------------------------
|
|
36
46
|
// Panel opening
|
|
37
47
|
// ---------------------------------------------------------------------------
|
|
@@ -39,7 +49,7 @@ export function setPanelDetails(details: SubagentDetails): void {
|
|
|
39
49
|
let opening: Promise<unknown> | null = null;
|
|
40
50
|
|
|
41
51
|
export function openAgentPanel(ctx: { ui: any; hasUI?: boolean }): void {
|
|
42
|
-
if (opening) return; // already open; Esc closes
|
|
52
|
+
if (opening) return; // already open; alt+a or Esc closes
|
|
43
53
|
if (ctx.hasUI === false) return;
|
|
44
54
|
opening = ctx.ui
|
|
45
55
|
.custom(
|
|
@@ -187,6 +197,12 @@ class AgentPanel {
|
|
|
187
197
|
// A newer spawn_agents call may have fewer agents — clamp the tab.
|
|
188
198
|
this.activeTab = Math.min(this.activeTab, details.results.length - 1);
|
|
189
199
|
|
|
200
|
+
// Inner padding: 1 column each side and 1 blank line above/below the
|
|
201
|
+
// body, so content breathes away from the rules and panel edges. The
|
|
202
|
+
// rules span the full width; content wraps at width - 2.
|
|
203
|
+
const innerWidth = Math.max(10, width - 2);
|
|
204
|
+
const pad = (line: string) => (line.length === 0 ? "" : ` ${line}`);
|
|
205
|
+
|
|
190
206
|
// --- header: tab bar ------------------------------------------------
|
|
191
207
|
// Full labels (index + name + status) when they fit the panel width;
|
|
192
208
|
// otherwise degrade to compact slots (index + status) which always fit
|
|
@@ -200,35 +216,47 @@ class AgentPanel {
|
|
|
200
216
|
return `${i + 1} ${name} ${statusIcon(r, theme)}`;
|
|
201
217
|
});
|
|
202
218
|
const fullFits =
|
|
203
|
-
7 + fullLabels.reduce((sum, l) => sum + visibleWidth(l) + 2, 0) + (fullLabels.length - 1) <=
|
|
219
|
+
7 + fullLabels.reduce((sum, l) => sum + visibleWidth(l) + 2, 0) + (fullLabels.length - 1) <= innerWidth;
|
|
204
220
|
const tabs = fullFits
|
|
205
221
|
? renderSlots(fullLabels)
|
|
206
222
|
: renderSlots(details.results.map((r, i) => `${i + 1}${statusIcon(r, theme)}`));
|
|
207
|
-
const header = [theme.fg("toolTitle", theme.bold("agents ")) + tabs, theme.fg("muted", "─".repeat(width))];
|
|
223
|
+
const header = [pad(theme.fg("toolTitle", theme.bold("agents ")) + tabs), theme.fg("muted", "─".repeat(width))];
|
|
208
224
|
|
|
209
225
|
// --- body: windowed timeline ----------------------------------------
|
|
210
226
|
const rows = this.tui?.terminal?.rows ?? process.stdout.rows ?? 24;
|
|
211
|
-
const headerH = header.length; // tab bar + rule
|
|
212
|
-
const footerH = 2; // rule + status line
|
|
227
|
+
const headerH = header.length + 1; // blank pad + tab bar + rule
|
|
228
|
+
const footerH = 2 + 1; // rule + status line + blank pad above the rule
|
|
213
229
|
this.bodyHeight = Math.max(4, rows - headerH - footerH);
|
|
214
230
|
|
|
215
|
-
const lines = buildTimeline(details.results[this.activeTab], theme,
|
|
231
|
+
const lines = buildTimeline(details.results[this.activeTab], theme, innerWidth);
|
|
216
232
|
this.lineCount = lines.length;
|
|
217
233
|
|
|
218
234
|
const maxOffset = Math.max(0, this.lineCount - this.bodyHeight);
|
|
219
235
|
if (this.follow) this.offset = maxOffset;
|
|
220
236
|
this.offset = Math.min(Math.max(0, this.offset), maxOffset);
|
|
221
|
-
const body = lines.slice(this.offset, this.offset + this.bodyHeight);
|
|
237
|
+
const body = lines.slice(this.offset, this.offset + this.bodyHeight).map(pad);
|
|
222
238
|
while (body.length < this.bodyHeight) body.push(""); // stable panel height
|
|
223
239
|
|
|
224
240
|
// --- footer: scroll position + hints ---------------------------------
|
|
225
241
|
const pos = this.lineCount > 0 ? `${this.offset + 1}-${Math.min(this.offset + this.bodyHeight, this.lineCount)}/${this.lineCount}` : "0";
|
|
226
242
|
const mode = this.follow ? "following" : "paused";
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
const statusText = theme.fg("dim", `${pos} ${mode}`);
|
|
244
|
+
// Hint set degrades as the panel narrows; truncate is only a backstop.
|
|
245
|
+
const hintVariants = [
|
|
246
|
+
" · ←/→ tab · ↑/↓ scroll · End follow · alt+a/Esc close",
|
|
247
|
+
" · ←/→ tab · End follow · alt+a/Esc close",
|
|
248
|
+
" · alt+a/Esc close",
|
|
249
|
+
];
|
|
250
|
+
let hints = "";
|
|
251
|
+
for (const hint of hintVariants) {
|
|
252
|
+
if (visibleWidth(statusText) + hint.length <= innerWidth) {
|
|
253
|
+
hints = theme.fg("muted", hint);
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const footer = truncateToWidth(pad(statusText + hints), width);
|
|
230
258
|
|
|
231
|
-
return [...header, ...body, theme.fg("muted", "─".repeat(width)), footer];
|
|
259
|
+
return ["", ...header, ...body, "", theme.fg("muted", "─".repeat(width)), footer];
|
|
232
260
|
}
|
|
233
261
|
|
|
234
262
|
handleInput(data: string): void {
|
|
@@ -241,6 +269,14 @@ class AgentPanel {
|
|
|
241
269
|
}
|
|
242
270
|
|
|
243
271
|
private handleInputInner(data: string): void {
|
|
272
|
+
// alt+a toggles: while we hold focus the host shortcut cannot fire, so
|
|
273
|
+
// the panel closes on the same key that opened it (matchesKey covers
|
|
274
|
+
// legacy ESC+a and kitty/CSI-u encodings alike).
|
|
275
|
+
if (matchesKey(data, "alt+a")) {
|
|
276
|
+
this.close();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
244
280
|
const details = currentDetails;
|
|
245
281
|
if (!details || details.results.length === 0) {
|
|
246
282
|
if (data === "\x1b") this.close();
|