@arhen/pi-core-subagent 1.3.45 → 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 +68 -10
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,9 +173,32 @@ 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
204
|
* Every row is a full-width bordered line: │ edge, one space of padding, the
|
|
@@ -208,7 +232,12 @@ export function createPeekPane(
|
|
|
208
232
|
const task = tasks[selected]!;
|
|
209
233
|
const hint = confirming
|
|
210
234
|
? theme.fg("error", `abort ${task.agent}? y / n`)
|
|
211
|
-
: 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
|
+
);
|
|
212
241
|
// Breadcrumb, not a bare name: while tailing, WHERE you are is the thing you
|
|
213
242
|
// lose track of first, and the child's status belongs next to its name.
|
|
214
243
|
const crumb = tailing
|
|
@@ -226,9 +255,23 @@ export function createPeekPane(
|
|
|
226
255
|
lines.push(row(theme.fg("dim", "(no session file — agent has not started yet)"), width));
|
|
227
256
|
} else {
|
|
228
257
|
// ponytail: re-reads the tail each render (700ms poll). A watcher only pays off for files far bigger than a child session.
|
|
229
|
-
const tail = tailLines(task.sessionFile
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
}
|
|
232
275
|
}
|
|
233
276
|
lines.push(edgeRow(width, "╰", "╯"));
|
|
234
277
|
return lines;
|
|
@@ -249,17 +292,32 @@ export function createPeekPane(
|
|
|
249
292
|
if (data === "x" || data === "X") {
|
|
250
293
|
if (tasks[selected]?.running) confirming = true;
|
|
251
294
|
} else if (matchesKey(data, Key.escape)) {
|
|
252
|
-
if (tailing)
|
|
253
|
-
|
|
295
|
+
if (tailing) {
|
|
296
|
+
tailing = false;
|
|
297
|
+
scrollback = 0; // leaving the tail forgets where you were reading
|
|
298
|
+
} else close();
|
|
254
299
|
} else if (matchesKey(data, Key.enter) || matchesKey(data, Key.right)) {
|
|
255
300
|
tailing = true;
|
|
301
|
+
scrollback = 0; // a freshly opened tail always starts live
|
|
256
302
|
} else if (matchesKey(data, Key.left)) {
|
|
257
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
|
|
258
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.
|
|
259
315
|
// shift+↑↓ and j/k are the reliable pair: bare arrows can be eaten by prompt history.
|
|
260
|
-
|
|
316
|
+
if (tailing) scrollBy(1);
|
|
317
|
+
else selected = clamp(selected - 1, len);
|
|
261
318
|
} else if (matchesKey(data, "shift+down") || matchesKey(data, Key.down) || data === "j") {
|
|
262
|
-
|
|
319
|
+
if (tailing) scrollBy(-1);
|
|
320
|
+
else selected = clamp(selected + 1, len);
|
|
263
321
|
}
|
|
264
322
|
requestRender();
|
|
265
323
|
},
|