@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 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: "Open the sub-agent details panel (tabs per agent, full timeline)",
206
- handler: (ctx) => openAgentPanel(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.0",
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). Shows the latest spawn_agents
5
- * call: one tab per sub-agent, a full scrollable timeline per tab (task,
6
- * tool calls, tool-result previews, assistant output rendered as markdown,
7
- * usage), live-updating while agents run.
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) <= width;
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, width);
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 footer =
228
- theme.fg("dim", `${pos} ${mode}`) +
229
- theme.fg("muted", " · ←/→ tab · ↑/↓ wheel scroll · End follow · Esc close");
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();