@gjczone/pi-swarm 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,9 +4,18 @@
4
4
  * Renders a real-time dashboard above the input area when a SwarmTeam
5
5
  * run is in progress. Shows phase statuses with compact braille spinner
6
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
7
16
  */
8
17
  import type { Component } from "@earendil-works/pi-tui";
9
- import type { TeamProgressSnapshot, SubagentUsage } from "../shared/types.js";
18
+ import type { TeamProgressSnapshot, SubagentUsage, PhaseDependencyEdge } from "../shared/types.js";
10
19
  export interface TeamDashboardState {
11
20
  title: string;
12
21
  goal: string;
@@ -20,6 +29,8 @@ export interface TeamDashboardState {
20
29
  mailboxCount: number;
21
30
  totalUsage: SubagentUsage;
22
31
  startedAt: number;
32
+ dependencyEdges?: ReadonlyArray<PhaseDependencyEdge>;
33
+ mailboxPath?: string;
23
34
  }
24
35
  interface TeamPhaseStatusWithMeta {
25
36
  name: string;
@@ -28,21 +39,43 @@ interface TeamPhaseStatusWithMeta {
28
39
  error?: string;
29
40
  phaseStartedAt: number;
30
41
  }
31
- export declare function snapshotToDashboardState(snapshot: TeamProgressSnapshot): TeamDashboardState;
42
+ export declare function snapshotToDashboardState(snapshot: TeamProgressSnapshot, mailboxPath?: string): TeamDashboardState;
32
43
  export declare class TeamDashboardComponent implements Component {
33
44
  private state_;
34
- private animationFrame;
35
45
  private renderedWidth;
36
46
  private cachedLines;
37
47
  private onRequestRender;
48
+ private animationFrame;
38
49
  private frameIndex;
50
+ private pollTimer;
51
+ private debounceTimer;
52
+ private pendingInvalidate;
53
+ private hasActivePhases;
54
+ private scrollOffset;
55
+ private selectedIndex;
56
+ private activePanel;
57
+ private overlay;
39
58
  constructor(onRequestRender?: () => void);
40
59
  update(state: TeamDashboardState): void;
41
60
  complete(): void;
42
61
  dispose(): void;
43
62
  invalidate(): void;
63
+ handleInput(data: string): void;
44
64
  render(width: number): string[];
65
+ private renderPhasesPanel;
66
+ private renderDepsPanel;
67
+ private renderMailboxPanel;
68
+ private renderOverlay;
69
+ private renderHelpOverlay;
70
+ private renderPhaseDetailOverlay;
71
+ private scrollDown;
72
+ private scrollUp;
73
+ private requestRender;
74
+ private startPolling;
75
+ private schedulePoll;
76
+ private updatePollingInterval;
45
77
  private startAnimation;
78
+ private stopTimers;
46
79
  }
47
80
  export {};
48
81
  //# sourceMappingURL=team-dashboard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"team-dashboard.d.ts","sourceRoot":"","sources":["../../src/tui/team-dashboard.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EACV,oBAAoB,EAEpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAyB5B,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,uBAAuB,EAAE,CAAC;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,aAAa,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,uBAAuB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;CACxB;AAMD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,oBAAoB,GAC7B,kBAAkB,CAoCpB;AAMD,qBAAa,sBAAuB,YAAW,SAAS;IACtD,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,UAAU,CAAK;gBAEX,eAAe,CAAC,EAAE,MAAM,IAAI;IAKxC,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI;IAKvC,QAAQ,IAAI,IAAI;IAgBhB,OAAO,IAAI,IAAI;IAQf,UAAU,IAAI,IAAI;IAKlB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAsD/B,OAAO,CAAC,cAAc;CAOvB"}
1
+ {"version":3,"file":"team-dashboard.d.ts","sourceRoot":"","sources":["../../src/tui/team-dashboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD,OAAO,KAAK,EACV,oBAAoB,EAEpB,aAAa,EACb,mBAAmB,EACpB,MAAM,oBAAoB,CAAC;AAqD5B,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,uBAAuB,EAAE,CAAC;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,aAAa,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,aAAa,CAAC,mBAAmB,CAAC,CAAC;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,uBAAuB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;CACxB;AAMD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,oBAAoB,EAC9B,WAAW,CAAC,EAAE,MAAM,GACnB,kBAAkB,CAsCpB;AAMD,qBAAa,sBAAuB,YAAW,SAAS;IACtD,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,eAAe,CAA2B;IAGlD,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,aAAa,CAA4C;IACjE,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,eAAe,CAAS;IAGhC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,aAAa,CAAM;IAC3B,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,OAAO,CAA6B;gBAEhC,eAAe,CAAC,EAAE,MAAM,IAAI;IAUxC,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI;IAevC,QAAQ,IAAI,IAAI;IAiBhB,OAAO,IAAI,IAAI;IAKf,UAAU,IAAI,IAAI;IASlB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAuH/B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAsE/B,OAAO,CAAC,iBAAiB;IAoCzB,OAAO,CAAC,eAAe;IA2CvB,OAAO,CAAC,kBAAkB;IA8D1B,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,iBAAiB;IAoCzB,OAAO,CAAC,wBAAwB;IA0ChC,OAAO,CAAC,UAAU;IAyBlB,OAAO,CAAC,QAAQ;IAgBhB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,UAAU;CAcnB"}
@@ -4,13 +4,32 @@
4
4
  * Renders a real-time dashboard above the input area when a SwarmTeam
5
5
  * run is in progress. Shows phase statuses with compact braille spinner
6
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
7
16
  */
17
+ import { matchesKey, Key, isKeyRepeat } from "@earendil-works/pi-tui";
18
+ import { readInbox, resolveMailboxPaths, } from "../team/mailbox.js";
8
19
  // ---------------------------------------------------------------------------
9
20
  // Constants
10
21
  // ---------------------------------------------------------------------------
11
22
  const FRAME_INTERVAL_MS = 80;
12
23
  const MAX_PHASES = 20;
24
+ const MAX_VISIBLE_PHASES = 8;
25
+ const VISIBLE_MAILBOX_MSGS = 8;
13
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;
14
33
  const BRAILLE_SPINNER = [
15
34
  "\u28BF",
16
35
  "\u28FB",
@@ -24,7 +43,7 @@ const BRAILLE_SPINNER = [
24
43
  // ---------------------------------------------------------------------------
25
44
  // Snapshot conversion
26
45
  // ---------------------------------------------------------------------------
27
- export function snapshotToDashboardState(snapshot) {
46
+ export function snapshotToDashboardState(snapshot, mailboxPath) {
28
47
  const now = Date.now();
29
48
  const runningPhases = snapshot.phases
30
49
  .filter((p) => p.status === "running")
@@ -57,6 +76,8 @@ export function snapshotToDashboardState(snapshot) {
57
76
  mailboxCount: snapshot.mailboxCount,
58
77
  totalUsage: snapshot.totalUsage,
59
78
  startedAt: snapshot.startedAt,
79
+ dependencyEdges: snapshot.dependencyEdges,
80
+ mailboxPath,
60
81
  };
61
82
  }
62
83
  // ---------------------------------------------------------------------------
@@ -64,18 +85,38 @@ export function snapshotToDashboardState(snapshot) {
64
85
  // ---------------------------------------------------------------------------
65
86
  export class TeamDashboardComponent {
66
87
  state_ = null;
67
- animationFrame;
68
88
  renderedWidth;
69
89
  cachedLines;
70
90
  onRequestRender;
91
+ // Render scheduler
92
+ animationFrame;
71
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;
72
103
  constructor(onRequestRender) {
73
104
  this.onRequestRender = onRequestRender;
105
+ this.startPolling();
74
106
  this.startAnimation();
75
107
  }
108
+ // -------------------------------------------------------------------
109
+ // Public API
110
+ // -------------------------------------------------------------------
76
111
  update(state) {
77
112
  this.state_ = state;
78
- this.invalidate();
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();
79
120
  }
80
121
  complete() {
81
122
  if (this.state_) {
@@ -90,19 +131,126 @@ export class TeamDashboardComponent {
90
131
  this.state_.currentPhase = undefined;
91
132
  this.state_.currentRoles = undefined;
92
133
  }
93
- this.invalidate();
134
+ this.hasActivePhases = false;
135
+ this.requestRender();
94
136
  }
95
137
  dispose() {
96
- if (this.animationFrame !== undefined) {
97
- clearInterval(this.animationFrame);
98
- this.animationFrame = undefined;
99
- }
138
+ this.stopTimers();
100
139
  this.onRequestRender = undefined;
101
140
  }
102
141
  invalidate() {
103
142
  this.renderedWidth = undefined;
104
143
  this.cachedLines = undefined;
105
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
+ // -------------------------------------------------------------------
106
254
  render(width) {
107
255
  const safeWidth = Math.max(20, width);
108
256
  if (this.cachedLines && this.renderedWidth === safeWidth) {
@@ -112,12 +260,23 @@ export class TeamDashboardComponent {
112
260
  this.cachedLines = [];
113
261
  return this.cachedLines;
114
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
+ }
115
269
  const state = this.state_;
116
270
  const lines = [];
117
271
  const contentWidth = safeWidth - 4;
118
- // Header: just title, truncated to fit
119
- const header = truncateText(state.title, contentWidth);
120
- lines.push(` ${header}`);
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}`);
121
280
  // Overall progress bar
122
281
  const barWidth = Math.max(1, contentWidth);
123
282
  const total = state.totalPhases || 1;
@@ -126,14 +285,14 @@ export class TeamDashboardComponent {
126
285
  const done = "\u2501".repeat(doneChars);
127
286
  const remaining = "\u2501".repeat(barWidth - doneChars);
128
287
  lines.push(` ${done}${remaining}`);
129
- // Phase rows
130
- const maxPhases = Math.min(state.phases.length, MAX_PHASES);
131
- for (let i = 0; i < maxPhases; i += 1) {
132
- const phase = state.phases[i];
133
- if (!phase)
134
- continue;
135
- const row = renderPhaseRow(phase, contentWidth, this.frameIndex);
136
- lines.push(` ${row}`);
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);
137
296
  }
138
297
  // Separator
139
298
  const sep = "\u2500".repeat(contentWidth);
@@ -148,23 +307,267 @@ export class TeamDashboardComponent {
148
307
  this.renderedWidth = safeWidth;
149
308
  return this.cachedLines;
150
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
+ }
151
537
  startAnimation() {
152
538
  this.animationFrame = setInterval(() => {
153
539
  this.frameIndex = (this.frameIndex + 1) % BRAILLE_SPINNER.length;
154
- this.invalidate();
155
- this.onRequestRender?.();
540
+ if (this.hasActivePhases || this.overlay) {
541
+ this.invalidate();
542
+ this.onRequestRender?.();
543
+ }
156
544
  }, FRAME_INTERVAL_MS);
157
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
+ }
158
560
  }
159
561
  // ---------------------------------------------------------------------------
160
562
  // Rendering helpers
161
563
  // ---------------------------------------------------------------------------
162
- function renderPhaseRow(phase, width, frameIndex) {
564
+ function renderPhaseRow(phase, width, frameIndex, selected) {
565
+ const prefix = selected ? ">" : " ";
163
566
  const icon = phaseStatusIcon(phase, frameIndex).padEnd(ICON_COL_WIDTH, " ");
164
567
  const displayName = shortenPhaseName(phase.name, phase.role);
165
568
  const nameWidth = Math.max(6, Math.min(16, Math.floor(width * 0.35)));
166
569
  const name = truncateText(displayName, nameWidth).padEnd(nameWidth, " ");
167
- const fixed = `${icon} ${name}`;
570
+ const fixed = `${prefix}${icon} ${name}`;
168
571
  const remaining = Math.max(0, width - visibleLen(fixed) - 1);
169
572
  if (phase.status === "running") {
170
573
  const roleLabel = `${phase.role}`;
@@ -246,4 +649,9 @@ function truncateText(text, maxLen) {
246
649
  return text.slice(0, 1);
247
650
  return text.slice(0, maxLen - 1) + "\u2026";
248
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
+ }
249
657
  //# sourceMappingURL=team-dashboard.js.map