@esso0428/pi-subagents 0.17.8 → 0.17.10

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.
@@ -5,7 +5,7 @@
5
5
  * Uses the callback form of setWidget for themed rendering.
6
6
  */
7
7
 
8
- import { truncateToWidth } from "@earendil-works/pi-tui";
8
+ import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
9
9
  import { renderAgentName } from "../agent-color.js";
10
10
  import { type AgentManager, isTopLevelAgent } from "../agent-manager.js";
11
11
  import { getConfig } from "../agent-types.js";
@@ -19,7 +19,8 @@ export const MAX_WIDGET_LINES = 12;
19
19
 
20
20
  /** Number of lines available above an editor, retaining room for the prompt. */
21
21
  export function getWidgetLineBudget(terminalRows: number): number {
22
- return Math.max(1, Math.min(MAX_WIDGET_LINES, Math.floor(terminalRows) - 4));
22
+ if (!Number.isFinite(terminalRows)) return MAX_WIDGET_LINES;
23
+ return Math.min(MAX_WIDGET_LINES, Math.max(0, Math.floor(terminalRows) - 4));
23
24
  }
24
25
 
25
26
  export type AgentWidgetOpenMode = "live" | "history";
@@ -348,19 +349,19 @@ export class AgentWidget {
348
349
  }
349
350
 
350
351
  /** Set the UI context (grabbed from first tool execution). */
351
- setUICtx(ctx: UICtx) {
352
- if (ctx !== this.uiCtx) {
353
- this.inputCleanup?.();
354
- this.inputCleanup = undefined;
355
- this.uiCtx = ctx;
356
- this.widgetRegistered = false;
357
- this.tui = undefined;
358
- this.lastStatusText = undefined;
359
- this.navigationActive = false;
360
- this.selectedAgentId = undefined;
361
- this.viewportStart = 0;
362
- if (ctx.onTerminalInput) this.inputCleanup = ctx.onTerminalInput(data => this.handleInput(data));
363
- }
352
+ setUICtx(ctx: UICtx): boolean {
353
+ if (ctx === this.uiCtx) return false;
354
+ this.inputCleanup?.();
355
+ this.inputCleanup = undefined;
356
+ this.uiCtx = ctx;
357
+ this.widgetRegistered = false;
358
+ this.tui = undefined;
359
+ this.lastStatusText = undefined;
360
+ this.navigationActive = false;
361
+ this.selectedAgentId = undefined;
362
+ this.viewportStart = 0;
363
+ if (ctx.onTerminalInput) this.inputCleanup = ctx.onTerminalInput(data => this.handleInput(data));
364
+ return true;
364
365
  }
365
366
 
366
367
  /**
@@ -379,7 +380,7 @@ export class AgentWidget {
379
380
  /** Ensure the widget update timer is running. */
380
381
  ensureTimer() {
381
382
  if (!this.widgetInterval) {
382
- this.widgetInterval = setInterval(() => this.update(), 80);
383
+ this.widgetInterval = setInterval(() => this.update(true), 250);
383
384
  }
384
385
  }
385
386
 
@@ -429,13 +430,26 @@ export class AgentWidget {
429
430
  this.viewportStart = Math.max(0, Math.min(this.viewportStart, Math.max(0, roster.length - visibleRows)));
430
431
  }
431
432
 
433
+ private editorHasFocus(): boolean {
434
+ const focused = (this.tui as { focusedComponent?: unknown } | undefined)?.focusedComponent;
435
+ return focused == null || focused instanceof Editor;
436
+ }
437
+
438
+ private deactivate(): void {
439
+ this.navigationActive = false;
440
+ this.requestUiRefresh();
441
+ }
442
+
432
443
  private handleInput(data: string): { consume?: boolean; data?: string } | undefined {
433
- const down = data === "\u001b[B";
434
- const up = data === "\u001b[A";
444
+ if (!this.uiCtx || isKeyRelease(data)) return undefined;
445
+ if (!this.editorHasFocus()) {
446
+ if (this.navigationActive) this.deactivate();
447
+ return undefined;
448
+ }
449
+
435
450
  if (!this.navigationActive) {
436
- if (!down || !this.tui || this.uiCtx?.getEditorText?.() !== "") return undefined;
437
451
  const roster = this.navigationAgents();
438
- if (roster.length === 0) return undefined;
452
+ if (!matchesKey(data, "down") || (this.uiCtx.getEditorText?.() ?? "") !== "" || roster.length === 0) return undefined;
439
453
  this.navigationActive = true;
440
454
  const index = this.selectedAgentId ? roster.findIndex(agent => agent.id === this.selectedAgentId) : -1;
441
455
  this.selectedRosterIndex = index >= 0 ? index : 0;
@@ -444,25 +458,25 @@ export class AgentWidget {
444
458
  this.requestUiRefresh();
445
459
  return { consume: true };
446
460
  }
447
- if (data === "\u001b") {
448
- this.navigationActive = false;
449
- this.requestUiRefresh();
461
+
462
+ if (matchesKey(data, "escape")) {
463
+ this.deactivate();
450
464
  return { consume: true };
451
465
  }
452
- if (down || up) {
466
+ if (matchesKey(data, "up") || matchesKey(data, "down")) {
453
467
  const roster = this.navigationAgents();
454
468
  if (roster.length === 0) return { consume: true };
455
- if (up && this.selectedRosterIndex === 0) {
456
- this.navigationActive = false;
469
+ if (matchesKey(data, "up") && this.selectedRosterIndex === 0) {
470
+ this.deactivate();
457
471
  } else {
458
- this.selectedRosterIndex = Math.max(0, Math.min(roster.length - 1, this.selectedRosterIndex + (down ? 1 : -1)));
472
+ this.selectedRosterIndex = Math.max(0, Math.min(roster.length - 1, this.selectedRosterIndex + (matchesKey(data, "down") ? 1 : -1)));
459
473
  this.selectedAgentId = roster[this.selectedRosterIndex]?.id;
460
474
  this.updateViewport(roster);
475
+ this.requestUiRefresh();
461
476
  }
462
- this.requestUiRefresh();
463
477
  return { consume: true };
464
478
  }
465
- if (data === "\r" || data === "\n") {
479
+ if (matchesKey(data, Key.enter)) {
466
480
  const record = this.navigationAgents()[this.selectedRosterIndex];
467
481
  if (record) {
468
482
  const terminal = record.status !== "running" && record.status !== "queued";
@@ -472,16 +486,24 @@ export class AgentWidget {
472
486
  }
473
487
  return { consume: true };
474
488
  }
475
- this.navigationActive = false;
489
+
490
+ // Only arrow keys navigate. Other keys flow to the editor and leave navigation mode.
491
+ this.deactivate();
476
492
  return undefined;
477
493
  }
478
494
 
479
- private requestUiRefresh(force = false): boolean {
495
+ private requestUiRefresh(force = true): boolean {
480
496
  if (!this.tui || typeof this.tui.requestRender !== "function") return false;
481
497
  this.tui.requestRender(force);
482
498
  return true;
483
499
  }
484
500
 
501
+ private selectionBullet(agentId: string, theme: Theme): string {
502
+ return this.navigationActive && this.selectedAgentId === agentId
503
+ ? theme.fg("accent", "●")
504
+ : theme.fg("dim", "○");
505
+ }
506
+
485
507
  /** Render a finished agent line. */
486
508
  private renderFinishedLine(a: { id: string; type: SubagentType; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string; lifetimeUsage?: LifetimeUsage }, theme: Theme): string {
487
509
  const modeLabel = getPromptModeLabel(a.type);
@@ -545,6 +567,7 @@ export class AgentWidget {
545
567
 
546
568
  const w = tui.terminal.columns;
547
569
  const budget = getWidgetLineBudget(tui.terminal.rows ?? 24);
570
+ if (budget === 0) return [];
548
571
  const truncate = (line: string) => truncateToWidth(line.replace(/[\r\n]+/g, " "), w);
549
572
  const headingColor = hasActive ? "accent" : "dim";
550
573
  const headingIcon = hasActive ? "●" : "○";
@@ -555,7 +578,7 @@ export class AgentWidget {
555
578
 
556
579
  const finishedLines: string[] = [];
557
580
  for (const a of finished) {
558
- finishedLines.push(truncate(theme.fg("dim", "├─") + " " + this.renderFinishedLine(a, theme)));
581
+ finishedLines.push(truncate(theme.fg("dim", "├─") + ` ${this.selectionBullet(a.id, theme)} ` + this.renderFinishedLine(a, theme)));
559
582
  }
560
583
 
561
584
  const runningLines: string[][] = []; // each entry is [header, activity]
@@ -595,13 +618,14 @@ export class AgentWidget {
595
618
  const activity = (bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…").replace(/[\r\n]+/g, " ");
596
619
 
597
620
  runningLines.push([
598
- truncate(theme.fg("dim", "├─") + ` ${theme.fg("accent", frame)} ${renderAgentName(a.type, theme, { bold: true })}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}`),
621
+ truncate(theme.fg("dim", "├─") + ` ${this.selectionBullet(a.id, theme)} ${theme.fg("accent", frame)} ${renderAgentName(a.type, theme, { bold: true })}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${fgPreservingNestedStyles(theme, "dim", statsText)}`),
599
622
  truncate(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`)),
600
623
  ]);
601
624
  }
602
625
 
626
+ const selectedQueued = this.navigationActive && this.selectedAgentId !== undefined && queued.some(agent => agent.id === this.selectedAgentId);
603
627
  const queuedLine = queued.length > 0
604
- ? truncate(theme.fg("dim", "├─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`)
628
+ ? truncate(theme.fg("dim", "├─") + ` ${selectedQueued ? theme.fg("accent", "●") : theme.fg("dim", "○")} ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`)
605
629
  : undefined;
606
630
 
607
631
  // Assemble with overflow cap (heading + overflow indicator = 2 reserved lines).
@@ -696,7 +720,7 @@ export class AgentWidget {
696
720
  const selected = roster[this.selectedRosterIndex];
697
721
  const hasSelected = selected ? lines.some(line => line.includes(selected.description.replace(/[\r\n]+/g, " "))) : true;
698
722
  if (selected && !hasSelected) {
699
- const selectedLine = truncate(theme.fg("dim", "└─") + ` ${selected.status === "queued" ? "◦" : "⠋"} ${theme.fg("muted", selected.description.replace(/[\r\n]+/g, " "))}`);
723
+ const selectedLine = truncate(theme.fg("dim", "└─") + ` ${this.selectionBullet(selected.id, theme)} ${selected.status === "queued" ? "◦" : "⠋"} ${theme.fg("muted", selected.description.replace(/[\r\n]+/g, " "))}`);
700
724
  if (lines.length >= budget) lines[lines.length - 1] = selectedLine;
701
725
  else lines.push(selectedLine);
702
726
  }
@@ -715,7 +739,7 @@ export class AgentWidget {
715
739
  }
716
740
 
717
741
  /** Force an immediate widget update. */
718
- update() {
742
+ update(advanceSpinner = false) {
719
743
  if (!this.uiCtx) return;
720
744
  const allAgents = this.widgetAgents();
721
745
  if (this.navigationActive || this.selectedAgentId) {
@@ -779,6 +803,8 @@ export class AgentWidget {
779
803
  this.lastStatusText = newStatusText;
780
804
  }
781
805
 
806
+ if (advanceSpinner && runningCount > 0) this.widgetFrame++;
807
+
782
808
  const snapshot = allAgents.map(agent => {
783
809
  const activity = this.agentActivity.get(agent.id);
784
810
  return [
@@ -794,9 +820,9 @@ export class AgentWidget {
794
820
  Math.floor((Date.now() - agent.startedAt) / 1000),
795
821
  ].join("\u0000");
796
822
  }).join("\u0001");
797
- if (this.widgetRegistered && snapshot === this.lastSnapshot) return;
823
+ if (this.widgetRegistered && snapshot === this.lastSnapshot && !advanceSpinner) return;
798
824
  this.lastSnapshot = snapshot;
799
- this.widgetFrame++;
825
+ if (!advanceSpinner) this.widgetFrame++;
800
826
 
801
827
  // Register widget callback once; subsequent updates use requestRender()
802
828
  // which re-invokes render() without replacing the component (avoids layout thrashing).