@arhen/pi-core-subagent 1.1.19 → 1.1.21

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 CHANGED
@@ -104,7 +104,7 @@ Background + intercom:
104
104
 
105
105
  Read-only pane over the session's subagents:
106
106
 
107
- - `↑`/`↓` — move between agents
107
+ - `shift+↑`/`shift+↓` (or `j`/`k`) — move between agents; bare arrows work too where the terminal doesn't reserve them
108
108
  - `enter` — live tail of that child's session file (`esc` goes back)
109
109
  - `x` then `y` — abort ONE subagent (only mutation; `n`/any other key cancels)
110
110
  - `esc` — close
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arhen/pi-core-subagent",
3
- "version": "1.1.19",
3
+ "version": "1.1.21",
4
4
  "type": "module",
5
5
  "description": "pi extension: fast in-process subagents with single/parallel/chain, background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -1238,10 +1238,10 @@ export default function (pi: ExtensionAPI) {
1238
1238
  createPeekPane(getTasks, theme, () => tui.requestRender(), () => done(undefined), (t) => {
1239
1239
  if (manager.cancelTask(t.runId, t.taskId, ctx)) ctx.ui.notify(`Aborted subagent ${t.agent}.`, "warning");
1240
1240
  }),
1241
- { overlay: true, overlayOptions: { anchor: "center", width: "80%", maxHeight: "70%" } },
1241
+ { overlay: true, overlayOptions: { anchor: "center", width: "70%", minWidth: 60, maxHeight: "70%", margin: 2 } },
1242
1242
  );
1243
1243
  };
1244
- pi.registerCommand("peek", { description: "Peek at running subagents (↑↓ move, enter tails)", handler: (_args, ctx) => openPeek(ctx) });
1244
+ pi.registerCommand("peek", { description: "Peek at running subagents (shift+↑↓ or j/k move, enter tails)", handler: (_args, ctx) => openPeek(ctx) });
1245
1245
  // ctrl+shift+s belongs to pi-web-access (search curator); 'a' for agents is free.
1246
1246
  pi.registerShortcut("ctrl+shift+a", { description: "Peek at running subagents", handler: openPeek });
1247
1247
 
package/src/peek.ts CHANGED
@@ -1,18 +1,19 @@
1
1
  /**
2
2
  * Peek pane — quick, read-only look at what subagents are doing.
3
3
  *
4
- * ↑/↓ (or ←/→) move between agents, enter opens a live tail of that child's
5
- * session file, esc goes back / closes. Never touches run state: no abort,
6
- * no cancel, no writes.
4
+ * shift+↑/↓ (or j/k) move between agents, enter opens a live tail of that
5
+ * child's session file, esc goes back / closes. Never touches run state:
6
+ * no abort except the explicit x + y confirmation.
7
7
  */
8
8
 
9
9
  import { closeSync, openSync, readSync, statSync } from "node:fs";
10
- import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
10
+ import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
11
  import type { Theme } from "@earendil-works/pi-coding-agent";
12
12
 
13
13
  /** Tail window: last 64KB of the child session file is plenty for a peek. */
14
14
  const TAIL_BYTES = 64 * 1024;
15
15
  const POLL_MS = 700;
16
+ const TAIL_ROWS = 18;
16
17
 
17
18
  export interface PeekTask {
18
19
  runId: string;
@@ -37,8 +38,13 @@ function readTail(path: string): string {
37
38
  }
38
39
  }
39
40
 
40
- /** One session-file line one display line. Unparseable/irrelevant → null. */
41
- function eventLine(raw: string): string | null {
41
+ function clip(text: string, max: number): string {
42
+ const flat = text.replace(/\s+/g, " ").trim();
43
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat;
44
+ }
45
+
46
+ /** One session-file line → one display line: [gutter, text]. Irrelevant → null. */
47
+ function eventLine(raw: string): [string, string] | null {
42
48
  let entry: any;
43
49
  try {
44
50
  entry = JSON.parse(raw);
@@ -47,25 +53,26 @@ function eventLine(raw: string): string | null {
47
53
  }
48
54
  const msg = entry?.message;
49
55
  if (!msg) return null;
50
- const parts: string[] = [];
56
+ const out: [string, string][] = [];
51
57
  for (const block of msg.content ?? []) {
52
- if (block.type === "text" && block.text?.trim()) parts.push(block.text.trim());
53
- else if (block.type === "toolCall") parts.push(`→ ${block.name} ${JSON.stringify(block.arguments ?? {})}`);
54
- else if (block.type === "thinking" && block.thinking?.trim()) parts.push(`(thinking) ${block.thinking.trim()}`);
58
+ if (block.type === "toolCall") {
59
+ const arg = Object.values(block.arguments ?? {}).find((v) => typeof v === "string" && v.trim() !== "") as string | undefined;
60
+ out.push(["", `${block.name}${arg ? ` ${clip(arg, 120)}` : ""}`]);
61
+ } else if (block.type === "text" && block.text?.trim()) {
62
+ out.push([msg.role === "toolResult" ? "←" : "·", clip(block.text, 160)]);
63
+ }
55
64
  }
56
- if (parts.length === 0) return null;
57
- const who = msg.role === "assistant" ? "" : msg.role === "toolResult" ? " ← " : `${msg.role}: `;
58
- return `${who}${parts.join(" ").replace(/\s+/g, " ")}`;
65
+ return out[0] ?? null;
59
66
  }
60
67
 
61
- function tailLines(path: string, max: number): string[] {
68
+ function tailLines(path: string, max: number): [string, string][] {
62
69
  let text: string;
63
70
  try {
64
71
  text = readTail(path);
65
72
  } catch {
66
- return ["(session file not readable yet)"];
73
+ return [["·", "(session file not readable yet)"]];
67
74
  }
68
- const lines: string[] = [];
75
+ const lines: [string, string][] = [];
69
76
  // First line of a mid-file read is usually a fragment — drop it.
70
77
  for (const raw of text.split("\n").slice(1)) {
71
78
  const line = eventLine(raw);
@@ -99,20 +106,44 @@ export function createPeekPane(
99
106
 
100
107
  const clamp = (n: number, len: number) => (len === 0 ? 0 : Math.max(0, Math.min(len - 1, n)));
101
108
 
109
+ /**
110
+ * Every row is padded to the full pane width and background-filled, so the
111
+ * transcript underneath never shows through the overlay.
112
+ */
113
+ const row = (content: string, width: number): string => {
114
+ const inner = width - 4; // 2 cols padding each side
115
+ const text = truncateToWidth(content, Math.max(0, inner), "…");
116
+ const pad = Math.max(0, inner - visibleWidth(text));
117
+ return theme.bg("selectedBg", ` ${text}${" ".repeat(pad)} `);
118
+ };
119
+
102
120
  return {
103
121
  render(width: number): string[] {
104
122
  const tasks = getTasks();
105
123
  selected = clamp(selected, tasks.length);
106
- if (tasks.length === 0) return [theme.fg("dim", "No subagents in this session.")];
124
+ if (tasks.length === 0) return [row(theme.fg("dim", "No subagents in this session."), width)];
107
125
  const task = tasks[selected]!;
108
- const hint = confirming ? theme.fg("error", `abort ${task.agent}? y / n`) : tailing ? "esc back · x abort" : "↑↓ move · enter tail · x abort · esc close";
109
- const head = `${theme.fg("accent", theme.bold(tailing ? task.agent : "Subagents"))} ${theme.fg("dim", `(${selected + 1}/${tasks.length}) · ${hint}`)}`;
126
+ const hint = confirming
127
+ ? theme.fg("error", `abort ${task.agent}? y / n`)
128
+ : theme.fg("dim", tailing ? "esc back · x abort" : "shift+↑↓ / jk move · enter tail · x abort · esc close");
129
+ const title = `${theme.fg("accent", theme.bold(tailing ? task.agent : "Subagents"))} ${theme.fg("muted", `${selected + 1}/${tasks.length}`)}`;
130
+ const lines = [row("", width), row(title, width), row(hint, width), row("", width)];
131
+
110
132
  if (!tailing) {
111
- return [head, ...tasks.map((t, i) => truncateToWidth(`${i === selected ? theme.fg("accent", "❯ ") : " "}${t.line}`, width, "…"))];
133
+ for (const [i, t] of tasks.entries()) {
134
+ const marker = i === selected ? theme.fg("accent", "❯ ") : " ";
135
+ lines.push(row(`${marker}${t.line}`, width));
136
+ }
137
+ } else if (!task.sessionFile) {
138
+ lines.push(row(theme.fg("dim", "(no session file — agent has not started yet)"), width));
139
+ } else {
140
+ // ponytail: re-reads the tail each render (700ms poll). A watcher only pays off for files far bigger than a child session.
141
+ for (const [gutter, text] of tailLines(task.sessionFile, TAIL_ROWS)) {
142
+ lines.push(row(`${theme.fg("dim", gutter)} ${gutter === "←" ? theme.fg("dim", text) : text}`, width));
143
+ }
112
144
  }
113
- if (!task.sessionFile) return [head, theme.fg("dim", "(no session file — agent has not started yet)")];
114
- // ponytail: re-reads the tail each render (700ms poll). A watcher only pays off for files far bigger than a child session.
115
- return [head, ...tailLines(task.sessionFile, 18).map((l) => truncateToWidth(` ${l}`, width, "…"))];
145
+ lines.push(row("", width));
146
+ return lines;
116
147
  },
117
148
  handleInput(data: string): void {
118
149
  const tasks = getTasks();
@@ -136,9 +167,10 @@ export function createPeekPane(
136
167
  tailing = true;
137
168
  } else if (matchesKey(data, Key.left)) {
138
169
  tailing = false;
139
- } else if (matchesKey(data, Key.up)) {
170
+ } else if (matchesKey(data, "shift+up") || matchesKey(data, Key.up) || data === "k") {
171
+ // shift+↑↓ and j/k are the reliable pair: bare arrows can be eaten by prompt history.
140
172
  selected = clamp(selected - 1, len);
141
- } else if (matchesKey(data, Key.down)) {
173
+ } else if (matchesKey(data, "shift+down") || matchesKey(data, Key.down) || data === "j") {
142
174
  selected = clamp(selected + 1, len);
143
175
  }
144
176
  requestRender();