@arhen/pi-core-subagent 1.3.44 → 1.3.46
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/package.json +1 -1
- package/src/peek.ts +88 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.46",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
6
|
"license": "MIT",
|
package/src/peek.ts
CHANGED
|
@@ -116,7 +116,8 @@ function eventLines(raw: string): PeekLine[] {
|
|
|
116
116
|
return out;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
/** The whole readable tail, unsliced — the viewport decides what is shown. */
|
|
120
|
+
function tailLines(path: string): PeekLine[] {
|
|
120
121
|
let text: string;
|
|
121
122
|
try {
|
|
122
123
|
text = readTail(path);
|
|
@@ -126,7 +127,7 @@ function tailLines(path: string, max: number): PeekLine[] {
|
|
|
126
127
|
const lines: PeekLine[] = [];
|
|
127
128
|
// First line of a mid-file read is usually a fragment — drop it.
|
|
128
129
|
for (const raw of text.split("\n").slice(1)) lines.push(...eventLines(raw));
|
|
129
|
-
return lines
|
|
130
|
+
return lines;
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
/** Gutter colour carries the line's role, so the eye can skip to failures without
|
|
@@ -172,38 +173,78 @@ export function createPeekPane(
|
|
|
172
173
|
let selected = 0;
|
|
173
174
|
let tailing = false;
|
|
174
175
|
let confirming = false;
|
|
176
|
+
/** Rows scrolled back from the newest line. 0 = pinned to live output.
|
|
177
|
+
* A tail that keeps jumping to the bottom while you read is unusable, so any
|
|
178
|
+
* scroll detaches from live and the 700ms poll stops moving the view. */
|
|
179
|
+
let scrollback = 0;
|
|
180
|
+
/** Content rows the last render could show — page keys need the real viewport,
|
|
181
|
+
* which only render() knows (it is given the width and derives the height). */
|
|
182
|
+
let viewport = TAIL_ROWS;
|
|
183
|
+
/** Tail length at the previous render, to keep a scrolled-back view pinned to
|
|
184
|
+
* the SAME lines as the child appends new ones. Without this the window is
|
|
185
|
+
* anchored to the end of a growing list, so the text you are reading crawls
|
|
186
|
+
* upward every poll. */
|
|
187
|
+
let lastTotal = 0;
|
|
175
188
|
const timer = setInterval(requestRender, POLL_MS);
|
|
176
189
|
|
|
177
190
|
const clamp = (n: number, len: number) => (len === 0 ? 0 : Math.max(0, Math.min(len - 1, n)));
|
|
191
|
+
/** Scrolling past either end is a no-op, never a wrap: the newest line is a
|
|
192
|
+
* hard floor, the oldest readable line a hard ceiling. */
|
|
193
|
+
const scrollBy = (rows: number) => {
|
|
194
|
+
const task = getTasks()[selected];
|
|
195
|
+
if (!tailing || !task?.sessionFile) return;
|
|
196
|
+
const total = tailLines(task.sessionFile).length;
|
|
197
|
+
// Anchor here as well as in render(): a scroll before the next render would
|
|
198
|
+
// otherwise be read as "the file grew by its whole length" and jump the view.
|
|
199
|
+
lastTotal = total;
|
|
200
|
+
scrollback = Math.max(0, Math.min(Math.max(0, total - viewport), scrollback + rows));
|
|
201
|
+
};
|
|
178
202
|
|
|
179
203
|
/**
|
|
180
|
-
* Every row is
|
|
181
|
-
*
|
|
204
|
+
* Every row is a full-width bordered line: │ edge, one space of padding, the
|
|
205
|
+
* text padded out, then the closing edge. Background-filled so the transcript
|
|
206
|
+
* underneath never shows through the overlay, and edged so the pane reads as
|
|
207
|
+
* one object instead of a floating block of colour.
|
|
182
208
|
*/
|
|
183
209
|
const row = (content: string, width: number): string => {
|
|
184
|
-
const inner = width - 4; //
|
|
210
|
+
const inner = width - 4; // │ + space each side
|
|
185
211
|
const text = truncateToWidth(content, Math.max(0, inner), "…");
|
|
186
212
|
const pad = Math.max(0, inner - visibleWidth(text));
|
|
187
|
-
|
|
213
|
+
const edge = theme.fg("border", "│");
|
|
214
|
+
return theme.bg("selectedBg", `${edge} ${text}${" ".repeat(pad)} ${edge}`);
|
|
188
215
|
};
|
|
189
216
|
|
|
217
|
+
/** Top/bottom cap, and the ├───┤ divider between chrome and content. */
|
|
218
|
+
const edgeRow = (width: number, left: string, right: string): string =>
|
|
219
|
+
theme.bg("selectedBg", theme.fg("border", `${left}${"─".repeat(Math.max(0, width - 2))}${right}`));
|
|
220
|
+
|
|
190
221
|
return {
|
|
191
222
|
render(width: number): string[] {
|
|
192
223
|
const tasks = getTasks();
|
|
193
224
|
selected = clamp(selected, tasks.length);
|
|
194
|
-
if (tasks.length === 0)
|
|
225
|
+
if (tasks.length === 0) {
|
|
226
|
+
return [
|
|
227
|
+
edgeRow(width, "╭", "╮"),
|
|
228
|
+
row(theme.fg("dim", "No subagents in this session."), width),
|
|
229
|
+
edgeRow(width, "╰", "╯"),
|
|
230
|
+
];
|
|
231
|
+
}
|
|
195
232
|
const task = tasks[selected]!;
|
|
196
233
|
const hint = confirming
|
|
197
234
|
? theme.fg("error", `abort ${task.agent}? y / n`)
|
|
198
|
-
: theme.fg(
|
|
235
|
+
: theme.fg(
|
|
236
|
+
"dim",
|
|
237
|
+
tailing
|
|
238
|
+
? "↑↓ / jk scroll · ⇧ page · g/G top·live · esc back · x abort"
|
|
239
|
+
: "shift+↑↓ / jk move · enter tail · x abort · esc close",
|
|
240
|
+
);
|
|
199
241
|
// Breadcrumb, not a bare name: while tailing, WHERE you are is the thing you
|
|
200
242
|
// lose track of first, and the child's status belongs next to its name.
|
|
201
243
|
const crumb = tailing
|
|
202
244
|
? `${theme.fg("muted", "Subagents")}${theme.fg("dim", " › ")}${theme.fg("accent", theme.bold(task.agent))} ${statusTag(task.status, theme)}`
|
|
203
245
|
: theme.fg("accent", theme.bold("Subagents"));
|
|
204
246
|
const title = `${crumb} ${theme.fg("muted", `${selected + 1}/${tasks.length}`)}`;
|
|
205
|
-
const
|
|
206
|
-
const lines = [row("", width), row(title, width), row(hint, width), row(rule, width)];
|
|
247
|
+
const lines = [edgeRow(width, "╭", "╮"), row(title, width), row(hint, width), edgeRow(width, "├", "┤")];
|
|
207
248
|
|
|
208
249
|
if (!tailing) {
|
|
209
250
|
for (const [i, t] of tasks.entries()) {
|
|
@@ -214,11 +255,25 @@ export function createPeekPane(
|
|
|
214
255
|
lines.push(row(theme.fg("dim", "(no session file — agent has not started yet)"), width));
|
|
215
256
|
} else {
|
|
216
257
|
// ponytail: re-reads the tail each render (700ms poll). A watcher only pays off for files far bigger than a child session.
|
|
217
|
-
const tail = tailLines(task.sessionFile
|
|
218
|
-
|
|
219
|
-
|
|
258
|
+
const tail = tailLines(task.sessionFile);
|
|
259
|
+
viewport = TAIL_ROWS;
|
|
260
|
+
if (scrollback > 0 && lastTotal > 0 && tail.length > lastTotal) scrollback += tail.length - lastTotal;
|
|
261
|
+
lastTotal = tail.length;
|
|
262
|
+
// Clamp here too: the readable tail is a sliding 64KB window, so old lines
|
|
263
|
+
// fall off the front and a deep scrollback would render a blank window.
|
|
264
|
+
scrollback = Math.max(0, Math.min(Math.max(0, tail.length - viewport), scrollback));
|
|
265
|
+
const end = tail.length - scrollback;
|
|
266
|
+
const window = tail.slice(Math.max(0, end - viewport), end);
|
|
267
|
+
if (window.length === 0) lines.push(row(theme.fg("dim", "(no activity yet)"), width));
|
|
268
|
+
for (const line of window) lines.push(row(renderLine(line, theme), width));
|
|
269
|
+
// Scrolled-back views must say so: an unmoving tail is otherwise
|
|
270
|
+
// indistinguishable from a stalled child.
|
|
271
|
+
if (scrollback > 0) {
|
|
272
|
+
lines.push(edgeRow(width, "├", "┤"));
|
|
273
|
+
lines.push(row(theme.fg("warning", `↑ ${scrollback} older · G / end → live`), width));
|
|
274
|
+
}
|
|
220
275
|
}
|
|
221
|
-
lines.push(
|
|
276
|
+
lines.push(edgeRow(width, "╰", "╯"));
|
|
222
277
|
return lines;
|
|
223
278
|
},
|
|
224
279
|
handleInput(data: string): void {
|
|
@@ -237,17 +292,32 @@ export function createPeekPane(
|
|
|
237
292
|
if (data === "x" || data === "X") {
|
|
238
293
|
if (tasks[selected]?.running) confirming = true;
|
|
239
294
|
} else if (matchesKey(data, Key.escape)) {
|
|
240
|
-
if (tailing)
|
|
241
|
-
|
|
295
|
+
if (tailing) {
|
|
296
|
+
tailing = false;
|
|
297
|
+
scrollback = 0; // leaving the tail forgets where you were reading
|
|
298
|
+
} else close();
|
|
242
299
|
} else if (matchesKey(data, Key.enter) || matchesKey(data, Key.right)) {
|
|
243
300
|
tailing = true;
|
|
301
|
+
scrollback = 0; // a freshly opened tail always starts live
|
|
244
302
|
} else if (matchesKey(data, Key.left)) {
|
|
245
303
|
tailing = false;
|
|
304
|
+
scrollback = 0;
|
|
305
|
+
} else if (matchesKey(data, Key.pageUp)) {
|
|
306
|
+
scrollBy(viewport);
|
|
307
|
+
} else if (matchesKey(data, Key.pageDown)) {
|
|
308
|
+
scrollBy(-viewport);
|
|
309
|
+
} else if (data === "g") {
|
|
310
|
+
scrollBy(Number.MAX_SAFE_INTEGER); // oldest readable line
|
|
311
|
+
} else if (data === "G" || matchesKey(data, Key.end)) {
|
|
312
|
+
scrollback = 0; // back to live
|
|
246
313
|
} else if (matchesKey(data, "shift+up") || matchesKey(data, Key.up) || data === "k") {
|
|
314
|
+
// In the tail the same keys scroll; in the list they move between agents.
|
|
247
315
|
// shift+↑↓ and j/k are the reliable pair: bare arrows can be eaten by prompt history.
|
|
248
|
-
|
|
316
|
+
if (tailing) scrollBy(1);
|
|
317
|
+
else selected = clamp(selected - 1, len);
|
|
249
318
|
} else if (matchesKey(data, "shift+down") || matchesKey(data, Key.down) || data === "j") {
|
|
250
|
-
|
|
319
|
+
if (tailing) scrollBy(-1);
|
|
320
|
+
else selected = clamp(selected + 1, len);
|
|
251
321
|
}
|
|
252
322
|
requestRender();
|
|
253
323
|
},
|