@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.
- package/README.md +33 -80
- package/dist/shared/controller.d.ts +9 -4
- package/dist/shared/controller.d.ts.map +1 -1
- package/dist/shared/controller.js +93 -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/types.d.ts +30 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- 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 +36 -21
- package/dist/swarm/tool.js.map +1 -1
- package/dist/team/supervisor.d.ts.map +1 -1
- package/dist/team/supervisor.js +18 -33
- package/dist/team/supervisor.js.map +1 -1
- package/dist/team/task-graph.d.ts.map +1 -1
- package/dist/team/task-graph.js.map +1 -1
- package/dist/team/tool.d.ts.map +1 -1
- package/dist/team/tool.js +57 -21
- package/dist/team/tool.js.map +1 -1
- package/dist/tui/progress.d.ts +51 -2
- package/dist/tui/progress.d.ts.map +1 -1
- package/dist/tui/progress.js +413 -31
- package/dist/tui/progress.js.map +1 -1
- package/dist/tui/team-dashboard.d.ts +36 -3
- package/dist/tui/team-dashboard.d.ts.map +1 -1
- package/dist/tui/team-dashboard.js +431 -23
- package/dist/tui/team-dashboard.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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.
|
|
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.
|
|
134
|
+
this.hasActivePhases = false;
|
|
135
|
+
this.requestRender();
|
|
94
136
|
}
|
|
95
137
|
dispose() {
|
|
96
|
-
|
|
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
|
|
119
|
-
const header = truncateText(state.title, contentWidth);
|
|
120
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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.
|
|
155
|
-
|
|
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
|