@gjczone/pi-swarm 0.5.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -21
- package/dist/index.d.ts +2 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -18
- package/dist/index.js.map +1 -1
- package/dist/shared/controller.d.ts +10 -1
- package/dist/shared/controller.d.ts.map +1 -1
- package/dist/shared/controller.js +48 -12
- package/dist/shared/controller.js.map +1 -1
- package/dist/shared/render.d.ts +1 -1
- package/dist/shared/render.js +1 -1
- package/dist/shared/spawner.js +54 -8
- package/dist/shared/spawner.js.map +1 -1
- package/dist/shared/types.d.ts +10 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/worktree.d.ts +2 -1
- package/dist/shared/worktree.d.ts.map +1 -1
- package/dist/shared/worktree.js +10 -4
- package/dist/shared/worktree.js.map +1 -1
- package/dist/state/recovery.d.ts.map +1 -1
- package/dist/state/recovery.js +25 -4
- package/dist/state/recovery.js.map +1 -1
- package/dist/swarm/command.d.ts +4 -5
- package/dist/swarm/command.d.ts.map +1 -1
- package/dist/swarm/command.js +26 -74
- package/dist/swarm/command.js.map +1 -1
- package/dist/swarm/mode.d.ts +1 -1
- package/dist/swarm/mode.js +2 -2
- package/dist/swarm/mode.js.map +1 -1
- package/dist/swarm/tool.d.ts +4 -4
- package/dist/swarm/tool.d.ts.map +1 -1
- package/dist/swarm/tool.js +112 -167
- package/dist/swarm/tool.js.map +1 -1
- package/dist/team/command.d.ts +2 -4
- package/dist/team/command.d.ts.map +1 -1
- package/dist/team/command.js +5 -13
- package/dist/team/command.js.map +1 -1
- package/dist/team/mailbox.d.ts +7 -2
- package/dist/team/mailbox.d.ts.map +1 -1
- package/dist/team/mailbox.js +121 -32
- package/dist/team/mailbox.js.map +1 -1
- package/dist/tui/progress.d.ts +35 -47
- package/dist/tui/progress.d.ts.map +1 -1
- package/dist/tui/progress.js +245 -489
- package/dist/tui/progress.js.map +1 -1
- package/dist/tui/swarm-markers.d.ts +1 -1
- package/dist/tui/swarm-markers.js +1 -1
- package/package.json +13 -2
- package/dist/team/supervisor.d.ts +0 -171
- package/dist/team/supervisor.d.ts.map +0 -1
- package/dist/team/supervisor.js +0 -685
- package/dist/team/supervisor.js.map +0 -1
- package/dist/team/task-graph.d.ts +0 -64
- package/dist/team/task-graph.d.ts.map +0 -1
- package/dist/team/task-graph.js +0 -216
- package/dist/team/task-graph.js.map +0 -1
- package/dist/team/tool.d.ts +0 -11
- package/dist/team/tool.d.ts.map +0 -1
- package/dist/team/tool.js +0 -491
- package/dist/team/tool.js.map +0 -1
- package/dist/tui/permission-prompt.d.ts +0 -26
- package/dist/tui/permission-prompt.d.ts.map +0 -1
- package/dist/tui/permission-prompt.js +0 -98
- package/dist/tui/permission-prompt.js.map +0 -1
- package/dist/tui/team-dashboard.d.ts +0 -81
- package/dist/tui/team-dashboard.d.ts.map +0 -1
- package/dist/tui/team-dashboard.js +0 -657
- 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
|