@esso0428/pi-subagents 0.15.17 → 0.17.0
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 +17 -0
- package/README.md +10 -12
- package/dist/agent-history-list.d.ts.map +1 -1
- package/dist/agent-history-list.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -40
- package/dist/index.js.map +1 -1
- package/dist/output-file.js +3 -3
- package/dist/output-file.js.map +1 -1
- package/dist/settings.d.ts +1 -8
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +0 -5
- package/dist/settings.js.map +1 -1
- package/dist/ui/agent-widget.d.ts +66 -28
- package/dist/ui/agent-widget.d.ts.map +1 -1
- package/dist/ui/agent-widget.js +325 -134
- package/dist/ui/agent-widget.js.map +1 -1
- package/dist/ui/ccstyle/diff/ansi-utils.js +2 -2
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -1
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js +0 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.js +15 -10
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -1
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/line-width-safety.js +1 -2
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -1
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts +19 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.js +85 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.js.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts +1 -1
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-renderer.js +1 -1
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -1
- package/dist/ui/ccstyle/tool-result.d.ts +9 -1
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-result.js +137 -5
- package/dist/ui/ccstyle/tool-result.js.map +1 -1
- package/dist/ui/conversation-blocks.d.ts.map +1 -1
- package/dist/ui/conversation-blocks.js +14 -13
- package/dist/ui/conversation-blocks.js.map +1 -1
- package/dist/ui/conversation-role.d.ts +1 -1
- package/dist/ui/conversation-role.d.ts.map +1 -1
- package/dist/ui/conversation-search.js +4 -4
- package/dist/ui/conversation-search.js.map +1 -1
- package/dist/ui/conversation-timeline.d.ts +17 -1
- package/dist/ui/conversation-timeline.d.ts.map +1 -1
- package/dist/ui/conversation-timeline.js +116 -43
- package/dist/ui/conversation-timeline.js.map +1 -1
- package/dist/ui/conversation-viewer.d.ts +5 -1
- package/dist/ui/conversation-viewer.d.ts.map +1 -1
- package/dist/ui/conversation-viewer.js +67 -26
- package/dist/ui/conversation-viewer.js.map +1 -1
- package/dist/write-execution.d.ts.map +1 -1
- package/dist/write-execution.js +1 -1
- package/dist/write-execution.js.map +1 -1
- package/package.json +6 -5
- package/src/agent-history-list.ts +1 -1
- package/src/index.ts +19 -73
- package/src/output-file.ts +3 -3
- package/src/settings.ts +1 -12
- package/src/ui/agent-widget.ts +264 -114
- package/src/ui/ccstyle/diff/ansi-utils.ts +2 -2
- package/src/ui/ccstyle/diff/diff-presentation.ts +0 -1
- package/src/ui/ccstyle/diff/diff-renderer.ts +22 -21
- package/src/ui/ccstyle/diff/line-width-safety.ts +1 -2
- package/src/ui/ccstyle/diff/shiki-highlight.ts +97 -0
- package/src/ui/ccstyle/tool-renderer.ts +3 -3
- package/src/ui/ccstyle/tool-result.ts +175 -7
- package/src/ui/conversation-role.ts +1 -1
- package/src/ui/conversation-search.ts +4 -4
- package/src/ui/conversation-timeline.ts +7 -7
- package/src/ui/conversation-viewer.ts +16 -7
- package/src/write-execution.ts +2 -2
- package/dist/ui/fleet-list.d.ts +0 -122
- package/dist/ui/fleet-list.d.ts.map +0 -1
- package/dist/ui/fleet-list.js +0 -375
- package/dist/ui/fleet-list.js.map +0 -1
- package/src/ui/deferred-ui-refresh.ts +0 -72
- package/src/ui/fleet-list.ts +0 -585
- package/test/agent-history-list.test.ts +0 -121
- package/test/agent-manager-history.test.ts +0 -88
- package/test/agent-menu-history-navigation.test.ts +0 -86
- package/test/output-file-history.test.ts +0 -50
- package/test/ui/agent-widget.test.ts +0 -172
- package/test/ui/conversation-blocks.test.ts +0 -103
- package/test/ui/conversation-diff.test.ts +0 -91
- package/test/ui/conversation-search.test.ts +0 -55
- package/test/ui/conversation-viewer.test.ts +0 -438
- package/test/ui/deferred-ui-refresh.test.ts +0 -55
- package/test/ui/fleet-list.test.ts +0 -389
- package/test/write-execution.test.ts +0 -83
- package/tsconfig.json +0 -16
package/dist/ui/agent-widget.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Displays a tree of agents with animated spinners, live stats, and activity descriptions.
|
|
5
5
|
* Uses the callback form of setWidget for themed rendering.
|
|
6
6
|
*/
|
|
7
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
7
|
+
import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
8
8
|
import { getConfig } from "../agent-types.js";
|
|
9
9
|
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
10
10
|
// ---- Constants ----
|
|
@@ -155,29 +155,35 @@ export class AgentWidget {
|
|
|
155
155
|
manager;
|
|
156
156
|
agentActivity;
|
|
157
157
|
mode;
|
|
158
|
+
options;
|
|
158
159
|
uiCtx;
|
|
159
160
|
widgetFrame = 0;
|
|
160
161
|
widgetInterval;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
inputUnsub;
|
|
163
|
+
/** Whether arrow keys currently navigate the agent roster. */
|
|
164
|
+
navigationActive = false;
|
|
165
|
+
/** Stable identity of the selected row, so roster changes do not jump selection. */
|
|
166
|
+
selectedAgentId;
|
|
167
|
+
/** First logical row currently represented by the bounded viewport. */
|
|
168
|
+
viewportStart = 0;
|
|
165
169
|
/** Whether the widget callback is currently registered with the TUI. */
|
|
166
170
|
widgetRegistered = false;
|
|
167
171
|
/** Cached TUI reference from widget factory callback, used for requestRender(). */
|
|
168
172
|
tui;
|
|
169
173
|
/** Last status bar text, used to avoid redundant setStatus calls. */
|
|
170
174
|
lastStatusText;
|
|
175
|
+
/** Snapshot of the state used for the last widget registration/render request. */
|
|
176
|
+
lastRenderKey;
|
|
171
177
|
constructor(manager, agentActivity,
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
mode = () => "all") {
|
|
178
|
+
/** Read live at render time. Selects which agents the widget shows. */
|
|
179
|
+
mode = () => "all", options = {
|
|
180
|
+
canOpenHistory: (record) => record.session !== undefined || record.completedAt !== undefined,
|
|
181
|
+
onOpen: () => { },
|
|
182
|
+
}) {
|
|
178
183
|
this.manager = manager;
|
|
179
184
|
this.agentActivity = agentActivity;
|
|
180
185
|
this.mode = mode;
|
|
186
|
+
this.options = options;
|
|
181
187
|
}
|
|
182
188
|
/**
|
|
183
189
|
* Agents eligible for the widget, per the current `WidgetMode`:
|
|
@@ -200,44 +206,160 @@ export class AgentWidget {
|
|
|
200
206
|
}
|
|
201
207
|
/** Set the UI context (grabbed from first tool execution). */
|
|
202
208
|
setUICtx(ctx) {
|
|
203
|
-
if (ctx
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
if (ctx === this.uiCtx)
|
|
210
|
+
return false;
|
|
211
|
+
// UICtx changed — the widget and input handler registered on the old
|
|
212
|
+
// context are gone. Re-register both on the next update().
|
|
213
|
+
this.inputUnsub?.();
|
|
214
|
+
this.inputUnsub = undefined;
|
|
215
|
+
this.uiCtx = ctx;
|
|
216
|
+
this.widgetRegistered = false;
|
|
217
|
+
this.tui = undefined;
|
|
218
|
+
this.lastStatusText = undefined;
|
|
219
|
+
this.lastRenderKey = undefined;
|
|
220
|
+
this.navigationActive = false;
|
|
221
|
+
this.selectedAgentId = undefined;
|
|
222
|
+
this.viewportStart = 0;
|
|
223
|
+
// Print/RPC tests and lightweight embedders may provide only the widget
|
|
224
|
+
// surface; real interactive contexts always implement this hook.
|
|
225
|
+
if (typeof ctx.onTerminalInput === "function") {
|
|
226
|
+
this.inputUnsub = ctx.onTerminalInput(data => this.handleKey(data));
|
|
210
227
|
}
|
|
228
|
+
return true;
|
|
211
229
|
}
|
|
212
|
-
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
230
|
+
/** Request a render on the currently registered TUI without touching input. */
|
|
231
|
+
requestUiRefresh(force = true) {
|
|
232
|
+
if (!this.tui)
|
|
233
|
+
return false;
|
|
234
|
+
this.tui.requestRender(force);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
/** Called on each new turn (tool_execution_start). */
|
|
216
238
|
onTurnStart() {
|
|
217
|
-
// Age all finished agents
|
|
218
|
-
for (const [id, age] of this.finishedTurnAge) {
|
|
219
|
-
this.finishedTurnAge.set(id, age + 1);
|
|
220
|
-
}
|
|
221
|
-
// Trigger a widget refresh (will filter out expired agents)
|
|
222
239
|
this.update();
|
|
223
240
|
}
|
|
224
|
-
/**
|
|
241
|
+
/** Keep the spinner/elapsed-time timer alive only while a visible agent runs. */
|
|
225
242
|
ensureTimer() {
|
|
243
|
+
if (!this.uiCtx || !this.widgetAgents().some(a => a.status === "running"))
|
|
244
|
+
return;
|
|
226
245
|
if (!this.widgetInterval) {
|
|
227
|
-
this.widgetInterval = setInterval(() => this.update(),
|
|
246
|
+
this.widgetInterval = setInterval(() => this.update(true), 250);
|
|
228
247
|
}
|
|
229
248
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
249
|
+
syncTimer(shouldRun) {
|
|
250
|
+
if (shouldRun) {
|
|
251
|
+
this.ensureTimer();
|
|
252
|
+
}
|
|
253
|
+
else if (this.widgetInterval) {
|
|
254
|
+
clearInterval(this.widgetInterval);
|
|
255
|
+
this.widgetInterval = undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Retained for the lifecycle call sites. Terminal visibility is determined
|
|
260
|
+
* by the manager record and the history capability, not a one-turn timer.
|
|
261
|
+
*/
|
|
262
|
+
markFinished(_agentId) { }
|
|
263
|
+
/**
|
|
264
|
+
* Records represented by selectable rows in the above-editor widget.
|
|
265
|
+
*
|
|
266
|
+
* Keep this order as the single source of truth for both rendering and key
|
|
267
|
+
* navigation. `listAgents()` is newest-first; active rows come first so the
|
|
268
|
+
* panel exposes currently useful work before terminal history.
|
|
269
|
+
*/
|
|
270
|
+
roster() {
|
|
271
|
+
const agents = this.widgetAgents();
|
|
272
|
+
const finished = agents.filter(record => record.status !== "running" && record.status !== "queued"
|
|
273
|
+
&& record.completedAt !== undefined
|
|
274
|
+
&& this.options.canOpenHistory(record));
|
|
275
|
+
const running = agents.filter(record => record.status === "running");
|
|
276
|
+
const queued = agents.filter(record => record.status === "queued");
|
|
277
|
+
return [...running, ...queued, ...finished];
|
|
235
278
|
}
|
|
236
|
-
/**
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
279
|
+
/** True when pi's prompt editor owns the keyboard. */
|
|
280
|
+
editorHasFocus() {
|
|
281
|
+
const focused = this.tui?.focusedComponent;
|
|
282
|
+
return focused == null || focused instanceof Editor;
|
|
283
|
+
}
|
|
284
|
+
selectedIndexOf(records) {
|
|
285
|
+
if (!this.selectedAgentId)
|
|
286
|
+
return -1;
|
|
287
|
+
return records.findIndex(record => record.id === this.selectedAgentId);
|
|
288
|
+
}
|
|
289
|
+
deactivate() {
|
|
290
|
+
this.navigationActive = false;
|
|
291
|
+
this.selectedAgentId = undefined;
|
|
292
|
+
this.viewportStart = 0;
|
|
293
|
+
this.update();
|
|
294
|
+
}
|
|
295
|
+
/** Move the selected row, activating only from an empty focused editor. */
|
|
296
|
+
moveSelection(direction) {
|
|
297
|
+
const records = this.roster();
|
|
298
|
+
const ui = this.uiCtx;
|
|
299
|
+
if (records.length === 0 || !ui)
|
|
300
|
+
return false;
|
|
301
|
+
if (!this.navigationActive) {
|
|
302
|
+
if (direction !== 1 || !this.editorHasFocus() || (ui.getEditorText?.() ?? "") !== "")
|
|
303
|
+
return false;
|
|
304
|
+
this.navigationActive = true;
|
|
305
|
+
this.selectedAgentId = records[0].id;
|
|
306
|
+
this.viewportStart = 0;
|
|
307
|
+
this.update();
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
const currentIndex = Math.max(0, this.selectedIndexOf(records));
|
|
311
|
+
if (direction === -1 && currentIndex === 0) {
|
|
312
|
+
this.deactivate();
|
|
313
|
+
return true;
|
|
240
314
|
}
|
|
315
|
+
const nextIndex = Math.max(0, Math.min(records.length - 1, currentIndex + direction));
|
|
316
|
+
this.selectedAgentId = records[nextIndex].id;
|
|
317
|
+
this.update();
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
openSelected() {
|
|
321
|
+
const record = this.roster().find(candidate => candidate.id === this.selectedAgentId);
|
|
322
|
+
this.deactivate();
|
|
323
|
+
if (!record)
|
|
324
|
+
return;
|
|
325
|
+
const mode = record.status === "running" || record.status === "queued" ? "live" : "history";
|
|
326
|
+
void this.options.onOpen(record, mode);
|
|
327
|
+
}
|
|
328
|
+
/** Handle terminal input before it reaches the focused prompt editor. */
|
|
329
|
+
handleKey(data) {
|
|
330
|
+
if (!this.uiCtx || isKeyRelease(data))
|
|
331
|
+
return undefined;
|
|
332
|
+
if (!this.editorHasFocus()) {
|
|
333
|
+
if (this.navigationActive)
|
|
334
|
+
this.deactivate();
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
if (!this.navigationActive) {
|
|
338
|
+
if (!matchesKey(data, "down") || (this.uiCtx.getEditorText?.() ?? "") !== "" || this.roster().length === 0) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
this.navigationActive = true;
|
|
342
|
+
this.selectedAgentId = this.roster()[0]?.id;
|
|
343
|
+
this.viewportStart = 0;
|
|
344
|
+
this.update();
|
|
345
|
+
return { consume: true };
|
|
346
|
+
}
|
|
347
|
+
if (matchesKey(data, "escape")) {
|
|
348
|
+
this.deactivate();
|
|
349
|
+
return { consume: true };
|
|
350
|
+
}
|
|
351
|
+
if (matchesKey(data, "up"))
|
|
352
|
+
return this.moveSelection(-1) ? { consume: true } : undefined;
|
|
353
|
+
if (matchesKey(data, "down"))
|
|
354
|
+
return this.moveSelection(1) ? { consume: true } : undefined;
|
|
355
|
+
if (matchesKey(data, Key.enter)) {
|
|
356
|
+
this.openSelected();
|
|
357
|
+
return { consume: true };
|
|
358
|
+
}
|
|
359
|
+
// Only ↑/↓ navigate. Other keys, including j/k/←/→, flow to the editor
|
|
360
|
+
// and leave navigation mode.
|
|
361
|
+
this.deactivate();
|
|
362
|
+
return undefined;
|
|
241
363
|
}
|
|
242
364
|
/** Render a finished agent line. */
|
|
243
365
|
renderFinishedLine(a, theme) {
|
|
@@ -286,8 +408,9 @@ export class AgentWidget {
|
|
|
286
408
|
const allAgents = this.widgetAgents();
|
|
287
409
|
const running = allAgents.filter(a => a.status === "running");
|
|
288
410
|
const queued = allAgents.filter(a => a.status === "queued");
|
|
289
|
-
const finished = allAgents.filter(a => a.status !== "running" && a.status !== "queued" && a.completedAt
|
|
290
|
-
&& this.
|
|
411
|
+
const finished = allAgents.filter(a => a.status !== "running" && a.status !== "queued" && a.completedAt !== undefined
|
|
412
|
+
&& this.options.canOpenHistory(a));
|
|
413
|
+
const selectedId = this.navigationActive ? this.selectedAgentId : undefined;
|
|
291
414
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
292
415
|
const hasFinished = finished.length > 0;
|
|
293
416
|
// Nothing to show — return empty (widget will be unregistered by update())
|
|
@@ -305,7 +428,11 @@ export class AgentWidget {
|
|
|
305
428
|
// Each running agent = 2 lines (header + activity), finished = 1 line, queued = 1 line.
|
|
306
429
|
const finishedLines = [];
|
|
307
430
|
for (const a of finished) {
|
|
308
|
-
|
|
431
|
+
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
432
|
+
finishedLines.push({
|
|
433
|
+
record: a,
|
|
434
|
+
lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ` + this.renderFinishedLine(a, theme))],
|
|
435
|
+
});
|
|
309
436
|
}
|
|
310
437
|
const runningLines = []; // each entry is [header, activity]
|
|
311
438
|
for (const a of running) {
|
|
@@ -328,115 +455,157 @@ export class AgentWidget {
|
|
|
328
455
|
parts.push(elapsed);
|
|
329
456
|
const statsText = parts.join(" · ");
|
|
330
457
|
const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
458
|
+
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
459
|
+
runningLines.push({
|
|
460
|
+
record: a,
|
|
461
|
+
lines: [
|
|
462
|
+
truncate(theme.fg("dim", "├─") + ` ${marker} ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", truncateLine(a.description))} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}`),
|
|
463
|
+
truncate(theme.fg("dim", "│ ") + ` ${theme.fg("dim", `⎿ ${activity}`)}`),
|
|
464
|
+
],
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
const queuedLines = queued.map(a => {
|
|
468
|
+
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
469
|
+
return {
|
|
470
|
+
record: a,
|
|
471
|
+
lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${getDisplayName(a.type)} ${truncateLine(a.description)} · queued`)}`)],
|
|
472
|
+
};
|
|
473
|
+
});
|
|
339
474
|
// Assemble with a responsive cap (heading + overflow indicator = 2
|
|
340
475
|
// reserved lines when content exceeds the available body budget).
|
|
341
476
|
const maxBody = maxLines - 1; // heading takes 1 line
|
|
342
|
-
const
|
|
343
|
-
const
|
|
477
|
+
const rows = [...runningLines, ...queuedLines, ...finishedLines];
|
|
478
|
+
const totalBody = rows.reduce((total, row) => total + row.lines.length, 0);
|
|
479
|
+
const heading = "Agents ↑↓ select · enter view · esc back";
|
|
480
|
+
const lines = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, heading))];
|
|
344
481
|
if (maxLines === 1) {
|
|
345
482
|
// There is room only for the heading; do not consume the editor/input row.
|
|
346
483
|
return lines;
|
|
347
484
|
}
|
|
348
485
|
if (totalBody <= maxBody) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
lines.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
lines[last] = lines[last].replace("├─", "└─");
|
|
359
|
-
// If last item is a running agent activity line, fix indent of that line
|
|
360
|
-
// and fix the header line above it.
|
|
361
|
-
if (runningLines.length > 0 && !queuedLine) {
|
|
362
|
-
// The last two lines are the last running agent's header + activity.
|
|
363
|
-
if (last >= 2) {
|
|
364
|
-
lines[last - 1] = lines[last - 1].replace("├─", "└─");
|
|
365
|
-
lines[last] = lines[last].replace("│ ", " ");
|
|
366
|
-
}
|
|
486
|
+
this.viewportStart = 0;
|
|
487
|
+
for (const row of rows)
|
|
488
|
+
lines.push(...row.lines);
|
|
489
|
+
if (rows.length > 0) {
|
|
490
|
+
const lastRow = rows[rows.length - 1];
|
|
491
|
+
const lastStart = lines.length - lastRow.lines.length;
|
|
492
|
+
lines[lastStart] = lines[lastStart].replace("├─", "└─");
|
|
493
|
+
if (lastRow.lines.length === 2) {
|
|
494
|
+
lines[lastStart + 1] = lines[lastStart + 1].replace("│ ", " ");
|
|
367
495
|
}
|
|
368
496
|
}
|
|
369
497
|
}
|
|
370
498
|
else {
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
budget -= 2;
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
hiddenRunning++;
|
|
499
|
+
// Reserve one line for a directional overflow summary. The viewport is
|
|
500
|
+
// a contiguous slice in roster order, so the same slice is navigable and
|
|
501
|
+
// renderable even when the selected row is currently hidden.
|
|
502
|
+
const contentBudget = Math.max(0, maxBody - 1);
|
|
503
|
+
const heightAt = (index) => rows[index]?.lines.length ?? 0;
|
|
504
|
+
const endFor = (start) => {
|
|
505
|
+
let used = 0;
|
|
506
|
+
let end = start;
|
|
507
|
+
while (end < rows.length && used + heightAt(end) <= contentBudget) {
|
|
508
|
+
used += heightAt(end++);
|
|
385
509
|
}
|
|
510
|
+
return end;
|
|
511
|
+
};
|
|
512
|
+
let start = Math.max(0, Math.min(this.viewportStart, Math.max(0, rows.length - 1)));
|
|
513
|
+
const selectedIndex = this.navigationActive && selectedId
|
|
514
|
+
? rows.findIndex(row => row.record.id === selectedId)
|
|
515
|
+
: -1;
|
|
516
|
+
if (selectedIndex >= 0) {
|
|
517
|
+
if (selectedIndex < start)
|
|
518
|
+
start = selectedIndex;
|
|
519
|
+
if (selectedIndex >= endFor(start))
|
|
520
|
+
start = selectedIndex;
|
|
386
521
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
hiddenQueued = 1;
|
|
395
|
-
}
|
|
522
|
+
this.viewportStart = start;
|
|
523
|
+
let used = 0;
|
|
524
|
+
let end = start;
|
|
525
|
+
while (end < rows.length && used + heightAt(end) <= contentBudget) {
|
|
526
|
+
lines.push(...rows[end].lines);
|
|
527
|
+
used += heightAt(end);
|
|
528
|
+
end++;
|
|
396
529
|
}
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
hiddenFinished++;
|
|
405
|
-
}
|
|
530
|
+
// A selected two-line row must remain addressable even if only one body
|
|
531
|
+
// line is available. Showing its header is preferable to hiding it.
|
|
532
|
+
if (end === start && rows[start] && contentBudget > 0) {
|
|
533
|
+
lines.push(rows[start].lines[0]);
|
|
534
|
+
end = start + 1;
|
|
406
535
|
}
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
536
|
+
const hiddenBefore = start;
|
|
537
|
+
const hiddenAfter = Math.max(0, rows.length - end);
|
|
538
|
+
const hidden = hiddenBefore + hiddenAfter;
|
|
539
|
+
const hiddenRows = rows.filter((_row, index) => index < start || index >= end);
|
|
540
|
+
const categoryCounts = [
|
|
541
|
+
["running", hiddenRows.filter(row => row.record.status === "running").length],
|
|
542
|
+
["queued", hiddenRows.filter(row => row.record.status === "queued").length],
|
|
543
|
+
["finished", hiddenRows.filter(row => row.record.status !== "running" && row.record.status !== "queued").length],
|
|
544
|
+
].filter(([, count]) => count > 0).map(([label, count]) => `${count} ${label}`);
|
|
545
|
+
const direction = [
|
|
546
|
+
...(hiddenBefore > 0 ? [`↑ ${hiddenBefore} more`] : []),
|
|
547
|
+
...(hiddenAfter > 0 ? [`↓ ${hiddenAfter} more`] : []),
|
|
548
|
+
].join(" · ");
|
|
549
|
+
const summary = `+${hidden} more (${direction}${categoryCounts.length > 0 ? `; ${categoryCounts.join(", ")}` : ""})`;
|
|
550
|
+
lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", summary)}`));
|
|
417
551
|
}
|
|
418
552
|
return lines;
|
|
419
553
|
}
|
|
420
|
-
/**
|
|
421
|
-
|
|
554
|
+
/** Build a render-relevant snapshot without capturing mutable records. */
|
|
555
|
+
renderKey(allAgents) {
|
|
556
|
+
const activities = allAgents.map(a => {
|
|
557
|
+
const activity = this.agentActivity.get(a.id);
|
|
558
|
+
return activity ? {
|
|
559
|
+
id: a.id,
|
|
560
|
+
activeTools: [...activity.activeTools.entries()],
|
|
561
|
+
toolUses: activity.toolUses,
|
|
562
|
+
responseText: activity.responseText,
|
|
563
|
+
turnCount: activity.turnCount,
|
|
564
|
+
maxTurns: activity.maxTurns,
|
|
565
|
+
lifetimeUsage: activity.lifetimeUsage,
|
|
566
|
+
contextPercent: getSessionContextPercent(activity.session),
|
|
567
|
+
} : undefined;
|
|
568
|
+
});
|
|
569
|
+
return JSON.stringify({
|
|
570
|
+
frame: this.widgetFrame,
|
|
571
|
+
agents: allAgents.map(a => ({
|
|
572
|
+
id: a.id,
|
|
573
|
+
type: a.type,
|
|
574
|
+
description: a.description,
|
|
575
|
+
status: a.status,
|
|
576
|
+
toolUses: a.toolUses,
|
|
577
|
+
startedAt: a.startedAt,
|
|
578
|
+
completedAt: a.completedAt,
|
|
579
|
+
error: a.error,
|
|
580
|
+
lifetimeUsage: a.lifetimeUsage,
|
|
581
|
+
compactionCount: a.compactionCount,
|
|
582
|
+
isBackground: a.isBackground,
|
|
583
|
+
hasSession: a.session !== undefined,
|
|
584
|
+
transcriptPath: a.transcriptPath,
|
|
585
|
+
openableHistory: this.options.canOpenHistory(a),
|
|
586
|
+
})),
|
|
587
|
+
activities,
|
|
588
|
+
navigationActive: this.navigationActive,
|
|
589
|
+
selectedAgentId: this.selectedAgentId,
|
|
590
|
+
viewportStart: this.viewportStart,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
/** Force an immediate widget update. `advanceSpinner` is reserved for the timer. */
|
|
594
|
+
update(advanceSpinner = false) {
|
|
422
595
|
if (!this.uiCtx)
|
|
423
596
|
return;
|
|
424
597
|
const allAgents = this.widgetAgents();
|
|
598
|
+
const roster = this.roster();
|
|
425
599
|
// Lightweight existence checks — full categorization happens in renderWidget()
|
|
426
600
|
let runningCount = 0;
|
|
427
601
|
let queuedCount = 0;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (a.status === "running") {
|
|
602
|
+
for (const a of roster) {
|
|
603
|
+
if (a.status === "running")
|
|
431
604
|
runningCount++;
|
|
432
|
-
|
|
433
|
-
else if (a.status === "queued") {
|
|
605
|
+
else if (a.status === "queued")
|
|
434
606
|
queuedCount++;
|
|
435
|
-
}
|
|
436
|
-
else if (a.completedAt && this.shouldShowFinished(a.id, a.status)) {
|
|
437
|
-
hasFinished = true;
|
|
438
|
-
}
|
|
439
607
|
}
|
|
608
|
+
const hasFinished = roster.some(a => a.status !== "running" && a.status !== "queued");
|
|
440
609
|
const hasActive = runningCount > 0 || queuedCount > 0;
|
|
441
610
|
// Nothing to show — clear widget
|
|
442
611
|
if (!hasActive && !hasFinished) {
|
|
@@ -449,15 +618,11 @@ export class AgentWidget {
|
|
|
449
618
|
this.uiCtx.setStatus("subagents", undefined);
|
|
450
619
|
this.lastStatusText = undefined;
|
|
451
620
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
for (const [id] of this.finishedTurnAge) {
|
|
458
|
-
if (!allAgents.some(a => a.id === id))
|
|
459
|
-
this.finishedTurnAge.delete(id);
|
|
460
|
-
}
|
|
621
|
+
this.lastRenderKey = undefined;
|
|
622
|
+
this.navigationActive = false;
|
|
623
|
+
this.selectedAgentId = undefined;
|
|
624
|
+
this.viewportStart = 0;
|
|
625
|
+
this.syncTimer(false);
|
|
461
626
|
return;
|
|
462
627
|
}
|
|
463
628
|
// Status bar — only call setStatus when the text actually changes
|
|
@@ -475,7 +640,23 @@ export class AgentWidget {
|
|
|
475
640
|
this.uiCtx.setStatus("subagents", newStatusText);
|
|
476
641
|
this.lastStatusText = newStatusText;
|
|
477
642
|
}
|
|
478
|
-
|
|
643
|
+
// Spinner animation is driven only by the timer while a visible running
|
|
644
|
+
// agent exists. Event-driven updates refresh changed stats without making
|
|
645
|
+
// an otherwise idle widget advance.
|
|
646
|
+
if (advanceSpinner && runningCount > 0)
|
|
647
|
+
this.widgetFrame++;
|
|
648
|
+
this.syncTimer(runningCount > 0);
|
|
649
|
+
if (this.navigationActive) {
|
|
650
|
+
if (roster.length === 0) {
|
|
651
|
+
this.navigationActive = false;
|
|
652
|
+
this.selectedAgentId = undefined;
|
|
653
|
+
this.viewportStart = 0;
|
|
654
|
+
}
|
|
655
|
+
else if (this.selectedIndexOf(roster) < 0) {
|
|
656
|
+
this.selectedAgentId = roster[Math.min(this.viewportStart, roster.length - 1)].id;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const renderKey = this.renderKey(allAgents);
|
|
479
660
|
// Register widget callback once; subsequent updates use requestRender()
|
|
480
661
|
// which re-invokes render() without replacing the component (avoids layout thrashing).
|
|
481
662
|
if (!this.widgetRegistered) {
|
|
@@ -487,15 +668,18 @@ export class AgentWidget {
|
|
|
487
668
|
// Theme changed — force re-registration so factory captures fresh theme.
|
|
488
669
|
this.widgetRegistered = false;
|
|
489
670
|
this.tui = undefined;
|
|
671
|
+
this.lastRenderKey = undefined;
|
|
490
672
|
},
|
|
491
673
|
};
|
|
492
674
|
}, { placement: "aboveEditor" });
|
|
493
675
|
this.widgetRegistered = true;
|
|
494
676
|
}
|
|
495
|
-
else {
|
|
496
|
-
// Widget already registered —
|
|
677
|
+
else if (renderKey !== this.lastRenderKey) {
|
|
678
|
+
// Widget already registered — request a re-render only when visible state
|
|
679
|
+
// changed (or the spinner timer advanced).
|
|
497
680
|
this.tui?.requestRender();
|
|
498
681
|
}
|
|
682
|
+
this.lastRenderKey = renderKey;
|
|
499
683
|
}
|
|
500
684
|
dispose() {
|
|
501
685
|
if (this.widgetInterval) {
|
|
@@ -506,9 +690,16 @@ export class AgentWidget {
|
|
|
506
690
|
this.uiCtx.setWidget("agents", undefined);
|
|
507
691
|
this.uiCtx.setStatus("subagents", undefined);
|
|
508
692
|
}
|
|
693
|
+
this.inputUnsub?.();
|
|
694
|
+
this.inputUnsub = undefined;
|
|
509
695
|
this.widgetRegistered = false;
|
|
510
696
|
this.tui = undefined;
|
|
511
697
|
this.lastStatusText = undefined;
|
|
698
|
+
this.lastRenderKey = undefined;
|
|
699
|
+
this.navigationActive = false;
|
|
700
|
+
this.selectedAgentId = undefined;
|
|
701
|
+
this.viewportStart = 0;
|
|
702
|
+
this.uiCtx = undefined;
|
|
512
703
|
}
|
|
513
704
|
}
|
|
514
705
|
//# sourceMappingURL=agent-widget.js.map
|