@gjczone/pi-swarm 0.5.0 → 0.7.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.
Files changed (68) hide show
  1. package/README.md +14 -21
  2. package/dist/index.d.ts +2 -12
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -18
  5. package/dist/index.js.map +1 -1
  6. package/dist/shared/controller.d.ts +10 -1
  7. package/dist/shared/controller.d.ts.map +1 -1
  8. package/dist/shared/controller.js +48 -12
  9. package/dist/shared/controller.js.map +1 -1
  10. package/dist/shared/render.d.ts +1 -1
  11. package/dist/shared/render.js +1 -1
  12. package/dist/shared/spawner.js +54 -8
  13. package/dist/shared/spawner.js.map +1 -1
  14. package/dist/shared/types.d.ts +10 -0
  15. package/dist/shared/types.d.ts.map +1 -1
  16. package/dist/shared/worktree.d.ts +2 -1
  17. package/dist/shared/worktree.d.ts.map +1 -1
  18. package/dist/shared/worktree.js +10 -4
  19. package/dist/shared/worktree.js.map +1 -1
  20. package/dist/state/recovery.d.ts.map +1 -1
  21. package/dist/state/recovery.js +25 -4
  22. package/dist/state/recovery.js.map +1 -1
  23. package/dist/swarm/command.d.ts +4 -5
  24. package/dist/swarm/command.d.ts.map +1 -1
  25. package/dist/swarm/command.js +26 -74
  26. package/dist/swarm/command.js.map +1 -1
  27. package/dist/swarm/mode.d.ts +1 -1
  28. package/dist/swarm/mode.js +2 -2
  29. package/dist/swarm/mode.js.map +1 -1
  30. package/dist/swarm/tool.d.ts +4 -4
  31. package/dist/swarm/tool.d.ts.map +1 -1
  32. package/dist/swarm/tool.js +112 -167
  33. package/dist/swarm/tool.js.map +1 -1
  34. package/dist/team/command.d.ts +2 -4
  35. package/dist/team/command.d.ts.map +1 -1
  36. package/dist/team/command.js +5 -13
  37. package/dist/team/command.js.map +1 -1
  38. package/dist/team/mailbox.d.ts +7 -2
  39. package/dist/team/mailbox.d.ts.map +1 -1
  40. package/dist/team/mailbox.js +121 -32
  41. package/dist/team/mailbox.js.map +1 -1
  42. package/dist/tui/progress.d.ts +35 -47
  43. package/dist/tui/progress.d.ts.map +1 -1
  44. package/dist/tui/progress.js +245 -489
  45. package/dist/tui/progress.js.map +1 -1
  46. package/dist/tui/swarm-markers.d.ts +1 -1
  47. package/dist/tui/swarm-markers.js +1 -1
  48. package/package.json +13 -2
  49. package/dist/team/supervisor.d.ts +0 -171
  50. package/dist/team/supervisor.d.ts.map +0 -1
  51. package/dist/team/supervisor.js +0 -685
  52. package/dist/team/supervisor.js.map +0 -1
  53. package/dist/team/task-graph.d.ts +0 -64
  54. package/dist/team/task-graph.d.ts.map +0 -1
  55. package/dist/team/task-graph.js +0 -216
  56. package/dist/team/task-graph.js.map +0 -1
  57. package/dist/team/tool.d.ts +0 -11
  58. package/dist/team/tool.d.ts.map +0 -1
  59. package/dist/team/tool.js +0 -491
  60. package/dist/team/tool.js.map +0 -1
  61. package/dist/tui/permission-prompt.d.ts +0 -26
  62. package/dist/tui/permission-prompt.d.ts.map +0 -1
  63. package/dist/tui/permission-prompt.js +0 -98
  64. package/dist/tui/permission-prompt.js.map +0 -1
  65. package/dist/tui/team-dashboard.d.ts +0 -81
  66. package/dist/tui/team-dashboard.d.ts.map +0 -1
  67. package/dist/tui/team-dashboard.js +0 -657
  68. package/dist/tui/team-dashboard.js.map +0 -1
@@ -1,38 +1,19 @@
1
1
  /**
2
2
  * tui/progress — AgentSwarm live progress panel.
3
3
  *
4
- * Renders a real-time progress display above the input area when
5
- * an AgentSwarm batch is running. Each subagent gets a compact braille
6
- * spinner with status and item description.
4
+ * Vertical panel layout with fixed-width tool-call-based braille progress
5
+ * bars and inline activity text. Each agent renders as a single line:
6
+ * 001 [braille bar] read: src/lib.rs lines 42-99
7
+ * Bar width is fixed (5 cells) so tool labels align across agents.
7
8
  *
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
9
+ * Progress is driven by actual tool calls / activity events (progressTick),
10
+ * not wall-clock time. An agent that makes more progress fills faster.
15
11
  *
16
- * Ported from MoonshotAI/kimi-code's AgentSwarmProgressComponent.
12
+ * For 5+ agents, switches to a 2-column compact grid (3-cell bars).
17
13
  */
18
- import { matchesKey, Key, isKeyRepeat } from "@earendil-works/pi-tui";
19
14
  // ---------------------------------------------------------------------------
20
15
  // Constants
21
16
  // ---------------------------------------------------------------------------
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. */
36
17
  const BRAILLE_LEVELS = [
37
18
  "\u28C0",
38
19
  "\u28C4",
@@ -42,36 +23,21 @@ const BRAILLE_LEVELS = [
42
23
  "\u28F7",
43
24
  "\u28FF",
44
25
  ];
45
- const BRAILLE_EMPTY = BRAILLE_LEVELS[0];
46
- const BRAILLE_FULL = BRAILLE_LEVELS[6];
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. */
26
+ const BRAILLE_EMPTY = "\u28C0"; // baseline empty (bottom dots), so bar track is always visible
53
27
  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;
28
+ const POLL_MS = 800;
29
+ const MAX_VISIBLE_MEMBERS = 20;
30
+ const ID_WIDTH = 3;
31
+ // Fixed-width braille bar — all agents share the same width so labels align.
32
+ // Vertical mode: 5 cells, grid mode: 3 cells.
33
+ const FIXED_BAR_CELLS = 5;
34
+ const GRID_BAR_CELLS = 3;
35
+ // Layout: agents shown vertically when count <= this
36
+ const VERTICAL_LAYOUT_MAX = 4;
60
37
  // ---------------------------------------------------------------------------
61
38
  // Snapshot conversion
62
39
  // ---------------------------------------------------------------------------
63
40
  export function snapshotToProgressState(snapshot, title) {
64
- const now = Date.now();
65
- const members = snapshot.members.map((m) => ({
66
- index: m.index,
67
- phase: mapMemberPhase(m.phase),
68
- item: m.item,
69
- error: m.error,
70
- phaseStartedAt: isAnimatedPhase(m.phase) ? now : undefined,
71
- currentTool: m.currentTool,
72
- activity: m.activity,
73
- usage: m.usage,
74
- }));
75
41
  return {
76
42
  title,
77
43
  total: snapshot.total,
@@ -79,11 +45,19 @@ export function snapshotToProgressState(snapshot, title) {
79
45
  failed: snapshot.failed,
80
46
  active: snapshot.active,
81
47
  queued: snapshot.queued,
82
- members,
48
+ members: snapshot.members.map((m) => ({
49
+ index: m.index,
50
+ phase: mapMemberPhase(m.phase),
51
+ item: m.item,
52
+ error: m.error,
53
+ currentTool: m.currentTool,
54
+ activity: m.activity,
55
+ usage: m.usage,
56
+ progressTick: m.progressTick,
57
+ startedAt: m.startedAt,
58
+ })),
83
59
  totalUsage: snapshot.totalUsage,
84
- startedAt: snapshot.startedAt ?? now,
85
- estimatedRemainingMs: snapshot.estimatedRemainingMs,
86
- eventLog: snapshot.eventLog ? [...snapshot.eventLog] : undefined,
60
+ startedAt: snapshot.startedAt ?? Date.now(),
87
61
  };
88
62
  }
89
63
  function mapMemberPhase(phase) {
@@ -100,44 +74,23 @@ function mapMemberPhase(phase) {
100
74
  return "suspended";
101
75
  }
102
76
  }
103
- function isAnimatedPhase(phase) {
104
- return phase === "working" || phase === "suspended";
105
- }
106
77
  // ---------------------------------------------------------------------------
107
78
  // Component
108
79
  // ---------------------------------------------------------------------------
109
80
  export class AgentSwarmProgressComponent {
110
81
  state_ = null;
111
- renderedWidth;
112
- cachedLines;
113
82
  onRequestRender;
114
- // Render scheduler
115
- animationFrame;
116
- frameIndex = 0;
117
83
  pollTimer;
118
84
  debounceTimer;
119
- pendingInvalidate = false;
120
- hasActiveMembers = false;
121
- // Input / UI state
122
- scrollOffset = 0;
123
- selectedIndex = -1;
124
- activePanel = "members";
125
- overlay = null;
126
85
  constructor(onRequestRender) {
127
86
  this.onRequestRender = onRequestRender;
128
87
  this.startPolling();
129
- this.startAnimation();
130
88
  }
131
89
  // -------------------------------------------------------------------
132
90
  // Public API
133
91
  // -------------------------------------------------------------------
134
92
  update(state) {
135
93
  this.state_ = state;
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
94
  this.requestRender();
142
95
  }
143
96
  complete() {
@@ -147,14 +100,12 @@ export class AgentSwarmProgressComponent {
147
100
  m.phase !== "failed" &&
148
101
  m.phase !== "cancelled") {
149
102
  m.phase = "completed";
150
- m.phaseStartedAt = Date.now();
151
103
  }
152
104
  }
153
105
  this.state_.active = 0;
154
106
  this.state_.queued = 0;
155
107
  this.state_.completed = this.state_.total - this.state_.failed;
156
108
  }
157
- this.hasActiveMembers = false;
158
109
  this.requestRender();
159
110
  }
160
111
  dispose() {
@@ -162,358 +113,261 @@ export class AgentSwarmProgressComponent {
162
113
  this.onRequestRender = undefined;
163
114
  }
164
115
  invalidate() {
165
- this.renderedWidth = undefined;
166
- this.cachedLines = undefined;
116
+ /* no-op */
167
117
  }
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
- }
118
+ handleInput(_data) {
119
+ /* minimal: no keyboard */
250
120
  }
251
121
  // -------------------------------------------------------------------
252
122
  // Rendering
253
123
  // -------------------------------------------------------------------
254
124
  render(width) {
255
125
  const safeWidth = Math.max(20, width);
256
- if (this.cachedLines && this.renderedWidth === safeWidth) {
257
- return this.cachedLines;
258
- }
259
- if (!this.state_ || this.state_.members.length === 0) {
260
- this.cachedLines = [];
261
- return this.cachedLines;
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
- }
126
+ if (!this.state_ || this.state_.members.length === 0)
127
+ return [];
269
128
  const state = this.state_;
270
- const contentWidth = safeWidth - 4;
271
129
  const lines = [];
272
- // Header: title + panel indicator
273
- const title = state.title ?? "Agent Swarm";
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);
290
- }
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}`);
297
- // Bottom border
298
- const bottom = `\u2514${"\u2500".repeat(contentWidth)}\u2518`;
299
- lines.push(bottom);
300
- this.cachedLines = lines;
301
- this.renderedWidth = safeWidth;
302
- return this.cachedLines;
130
+ // Header: Agent Swarm ─ <title> ──────────
131
+ lines.push(this.renderHeader(safeWidth, state));
132
+ lines.push("");
133
+ // Agent panels (vertical or compact grid)
134
+ const memberLines = this.renderAgentPanels(safeWidth - 2, state);
135
+ lines.push(...memberLines);
136
+ lines.push("");
137
+ // Bottom separator
138
+ const sepWidth = safeWidth - 2;
139
+ lines.push(truncateText(repeatStr("\u2500", sepWidth), sepWidth));
140
+ // Status line: Working... N/M (P%) elapsed
141
+ lines.push(this.renderStatusLine(safeWidth, state));
142
+ return lines;
303
143
  }
304
144
  // -------------------------------------------------------------------
305
- // Panel rendering
145
+ // Header: ─ Agent Swarm ─ <desc> ──────────
306
146
  // -------------------------------------------------------------------
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
- }
147
+ renderHeader(width, state) {
148
+ const mode = state.mailbox ? "Swarm Team" : "Agent Swarm";
149
+ const desc = state.title ?? "";
150
+ const mailboxInfo = state.mailboxCount && state.mailboxCount > 0
151
+ ? ` | Mailbox: ${state.mailboxCount}`
152
+ : "";
153
+ const prefix = `\u2500 ${mode}`;
154
+ const content = desc ? ` \u2500 ${desc}` : "";
155
+ const label = `${prefix}${content}${mailboxInfo}`;
156
+ const suffixLen = Math.max(0, width - visibleLen(label) - 2);
157
+ const suffix = suffixLen > 0 ? ` \u2500${repeatStr("\u2500", suffixLen)}` : "";
158
+ return truncateText(`${label}${suffix}`, width);
328
159
  }
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
- }
160
+ // -------------------------------------------------------------------
161
+ // Status line
162
+ // -------------------------------------------------------------------
163
+ renderStatusLine(width, state) {
164
+ const done = state.completed + state.failed;
165
+ const total = state.total;
166
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0;
167
+ const elapsed = formatElapsed(Date.now() - state.startedAt);
168
+ const label = state.active > 0 ? "Working..." : "Completed";
169
+ return truncateText(`${label} ${done}/${total} (${pct}%) ${elapsed}`, width);
342
170
  }
343
171
  // -------------------------------------------------------------------
344
- // Overlays
172
+ // Agent panel layout selection
345
173
  // -------------------------------------------------------------------
346
- renderOverlay(width) {
347
- if (!this.overlay)
174
+ /**
175
+ * Render all agents. Uses vertical panels for small counts
176
+ * and a compact 2-column grid for larger batches.
177
+ */
178
+ renderAgentPanels(width, state) {
179
+ const count = Math.min(state.members.length, MAX_VISIBLE_MEMBERS);
180
+ if (count <= 0)
348
181
  return [];
349
- if (this.overlay.kind === "help") {
350
- return this.renderHelpOverlay(width);
182
+ if (count <= VERTICAL_LAYOUT_MAX) {
183
+ return this.renderVerticalPanels(width, state);
351
184
  }
352
- return this.renderDetailOverlay(width);
185
+ return this.renderCompactGrid(width, state);
353
186
  }
354
- renderHelpOverlay(width) {
355
- const safeWidth = Math.max(30, width);
356
- const contentWidth = safeWidth - 6;
187
+ // -------------------------------------------------------------------
188
+ // Vertical panel layout (1-4 agents)
189
+ //
190
+ // Each agent is a single line with live activity inline:
191
+ // 001 [▓▓▓▓▓░░░] read: src/lib.rs lines 42-99
192
+ // (blank line between agents)
193
+ // -------------------------------------------------------------------
194
+ renderVerticalPanels(width, state) {
195
+ const count = Math.min(state.members.length, MAX_VISIBLE_MEMBERS);
357
196
  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`);
197
+ for (let i = 0; i < count; i++) {
198
+ const member = state.members[i];
199
+ lines.push(this.renderAgentLine(member, width));
200
+ if (i < count - 1) {
201
+ lines.push(""); // blank line separator between agents
202
+ }
203
+ }
204
+ if (state.members.length > MAX_VISIBLE_MEMBERS) {
205
+ lines.push(` ... ${state.members.length - MAX_VISIBLE_MEMBERS} more`);
374
206
  }
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
207
  return lines;
379
208
  }
380
- renderDetailOverlay(width) {
381
- const safeWidth = Math.max(30, width);
382
- const contentWidth = safeWidth - 6;
209
+ /**
210
+ * Render one agent as a single line with fixed-width progress bar.
211
+ *
212
+ * Format: `001 [braille bar] read: src/lib.rs lines 42-99`
213
+ * Bar is always FIXED_BAR_CELLS (5) wide so tool labels align across agents.
214
+ *
215
+ * Progress is driven by progressTick (actual tool calls / activity events),
216
+ * not wall-clock time. Each tick fills one level of the braille bar.
217
+ */
218
+ renderAgentLine(member, width) {
219
+ const id = String(member.index).padStart(ID_WIDTH, "0");
220
+ // Fixed bar width — ensures all agent labels align
221
+ const barCells = FIXED_BAR_CELLS;
222
+ // Label gets the remaining space after id + bar + fixed gaps
223
+ const labelWidth = Math.max(4, width - ID_WIDTH - barCells - 4);
224
+ const bar = this.renderBrailleBar(member, barCells);
225
+ const label = this.renderCellLabel(member, labelWidth);
226
+ return truncateText(`${id} ${bar} ${label}`, width);
227
+ }
228
+ // -------------------------------------------------------------------
229
+ // Compact grid layout (5+ agents)
230
+ //
231
+ // 2 columns, single-line cells:
232
+ // 001 [▓▓▓] task 002 [▓▓▓] task
233
+ // 003 [▓▓▓] task 004 [▓▓▓] task
234
+ // -------------------------------------------------------------------
235
+ renderCompactGrid(width, state) {
236
+ const cols = 2;
237
+ const count = Math.min(state.members.length, MAX_VISIBLE_MEMBERS);
238
+ const gap = 3; // spaces between columns
239
+ const cellWidth = Math.floor((width - gap) / cols);
240
+ const rows = Math.ceil(count / cols);
383
241
  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 });
242
+ for (let row = 0; row < rows; row++) {
243
+ let line = "";
244
+ for (let col = 0; col < cols; col++) {
245
+ const idx = row * cols + col;
246
+ if (idx >= count)
247
+ break;
248
+ const member = state.members[idx];
249
+ const cw = col < cols - 1 ? cellWidth : width - line.length;
250
+ const cell = this.renderGridCell(member, cw);
251
+ line += cell;
252
+ if (col < cols - 1) {
253
+ // Pad to fill the remaining cell width + gap
254
+ const padLen = Math.max(0, cellWidth - visibleLen(cell) + gap);
255
+ line += " ".repeat(padLen);
256
+ }
257
+ }
258
+ lines.push(truncateText(line, width));
414
259
  }
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`);
260
+ if (state.members.length > MAX_VISIBLE_MEMBERS) {
261
+ lines.push(` ... ${state.members.length - MAX_VISIBLE_MEMBERS} more`);
418
262
  }
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
263
  return lines;
423
264
  }
265
+ /**
266
+ * Compact single-line grid cell.
267
+ * Format: `001 [▓▓] label`
268
+ */
269
+ renderGridCell(member, width) {
270
+ const id = String(member.index).padStart(ID_WIDTH, "0");
271
+ // Compact grid uses fewer bar cells
272
+ const barCells = GRID_BAR_CELLS;
273
+ const bar = this.renderBrailleBar(member, barCells);
274
+ const labelWidth = Math.max(2, width - ID_WIDTH - barCells - 4);
275
+ const label = this.renderCellLabel(member, labelWidth);
276
+ return `${id} ${bar} ${label}`;
277
+ }
424
278
  // -------------------------------------------------------------------
425
- // Scroll helpers
279
+ // Braille progress bar
280
+ //
281
+ // Tool-call-based progress for working agents:
282
+ // - Each progressTick (tool call / activity event) fills one level.
283
+ // - Bar fills up as the agent works; capped at 85% so completed
284
+ // agents (full bar) are visually distinguishable from working ones.
285
+ // - Empty cells show the baseline character so the bar track is always visible.
286
+ //
287
+ // Completed agents: full bar
288
+ // Failed agents: half bar
289
+ // Queued/suspended: empty bar (baseline only)
426
290
  // -------------------------------------------------------------------
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);
291
+ renderBrailleBar(member, width) {
292
+ if (width <= 0)
293
+ return "";
294
+ const capacity = width * BRAILLE_LEVELS.length;
295
+ let ticks;
296
+ if (member.phase === "completed") {
297
+ ticks = capacity;
298
+ }
299
+ else if (member.phase === "failed") {
300
+ ticks = Math.floor(capacity / 2);
301
+ }
302
+ else if (member.phase === "working" || member.phase === "prompting") {
303
+ // Fill based on actual tool-call progress.
304
+ // Each tool execution or model output event increments progressTick by 1.
305
+ // Cap at 85% so "almost done" is visually distinct from "completed".
306
+ ticks = Math.min(member.progressTick ?? 0, Math.floor(capacity * 0.85));
438
307
  }
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);
308
+ else {
309
+ ticks = 0;
310
+ }
311
+ const fullBars = Math.floor(ticks / BRAILLE_LEVELS.length);
312
+ const partial = ticks % BRAILLE_LEVELS.length;
313
+ const partialChar = partial > 0 ? BRAILLE_LEVELS[partial - 1] : "";
314
+ let bar = "";
315
+ for (let i = 0; i < width; i++) {
316
+ if (i < fullBars) {
317
+ bar += BRAILLE_LEVELS[BRAILLE_LEVELS.length - 1]; // Full cell
318
+ }
319
+ else if (i === fullBars && partialChar) {
320
+ bar += partialChar;
321
+ }
322
+ else {
323
+ bar += BRAILLE_EMPTY;
324
+ }
447
325
  }
448
- this.requestRender();
326
+ return bar;
327
+ }
328
+ // -------------------------------------------------------------------
329
+ // Cell label rendering
330
+ // -------------------------------------------------------------------
331
+ renderCellLabel(member, width) {
332
+ if (width <= 0)
333
+ return "";
334
+ if (member.phase === "working" || member.phase === "prompting") {
335
+ // Show tool: activity_text (scrolling model output / shell command)
336
+ const toolPart = member.currentTool ? `${member.currentTool}: ` : "";
337
+ const activityText = member.activity ?? member.item ?? "";
338
+ const text = toolPart + activityText;
339
+ return truncateText(text, width);
340
+ }
341
+ if (member.phase === "completed")
342
+ return truncateText("ok", width);
343
+ if (member.phase === "failed" && member.error)
344
+ return truncateText(member.error, Math.min(width, 20));
345
+ if (member.phase === "queued" || member.phase === "suspended")
346
+ return truncateText(member.item ?? "...", width);
347
+ return "";
449
348
  }
450
349
  // -------------------------------------------------------------------
451
350
  // Render scheduling
452
351
  // -------------------------------------------------------------------
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
352
  requestRender() {
459
- this.invalidate();
460
353
  if (this.debounceTimer !== undefined) {
461
- this.pendingInvalidate = true;
462
354
  return;
463
355
  }
464
356
  this.debounceTimer = setTimeout(() => {
465
357
  this.debounceTimer = undefined;
466
- this.pendingInvalidate = false;
467
358
  this.onRequestRender?.();
468
- this.updatePollingInterval();
469
359
  }, DEBOUNCE_MS);
470
- // Also trigger an immediate render for faster response to inputs
471
360
  this.onRequestRender?.();
472
361
  }
473
- /**
474
- * Start the fallback polling timer. Falls back to polling when no
475
- * state changes trigger requests, ensuring the spinner animates.
476
- */
477
362
  startPolling() {
478
- this.schedulePoll();
479
- }
480
- schedulePoll() {
481
- const interval = this.hasActiveMembers ? ACTIVE_POLL_MS : IDLE_POLL_MS;
482
363
  this.pollTimer = setTimeout(() => {
483
364
  this.pollTimer = undefined;
484
- // Tick the spinner frame even without state changes
485
- if (this.state_) {
486
- this.invalidate();
365
+ if (this.state_)
487
366
  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
- */
501
- startAnimation() {
502
- this.animationFrame = setInterval(() => {
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
- }
510
- }, FRAME_INTERVAL_MS);
367
+ this.startPolling();
368
+ }, POLL_MS);
511
369
  }
512
370
  stopTimers() {
513
- if (this.animationFrame !== undefined) {
514
- clearInterval(this.animationFrame);
515
- this.animationFrame = undefined;
516
- }
517
371
  if (this.debounceTimer !== undefined) {
518
372
  clearTimeout(this.debounceTimer);
519
373
  this.debounceTimer = undefined;
@@ -525,108 +379,13 @@ export class AgentSwarmProgressComponent {
525
379
  }
526
380
  }
527
381
  // ---------------------------------------------------------------------------
528
- // Rendering helpers
382
+ // Text utilities
529
383
  // ---------------------------------------------------------------------------
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"; // →
544
- }
545
- }
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}`;
553
- const fixedLen = visibleLen(fixed);
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
- }
565
- }
566
- const itemLabel = member.item ?? "";
567
- const truncatedItem = truncateText(itemLabel, Math.max(0, remaining));
568
- return `${fixed} ${truncatedItem}${activitySuffix}`;
569
- }
570
- function memberIcon(member, frameIndex) {
571
- switch (member.phase) {
572
- case "completed":
573
- return BRAILLE_FULL.repeat(2);
574
- case "failed":
575
- case "cancelled":
576
- return "\u2717 ";
577
- case "pending":
578
- case "queued":
579
- return "\u25CB ";
580
- case "prompting":
581
- case "working":
582
- case "suspended":
583
- return BRAILLE_SPINNER[frameIndex % BRAILLE_SPINNER.length] + " ";
584
- }
585
- }
586
- function shortPhaseLabel(phase) {
587
- switch (phase) {
588
- case "pending":
589
- case "queued":
590
- return "wait";
591
- case "prompting":
592
- return "init";
593
- case "working":
594
- return "work";
595
- case "completed":
596
- return "done";
597
- case "failed":
598
- return "fail";
599
- case "cancelled":
600
- return "abort";
601
- case "suspended":
602
- return "retry";
603
- }
604
- }
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];
617
- if (state.failed > 0)
618
- parts.splice(1, 0, `${state.failed}fail`);
619
- if (state.active > 0)
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(" | ");
627
- if (full.length <= width)
628
- return full;
629
- return truncateText(full, width);
384
+ function repeatStr(ch, count) {
385
+ let out = "";
386
+ for (let i = 0; i < count; i++)
387
+ out += ch;
388
+ return out;
630
389
  }
631
390
  function formatElapsed(ms) {
632
391
  const totalSec = Math.floor(ms / 1000);
@@ -634,24 +393,21 @@ function formatElapsed(ms) {
634
393
  return `${totalSec}s`;
635
394
  const m = Math.floor(totalSec / 60);
636
395
  const s = totalSec % 60;
637
- return `${m}m${s}s`;
396
+ if (m < 60)
397
+ return `${m}m ${s}s`;
398
+ const h = Math.floor(m / 60);
399
+ return `${h}h ${m % 60}m ${s}s`;
638
400
  }
639
- // ---------------------------------------------------------------------------
640
- // Text utilities
641
- // ---------------------------------------------------------------------------
642
401
  function visibleLen(text) {
643
402
  return text.replace(/\x1b\[[0-9;]*m/g, "").length;
644
403
  }
645
404
  function truncateText(text, maxLen) {
405
+ if (maxLen <= 0)
406
+ return "";
646
407
  if (text.length <= maxLen)
647
408
  return text;
648
409
  if (maxLen <= 1)
649
410
  return text.slice(0, 1);
650
- return text.slice(0, Math.max(0, maxLen - 1)) + "\u2026";
651
- }
652
- function padRight(text, width) {
653
- if (text.length >= width)
654
- return text.slice(0, width);
655
- return text + " ".repeat(width - text.length);
411
+ return text.slice(0, maxLen - 1) + "\u2026";
656
412
  }
657
413
  //# sourceMappingURL=progress.js.map