@gjczone/pi-swarm 0.3.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -71
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/shared/controller.d.ts +10 -4
- package/dist/shared/controller.d.ts.map +1 -1
- package/dist/shared/controller.js +139 -6
- package/dist/shared/controller.js.map +1 -1
- package/dist/shared/render.d.ts +0 -11
- package/dist/shared/render.d.ts.map +1 -1
- package/dist/shared/render.js +3 -36
- package/dist/shared/render.js.map +1 -1
- package/dist/shared/spawner.d.ts.map +1 -1
- package/dist/shared/spawner.js +212 -17
- package/dist/shared/spawner.js.map +1 -1
- package/dist/shared/types.d.ts +58 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/shared/worktree.d.ts +81 -0
- package/dist/shared/worktree.d.ts.map +1 -0
- package/dist/shared/worktree.js +417 -0
- package/dist/shared/worktree.js.map +1 -0
- package/dist/shared/xml.d.ts +18 -0
- package/dist/shared/xml.d.ts.map +1 -0
- package/dist/shared/xml.js +31 -0
- package/dist/shared/xml.js.map +1 -0
- package/dist/swarm/tool.d.ts.map +1 -1
- package/dist/swarm/tool.js +69 -15
- package/dist/swarm/tool.js.map +1 -1
- package/dist/team/mailbox.d.ts +5 -0
- package/dist/team/mailbox.d.ts.map +1 -1
- package/dist/team/mailbox.js +43 -2
- package/dist/team/mailbox.js.map +1 -1
- package/dist/team/supervisor.d.ts +27 -2
- package/dist/team/supervisor.d.ts.map +1 -1
- package/dist/team/supervisor.js +93 -50
- package/dist/team/supervisor.js.map +1 -1
- package/dist/team/task-graph.d.ts +5 -2
- package/dist/team/task-graph.d.ts.map +1 -1
- package/dist/team/task-graph.js +27 -1
- package/dist/team/task-graph.js.map +1 -1
- package/dist/team/tool.d.ts.map +1 -1
- package/dist/team/tool.js +102 -18
- package/dist/team/tool.js.map +1 -1
- package/dist/tui/progress.d.ts +56 -44
- package/dist/tui/progress.d.ts.map +1 -1
- package/dist/tui/progress.js +497 -179
- package/dist/tui/progress.js.map +1 -1
- package/dist/tui/team-dashboard.d.ts +39 -23
- package/dist/tui/team-dashboard.d.ts.map +1 -1
- package/dist/tui/team-dashboard.js +506 -143
- package/dist/tui/team-dashboard.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,39 +2,49 @@
|
|
|
2
2
|
* tui/team-dashboard — SwarmTeam live phase progress dashboard.
|
|
3
3
|
*
|
|
4
4
|
* Renders a real-time dashboard above the input area when a SwarmTeam
|
|
5
|
-
* run is in progress. Shows phase statuses with braille
|
|
6
|
-
*
|
|
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
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
10
16
|
*/
|
|
17
|
+
import { matchesKey, Key, isKeyRepeat } from "@earendil-works/pi-tui";
|
|
18
|
+
import { readInbox, resolveMailboxPaths, } from "../team/mailbox.js";
|
|
11
19
|
// ---------------------------------------------------------------------------
|
|
12
|
-
// Constants
|
|
20
|
+
// Constants
|
|
13
21
|
// ---------------------------------------------------------------------------
|
|
14
|
-
const BRAILLE_BAR_MAX_WIDTH = 8;
|
|
15
22
|
const FRAME_INTERVAL_MS = 80;
|
|
16
23
|
const MAX_PHASES = 20;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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",
|
|
23
38
|
"\u28F7",
|
|
24
|
-
"\
|
|
39
|
+
"\u28EF",
|
|
40
|
+
"\u28DF",
|
|
41
|
+
"\u287F",
|
|
25
42
|
];
|
|
26
|
-
const BRAILLE_EMPTY = BRAILLE_LEVELS[0];
|
|
27
|
-
const BRAILLE_FULL = BRAILLE_LEVELS[6];
|
|
28
43
|
// ---------------------------------------------------------------------------
|
|
29
44
|
// Snapshot conversion
|
|
30
45
|
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
* Convert a TeamProgressSnapshot from the supervisor into the
|
|
33
|
-
* TeamDashboardState expected by TeamDashboardComponent.
|
|
34
|
-
*/
|
|
35
|
-
export function snapshotToDashboardState(snapshot) {
|
|
46
|
+
export function snapshotToDashboardState(snapshot, mailboxPath) {
|
|
36
47
|
const now = Date.now();
|
|
37
|
-
// Collect all currently running phase names for multi-running display
|
|
38
48
|
const runningPhases = snapshot.phases
|
|
39
49
|
.filter((p) => p.status === "running")
|
|
40
50
|
.map((p) => p.name);
|
|
@@ -64,7 +74,10 @@ export function snapshotToDashboardState(snapshot) {
|
|
|
64
74
|
phaseStartedAt: p.status === "running" ? now : 0,
|
|
65
75
|
})),
|
|
66
76
|
mailboxCount: snapshot.mailboxCount,
|
|
77
|
+
totalUsage: snapshot.totalUsage,
|
|
67
78
|
startedAt: snapshot.startedAt,
|
|
79
|
+
dependencyEdges: snapshot.dependencyEdges,
|
|
80
|
+
mailboxPath,
|
|
68
81
|
};
|
|
69
82
|
}
|
|
70
83
|
// ---------------------------------------------------------------------------
|
|
@@ -72,22 +85,24 @@ export function snapshotToDashboardState(snapshot) {
|
|
|
72
85
|
// ---------------------------------------------------------------------------
|
|
73
86
|
export class TeamDashboardComponent {
|
|
74
87
|
state_ = null;
|
|
75
|
-
animationFrame;
|
|
76
88
|
renderedWidth;
|
|
77
89
|
cachedLines;
|
|
78
90
|
onRequestRender;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
89
103
|
constructor(onRequestRender) {
|
|
90
104
|
this.onRequestRender = onRequestRender;
|
|
105
|
+
this.startPolling();
|
|
91
106
|
this.startAnimation();
|
|
92
107
|
}
|
|
93
108
|
// -------------------------------------------------------------------
|
|
@@ -95,7 +110,13 @@ export class TeamDashboardComponent {
|
|
|
95
110
|
// -------------------------------------------------------------------
|
|
96
111
|
update(state) {
|
|
97
112
|
this.state_ = state;
|
|
98
|
-
this.
|
|
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();
|
|
99
120
|
}
|
|
100
121
|
complete() {
|
|
101
122
|
if (this.state_) {
|
|
@@ -110,25 +131,128 @@ export class TeamDashboardComponent {
|
|
|
110
131
|
this.state_.currentPhase = undefined;
|
|
111
132
|
this.state_.currentRoles = undefined;
|
|
112
133
|
}
|
|
113
|
-
this.
|
|
134
|
+
this.hasActivePhases = false;
|
|
135
|
+
this.requestRender();
|
|
114
136
|
}
|
|
115
137
|
dispose() {
|
|
116
|
-
|
|
117
|
-
clearInterval(this.animationFrame);
|
|
118
|
-
this.animationFrame = undefined;
|
|
119
|
-
}
|
|
120
|
-
// 断开与 TUI 框架的连接,防止内存泄漏
|
|
138
|
+
this.stopTimers();
|
|
121
139
|
this.onRequestRender = undefined;
|
|
122
140
|
}
|
|
123
|
-
// -------------------------------------------------------------------
|
|
124
|
-
// Component interface
|
|
125
|
-
// -------------------------------------------------------------------
|
|
126
141
|
invalidate() {
|
|
127
142
|
this.renderedWidth = undefined;
|
|
128
143
|
this.cachedLines = undefined;
|
|
129
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
|
+
// -------------------------------------------------------------------
|
|
130
254
|
render(width) {
|
|
131
|
-
const safeWidth = Math.max(
|
|
255
|
+
const safeWidth = Math.max(20, width);
|
|
132
256
|
if (this.cachedLines && this.renderedWidth === safeWidth) {
|
|
133
257
|
return this.cachedLines;
|
|
134
258
|
}
|
|
@@ -136,149 +260,381 @@ export class TeamDashboardComponent {
|
|
|
136
260
|
this.cachedLines = [];
|
|
137
261
|
return this.cachedLines;
|
|
138
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
|
+
}
|
|
139
269
|
const state = this.state_;
|
|
140
270
|
const lines = [];
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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}`);
|
|
144
280
|
// Overall progress bar
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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);
|
|
155
296
|
}
|
|
156
297
|
// Separator
|
|
157
|
-
const sep = "\u2500".repeat(
|
|
298
|
+
const sep = "\u2500".repeat(contentWidth);
|
|
158
299
|
lines.push(` ${sep}`);
|
|
159
|
-
// Footer
|
|
160
|
-
const footerLine = buildFooter(state,
|
|
300
|
+
// Footer with all info
|
|
301
|
+
const footerLine = buildFooter(state, contentWidth);
|
|
161
302
|
lines.push(` ${footerLine}`);
|
|
162
303
|
// Bottom border
|
|
163
|
-
const bottom =
|
|
304
|
+
const bottom = `\u2514${"\u2500".repeat(contentWidth)}\u2518`;
|
|
164
305
|
lines.push(bottom);
|
|
165
306
|
this.cachedLines = lines;
|
|
166
307
|
this.renderedWidth = safeWidth;
|
|
167
308
|
return this.cachedLines;
|
|
168
309
|
}
|
|
169
310
|
// -------------------------------------------------------------------
|
|
170
|
-
//
|
|
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
|
|
171
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
|
+
}
|
|
172
537
|
startAnimation() {
|
|
173
538
|
this.animationFrame = setInterval(() => {
|
|
174
|
-
this.
|
|
175
|
-
|
|
176
|
-
|
|
539
|
+
this.frameIndex = (this.frameIndex + 1) % BRAILLE_SPINNER.length;
|
|
540
|
+
if (this.hasActivePhases || this.overlay) {
|
|
541
|
+
this.invalidate();
|
|
542
|
+
this.onRequestRender?.();
|
|
543
|
+
}
|
|
177
544
|
}, FRAME_INTERVAL_MS);
|
|
178
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
|
+
}
|
|
179
560
|
}
|
|
180
561
|
// ---------------------------------------------------------------------------
|
|
181
562
|
// Rendering helpers
|
|
182
563
|
// ---------------------------------------------------------------------------
|
|
183
|
-
function
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return full.slice(0, width);
|
|
195
|
-
}
|
|
196
|
-
function buildProgressBar(state, width) {
|
|
197
|
-
const barWidth = Math.max(1, width - 4);
|
|
198
|
-
const total = state.totalPhases || 1;
|
|
199
|
-
const doneRatio = (state.completedPhases + state.failedPhases) / total;
|
|
200
|
-
const doneChars = Math.round(doneRatio * barWidth);
|
|
201
|
-
const remainingChars = barWidth - doneChars;
|
|
202
|
-
const done = "\u2501".repeat(doneChars);
|
|
203
|
-
const remaining = "\u2501".repeat(remainingChars);
|
|
204
|
-
return ` ${done}${remaining}`;
|
|
205
|
-
}
|
|
206
|
-
function renderPhaseRow(phase, width) {
|
|
207
|
-
const statusIcon = phaseStatusIcon(phase);
|
|
208
|
-
const maxNameLen = Math.max(4, Math.min(14, Math.floor(width / 3)));
|
|
209
|
-
const phaseName = truncateText(phase.name, maxNameLen);
|
|
210
|
-
const statusLabel = phase.status;
|
|
211
|
-
const roleLabel = `(${phase.role})`;
|
|
212
|
-
// Layout: <icon> <phaseName> <status> <role>
|
|
213
|
-
const fixed = `${statusIcon} ${phaseName} ${statusLabel} ${roleLabel}`;
|
|
214
|
-
// If the fixed part alone exceeds width, truncate by component priority
|
|
215
|
-
if (visibleLen(fixed) >= width) {
|
|
216
|
-
const bare = `${statusIcon} ${phaseName}`;
|
|
217
|
-
if (visibleLen(bare) >= width)
|
|
218
|
-
return bare.slice(0, Math.max(0, width));
|
|
219
|
-
const withStatus = `${bare} ${statusLabel}`;
|
|
220
|
-
if (visibleLen(withStatus) >= width)
|
|
221
|
-
return withStatus.slice(0, Math.max(0, width));
|
|
222
|
-
return fixed.slice(0, Math.max(0, width));
|
|
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)}`;
|
|
223
575
|
}
|
|
224
576
|
if (phase.status === "failed" && phase.error) {
|
|
225
|
-
const errorPart =
|
|
226
|
-
|
|
227
|
-
if (avail > 0) {
|
|
228
|
-
const full = `${fixed}${errorPart.slice(0, avail)}`;
|
|
229
|
-
return full.slice(0, Math.max(0, width));
|
|
230
|
-
}
|
|
231
|
-
return fixed.slice(0, Math.max(0, width));
|
|
577
|
+
const errorPart = phase.error;
|
|
578
|
+
return `${fixed} ${truncateText(errorPart, remaining)}`;
|
|
232
579
|
}
|
|
233
|
-
|
|
580
|
+
if (phase.status === "completed") {
|
|
581
|
+
return `${fixed} ok`;
|
|
582
|
+
}
|
|
583
|
+
if (phase.status === "skipped") {
|
|
584
|
+
return `${fixed} skip`;
|
|
585
|
+
}
|
|
586
|
+
return `${fixed} ...`;
|
|
234
587
|
}
|
|
235
|
-
function
|
|
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) {
|
|
236
600
|
switch (phase.status) {
|
|
237
601
|
case "completed":
|
|
238
|
-
return "\u2713";
|
|
602
|
+
return "\u2713";
|
|
239
603
|
case "running":
|
|
240
|
-
return
|
|
604
|
+
return BRAILLE_SPINNER[frameIndex % BRAILLE_SPINNER.length];
|
|
241
605
|
case "failed":
|
|
242
|
-
return "\u2717";
|
|
606
|
+
return "\u2717";
|
|
243
607
|
case "skipped":
|
|
244
|
-
return "\u2298";
|
|
608
|
+
return "\u2298";
|
|
245
609
|
case "queued":
|
|
246
|
-
return "\u25CB";
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
function renderBrailleBar(phase) {
|
|
250
|
-
const elapsed = Date.now() - phase.phaseStartedAt;
|
|
251
|
-
const cycleMs = 800;
|
|
252
|
-
const progress = (elapsed % cycleMs) / cycleMs;
|
|
253
|
-
const maxWidth = BRAILLE_BAR_MAX_WIDTH;
|
|
254
|
-
const totalDots = maxWidth * 6;
|
|
255
|
-
const filledDots = Math.floor(progress * totalDots);
|
|
256
|
-
let result = "";
|
|
257
|
-
for (let i = 0; i < maxWidth; i += 1) {
|
|
258
|
-
const cellStart = i * 6;
|
|
259
|
-
const dotsInCell = Math.max(0, Math.min(6, filledDots - cellStart));
|
|
260
|
-
result += dotsInCell === 0 ? BRAILLE_EMPTY : BRAILLE_LEVELS[dotsInCell];
|
|
610
|
+
return "\u25CB";
|
|
261
611
|
}
|
|
262
|
-
return result;
|
|
263
612
|
}
|
|
264
613
|
function buildFooter(state, width) {
|
|
265
|
-
const
|
|
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` : "";
|
|
266
624
|
const elapsed = formatElapsed(Date.now() - state.startedAt);
|
|
267
|
-
const
|
|
625
|
+
const parts = [`${phaseCount} ph`, tokens, mailbox.trim(), elapsed].filter(Boolean);
|
|
626
|
+
const full = parts.join(" | ");
|
|
268
627
|
if (full.length <= width)
|
|
269
628
|
return full;
|
|
270
|
-
|
|
271
|
-
return `Elapsed: ${elapsed}`.slice(0, Math.max(0, width));
|
|
629
|
+
return truncateText(full, width);
|
|
272
630
|
}
|
|
273
631
|
function formatElapsed(ms) {
|
|
274
632
|
const totalSec = Math.floor(ms / 1000);
|
|
633
|
+
if (totalSec < 60)
|
|
634
|
+
return `${totalSec}s`;
|
|
275
635
|
const m = Math.floor(totalSec / 60);
|
|
276
636
|
const s = totalSec % 60;
|
|
277
|
-
return `${m}m
|
|
278
|
-
}
|
|
279
|
-
function bottomBorder(width) {
|
|
280
|
-
const safeWidth = Math.max(2, width);
|
|
281
|
-
return `\u2514${"\u2500".repeat(safeWidth - 2)}\u2518`;
|
|
637
|
+
return `${m}m${s}s`;
|
|
282
638
|
}
|
|
283
639
|
// ---------------------------------------------------------------------------
|
|
284
640
|
// Text utilities
|
|
@@ -289,6 +645,13 @@ function visibleLen(text) {
|
|
|
289
645
|
function truncateText(text, maxLen) {
|
|
290
646
|
if (text.length <= maxLen)
|
|
291
647
|
return text;
|
|
292
|
-
|
|
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);
|
|
293
656
|
}
|
|
294
657
|
//# sourceMappingURL=team-dashboard.js.map
|