@esso0428/pi-subagents 0.16.0 → 0.17.1

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.
@@ -1,163 +0,0 @@
1
- /**
2
- * fleet-list.ts — Claude Code-style "FleetView" list rendered below the editor.
3
- *
4
- * Shows `main` + each running/queued subagent as a navigable list. Pressing ↓ (or
5
- * `j`) at an empty prompt activates the list; ↑/↓ move the selection (filled ● marker),
6
- * Enter opens the selected agent's live conversation overlay, Esc returns to the prompt.
7
- * A viewer stays open when its agent finishes so the final output remains readable.
8
- *
9
- * Mechanics (see plan): the list is a `belowEditor` widget (render-only), and ALL key
10
- * handling goes through `onTerminalInput` — which fires before the focused editor and
11
- * can `consume` keys — gated on `getEditorText() === ""` so normal typing is untouched.
12
- */
13
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
14
- import type { AgentManager } from "../agent-manager.js";
15
- import { type AgentActivity, type Theme } from "./agent-widget.js";
16
- /** Maximum total rows contributed by FleetView, including hint and indicators. */
17
- export declare const FLEET_MAX_RENDER_ROWS = 6;
18
- /** Normalized mouse input accepted by FleetView's widget surface. */
19
- export type FleetMouseEvent = {
20
- type: string;
21
- wheelDelta?: number;
22
- };
23
- type FleetMouseResult = {
24
- handled: true;
25
- render: true;
26
- };
27
- type FleetWidgetContent = {
28
- render(width: number): string[];
29
- invalidate(): void;
30
- dispose?(): void;
31
- handleMouse?(event: FleetMouseEvent): FleetMouseResult | undefined;
32
- };
33
- /** Minimal UI surface the FleetView needs from `ctx.ui` (structural subset). */
34
- export type FleetUICtx = {
35
- setWidget(key: string, content: undefined | ((tui: any, theme: Theme) => FleetWidgetContent), options?: {
36
- placement?: "aboveEditor" | "belowEditor";
37
- }): void;
38
- onTerminalInput(handler: (data: string) => {
39
- consume?: boolean;
40
- data?: string;
41
- } | undefined): () => void;
42
- getEditorText(): string;
43
- notify(message: string, type?: "info" | "warning" | "error"): void;
44
- custom<T>(factory: (tui: any, theme: Theme, keybindings: any, done: (result: T) => void) => {
45
- render(width: number): string[];
46
- invalidate(): void;
47
- dispose?(): void;
48
- }, options?: {
49
- overlay?: boolean;
50
- overlayOptions?: unknown;
51
- onHandle?: (handle: unknown) => void;
52
- }): Promise<T>;
53
- };
54
- /** `11s` — integer seconds, no decimal/suffix (matches Claude Code, unlike formatMs). */
55
- export declare function formatFleetElapsed(ms: number): string;
56
- /** `↓ 13.1k tokens` — down-arrow prefix, compact magnitude, plural "tokens". */
57
- export declare function formatFleetTokens(count: number): string;
58
- /** A bounded slice of the agent-only roster rendered by FleetView. */
59
- export type FleetAgentWindow = {
60
- start: number;
61
- end: number;
62
- hiddenAbove: number;
63
- hiddenBelow: number;
64
- };
65
- /**
66
- * Calculate a bounded agent slice from an agent-only viewport start.
67
- * `end` is exclusive; `main` is rendered separately and is not part of this
68
- * window. Marker rows are reserved only when the corresponding side is hidden.
69
- */
70
- export declare function calculateFleetAgentWindow(agentCount: number, viewportStart: number): FleetAgentWindow;
71
- /**
72
- * Return the smallest viewport adjustment that makes an agent visible. The
73
- * selected index is 0-based within the agent-only roster; `main` is handled by
74
- * FleetList rather than this pure helper.
75
- */
76
- export declare function ensureFleetSelectionVisible(agentCount: number, selectedAgentIndex: number, viewportStart: number): number;
77
- export declare class FleetList {
78
- private manager;
79
- private agentActivity;
80
- private pi?;
81
- private getCtx;
82
- private ui;
83
- private tui;
84
- private inputUnsub;
85
- private widgetRegistered;
86
- private lastRosterKey;
87
- private lastRenderKey;
88
- private timer;
89
- private enabled;
90
- /** Whether arrow keys currently navigate the list (vs. flow to the editor). */
91
- private active;
92
- /** 0 = `main`, 1..N = subagents. */
93
- private selectedIndex;
94
- /** 0-based start in the agent-only roster; main is rendered separately. */
95
- private viewportStart;
96
- /** Set while a conversation overlay is open; calling it closes the overlay. */
97
- private viewerClose;
98
- private viewingAgentId;
99
- constructor(manager: AgentManager, agentActivity: Map<string, AgentActivity>, _getCwd?: () => string | undefined, pi?: ExtensionAPI | undefined, getCtx?: () => ExtensionContext | undefined);
100
- setEnabled(enabled: boolean): void;
101
- /** Capture the UI context and (re)register the global input handler. */
102
- setUICtx(ui: FleetUICtx): boolean;
103
- /** Request a render on the currently registered TUI without touching input. */
104
- requestUiRefresh(force?: boolean): boolean;
105
- /** Ensure the re-render timer is running (called when an agent spawns). */
106
- ensureTimer(): void;
107
- /**
108
- * Called when an agent finishes. The viewer (if open on it) stays open so the
109
- * final output remains readable; refresh the active-only roster.
110
- */
111
- onAgentFinished(_id: string): void;
112
- dispose(): void;
113
- /** Re-register/refresh the below-editor widget; clears it when no agents remain. */
114
- update(): void;
115
- /**
116
- * Return only values that affect `renderBar()`. Elapsed time is rendered in
117
- * whole seconds, so the 200ms timer can skip four out of five updates while
118
- * still refreshing the row at the visible cadence.
119
- */
120
- private renderKey;
121
- /**
122
- * Active agents shown in the list, ordered earliest-launched first so the
123
- * ones you started sooner sit at the top. Pending agents with no session yet
124
- * are hidden until they start. (`listAgents()` is newest-first, so we
125
- * re-sort.)
126
- */
127
- private agentRecords;
128
- private roster;
129
- private clampSelection;
130
- /** Keep the selected agent visible without recentering an already-valid window. */
131
- private ensureSelectionVisible;
132
- /**
133
- * Move the selection while keeping keyboard and wheel input on one path.
134
- * Returns whether the direction was handled; inactive upward input and input
135
- * that cannot activate the list are left for the editor/transcript.
136
- */
137
- private moveSelection;
138
- /** Handle normalized wheel input from the registered widget. */
139
- handleMouse(event: FleetMouseEvent): FleetMouseResult | undefined;
140
- /** Returns `{consume:true}` to swallow a key, or undefined to let it through. */
141
- handleKey(data: string): {
142
- consume?: boolean;
143
- data?: string;
144
- } | undefined;
145
- /**
146
- * True when pi's prompt editor owns the keyboard. pi's editor is an `Editor`
147
- * subclass (CustomEditor) while every dialog/selector is not, and the loader
148
- * aliases pi-tui to pi's own copy, so `instanceof` is a reliable identity
149
- * check. `focusedComponent` is TUI-private (no public accessor), hence the
150
- * best-effort peek: unknowable focus (no tui seen yet, nothing focused)
151
- * counts as the editor so activation keeps working.
152
- */
153
- private editorHasFocus;
154
- private deactivate;
155
- private openSelected;
156
- /** Reset overlay state and return to the list (on close, auto-close, or error). */
157
- private clearViewer;
158
- private renderBar;
159
- private bullet;
160
- private renderAgentRow;
161
- }
162
- export {};
163
- //# sourceMappingURL=fleet-list.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fleet-list.d.ts","sourceRoot":"","sources":["../../src/ui/fleet-list.ts"],"names":[],"mappings":"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"}
@@ -1,509 +0,0 @@
1
- /**
2
- * fleet-list.ts — Claude Code-style "FleetView" list rendered below the editor.
3
- *
4
- * Shows `main` + each running/queued subagent as a navigable list. Pressing ↓ (or
5
- * `j`) at an empty prompt activates the list; ↑/↓ move the selection (filled ● marker),
6
- * Enter opens the selected agent's live conversation overlay, Esc returns to the prompt.
7
- * A viewer stays open when its agent finishes so the final output remains readable.
8
- *
9
- * Mechanics (see plan): the list is a `belowEditor` widget (render-only), and ALL key
10
- * handling goes through `onTerminalInput` — which fires before the focused editor and
11
- * can `consume` keys — gated on `getEditorText() === ""` so normal typing is untouched.
12
- */
13
- import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
14
- import { canOpenActiveAgent } from "../agent-history-list.js";
15
- import { getLifetimeTotal } from "../usage.js";
16
- import { getDisplayName } from "./agent-widget.js";
17
- import { ConversationViewer, VIEWPORT_HEIGHT_PCT } from "./conversation-viewer.js";
18
- /** Widget key for the below-editor fleet list. */
19
- const FLEET_KEY = "fleet";
20
- /** Maximum total rows contributed by FleetView, including hint and indicators. */
21
- export const FLEET_MAX_RENDER_ROWS = 6;
22
- const FLEET_BASE_ROWS = 2; // hint + main
23
- const FLEET_BODY_ROWS = FLEET_MAX_RENDER_ROWS - FLEET_BASE_ROWS;
24
- /** Re-render cadence so elapsed/token stats tick while agents run. */
25
- const TICK_MS = 200;
26
- /** `11s` — integer seconds, no decimal/suffix (matches Claude Code, unlike formatMs). */
27
- export function formatFleetElapsed(ms) {
28
- return `${Math.max(0, Math.round(ms / 1000))}s`;
29
- }
30
- /** `↓ 13.1k tokens` — down-arrow prefix, compact magnitude, plural "tokens". */
31
- export function formatFleetTokens(count) {
32
- let compact;
33
- if (count >= 1_000_000)
34
- compact = `${(count / 1_000_000).toFixed(1)}M`;
35
- else if (count >= 1_000)
36
- compact = `${(count / 1_000).toFixed(1)}k`;
37
- else
38
- compact = `${count}`;
39
- return `↓ ${compact} tokens`;
40
- }
41
- /**
42
- * Calculate a bounded agent slice from an agent-only viewport start.
43
- * `end` is exclusive; `main` is rendered separately and is not part of this
44
- * window. Marker rows are reserved only when the corresponding side is hidden.
45
- */
46
- export function calculateFleetAgentWindow(agentCount, viewportStart) {
47
- const count = Math.max(0, Math.floor(agentCount));
48
- if (count === 0)
49
- return { start: 0, end: 0, hiddenAbove: 0, hiddenBelow: 0 };
50
- const maxStart = Math.max(0, count - (FLEET_BODY_ROWS - 1));
51
- const start = Math.min(maxStart, Math.max(0, Math.floor(viewportStart)));
52
- const hasAbove = start > 0;
53
- const agentBudget = Math.max(1, FLEET_BODY_ROWS - (hasAbove ? 1 : 0));
54
- let end = Math.min(count, start + agentBudget);
55
- if (end < count)
56
- end -= 1; // reserve the ↓ marker row
57
- return {
58
- start,
59
- end,
60
- hiddenAbove: start,
61
- hiddenBelow: count - end,
62
- };
63
- }
64
- /**
65
- * Return the smallest viewport adjustment that makes an agent visible. The
66
- * selected index is 0-based within the agent-only roster; `main` is handled by
67
- * FleetList rather than this pure helper.
68
- */
69
- export function ensureFleetSelectionVisible(agentCount, selectedAgentIndex, viewportStart) {
70
- const count = Math.max(0, Math.floor(agentCount));
71
- if (count === 0)
72
- return 0;
73
- const selected = Math.min(count - 1, Math.max(0, Math.floor(selectedAgentIndex)));
74
- const maxStart = Math.max(0, count - (FLEET_BODY_ROWS - 1));
75
- let start = Math.min(maxStart, Math.max(0, Math.floor(viewportStart)));
76
- const current = calculateFleetAgentWindow(count, start);
77
- if (selected < current.start) {
78
- return Math.min(maxStart, selected);
79
- }
80
- if (selected < current.end)
81
- return start;
82
- // With a possible ↓ marker, selected - 1 is the least forward shift that
83
- // exposes the selected row while preserving the marker row budget.
84
- return Math.min(maxStart, Math.max(start, selected - 1));
85
- }
86
- /**
87
- * Place `right` flush to `width`, truncating `left` first so the stats survive.
88
- * The final clamp guarantees the line never exceeds `width` (which would wrap and
89
- * desync pi's line-diff → flicker) even on a terminal too narrow for the stats.
90
- */
91
- function rightAlign(left, right, width) {
92
- const rightW = visibleWidth(right);
93
- const maxLeft = Math.max(0, width - rightW - 1);
94
- const leftClamped = truncateToWidth(left, maxLeft);
95
- const gap = Math.max(1, width - visibleWidth(leftClamped) - rightW);
96
- return truncateToWidth(leftClamped + " ".repeat(gap) + right, width);
97
- }
98
- export class FleetList {
99
- manager;
100
- agentActivity;
101
- pi;
102
- getCtx;
103
- ui;
104
- tui;
105
- inputUnsub;
106
- widgetRegistered = false;
107
- lastRosterKey;
108
- lastRenderKey;
109
- timer;
110
- enabled = true;
111
- /** Whether arrow keys currently navigate the list (vs. flow to the editor). */
112
- active = false;
113
- /** 0 = `main`, 1..N = subagents. */
114
- selectedIndex = 0;
115
- /** 0-based start in the agent-only roster; main is rendered separately. */
116
- viewportStart = 0;
117
- /** Set while a conversation overlay is open; calling it closes the overlay. */
118
- viewerClose;
119
- viewingAgentId;
120
- constructor(manager, agentActivity, _getCwd = () => undefined, pi, getCtx = () => undefined) {
121
- this.manager = manager;
122
- this.agentActivity = agentActivity;
123
- this.pi = pi;
124
- this.getCtx = getCtx;
125
- }
126
- // ---- Lifecycle ----
127
- setEnabled(enabled) {
128
- if (enabled === this.enabled)
129
- return;
130
- this.enabled = enabled;
131
- if (!enabled)
132
- this.active = false;
133
- this.update();
134
- }
135
- /** Capture the UI context and (re)register the global input handler. */
136
- setUICtx(ui) {
137
- if (ui === this.ui)
138
- return false;
139
- this.inputUnsub?.();
140
- this.ui = ui;
141
- this.widgetRegistered = false;
142
- this.lastRosterKey = undefined;
143
- this.lastRenderKey = undefined;
144
- this.tui = undefined;
145
- this.inputUnsub = ui.onTerminalInput(data => this.handleKey(data));
146
- return true;
147
- }
148
- /** Request a render on the currently registered TUI without touching input. */
149
- requestUiRefresh(force = true) {
150
- if (!this.tui)
151
- return false;
152
- this.tui.requestRender(force);
153
- return true;
154
- }
155
- /** Ensure the re-render timer is running (called when an agent spawns). */
156
- ensureTimer() {
157
- if (!this.timer)
158
- this.timer = setInterval(() => this.update(), TICK_MS);
159
- }
160
- /**
161
- * Called when an agent finishes. The viewer (if open on it) stays open so the
162
- * final output remains readable; refresh the active-only roster.
163
- */
164
- onAgentFinished(_id) {
165
- this.update();
166
- }
167
- dispose() {
168
- if (this.timer) {
169
- clearInterval(this.timer);
170
- this.timer = undefined;
171
- }
172
- this.inputUnsub?.();
173
- this.inputUnsub = undefined;
174
- if (this.viewerClose) {
175
- this.viewerClose();
176
- this.viewerClose = undefined;
177
- }
178
- this.viewingAgentId = undefined;
179
- if (this.ui && this.widgetRegistered)
180
- this.ui.setWidget(FLEET_KEY, undefined);
181
- this.widgetRegistered = false;
182
- this.lastRosterKey = undefined;
183
- this.lastRenderKey = undefined;
184
- this.tui = undefined;
185
- this.active = false;
186
- this.selectedIndex = 0;
187
- this.viewportStart = 0;
188
- // Null last so a `viewerClose()` microtask above can't re-register the widget.
189
- this.ui = undefined;
190
- }
191
- /** Re-register/refresh the below-editor widget; clears it when no agents remain. */
192
- update() {
193
- if (!this.ui)
194
- return;
195
- const records = this.agentRecords();
196
- const hasAgents = this.enabled && records.length > 0;
197
- const rosterKey = records.map(record => record.id).join("\0");
198
- const rosterChanged = rosterKey !== this.lastRosterKey;
199
- this.lastRosterKey = rosterKey;
200
- if (!hasAgents) {
201
- if (this.widgetRegistered) {
202
- this.ui.setWidget(FLEET_KEY, undefined);
203
- this.widgetRegistered = false;
204
- this.tui = undefined;
205
- }
206
- if (this.timer) {
207
- clearInterval(this.timer);
208
- this.timer = undefined;
209
- }
210
- this.active = false;
211
- this.selectedIndex = 0;
212
- this.viewportStart = 0;
213
- this.lastRosterKey = undefined;
214
- this.lastRenderKey = undefined;
215
- return;
216
- }
217
- this.clampSelection();
218
- this.ensureSelectionVisible();
219
- this.ensureTimer(); // keep stats ticking whenever the list is shown (e.g. after a re-enable)
220
- const renderKey = this.renderKey(records);
221
- if (!this.widgetRegistered) {
222
- this.ui.setWidget(FLEET_KEY, (tui, theme) => {
223
- this.tui = tui;
224
- return {
225
- render: (w) => this.renderBar(w, theme),
226
- invalidate: () => { this.widgetRegistered = false; this.tui = undefined; },
227
- handleMouse: (event) => this.handleMouse(event),
228
- };
229
- }, { placement: "belowEditor" });
230
- this.widgetRegistered = true;
231
- }
232
- else if (renderKey !== this.lastRenderKey) {
233
- // The timer runs more frequently than elapsed time is displayed. Avoid
234
- // waking the TUI when the visible FleetView snapshot is unchanged, but
235
- // preserve a forced redraw when rows were added/removed.
236
- this.tui?.requestRender(rosterChanged);
237
- }
238
- this.lastRenderKey = renderKey;
239
- }
240
- /**
241
- * Return only values that affect `renderBar()`. Elapsed time is rendered in
242
- * whole seconds, so the 200ms timer can skip four out of five updates while
243
- * still refreshing the row at the visible cadence.
244
- */
245
- renderKey(records) {
246
- const now = Date.now();
247
- const agentState = records.map(record => {
248
- const activity = this.agentActivity.get(record.id);
249
- const usage = activity?.lifetimeUsage ?? record.lifetimeUsage;
250
- return [
251
- record.id,
252
- record.type,
253
- record.description,
254
- record.status,
255
- record.startedAt,
256
- record.completedAt ?? "",
257
- getLifetimeTotal(usage),
258
- Math.round(((record.completedAt ?? now) - record.startedAt) / 1000),
259
- ].join("\u001f");
260
- });
261
- return [
262
- this.enabled ? "1" : "0",
263
- this.active ? "1" : "0",
264
- String(this.selectedIndex),
265
- String(this.viewportStart),
266
- ...agentState,
267
- ].join("\u001e");
268
- }
269
- // ---- Roster ----
270
- /**
271
- * Active agents shown in the list, ordered earliest-launched first so the
272
- * ones you started sooner sit at the top. Pending agents with no session yet
273
- * are hidden until they start. (`listAgents()` is newest-first, so we
274
- * re-sort.)
275
- */
276
- agentRecords() {
277
- return this.manager.listAgents()
278
- .filter(record => canOpenActiveAgent(record))
279
- .sort((a, b) => a.startedAt - b.startedAt);
280
- }
281
- roster() {
282
- return [{ kind: "main" }, ...this.agentRecords().map(record => ({ kind: "agent", record }))];
283
- }
284
- clampSelection() {
285
- const max = this.roster().length - 1;
286
- if (this.selectedIndex > max)
287
- this.selectedIndex = Math.max(0, max);
288
- if (this.selectedIndex < 0)
289
- this.selectedIndex = 0;
290
- }
291
- /** Keep the selected agent visible without recentering an already-valid window. */
292
- ensureSelectionVisible() {
293
- const agents = this.agentRecords();
294
- if (agents.length === 0) {
295
- this.viewportStart = 0;
296
- return;
297
- }
298
- if (this.selectedIndex > 0) {
299
- this.viewportStart = ensureFleetSelectionVisible(agents.length, this.selectedIndex - 1, this.viewportStart);
300
- return;
301
- }
302
- // `main` does not constrain the agent window, but a roster shrink can make
303
- // its previous start invalid. Use the same pure clamp as renderBar.
304
- this.viewportStart = calculateFleetAgentWindow(agents.length, this.viewportStart).start;
305
- }
306
- // ---- Key handling ----
307
- /**
308
- * Move the selection while keeping keyboard and wheel input on one path.
309
- * Returns whether the direction was handled; inactive upward input and input
310
- * that cannot activate the list are left for the editor/transcript.
311
- */
312
- moveSelection(direction) {
313
- const max = this.roster().length - 1;
314
- if (max < 0)
315
- return false;
316
- if (!this.active) {
317
- // Match ↓ activation: only a downward direction at an empty prompt with
318
- // the editor focused enters the list. Upward input remains a no-op.
319
- if (direction !== 1 || !this.editorHasFocus() || this.agentRecords().length === 0 || this.ui?.getEditorText() !== "") {
320
- return false;
321
- }
322
- this.active = true;
323
- this.selectedIndex = 0;
324
- this.viewportStart = 0;
325
- this.update();
326
- return true;
327
- }
328
- this.clampSelection();
329
- if (direction === -1 && this.selectedIndex === 0) {
330
- this.deactivate();
331
- return true;
332
- }
333
- this.selectedIndex = direction === 1
334
- ? Math.min(max, this.selectedIndex + 1)
335
- : Math.max(0, this.selectedIndex - 1);
336
- this.ensureSelectionVisible();
337
- this.update();
338
- return true;
339
- }
340
- /** Handle normalized wheel input from the registered widget. */
341
- handleMouse(event) {
342
- if (!this.enabled || !this.ui || this.viewerClose)
343
- return undefined;
344
- if (event.type !== "wheel")
345
- return undefined;
346
- const wheelDelta = event.wheelDelta;
347
- const direction = wheelDelta == null
348
- ? undefined
349
- : wheelDelta < 0
350
- ? -1
351
- : wheelDelta > 0
352
- ? 1
353
- : undefined;
354
- if (!direction || !this.moveSelection(direction))
355
- return undefined;
356
- // The widget handled the normalized wheel, including at selection
357
- // boundaries; explicitly request a redraw so it never falls through to
358
- // Pi's transcript fallback.
359
- return { handled: true, render: true };
360
- }
361
- /** Returns `{consume:true}` to swallow a key, or undefined to let it through. */
362
- handleKey(data) {
363
- if (!this.enabled || !this.ui)
364
- return undefined;
365
- // Input listeners receive BOTH key-press and key-release (the kitty protocol
366
- // emits both, and matchesKey matches either) — act on press only, or every
367
- // tap would move/fire twice. Repeats still pass through for held-key nav.
368
- if (isKeyRelease(data))
369
- return undefined;
370
- // While an overlay is open, let it own all input.
371
- if (this.viewerClose)
372
- return undefined;
373
- // Input listeners fire BEFORE the focused component, and dialogs
374
- // (ctx.ui.select/confirm/input, pi's own menus) swap the prompt editor out
375
- // while getEditorText() still reads the detached — empty — editor. So when
376
- // anything but the editor owns the keyboard, stay out of its keys (#123).
377
- if (!this.editorHasFocus()) {
378
- if (this.active)
379
- this.deactivate();
380
- return undefined;
381
- }
382
- const direction = matchesKey(data, "down") || matchesKey(data, "j")
383
- || (!this.active && matchesKey(data, "left"))
384
- ? 1
385
- : matchesKey(data, "up") || matchesKey(data, "k")
386
- ? -1
387
- : undefined;
388
- if (direction && this.moveSelection(direction))
389
- return { consume: true };
390
- if (!this.active)
391
- return undefined;
392
- // Active — Enter opens, Esc / Up-past-top exits.
393
- if (matchesKey(data, "escape")) {
394
- this.deactivate();
395
- return { consume: true };
396
- }
397
- if (matchesKey(data, Key.enter)) {
398
- this.openSelected();
399
- return { consume: true };
400
- }
401
- // Any other key cancels navigation and flows to the editor.
402
- this.deactivate();
403
- return undefined;
404
- }
405
- /**
406
- * True when pi's prompt editor owns the keyboard. pi's editor is an `Editor`
407
- * subclass (CustomEditor) while every dialog/selector is not, and the loader
408
- * aliases pi-tui to pi's own copy, so `instanceof` is a reliable identity
409
- * check. `focusedComponent` is TUI-private (no public accessor), hence the
410
- * best-effort peek: unknowable focus (no tui seen yet, nothing focused)
411
- * counts as the editor so activation keeps working.
412
- */
413
- editorHasFocus() {
414
- const focused = this.tui?.focusedComponent;
415
- return focused == null || focused instanceof Editor;
416
- }
417
- deactivate() {
418
- this.active = false;
419
- this.selectedIndex = 0;
420
- this.viewportStart = 0;
421
- this.update();
422
- }
423
- openSelected() {
424
- const entry = this.roster()[this.selectedIndex];
425
- if (!entry || entry.kind === "main") {
426
- // `main` = return to the prompt; the native transcript is already shown.
427
- this.deactivate();
428
- return;
429
- }
430
- const record = entry.record;
431
- if (!this.ui)
432
- return;
433
- if (!canOpenActiveAgent(record) || !record.session) {
434
- this.ui.notify(`Agent is ${record.status} — no live session available.`, "info");
435
- return;
436
- }
437
- const session = record.session;
438
- const ctx = this.getCtx();
439
- const pi = this.pi;
440
- if (!ctx || !pi) {
441
- this.ui.notify("Agent conversation is unavailable — no active session.", "info");
442
- return;
443
- }
444
- const activity = this.agentActivity.get(record.id);
445
- this.viewingAgentId = record.id;
446
- void this.ui.custom((tui, theme, keybindings, done) => {
447
- this.viewerClose = () => done(undefined);
448
- return new ConversationViewer(tui, session, record, activity, theme, done, () => {
449
- if (this.manager.abort(record.id))
450
- this.ui?.notify(`Stopped "${record.description}".`, "info");
451
- }, keybindings, (message) => this.manager.steer(record.id, message), { pi, ctx, readOnly: false });
452
- }, {
453
- overlay: true,
454
- overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
455
- }).then(() => this.clearViewer(), () => this.clearViewer());
456
- }
457
- /** Reset overlay state and return to the list (on close, auto-close, or error). */
458
- clearViewer() {
459
- // Keep the cursor on the agent we were viewing — re-resolve by id so it
460
- // still feels natural if the list reordered (an earlier agent finished)
461
- // while the overlay was open. If that agent is gone, leave the index for
462
- // update()'s clamp to settle.
463
- if (this.viewingAgentId) {
464
- const idx = this.roster().findIndex(e => e.kind === "agent" && e.record.id === this.viewingAgentId);
465
- if (idx >= 0)
466
- this.selectedIndex = idx;
467
- }
468
- this.viewerClose = undefined;
469
- this.viewingAgentId = undefined;
470
- this.update();
471
- }
472
- // ---- Rendering ----
473
- renderBar(width, theme) {
474
- const agents = this.roster().slice(1);
475
- if (agents.length === 0)
476
- return [];
477
- // Clamp locally so a render between a roster shrink and the next update()
478
- // (e.g. on terminal resize) never loses the selection marker.
479
- const sel = Math.min(this.selectedIndex, agents.length);
480
- const hint = this.active
481
- ? "↑↓ select · enter view · esc back"
482
- : "esc to interrupt · ↓ to manage";
483
- const lines = [];
484
- lines.push(truncateToWidth(" " + theme.fg("dim", hint), width));
485
- lines.push(truncateToWidth(` ${this.bullet(0, sel, theme)} main`, width));
486
- const window = calculateFleetAgentWindow(agents.length, this.viewportStart);
487
- if (window.hiddenAbove > 0) {
488
- lines.push(rightAlign("", theme.fg("dim", `↑ ${window.hiddenAbove} more`), width));
489
- }
490
- for (let a = window.start; a < window.end; a++) {
491
- lines.push(this.renderAgentRow(a + 1, sel, agents[a].record, width, theme));
492
- }
493
- if (window.hiddenBelow > 0) {
494
- lines.push(rightAlign("", theme.fg("dim", `↓ ${window.hiddenBelow} more`), width));
495
- }
496
- return lines;
497
- }
498
- bullet(rosterIndex, sel, theme) {
499
- return rosterIndex === sel ? theme.fg("accent", "●") : theme.fg("dim", "○");
500
- }
501
- renderAgentRow(rosterIndex, sel, record, width, theme) {
502
- const left = ` ${this.bullet(rosterIndex, sel, theme)} ${theme.fg("muted", getDisplayName(record.type))} ${record.description}`;
503
- const tokens = getLifetimeTotal(this.agentActivity.get(record.id)?.lifetimeUsage ?? record.lifetimeUsage);
504
- const elapsedMs = (record.completedAt ?? Date.now()) - record.startedAt; // freezes once finished
505
- const right = theme.fg("dim", `${formatFleetElapsed(elapsedMs)} · ${formatFleetTokens(tokens)}`);
506
- return rightAlign(left, right, width);
507
- }
508
- }
509
- //# sourceMappingURL=fleet-list.js.map