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