@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,657 +0,0 @@
1
- /**
2
- * tui/team-dashboard — SwarmTeam live phase progress dashboard.
3
- *
4
- * Renders a real-time dashboard above the input area when a SwarmTeam
5
- * run is in progress. Shows phase statuses with compact braille spinner
6
- * for active phases, mailbox message count, token usage, and elapsed time.
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: phases list / dependency viz / mailbox messages
12
- * - Phase detail overlay
13
- * - Mailbox message viewing with ack support
14
- * - Dependency chain visualization
15
- * - Phase-level ETA
16
- */
17
- import { matchesKey, Key, isKeyRepeat } from "@earendil-works/pi-tui";
18
- import { readInbox, resolveMailboxPaths, } from "../team/mailbox.js";
19
- // ---------------------------------------------------------------------------
20
- // Constants
21
- // ---------------------------------------------------------------------------
22
- const FRAME_INTERVAL_MS = 80;
23
- const MAX_PHASES = 20;
24
- const MAX_VISIBLE_PHASES = 8;
25
- const VISIBLE_MAILBOX_MSGS = 8;
26
- const ICON_COL_WIDTH = 3;
27
- /** Debounce window for coalescing render requests. */
28
- const DEBOUNCE_MS = 75;
29
- /** Fallback polling interval when state is active (has running phases). */
30
- const ACTIVE_POLL_MS = 800;
31
- /** Idle polling interval. */
32
- const IDLE_POLL_MS = 2000;
33
- const BRAILLE_SPINNER = [
34
- "\u28BF",
35
- "\u28FB",
36
- "\u28FD",
37
- "\u28FE",
38
- "\u28F7",
39
- "\u28EF",
40
- "\u28DF",
41
- "\u287F",
42
- ];
43
- // ---------------------------------------------------------------------------
44
- // Snapshot conversion
45
- // ---------------------------------------------------------------------------
46
- export function snapshotToDashboardState(snapshot, mailboxPath) {
47
- const now = Date.now();
48
- const runningPhases = snapshot.phases
49
- .filter((p) => p.status === "running")
50
- .map((p) => p.name);
51
- const runningRoles = snapshot.phases
52
- .filter((p) => p.status === "running")
53
- .map((p) => p.role);
54
- return {
55
- title: snapshot.title,
56
- goal: snapshot.goal,
57
- status: snapshot.status,
58
- totalPhases: snapshot.totalPhases,
59
- completedPhases: snapshot.completedPhases,
60
- failedPhases: snapshot.failedPhases,
61
- currentPhase: runningPhases.length > 0
62
- ? runningPhases.join(", ")
63
- : snapshot.currentPhase,
64
- currentRoles: runningRoles.length > 0
65
- ? runningRoles
66
- : snapshot.currentRole
67
- ? [snapshot.currentRole]
68
- : undefined,
69
- phases: snapshot.phases.map((p) => ({
70
- name: p.name,
71
- role: p.role,
72
- status: p.status,
73
- error: p.error,
74
- phaseStartedAt: p.status === "running" ? now : 0,
75
- })),
76
- mailboxCount: snapshot.mailboxCount,
77
- totalUsage: snapshot.totalUsage,
78
- startedAt: snapshot.startedAt,
79
- dependencyEdges: snapshot.dependencyEdges,
80
- mailboxPath,
81
- };
82
- }
83
- // ---------------------------------------------------------------------------
84
- // Component
85
- // ---------------------------------------------------------------------------
86
- export class TeamDashboardComponent {
87
- state_ = null;
88
- renderedWidth;
89
- cachedLines;
90
- onRequestRender;
91
- // Render scheduler
92
- animationFrame;
93
- frameIndex = 0;
94
- pollTimer;
95
- debounceTimer;
96
- pendingInvalidate = false;
97
- hasActivePhases = false;
98
- // Input / UI state
99
- scrollOffset = 0;
100
- selectedIndex = -1;
101
- activePanel = "phases";
102
- overlay = null;
103
- constructor(onRequestRender) {
104
- this.onRequestRender = onRequestRender;
105
- this.startPolling();
106
- this.startAnimation();
107
- }
108
- // -------------------------------------------------------------------
109
- // Public API
110
- // -------------------------------------------------------------------
111
- update(state) {
112
- this.state_ = state;
113
- this.hasActivePhases =
114
- state.phases.some((p) => p.status === "running") &&
115
- state.status === "running";
116
- if (this.scrollOffset > Math.max(0, state.phases.length - MAX_VISIBLE_PHASES)) {
117
- this.scrollOffset = Math.max(0, state.phases.length - MAX_VISIBLE_PHASES);
118
- }
119
- this.requestRender();
120
- }
121
- complete() {
122
- if (this.state_) {
123
- this.state_.phases.forEach((p) => {
124
- if (p.status === "queued" || p.status === "running") {
125
- p.status = "completed";
126
- }
127
- });
128
- this.state_.status = "completed";
129
- this.state_.completedPhases =
130
- this.state_.totalPhases - this.state_.failedPhases;
131
- this.state_.currentPhase = undefined;
132
- this.state_.currentRoles = undefined;
133
- }
134
- this.hasActivePhases = false;
135
- this.requestRender();
136
- }
137
- dispose() {
138
- this.stopTimers();
139
- this.onRequestRender = undefined;
140
- }
141
- invalidate() {
142
- this.renderedWidth = undefined;
143
- this.cachedLines = undefined;
144
- }
145
- // -------------------------------------------------------------------
146
- // Keyboard input
147
- // -------------------------------------------------------------------
148
- handleInput(data) {
149
- const isRepeat = isKeyRepeat(data);
150
- // Close overlay on Escape / q
151
- if (this.overlay) {
152
- if (matchesKey(data, Key.escape) || matchesKey(data, "q")) {
153
- this.overlay = null;
154
- this.requestRender();
155
- return;
156
- }
157
- if (this.overlay.kind === "mailbox_detail") {
158
- // Acknowledge message
159
- if (matchesKey(data, "a")) {
160
- if (this.state_?.mailboxPath) {
161
- try {
162
- const paths = resolveMailboxPaths(this.state_.mailboxPath.replace(/\/mailbox$/, ""), "");
163
- // Actually we need the runId from the path... this is complex.
164
- // For now just close the overlay
165
- }
166
- catch {
167
- // Best effort
168
- }
169
- }
170
- }
171
- }
172
- return;
173
- }
174
- // Navigation
175
- if (matchesKey(data, "j") || matchesKey(data, Key.down)) {
176
- if (!isRepeat) {
177
- this.scrollDown(1);
178
- }
179
- return;
180
- }
181
- if (matchesKey(data, "k") || matchesKey(data, Key.up)) {
182
- if (!isRepeat) {
183
- this.scrollUp(1);
184
- }
185
- return;
186
- }
187
- if (matchesKey(data, Key.pageDown)) {
188
- this.scrollDown(MAX_VISIBLE_PHASES);
189
- return;
190
- }
191
- if (matchesKey(data, Key.pageUp)) {
192
- this.scrollUp(MAX_VISIBLE_PHASES);
193
- return;
194
- }
195
- if (matchesKey(data, "g") || matchesKey(data, Key.home)) {
196
- this.scrollOffset = 0;
197
- this.selectedIndex = -1;
198
- this.requestRender();
199
- return;
200
- }
201
- if (matchesKey(data, Key.shift("g")) || matchesKey(data, Key.end)) {
202
- if (this.state_) {
203
- const maxScroll = Math.max(0, this.state_.phases.length - MAX_VISIBLE_PHASES);
204
- this.scrollOffset = maxScroll;
205
- this.selectedIndex = Math.max(0, this.state_.phases.length - 1);
206
- }
207
- this.requestRender();
208
- return;
209
- }
210
- // Panel switching
211
- if (matchesKey(data, Key.tab) || matchesKey(data, "1")) {
212
- this.activePanel = "phases";
213
- this.scrollOffset = 0;
214
- this.selectedIndex = -1;
215
- this.requestRender();
216
- return;
217
- }
218
- if (matchesKey(data, "2")) {
219
- this.activePanel = "deps";
220
- this.scrollOffset = 0;
221
- this.requestRender();
222
- return;
223
- }
224
- if (matchesKey(data, "3") && this.state_ && this.state_.mailboxCount > 0) {
225
- this.activePanel = "mailbox";
226
- this.scrollOffset = 0;
227
- this.requestRender();
228
- return;
229
- }
230
- // Detail overlay
231
- if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
232
- if (this.activePanel === "phases" && this.state_) {
233
- const idx = this.selectedIndex >= 0 ? this.selectedIndex : this.scrollOffset;
234
- if (idx >= 0 && idx < this.state_.phases.length) {
235
- this.overlay = {
236
- kind: "detail",
237
- phaseName: this.state_.phases[idx].name,
238
- };
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
- // -------------------------------------------------------------------
254
- render(width) {
255
- const safeWidth = Math.max(20, width);
256
- if (this.cachedLines && this.renderedWidth === safeWidth) {
257
- return this.cachedLines;
258
- }
259
- if (!this.state_ || this.state_.phases.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
- }
269
- const state = this.state_;
270
- const lines = [];
271
- const contentWidth = safeWidth - 4;
272
- // Header with panel indicator
273
- const header = truncateText(state.title, contentWidth - 20);
274
- const panelLabel = this.activePanel === "phases"
275
- ? "[1]phases"
276
- : this.activePanel === "deps"
277
- ? "[2]deps"
278
- : "[3]mailbox";
279
- lines.push(` ${header} ${panelLabel}`);
280
- // Overall progress bar
281
- const barWidth = Math.max(1, contentWidth);
282
- const total = state.totalPhases || 1;
283
- const doneRatio = (state.completedPhases + state.failedPhases) / total;
284
- const doneChars = Math.round(doneRatio * barWidth);
285
- const done = "\u2501".repeat(doneChars);
286
- const remaining = "\u2501".repeat(barWidth - doneChars);
287
- lines.push(` ${done}${remaining}`);
288
- if (this.activePanel === "phases") {
289
- this.renderPhasesPanel(lines, state, contentWidth);
290
- }
291
- else if (this.activePanel === "deps") {
292
- this.renderDepsPanel(lines, state, contentWidth);
293
- }
294
- else {
295
- this.renderMailboxPanel(lines, state, contentWidth);
296
- }
297
- // Separator
298
- const sep = "\u2500".repeat(contentWidth);
299
- lines.push(` ${sep}`);
300
- // Footer with all info
301
- const footerLine = buildFooter(state, contentWidth);
302
- lines.push(` ${footerLine}`);
303
- // Bottom border
304
- const bottom = `\u2514${"\u2500".repeat(contentWidth)}\u2518`;
305
- lines.push(bottom);
306
- this.cachedLines = lines;
307
- this.renderedWidth = safeWidth;
308
- return this.cachedLines;
309
- }
310
- // -------------------------------------------------------------------
311
- // Panel rendering
312
- // -------------------------------------------------------------------
313
- renderPhasesPanel(lines, state, contentWidth) {
314
- const maxPhases = Math.min(state.phases.length, this.scrollOffset + MAX_VISIBLE_PHASES);
315
- const hasMore = state.phases.length > MAX_VISIBLE_PHASES;
316
- if (hasMore && this.scrollOffset > 0) {
317
- lines.push(` \u2191 ${this.scrollOffset} more above`);
318
- }
319
- for (let i = this.scrollOffset; i < maxPhases; i += 1) {
320
- const phase = state.phases[i];
321
- if (!phase)
322
- continue;
323
- const isSelected = i === this.selectedIndex ||
324
- (this.selectedIndex < 0 && i === this.scrollOffset);
325
- const row = renderPhaseRow(phase, contentWidth, this.frameIndex, isSelected);
326
- lines.push(` ${row}`);
327
- }
328
- if (hasMore && maxPhases < state.phases.length) {
329
- const remaining = state.phases.length - maxPhases;
330
- lines.push(` \u2193 ${remaining} more below`);
331
- }
332
- }
333
- renderDepsPanel(lines, state, contentWidth) {
334
- const edges = state.dependencyEdges ?? [];
335
- if (edges.length === 0) {
336
- // Build dependency edges from phase names (default sequence)
337
- const names = state.phases.map((p) => p.name);
338
- if (names.length <= 1) {
339
- lines.push(` ${padRight("(no dependencies)", contentWidth)}`);
340
- return;
341
- }
342
- // Show as sequential flow
343
- let depLine = "";
344
- for (let i = 0; i < names.length; i += 1) {
345
- if (i > 0)
346
- depLine += " \u2192 ";
347
- depLine += names[i];
348
- if (depLine.length > contentWidth - 10) {
349
- lines.push(` ${depLine}`);
350
- depLine = ` ${" ".repeat(2)}`;
351
- }
352
- }
353
- if (depLine) {
354
- lines.push(` ${depLine}`);
355
- }
356
- }
357
- else {
358
- // Show explicit dependency edges
359
- for (const edge of edges) {
360
- const depLine = `${edge.from} \u2192 ${edge.to}`;
361
- lines.push(` ${truncateText(depLine, contentWidth)}`);
362
- }
363
- }
364
- // Show current phase context
365
- if (state.currentPhase) {
366
- lines.push("");
367
- lines.push(` \u25B6 Current: ${truncateText(state.currentPhase, contentWidth - 12)}`);
368
- }
369
- }
370
- renderMailboxPanel(lines, state, contentWidth) {
371
- if (state.mailboxCount === 0) {
372
- lines.push(` ${padRight("(no messages)", contentWidth)}`);
373
- return;
374
- }
375
- // Try to read actual messages
376
- let messages = [];
377
- if (state.mailboxPath) {
378
- try {
379
- const paths = resolveMailboxPaths(state.mailboxPath.replace(/\/mailbox$/, ""), "");
380
- const raw = readInbox(paths);
381
- messages = raw.slice(-VISIBLE_MAILBOX_MSGS).map((m) => ({
382
- from: m.from,
383
- type: m.type,
384
- preview: typeof m.payload.content === "string"
385
- ? m.payload.content.slice(0, 40)
386
- : JSON.stringify(m.payload).slice(0, 40),
387
- }));
388
- }
389
- catch {
390
- // If we can't read messages, just show count
391
- }
392
- }
393
- if (messages.length === 0) {
394
- lines.push(` ${padRight(`${state.mailboxCount} message(s) in outbox`, contentWidth)}`);
395
- return;
396
- }
397
- for (const msg of messages) {
398
- const prefix = msg.type === "task_assignment"
399
- ? "\u2190"
400
- : msg.type === "task_result"
401
- ? "\u2713"
402
- : msg.type === "handoff"
403
- ? "\u2194"
404
- : "\u25CB";
405
- const line = `${prefix} ${msg.from}: ${truncateText(msg.preview, contentWidth - 8)}`;
406
- lines.push(` ${line}`);
407
- }
408
- }
409
- // -------------------------------------------------------------------
410
- // Overlays
411
- // -------------------------------------------------------------------
412
- renderOverlay(width) {
413
- if (!this.overlay)
414
- return [];
415
- if (this.overlay.kind === "help") {
416
- return this.renderHelpOverlay(width);
417
- }
418
- if (this.overlay.kind === "detail" && this.overlay.phaseName) {
419
- return this.renderPhaseDetailOverlay(width, this.overlay.phaseName);
420
- }
421
- return [];
422
- }
423
- renderHelpOverlay(width) {
424
- const safeWidth = Math.max(30, width);
425
- const contentWidth = safeWidth - 6;
426
- const lines = [];
427
- lines.push(` \u250C${"\u2500".repeat(safeWidth - 2)}\u2510`);
428
- lines.push(` \u2502 ${padRight("Help" + " ".repeat(contentWidth - 4), safeWidth - 4)}\u2502`);
429
- lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
430
- const helpItems = [
431
- ["j/k, up/down", "Scroll phase list"],
432
- ["g / Shift+G", "Go to top / bottom"],
433
- ["Enter", "View phase detail"],
434
- ["1 or Tab", "Phases panel"],
435
- ["2", "Dependencies panel"],
436
- ["3", "Mailbox messages"],
437
- ["?", "Toggle this help"],
438
- ["q / Esc", "Close overlay"],
439
- ];
440
- for (const [key, desc] of helpItems) {
441
- const line = `${padRight(key, 18)} ${desc}`;
442
- lines.push(` \u2502 ${padRight(line, safeWidth - 4)}\u2502`);
443
- }
444
- lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
445
- lines.push(` \u2502 ${padRight("Press q or Esc to close", safeWidth - 4)}\u2502`);
446
- lines.push(` \u2514${"\u2500".repeat(safeWidth - 2)}\u2518`);
447
- return lines;
448
- }
449
- renderPhaseDetailOverlay(width, phaseName) {
450
- if (!this.state_)
451
- return [];
452
- const phase = this.state_.phases.find((p) => p.name === phaseName);
453
- if (!phase)
454
- return [];
455
- const safeWidth = Math.max(30, width);
456
- const contentWidth = safeWidth - 6;
457
- const lines = [];
458
- const bottom = `\u2514${"\u2500".repeat(safeWidth - 2)}\u2518`;
459
- lines.push(` \u250C${"\u2500".repeat(safeWidth - 2)}\u2510`);
460
- lines.push(` \u2502 ${padRight(`Phase: ${phase.name} (${phase.role})`, safeWidth - 4)}\u2502`);
461
- lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
462
- const fields = [
463
- { label: "Status", value: phase.status },
464
- ];
465
- if (phase.error) {
466
- fields.push({ label: "Error", value: phase.error });
467
- }
468
- for (const field of fields) {
469
- const line = `${padRight(field.label + ":", 10)} ${truncateText(field.value, contentWidth - 12)}`;
470
- lines.push(` \u2502 ${padRight(line, safeWidth - 4)}\u2502`);
471
- }
472
- lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
473
- lines.push(` \u2502 ${padRight("Press q or Esc to close", safeWidth - 4)}\u2502`);
474
- lines.push(bottom);
475
- return lines;
476
- }
477
- // -------------------------------------------------------------------
478
- // Scroll helpers
479
- // -------------------------------------------------------------------
480
- scrollDown(n) {
481
- if (!this.state_)
482
- return;
483
- const phaseCount = this.state_.phases.length;
484
- const maxOffset = Math.max(0, phaseCount - 1);
485
- this.selectedIndex = Math.min(maxOffset, Math.max(0, this.selectedIndex < 0 ? this.scrollOffset : this.selectedIndex) + n);
486
- if (this.selectedIndex - this.scrollOffset >= MAX_VISIBLE_PHASES ||
487
- this.selectedIndex < this.scrollOffset) {
488
- this.scrollOffset = Math.max(0, this.selectedIndex - MAX_VISIBLE_PHASES + 1);
489
- const maxScroll = Math.max(0, phaseCount - MAX_VISIBLE_PHASES);
490
- this.scrollOffset = Math.min(this.scrollOffset, maxScroll);
491
- }
492
- this.requestRender();
493
- }
494
- scrollUp(n) {
495
- if (!this.state_)
496
- return;
497
- this.selectedIndex = Math.max(0, (this.selectedIndex < 0 ? this.scrollOffset : this.selectedIndex) - n);
498
- if (this.selectedIndex < this.scrollOffset) {
499
- this.scrollOffset = Math.max(0, this.selectedIndex);
500
- }
501
- this.requestRender();
502
- }
503
- // -------------------------------------------------------------------
504
- // Render scheduling
505
- // -------------------------------------------------------------------
506
- requestRender() {
507
- this.invalidate();
508
- if (this.debounceTimer !== undefined) {
509
- this.pendingInvalidate = true;
510
- return;
511
- }
512
- this.debounceTimer = setTimeout(() => {
513
- this.debounceTimer = undefined;
514
- this.pendingInvalidate = false;
515
- this.onRequestRender?.();
516
- this.updatePollingInterval();
517
- }, DEBOUNCE_MS);
518
- this.onRequestRender?.();
519
- }
520
- startPolling() {
521
- this.schedulePoll();
522
- }
523
- schedulePoll() {
524
- const interval = this.hasActivePhases ? ACTIVE_POLL_MS : IDLE_POLL_MS;
525
- this.pollTimer = setTimeout(() => {
526
- this.pollTimer = undefined;
527
- if (this.state_) {
528
- this.invalidate();
529
- this.onRequestRender?.();
530
- }
531
- this.schedulePoll();
532
- }, interval);
533
- }
534
- updatePollingInterval() {
535
- // Will be picked up on next poll cycle
536
- }
537
- startAnimation() {
538
- this.animationFrame = setInterval(() => {
539
- this.frameIndex = (this.frameIndex + 1) % BRAILLE_SPINNER.length;
540
- if (this.hasActivePhases || this.overlay) {
541
- this.invalidate();
542
- this.onRequestRender?.();
543
- }
544
- }, FRAME_INTERVAL_MS);
545
- }
546
- stopTimers() {
547
- if (this.animationFrame !== undefined) {
548
- clearInterval(this.animationFrame);
549
- this.animationFrame = undefined;
550
- }
551
- if (this.debounceTimer !== undefined) {
552
- clearTimeout(this.debounceTimer);
553
- this.debounceTimer = undefined;
554
- }
555
- if (this.pollTimer !== undefined) {
556
- clearTimeout(this.pollTimer);
557
- this.pollTimer = undefined;
558
- }
559
- }
560
- }
561
- // ---------------------------------------------------------------------------
562
- // Rendering helpers
563
- // ---------------------------------------------------------------------------
564
- function renderPhaseRow(phase, width, frameIndex, selected) {
565
- const prefix = selected ? ">" : " ";
566
- const icon = phaseStatusIcon(phase, frameIndex).padEnd(ICON_COL_WIDTH, " ");
567
- const displayName = shortenPhaseName(phase.name, phase.role);
568
- const nameWidth = Math.max(6, Math.min(16, Math.floor(width * 0.35)));
569
- const name = truncateText(displayName, nameWidth).padEnd(nameWidth, " ");
570
- const fixed = `${prefix}${icon} ${name}`;
571
- const remaining = Math.max(0, width - visibleLen(fixed) - 1);
572
- if (phase.status === "running") {
573
- const roleLabel = `${phase.role}`;
574
- return `${fixed} ${truncateText(roleLabel, remaining)}`;
575
- }
576
- if (phase.status === "failed" && phase.error) {
577
- const errorPart = phase.error;
578
- return `${fixed} ${truncateText(errorPart, remaining)}`;
579
- }
580
- if (phase.status === "completed") {
581
- return `${fixed} ok`;
582
- }
583
- if (phase.status === "skipped") {
584
- return `${fixed} skip`;
585
- }
586
- return `${fixed} ...`;
587
- }
588
- function shortenPhaseName(name, role) {
589
- if (name.length <= 12)
590
- return name;
591
- if (name.startsWith(role))
592
- return name.slice(0, Math.max(role.length, 12));
593
- const parts = name.split("-");
594
- if (parts.length > 1) {
595
- return parts.slice(0, 2).join("-");
596
- }
597
- return name.slice(0, 12);
598
- }
599
- function phaseStatusIcon(phase, frameIndex) {
600
- switch (phase.status) {
601
- case "completed":
602
- return "\u2713";
603
- case "running":
604
- return BRAILLE_SPINNER[frameIndex % BRAILLE_SPINNER.length];
605
- case "failed":
606
- return "\u2717";
607
- case "skipped":
608
- return "\u2298";
609
- case "queued":
610
- return "\u25CB";
611
- }
612
- }
613
- function buildFooter(state, width) {
614
- const usage = state.totalUsage ?? {
615
- input: 0,
616
- output: 0,
617
- cacheRead: 0,
618
- cacheWrite: 0,
619
- totalTokens: 0,
620
- };
621
- const phaseCount = `${state.completedPhases + state.failedPhases}/${state.totalPhases}`;
622
- const tokens = `${Math.round(usage.input)}in/${Math.round(usage.output)}out`;
623
- const mailbox = state.mailboxCount > 0 ? ` ${state.mailboxCount}msg` : "";
624
- const elapsed = formatElapsed(Date.now() - state.startedAt);
625
- const parts = [`${phaseCount} ph`, tokens, mailbox.trim(), elapsed].filter(Boolean);
626
- const full = parts.join(" | ");
627
- if (full.length <= width)
628
- return full;
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`;
638
- }
639
- // ---------------------------------------------------------------------------
640
- // Text utilities
641
- // ---------------------------------------------------------------------------
642
- function visibleLen(text) {
643
- return text.replace(/\x1b\[[0-9;]*m/g, "").length;
644
- }
645
- function truncateText(text, maxLen) {
646
- if (text.length <= maxLen)
647
- return text;
648
- if (maxLen <= 1)
649
- return text.slice(0, 1);
650
- return text.slice(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);
656
- }
657
- //# sourceMappingURL=team-dashboard.js.map