@esso0428/pi-subagents 0.15.4 → 0.15.6

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +4 -17
  2. package/README.md +5 -5
  3. package/dist/agent-history-list.d.ts +18 -0
  4. package/dist/agent-history-list.js +68 -0
  5. package/dist/agent-history.d.ts +1 -1
  6. package/dist/agent-history.js +31 -34
  7. package/dist/agent-manager.d.ts +22 -8
  8. package/dist/agent-manager.js +100 -93
  9. package/dist/agent-runner.js +67 -86
  10. package/dist/agent-types.js +27 -45
  11. package/dist/context.js +2 -6
  12. package/dist/cross-extension-rpc.js +5 -9
  13. package/dist/custom-agents.js +15 -18
  14. package/dist/default-agents.js +1 -4
  15. package/dist/enabled-models.js +11 -16
  16. package/dist/env.js +1 -4
  17. package/dist/group-join.js +1 -5
  18. package/dist/index.js +273 -284
  19. package/dist/invocation-config.js +2 -6
  20. package/dist/memory.js +24 -34
  21. package/dist/model-resolver.js +1 -4
  22. package/dist/nico-overrides.js +14 -20
  23. package/dist/output-file.js +16 -22
  24. package/dist/prompts.js +1 -4
  25. package/dist/schedule-store.js +16 -21
  26. package/dist/schedule.js +8 -12
  27. package/dist/settings.js +15 -23
  28. package/dist/skill-loader.js +20 -23
  29. package/dist/status-note.js +1 -4
  30. package/dist/types.d.ts +0 -1
  31. package/dist/types.js +1 -4
  32. package/dist/ui/agent-widget.d.ts +1 -5
  33. package/dist/ui/agent-widget.js +25 -49
  34. package/dist/ui/conversation-blocks.d.ts +18 -0
  35. package/dist/ui/conversation-blocks.js +214 -0
  36. package/dist/ui/conversation-nvim.d.ts +7 -0
  37. package/dist/ui/conversation-nvim.js +116 -0
  38. package/dist/ui/conversation-search.d.ts +38 -0
  39. package/dist/ui/conversation-search.js +123 -0
  40. package/dist/ui/conversation-viewer.d.ts +55 -19
  41. package/dist/ui/conversation-viewer.js +398 -215
  42. package/dist/ui/fleet-list.d.ts +23 -11
  43. package/dist/ui/fleet-list.js +85 -78
  44. package/dist/ui/schedule-menu.js +1 -4
  45. package/dist/ui/viewer-keys.js +7 -10
  46. package/dist/usage.js +4 -10
  47. package/dist/worktree.js +26 -31
  48. package/package.json +1 -1
  49. package/src/agent-history-list.ts +91 -0
  50. package/src/agent-history.ts +12 -17
  51. package/src/agent-manager.ts +108 -81
  52. package/src/agent-runner.ts +5 -13
  53. package/src/index.ts +84 -85
  54. package/src/output-file.ts +1 -1
  55. package/src/types.ts +0 -1
  56. package/src/ui/agent-widget.ts +1 -8
  57. package/src/ui/conversation-blocks.ts +270 -0
  58. package/src/ui/conversation-nvim.ts +127 -0
  59. package/src/ui/conversation-search.ts +161 -0
  60. package/src/ui/conversation-viewer.ts +376 -205
  61. package/src/ui/fleet-list.ts +73 -52
  62. package/test/agent-history-list.test.ts +121 -0
  63. package/test/agent-manager-history.test.ts +59 -58
  64. package/test/agent-menu-history-navigation.test.ts +86 -0
  65. package/test/ui/conversation-blocks.test.ts +72 -0
  66. package/test/ui/conversation-search.test.ts +55 -0
  67. package/test/ui/conversation-viewer.test.ts +132 -0
  68. package/test/ui/fleet-list.test.ts +131 -0
  69. package/dist/ui/markdown-result.d.ts +0 -3
  70. package/dist/ui/markdown-result.js +0 -53
  71. package/src/ui/markdown-result.ts +0 -56
  72. package/test/agent-history.test.ts +0 -63
  73. package/test/ui/markdown-result.test.ts +0 -45
@@ -4,14 +4,17 @@
4
4
  * Shows `main` + each running/queued subagent as a navigable list. Pressing ↓ (or
5
5
  * ←) at an empty prompt activates the list; ↑/↓ move the selection (filled ● marker),
6
6
  * Enter opens the selected agent's live conversation overlay, Esc returns to the prompt.
7
- * A viewer stays open when its agent finishes; finished agents linger briefly in the list.
7
+ * A viewer stays open when its agent finishes so the final output remains readable.
8
8
  *
9
9
  * Mechanics (see plan): the list is a `belowEditor` widget (render-only), and ALL key
10
10
  * handling goes through `onTerminalInput` — which fires before the focused editor and
11
11
  * can `consume` keys — gated on `getEditorText() === ""` so normal typing is untouched.
12
12
  */
13
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
14
  import type { AgentManager } from "../agent-manager.js";
14
15
  import { type AgentActivity, type Theme } from "./agent-widget.js";
16
+ /** Maximum total rows contributed by FleetView, including hint and indicators. */
17
+ export declare const FLEET_MAX_RENDER_ROWS = 6;
15
18
  /** Minimal UI surface the FleetView needs from `ctx.ui` (structural subset). */
16
19
  export type FleetUICtx = {
17
20
  setWidget(key: string, content: undefined | ((tui: any, theme: Theme) => {
@@ -41,14 +44,27 @@ export type FleetUICtx = {
41
44
  export declare function formatFleetElapsed(ms: number): string;
42
45
  /** `↓ 13.1k tokens` — down-arrow prefix, compact magnitude, plural "tokens". */
43
46
  export declare function formatFleetTokens(count: number): string;
47
+ /**
48
+ * Calculate a bounded agent slice for the complete logical roster.
49
+ * `selectedRosterIndex` includes main at index 0; `end` is exclusive.
50
+ */
51
+ export declare function calculateFleetAgentWindow(agentCount: number, selectedRosterIndex: number): {
52
+ start: number;
53
+ end: number;
54
+ hiddenAbove: number;
55
+ hiddenBelow: number;
56
+ };
44
57
  export declare class FleetList {
45
58
  private manager;
46
59
  private agentActivity;
47
60
  private getCwd;
61
+ private pi?;
62
+ private getCtx;
48
63
  private ui;
49
64
  private tui;
50
65
  private inputUnsub;
51
66
  private widgetRegistered;
67
+ private lastRosterKey;
52
68
  private timer;
53
69
  private enabled;
54
70
  /** Whether arrow keys currently navigate the list (vs. flow to the editor). */
@@ -58,7 +74,7 @@ export declare class FleetList {
58
74
  /** Set while a conversation overlay is open; calling it closes the overlay. */
59
75
  private viewerClose;
60
76
  private viewingAgentId;
61
- constructor(manager: AgentManager, agentActivity: Map<string, AgentActivity>, getCwd?: () => string | undefined);
77
+ constructor(manager: AgentManager, agentActivity: Map<string, AgentActivity>, getCwd?: () => string | undefined, pi?: ExtensionAPI | undefined, getCtx?: () => ExtensionContext | undefined);
62
78
  setEnabled(enabled: boolean): void;
63
79
  /** Capture the UI context and (re)register the global input handler. */
64
80
  setUICtx(ui: FleetUICtx): void;
@@ -66,21 +82,17 @@ export declare class FleetList {
66
82
  ensureTimer(): void;
67
83
  /**
68
84
  * Called when an agent finishes. The viewer (if open on it) stays open so the
69
- * final output remains readable, and the row lingers in the list — just refresh.
85
+ * final output remains readable; refresh the active-only roster.
70
86
  */
71
87
  onAgentFinished(_id: string): void;
72
88
  dispose(): void;
73
89
  /** Re-register/refresh the below-editor widget; clears it when no agents remain. */
74
90
  update(): void;
75
91
  /**
76
- * Agents shown in the list, ordered earliest-launched first so the ones you
77
- * started sooner sit at the top. Live rows are openable through their session;
78
- * terminal rows are included only when their durable transcript exists. This
79
- * prevents Enter from dead-ending on stale rehydrated metadata.
80
- * Included: running/queued, plus the agent currently being viewed, plus
81
- * recently-finished ones (they linger briefly before dropping out).
82
- * Pending agents with no session yet are hidden until they start.
83
- * (`listAgents()` is newest-first, so we re-sort.)
92
+ * Active agents shown in the list, ordered earliest-launched first so the
93
+ * ones you started sooner sit at the top. Pending agents with no session yet
94
+ * are hidden until they start. (`listAgents()` is newest-first, so we
95
+ * re-sort.)
84
96
  */
85
97
  private agentRecords;
86
98
  private roster;
@@ -1,39 +1,34 @@
1
- "use strict";
2
1
  /**
3
2
  * fleet-list.ts — Claude Code-style "FleetView" list rendered below the editor.
4
3
  *
5
4
  * Shows `main` + each running/queued subagent as a navigable list. Pressing ↓ (or
6
5
  * ←) at an empty prompt activates the list; ↑/↓ move the selection (filled ● marker),
7
6
  * Enter opens the selected agent's live conversation overlay, Esc returns to the prompt.
8
- * A viewer stays open when its agent finishes; finished agents linger briefly in the list.
7
+ * A viewer stays open when its agent finishes so the final output remains readable.
9
8
  *
10
9
  * Mechanics (see plan): the list is a `belowEditor` widget (render-only), and ALL key
11
10
  * handling goes through `onTerminalInput` — which fires before the focused editor and
12
11
  * can `consume` keys — gated on `getEditorText() === ""` so normal typing is untouched.
13
12
  */
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.FleetList = void 0;
16
- exports.formatFleetElapsed = formatFleetElapsed;
17
- exports.formatFleetTokens = formatFleetTokens;
18
- const pi_tui_1 = require("@earendil-works/pi-tui");
19
- const agent_history_js_1 = require("../agent-history.js");
20
- const usage_js_1 = require("../usage.js");
21
- const agent_widget_js_1 = require("./agent-widget.js");
22
- const conversation_viewer_js_1 = require("./conversation-viewer.js");
13
+ import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
14
+ import { canOpenActiveAgent } from "../agent-history-list.js";
15
+ import { getLifetimeTotal } from "../usage.js";
16
+ import { getDisplayName } from "./agent-widget.js";
17
+ import { ConversationViewer, VIEWPORT_HEIGHT_PCT } from "./conversation-viewer.js";
23
18
  /** Widget key for the below-editor fleet list. */
24
19
  const FLEET_KEY = "fleet";
25
- /** Max agent rows shown at once; extras collapse into a "↓ N more" indicator. */
26
- const MAX_AGENT_ROWS = 5;
20
+ /** Maximum total rows contributed by FleetView, including hint and indicators. */
21
+ export const FLEET_MAX_RENDER_ROWS = 6;
22
+ const FLEET_BASE_ROWS = 2; // hint + main
23
+ const FLEET_BODY_ROWS = FLEET_MAX_RENDER_ROWS - FLEET_BASE_ROWS;
27
24
  /** Re-render cadence so elapsed/token stats tick while agents run. */
28
25
  const TICK_MS = 200;
29
- /** How long a finished agent lingers in the list before it drops out. */
30
- const FINISHED_LINGER_MS = 4000;
31
26
  /** `11s` — integer seconds, no decimal/suffix (matches Claude Code, unlike formatMs). */
32
- function formatFleetElapsed(ms) {
27
+ export function formatFleetElapsed(ms) {
33
28
  return `${Math.max(0, Math.round(ms / 1000))}s`;
34
29
  }
35
30
  /** `↓ 13.1k tokens` — down-arrow prefix, compact magnitude, plural "tokens". */
36
- function formatFleetTokens(count) {
31
+ export function formatFleetTokens(count) {
37
32
  let compact;
38
33
  if (count >= 1_000_000)
39
34
  compact = `${(count / 1_000_000).toFixed(1)}M`;
@@ -43,26 +38,45 @@ function formatFleetTokens(count) {
43
38
  compact = `${count}`;
44
39
  return `↓ ${compact} tokens`;
45
40
  }
41
+ /**
42
+ * Calculate a bounded agent slice for the complete logical roster.
43
+ * `selectedRosterIndex` includes main at index 0; `end` is exclusive.
44
+ */
45
+ export function calculateFleetAgentWindow(agentCount, selectedRosterIndex) {
46
+ const count = Math.max(0, Math.floor(agentCount));
47
+ if (count === 0)
48
+ return { start: 0, end: 0, hiddenAbove: 0, hiddenBelow: 0 };
49
+ const selectedAgent = Math.min(count - 1, Math.max(0, Math.floor(selectedRosterIndex) - 1));
50
+ const reserveAbove = selectedAgent > 0 ? 1 : 0;
51
+ const reserveBelow = selectedAgent < count - 1 ? 1 : 0;
52
+ const visibleCount = Math.max(1, FLEET_BODY_ROWS - reserveAbove - reserveBelow);
53
+ const start = Math.min(Math.max(0, selectedAgent - visibleCount + 1), Math.max(0, count - visibleCount));
54
+ const end = Math.min(count, start + visibleCount);
55
+ return { start, end, hiddenAbove: start, hiddenBelow: count - end };
56
+ }
46
57
  /**
47
58
  * Place `right` flush to `width`, truncating `left` first so the stats survive.
48
59
  * The final clamp guarantees the line never exceeds `width` (which would wrap and
49
60
  * desync pi's line-diff → flicker) even on a terminal too narrow for the stats.
50
61
  */
51
62
  function rightAlign(left, right, width) {
52
- const rightW = (0, pi_tui_1.visibleWidth)(right);
63
+ const rightW = visibleWidth(right);
53
64
  const maxLeft = Math.max(0, width - rightW - 1);
54
- const leftClamped = (0, pi_tui_1.truncateToWidth)(left, maxLeft);
55
- const gap = Math.max(1, width - (0, pi_tui_1.visibleWidth)(leftClamped) - rightW);
56
- return (0, pi_tui_1.truncateToWidth)(leftClamped + " ".repeat(gap) + right, width);
65
+ const leftClamped = truncateToWidth(left, maxLeft);
66
+ const gap = Math.max(1, width - visibleWidth(leftClamped) - rightW);
67
+ return truncateToWidth(leftClamped + " ".repeat(gap) + right, width);
57
68
  }
58
- class FleetList {
69
+ export class FleetList {
59
70
  manager;
60
71
  agentActivity;
61
72
  getCwd;
73
+ pi;
74
+ getCtx;
62
75
  ui;
63
76
  tui;
64
77
  inputUnsub;
65
78
  widgetRegistered = false;
79
+ lastRosterKey;
66
80
  timer;
67
81
  enabled = true;
68
82
  /** Whether arrow keys currently navigate the list (vs. flow to the editor). */
@@ -72,10 +86,12 @@ class FleetList {
72
86
  /** Set while a conversation overlay is open; calling it closes the overlay. */
73
87
  viewerClose;
74
88
  viewingAgentId;
75
- constructor(manager, agentActivity, getCwd = () => undefined) {
89
+ constructor(manager, agentActivity, getCwd = () => undefined, pi, getCtx = () => undefined) {
76
90
  this.manager = manager;
77
91
  this.agentActivity = agentActivity;
78
92
  this.getCwd = getCwd;
93
+ this.pi = pi;
94
+ this.getCtx = getCtx;
79
95
  }
80
96
  // ---- Lifecycle ----
81
97
  setEnabled(enabled) {
@@ -93,6 +109,7 @@ class FleetList {
93
109
  this.inputUnsub?.();
94
110
  this.ui = ui;
95
111
  this.widgetRegistered = false;
112
+ this.lastRosterKey = undefined;
96
113
  this.tui = undefined;
97
114
  this.inputUnsub = ui.onTerminalInput(data => this.handleKey(data));
98
115
  }
@@ -103,7 +120,7 @@ class FleetList {
103
120
  }
104
121
  /**
105
122
  * Called when an agent finishes. The viewer (if open on it) stays open so the
106
- * final output remains readable, and the row lingers in the list — just refresh.
123
+ * final output remains readable; refresh the active-only roster.
107
124
  */
108
125
  onAgentFinished(_id) {
109
126
  this.update();
@@ -123,6 +140,7 @@ class FleetList {
123
140
  if (this.ui && this.widgetRegistered)
124
141
  this.ui.setWidget(FLEET_KEY, undefined);
125
142
  this.widgetRegistered = false;
143
+ this.lastRosterKey = undefined;
126
144
  this.tui = undefined;
127
145
  this.active = false;
128
146
  // Null last so a `viewerClose()` microtask above can't re-register the widget.
@@ -132,7 +150,11 @@ class FleetList {
132
150
  update() {
133
151
  if (!this.ui)
134
152
  return;
135
- const hasAgents = this.enabled && this.agentRecords().length > 0;
153
+ const records = this.agentRecords();
154
+ const hasAgents = this.enabled && records.length > 0;
155
+ const rosterKey = records.map(record => record.id).join("\0");
156
+ const rosterChanged = rosterKey !== this.lastRosterKey;
157
+ this.lastRosterKey = rosterKey;
136
158
  if (!hasAgents) {
137
159
  if (this.widgetRegistered) {
138
160
  this.ui.setWidget(FLEET_KEY, undefined);
@@ -145,6 +167,7 @@ class FleetList {
145
167
  }
146
168
  this.active = false;
147
169
  this.selectedIndex = 0;
170
+ this.lastRosterKey = undefined;
148
171
  return;
149
172
  }
150
173
  this.clampSelection();
@@ -160,31 +183,19 @@ class FleetList {
160
183
  this.widgetRegistered = true;
161
184
  }
162
185
  else {
163
- this.tui?.requestRender();
186
+ this.tui?.requestRender(rosterChanged);
164
187
  }
165
188
  }
166
189
  // ---- Roster ----
167
190
  /**
168
- * Agents shown in the list, ordered earliest-launched first so the ones you
169
- * started sooner sit at the top. Live rows are openable through their session;
170
- * terminal rows are included only when their durable transcript exists. This
171
- * prevents Enter from dead-ending on stale rehydrated metadata.
172
- * Included: running/queued, plus the agent currently being viewed, plus
173
- * recently-finished ones (they linger briefly before dropping out).
174
- * Pending agents with no session yet are hidden until they start.
175
- * (`listAgents()` is newest-first, so we re-sort.)
191
+ * Active agents shown in the list, ordered earliest-launched first so the
192
+ * ones you started sooner sit at the top. Pending agents with no session yet
193
+ * are hidden until they start. (`listAgents()` is newest-first, so we
194
+ * re-sort.)
176
195
  */
177
196
  agentRecords() {
178
- const now = Date.now();
179
197
  return this.manager.listAgents()
180
- .filter(a => {
181
- const live = a.session && (a.status === "running" || a.status === "queued"
182
- || a.id === this.viewingAgentId
183
- || (a.completedAt != null && now - a.completedAt < FINISHED_LINGER_MS));
184
- const cwd = this.getCwd();
185
- const history = cwd !== undefined && (0, agent_history_js_1.hasAgentHistory)(cwd, a.transcriptPath);
186
- return !!live || history;
187
- })
198
+ .filter(record => canOpenActiveAgent(record))
188
199
  .sort((a, b) => a.startedAt - b.startedAt);
189
200
  }
190
201
  roster() {
@@ -205,7 +216,7 @@ class FleetList {
205
216
  // Input listeners receive BOTH key-press and key-release (the kitty protocol
206
217
  // emits both, and matchesKey matches either) — act on press only, or every
207
218
  // tap would move/fire twice. Repeats still pass through for held-key nav.
208
- if ((0, pi_tui_1.isKeyRelease)(data))
219
+ if (isKeyRelease(data))
209
220
  return undefined;
210
221
  // While an overlay is open, let it own all input.
211
222
  if (this.viewerClose)
@@ -221,7 +232,7 @@ class FleetList {
221
232
  }
222
233
  if (!this.active) {
223
234
  // Activate: ↓ or ← at an empty prompt moves focus into the list.
224
- const isActivator = (0, pi_tui_1.matchesKey)(data, "down") || (0, pi_tui_1.matchesKey)(data, "left");
235
+ const isActivator = matchesKey(data, "down") || matchesKey(data, "left");
225
236
  if (isActivator && this.agentRecords().length > 0 && this.ui.getEditorText() === "") {
226
237
  this.active = true;
227
238
  this.selectedIndex = 0;
@@ -231,13 +242,13 @@ class FleetList {
231
242
  return undefined;
232
243
  }
233
244
  // Active — arrows navigate, Enter opens, Esc / Up-past-top exits.
234
- if ((0, pi_tui_1.matchesKey)(data, "down")) {
245
+ if (matchesKey(data, "down")) {
235
246
  const max = this.roster().length - 1;
236
247
  this.selectedIndex = Math.min(max, this.selectedIndex + 1);
237
248
  this.update();
238
249
  return { consume: true };
239
250
  }
240
- if ((0, pi_tui_1.matchesKey)(data, "up")) {
251
+ if (matchesKey(data, "up")) {
241
252
  if (this.selectedIndex === 0) {
242
253
  this.deactivate();
243
254
  return { consume: true };
@@ -246,11 +257,11 @@ class FleetList {
246
257
  this.update();
247
258
  return { consume: true };
248
259
  }
249
- if ((0, pi_tui_1.matchesKey)(data, "escape")) {
260
+ if (matchesKey(data, "escape")) {
250
261
  this.deactivate();
251
262
  return { consume: true };
252
263
  }
253
- if ((0, pi_tui_1.matchesKey)(data, pi_tui_1.Key.enter)) {
264
+ if (matchesKey(data, Key.enter)) {
254
265
  this.openSelected();
255
266
  return { consume: true };
256
267
  }
@@ -268,7 +279,7 @@ class FleetList {
268
279
  */
269
280
  editorHasFocus() {
270
281
  const focused = this.tui?.focusedComponent;
271
- return focused == null || focused instanceof pi_tui_1.Editor;
282
+ return focused == null || focused instanceof Editor;
272
283
  }
273
284
  deactivate() {
274
285
  this.active = false;
@@ -285,28 +296,28 @@ class FleetList {
285
296
  const record = entry.record;
286
297
  if (!this.ui)
287
298
  return;
288
- const session = record.session ?? (record.transcriptPath && this.getCwd()
289
- ? (() => {
290
- const messages = (0, agent_history_js_1.readAgentHistory)(this.getCwd(), record.transcriptPath);
291
- return messages ? (0, conversation_viewer_js_1.createStaticConversationSource)(messages) : undefined;
292
- })()
293
- : undefined);
294
- if (!session) {
295
- this.ui.notify(`Agent is ${record.status} — no history available.`, "info");
299
+ if (!canOpenActiveAgent(record) || !record.session) {
300
+ this.ui.notify(`Agent is ${record.status} — no live session available.`, "info");
301
+ return;
302
+ }
303
+ const session = record.session;
304
+ const ctx = this.getCtx();
305
+ const pi = this.pi;
306
+ if (!ctx || !pi) {
307
+ this.ui.notify("Agent conversation is unavailable — no active session.", "info");
296
308
  return;
297
309
  }
298
310
  const activity = this.agentActivity.get(record.id);
299
- const isLive = record.session !== undefined;
300
311
  this.viewingAgentId = record.id;
301
312
  void this.ui.custom((tui, theme, keybindings, done) => {
302
313
  this.viewerClose = () => done(undefined);
303
- return new conversation_viewer_js_1.ConversationViewer(tui, session, record, activity, theme, done, isLive ? () => {
314
+ return new ConversationViewer(tui, session, record, activity, theme, done, () => {
304
315
  if (this.manager.abort(record.id))
305
316
  this.ui?.notify(`Stopped "${record.description}".`, "info");
306
- } : undefined, keybindings, isLive ? (message) => this.manager.steer(record.id, message) : undefined);
317
+ }, keybindings, (message) => this.manager.steer(record.id, message), { pi, ctx, readOnly: false });
307
318
  }, {
308
319
  overlay: true,
309
- overlayOptions: { anchor: "center", width: "90%", maxHeight: `${conversation_viewer_js_1.VIEWPORT_HEIGHT_PCT}%` },
320
+ overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
310
321
  }).then(() => this.clearViewer(), () => this.clearViewer());
311
322
  }
312
323
  /** Reset overlay state and return to the list (on close, auto-close, or error). */
@@ -336,32 +347,28 @@ class FleetList {
336
347
  ? "↑↓ select · enter view · esc back"
337
348
  : "esc to interrupt · ← for agents · ↓ to manage";
338
349
  const lines = [];
339
- lines.push((0, pi_tui_1.truncateToWidth)(" " + theme.fg("dim", hint), width));
340
- lines.push("");
341
- lines.push((0, pi_tui_1.truncateToWidth)(` ${this.bullet(0, sel, theme)} main`, width));
342
- // Window the agent rows so the selected one stays visible.
343
- const visible = Math.min(MAX_AGENT_ROWS, agents.length);
344
- const selAgent = Math.max(0, sel - 1);
345
- const start = selAgent < visible ? 0 : selAgent - visible + 1;
346
- const hiddenBelow = agents.length - (start + visible);
347
- if (start > 0)
348
- lines.push(rightAlign("", theme.fg("dim", `↑ ${start} more`), width));
349
- for (let a = start; a < start + visible; a++) {
350
+ lines.push(truncateToWidth(" " + theme.fg("dim", hint), width));
351
+ lines.push(truncateToWidth(` ${this.bullet(0, sel, theme)} main`, width));
352
+ const window = calculateFleetAgentWindow(agents.length, sel);
353
+ if (window.hiddenAbove > 0) {
354
+ lines.push(rightAlign("", theme.fg("dim", `↑ ${window.hiddenAbove} more`), width));
355
+ }
356
+ for (let a = window.start; a < window.end; a++) {
350
357
  lines.push(this.renderAgentRow(a + 1, sel, agents[a].record, width, theme));
351
358
  }
352
- if (hiddenBelow > 0)
353
- lines.push(rightAlign("", theme.fg("dim", `↓ ${hiddenBelow} more`), width));
359
+ if (window.hiddenBelow > 0) {
360
+ lines.push(rightAlign("", theme.fg("dim", `↓ ${window.hiddenBelow} more`), width));
361
+ }
354
362
  return lines;
355
363
  }
356
364
  bullet(rosterIndex, sel, theme) {
357
365
  return rosterIndex === sel ? theme.fg("accent", "●") : theme.fg("dim", "○");
358
366
  }
359
367
  renderAgentRow(rosterIndex, sel, record, width, theme) {
360
- const left = ` ${this.bullet(rosterIndex, sel, theme)} ${theme.fg("muted", (0, agent_widget_js_1.getDisplayName)(record.type))} ${record.description}`;
361
- const tokens = (0, usage_js_1.getLifetimeTotal)(this.agentActivity.get(record.id)?.lifetimeUsage ?? record.lifetimeUsage);
368
+ const left = ` ${this.bullet(rosterIndex, sel, theme)} ${theme.fg("muted", getDisplayName(record.type))} ${record.description}`;
369
+ const tokens = getLifetimeTotal(this.agentActivity.get(record.id)?.lifetimeUsage ?? record.lifetimeUsage);
362
370
  const elapsedMs = (record.completedAt ?? Date.now()) - record.startedAt; // freezes once finished
363
371
  const right = theme.fg("dim", `${formatFleetElapsed(elapsedMs)} · ${formatFleetTokens(tokens)}`);
364
372
  return rightAlign(left, right, width);
365
373
  }
366
374
  }
367
- exports.FleetList = FleetList;
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * schedule-menu.ts — `/agents → Scheduled jobs` submenu.
4
3
  *
@@ -8,8 +7,6 @@
8
7
  * "I scheduled something dumb, get rid of it"). Add management surfaces here
9
8
  * if real demand emerges.
10
9
  */
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.showSchedulesMenu = showSchedulesMenu;
13
10
  /** Format an ISO timestamp as relative time ("in 4h", "2d ago", "—"). */
14
11
  function relTime(iso, now = Date.now()) {
15
12
  if (!iso)
@@ -72,7 +69,7 @@ function formatDetails(j, scheduler) {
72
69
  * List scheduled jobs; selecting one opens a cancel-confirm with details.
73
70
  * Returns when the user backs out or after a cancellation.
74
71
  */
75
- async function showSchedulesMenu(ctx, scheduler) {
72
+ export async function showSchedulesMenu(ctx, scheduler) {
76
73
  if (!scheduler.isActive()) {
77
74
  ctx.ui.notify("Scheduler is not active in this session.", "warning");
78
75
  return;
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * viewer-keys.ts — Scroll key matchers for the conversation viewer.
4
3
  *
@@ -6,15 +5,13 @@
6
5
  * manager, falling back to the previous hardcoded keys otherwise. The viewer's
7
6
  * k/j and shift+arrow aliases always work alongside whatever is bound.
8
7
  */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.createViewerKeys = createViewerKeys;
11
- const pi_tui_1 = require("@earendil-works/pi-tui");
12
- function createViewerKeys(keybindings) {
13
- const matches = (data, id, fallback) => keybindings ? keybindings.matches(data, id) : (0, pi_tui_1.matchesKey)(data, fallback);
8
+ import { matchesKey } from "@earendil-works/pi-tui";
9
+ export function createViewerKeys(keybindings) {
10
+ const matches = (data, id, fallback) => keybindings ? keybindings.matches(data, id) : matchesKey(data, fallback);
14
11
  return {
15
- scrollUp: (data) => matches(data, "tui.select.up", "up") || (0, pi_tui_1.matchesKey)(data, "k"),
16
- scrollDown: (data) => matches(data, "tui.select.down", "down") || (0, pi_tui_1.matchesKey)(data, "j"),
17
- pageUp: (data) => matches(data, "tui.select.pageUp", "pageUp") || (0, pi_tui_1.matchesKey)(data, "shift+up"),
18
- pageDown: (data) => matches(data, "tui.select.pageDown", "pageDown") || (0, pi_tui_1.matchesKey)(data, "shift+down"),
12
+ scrollUp: (data) => matches(data, "tui.select.up", "up") || matchesKey(data, "k"),
13
+ scrollDown: (data) => matches(data, "tui.select.down", "down") || matchesKey(data, "j"),
14
+ pageUp: (data) => matches(data, "tui.select.pageUp", "pageUp") || matchesKey(data, "shift+up"),
15
+ pageDown: (data) => matches(data, "tui.select.pageDown", "pageDown") || matchesKey(data, "shift+down"),
19
16
  };
20
17
  }
package/dist/usage.js CHANGED
@@ -1,16 +1,10 @@
1
- "use strict";
2
1
  /** usage.ts — Token usage: shapes, accumulator operators, session-stats readers. */
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.getLifetimeTotal = getLifetimeTotal;
5
- exports.addUsage = addUsage;
6
- exports.getSessionTokens = getSessionTokens;
7
- exports.getSessionContextPercent = getSessionContextPercent;
8
2
  /** Sum of lifetime usage components, or 0 if undefined. */
9
- function getLifetimeTotal(u) {
3
+ export function getLifetimeTotal(u) {
10
4
  return u ? u.input + u.output + u.cacheWrite : 0;
11
5
  }
12
6
  /** Add a usage delta into a target accumulator (mutates target). */
13
- function addUsage(into, delta) {
7
+ export function addUsage(into, delta) {
14
8
  into.input += delta.input;
15
9
  into.output += delta.output;
16
10
  into.cacheWrite += delta.cacheWrite;
@@ -28,7 +22,7 @@ function addUsage(into, delta) {
28
22
  * and so counts the cumulative cached prefix N times across N turns
29
23
  * (issue #38).
30
24
  */
31
- function getSessionTokens(session) {
25
+ export function getSessionTokens(session) {
32
26
  if (!session)
33
27
  return 0;
34
28
  try {
@@ -43,7 +37,7 @@ function getSessionTokens(session) {
43
37
  * Context-window utilization (0–100), or null when unavailable
44
38
  * (no model contextWindow, or post-compaction before the next response).
45
39
  */
46
- function getSessionContextPercent(session) {
40
+ export function getSessionContextPercent(session) {
47
41
  if (!session)
48
42
  return null;
49
43
  try {