@gjczone/pi-swarm 0.3.5 → 0.5.0

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 (53) hide show
  1. package/README.md +33 -71
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +8 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/shared/controller.d.ts +10 -4
  6. package/dist/shared/controller.d.ts.map +1 -1
  7. package/dist/shared/controller.js +139 -6
  8. package/dist/shared/controller.js.map +1 -1
  9. package/dist/shared/render.d.ts +0 -11
  10. package/dist/shared/render.d.ts.map +1 -1
  11. package/dist/shared/render.js +3 -36
  12. package/dist/shared/render.js.map +1 -1
  13. package/dist/shared/spawner.d.ts.map +1 -1
  14. package/dist/shared/spawner.js +212 -17
  15. package/dist/shared/spawner.js.map +1 -1
  16. package/dist/shared/types.d.ts +58 -0
  17. package/dist/shared/types.d.ts.map +1 -1
  18. package/dist/shared/types.js.map +1 -1
  19. package/dist/shared/worktree.d.ts +81 -0
  20. package/dist/shared/worktree.d.ts.map +1 -0
  21. package/dist/shared/worktree.js +417 -0
  22. package/dist/shared/worktree.js.map +1 -0
  23. package/dist/shared/xml.d.ts +18 -0
  24. package/dist/shared/xml.d.ts.map +1 -0
  25. package/dist/shared/xml.js +31 -0
  26. package/dist/shared/xml.js.map +1 -0
  27. package/dist/swarm/tool.d.ts.map +1 -1
  28. package/dist/swarm/tool.js +69 -15
  29. package/dist/swarm/tool.js.map +1 -1
  30. package/dist/team/mailbox.d.ts +5 -0
  31. package/dist/team/mailbox.d.ts.map +1 -1
  32. package/dist/team/mailbox.js +43 -2
  33. package/dist/team/mailbox.js.map +1 -1
  34. package/dist/team/supervisor.d.ts +27 -2
  35. package/dist/team/supervisor.d.ts.map +1 -1
  36. package/dist/team/supervisor.js +93 -50
  37. package/dist/team/supervisor.js.map +1 -1
  38. package/dist/team/task-graph.d.ts +5 -2
  39. package/dist/team/task-graph.d.ts.map +1 -1
  40. package/dist/team/task-graph.js +27 -1
  41. package/dist/team/task-graph.js.map +1 -1
  42. package/dist/team/tool.d.ts.map +1 -1
  43. package/dist/team/tool.js +102 -18
  44. package/dist/team/tool.js.map +1 -1
  45. package/dist/tui/progress.d.ts +56 -44
  46. package/dist/tui/progress.d.ts.map +1 -1
  47. package/dist/tui/progress.js +497 -179
  48. package/dist/tui/progress.js.map +1 -1
  49. package/dist/tui/team-dashboard.d.ts +39 -23
  50. package/dist/tui/team-dashboard.d.ts.map +1 -1
  51. package/dist/tui/team-dashboard.js +506 -143
  52. package/dist/tui/team-dashboard.js.map +1 -1
  53. package/package.json +1 -1
@@ -2,56 +2,64 @@
2
2
  * tui/progress — AgentSwarm live progress panel.
3
3
  *
4
4
  * Renders a real-time progress display above the input area when
5
- * an AgentSwarm batch is running. Each subagent gets a braille
6
- * progress bar with status labels.
5
+ * an AgentSwarm batch is running. Each subagent gets a compact braille
6
+ * spinner with status and item description.
7
+ *
8
+ * Features:
9
+ * - Debounced, event-driven rendering (replaces pure setInterval polling)
10
+ * - Keyboard interaction: j/k scroll, Enter detail, ? help, tab panel switch
11
+ * - Panel switching: members list / event log
12
+ * - Detail overlay for individual members
13
+ * - Activity/tool tracking per member
14
+ * - ETA estimation
7
15
  *
8
16
  * Ported from MoonshotAI/kimi-code's AgentSwarmProgressComponent.
9
17
  */
18
+ import { matchesKey, Key, isKeyRepeat } from "@earendil-works/pi-tui";
10
19
  // ---------------------------------------------------------------------------
11
- // Constants (from kimi-code)
20
+ // Constants
12
21
  // ---------------------------------------------------------------------------
13
- /** Preferred width for the item text column. */
14
- const TEXT_CELL_PREFERRED_WIDTH = 30;
15
- /** Gap between columns. */
16
- const CELL_GAP = " ";
17
- /** Braille bar max width in characters. */
18
- const BRAILLE_BAR_MAX_WIDTH = 8;
19
- /** Minimum braille bar width. */
20
- const TEXT_BRAILLE_BAR_MIN_WIDTH = 6;
21
- /** Animation frame interval in ms. */
22
- const FRAME_INTERVAL_MS = 80;
23
- /** How long the completion-fill animation lasts in ms. */
24
- const COMPLETE_FILL_MS = 360;
25
- /** Braille characters representing fill levels 0-6 dots. */
22
+ /** Max item description length to display. */
23
+ const MAX_ITEM_LABEL_LEN = 40;
24
+ /** Compact braille spinner frames for running agents. */
25
+ const BRAILLE_SPINNER = [
26
+ "\u28BF",
27
+ "\u28FB",
28
+ "\u28FD",
29
+ "\u28FE",
30
+ "\u28F7",
31
+ "\u28EF",
32
+ "\u28DF",
33
+ "\u287F",
34
+ ];
35
+ /** Braille characters representing fill levels 0-6 dots for completed bar. */
26
36
  const BRAILLE_LEVELS = [
27
- "\u28C0", // 0 dots (empty)
28
- "\u28C4", // 1 dot
29
- "\u28E4", // 2 dots
30
- "\u28E6", // 3 dots
31
- "\u28F6", // 4 dots
32
- "\u28F7", // 5 dots
33
- "\u28FF", // 6 dots (full)
37
+ "\u28C0",
38
+ "\u28C4",
39
+ "\u28E4",
40
+ "\u28E6",
41
+ "\u28F6",
42
+ "\u28F7",
43
+ "\u28FF",
34
44
  ];
35
45
  const BRAILLE_EMPTY = BRAILLE_LEVELS[0];
36
46
  const BRAILLE_FULL = BRAILLE_LEVELS[6];
37
- /** Status labels for each phase. */
38
- const ORCHESTRATING_LABEL = "Orchestrating...";
39
- const WORKING_LABEL = "Working...";
40
- const COMPLETED_LABEL = "Completed.";
41
- const FAILED_LABEL = "Failed.";
42
- const ABORTED_LABEL = "Aborted.";
43
- const QUEUED_LABEL = "Queued...";
47
+ const COMPLETED_BAR_WIDTH = 4;
48
+ /** Maximum members shown at once in the list view. */
49
+ const VISIBLE_MEMBERS = 8;
50
+ /** Maximum events shown in the event log panel. */
51
+ const VISIBLE_EVENTS = 10;
52
+ /** Debounce window for coalescing render requests. */
53
+ const DEBOUNCE_MS = 75;
54
+ /** Fallback polling interval when state is active (has running members). */
55
+ const ACTIVE_POLL_MS = 800;
56
+ /** Fallback polling interval when idle (no running members). */
57
+ const IDLE_POLL_MS = 2000;
58
+ /** Animation interval for the braille spinner. */
59
+ const FRAME_INTERVAL_MS = 80;
44
60
  // ---------------------------------------------------------------------------
45
- // Snapshot conversion (controller snapshot -> component state)
61
+ // Snapshot conversion
46
62
  // ---------------------------------------------------------------------------
47
- /**
48
- * Convert a BatchProgressSnapshot from the concurrency controller into
49
- * the SwarmProgressState expected by AgentSwarmProgressComponent.
50
- *
51
- * Maps the controller's coarse member phases onto the component's
52
- * richer MemberPhase set, attaching a phaseStartedAt timestamp so the
53
- * braille animation can run for working/suspended members.
54
- */
55
63
  export function snapshotToProgressState(snapshot, title) {
56
64
  const now = Date.now();
57
65
  const members = snapshot.members.map((m) => ({
@@ -60,6 +68,9 @@ export function snapshotToProgressState(snapshot, title) {
60
68
  item: m.item,
61
69
  error: m.error,
62
70
  phaseStartedAt: isAnimatedPhase(m.phase) ? now : undefined,
71
+ currentTool: m.currentTool,
72
+ activity: m.activity,
73
+ usage: m.usage,
63
74
  }));
64
75
  return {
65
76
  title,
@@ -69,6 +80,10 @@ export function snapshotToProgressState(snapshot, title) {
69
80
  active: snapshot.active,
70
81
  queued: snapshot.queued,
71
82
  members,
83
+ totalUsage: snapshot.totalUsage,
84
+ startedAt: snapshot.startedAt ?? now,
85
+ estimatedRemainingMs: snapshot.estimatedRemainingMs,
86
+ eventLog: snapshot.eventLog ? [...snapshot.eventLog] : undefined,
72
87
  };
73
88
  }
74
89
  function mapMemberPhase(phase) {
@@ -93,37 +108,40 @@ function isAnimatedPhase(phase) {
93
108
  // ---------------------------------------------------------------------------
94
109
  export class AgentSwarmProgressComponent {
95
110
  state_ = null;
96
- animationFrame;
97
- startTime = Date.now();
98
111
  renderedWidth;
99
112
  cachedLines;
100
113
  onRequestRender;
101
- /**
102
- * @param onRequestRender Optional callback to request a TUI re-render.
103
- * When provided, called on every animation tick so the braille bars
104
- * animate. Without it the component still renders correctly but the
105
- * animation won't be visible to the user.
106
- *
107
- * 业务说明:TUI 框架不会自动轮询组件;需要通过 requestRender() 主动
108
- * 触发重绘才能使 braille 进度条动起来。此回调由 setWidget 工厂函数
109
- * 在捕获 tui 引用后传入。
110
- */
114
+ // Render scheduler
115
+ animationFrame;
116
+ frameIndex = 0;
117
+ pollTimer;
118
+ debounceTimer;
119
+ pendingInvalidate = false;
120
+ hasActiveMembers = false;
121
+ // Input / UI state
122
+ scrollOffset = 0;
123
+ selectedIndex = -1;
124
+ activePanel = "members";
125
+ overlay = null;
111
126
  constructor(onRequestRender) {
112
127
  this.onRequestRender = onRequestRender;
128
+ this.startPolling();
113
129
  this.startAnimation();
114
130
  }
115
131
  // -------------------------------------------------------------------
116
132
  // Public API
117
133
  // -------------------------------------------------------------------
118
- /** Update the progress state from the tool execution. */
119
134
  update(state) {
120
135
  this.state_ = state;
121
- this.invalidate();
136
+ this.hasActiveMembers = state.active > 0;
137
+ // Reset scroll offset if it exceeds member count
138
+ if (this.scrollOffset > Math.max(0, state.members.length - VISIBLE_MEMBERS)) {
139
+ this.scrollOffset = Math.max(0, state.members.length - VISIBLE_MEMBERS);
140
+ }
141
+ this.requestRender();
122
142
  }
123
- /** Mark the swarm as completed (triggers completion animation). */
124
143
  complete() {
125
144
  if (this.state_) {
126
- // Mark all non-terminal members as completed
127
145
  for (const m of this.state_.members) {
128
146
  if (m.phase !== "completed" &&
129
147
  m.phase !== "failed" &&
@@ -135,28 +153,106 @@ export class AgentSwarmProgressComponent {
135
153
  this.state_.active = 0;
136
154
  this.state_.queued = 0;
137
155
  this.state_.completed = this.state_.total - this.state_.failed;
138
- this.state_.status = COMPLETED_LABEL;
139
156
  }
140
- this.invalidate();
157
+ this.hasActiveMembers = false;
158
+ this.requestRender();
141
159
  }
142
- /** Stop the animation loop. */
143
160
  dispose() {
144
- if (this.animationFrame !== undefined) {
145
- clearInterval(this.animationFrame);
146
- this.animationFrame = undefined;
147
- }
148
- // 断开与 TUI 框架的连接,防止内存泄漏
161
+ this.stopTimers();
149
162
  this.onRequestRender = undefined;
150
163
  }
151
- // -------------------------------------------------------------------
152
- // Component interface
153
- // -------------------------------------------------------------------
154
164
  invalidate() {
155
165
  this.renderedWidth = undefined;
156
166
  this.cachedLines = undefined;
157
167
  }
168
+ // -------------------------------------------------------------------
169
+ // Keyboard input
170
+ // -------------------------------------------------------------------
171
+ handleInput(data) {
172
+ // Ignore key repeats for navigation keys to avoid scroll jank
173
+ const isRepeat = isKeyRepeat(data);
174
+ // Close overlay on Escape / q
175
+ if (this.overlay) {
176
+ if (matchesKey(data, Key.escape) || matchesKey(data, "q")) {
177
+ this.overlay = null;
178
+ this.requestRender();
179
+ return;
180
+ }
181
+ return;
182
+ }
183
+ // Navigation
184
+ if (matchesKey(data, "j") || matchesKey(data, Key.down)) {
185
+ if (!isRepeat) {
186
+ this.scrollDown(1);
187
+ }
188
+ return;
189
+ }
190
+ if (matchesKey(data, "k") || matchesKey(data, Key.up)) {
191
+ if (!isRepeat) {
192
+ this.scrollUp(1);
193
+ }
194
+ return;
195
+ }
196
+ if (matchesKey(data, Key.pageDown)) {
197
+ this.scrollDown(VISIBLE_MEMBERS);
198
+ return;
199
+ }
200
+ if (matchesKey(data, Key.pageUp)) {
201
+ this.scrollUp(VISIBLE_MEMBERS);
202
+ return;
203
+ }
204
+ if (matchesKey(data, "g") || matchesKey(data, Key.home)) {
205
+ this.scrollOffset = 0;
206
+ this.selectedIndex = -1;
207
+ this.requestRender();
208
+ return;
209
+ }
210
+ if (matchesKey(data, Key.shift("g")) || matchesKey(data, Key.end)) {
211
+ if (this.state_) {
212
+ const maxScroll = Math.max(0, this.state_.members.length - VISIBLE_MEMBERS);
213
+ this.scrollOffset = maxScroll;
214
+ this.selectedIndex = Math.max(0, this.state_.members.length - 1);
215
+ }
216
+ this.requestRender();
217
+ return;
218
+ }
219
+ // Panel switching
220
+ if (matchesKey(data, Key.tab) || matchesKey(data, "1")) {
221
+ this.activePanel = "members";
222
+ this.scrollOffset = 0;
223
+ this.selectedIndex = -1;
224
+ this.requestRender();
225
+ return;
226
+ }
227
+ if (matchesKey(data, "2")) {
228
+ this.activePanel = "events";
229
+ this.scrollOffset = 0;
230
+ this.requestRender();
231
+ return;
232
+ }
233
+ // Detail overlay
234
+ if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
235
+ if (this.activePanel === "members" && this.state_) {
236
+ const idx = this.selectedIndex >= 0 ? this.selectedIndex : this.scrollOffset;
237
+ if (idx >= 0 && idx < this.state_.members.length) {
238
+ this.overlay = { kind: "detail" };
239
+ this.requestRender();
240
+ }
241
+ }
242
+ return;
243
+ }
244
+ // Help overlay
245
+ if (matchesKey(data, "?")) {
246
+ this.overlay = { kind: "help" };
247
+ this.requestRender();
248
+ return;
249
+ }
250
+ }
251
+ // -------------------------------------------------------------------
252
+ // Rendering
253
+ // -------------------------------------------------------------------
158
254
  render(width) {
159
- const safeWidth = Math.max(10, width);
255
+ const safeWidth = Math.max(20, width);
160
256
  if (this.cachedLines && this.renderedWidth === safeWidth) {
161
257
  return this.cachedLines;
162
258
  }
@@ -164,176 +260,398 @@ export class AgentSwarmProgressComponent {
164
260
  this.cachedLines = [];
165
261
  return this.cachedLines;
166
262
  }
263
+ // If overlay is active, render overlay content
264
+ if (this.overlay) {
265
+ const lines = this.renderOverlay(safeWidth);
266
+ this.cachedLines = lines;
267
+ return this.cachedLines;
268
+ }
167
269
  const state = this.state_;
270
+ const contentWidth = safeWidth - 4;
168
271
  const lines = [];
169
- // Title bar
272
+ // Header: title + panel indicator
170
273
  const title = state.title ?? "Agent Swarm";
171
- lines.push(borderTop(title, safeWidth));
172
- // Overall status
173
- const statusLabel = state.status ?? resolveOverallStatus(state);
174
- const statusBar = buildStatusBar(state, safeWidth);
175
- lines.push(` ${statusLabel}`);
176
- lines.push(` ${statusBar}`);
177
- // Member rows
178
- const maxMembers = Math.min(state.members.length, 20); // Cap for performance
179
- for (let i = 0; i < maxMembers; i += 1) {
180
- const member = state.members[i];
181
- if (!member)
182
- continue;
183
- const row = renderMemberRow(member, safeWidth - 4);
184
- lines.push(` ${row}`);
274
+ const panelLabel = this.activePanel === "members" ? "[1]members" : "[2]events";
275
+ const headerText = `${title} ${panelLabel}`;
276
+ lines.push(` ${truncateText(headerText, contentWidth)}`);
277
+ // Overall progress bar
278
+ const barWidth = Math.max(1, contentWidth);
279
+ const total = state.total || 1;
280
+ const doneRatio = (state.completed + state.failed) / total;
281
+ const doneChars = Math.round(doneRatio * barWidth);
282
+ const done = "\u2501".repeat(doneChars);
283
+ const remaining = "\u2501".repeat(barWidth - doneChars);
284
+ lines.push(` ${done}${remaining}`);
285
+ if (this.activePanel === "members") {
286
+ this.renderMembersPanel(lines, state, contentWidth);
287
+ }
288
+ else {
289
+ this.renderEventsPanel(lines, state, contentWidth);
185
290
  }
186
- // Summary
187
- const summary = buildSummary(state, safeWidth - 4);
188
- lines.push(` ${summary}`);
291
+ // Separator
292
+ const sep = "\u2500".repeat(contentWidth);
293
+ lines.push(` ${sep}`);
294
+ // Footer: counts + tokens + elapsed + ETA
295
+ const footer = buildFooter(state, contentWidth);
296
+ lines.push(` ${footer}`);
189
297
  // Bottom border
190
- lines.push(borderBottom(safeWidth));
298
+ const bottom = `\u2514${"\u2500".repeat(contentWidth)}\u2518`;
299
+ lines.push(bottom);
191
300
  this.cachedLines = lines;
192
301
  this.renderedWidth = safeWidth;
193
302
  return this.cachedLines;
194
303
  }
195
304
  // -------------------------------------------------------------------
196
- // Animation
305
+ // Panel rendering
306
+ // -------------------------------------------------------------------
307
+ renderMembersPanel(lines, state, contentWidth) {
308
+ const maxMembers = Math.min(state.members.length, this.scrollOffset + VISIBLE_MEMBERS);
309
+ const hasMore = state.members.length > VISIBLE_MEMBERS;
310
+ // Scroll indicator (top)
311
+ if (hasMore && this.scrollOffset > 0) {
312
+ lines.push(` \u2191 ${this.scrollOffset} more above`);
313
+ }
314
+ for (let i = this.scrollOffset; i < maxMembers; i += 1) {
315
+ const member = state.members[i];
316
+ if (!member)
317
+ continue;
318
+ const isSelected = i === this.selectedIndex ||
319
+ (this.selectedIndex < 0 && i === this.scrollOffset);
320
+ const row = renderMemberRow(member, contentWidth, this.frameIndex, isSelected);
321
+ lines.push(` ${row}`);
322
+ }
323
+ // Scroll indicator (bottom)
324
+ if (hasMore && maxMembers < state.members.length) {
325
+ const remaining = state.members.length - maxMembers;
326
+ lines.push(` \u2193 ${remaining} more below`);
327
+ }
328
+ }
329
+ renderEventsPanel(lines, state, contentWidth) {
330
+ const events = state.eventLog ?? [];
331
+ const displayEvents = events.slice(-VISIBLE_EVENTS);
332
+ if (displayEvents.length === 0) {
333
+ lines.push(` ${padRight("(no events yet)", contentWidth)}`);
334
+ return;
335
+ }
336
+ for (const event of displayEvents) {
337
+ const time = new Date(event.timestamp).toLocaleTimeString();
338
+ const icon = eventIcon(event.type);
339
+ const detail = truncateText(event.detail, contentWidth - 12);
340
+ lines.push(` ${icon} ${time} ${detail}`);
341
+ }
342
+ }
343
+ // -------------------------------------------------------------------
344
+ // Overlays
197
345
  // -------------------------------------------------------------------
346
+ renderOverlay(width) {
347
+ if (!this.overlay)
348
+ return [];
349
+ if (this.overlay.kind === "help") {
350
+ return this.renderHelpOverlay(width);
351
+ }
352
+ return this.renderDetailOverlay(width);
353
+ }
354
+ renderHelpOverlay(width) {
355
+ const safeWidth = Math.max(30, width);
356
+ const contentWidth = safeWidth - 6;
357
+ const lines = [];
358
+ const bottom = `\u2514${"\u2500".repeat(safeWidth - 2)}\u2518`;
359
+ lines.push(` \u250C${"\u2500".repeat(safeWidth - 2)}\u2510`);
360
+ lines.push(` \u2502 ${padRight("Help" + " ".repeat(contentWidth - 4), safeWidth - 4)}\u2502`);
361
+ lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
362
+ const helpItems = [
363
+ ["j/k, up/down", "Scroll member list"],
364
+ ["g / Shift+G", "Go to top / bottom"],
365
+ ["Enter", "View member detail"],
366
+ ["1 or Tab", "Members panel"],
367
+ ["2", "Events panel"],
368
+ ["?", "Toggle this help"],
369
+ ["q / Esc", "Close overlay"],
370
+ ];
371
+ for (const [key, desc] of helpItems) {
372
+ const line = `${padRight(key, 18)} ${desc}`;
373
+ lines.push(` \u2502 ${padRight(line, safeWidth - 4)}\u2502`);
374
+ }
375
+ lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
376
+ lines.push(` \u2502 ${padRight("Press q or Esc to close", safeWidth - 4)}\u2502`);
377
+ lines.push(bottom);
378
+ return lines;
379
+ }
380
+ renderDetailOverlay(width) {
381
+ const safeWidth = Math.max(30, width);
382
+ const contentWidth = safeWidth - 6;
383
+ const lines = [];
384
+ if (!this.state_)
385
+ return [];
386
+ const idx = this.selectedIndex >= 0 ? this.selectedIndex : this.scrollOffset;
387
+ const member = this.state_.members[idx];
388
+ if (!member)
389
+ return [];
390
+ const bottom = `\u2514${"\u2500".repeat(safeWidth - 2)}\u2518`;
391
+ lines.push(` \u250C${"\u2500".repeat(safeWidth - 2)}\u2510`);
392
+ lines.push(` \u2502 ${padRight(`Agent #${String(member.index).padStart(2, "0")}`, safeWidth - 4)}\u2502`);
393
+ lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
394
+ const fields = [
395
+ { label: "Status", value: shortPhaseLabel(member.phase) },
396
+ ];
397
+ if (member.item) {
398
+ fields.push({ label: "Item", value: member.item });
399
+ }
400
+ if (member.currentTool) {
401
+ fields.push({ label: "Tool", value: member.currentTool });
402
+ }
403
+ if (member.activity) {
404
+ fields.push({ label: "Activity", value: member.activity });
405
+ }
406
+ if (member.usage) {
407
+ fields.push({
408
+ label: "Tokens",
409
+ value: `${Math.round(member.usage.input)}in / ${Math.round(member.usage.output)}out`,
410
+ });
411
+ }
412
+ if (member.error) {
413
+ fields.push({ label: "Error", value: member.error });
414
+ }
415
+ for (const field of fields) {
416
+ const line = `${padRight(field.label + ":", 10)} ${truncateText(field.value, contentWidth - 12)}`;
417
+ lines.push(` \u2502 ${padRight(line, safeWidth - 4)}\u2502`);
418
+ }
419
+ lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
420
+ lines.push(` \u2502 ${padRight("Press q or Esc to close", safeWidth - 4)}\u2502`);
421
+ lines.push(bottom);
422
+ return lines;
423
+ }
424
+ // -------------------------------------------------------------------
425
+ // Scroll helpers
426
+ // -------------------------------------------------------------------
427
+ scrollDown(n) {
428
+ if (!this.state_)
429
+ return;
430
+ const memberCount = this.state_.members.length;
431
+ const maxOffset = Math.max(0, memberCount - 1);
432
+ this.selectedIndex = Math.min(maxOffset, Math.max(0, this.selectedIndex < 0 ? this.scrollOffset : this.selectedIndex) + n);
433
+ if (this.selectedIndex - this.scrollOffset >= VISIBLE_MEMBERS ||
434
+ this.selectedIndex < this.scrollOffset) {
435
+ this.scrollOffset = Math.max(0, this.selectedIndex - VISIBLE_MEMBERS + 1);
436
+ const maxScroll = Math.max(0, memberCount - VISIBLE_MEMBERS);
437
+ this.scrollOffset = Math.min(this.scrollOffset, maxScroll);
438
+ }
439
+ this.requestRender();
440
+ }
441
+ scrollUp(n) {
442
+ if (!this.state_)
443
+ return;
444
+ this.selectedIndex = Math.max(0, (this.selectedIndex < 0 ? this.scrollOffset : this.selectedIndex) - n);
445
+ if (this.selectedIndex < this.scrollOffset) {
446
+ this.scrollOffset = Math.max(0, this.selectedIndex);
447
+ }
448
+ this.requestRender();
449
+ }
450
+ // -------------------------------------------------------------------
451
+ // Render scheduling
452
+ // -------------------------------------------------------------------
453
+ /**
454
+ * Request a debounced render. Multiple calls within the debounce window
455
+ * coalesce into a single render pass. State changes also trigger immediate
456
+ * invalidation.
457
+ */
458
+ requestRender() {
459
+ this.invalidate();
460
+ if (this.debounceTimer !== undefined) {
461
+ this.pendingInvalidate = true;
462
+ return;
463
+ }
464
+ this.debounceTimer = setTimeout(() => {
465
+ this.debounceTimer = undefined;
466
+ this.pendingInvalidate = false;
467
+ this.onRequestRender?.();
468
+ this.updatePollingInterval();
469
+ }, DEBOUNCE_MS);
470
+ // Also trigger an immediate render for faster response to inputs
471
+ this.onRequestRender?.();
472
+ }
473
+ /**
474
+ * Start the fallback polling timer. Falls back to polling when no
475
+ * state changes trigger requests, ensuring the spinner animates.
476
+ */
477
+ startPolling() {
478
+ this.schedulePoll();
479
+ }
480
+ schedulePoll() {
481
+ const interval = this.hasActiveMembers ? ACTIVE_POLL_MS : IDLE_POLL_MS;
482
+ this.pollTimer = setTimeout(() => {
483
+ this.pollTimer = undefined;
484
+ // Tick the spinner frame even without state changes
485
+ if (this.state_) {
486
+ this.invalidate();
487
+ this.onRequestRender?.();
488
+ }
489
+ this.schedulePoll();
490
+ }, interval);
491
+ }
492
+ /**
493
+ * Adjust polling interval based on active member count.
494
+ */
495
+ updatePollingInterval() {
496
+ // Will be picked up on next poll cycle
497
+ }
498
+ /**
499
+ * Start the animation tick for the braille spinner.
500
+ */
198
501
  startAnimation() {
199
502
  this.animationFrame = setInterval(() => {
200
- this.invalidate();
201
- // 通知 TUI 框架重绘,使 braille 动画可见
202
- this.onRequestRender?.();
503
+ this.frameIndex = (this.frameIndex + 1) % BRAILLE_SPINNER.length;
504
+ // Only invalidate and re-render if there are active members (spinner visible)
505
+ // or if we're in an overlay that uses animation
506
+ if (this.hasActiveMembers || this.overlay) {
507
+ this.invalidate();
508
+ this.onRequestRender?.();
509
+ }
203
510
  }, FRAME_INTERVAL_MS);
204
511
  }
512
+ stopTimers() {
513
+ if (this.animationFrame !== undefined) {
514
+ clearInterval(this.animationFrame);
515
+ this.animationFrame = undefined;
516
+ }
517
+ if (this.debounceTimer !== undefined) {
518
+ clearTimeout(this.debounceTimer);
519
+ this.debounceTimer = undefined;
520
+ }
521
+ if (this.pollTimer !== undefined) {
522
+ clearTimeout(this.pollTimer);
523
+ this.pollTimer = undefined;
524
+ }
525
+ }
205
526
  }
206
527
  // ---------------------------------------------------------------------------
207
528
  // Rendering helpers
208
529
  // ---------------------------------------------------------------------------
209
- function borderTop(title, width) {
210
- const inner = ` ${title} `;
211
- // Reserve 2 chars for corner symbols (┌ and ┐), 1 for the leading dash
212
- const minLen = inner.length + 3;
213
- if (minLen > width) {
214
- const maxInner = Math.max(0, width - 2);
215
- const truncated = inner.slice(0, maxInner);
216
- return `\u250C${truncated}\u2510`;
217
- }
218
- const dashes = width - inner.length - 3;
219
- return `\u250C${inner}\u2500${"\u2500".repeat(Math.max(0, dashes))}\u2510`;
220
- }
221
- function borderBottom(width) {
222
- return `\u2514${"\u2500".repeat(Math.max(0, width - 2))}\u2518`;
223
- }
224
- function resolveOverallStatus(state) {
225
- if (state.queued === 0 && state.active === 0) {
226
- if (state.failed > 0)
227
- return FAILED_LABEL;
228
- return COMPLETED_LABEL;
530
+ function eventIcon(type) {
531
+ switch (type) {
532
+ case "started":
533
+ return "\u25B6"; //
534
+ case "completed":
535
+ return "\u2713"; //
536
+ case "failed":
537
+ return "\u2717"; // ✗
538
+ case "tool_execution":
539
+ return "\u2699"; //
540
+ case "suspended":
541
+ return "\u23F3"; // ⏳
542
+ case "phase_change":
543
+ return "\u2192"; //
229
544
  }
230
- if (state.active > 0)
231
- return WORKING_LABEL;
232
- return ORCHESTRATING_LABEL;
233
545
  }
234
- function buildStatusBar(state, width) {
235
- const barWidth = Math.max(1, width - 4);
236
- const total = state.total || 1;
237
- const doneRatio = (state.completed + state.failed) / total;
238
- const doneChars = Math.round(doneRatio * barWidth);
239
- const remainingChars = barWidth - doneChars;
240
- const done = "\u2501".repeat(doneChars);
241
- const remaining = "\u2501".repeat(remainingChars);
242
- return done + remaining;
243
- }
244
- function renderMemberRow(member, width) {
245
- const indexLabel = `#${String(member.index)}`;
246
- const brailleBar = renderBrailleBar(member, BRAILLE_BAR_MAX_WIDTH);
247
- const statusLabel = memberPhaseLabel(member.phase);
248
- const itemLabel = (member.item ?? "").slice(0, TEXT_CELL_PREFERRED_WIDTH);
249
- // Layout: #N [braille] Status item
250
- const fixed = `${indexLabel} ${brailleBar} ${statusLabel}`;
546
+ function renderMemberRow(member, width, frameIndex, selected) {
547
+ const prefix = selected ? ">" : " ";
548
+ const indexLabel = `#${String(member.index).padStart(2, "0")}`;
549
+ const icon = memberIcon(member, frameIndex);
550
+ const statusLabel = shortPhaseLabel(member.phase);
551
+ // Layout with optional activity column
552
+ const fixed = `${prefix}${indexLabel} ${icon} ${statusLabel}`;
251
553
  const fixedLen = visibleLen(fixed);
252
- // If the fixed part alone exceeds width, truncate it
253
- if (fixedLen >= width) {
254
- return fixed.slice(0, width);
554
+ let remaining = Math.max(0, width - fixedLen - 2);
555
+ // Show current tool if available
556
+ let activitySuffix = "";
557
+ if (member.currentTool && member.phase === "working") {
558
+ const toolInfo = member.activity
559
+ ? `${member.currentTool}: ${member.activity}`
560
+ : member.currentTool;
561
+ if (toolInfo.length < remaining - 2) {
562
+ activitySuffix = ` ${truncateText(toolInfo, Math.min(30, remaining - 2))}`;
563
+ remaining -= visibleLen(activitySuffix);
564
+ }
255
565
  }
256
- const itemSpace = Math.max(0, width - fixedLen - 2);
257
- const truncatedItem = truncateText(itemLabel, itemSpace);
258
- return `${fixed} ${truncatedItem}`;
566
+ const itemLabel = member.item ?? "";
567
+ const truncatedItem = truncateText(itemLabel, Math.max(0, remaining));
568
+ return `${fixed} ${truncatedItem}${activitySuffix}`;
259
569
  }
260
- function renderBrailleBar(member, maxWidth) {
261
- const phase = member.phase;
262
- const elapsed = member.phaseStartedAt
263
- ? Date.now() - member.phaseStartedAt
264
- : 0;
265
- switch (phase) {
570
+ function memberIcon(member, frameIndex) {
571
+ switch (member.phase) {
266
572
  case "completed":
267
- return BRAILLE_FULL.repeat(maxWidth);
573
+ return BRAILLE_FULL.repeat(2);
268
574
  case "failed":
269
575
  case "cancelled":
576
+ return "\u2717 ";
270
577
  case "pending":
271
578
  case "queued":
272
- return BRAILLE_EMPTY.repeat(maxWidth);
579
+ return "\u25CB ";
273
580
  case "prompting":
274
581
  case "working":
275
582
  case "suspended":
276
- return animatedBrailleBar(maxWidth, elapsed);
277
- }
278
- }
279
- function animatedBrailleBar(maxWidth, elapsedMs) {
280
- const cycleMs = 800; // One full animation cycle
281
- const progress = (elapsedMs % cycleMs) / cycleMs; // 0..1
282
- // Fill from left to right
283
- const totalDots = maxWidth * 6; // 6 dots per braille char
284
- const filledDots = Math.floor(progress * totalDots);
285
- let result = "";
286
- for (let i = 0; i < maxWidth; i += 1) {
287
- const cellStart = i * 6;
288
- const dotsInCell = Math.max(0, Math.min(6, filledDots - cellStart));
289
- result += dotsInCell === 0 ? BRAILLE_EMPTY : BRAILLE_LEVELS[dotsInCell];
583
+ return BRAILLE_SPINNER[frameIndex % BRAILLE_SPINNER.length] + " ";
290
584
  }
291
- return result;
292
585
  }
293
- function memberPhaseLabel(phase) {
586
+ function shortPhaseLabel(phase) {
294
587
  switch (phase) {
295
588
  case "pending":
296
589
  case "queued":
297
- return QUEUED_LABEL;
590
+ return "wait";
298
591
  case "prompting":
299
- return "Prompting...";
592
+ return "init";
300
593
  case "working":
301
- return WORKING_LABEL;
594
+ return "work";
302
595
  case "completed":
303
- return COMPLETED_LABEL;
596
+ return "done";
304
597
  case "failed":
305
- return FAILED_LABEL;
598
+ return "fail";
306
599
  case "cancelled":
307
- return ABORTED_LABEL;
600
+ return "abort";
308
601
  case "suspended":
309
- return "Rate limited...";
602
+ return "retry";
310
603
  }
311
604
  }
312
- function buildSummary(state, width) {
313
- const parts = [];
314
- if (state.completed > 0)
315
- parts.push(`completed: ${state.completed}`);
605
+ function buildFooter(state, width) {
606
+ const usage = state.totalUsage ?? {
607
+ input: 0,
608
+ output: 0,
609
+ cacheRead: 0,
610
+ cacheWrite: 0,
611
+ totalTokens: 0,
612
+ };
613
+ const counts = `${state.completed + state.failed}/${state.total} ag`;
614
+ const tokens = `${Math.round(usage.input)}in/${Math.round(usage.output)}out`;
615
+ const elapsed = formatElapsed(Date.now() - state.startedAt);
616
+ const parts = [counts, tokens, elapsed];
316
617
  if (state.failed > 0)
317
- parts.push(`failed: ${state.failed}`);
618
+ parts.splice(1, 0, `${state.failed}fail`);
318
619
  if (state.active > 0)
319
- parts.push(`active: ${state.active}`);
320
- if (state.queued > 0)
321
- parts.push(`queued: ${state.queued}`);
322
- const full = parts.join(", ");
620
+ parts.splice(1, 0, `${state.active}act`);
621
+ // ETA
622
+ if (state.estimatedRemainingMs !== undefined &&
623
+ state.estimatedRemainingMs > 0) {
624
+ parts.push(formatElapsed(state.estimatedRemainingMs));
625
+ }
626
+ const full = parts.join(" | ");
323
627
  if (full.length <= width)
324
628
  return full;
325
- return full.slice(0, width);
629
+ return truncateText(full, width);
630
+ }
631
+ function formatElapsed(ms) {
632
+ const totalSec = Math.floor(ms / 1000);
633
+ if (totalSec < 60)
634
+ return `${totalSec}s`;
635
+ const m = Math.floor(totalSec / 60);
636
+ const s = totalSec % 60;
637
+ return `${m}m${s}s`;
326
638
  }
327
639
  // ---------------------------------------------------------------------------
328
- // Text utilities (no external dependency — avoids import issues)
640
+ // Text utilities
329
641
  // ---------------------------------------------------------------------------
330
642
  function visibleLen(text) {
331
- // Simple implementation: count characters, stripping ANSI escapes
332
643
  return text.replace(/\x1b\[[0-9;]*m/g, "").length;
333
644
  }
334
645
  function truncateText(text, maxLen) {
335
646
  if (text.length <= maxLen)
336
647
  return text;
648
+ if (maxLen <= 1)
649
+ return text.slice(0, 1);
337
650
  return text.slice(0, Math.max(0, maxLen - 1)) + "\u2026";
338
651
  }
652
+ function padRight(text, width) {
653
+ if (text.length >= width)
654
+ return text.slice(0, width);
655
+ return text + " ".repeat(width - text.length);
656
+ }
339
657
  //# sourceMappingURL=progress.js.map