@gjczone/pi-swarm 0.5.0 → 0.7.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 (68) hide show
  1. package/README.md +14 -21
  2. package/dist/index.d.ts +3 -10
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +4 -16
  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 +47 -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 +49 -5
  13. package/dist/shared/spawner.js.map +1 -1
  14. package/dist/shared/types.d.ts +8 -0
  15. package/dist/shared/types.d.ts.map +1 -1
  16. package/dist/shared/worktree.d.ts +2 -0
  17. package/dist/shared/worktree.d.ts.map +1 -1
  18. package/dist/shared/worktree.js +10 -3
  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 +107 -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 -0
  39. package/dist/team/mailbox.d.ts.map +1 -1
  40. package/dist/team/mailbox.js +99 -13
  41. package/dist/team/mailbox.js.map +1 -1
  42. package/dist/tui/progress.d.ts +18 -50
  43. package/dist/tui/progress.d.ts.map +1 -1
  44. package/dist/tui/progress.js +200 -483
  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,27 +1,26 @@
1
1
  /**
2
- * tui/progress — AgentSwarm live progress panel.
2
+ * tui/progress — AgentSwarm live progress panel (minimal).
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
+ * Grid layout with braille progress bars and scrolling model output.
5
+ * - Multiple agents: grid cells with ID + braille bar + model text
6
+ * - Single agent: compact status line with spinner + text
7
+ * - No token/in/out display — replaced by scrolling model output
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
15
- *
16
- * Ported from MoonshotAI/kimi-code's AgentSwarmProgressComponent.
9
+ * Architecture reference: AgentSwarm pattern.
17
10
  */
18
- import { matchesKey, Key, isKeyRepeat } from "@earendil-works/pi-tui";
19
11
  // ---------------------------------------------------------------------------
20
12
  // Constants
21
13
  // ---------------------------------------------------------------------------
22
- /** Max item description length to display. */
23
- const MAX_ITEM_LABEL_LEN = 40;
24
- /** Compact braille spinner frames for running agents. */
14
+ const BRAILLE_LEVELS = [
15
+ "\u28C0",
16
+ "\u28C4",
17
+ "\u28E4",
18
+ "\u28E6",
19
+ "\u28F6",
20
+ "\u28F7",
21
+ "\u28FF",
22
+ ];
23
+ const BRAILLE_EMPTY = "\u2800"; // truly empty braille cell (no dots)
25
24
  const BRAILLE_SPINNER = [
26
25
  "\u28BF",
27
26
  "\u28FB",
@@ -32,46 +31,18 @@ const BRAILLE_SPINNER = [
32
31
  "\u28DF",
33
32
  "\u287F",
34
33
  ];
35
- /** Braille characters representing fill levels 0-6 dots for completed bar. */
36
- const BRAILLE_LEVELS = [
37
- "\u28C0",
38
- "\u28C4",
39
- "\u28E4",
40
- "\u28E6",
41
- "\u28F6",
42
- "\u28F7",
43
- "\u28FF",
44
- ];
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. */
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
34
  const FRAME_INTERVAL_MS = 80;
35
+ const DEBOUNCE_MS = 75;
36
+ const POLL_MS = 800;
37
+ const MAX_VISIBLE_MEMBERS = 20;
38
+ const CELL_GAP = " ";
39
+ const BRAILLE_BAR_MIN_WIDTH = 2;
40
+ const BRAILLE_BAR_MAX_WIDTH = 5;
41
+ const ID_WIDTH = 3;
60
42
  // ---------------------------------------------------------------------------
61
43
  // Snapshot conversion
62
44
  // ---------------------------------------------------------------------------
63
45
  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
46
  return {
76
47
  title,
77
48
  total: snapshot.total,
@@ -79,11 +50,18 @@ export function snapshotToProgressState(snapshot, title) {
79
50
  failed: snapshot.failed,
80
51
  active: snapshot.active,
81
52
  queued: snapshot.queued,
82
- members,
53
+ members: snapshot.members.map((m) => ({
54
+ index: m.index,
55
+ phase: mapMemberPhase(m.phase),
56
+ item: m.item,
57
+ error: m.error,
58
+ currentTool: m.currentTool,
59
+ activity: m.activity,
60
+ usage: m.usage,
61
+ progressTick: m.progressTick,
62
+ })),
83
63
  totalUsage: snapshot.totalUsage,
84
- startedAt: snapshot.startedAt ?? now,
85
- estimatedRemainingMs: snapshot.estimatedRemainingMs,
86
- eventLog: snapshot.eventLog ? [...snapshot.eventLog] : undefined,
64
+ startedAt: snapshot.startedAt ?? Date.now(),
87
65
  };
88
66
  }
89
67
  function mapMemberPhase(phase) {
@@ -100,29 +78,16 @@ function mapMemberPhase(phase) {
100
78
  return "suspended";
101
79
  }
102
80
  }
103
- function isAnimatedPhase(phase) {
104
- return phase === "working" || phase === "suspended";
105
- }
106
81
  // ---------------------------------------------------------------------------
107
82
  // Component
108
83
  // ---------------------------------------------------------------------------
109
84
  export class AgentSwarmProgressComponent {
110
85
  state_ = null;
111
- renderedWidth;
112
- cachedLines;
113
86
  onRequestRender;
114
- // Render scheduler
115
87
  animationFrame;
116
88
  frameIndex = 0;
117
89
  pollTimer;
118
90
  debounceTimer;
119
- pendingInvalidate = false;
120
- hasActiveMembers = false;
121
- // Input / UI state
122
- scrollOffset = 0;
123
- selectedIndex = -1;
124
- activePanel = "members";
125
- overlay = null;
126
91
  constructor(onRequestRender) {
127
92
  this.onRequestRender = onRequestRender;
128
93
  this.startPolling();
@@ -133,11 +98,6 @@ export class AgentSwarmProgressComponent {
133
98
  // -------------------------------------------------------------------
134
99
  update(state) {
135
100
  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
101
  this.requestRender();
142
102
  }
143
103
  complete() {
@@ -147,14 +107,12 @@ export class AgentSwarmProgressComponent {
147
107
  m.phase !== "failed" &&
148
108
  m.phase !== "cancelled") {
149
109
  m.phase = "completed";
150
- m.phaseStartedAt = Date.now();
151
110
  }
152
111
  }
153
112
  this.state_.active = 0;
154
113
  this.state_.queued = 0;
155
114
  this.state_.completed = this.state_.total - this.state_.failed;
156
115
  }
157
- this.hasActiveMembers = false;
158
116
  this.requestRender();
159
117
  }
160
118
  dispose() {
@@ -162,351 +120,208 @@ export class AgentSwarmProgressComponent {
162
120
  this.onRequestRender = undefined;
163
121
  }
164
122
  invalidate() {
165
- this.renderedWidth = undefined;
166
- this.cachedLines = undefined;
123
+ /* no-op */
167
124
  }
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
- }
125
+ handleInput(_data) {
126
+ /* minimal: no keyboard */
250
127
  }
251
128
  // -------------------------------------------------------------------
252
129
  // Rendering
253
130
  // -------------------------------------------------------------------
254
131
  render(width) {
255
132
  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
- }
133
+ if (!this.state_ || this.state_.members.length === 0)
134
+ return [];
269
135
  const state = this.state_;
270
- const contentWidth = safeWidth - 4;
271
136
  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);
137
+ // Layout: header grid → status line
138
+ lines.push(this.renderHeader(safeWidth, state));
139
+ lines.push("");
140
+ if (state.members.length === 1) {
141
+ // Single agent: compact line, no grid
142
+ const row = this.renderSingleAgent(state.members[0], safeWidth - 2);
143
+ lines.push(row);
287
144
  }
288
145
  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;
146
+ // Multiple agents: grid layout
147
+ const gridLines = this.renderGrid(safeWidth - 2, state);
148
+ lines.push(...gridLines);
149
+ }
150
+ lines.push("");
151
+ // Bottom separator
152
+ const sepWidth = safeWidth - 2;
153
+ lines.push(truncateText(repeatStr("─", sepWidth), sepWidth));
154
+ lines.push(this.renderStatusLine(safeWidth, state));
155
+ return lines;
303
156
  }
304
157
  // -------------------------------------------------------------------
305
- // Panel rendering
158
+ // Kimi-code style header: ─ Agent Swarm ─ description ──────
306
159
  // -------------------------------------------------------------------
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
- }
160
+ renderHeader(width, state) {
161
+ const mode = state.mailbox ? "Swarm Team" : "Agent Swarm";
162
+ const desc = state.title ?? "";
163
+ const mailboxInfo = state.mailboxCount && state.mailboxCount > 0
164
+ ? ` Mailbox: ${state.mailboxCount}`
165
+ : "";
166
+ // ─ Agent Swarm ─ <desc> ────── or ─ Swarm Team ─ <desc> ─ Mailbox: 3
167
+ const prefix = `─ ${mode}`;
168
+ const content = desc ? ` ─ ${desc}` : "";
169
+ const label = `${prefix}${content}${mailboxInfo}`;
170
+ const suffixLen = Math.max(0, width - visibleLen(label) - 2);
171
+ const suffix = suffixLen > 0 ? ` ─${repeatStr("─", suffixLen)}` : "";
172
+ return truncateText(`${label}${suffix}`, width);
342
173
  }
343
174
  // -------------------------------------------------------------------
344
- // Overlays
175
+ // Status line
345
176
  // -------------------------------------------------------------------
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);
177
+ renderStatusLine(width, state) {
178
+ const done = state.completed + state.failed;
179
+ const total = state.total;
180
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0;
181
+ const elapsed = formatElapsed(Date.now() - state.startedAt);
182
+ const label = state.active > 0 ? "Working..." : "Completed";
183
+ return truncateText(`${label} ${done}/${total} (${pct}%) ${elapsed}`, width);
353
184
  }
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;
185
+ // -------------------------------------------------------------------
186
+ // Single agent mode
187
+ // -------------------------------------------------------------------
188
+ renderSingleAgent(member, width) {
189
+ const spinner = member.phase === "working" || member.phase === "prompting"
190
+ ? BRAILLE_SPINNER[this.frameIndex % BRAILLE_SPINNER.length]
191
+ : member.phase === "completed"
192
+ ? "\u2713"
193
+ : member.phase === "failed"
194
+ ? "\u2717"
195
+ : "\u25CB";
196
+ const item = member.item ?? `#${String(member.index).padStart(2, "0")}`;
197
+ const itemTrunc = truncateText(item, Math.max(4, Math.floor(width * 0.4)));
198
+ let suffix = "";
199
+ const remaining = Math.max(0, width - visibleLen(`${spinner} ${itemTrunc}`) - 2);
200
+ if (member.phase === "working" && member.activity && remaining > 4) {
201
+ suffix = ` ${truncateText(member.activity, Math.min(remaining - 1, width - 10))}`;
202
+ }
203
+ else if (member.phase === "completed") {
204
+ suffix = " ok";
205
+ }
206
+ else if (member.phase === "failed" && member.error && remaining > 4) {
207
+ suffix = ` ${truncateText(member.error, Math.min(remaining - 1, 30))}`;
208
+ }
209
+ // No braille bar for single agent — just spinner + item + scrolling text
210
+ return truncateText(`${spinner} ${itemTrunc}${suffix}`, width);
379
211
  }
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)
212
+ // -------------------------------------------------------------------
213
+ // Grid layout (multiple agents)
214
+ // -------------------------------------------------------------------
215
+ renderGrid(width, state) {
216
+ const count = Math.min(state.members.length, MAX_VISIBLE_MEMBERS);
217
+ if (count <= 0)
389
218
  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 });
219
+ // Calculate grid dimensions
220
+ const gapWidth = visibleLen(CELL_GAP);
221
+ const minLabelWidth = 8;
222
+ const estCellWidth = ID_WIDTH + BRAILLE_BAR_MIN_WIDTH + minLabelWidth;
223
+ const columns = Math.max(1, Math.min(count, Math.floor((width + gapWidth) / (estCellWidth + gapWidth))));
224
+ const rows = Math.ceil(count / columns);
225
+ const actualCellWidth = Math.floor((width - gapWidth * (columns - 1)) / columns);
226
+ // Bar gets what's left after ID + minimum label; cap to keep label readable
227
+ const barCells = Math.max(BRAILLE_BAR_MIN_WIDTH, Math.min(BRAILLE_BAR_MAX_WIDTH, actualCellWidth - ID_WIDTH - minLabelWidth));
228
+ const leftPad = Math.floor((width - (actualCellWidth * columns + gapWidth * (columns - 1))) / 2);
229
+ const lines = [];
230
+ for (let row = 0; row < rows; row++) {
231
+ let line = " ".repeat(Math.max(0, leftPad));
232
+ for (let col = 0; col < columns; col++) {
233
+ const idx = row * columns + col;
234
+ const member = state.members[idx];
235
+ if (!member)
236
+ continue;
237
+ const cell = this.renderCell(member, actualCellWidth, barCells);
238
+ line += cell;
239
+ if (col < columns - 1)
240
+ line += CELL_GAP;
241
+ }
242
+ lines.push(truncateText(line, width));
414
243
  }
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`);
244
+ // Extra info line for truncated members
245
+ if (state.members.length > MAX_VISIBLE_MEMBERS) {
246
+ lines.push(` ... ${state.members.length - MAX_VISIBLE_MEMBERS} more`);
418
247
  }
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
248
  return lines;
423
249
  }
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();
250
+ renderCell(member, cellWidth, barCells) {
251
+ const id = String(member.index).padStart(ID_WIDTH, "0");
252
+ const bar = this.renderBrailleBar(member, barCells);
253
+ const label = this.renderCellLabel(member, Math.max(1, cellWidth - ID_WIDTH - barCells - 3));
254
+ return `${id} ${bar}${label ? " " + label : ""}`;
440
255
  }
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);
256
+ renderBrailleBar(member, width) {
257
+ if (width <= 0)
258
+ return "";
259
+ const capacity = width * BRAILLE_LEVELS.length;
260
+ const ticks = member.phase === "completed"
261
+ ? capacity
262
+ : member.phase === "working"
263
+ ? Math.min(capacity, (member.progressTick ?? 0) + (this.frameIndex % 3))
264
+ : 0;
265
+ const fullBars = Math.floor(ticks / BRAILLE_LEVELS.length);
266
+ const partial = ticks % BRAILLE_LEVELS.length;
267
+ const partialChar = partial > 0 ? BRAILLE_LEVELS[partial - 1] : "";
268
+ let bar = "";
269
+ for (let i = 0; i < width; i++) {
270
+ if (i < fullBars) {
271
+ bar += BRAILLE_LEVELS[BRAILLE_LEVELS.length - 1]; // Full █
272
+ }
273
+ else if (i === fullBars && partialChar) {
274
+ bar += partialChar;
275
+ }
276
+ else {
277
+ bar += BRAILLE_EMPTY;
278
+ }
447
279
  }
448
- this.requestRender();
280
+ return bar;
281
+ }
282
+ renderCellLabel(member, width) {
283
+ if (width <= 0)
284
+ return "";
285
+ // Show latest activity (model output or tool call) for running agents
286
+ if (member.phase === "working" || member.phase === "prompting") {
287
+ const text = member.activity ?? member.item ?? "";
288
+ return truncateText(text, width);
289
+ }
290
+ if (member.phase === "completed")
291
+ return truncateText("ok", width);
292
+ if (member.phase === "failed" && member.error)
293
+ return truncateText(member.error, Math.min(width, 20));
294
+ if (member.phase === "queued")
295
+ return truncateText(member.item ?? "...", width);
296
+ return "";
449
297
  }
450
298
  // -------------------------------------------------------------------
451
299
  // Render scheduling
452
300
  // -------------------------------------------------------------------
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
301
  requestRender() {
459
- this.invalidate();
460
302
  if (this.debounceTimer !== undefined) {
461
- this.pendingInvalidate = true;
462
303
  return;
463
304
  }
464
305
  this.debounceTimer = setTimeout(() => {
465
306
  this.debounceTimer = undefined;
466
- this.pendingInvalidate = false;
467
307
  this.onRequestRender?.();
468
- this.updatePollingInterval();
469
308
  }, DEBOUNCE_MS);
470
- // Also trigger an immediate render for faster response to inputs
471
309
  this.onRequestRender?.();
472
310
  }
473
- /**
474
- * Start the fallback polling timer. Falls back to polling when no
475
- * state changes trigger requests, ensuring the spinner animates.
476
- */
477
311
  startPolling() {
478
- this.schedulePoll();
479
- }
480
- schedulePoll() {
481
- const interval = this.hasActiveMembers ? ACTIVE_POLL_MS : IDLE_POLL_MS;
482
312
  this.pollTimer = setTimeout(() => {
483
313
  this.pollTimer = undefined;
484
- // Tick the spinner frame even without state changes
485
- if (this.state_) {
486
- this.invalidate();
314
+ if (this.state_)
487
315
  this.onRequestRender?.();
488
- }
489
- this.schedulePoll();
490
- }, interval);
316
+ this.startPolling();
317
+ }, POLL_MS);
491
318
  }
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
319
  startAnimation() {
502
320
  this.animationFrame = setInterval(() => {
503
321
  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();
322
+ const hasActive = this.state_?.active ?? 0 > 0;
323
+ if (hasActive)
508
324
  this.onRequestRender?.();
509
- }
510
325
  }, FRAME_INTERVAL_MS);
511
326
  }
512
327
  stopTimers() {
@@ -525,108 +340,13 @@ export class AgentSwarmProgressComponent {
525
340
  }
526
341
  }
527
342
  // ---------------------------------------------------------------------------
528
- // Rendering helpers
343
+ // Text utilities
529
344
  // ---------------------------------------------------------------------------
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);
345
+ function repeatStr(ch, count) {
346
+ let out = "";
347
+ for (let i = 0; i < count; i++)
348
+ out += ch;
349
+ return out;
630
350
  }
631
351
  function formatElapsed(ms) {
632
352
  const totalSec = Math.floor(ms / 1000);
@@ -634,24 +354,21 @@ function formatElapsed(ms) {
634
354
  return `${totalSec}s`;
635
355
  const m = Math.floor(totalSec / 60);
636
356
  const s = totalSec % 60;
637
- return `${m}m${s}s`;
357
+ if (m < 60)
358
+ return `${m}m ${s}s`;
359
+ const h = Math.floor(m / 60);
360
+ return `${h}h ${m % 60}m ${s}s`;
638
361
  }
639
- // ---------------------------------------------------------------------------
640
- // Text utilities
641
- // ---------------------------------------------------------------------------
642
362
  function visibleLen(text) {
643
363
  return text.replace(/\x1b\[[0-9;]*m/g, "").length;
644
364
  }
645
365
  function truncateText(text, maxLen) {
366
+ if (maxLen <= 0)
367
+ return "";
646
368
  if (text.length <= maxLen)
647
369
  return text;
648
370
  if (maxLen <= 1)
649
371
  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);
372
+ return text.slice(0, maxLen - 1) + "\u2026";
656
373
  }
657
374
  //# sourceMappingURL=progress.js.map