@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/src/ui/agent-widget.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Uses the callback form of setWidget for themed rendering.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { AgentManager } from "../agent-manager.js";
|
|
10
10
|
import { getConfig } from "../agent-types.js";
|
|
11
|
-
import type { AgentInvocation, SubagentType, WidgetMode } from "../types.js";
|
|
11
|
+
import type { AgentInvocation, AgentRecord, SubagentType, WidgetMode } from "../types.js";
|
|
12
12
|
import { getLifetimeTotal, getSessionContextPercent, type LifetimeUsage, type SessionLike } from "../usage.js";
|
|
13
13
|
|
|
14
14
|
// ---- Constants ----
|
|
@@ -52,6 +52,19 @@ export type Theme = {
|
|
|
52
52
|
bold(text: string): string;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
export type AgentWidgetOpenMode = "live" | "history";
|
|
56
|
+
export type AgentWidgetOpenCallback = (record: AgentRecord, mode: AgentWidgetOpenMode) => void | Promise<void>;
|
|
57
|
+
export type AgentWidgetOptions = {
|
|
58
|
+
canOpenHistory: (record: AgentRecord) => boolean;
|
|
59
|
+
onOpen: AgentWidgetOpenCallback;
|
|
60
|
+
};
|
|
61
|
+
/** @deprecated Use AgentWidgetOpenMode. */
|
|
62
|
+
export type AgentOpenMode = AgentWidgetOpenMode;
|
|
63
|
+
/** @deprecated Use AgentWidgetOpenCallback. */
|
|
64
|
+
export type AgentOpenCallback = AgentWidgetOpenCallback;
|
|
65
|
+
/** @deprecated Use AgentWidgetOptions.canOpenHistory. */
|
|
66
|
+
export type AgentHistoryCapability = AgentWidgetOptions["canOpenHistory"];
|
|
67
|
+
|
|
55
68
|
export type UICtx = {
|
|
56
69
|
setStatus(key: string, text: string | undefined): void;
|
|
57
70
|
setWidget(
|
|
@@ -59,6 +72,8 @@ export type UICtx = {
|
|
|
59
72
|
content: undefined | ((tui: any, theme: Theme) => { render(): string[]; invalidate(): void }),
|
|
60
73
|
options?: { placement?: "aboveEditor" | "belowEditor" },
|
|
61
74
|
): void;
|
|
75
|
+
onTerminalInput(handler: (data: string) => { consume?: boolean; data?: string } | undefined): () => void;
|
|
76
|
+
getEditorText(): string;
|
|
62
77
|
};
|
|
63
78
|
|
|
64
79
|
/** Per-agent live activity state. */
|
|
@@ -229,10 +244,13 @@ export class AgentWidget {
|
|
|
229
244
|
private uiCtx: UICtx | undefined;
|
|
230
245
|
private widgetFrame = 0;
|
|
231
246
|
private widgetInterval: ReturnType<typeof setInterval> | undefined;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
247
|
+
private inputUnsub: (() => void) | undefined;
|
|
248
|
+
/** Whether arrow keys currently navigate the agent roster. */
|
|
249
|
+
private navigationActive = false;
|
|
250
|
+
/** Stable identity of the selected row, so roster changes do not jump selection. */
|
|
251
|
+
private selectedAgentId: string | undefined;
|
|
252
|
+
/** First logical row currently represented by the bounded viewport. */
|
|
253
|
+
private viewportStart = 0;
|
|
236
254
|
|
|
237
255
|
/** Whether the widget callback is currently registered with the TUI. */
|
|
238
256
|
private widgetRegistered = false;
|
|
@@ -246,12 +264,12 @@ export class AgentWidget {
|
|
|
246
264
|
constructor(
|
|
247
265
|
private manager: AgentManager,
|
|
248
266
|
private agentActivity: Map<string, AgentActivity>,
|
|
249
|
-
/**
|
|
250
|
-
* Read live at render time. Selects which agents the widget shows — see
|
|
251
|
-
* `WidgetMode`. Defaults to `"all"` when a caller supplies no policy; the
|
|
252
|
-
* extension supplies one defaulting to `"background"`.
|
|
253
|
-
*/
|
|
267
|
+
/** Read live at render time. Selects which agents the widget shows. */
|
|
254
268
|
private mode: () => WidgetMode = () => "all",
|
|
269
|
+
private options: AgentWidgetOptions = {
|
|
270
|
+
canOpenHistory: (record) => record.session !== undefined || record.completedAt !== undefined,
|
|
271
|
+
onOpen: () => {},
|
|
272
|
+
},
|
|
255
273
|
) {}
|
|
256
274
|
|
|
257
275
|
/**
|
|
@@ -278,13 +296,23 @@ export class AgentWidget {
|
|
|
278
296
|
setUICtx(ctx: UICtx): boolean {
|
|
279
297
|
if (ctx === this.uiCtx) return false;
|
|
280
298
|
|
|
281
|
-
// UICtx changed — the widget registered on the old
|
|
282
|
-
//
|
|
299
|
+
// UICtx changed — the widget and input handler registered on the old
|
|
300
|
+
// context are gone. Re-register both on the next update().
|
|
301
|
+
this.inputUnsub?.();
|
|
302
|
+
this.inputUnsub = undefined;
|
|
283
303
|
this.uiCtx = ctx;
|
|
284
304
|
this.widgetRegistered = false;
|
|
285
305
|
this.tui = undefined;
|
|
286
306
|
this.lastStatusText = undefined;
|
|
287
307
|
this.lastRenderKey = undefined;
|
|
308
|
+
this.navigationActive = false;
|
|
309
|
+
this.selectedAgentId = undefined;
|
|
310
|
+
this.viewportStart = 0;
|
|
311
|
+
// Print/RPC tests and lightweight embedders may provide only the widget
|
|
312
|
+
// surface; real interactive contexts always implement this hook.
|
|
313
|
+
if (typeof ctx.onTerminalInput === "function") {
|
|
314
|
+
this.inputUnsub = ctx.onTerminalInput(data => this.handleKey(data));
|
|
315
|
+
}
|
|
288
316
|
return true;
|
|
289
317
|
}
|
|
290
318
|
|
|
@@ -295,16 +323,8 @@ export class AgentWidget {
|
|
|
295
323
|
return true;
|
|
296
324
|
}
|
|
297
325
|
|
|
298
|
-
/**
|
|
299
|
-
* Called on each new turn (tool_execution_start).
|
|
300
|
-
* Ages finished agents and clears those that have lingered long enough.
|
|
301
|
-
*/
|
|
326
|
+
/** Called on each new turn (tool_execution_start). */
|
|
302
327
|
onTurnStart() {
|
|
303
|
-
// Age all finished agents
|
|
304
|
-
for (const [id, age] of this.finishedTurnAge) {
|
|
305
|
-
this.finishedTurnAge.set(id, age + 1);
|
|
306
|
-
}
|
|
307
|
-
// Trigger a widget refresh (will filter out expired agents)
|
|
308
328
|
this.update();
|
|
309
329
|
}
|
|
310
330
|
|
|
@@ -312,7 +332,7 @@ export class AgentWidget {
|
|
|
312
332
|
ensureTimer() {
|
|
313
333
|
if (!this.uiCtx || !this.widgetAgents().some(a => a.status === "running")) return;
|
|
314
334
|
if (!this.widgetInterval) {
|
|
315
|
-
this.widgetInterval = setInterval(() => this.update(true),
|
|
335
|
+
this.widgetInterval = setInterval(() => this.update(true), 250);
|
|
316
336
|
}
|
|
317
337
|
}
|
|
318
338
|
|
|
@@ -325,18 +345,117 @@ export class AgentWidget {
|
|
|
325
345
|
}
|
|
326
346
|
}
|
|
327
347
|
|
|
328
|
-
/**
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Retained for the lifecycle call sites. Terminal visibility is determined
|
|
350
|
+
* by the manager record and the history capability, not a one-turn timer.
|
|
351
|
+
*/
|
|
352
|
+
markFinished(_agentId: string) {}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Records represented by selectable rows in the above-editor widget.
|
|
356
|
+
*
|
|
357
|
+
* Keep this order as the single source of truth for both rendering and key
|
|
358
|
+
* navigation. `listAgents()` is newest-first; active rows come first so the
|
|
359
|
+
* panel exposes currently useful work before terminal history.
|
|
360
|
+
*/
|
|
361
|
+
private roster(): AgentRecord[] {
|
|
362
|
+
const agents = this.widgetAgents();
|
|
363
|
+
const finished = agents.filter(record =>
|
|
364
|
+
record.status !== "running" && record.status !== "queued"
|
|
365
|
+
&& record.completedAt !== undefined
|
|
366
|
+
&& this.options.canOpenHistory(record),
|
|
367
|
+
);
|
|
368
|
+
const running = agents.filter(record => record.status === "running");
|
|
369
|
+
const queued = agents.filter(record => record.status === "queued");
|
|
370
|
+
return [...running, ...queued, ...finished];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** True when pi's prompt editor owns the keyboard. */
|
|
374
|
+
private editorHasFocus(): boolean {
|
|
375
|
+
const focused = (this.tui as { focusedComponent?: unknown } | undefined)?.focusedComponent;
|
|
376
|
+
return focused == null || focused instanceof Editor;
|
|
333
377
|
}
|
|
334
378
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
379
|
+
private selectedIndexOf(records: readonly AgentRecord[]): number {
|
|
380
|
+
if (!this.selectedAgentId) return -1;
|
|
381
|
+
return records.findIndex(record => record.id === this.selectedAgentId);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private deactivate(): void {
|
|
385
|
+
this.navigationActive = false;
|
|
386
|
+
this.selectedAgentId = undefined;
|
|
387
|
+
this.viewportStart = 0;
|
|
388
|
+
this.update();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Move the selected row, activating only from an empty focused editor. */
|
|
392
|
+
private moveSelection(direction: -1 | 1): boolean {
|
|
393
|
+
const records = this.roster();
|
|
394
|
+
const ui = this.uiCtx;
|
|
395
|
+
if (records.length === 0 || !ui) return false;
|
|
396
|
+
|
|
397
|
+
if (!this.navigationActive) {
|
|
398
|
+
if (direction !== 1 || !this.editorHasFocus() || (ui.getEditorText?.() ?? "") !== "") return false;
|
|
399
|
+
this.navigationActive = true;
|
|
400
|
+
this.selectedAgentId = records[0].id;
|
|
401
|
+
this.viewportStart = 0;
|
|
402
|
+
this.update();
|
|
403
|
+
return true;
|
|
339
404
|
}
|
|
405
|
+
|
|
406
|
+
const currentIndex = Math.max(0, this.selectedIndexOf(records));
|
|
407
|
+
if (direction === -1 && currentIndex === 0) {
|
|
408
|
+
this.deactivate();
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
const nextIndex = Math.max(0, Math.min(records.length - 1, currentIndex + direction));
|
|
412
|
+
this.selectedAgentId = records[nextIndex].id;
|
|
413
|
+
this.update();
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private openSelected(): void {
|
|
418
|
+
const record = this.roster().find(candidate => candidate.id === this.selectedAgentId);
|
|
419
|
+
this.deactivate();
|
|
420
|
+
if (!record) return;
|
|
421
|
+
const mode: AgentWidgetOpenMode = record.status === "running" || record.status === "queued" ? "live" : "history";
|
|
422
|
+
void this.options.onOpen(record, mode);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Handle terminal input before it reaches the focused prompt editor. */
|
|
426
|
+
handleKey(data: string): { consume?: boolean; data?: string } | undefined {
|
|
427
|
+
if (!this.uiCtx || isKeyRelease(data)) return undefined;
|
|
428
|
+
if (!this.editorHasFocus()) {
|
|
429
|
+
if (this.navigationActive) this.deactivate();
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!this.navigationActive) {
|
|
434
|
+
if (!matchesKey(data, "down") || (this.uiCtx.getEditorText?.() ?? "") !== "" || this.roster().length === 0) {
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
this.navigationActive = true;
|
|
438
|
+
this.selectedAgentId = this.roster()[0]?.id;
|
|
439
|
+
this.viewportStart = 0;
|
|
440
|
+
this.update();
|
|
441
|
+
return { consume: true };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (matchesKey(data, "escape")) {
|
|
445
|
+
this.deactivate();
|
|
446
|
+
return { consume: true };
|
|
447
|
+
}
|
|
448
|
+
if (matchesKey(data, "up")) return this.moveSelection(-1) ? { consume: true } : undefined;
|
|
449
|
+
if (matchesKey(data, "down")) return this.moveSelection(1) ? { consume: true } : undefined;
|
|
450
|
+
if (matchesKey(data, Key.enter)) {
|
|
451
|
+
this.openSelected();
|
|
452
|
+
return { consume: true };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Only ↑/↓ navigate. Other keys, including j/k/←/→, flow to the editor
|
|
456
|
+
// and leave navigation mode.
|
|
457
|
+
this.deactivate();
|
|
458
|
+
return undefined;
|
|
340
459
|
}
|
|
341
460
|
|
|
342
461
|
/** Render a finished agent line. */
|
|
@@ -385,10 +504,11 @@ export class AgentWidget {
|
|
|
385
504
|
const running = allAgents.filter(a => a.status === "running");
|
|
386
505
|
const queued = allAgents.filter(a => a.status === "queued");
|
|
387
506
|
const finished = allAgents.filter(a =>
|
|
388
|
-
a.status !== "running" && a.status !== "queued" && a.completedAt
|
|
389
|
-
&& this.
|
|
507
|
+
a.status !== "running" && a.status !== "queued" && a.completedAt !== undefined
|
|
508
|
+
&& this.options.canOpenHistory(a),
|
|
390
509
|
);
|
|
391
510
|
|
|
511
|
+
const selectedId = this.navigationActive ? this.selectedAgentId : undefined;
|
|
392
512
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
393
513
|
const hasFinished = finished.length > 0;
|
|
394
514
|
|
|
@@ -406,12 +526,16 @@ export class AgentWidget {
|
|
|
406
526
|
// Build sections separately for overflow-aware assembly.
|
|
407
527
|
// Each running agent = 2 lines (header + activity), finished = 1 line, queued = 1 line.
|
|
408
528
|
|
|
409
|
-
const finishedLines: string[] = [];
|
|
529
|
+
const finishedLines: { record: AgentRecord; lines: string[] }[] = [];
|
|
410
530
|
for (const a of finished) {
|
|
411
|
-
|
|
531
|
+
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
532
|
+
finishedLines.push({
|
|
533
|
+
record: a,
|
|
534
|
+
lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ` + this.renderFinishedLine(a, theme))],
|
|
535
|
+
});
|
|
412
536
|
}
|
|
413
537
|
|
|
414
|
-
const runningLines: string[][] = []; // each entry is [header, activity]
|
|
538
|
+
const runningLines: { record: AgentRecord; lines: string[] }[] = []; // each entry is [header, activity]
|
|
415
539
|
for (const a of running) {
|
|
416
540
|
const name = getDisplayName(a.type);
|
|
417
541
|
const modeLabel = getPromptModeLabel(a.type);
|
|
@@ -433,22 +557,32 @@ export class AgentWidget {
|
|
|
433
557
|
|
|
434
558
|
const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
|
|
435
559
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
560
|
+
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
561
|
+
runningLines.push({
|
|
562
|
+
record: a,
|
|
563
|
+
lines: [
|
|
564
|
+
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)}`),
|
|
565
|
+
truncate(theme.fg("dim", "│ ") + ` ${theme.fg("dim", `⎿ ${activity}`)}`),
|
|
566
|
+
],
|
|
567
|
+
});
|
|
440
568
|
}
|
|
441
569
|
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
570
|
+
const queuedLines: { record: AgentRecord; lines: string[] }[] = queued.map(a => {
|
|
571
|
+
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
572
|
+
return {
|
|
573
|
+
record: a,
|
|
574
|
+
lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${getDisplayName(a.type)} ${truncateLine(a.description)} · queued`)}`)],
|
|
575
|
+
};
|
|
576
|
+
});
|
|
445
577
|
|
|
446
578
|
// Assemble with a responsive cap (heading + overflow indicator = 2
|
|
447
579
|
// reserved lines when content exceeds the available body budget).
|
|
448
580
|
const maxBody = maxLines - 1; // heading takes 1 line
|
|
449
|
-
const
|
|
581
|
+
const rows = [...runningLines, ...queuedLines, ...finishedLines];
|
|
582
|
+
const totalBody = rows.reduce((total, row) => total + row.lines.length, 0);
|
|
450
583
|
|
|
451
|
-
const
|
|
584
|
+
const heading = "Agents ↑↓ select · enter view · esc back";
|
|
585
|
+
const lines: string[] = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, heading))];
|
|
452
586
|
|
|
453
587
|
if (maxLines === 1) {
|
|
454
588
|
// There is room only for the heading; do not consume the editor/input row.
|
|
@@ -456,71 +590,70 @@ export class AgentWidget {
|
|
|
456
590
|
}
|
|
457
591
|
|
|
458
592
|
if (totalBody <= maxBody) {
|
|
459
|
-
|
|
460
|
-
lines.push(...
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
lines[last] = lines[last].replace("├─", "└─");
|
|
468
|
-
// If last item is a running agent activity line, fix indent of that line
|
|
469
|
-
// and fix the header line above it.
|
|
470
|
-
if (runningLines.length > 0 && !queuedLine) {
|
|
471
|
-
// The last two lines are the last running agent's header + activity.
|
|
472
|
-
if (last >= 2) {
|
|
473
|
-
lines[last - 1] = lines[last - 1].replace("├─", "└─");
|
|
474
|
-
lines[last] = lines[last].replace("│ ", " ");
|
|
475
|
-
}
|
|
593
|
+
this.viewportStart = 0;
|
|
594
|
+
for (const row of rows) lines.push(...row.lines);
|
|
595
|
+
if (rows.length > 0) {
|
|
596
|
+
const lastRow = rows[rows.length - 1];
|
|
597
|
+
const lastStart = lines.length - lastRow.lines.length;
|
|
598
|
+
lines[lastStart] = lines[lastStart].replace("├─", "└─");
|
|
599
|
+
if (lastRow.lines.length === 2) {
|
|
600
|
+
lines[lastStart + 1] = lines[lastStart + 1].replace("│ ", " ");
|
|
476
601
|
}
|
|
477
602
|
}
|
|
478
603
|
} else {
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
lines.push(...pair);
|
|
490
|
-
budget -= 2;
|
|
491
|
-
} else {
|
|
492
|
-
hiddenRunning++;
|
|
604
|
+
// Reserve one line for a directional overflow summary. The viewport is
|
|
605
|
+
// a contiguous slice in roster order, so the same slice is navigable and
|
|
606
|
+
// renderable even when the selected row is currently hidden.
|
|
607
|
+
const contentBudget = Math.max(0, maxBody - 1);
|
|
608
|
+
const heightAt = (index: number) => rows[index]?.lines.length ?? 0;
|
|
609
|
+
const endFor = (start: number): number => {
|
|
610
|
+
let used = 0;
|
|
611
|
+
let end = start;
|
|
612
|
+
while (end < rows.length && used + heightAt(end) <= contentBudget) {
|
|
613
|
+
used += heightAt(end++);
|
|
493
614
|
}
|
|
615
|
+
return end;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
let start = Math.max(0, Math.min(this.viewportStart, Math.max(0, rows.length - 1)));
|
|
619
|
+
const selectedIndex = this.navigationActive && selectedId
|
|
620
|
+
? rows.findIndex(row => row.record.id === selectedId)
|
|
621
|
+
: -1;
|
|
622
|
+
if (selectedIndex >= 0) {
|
|
623
|
+
if (selectedIndex < start) start = selectedIndex;
|
|
624
|
+
if (selectedIndex >= endFor(start)) start = selectedIndex;
|
|
494
625
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
626
|
+
this.viewportStart = start;
|
|
627
|
+
|
|
628
|
+
let used = 0;
|
|
629
|
+
let end = start;
|
|
630
|
+
while (end < rows.length && used + heightAt(end) <= contentBudget) {
|
|
631
|
+
lines.push(...rows[end].lines);
|
|
632
|
+
used += heightAt(end);
|
|
633
|
+
end++;
|
|
504
634
|
}
|
|
505
|
-
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
budget--;
|
|
511
|
-
} else {
|
|
512
|
-
hiddenFinished++;
|
|
513
|
-
}
|
|
635
|
+
// A selected two-line row must remain addressable even if only one body
|
|
636
|
+
// line is available. Showing its header is preferable to hiding it.
|
|
637
|
+
if (end === start && rows[start] && contentBudget > 0) {
|
|
638
|
+
lines.push(rows[start].lines[0]);
|
|
639
|
+
end = start + 1;
|
|
514
640
|
}
|
|
515
641
|
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
642
|
+
const hiddenBefore = start;
|
|
643
|
+
const hiddenAfter = Math.max(0, rows.length - end);
|
|
644
|
+
const hidden = hiddenBefore + hiddenAfter;
|
|
645
|
+
const hiddenRows = rows.filter((_row, index) => index < start || index >= end);
|
|
646
|
+
const categoryCounts: string[] = [
|
|
647
|
+
["running", hiddenRows.filter(row => row.record.status === "running").length] as [string, number],
|
|
648
|
+
["queued", hiddenRows.filter(row => row.record.status === "queued").length] as [string, number],
|
|
649
|
+
["finished", hiddenRows.filter(row => row.record.status !== "running" && row.record.status !== "queued").length] as [string, number],
|
|
650
|
+
].filter(([, count]) => count > 0).map(([label, count]) => `${count} ${label}`);
|
|
651
|
+
const direction = [
|
|
652
|
+
...(hiddenBefore > 0 ? [`↑ ${hiddenBefore} more`] : []),
|
|
653
|
+
...(hiddenAfter > 0 ? [`↓ ${hiddenAfter} more`] : []),
|
|
654
|
+
].join(" · ");
|
|
655
|
+
const summary = `+${hidden} more (${direction}${categoryCounts.length > 0 ? `; ${categoryCounts.join(", ")}` : ""})`;
|
|
656
|
+
lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", summary)}`));
|
|
524
657
|
}
|
|
525
658
|
|
|
526
659
|
return lines;
|
|
@@ -555,9 +688,14 @@ export class AgentWidget {
|
|
|
555
688
|
lifetimeUsage: a.lifetimeUsage,
|
|
556
689
|
compactionCount: a.compactionCount,
|
|
557
690
|
isBackground: a.isBackground,
|
|
691
|
+
hasSession: a.session !== undefined,
|
|
692
|
+
transcriptPath: a.transcriptPath,
|
|
693
|
+
openableHistory: this.options.canOpenHistory(a),
|
|
558
694
|
})),
|
|
559
695
|
activities,
|
|
560
|
-
|
|
696
|
+
navigationActive: this.navigationActive,
|
|
697
|
+
selectedAgentId: this.selectedAgentId,
|
|
698
|
+
viewportStart: this.viewportStart,
|
|
561
699
|
});
|
|
562
700
|
}
|
|
563
701
|
|
|
@@ -565,16 +703,16 @@ export class AgentWidget {
|
|
|
565
703
|
update(advanceSpinner = false) {
|
|
566
704
|
if (!this.uiCtx) return;
|
|
567
705
|
const allAgents = this.widgetAgents();
|
|
706
|
+
const roster = this.roster();
|
|
568
707
|
|
|
569
708
|
// Lightweight existence checks — full categorization happens in renderWidget()
|
|
570
709
|
let runningCount = 0;
|
|
571
710
|
let queuedCount = 0;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (a.status === "
|
|
575
|
-
else if (a.status === "queued") { queuedCount++; }
|
|
576
|
-
else if (a.completedAt && this.shouldShowFinished(a.id, a.status)) { hasFinished = true; }
|
|
711
|
+
for (const a of roster) {
|
|
712
|
+
if (a.status === "running") runningCount++;
|
|
713
|
+
else if (a.status === "queued") queuedCount++;
|
|
577
714
|
}
|
|
715
|
+
const hasFinished = roster.some(a => a.status !== "running" && a.status !== "queued");
|
|
578
716
|
const hasActive = runningCount > 0 || queuedCount > 0;
|
|
579
717
|
|
|
580
718
|
// Nothing to show — clear widget
|
|
@@ -589,11 +727,10 @@ export class AgentWidget {
|
|
|
589
727
|
this.lastStatusText = undefined;
|
|
590
728
|
}
|
|
591
729
|
this.lastRenderKey = undefined;
|
|
730
|
+
this.navigationActive = false;
|
|
731
|
+
this.selectedAgentId = undefined;
|
|
732
|
+
this.viewportStart = 0;
|
|
592
733
|
this.syncTimer(false);
|
|
593
|
-
// Clean up stale entries
|
|
594
|
-
for (const [id] of this.finishedTurnAge) {
|
|
595
|
-
if (!allAgents.some(a => a.id === id)) this.finishedTurnAge.delete(id);
|
|
596
|
-
}
|
|
597
734
|
return;
|
|
598
735
|
}
|
|
599
736
|
|
|
@@ -616,6 +753,15 @@ export class AgentWidget {
|
|
|
616
753
|
// an otherwise idle widget advance.
|
|
617
754
|
if (advanceSpinner && runningCount > 0) this.widgetFrame++;
|
|
618
755
|
this.syncTimer(runningCount > 0);
|
|
756
|
+
if (this.navigationActive) {
|
|
757
|
+
if (roster.length === 0) {
|
|
758
|
+
this.navigationActive = false;
|
|
759
|
+
this.selectedAgentId = undefined;
|
|
760
|
+
this.viewportStart = 0;
|
|
761
|
+
} else if (this.selectedIndexOf(roster) < 0) {
|
|
762
|
+
this.selectedAgentId = roster[Math.min(this.viewportStart, roster.length - 1)].id;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
619
765
|
const renderKey = this.renderKey(allAgents);
|
|
620
766
|
|
|
621
767
|
// Register widget callback once; subsequent updates use requestRender()
|
|
@@ -651,11 +797,15 @@ export class AgentWidget {
|
|
|
651
797
|
this.uiCtx.setWidget("agents", undefined);
|
|
652
798
|
this.uiCtx.setStatus("subagents", undefined);
|
|
653
799
|
}
|
|
800
|
+
this.inputUnsub?.();
|
|
801
|
+
this.inputUnsub = undefined;
|
|
654
802
|
this.widgetRegistered = false;
|
|
655
803
|
this.tui = undefined;
|
|
656
804
|
this.lastStatusText = undefined;
|
|
657
805
|
this.lastRenderKey = undefined;
|
|
806
|
+
this.navigationActive = false;
|
|
807
|
+
this.selectedAgentId = undefined;
|
|
808
|
+
this.viewportStart = 0;
|
|
658
809
|
this.uiCtx = undefined;
|
|
659
|
-
this.finishedTurnAge.clear();
|
|
660
810
|
}
|
|
661
811
|
}
|
|
@@ -90,7 +90,7 @@ export function stripBackgroundSgrParams(params: readonly number[]): number[] {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function filterSgrSequences(text: string, filter: (params: number[]) => number[]): string {
|
|
93
|
-
if (!text
|
|
93
|
+
if (!text?.includes("\x1b[")) {
|
|
94
94
|
return text;
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -159,7 +159,7 @@ function sanitizeColonSgr(rawParams: string): string {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
export function sanitizeAnsiForThemedOutput(text: string): string {
|
|
162
|
-
if (!text
|
|
162
|
+
if (!text?.includes("\x1b[")) return text;
|
|
163
163
|
return text.replace(ANSI_SGR_WITH_COLON_PATTERN, (sequence, rawParams: string) =>
|
|
164
164
|
rawParams.includes(":")
|
|
165
165
|
? sanitizeColonSgr(rawParams)
|
|
@@ -39,7 +39,6 @@ export function resolveDiffPresentationMode(
|
|
|
39
39
|
return canRenderSplitLayout ? "split" : "unified";
|
|
40
40
|
case "unified":
|
|
41
41
|
return "unified";
|
|
42
|
-
case "auto":
|
|
43
42
|
default:
|
|
44
43
|
return safeWidth >= (config.diffSplitMinWidth ?? 120) && canRenderSplitLayout ? "split" : "unified";
|
|
45
44
|
}
|