@esso0428/pi-subagents 0.17.12 → 0.17.13
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 +7 -0
- package/dist/ui/agent-widget.d.ts +100 -94
- package/dist/ui/agent-widget.d.ts.map +1 -1
- package/dist/ui/agent-widget.js +573 -368
- package/dist/ui/agent-widget.js.map +1 -1
- package/package.json +1 -1
- package/src/ui/agent-widget.ts +603 -362
package/src/ui/agent-widget.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* Uses the callback form of setWidget for themed rendering.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import { renderAgentName } from "../agent-color.js";
|
|
10
|
-
import {
|
|
10
|
+
import type { AgentManager } from "../agent-manager.js";
|
|
11
11
|
import { getConfig } from "../agent-types.js";
|
|
12
12
|
import type { AgentInvocation, AgentRecord, SubagentType, WidgetMode } from "../types.js";
|
|
13
13
|
import { getLifetimeCost, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage, type SessionLike } from "../usage.js";
|
|
@@ -16,19 +16,17 @@ import { getLifetimeCost, getLifetimeTotal, getSessionContextPercent, type Lifet
|
|
|
16
16
|
|
|
17
17
|
/** Maximum number of rendered lines before overflow collapse kicks in. */
|
|
18
18
|
export const MAX_WIDGET_LINES = 12;
|
|
19
|
+
/** Keep a small editor/input area visible below the above-editor widget. */
|
|
20
|
+
const MIN_EDITOR_LINES = 4;
|
|
19
21
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export interface AgentWidgetOptions {
|
|
29
|
-
canOpenHistory?: (record: import("../types.js").AgentRecord) => boolean;
|
|
30
|
-
onOpen?: (record: import("../types.js").AgentRecord, mode: AgentWidgetOpenMode) => void;
|
|
31
|
-
showCost?: () => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Derive the widget ceiling from the terminal height while retaining the
|
|
24
|
+
* historical MAX_WIDGET_LINES ceiling. Very short terminals may have no room
|
|
25
|
+
* for the widget; that is preferable to consuming the editor/input area.
|
|
26
|
+
*/
|
|
27
|
+
export function getWidgetLineBudget(rows: number): number {
|
|
28
|
+
if (!Number.isFinite(rows)) return MAX_WIDGET_LINES;
|
|
29
|
+
return Math.min(MAX_WIDGET_LINES, Math.max(0, Math.floor(rows) - MIN_EDITOR_LINES));
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
/** Braille spinner frames for animated running indicator. */
|
|
@@ -55,6 +53,20 @@ export type Theme = {
|
|
|
55
53
|
bold(text: string): string;
|
|
56
54
|
};
|
|
57
55
|
|
|
56
|
+
export type AgentWidgetOpenMode = "live" | "history";
|
|
57
|
+
export type AgentWidgetOpenCallback = (record: AgentRecord, mode: AgentWidgetOpenMode) => void | Promise<void>;
|
|
58
|
+
export interface AgentWidgetOptions {
|
|
59
|
+
canOpenHistory?: (record: AgentRecord) => boolean;
|
|
60
|
+
onOpen?: AgentWidgetOpenCallback;
|
|
61
|
+
showCost?: () => boolean;
|
|
62
|
+
}
|
|
63
|
+
/** @deprecated Use AgentWidgetOpenMode. */
|
|
64
|
+
export type AgentOpenMode = AgentWidgetOpenMode;
|
|
65
|
+
/** @deprecated Use AgentWidgetOpenCallback. */
|
|
66
|
+
export type AgentOpenCallback = AgentWidgetOpenCallback;
|
|
67
|
+
/** @deprecated Use AgentWidgetOptions.canOpenHistory. */
|
|
68
|
+
export type AgentHistoryCapability = AgentWidgetOptions["canOpenHistory"];
|
|
69
|
+
|
|
58
70
|
export type UICtx = {
|
|
59
71
|
setStatus(key: string, text: string | undefined): void;
|
|
60
72
|
setWidget(
|
|
@@ -76,7 +88,7 @@ export interface AgentActivity {
|
|
|
76
88
|
turnCount: number;
|
|
77
89
|
/** Effective max turns for this agent (undefined = unlimited). */
|
|
78
90
|
maxTurns?: number;
|
|
79
|
-
/** Lifetime
|
|
91
|
+
/** Lifetime usage breakdown — see LifetimeUsage docs. */
|
|
80
92
|
lifetimeUsage: LifetimeUsage;
|
|
81
93
|
}
|
|
82
94
|
|
|
@@ -93,7 +105,7 @@ export interface AgentDetails {
|
|
|
93
105
|
activity?: string;
|
|
94
106
|
/** Current spinner frame index (for animated running indicator). */
|
|
95
107
|
spinnerFrame?: number;
|
|
96
|
-
/** Short
|
|
108
|
+
/** Short model name if different from parent (e.g. "haiku", "sonnet"). */
|
|
97
109
|
modelName?: string;
|
|
98
110
|
/** Notable config tags (e.g. ["thinking: high", "isolated"]). */
|
|
99
111
|
tags?: string[];
|
|
@@ -123,31 +135,6 @@ export function formatTokens(count: number): string {
|
|
|
123
135
|
return `${count} token`;
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
/**
|
|
127
|
-
* Format a cost as `~$0.0042`, or "" when there is nothing to show.
|
|
128
|
-
*
|
|
129
|
-
* The tilde is load-bearing: this is pi's own estimate from the model's listed
|
|
130
|
-
* rates, not a billed figure, and the surfaces that print it sit next to token
|
|
131
|
-
* counts that ARE exact.
|
|
132
|
-
*
|
|
133
|
-
* Nothing is printed for zero, which is also what a model with no pricing data
|
|
134
|
-
* reports: `$0.00` beside a local model's tokens would claim its cost was
|
|
135
|
-
* measured and found to be nothing, rather than never measured at all. For the
|
|
136
|
-
* same reason a real cost too small for four decimals reads `<$0.0001` — it was
|
|
137
|
-
* measured, and rounding it to `~$0.0000` would say the opposite.
|
|
138
|
-
*/
|
|
139
|
-
export function formatCost(cost: number): string {
|
|
140
|
-
if (!(cost > 0)) return ""; // also catches NaN
|
|
141
|
-
if (cost < 0.0001) return "<$0.0001";
|
|
142
|
-
if (cost >= 1) return `~$${cost.toFixed(2)}`;
|
|
143
|
-
// Under a dollar: cents at minimum, four decimals at most, nothing trailing.
|
|
144
|
-
// Most single runs land between a tenth of a cent and a dime, where rounding
|
|
145
|
-
// to cents would collapse a 4x difference in spend into the same figure.
|
|
146
|
-
const rounded = Number(cost.toFixed(4));
|
|
147
|
-
const decimals = (String(rounded).split(".")[1] ?? "").length;
|
|
148
|
-
return `~$${rounded.toFixed(Math.max(2, decimals))}`;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
138
|
/**
|
|
152
139
|
* Token count with optional context-fill % and compaction-count annotations.
|
|
153
140
|
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
@@ -193,6 +180,16 @@ export function formatDuration(startedAt: number, completedAt?: number): string
|
|
|
193
180
|
return `${formatMs(Date.now() - startedAt)} (running)`;
|
|
194
181
|
}
|
|
195
182
|
|
|
183
|
+
/** Format an estimated model cost without implying billing precision. */
|
|
184
|
+
export function formatCost(cost: number): string {
|
|
185
|
+
if (!(cost > 0)) return "";
|
|
186
|
+
if (cost < 0.0001) return "<$0.0001";
|
|
187
|
+
if (cost >= 1) return `~$${cost.toFixed(2)}`;
|
|
188
|
+
const rounded = Number(cost.toFixed(4));
|
|
189
|
+
const decimals = (String(rounded).split(".")[1] ?? "").length;
|
|
190
|
+
return `~$${rounded.toFixed(Math.max(2, decimals))}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
196
193
|
/** Get display name for any agent type (built-in or custom). */
|
|
197
194
|
export function getDisplayName(type: SubagentType): string {
|
|
198
195
|
return getConfig(type).displayName;
|
|
@@ -204,13 +201,7 @@ export function getPromptModeLabel(type: SubagentType): string | undefined {
|
|
|
204
201
|
return config.promptMode === "append" ? "twin" : undefined;
|
|
205
202
|
}
|
|
206
203
|
|
|
207
|
-
/**
|
|
208
|
-
* Mode label is not included — callers add it where they want it.
|
|
209
|
-
*
|
|
210
|
-
* Both model forms come back so each surface can pick by width; the
|
|
211
|
-
* "(asked X)" annotation is applied here rather than by callers, so a value the
|
|
212
|
-
* spawn did not honor cannot be rendered as though it had been (#182).
|
|
213
|
-
*/
|
|
204
|
+
/** Mode label is not included — callers add it where they want it. */
|
|
214
205
|
export function buildInvocationTags(
|
|
215
206
|
invocation: AgentInvocation | undefined,
|
|
216
207
|
): { modelName?: string; modelId?: string; tags: string[] } {
|
|
@@ -232,9 +223,9 @@ export function buildInvocationTags(
|
|
|
232
223
|
};
|
|
233
224
|
}
|
|
234
225
|
|
|
235
|
-
/**
|
|
226
|
+
/** Normalize and truncate text so it can never add physical widget rows. */
|
|
236
227
|
function truncateLine(text: string, len = 60): string {
|
|
237
|
-
const line = text.
|
|
228
|
+
const line = text.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
|
|
238
229
|
if (line.length <= len) return line;
|
|
239
230
|
return line.slice(0, len) + "…";
|
|
240
231
|
}
|
|
@@ -273,10 +264,25 @@ export class AgentWidget {
|
|
|
273
264
|
private uiCtx: UICtx | undefined;
|
|
274
265
|
private widgetFrame = 0;
|
|
275
266
|
private widgetInterval: ReturnType<typeof setInterval> | undefined;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
267
|
+
private inputUnsub: (() => void) | undefined;
|
|
268
|
+
/** Whether arrow keys currently navigate the agent roster. */
|
|
269
|
+
private navigationActive = false;
|
|
270
|
+
/** Number of modal surfaces currently owning this listener's input suspension. */
|
|
271
|
+
private navigationSuspensionDepth = 0;
|
|
272
|
+
/** Stable identity of the selected row, so roster changes do not jump selection. */
|
|
273
|
+
private selectedAgentId: string | undefined;
|
|
274
|
+
/** Last logical roster index of the selected row, used when it disappears. */
|
|
275
|
+
private selectedRosterIndex = 0;
|
|
276
|
+
/** First logical row currently represented by the bounded viewport. */
|
|
277
|
+
private viewportStart = 0;
|
|
278
|
+
|
|
279
|
+
/** Cached records and roster. Rebuilt by update(), never by an arrow press. */
|
|
280
|
+
private cachedAgents: AgentRecord[] = [];
|
|
281
|
+
private cachedRoster: AgentRecord[] = [];
|
|
282
|
+
private rosterCacheKey: string | undefined;
|
|
283
|
+
private rosterCacheInitialized = false;
|
|
284
|
+
/** Terminal history capability is a potentially filesystem-backed lookup. */
|
|
285
|
+
private historyOpenabilityCache = new Map<string, { key: string; value: boolean }>();
|
|
280
286
|
|
|
281
287
|
/** Whether the widget callback is currently registered with the TUI. */
|
|
282
288
|
private widgetRegistered = false;
|
|
@@ -284,38 +290,17 @@ export class AgentWidget {
|
|
|
284
290
|
private tui: any | undefined;
|
|
285
291
|
/** Last status bar text, used to avoid redundant setStatus calls. */
|
|
286
292
|
private lastStatusText: string | undefined;
|
|
287
|
-
|
|
288
|
-
private
|
|
289
|
-
private
|
|
290
|
-
private
|
|
291
|
-
private
|
|
292
|
-
private viewportStart = 0;
|
|
293
|
-
private selectedRosterIndex = 0;
|
|
294
|
-
private openOptions: AgentWidgetOptions;
|
|
295
|
-
private showCost: () => boolean;
|
|
296
|
-
private showModel: () => boolean;
|
|
293
|
+
/** Snapshot of the state used for the last widget registration/render request. */
|
|
294
|
+
private lastRenderKey: string | undefined;
|
|
295
|
+
private readonly openOptions: AgentWidgetOptions;
|
|
296
|
+
private readonly showCost: () => boolean;
|
|
297
|
+
private readonly showModel: () => boolean;
|
|
297
298
|
|
|
298
299
|
constructor(
|
|
299
300
|
private manager: AgentManager,
|
|
300
301
|
private agentActivity: Map<string, AgentActivity>,
|
|
301
|
-
/**
|
|
302
|
-
* Read live at render time. Selects which agents the widget shows — see
|
|
303
|
-
* `WidgetMode`. Defaults to `"all"` when a caller supplies no policy; the
|
|
304
|
-
* extension supplies one defaulting to `"background"`.
|
|
305
|
-
*/
|
|
302
|
+
/** Read live at render time. Selects which agents the widget shows. */
|
|
306
303
|
private mode: () => WidgetMode = () => "all",
|
|
307
|
-
/**
|
|
308
|
-
* Read live at render time, like `mode`. Whether running agents show an
|
|
309
|
-
* estimated cost beside their token count. Defaults to off — the extension
|
|
310
|
-
* supplies the user's `showCost` setting.
|
|
311
|
-
*/
|
|
312
|
-
/**
|
|
313
|
-
* Read live at render time, like `mode`. Whether running agents name the
|
|
314
|
-
* model driving them and the thinking level it is running at. Defaults to
|
|
315
|
-
* off — the extension supplies the user's `showModel` setting — because the
|
|
316
|
-
* row is already dense and the same pair is on the tool result and in the
|
|
317
|
-
* conversation viewer unconditionally.
|
|
318
|
-
*/
|
|
319
304
|
showCostOrOptions: (() => boolean) | AgentWidgetOptions = () => false,
|
|
320
305
|
showModel: () => boolean = () => false,
|
|
321
306
|
) {
|
|
@@ -340,8 +325,8 @@ export class AgentWidget {
|
|
|
340
325
|
* only proven-foreground runs drop out — nothing else silently vanishes.
|
|
341
326
|
* - `all`: every agent.
|
|
342
327
|
*/
|
|
343
|
-
private
|
|
344
|
-
const all = this.manager.listAgents()
|
|
328
|
+
private widgetAgentsFromManager(): AgentRecord[] {
|
|
329
|
+
const all = this.manager.listAgents();
|
|
345
330
|
switch (this.mode()) {
|
|
346
331
|
case "off": return [];
|
|
347
332
|
case "background": return all.filter(a => a.isBackground !== false);
|
|
@@ -349,130 +334,257 @@ export class AgentWidget {
|
|
|
349
334
|
}
|
|
350
335
|
}
|
|
351
336
|
|
|
352
|
-
/**
|
|
337
|
+
/** Records currently visible to the widget; safe for render and input paths. */
|
|
338
|
+
private widgetAgents(): AgentRecord[] {
|
|
339
|
+
return this.rosterCacheInitialized ? this.cachedAgents : this.widgetAgentsFromManager();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private canOpenHistory(record: AgentRecord): boolean {
|
|
343
|
+
return this.openOptions.canOpenHistory?.(record) ?? true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private historyOpenabilityKey(record: AgentRecord): string {
|
|
347
|
+
return JSON.stringify([
|
|
348
|
+
record.status,
|
|
349
|
+
record.completedAt ?? null,
|
|
350
|
+
record.transcriptPath ?? null,
|
|
351
|
+
record.session !== undefined,
|
|
352
|
+
]);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Cache terminal history capability by the record state that can affect it.
|
|
357
|
+
* The supplied predicate may call `existsSync`, so it must not run from a
|
|
358
|
+
* timer refresh, render-key calculation, or arrow-navigation path more than
|
|
359
|
+
* once for the same terminal record state.
|
|
360
|
+
*/
|
|
361
|
+
private cachedCanOpenHistory(record: AgentRecord): boolean {
|
|
362
|
+
if (record.status === "running" || record.status === "queued") return true;
|
|
363
|
+
|
|
364
|
+
const key = this.historyOpenabilityKey(record);
|
|
365
|
+
const cached = this.historyOpenabilityCache.get(record.id);
|
|
366
|
+
if (cached?.key === key) return cached.value;
|
|
367
|
+
|
|
368
|
+
const value = this.canOpenHistory(record);
|
|
369
|
+
this.historyOpenabilityCache.set(record.id, { key, value });
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Refresh the manager snapshot and openability cache at lifecycle/update time. */
|
|
374
|
+
private refreshRoster(): AgentRecord[] {
|
|
375
|
+
const agents = this.widgetAgentsFromManager();
|
|
376
|
+
const key = JSON.stringify({
|
|
377
|
+
mode: this.mode(),
|
|
378
|
+
agents: agents.map(record => [
|
|
379
|
+
record.id,
|
|
380
|
+
record.type,
|
|
381
|
+
record.description,
|
|
382
|
+
record.status,
|
|
383
|
+
record.completedAt ?? null,
|
|
384
|
+
record.startedAt,
|
|
385
|
+
record.toolUses,
|
|
386
|
+
record.error ?? null,
|
|
387
|
+
record.transcriptPath ?? null,
|
|
388
|
+
record.session !== undefined,
|
|
389
|
+
record.isBackground,
|
|
390
|
+
record.status !== "running" && record.status !== "queued"
|
|
391
|
+
? this.cachedCanOpenHistory(record)
|
|
392
|
+
: undefined,
|
|
393
|
+
]),
|
|
394
|
+
});
|
|
395
|
+
if (this.rosterCacheInitialized && key === this.rosterCacheKey) return this.cachedRoster;
|
|
396
|
+
|
|
397
|
+
const finished = agents.filter(record =>
|
|
398
|
+
record.status !== "running" && record.status !== "queued"
|
|
399
|
+
&& record.completedAt !== undefined
|
|
400
|
+
&& this.cachedCanOpenHistory(record),
|
|
401
|
+
);
|
|
402
|
+
const running = agents.filter(record => record.status === "running");
|
|
403
|
+
const queued = agents.filter(record => record.status === "queued");
|
|
404
|
+
this.cachedAgents = agents;
|
|
405
|
+
this.cachedRoster = [...running, ...queued, ...finished];
|
|
406
|
+
this.rosterCacheKey = key;
|
|
407
|
+
this.rosterCacheInitialized = true;
|
|
408
|
+
return this.cachedRoster;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Suspend only this listener's keyboard ownership; never alter the widget. */
|
|
353
412
|
suspendNavigation(): void {
|
|
354
|
-
this.
|
|
413
|
+
this.navigationSuspensionDepth++;
|
|
355
414
|
}
|
|
356
415
|
|
|
357
|
-
/**
|
|
416
|
+
/** Restore one layer of this listener's keyboard ownership. */
|
|
358
417
|
resumeNavigation(): void {
|
|
359
|
-
this.
|
|
418
|
+
if (this.navigationSuspensionDepth > 0) this.navigationSuspensionDepth--;
|
|
360
419
|
}
|
|
361
420
|
|
|
362
421
|
/** Set the UI context (grabbed from first tool execution). */
|
|
363
422
|
setUICtx(ctx: UICtx): boolean {
|
|
364
423
|
if (ctx === this.uiCtx) return false;
|
|
365
|
-
|
|
366
|
-
|
|
424
|
+
|
|
425
|
+
// UICtx changed — the widget and input handler registered on the old
|
|
426
|
+
// context are gone. Re-register both on the next update().
|
|
427
|
+
this.inputUnsub?.();
|
|
428
|
+
this.inputUnsub = undefined;
|
|
367
429
|
this.uiCtx = ctx;
|
|
368
430
|
this.widgetRegistered = false;
|
|
369
431
|
this.tui = undefined;
|
|
370
432
|
this.lastStatusText = undefined;
|
|
371
|
-
this.
|
|
433
|
+
this.lastRenderKey = undefined;
|
|
434
|
+
this.navigationSuspensionDepth = 0;
|
|
372
435
|
this.navigationActive = false;
|
|
373
436
|
this.selectedAgentId = undefined;
|
|
437
|
+
this.selectedRosterIndex = 0;
|
|
374
438
|
this.viewportStart = 0;
|
|
375
|
-
|
|
439
|
+
this.cachedAgents = [];
|
|
440
|
+
this.cachedRoster = [];
|
|
441
|
+
this.rosterCacheKey = undefined;
|
|
442
|
+
this.rosterCacheInitialized = false;
|
|
443
|
+
this.historyOpenabilityCache.clear();
|
|
444
|
+
// Print/RPC tests and lightweight embedders may provide only the widget
|
|
445
|
+
// surface; real interactive contexts always implement this hook.
|
|
446
|
+
if (typeof ctx.onTerminalInput === "function") {
|
|
447
|
+
this.inputUnsub = ctx.onTerminalInput(data => this.handleKey(data));
|
|
448
|
+
}
|
|
376
449
|
return true;
|
|
377
450
|
}
|
|
378
451
|
|
|
379
|
-
/**
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
452
|
+
/** Request a render on the currently registered TUI without touching input. */
|
|
453
|
+
requestUiRefresh(force = true): boolean {
|
|
454
|
+
if (!this.tui || typeof this.tui.requestRender !== "function") return false;
|
|
455
|
+
this.tui.requestRender(force);
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Called on each new turn (tool_execution_start). */
|
|
383
460
|
onTurnStart() {
|
|
384
|
-
// Age all finished agents
|
|
385
|
-
for (const [id, age] of this.finishedTurnAge) {
|
|
386
|
-
this.finishedTurnAge.set(id, age + 1);
|
|
387
|
-
}
|
|
388
|
-
// Trigger a widget refresh (will filter out expired agents)
|
|
389
461
|
this.update();
|
|
390
462
|
}
|
|
391
463
|
|
|
392
|
-
/**
|
|
464
|
+
/** Keep the spinner/elapsed-time timer alive only while a visible agent runs. */
|
|
393
465
|
ensureTimer() {
|
|
466
|
+
if (!this.uiCtx) return;
|
|
467
|
+
const roster = this.rosterCacheInitialized ? this.cachedRoster : this.refreshRoster();
|
|
468
|
+
if (!roster.some(a => a.status === "running")) return;
|
|
394
469
|
if (!this.widgetInterval) {
|
|
395
470
|
this.widgetInterval = setInterval(() => this.update(true), 250);
|
|
396
471
|
}
|
|
397
472
|
}
|
|
398
473
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const age = this.finishedTurnAge.get(agentId) ?? 0;
|
|
406
|
-
const maxAge = ERROR_STATUSES.has(status) ? AgentWidget.ERROR_LINGER_TURNS : 1;
|
|
407
|
-
return age < maxAge;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/** Record an agent as finished (call when agent completes). */
|
|
411
|
-
markFinished(agentId: string) {
|
|
412
|
-
if (!this.finishedTurnAge.has(agentId)) {
|
|
413
|
-
this.finishedTurnAge.set(agentId, 0);
|
|
474
|
+
private syncTimer(shouldRun: boolean): void {
|
|
475
|
+
if (shouldRun) {
|
|
476
|
+
this.ensureTimer();
|
|
477
|
+
} else if (this.widgetInterval) {
|
|
478
|
+
clearInterval(this.widgetInterval);
|
|
479
|
+
this.widgetInterval = undefined;
|
|
414
480
|
}
|
|
415
481
|
}
|
|
416
482
|
|
|
417
483
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
* seen before, so a resumed agent would otherwise keep the age from its
|
|
421
|
-
* previous run — already past the linger limit, hiding the new run's
|
|
422
|
-
* completion line entirely.
|
|
484
|
+
* Retained for the lifecycle call sites. Terminal visibility is determined
|
|
485
|
+
* by the manager record and the history capability, not a one-turn timer.
|
|
423
486
|
*/
|
|
424
|
-
|
|
425
|
-
this.finishedTurnAge.delete(agentId);
|
|
426
|
-
}
|
|
487
|
+
markFinished(_agentId: string) {}
|
|
427
488
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
agent.status === "running" || agent.status === "queued" || (agent.completedAt !== undefined && this.shouldShowFinished(agent.id, agent.status, agent.transcriptPath !== undefined)),
|
|
431
|
-
);
|
|
432
|
-
return agents.filter(agent =>
|
|
433
|
-
agent.status === "running" || agent.status === "queued" || (this.openOptions.canOpenHistory?.(agent) ?? true),
|
|
434
|
-
).sort((a, b) => {
|
|
435
|
-
const activeA = a.status === "running" || a.status === "queued" ? 0 : 1;
|
|
436
|
-
const activeB = b.status === "running" || b.status === "queued" ? 0 : 1;
|
|
437
|
-
return activeA - activeB;
|
|
438
|
-
});
|
|
439
|
-
}
|
|
489
|
+
/** Retained for lifecycle call sites; terminal rows are record-driven now. */
|
|
490
|
+
markRunning(_agentId: string) {}
|
|
440
491
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
492
|
+
/**
|
|
493
|
+
* Records represented by selectable rows in the above-editor widget.
|
|
494
|
+
*
|
|
495
|
+
* Keep this order as the single source of truth for both rendering and key
|
|
496
|
+
* navigation. `listAgents()` is newest-first; active rows come first so the
|
|
497
|
+
* panel exposes currently useful work before terminal history.
|
|
498
|
+
*/
|
|
499
|
+
private roster(): AgentRecord[] {
|
|
500
|
+
return this.rosterCacheInitialized ? this.cachedRoster : this.refreshRoster();
|
|
447
501
|
}
|
|
448
502
|
|
|
503
|
+
/** True when pi's prompt editor owns the keyboard. */
|
|
449
504
|
private editorHasFocus(): boolean {
|
|
450
505
|
const focused = (this.tui as { focusedComponent?: unknown } | undefined)?.focusedComponent;
|
|
451
506
|
return focused == null || focused instanceof Editor;
|
|
452
507
|
}
|
|
453
508
|
|
|
509
|
+
private selectedIndexOf(records: readonly AgentRecord[]): number {
|
|
510
|
+
if (!this.selectedAgentId) return -1;
|
|
511
|
+
return records.findIndex(record => record.id === this.selectedAgentId);
|
|
512
|
+
}
|
|
513
|
+
|
|
454
514
|
private deactivate(): void {
|
|
515
|
+
// Keep the selected row and viewport so re-entering navigation can resume
|
|
516
|
+
// where the user left off. Lifecycle resets (context, dispose, empty
|
|
517
|
+
// roster) clear this state explicitly instead of treating every exit as a
|
|
518
|
+
// reset.
|
|
455
519
|
this.navigationActive = false;
|
|
456
520
|
this.requestUiRefresh();
|
|
457
521
|
}
|
|
458
522
|
|
|
459
|
-
|
|
523
|
+
/** Resume navigation from the retained row, or select the first row. */
|
|
524
|
+
private activate(records: readonly AgentRecord[]): void {
|
|
525
|
+
this.navigationActive = true;
|
|
526
|
+
const selectedIndex = this.selectedIndexOf(records);
|
|
527
|
+
if (selectedIndex >= 0) {
|
|
528
|
+
this.selectedRosterIndex = selectedIndex;
|
|
529
|
+
} else {
|
|
530
|
+
// The previously selected row may have been cleaned up while navigation
|
|
531
|
+
// was inactive. Resume at its old logical position, clamped to the new
|
|
532
|
+
// roster, rather than jumping back to the first row.
|
|
533
|
+
const fallbackIndex = Math.max(0, Math.min(this.selectedRosterIndex, records.length - 1));
|
|
534
|
+
this.selectedAgentId = records[fallbackIndex].id;
|
|
535
|
+
this.selectedRosterIndex = fallbackIndex;
|
|
536
|
+
this.viewportStart = Math.min(this.viewportStart, Math.max(0, records.length - 1));
|
|
537
|
+
}
|
|
538
|
+
this.requestUiRefresh();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Move the selected row, activating only from an empty focused editor. */
|
|
542
|
+
private moveSelection(direction: -1 | 1): boolean {
|
|
543
|
+
const records = this.roster();
|
|
544
|
+
const ui = this.uiCtx;
|
|
545
|
+
if (records.length === 0 || !ui) return false;
|
|
546
|
+
|
|
547
|
+
if (!this.navigationActive) {
|
|
548
|
+
if (direction !== 1 || !this.editorHasFocus() || (ui.getEditorText?.() ?? "") !== "") return false;
|
|
549
|
+
this.activate(records);
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const currentIndex = Math.max(0, this.selectedIndexOf(records));
|
|
554
|
+
if (direction === -1 && currentIndex === 0) {
|
|
555
|
+
this.deactivate();
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
const nextIndex = Math.max(0, Math.min(records.length - 1, currentIndex + direction));
|
|
559
|
+
this.selectedAgentId = records[nextIndex].id;
|
|
560
|
+
this.selectedRosterIndex = nextIndex;
|
|
561
|
+
this.requestUiRefresh();
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private openSelected(): void {
|
|
566
|
+
const record = this.roster().find(candidate => candidate.id === this.selectedAgentId);
|
|
567
|
+
this.deactivate();
|
|
568
|
+
if (!record) return;
|
|
569
|
+
const mode: AgentWidgetOpenMode = record.status === "running" || record.status === "queued" ? "live" : "history";
|
|
570
|
+
void this.openOptions.onOpen?.(record, mode);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Handle terminal input before it reaches the focused prompt editor. */
|
|
574
|
+
handleKey(data: string): { consume?: boolean; data?: string } | undefined {
|
|
460
575
|
if (!this.uiCtx || isKeyRelease(data)) return undefined;
|
|
461
|
-
if (this.
|
|
576
|
+
if (this.navigationSuspensionDepth > 0) return undefined;
|
|
462
577
|
if (!this.editorHasFocus()) {
|
|
463
578
|
if (this.navigationActive) this.deactivate();
|
|
464
579
|
return undefined;
|
|
465
580
|
}
|
|
466
581
|
|
|
467
582
|
if (!this.navigationActive) {
|
|
468
|
-
const
|
|
469
|
-
if (!matchesKey(data, "down") || (this.uiCtx.getEditorText?.() ?? "") !== "" ||
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
this.
|
|
473
|
-
this.selectedAgentId = roster[this.selectedRosterIndex]?.id;
|
|
474
|
-
this.updateViewport(roster);
|
|
475
|
-
this.requestUiRefresh();
|
|
583
|
+
const records = this.roster();
|
|
584
|
+
if (!matchesKey(data, "down") || (this.uiCtx.getEditorText?.() ?? "") !== "" || records.length === 0) {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
this.activate(records);
|
|
476
588
|
return { consume: true };
|
|
477
589
|
}
|
|
478
590
|
|
|
@@ -480,47 +592,19 @@ export class AgentWidget {
|
|
|
480
592
|
this.deactivate();
|
|
481
593
|
return { consume: true };
|
|
482
594
|
}
|
|
483
|
-
if (matchesKey(data, "up")
|
|
484
|
-
|
|
485
|
-
if (roster.length === 0) return { consume: true };
|
|
486
|
-
if (matchesKey(data, "up") && this.selectedRosterIndex === 0) {
|
|
487
|
-
this.deactivate();
|
|
488
|
-
} else {
|
|
489
|
-
this.selectedRosterIndex = Math.max(0, Math.min(roster.length - 1, this.selectedRosterIndex + (matchesKey(data, "down") ? 1 : -1)));
|
|
490
|
-
this.selectedAgentId = roster[this.selectedRosterIndex]?.id;
|
|
491
|
-
this.updateViewport(roster);
|
|
492
|
-
this.requestUiRefresh();
|
|
493
|
-
}
|
|
494
|
-
return { consume: true };
|
|
495
|
-
}
|
|
595
|
+
if (matchesKey(data, "up")) return this.moveSelection(-1) ? { consume: true } : undefined;
|
|
596
|
+
if (matchesKey(data, "down")) return this.moveSelection(1) ? { consume: true } : undefined;
|
|
496
597
|
if (matchesKey(data, Key.enter)) {
|
|
497
|
-
|
|
498
|
-
if (record) {
|
|
499
|
-
const terminal = record.status !== "running" && record.status !== "queued";
|
|
500
|
-
if (!terminal || (this.openOptions.canOpenHistory?.(record) ?? true)) {
|
|
501
|
-
this.openOptions.onOpen?.(record, terminal ? "history" : "live");
|
|
502
|
-
}
|
|
503
|
-
}
|
|
598
|
+
this.openSelected();
|
|
504
599
|
return { consume: true };
|
|
505
600
|
}
|
|
506
601
|
|
|
507
|
-
// Only
|
|
602
|
+
// Only ↑/↓ navigate. Other keys, including j/k/←/→, flow to the editor
|
|
603
|
+
// and leave navigation mode.
|
|
508
604
|
this.deactivate();
|
|
509
605
|
return undefined;
|
|
510
606
|
}
|
|
511
607
|
|
|
512
|
-
private requestUiRefresh(force = true): boolean {
|
|
513
|
-
if (!this.tui || typeof this.tui.requestRender !== "function") return false;
|
|
514
|
-
this.tui.requestRender(force);
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
private selectionBullet(agentId: string, theme: Theme): string {
|
|
519
|
-
return this.navigationActive && this.selectedAgentId === agentId
|
|
520
|
-
? theme.fg("accent", "●")
|
|
521
|
-
: theme.fg("dim", "○");
|
|
522
|
-
}
|
|
523
|
-
|
|
524
608
|
/** Render a finished agent line. */
|
|
525
609
|
private renderFinishedLine(a: { id: string; type: SubagentType; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string; lifetimeUsage?: LifetimeUsage }, theme: Theme): string {
|
|
526
610
|
const modeLabel = getPromptModeLabel(a.type);
|
|
@@ -539,7 +623,7 @@ export class AgentWidget {
|
|
|
539
623
|
statusText = theme.fg("dim", " stopped");
|
|
540
624
|
} else if (a.status === "error") {
|
|
541
625
|
icon = theme.fg("error", "✗");
|
|
542
|
-
const errMsg = a.error ? `: ${a.error
|
|
626
|
+
const errMsg = a.error ? `: ${truncateLine(a.error)}` : "";
|
|
543
627
|
statusText = theme.fg("error", ` error${errMsg}`);
|
|
544
628
|
} else {
|
|
545
629
|
// aborted
|
|
@@ -551,15 +635,12 @@ export class AgentWidget {
|
|
|
551
635
|
const activity = this.agentActivity.get(a.id);
|
|
552
636
|
if (activity) parts.push(formatTurns(activity.turnCount, activity.maxTurns));
|
|
553
637
|
if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
|
|
554
|
-
|
|
555
|
-
// moment an agent finishes, and "what did it cost" is a question asked
|
|
556
|
-
// about finished agents.
|
|
557
|
-
const costText = this.showCost() ? formatCost(getLifetimeCost(a.lifetimeUsage)) : "";
|
|
638
|
+
const costText = this.showCost() && a.lifetimeUsage ? formatCost(getLifetimeCost(a.lifetimeUsage)) : "";
|
|
558
639
|
if (costText) parts.push(costText);
|
|
559
640
|
parts.push(duration);
|
|
560
641
|
|
|
561
642
|
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
562
|
-
const description = a.description
|
|
643
|
+
const description = truncateLine(a.description);
|
|
563
644
|
return `${icon} ${renderAgentName(a.type, theme, { fallbackColor: "dim" })}${modeTag} ${theme.fg("dim", description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
|
|
564
645
|
}
|
|
565
646
|
|
|
@@ -568,14 +649,12 @@ export class AgentWidget {
|
|
|
568
649
|
* reading live state each time instead of capturing it in a closure.
|
|
569
650
|
*/
|
|
570
651
|
private renderWidget(tui: any, theme: Theme): string[] {
|
|
571
|
-
const
|
|
572
|
-
const running =
|
|
573
|
-
const queued =
|
|
574
|
-
const finished =
|
|
575
|
-
a.status !== "running" && a.status !== "queued" && a.completedAt
|
|
576
|
-
&& this.shouldShowFinished(a.id, a.status, a.transcriptPath !== undefined),
|
|
577
|
-
);
|
|
652
|
+
const roster = this.roster();
|
|
653
|
+
const running = roster.filter(a => a.status === "running");
|
|
654
|
+
const queued = roster.filter(a => a.status === "queued");
|
|
655
|
+
const finished = roster.filter(a => a.status !== "running" && a.status !== "queued");
|
|
578
656
|
|
|
657
|
+
const selectedId = this.navigationActive ? this.selectedAgentId : undefined;
|
|
579
658
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
580
659
|
const hasFinished = finished.length > 0;
|
|
581
660
|
|
|
@@ -583,9 +662,9 @@ export class AgentWidget {
|
|
|
583
662
|
if (!hasActive && !hasFinished) return [];
|
|
584
663
|
|
|
585
664
|
const w = tui.terminal.columns;
|
|
586
|
-
const
|
|
587
|
-
if (
|
|
588
|
-
const truncate = (line: string) => truncateToWidth(line.
|
|
665
|
+
const maxLines = getWidgetLineBudget(tui.terminal.rows);
|
|
666
|
+
if (maxLines === 0) return [];
|
|
667
|
+
const truncate = (line: string, width = w) => truncateToWidth(line, Math.max(0, width));
|
|
589
668
|
const headingColor = hasActive ? "accent" : "dim";
|
|
590
669
|
const headingIcon = hasActive ? "●" : "○";
|
|
591
670
|
const frame = SPINNER[this.widgetFrame % SPINNER.length];
|
|
@@ -593,12 +672,18 @@ export class AgentWidget {
|
|
|
593
672
|
// Build sections separately for overflow-aware assembly.
|
|
594
673
|
// Each running agent = 2 lines (header + activity), finished = 1 line, queued = 1 line.
|
|
595
674
|
|
|
596
|
-
|
|
675
|
+
type WidgetRow = { record: AgentRecord; lines: string[]; recordCount?: number };
|
|
676
|
+
|
|
677
|
+
const finishedLines: WidgetRow[] = [];
|
|
597
678
|
for (const a of finished) {
|
|
598
|
-
|
|
679
|
+
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
680
|
+
finishedLines.push({
|
|
681
|
+
record: a,
|
|
682
|
+
lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ` + this.renderFinishedLine(a, theme))],
|
|
683
|
+
});
|
|
599
684
|
}
|
|
600
685
|
|
|
601
|
-
const runningLines:
|
|
686
|
+
const runningLines: WidgetRow[] = []; // each entry is [header, activity]
|
|
602
687
|
for (const a of running) {
|
|
603
688
|
const modeLabel = getPromptModeLabel(a.type);
|
|
604
689
|
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
@@ -607,9 +692,7 @@ export class AgentWidget {
|
|
|
607
692
|
const bg = this.agentActivity.get(a.id);
|
|
608
693
|
const toolUses = bg?.toolUses ?? a.toolUses;
|
|
609
694
|
// Spend comes from the record, never from the activity tracker: the record
|
|
610
|
-
//
|
|
611
|
-
// folds a hidden child's spend into. Reading the tracker while an agent
|
|
612
|
-
// runs and the record once it stops made the figure jump at completion.
|
|
695
|
+
// survives the agent finishing and includes nested-child usage.
|
|
613
696
|
const tokens = getLifetimeTotal(a.lifetimeUsage);
|
|
614
697
|
const contextPercent = getSessionContextPercent(bg?.session);
|
|
615
698
|
const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, a.compactionCount) : "";
|
|
@@ -617,9 +700,6 @@ export class AgentWidget {
|
|
|
617
700
|
|
|
618
701
|
const parts: string[] = [];
|
|
619
702
|
if (this.showModel()) {
|
|
620
|
-
// Leading, and paired: a thinking level means nothing without the model
|
|
621
|
-
// it applies to. The tag is taken from buildInvocationTags rather than
|
|
622
|
-
// rebuilt so the "(asked X)" annotation survives.
|
|
623
703
|
const { modelName, tags } = buildInvocationTags(a.invocation);
|
|
624
704
|
if (modelName) parts.push(modelName);
|
|
625
705
|
const thinkingTag = tags.find(tag => tag.startsWith("thinking: "));
|
|
@@ -632,159 +712,305 @@ export class AgentWidget {
|
|
|
632
712
|
parts.push(elapsed);
|
|
633
713
|
const statsText = parts.join(" · ");
|
|
634
714
|
|
|
635
|
-
const activity =
|
|
715
|
+
const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
|
|
636
716
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
717
|
+
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
718
|
+
runningLines.push({
|
|
719
|
+
record: a,
|
|
720
|
+
lines: [
|
|
721
|
+
truncate(theme.fg("dim", "├─") + ` ${marker} ${theme.fg("accent", frame)} ${renderAgentName(a.type, theme, { bold: true })}${modeTag} ${theme.fg("muted", truncateLine(a.description))} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}`),
|
|
722
|
+
truncate(theme.fg("dim", "│ ") + ` ${theme.fg("dim", `⎿ ${truncateLine(activity)}`)}`),
|
|
723
|
+
],
|
|
724
|
+
});
|
|
641
725
|
}
|
|
642
726
|
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
|
|
727
|
+
const queuedLines: WidgetRow[] = queued.map(a => {
|
|
728
|
+
const marker = a.id === selectedId ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
729
|
+
return {
|
|
730
|
+
record: a,
|
|
731
|
+
lines: [truncate(theme.fg("dim", "├─") + ` ${marker} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${getDisplayName(a.type)} ${truncateLine(a.description)} · queued`)}`)],
|
|
732
|
+
};
|
|
733
|
+
});
|
|
651
734
|
|
|
652
|
-
|
|
653
|
-
|
|
735
|
+
// Keep the idle widget compact: a queue is one logical status line until
|
|
736
|
+
// navigation starts. While navigating, expand it back to the same per-agent
|
|
737
|
+
// roster used by key handling so every selected queued record can render.
|
|
738
|
+
const queuedSummary: WidgetRow[] = queued.length > 0 && !this.navigationActive
|
|
739
|
+
? [{
|
|
740
|
+
record: queued[0],
|
|
741
|
+
recordCount: queued.length,
|
|
742
|
+
lines: [truncate(theme.fg("dim", "├─") + ` ${theme.fg("dim", "○")} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`)],
|
|
743
|
+
}]
|
|
744
|
+
: [];
|
|
745
|
+
|
|
746
|
+
// Assemble with a responsive cap (heading + overflow indicator = 2
|
|
747
|
+
// reserved lines when content exceeds the available body budget).
|
|
748
|
+
const maxBody = maxLines - 1; // heading takes 1 line
|
|
749
|
+
const rows: WidgetRow[] = [
|
|
750
|
+
...runningLines,
|
|
751
|
+
...(this.navigationActive ? queuedLines : queuedSummary),
|
|
752
|
+
...finishedLines,
|
|
753
|
+
];
|
|
754
|
+
const totalBody = rows.reduce((total, row) => total + row.lines.length, 0);
|
|
755
|
+
|
|
756
|
+
const heading = "Agents ↑↓ select · enter view · esc back";
|
|
757
|
+
const lines: string[] = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, heading))];
|
|
758
|
+
|
|
759
|
+
if (maxLines === 1) {
|
|
760
|
+
// There is room only for the heading; do not consume the editor/input row.
|
|
761
|
+
return lines;
|
|
762
|
+
}
|
|
654
763
|
|
|
655
764
|
if (totalBody <= maxBody) {
|
|
656
|
-
|
|
657
|
-
lines.push(...
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
lines[
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
if (runningLines.length > 0 && !queuedLine) {
|
|
668
|
-
// The last two lines are the last running agent's header + activity.
|
|
669
|
-
if (last >= 2) {
|
|
670
|
-
lines[last - 1] = lines[last - 1].replace("├─", "└─");
|
|
671
|
-
lines[last] = lines[last].replace("│ ", " ");
|
|
672
|
-
}
|
|
765
|
+
this.viewportStart = 0;
|
|
766
|
+
for (const row of rows) lines.push(...row.lines);
|
|
767
|
+
if (rows.length > 0) {
|
|
768
|
+
const lastRow = rows[rows.length - 1];
|
|
769
|
+
const lastStart = lines.length - lastRow.lines.length;
|
|
770
|
+
// Keep the selected row's bullet adjacent to the normal branch marker.
|
|
771
|
+
// This makes the active row unambiguous even when it is the last row.
|
|
772
|
+
const lastRowSelected = this.navigationActive && lastRow.record.id === selectedId;
|
|
773
|
+
if (!lastRowSelected) lines[lastStart] = lines[lastStart].replace("├─", "└─");
|
|
774
|
+
if (lastRow.lines.length === 2) {
|
|
775
|
+
lines[lastStart + 1] = lines[lastStart + 1].replace("│ ", " ");
|
|
673
776
|
}
|
|
674
777
|
}
|
|
675
|
-
} else {
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
// 1. Running agents (2 lines each)
|
|
693
|
-
for (const pair of runningLines) {
|
|
694
|
-
if (budget >= 2) {
|
|
695
|
-
lines.push(...pair);
|
|
696
|
-
budget -= 2;
|
|
778
|
+
} else if (!this.navigationActive && queuedSummary.length > 0) {
|
|
779
|
+
// In the idle view the queue is deliberately one summary row. Preserve
|
|
780
|
+
// that row before spending space on terminal history, and account for
|
|
781
|
+
// the real number of queued records if the summary itself cannot fit.
|
|
782
|
+
// Navigation expands the queue into per-record rows and uses the full
|
|
783
|
+
// contiguous viewport below.
|
|
784
|
+
const visibleRows: WidgetRow[] = [];
|
|
785
|
+
const hiddenRows: WidgetRow[] = [];
|
|
786
|
+
let bodyBudget = Math.max(0, maxBody - 1); // reserve the footer
|
|
787
|
+
let runningBudget = Math.max(0, bodyBudget - 1); // reserve queued summary
|
|
788
|
+
|
|
789
|
+
for (const row of runningLines) {
|
|
790
|
+
const height = row.lines.length;
|
|
791
|
+
if (height <= runningBudget) {
|
|
792
|
+
visibleRows.push(row);
|
|
793
|
+
runningBudget -= height;
|
|
794
|
+
bodyBudget -= height;
|
|
697
795
|
} else {
|
|
698
|
-
|
|
796
|
+
hiddenRows.push(row);
|
|
699
797
|
}
|
|
700
798
|
}
|
|
701
799
|
|
|
702
|
-
|
|
703
|
-
if (
|
|
704
|
-
|
|
705
|
-
lines.
|
|
706
|
-
|
|
800
|
+
const summaryRow = queuedSummary[0];
|
|
801
|
+
if (bodyBudget > 0) {
|
|
802
|
+
visibleRows.push(summaryRow);
|
|
803
|
+
bodyBudget -= summaryRow.lines.length;
|
|
804
|
+
} else {
|
|
805
|
+
hiddenRows.push(summaryRow);
|
|
707
806
|
}
|
|
708
807
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
|
|
808
|
+
for (const row of finishedLines) {
|
|
809
|
+
const height = row.lines.length;
|
|
810
|
+
if (height <= bodyBudget) {
|
|
811
|
+
visibleRows.push(row);
|
|
812
|
+
bodyBudget -= height;
|
|
714
813
|
} else {
|
|
715
|
-
|
|
814
|
+
hiddenRows.push(row);
|
|
716
815
|
}
|
|
717
816
|
}
|
|
718
817
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
818
|
+
for (const row of visibleRows) lines.push(...row.lines);
|
|
819
|
+
if (visibleRows.length > 0) {
|
|
820
|
+
const lastRow = visibleRows[visibleRows.length - 1];
|
|
821
|
+
const lastStart = lines.length - lastRow.lines.length;
|
|
822
|
+
lines[lastStart] = lines[lastStart].replace("├─", "└─");
|
|
823
|
+
if (lastRow.lines.length === 2) {
|
|
824
|
+
lines[lastStart + 1] = lines[lastStart + 1].replace("│ ", " ");
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const hiddenCount = (rows: readonly WidgetRow[]): number =>
|
|
829
|
+
rows.reduce((total, row) => total + (row.recordCount ?? 1), 0);
|
|
830
|
+
const categoryCount = (status: string): number => hiddenRows
|
|
831
|
+
.filter(row => row.record.status === status)
|
|
832
|
+
.reduce((total, row) => total + (row.recordCount ?? 1), 0);
|
|
833
|
+
const finishedCount = hiddenRows
|
|
834
|
+
.filter(row => row.record.status !== "running" && row.record.status !== "queued")
|
|
835
|
+
.reduce((total, row) => total + (row.recordCount ?? 1), 0);
|
|
836
|
+
const categoryCounts: string[] = [
|
|
837
|
+
["running", categoryCount("running")] as [string, number],
|
|
838
|
+
["queued", categoryCount("queued")] as [string, number],
|
|
839
|
+
["finished", finishedCount] as [string, number],
|
|
840
|
+
].filter(([, count]) => count > 0).map(([label, count]) => `${count} ${label}`);
|
|
841
|
+
const hidden = hiddenCount(hiddenRows);
|
|
842
|
+
lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hidden} more (${categoryCounts.join(", ")})`)}`));
|
|
843
|
+
} else {
|
|
844
|
+
// Reserve one line for a directional overflow summary. The viewport is
|
|
845
|
+
// a contiguous slice in roster order, so the same slice is navigable and
|
|
846
|
+
// renderable even when the selected row is currently hidden.
|
|
847
|
+
const baseContentBudget = Math.max(0, maxBody - 1);
|
|
848
|
+
const heightAt = (index: number) => rows[index]?.lines.length ?? 0;
|
|
849
|
+
const endFor = (start: number, budget: number): number => {
|
|
850
|
+
let used = 0;
|
|
851
|
+
let end = start;
|
|
852
|
+
while (end < rows.length && used + heightAt(end) <= budget) {
|
|
853
|
+
used += heightAt(end++);
|
|
854
|
+
}
|
|
855
|
+
return end;
|
|
856
|
+
};
|
|
734
857
|
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if (
|
|
740
|
-
|
|
741
|
-
if (
|
|
742
|
-
else lines.push(selectedLine);
|
|
858
|
+
let start = Math.max(0, Math.min(this.viewportStart, Math.max(0, rows.length - 1)));
|
|
859
|
+
const selectedIndex = this.navigationActive && selectedId
|
|
860
|
+
? rows.findIndex(row => row.record.id === selectedId)
|
|
861
|
+
: -1;
|
|
862
|
+
if (selectedIndex >= 0) {
|
|
863
|
+
if (selectedIndex < start) start = selectedIndex;
|
|
864
|
+
if (selectedIndex >= endFor(start, baseContentBudget)) start = selectedIndex;
|
|
743
865
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
866
|
+
|
|
867
|
+
// Keep both directional affordances in the viewport chrome: the top
|
|
868
|
+
// count sits below the heading and the bottom count sits below the
|
|
869
|
+
// bounded roster. Each consumes one row when that direction is hidden.
|
|
870
|
+
let showTopMore = start > 0;
|
|
871
|
+
let contentBudget = Math.max(0, maxBody - (showTopMore ? 2 : 1));
|
|
872
|
+
if (selectedIndex >= 0 && selectedIndex >= endFor(start, contentBudget)) {
|
|
873
|
+
start = selectedIndex;
|
|
874
|
+
showTopMore = start > 0;
|
|
875
|
+
contentBudget = Math.max(0, maxBody - (showTopMore ? 2 : 1));
|
|
876
|
+
}
|
|
877
|
+
this.viewportStart = start;
|
|
878
|
+
|
|
879
|
+
let used = 0;
|
|
880
|
+
let end = start;
|
|
881
|
+
const visibleBody: string[] = [];
|
|
882
|
+
while (end < rows.length && used + heightAt(end) <= contentBudget) {
|
|
883
|
+
visibleBody.push(...rows[end].lines);
|
|
884
|
+
used += heightAt(end);
|
|
885
|
+
end++;
|
|
886
|
+
}
|
|
887
|
+
// A selected two-line row must remain addressable even if only one body
|
|
888
|
+
// line is available. Showing its header is preferable to hiding it.
|
|
889
|
+
if (end === start && rows[start] && contentBudget > 0) {
|
|
890
|
+
visibleBody.push(rows[start].lines[0]);
|
|
891
|
+
end = start + 1;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const countRows = (selectedRows: readonly WidgetRow[]): number =>
|
|
895
|
+
selectedRows.reduce((total, row) => total + (row.recordCount ?? 1), 0);
|
|
896
|
+
const hiddenBefore = countRows(rows.slice(0, start));
|
|
897
|
+
const hiddenAfter = countRows(rows.slice(end));
|
|
898
|
+
const hidden = hiddenBefore + hiddenAfter;
|
|
899
|
+
const hiddenRows = rows.filter((_row, index) => index < start || index >= end);
|
|
900
|
+
const categoryCount = (status: string): number => hiddenRows
|
|
901
|
+
.filter(row => row.record.status === status)
|
|
902
|
+
.reduce((total, row) => total + (row.recordCount ?? 1), 0);
|
|
903
|
+
const categoryCounts: string[] = [
|
|
904
|
+
["running", categoryCount("running")] as [string, number],
|
|
905
|
+
["queued", categoryCount("queued")] as [string, number],
|
|
906
|
+
["finished", hiddenRows
|
|
907
|
+
.filter(row => row.record.status !== "running" && row.record.status !== "queued")
|
|
908
|
+
.reduce((total, row) => total + (row.recordCount ?? 1), 0)] as [string, number],
|
|
909
|
+
].filter(([, count]) => count > 0).map(([label, count]) => `${count} ${label}`);
|
|
910
|
+
const direction = [
|
|
911
|
+
...(hiddenBefore > 0 ? [`↑ ${hiddenBefore} more`] : []),
|
|
912
|
+
...(hiddenAfter > 0 ? [`↓ ${hiddenAfter} more`] : []),
|
|
913
|
+
].join(" · ");
|
|
914
|
+
const summary = `+${hidden} more (${direction}${categoryCounts.length > 0 ? `; ${categoryCounts.join(", ")}` : ""})`;
|
|
915
|
+
|
|
916
|
+
if (showTopMore) lines.push(truncate(theme.fg("dim", `↑ ${hiddenBefore} more`)));
|
|
917
|
+
|
|
918
|
+
// Fill the bounded viewport so the scrollbar track has a stable height.
|
|
919
|
+
while (visibleBody.length < contentBudget) visibleBody.push("");
|
|
920
|
+
const totalBody = rows.reduce((total, row) => total + row.lines.length, 0);
|
|
921
|
+
const startOffset = rows.slice(0, start).reduce((total, row) => total + row.lines.length, 0);
|
|
922
|
+
const trackHeight = Math.max(0, contentBudget);
|
|
923
|
+
const minThumbHeight = Math.min(2, trackHeight);
|
|
924
|
+
const thumbHeight = Math.max(
|
|
925
|
+
minThumbHeight,
|
|
926
|
+
Math.min(trackHeight, Math.round((trackHeight * trackHeight) / totalBody)),
|
|
927
|
+
);
|
|
928
|
+
const maxScrollTop = Math.max(0, totalBody - trackHeight);
|
|
929
|
+
const maxThumbTop = Math.max(0, trackHeight - thumbHeight);
|
|
930
|
+
const thumbOffset = maxScrollTop === 0
|
|
931
|
+
? 0
|
|
932
|
+
: Math.round((startOffset / maxScrollTop) * maxThumbTop);
|
|
933
|
+
const contentWidth = Math.max(0, w - 1);
|
|
934
|
+
for (let index = 0; index < visibleBody.length; index++) {
|
|
935
|
+
const isThumb = index >= thumbOffset && index < thumbOffset + thumbHeight;
|
|
936
|
+
const scrollbar = isThumb
|
|
937
|
+
? theme.fg("scrollbarThumb", this.navigationActive ? "█" : "┃")
|
|
938
|
+
: theme.fg("scrollbarTrack", "│");
|
|
939
|
+
const bodyLine = truncate(visibleBody[index] ?? "", contentWidth);
|
|
940
|
+
lines.push(bodyLine + " ".repeat(Math.max(0, contentWidth - visibleWidth(bodyLine))) + scrollbar);
|
|
747
941
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
942
|
+
|
|
943
|
+
// The directional count is outside the viewport, like ScrollView's
|
|
944
|
+
// surrounding chrome, and therefore does not consume scrollbar track.
|
|
945
|
+
if (hiddenAfter > 0) {
|
|
946
|
+
lines.push(truncate(theme.fg("dim", `↓ ${hiddenAfter} more · +${hidden} more (${categoryCounts.length > 0 ? categoryCounts.join(", ") : ""})`)));
|
|
947
|
+
} else if (!showTopMore) {
|
|
948
|
+
lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", summary)}`));
|
|
949
|
+
} else {
|
|
950
|
+
// Keep a stable footer row when the selected viewport reaches the end.
|
|
951
|
+
lines.push(truncate(theme.fg("dim", "└─")));
|
|
752
952
|
}
|
|
753
|
-
if (lines.length > budget) lines.length = budget;
|
|
754
953
|
}
|
|
954
|
+
|
|
755
955
|
return lines;
|
|
756
956
|
}
|
|
757
957
|
|
|
758
|
-
/**
|
|
958
|
+
/** Build a render-relevant snapshot without capturing mutable records. */
|
|
959
|
+
private renderKey(allAgents: ReturnType<AgentManager["listAgents"]>): string {
|
|
960
|
+
const activities = allAgents.map(a => {
|
|
961
|
+
const activity = this.agentActivity.get(a.id);
|
|
962
|
+
return activity ? {
|
|
963
|
+
id: a.id,
|
|
964
|
+
activeTools: [...activity.activeTools.entries()],
|
|
965
|
+
toolUses: activity.toolUses,
|
|
966
|
+
responseText: activity.responseText,
|
|
967
|
+
turnCount: activity.turnCount,
|
|
968
|
+
maxTurns: activity.maxTurns,
|
|
969
|
+
lifetimeUsage: activity.lifetimeUsage,
|
|
970
|
+
contextPercent: getSessionContextPercent(activity.session),
|
|
971
|
+
} : undefined;
|
|
972
|
+
});
|
|
973
|
+
return JSON.stringify({
|
|
974
|
+
frame: this.widgetFrame,
|
|
975
|
+
agents: allAgents.map(a => ({
|
|
976
|
+
id: a.id,
|
|
977
|
+
type: a.type,
|
|
978
|
+
description: a.description,
|
|
979
|
+
status: a.status,
|
|
980
|
+
toolUses: a.toolUses,
|
|
981
|
+
startedAt: a.startedAt,
|
|
982
|
+
completedAt: a.completedAt,
|
|
983
|
+
error: a.error,
|
|
984
|
+
lifetimeUsage: a.lifetimeUsage,
|
|
985
|
+
compactionCount: a.compactionCount,
|
|
986
|
+
isBackground: a.isBackground,
|
|
987
|
+
hasSession: a.session !== undefined,
|
|
988
|
+
transcriptPath: a.transcriptPath,
|
|
989
|
+
openableHistory: this.cachedCanOpenHistory(a),
|
|
990
|
+
})),
|
|
991
|
+
activities,
|
|
992
|
+
navigationActive: this.navigationActive,
|
|
993
|
+
selectedAgentId: this.selectedAgentId,
|
|
994
|
+
viewportStart: this.viewportStart,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/** Force an immediate widget update. `advanceSpinner` is reserved for the timer. */
|
|
759
999
|
update(advanceSpinner = false) {
|
|
760
1000
|
if (!this.uiCtx) return;
|
|
1001
|
+
// Refresh the manager snapshot once per lifecycle update. Arrow handling
|
|
1002
|
+
// only changes selection and requests a render; it never scans the manager.
|
|
1003
|
+
const roster = this.refreshRoster();
|
|
761
1004
|
const allAgents = this.widgetAgents();
|
|
762
|
-
if (this.navigationActive || this.selectedAgentId) {
|
|
763
|
-
const roster = this.navigationAgents();
|
|
764
|
-
const selectedIndex = this.selectedAgentId ? roster.findIndex(agent => agent.id === this.selectedAgentId) : -1;
|
|
765
|
-
if (selectedIndex >= 0) {
|
|
766
|
-
this.selectedRosterIndex = selectedIndex;
|
|
767
|
-
} else if (roster.length > 0 && this.selectedAgentId) {
|
|
768
|
-
this.selectedRosterIndex = Math.min(this.selectedRosterIndex, roster.length - 1);
|
|
769
|
-
this.selectedAgentId = roster[this.selectedRosterIndex]?.id;
|
|
770
|
-
} else if (roster.length === 0) {
|
|
771
|
-
this.navigationActive = false;
|
|
772
|
-
this.selectedAgentId = undefined;
|
|
773
|
-
this.selectedRosterIndex = 0;
|
|
774
|
-
this.viewportStart = 0;
|
|
775
|
-
}
|
|
776
|
-
this.updateViewport(roster);
|
|
777
|
-
}
|
|
778
1005
|
|
|
779
1006
|
// Lightweight existence checks — full categorization happens in renderWidget()
|
|
780
1007
|
let runningCount = 0;
|
|
781
1008
|
let queuedCount = 0;
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (a.status === "
|
|
785
|
-
else if (a.status === "queued") { queuedCount++; }
|
|
786
|
-
else if (a.completedAt && this.shouldShowFinished(a.id, a.status, a.transcriptPath !== undefined)) { hasFinished = true; }
|
|
1009
|
+
for (const a of roster) {
|
|
1010
|
+
if (a.status === "running") runningCount++;
|
|
1011
|
+
else if (a.status === "queued") queuedCount++;
|
|
787
1012
|
}
|
|
1013
|
+
const hasFinished = roster.some(a => a.status !== "running" && a.status !== "queued");
|
|
788
1014
|
const hasActive = runningCount > 0 || queuedCount > 0;
|
|
789
1015
|
|
|
790
1016
|
// Nothing to show — clear widget
|
|
@@ -798,11 +1024,12 @@ export class AgentWidget {
|
|
|
798
1024
|
this.uiCtx.setStatus("subagents", undefined);
|
|
799
1025
|
this.lastStatusText = undefined;
|
|
800
1026
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1027
|
+
this.lastRenderKey = undefined;
|
|
1028
|
+
this.navigationActive = false;
|
|
1029
|
+
this.selectedAgentId = undefined;
|
|
1030
|
+
this.selectedRosterIndex = 0;
|
|
1031
|
+
this.viewportStart = 0;
|
|
1032
|
+
this.syncTimer(false);
|
|
806
1033
|
return;
|
|
807
1034
|
}
|
|
808
1035
|
|
|
@@ -820,26 +1047,30 @@ export class AgentWidget {
|
|
|
820
1047
|
this.lastStatusText = newStatusText;
|
|
821
1048
|
}
|
|
822
1049
|
|
|
1050
|
+
// Spinner animation is driven only by the timer while a visible running
|
|
1051
|
+
// agent exists. Event-driven updates refresh changed stats without making
|
|
1052
|
+
// an otherwise idle widget advance.
|
|
823
1053
|
if (advanceSpinner && runningCount > 0) this.widgetFrame++;
|
|
824
|
-
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
this.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
Math.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1054
|
+
this.syncTimer(runningCount > 0);
|
|
1055
|
+
const selectedIndex = this.selectedIndexOf(roster);
|
|
1056
|
+
if (selectedIndex >= 0) {
|
|
1057
|
+
this.selectedRosterIndex = selectedIndex;
|
|
1058
|
+
} else if (this.navigationActive) {
|
|
1059
|
+
if (roster.length === 0) {
|
|
1060
|
+
this.navigationActive = false;
|
|
1061
|
+
this.selectedAgentId = undefined;
|
|
1062
|
+
this.selectedRosterIndex = 0;
|
|
1063
|
+
this.viewportStart = 0;
|
|
1064
|
+
} else {
|
|
1065
|
+
// Keep the selection near the row that disappeared. Both the saved
|
|
1066
|
+
// logical index and viewport are clamped as the roster shrinks.
|
|
1067
|
+
const fallbackIndex = Math.max(0, Math.min(this.selectedRosterIndex, roster.length - 1));
|
|
1068
|
+
this.selectedAgentId = roster[fallbackIndex].id;
|
|
1069
|
+
this.selectedRosterIndex = fallbackIndex;
|
|
1070
|
+
this.viewportStart = Math.min(this.viewportStart, roster.length - 1);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const renderKey = this.renderKey(allAgents);
|
|
843
1074
|
|
|
844
1075
|
// Register widget callback once; subsequent updates use requestRender()
|
|
845
1076
|
// which re-invokes render() without replacing the component (avoids layout thrashing).
|
|
@@ -852,23 +1083,20 @@ export class AgentWidget {
|
|
|
852
1083
|
// Theme changed — force re-registration so factory captures fresh theme.
|
|
853
1084
|
this.widgetRegistered = false;
|
|
854
1085
|
this.tui = undefined;
|
|
1086
|
+
this.lastRenderKey = undefined;
|
|
855
1087
|
},
|
|
856
1088
|
};
|
|
857
1089
|
}, { placement: "aboveEditor" });
|
|
858
1090
|
this.widgetRegistered = true;
|
|
859
|
-
} else {
|
|
860
|
-
// Widget already registered —
|
|
1091
|
+
} else if (renderKey !== this.lastRenderKey) {
|
|
1092
|
+
// Widget already registered — request a re-render only when visible state
|
|
1093
|
+
// changed (or the spinner timer advanced).
|
|
861
1094
|
this.tui?.requestRender();
|
|
862
1095
|
}
|
|
1096
|
+
this.lastRenderKey = renderKey;
|
|
863
1097
|
}
|
|
864
1098
|
|
|
865
1099
|
dispose() {
|
|
866
|
-
this.inputCleanup?.();
|
|
867
|
-
this.inputCleanup = undefined;
|
|
868
|
-
this.navigationActive = false;
|
|
869
|
-
this.selectedAgentId = undefined;
|
|
870
|
-
this.viewportStart = 0;
|
|
871
|
-
this.selectedRosterIndex = 0;
|
|
872
1100
|
if (this.widgetInterval) {
|
|
873
1101
|
clearInterval(this.widgetInterval);
|
|
874
1102
|
this.widgetInterval = undefined;
|
|
@@ -877,9 +1105,22 @@ export class AgentWidget {
|
|
|
877
1105
|
this.uiCtx.setWidget("agents", undefined);
|
|
878
1106
|
this.uiCtx.setStatus("subagents", undefined);
|
|
879
1107
|
}
|
|
1108
|
+
this.inputUnsub?.();
|
|
1109
|
+
this.inputUnsub = undefined;
|
|
880
1110
|
this.widgetRegistered = false;
|
|
881
1111
|
this.tui = undefined;
|
|
882
1112
|
this.lastStatusText = undefined;
|
|
883
|
-
this.
|
|
1113
|
+
this.lastRenderKey = undefined;
|
|
1114
|
+
this.navigationSuspensionDepth = 0;
|
|
1115
|
+
this.navigationActive = false;
|
|
1116
|
+
this.selectedAgentId = undefined;
|
|
1117
|
+
this.selectedRosterIndex = 0;
|
|
1118
|
+
this.viewportStart = 0;
|
|
1119
|
+
this.cachedAgents = [];
|
|
1120
|
+
this.cachedRoster = [];
|
|
1121
|
+
this.rosterCacheKey = undefined;
|
|
1122
|
+
this.rosterCacheInitialized = false;
|
|
1123
|
+
this.historyOpenabilityCache.clear();
|
|
1124
|
+
this.uiCtx = undefined;
|
|
884
1125
|
}
|
|
885
1126
|
}
|