@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
package/dist/tui/progress.js
CHANGED
|
@@ -2,56 +2,64 @@
|
|
|
2
2
|
* tui/progress — AgentSwarm live progress panel.
|
|
3
3
|
*
|
|
4
4
|
* Renders a real-time progress display above the input area when
|
|
5
|
-
* an AgentSwarm batch is running.
|
|
6
|
-
*
|
|
5
|
+
* an AgentSwarm batch is running. Each subagent gets a compact braille
|
|
6
|
+
* spinner with status and item description.
|
|
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: members list / event log
|
|
12
|
+
* - Detail overlay for individual members
|
|
13
|
+
* - Activity/tool tracking per member
|
|
14
|
+
* - ETA estimation
|
|
7
15
|
*
|
|
8
16
|
* Ported from MoonshotAI/kimi-code's AgentSwarmProgressComponent.
|
|
9
17
|
*/
|
|
18
|
+
import { matchesKey, Key, isKeyRepeat } from "@earendil-works/pi-tui";
|
|
10
19
|
// ---------------------------------------------------------------------------
|
|
11
|
-
// Constants
|
|
20
|
+
// Constants
|
|
12
21
|
// ---------------------------------------------------------------------------
|
|
13
|
-
/**
|
|
14
|
-
const
|
|
15
|
-
/**
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
/** Max item description length to display. */
|
|
23
|
+
const MAX_ITEM_LABEL_LEN = 40;
|
|
24
|
+
/** Compact braille spinner frames for running agents. */
|
|
25
|
+
const BRAILLE_SPINNER = [
|
|
26
|
+
"\u28BF",
|
|
27
|
+
"\u28FB",
|
|
28
|
+
"\u28FD",
|
|
29
|
+
"\u28FE",
|
|
30
|
+
"\u28F7",
|
|
31
|
+
"\u28EF",
|
|
32
|
+
"\u28DF",
|
|
33
|
+
"\u287F",
|
|
34
|
+
];
|
|
35
|
+
/** Braille characters representing fill levels 0-6 dots for completed bar. */
|
|
26
36
|
const BRAILLE_LEVELS = [
|
|
27
|
-
"\u28C0",
|
|
28
|
-
"\u28C4",
|
|
29
|
-
"\u28E4",
|
|
30
|
-
"\u28E6",
|
|
31
|
-
"\u28F6",
|
|
32
|
-
"\u28F7",
|
|
33
|
-
"\u28FF",
|
|
37
|
+
"\u28C0",
|
|
38
|
+
"\u28C4",
|
|
39
|
+
"\u28E4",
|
|
40
|
+
"\u28E6",
|
|
41
|
+
"\u28F6",
|
|
42
|
+
"\u28F7",
|
|
43
|
+
"\u28FF",
|
|
34
44
|
];
|
|
35
45
|
const BRAILLE_EMPTY = BRAILLE_LEVELS[0];
|
|
36
46
|
const BRAILLE_FULL = BRAILLE_LEVELS[6];
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
47
|
+
const COMPLETED_BAR_WIDTH = 4;
|
|
48
|
+
/** Maximum members shown at once in the list view. */
|
|
49
|
+
const VISIBLE_MEMBERS = 8;
|
|
50
|
+
/** Maximum events shown in the event log panel. */
|
|
51
|
+
const VISIBLE_EVENTS = 10;
|
|
52
|
+
/** Debounce window for coalescing render requests. */
|
|
53
|
+
const DEBOUNCE_MS = 75;
|
|
54
|
+
/** Fallback polling interval when state is active (has running members). */
|
|
55
|
+
const ACTIVE_POLL_MS = 800;
|
|
56
|
+
/** Fallback polling interval when idle (no running members). */
|
|
57
|
+
const IDLE_POLL_MS = 2000;
|
|
58
|
+
/** Animation interval for the braille spinner. */
|
|
59
|
+
const FRAME_INTERVAL_MS = 80;
|
|
44
60
|
// ---------------------------------------------------------------------------
|
|
45
|
-
// Snapshot conversion
|
|
61
|
+
// Snapshot conversion
|
|
46
62
|
// ---------------------------------------------------------------------------
|
|
47
|
-
/**
|
|
48
|
-
* Convert a BatchProgressSnapshot from the concurrency controller into
|
|
49
|
-
* the SwarmProgressState expected by AgentSwarmProgressComponent.
|
|
50
|
-
*
|
|
51
|
-
* Maps the controller's coarse member phases onto the component's
|
|
52
|
-
* richer MemberPhase set, attaching a phaseStartedAt timestamp so the
|
|
53
|
-
* braille animation can run for working/suspended members.
|
|
54
|
-
*/
|
|
55
63
|
export function snapshotToProgressState(snapshot, title) {
|
|
56
64
|
const now = Date.now();
|
|
57
65
|
const members = snapshot.members.map((m) => ({
|
|
@@ -60,6 +68,9 @@ export function snapshotToProgressState(snapshot, title) {
|
|
|
60
68
|
item: m.item,
|
|
61
69
|
error: m.error,
|
|
62
70
|
phaseStartedAt: isAnimatedPhase(m.phase) ? now : undefined,
|
|
71
|
+
currentTool: m.currentTool,
|
|
72
|
+
activity: m.activity,
|
|
73
|
+
usage: m.usage,
|
|
63
74
|
}));
|
|
64
75
|
return {
|
|
65
76
|
title,
|
|
@@ -69,6 +80,10 @@ export function snapshotToProgressState(snapshot, title) {
|
|
|
69
80
|
active: snapshot.active,
|
|
70
81
|
queued: snapshot.queued,
|
|
71
82
|
members,
|
|
83
|
+
totalUsage: snapshot.totalUsage,
|
|
84
|
+
startedAt: snapshot.startedAt ?? now,
|
|
85
|
+
estimatedRemainingMs: snapshot.estimatedRemainingMs,
|
|
86
|
+
eventLog: snapshot.eventLog ? [...snapshot.eventLog] : undefined,
|
|
72
87
|
};
|
|
73
88
|
}
|
|
74
89
|
function mapMemberPhase(phase) {
|
|
@@ -93,37 +108,40 @@ function isAnimatedPhase(phase) {
|
|
|
93
108
|
// ---------------------------------------------------------------------------
|
|
94
109
|
export class AgentSwarmProgressComponent {
|
|
95
110
|
state_ = null;
|
|
96
|
-
animationFrame;
|
|
97
|
-
startTime = Date.now();
|
|
98
111
|
renderedWidth;
|
|
99
112
|
cachedLines;
|
|
100
113
|
onRequestRender;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
// Render scheduler
|
|
115
|
+
animationFrame;
|
|
116
|
+
frameIndex = 0;
|
|
117
|
+
pollTimer;
|
|
118
|
+
debounceTimer;
|
|
119
|
+
pendingInvalidate = false;
|
|
120
|
+
hasActiveMembers = false;
|
|
121
|
+
// Input / UI state
|
|
122
|
+
scrollOffset = 0;
|
|
123
|
+
selectedIndex = -1;
|
|
124
|
+
activePanel = "members";
|
|
125
|
+
overlay = null;
|
|
111
126
|
constructor(onRequestRender) {
|
|
112
127
|
this.onRequestRender = onRequestRender;
|
|
128
|
+
this.startPolling();
|
|
113
129
|
this.startAnimation();
|
|
114
130
|
}
|
|
115
131
|
// -------------------------------------------------------------------
|
|
116
132
|
// Public API
|
|
117
133
|
// -------------------------------------------------------------------
|
|
118
|
-
/** Update the progress state from the tool execution. */
|
|
119
134
|
update(state) {
|
|
120
135
|
this.state_ = state;
|
|
121
|
-
this.
|
|
136
|
+
this.hasActiveMembers = state.active > 0;
|
|
137
|
+
// Reset scroll offset if it exceeds member count
|
|
138
|
+
if (this.scrollOffset > Math.max(0, state.members.length - VISIBLE_MEMBERS)) {
|
|
139
|
+
this.scrollOffset = Math.max(0, state.members.length - VISIBLE_MEMBERS);
|
|
140
|
+
}
|
|
141
|
+
this.requestRender();
|
|
122
142
|
}
|
|
123
|
-
/** Mark the swarm as completed (triggers completion animation). */
|
|
124
143
|
complete() {
|
|
125
144
|
if (this.state_) {
|
|
126
|
-
// Mark all non-terminal members as completed
|
|
127
145
|
for (const m of this.state_.members) {
|
|
128
146
|
if (m.phase !== "completed" &&
|
|
129
147
|
m.phase !== "failed" &&
|
|
@@ -135,28 +153,106 @@ export class AgentSwarmProgressComponent {
|
|
|
135
153
|
this.state_.active = 0;
|
|
136
154
|
this.state_.queued = 0;
|
|
137
155
|
this.state_.completed = this.state_.total - this.state_.failed;
|
|
138
|
-
this.state_.status = COMPLETED_LABEL;
|
|
139
156
|
}
|
|
140
|
-
this.
|
|
157
|
+
this.hasActiveMembers = false;
|
|
158
|
+
this.requestRender();
|
|
141
159
|
}
|
|
142
|
-
/** Stop the animation loop. */
|
|
143
160
|
dispose() {
|
|
144
|
-
|
|
145
|
-
clearInterval(this.animationFrame);
|
|
146
|
-
this.animationFrame = undefined;
|
|
147
|
-
}
|
|
148
|
-
// 断开与 TUI 框架的连接,防止内存泄漏
|
|
161
|
+
this.stopTimers();
|
|
149
162
|
this.onRequestRender = undefined;
|
|
150
163
|
}
|
|
151
|
-
// -------------------------------------------------------------------
|
|
152
|
-
// Component interface
|
|
153
|
-
// -------------------------------------------------------------------
|
|
154
164
|
invalidate() {
|
|
155
165
|
this.renderedWidth = undefined;
|
|
156
166
|
this.cachedLines = undefined;
|
|
157
167
|
}
|
|
168
|
+
// -------------------------------------------------------------------
|
|
169
|
+
// Keyboard input
|
|
170
|
+
// -------------------------------------------------------------------
|
|
171
|
+
handleInput(data) {
|
|
172
|
+
// Ignore key repeats for navigation keys to avoid scroll jank
|
|
173
|
+
const isRepeat = isKeyRepeat(data);
|
|
174
|
+
// Close overlay on Escape / q
|
|
175
|
+
if (this.overlay) {
|
|
176
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, "q")) {
|
|
177
|
+
this.overlay = null;
|
|
178
|
+
this.requestRender();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Navigation
|
|
184
|
+
if (matchesKey(data, "j") || matchesKey(data, Key.down)) {
|
|
185
|
+
if (!isRepeat) {
|
|
186
|
+
this.scrollDown(1);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (matchesKey(data, "k") || matchesKey(data, Key.up)) {
|
|
191
|
+
if (!isRepeat) {
|
|
192
|
+
this.scrollUp(1);
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
197
|
+
this.scrollDown(VISIBLE_MEMBERS);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
201
|
+
this.scrollUp(VISIBLE_MEMBERS);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (matchesKey(data, "g") || matchesKey(data, Key.home)) {
|
|
205
|
+
this.scrollOffset = 0;
|
|
206
|
+
this.selectedIndex = -1;
|
|
207
|
+
this.requestRender();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (matchesKey(data, Key.shift("g")) || matchesKey(data, Key.end)) {
|
|
211
|
+
if (this.state_) {
|
|
212
|
+
const maxScroll = Math.max(0, this.state_.members.length - VISIBLE_MEMBERS);
|
|
213
|
+
this.scrollOffset = maxScroll;
|
|
214
|
+
this.selectedIndex = Math.max(0, this.state_.members.length - 1);
|
|
215
|
+
}
|
|
216
|
+
this.requestRender();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Panel switching
|
|
220
|
+
if (matchesKey(data, Key.tab) || matchesKey(data, "1")) {
|
|
221
|
+
this.activePanel = "members";
|
|
222
|
+
this.scrollOffset = 0;
|
|
223
|
+
this.selectedIndex = -1;
|
|
224
|
+
this.requestRender();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (matchesKey(data, "2")) {
|
|
228
|
+
this.activePanel = "events";
|
|
229
|
+
this.scrollOffset = 0;
|
|
230
|
+
this.requestRender();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Detail overlay
|
|
234
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
|
|
235
|
+
if (this.activePanel === "members" && this.state_) {
|
|
236
|
+
const idx = this.selectedIndex >= 0 ? this.selectedIndex : this.scrollOffset;
|
|
237
|
+
if (idx >= 0 && idx < this.state_.members.length) {
|
|
238
|
+
this.overlay = { kind: "detail" };
|
|
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
|
+
// -------------------------------------------------------------------
|
|
158
254
|
render(width) {
|
|
159
|
-
const safeWidth = Math.max(
|
|
255
|
+
const safeWidth = Math.max(20, width);
|
|
160
256
|
if (this.cachedLines && this.renderedWidth === safeWidth) {
|
|
161
257
|
return this.cachedLines;
|
|
162
258
|
}
|
|
@@ -164,176 +260,398 @@ export class AgentSwarmProgressComponent {
|
|
|
164
260
|
this.cachedLines = [];
|
|
165
261
|
return this.cachedLines;
|
|
166
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
|
+
}
|
|
167
269
|
const state = this.state_;
|
|
270
|
+
const contentWidth = safeWidth - 4;
|
|
168
271
|
const lines = [];
|
|
169
|
-
//
|
|
272
|
+
// Header: title + panel indicator
|
|
170
273
|
const title = state.title ?? "Agent Swarm";
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
274
|
+
const panelLabel = this.activePanel === "members" ? "[1]members" : "[2]events";
|
|
275
|
+
const headerText = `${title} ${panelLabel}`;
|
|
276
|
+
lines.push(` ${truncateText(headerText, contentWidth)}`);
|
|
277
|
+
// Overall progress bar
|
|
278
|
+
const barWidth = Math.max(1, contentWidth);
|
|
279
|
+
const total = state.total || 1;
|
|
280
|
+
const doneRatio = (state.completed + state.failed) / total;
|
|
281
|
+
const doneChars = Math.round(doneRatio * barWidth);
|
|
282
|
+
const done = "\u2501".repeat(doneChars);
|
|
283
|
+
const remaining = "\u2501".repeat(barWidth - doneChars);
|
|
284
|
+
lines.push(` ${done}${remaining}`);
|
|
285
|
+
if (this.activePanel === "members") {
|
|
286
|
+
this.renderMembersPanel(lines, state, contentWidth);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
this.renderEventsPanel(lines, state, contentWidth);
|
|
185
290
|
}
|
|
186
|
-
//
|
|
187
|
-
const
|
|
188
|
-
lines.push(` ${
|
|
291
|
+
// Separator
|
|
292
|
+
const sep = "\u2500".repeat(contentWidth);
|
|
293
|
+
lines.push(` ${sep}`);
|
|
294
|
+
// Footer: counts + tokens + elapsed + ETA
|
|
295
|
+
const footer = buildFooter(state, contentWidth);
|
|
296
|
+
lines.push(` ${footer}`);
|
|
189
297
|
// Bottom border
|
|
190
|
-
|
|
298
|
+
const bottom = `\u2514${"\u2500".repeat(contentWidth)}\u2518`;
|
|
299
|
+
lines.push(bottom);
|
|
191
300
|
this.cachedLines = lines;
|
|
192
301
|
this.renderedWidth = safeWidth;
|
|
193
302
|
return this.cachedLines;
|
|
194
303
|
}
|
|
195
304
|
// -------------------------------------------------------------------
|
|
196
|
-
//
|
|
305
|
+
// Panel rendering
|
|
306
|
+
// -------------------------------------------------------------------
|
|
307
|
+
renderMembersPanel(lines, state, contentWidth) {
|
|
308
|
+
const maxMembers = Math.min(state.members.length, this.scrollOffset + VISIBLE_MEMBERS);
|
|
309
|
+
const hasMore = state.members.length > VISIBLE_MEMBERS;
|
|
310
|
+
// Scroll indicator (top)
|
|
311
|
+
if (hasMore && this.scrollOffset > 0) {
|
|
312
|
+
lines.push(` \u2191 ${this.scrollOffset} more above`);
|
|
313
|
+
}
|
|
314
|
+
for (let i = this.scrollOffset; i < maxMembers; i += 1) {
|
|
315
|
+
const member = state.members[i];
|
|
316
|
+
if (!member)
|
|
317
|
+
continue;
|
|
318
|
+
const isSelected = i === this.selectedIndex ||
|
|
319
|
+
(this.selectedIndex < 0 && i === this.scrollOffset);
|
|
320
|
+
const row = renderMemberRow(member, contentWidth, this.frameIndex, isSelected);
|
|
321
|
+
lines.push(` ${row}`);
|
|
322
|
+
}
|
|
323
|
+
// Scroll indicator (bottom)
|
|
324
|
+
if (hasMore && maxMembers < state.members.length) {
|
|
325
|
+
const remaining = state.members.length - maxMembers;
|
|
326
|
+
lines.push(` \u2193 ${remaining} more below`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
renderEventsPanel(lines, state, contentWidth) {
|
|
330
|
+
const events = state.eventLog ?? [];
|
|
331
|
+
const displayEvents = events.slice(-VISIBLE_EVENTS);
|
|
332
|
+
if (displayEvents.length === 0) {
|
|
333
|
+
lines.push(` ${padRight("(no events yet)", contentWidth)}`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
for (const event of displayEvents) {
|
|
337
|
+
const time = new Date(event.timestamp).toLocaleTimeString();
|
|
338
|
+
const icon = eventIcon(event.type);
|
|
339
|
+
const detail = truncateText(event.detail, contentWidth - 12);
|
|
340
|
+
lines.push(` ${icon} ${time} ${detail}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// -------------------------------------------------------------------
|
|
344
|
+
// Overlays
|
|
197
345
|
// -------------------------------------------------------------------
|
|
346
|
+
renderOverlay(width) {
|
|
347
|
+
if (!this.overlay)
|
|
348
|
+
return [];
|
|
349
|
+
if (this.overlay.kind === "help") {
|
|
350
|
+
return this.renderHelpOverlay(width);
|
|
351
|
+
}
|
|
352
|
+
return this.renderDetailOverlay(width);
|
|
353
|
+
}
|
|
354
|
+
renderHelpOverlay(width) {
|
|
355
|
+
const safeWidth = Math.max(30, width);
|
|
356
|
+
const contentWidth = safeWidth - 6;
|
|
357
|
+
const lines = [];
|
|
358
|
+
const bottom = `\u2514${"\u2500".repeat(safeWidth - 2)}\u2518`;
|
|
359
|
+
lines.push(` \u250C${"\u2500".repeat(safeWidth - 2)}\u2510`);
|
|
360
|
+
lines.push(` \u2502 ${padRight("Help" + " ".repeat(contentWidth - 4), safeWidth - 4)}\u2502`);
|
|
361
|
+
lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
|
|
362
|
+
const helpItems = [
|
|
363
|
+
["j/k, up/down", "Scroll member list"],
|
|
364
|
+
["g / Shift+G", "Go to top / bottom"],
|
|
365
|
+
["Enter", "View member detail"],
|
|
366
|
+
["1 or Tab", "Members panel"],
|
|
367
|
+
["2", "Events panel"],
|
|
368
|
+
["?", "Toggle this help"],
|
|
369
|
+
["q / Esc", "Close overlay"],
|
|
370
|
+
];
|
|
371
|
+
for (const [key, desc] of helpItems) {
|
|
372
|
+
const line = `${padRight(key, 18)} ${desc}`;
|
|
373
|
+
lines.push(` \u2502 ${padRight(line, safeWidth - 4)}\u2502`);
|
|
374
|
+
}
|
|
375
|
+
lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
|
|
376
|
+
lines.push(` \u2502 ${padRight("Press q or Esc to close", safeWidth - 4)}\u2502`);
|
|
377
|
+
lines.push(bottom);
|
|
378
|
+
return lines;
|
|
379
|
+
}
|
|
380
|
+
renderDetailOverlay(width) {
|
|
381
|
+
const safeWidth = Math.max(30, width);
|
|
382
|
+
const contentWidth = safeWidth - 6;
|
|
383
|
+
const lines = [];
|
|
384
|
+
if (!this.state_)
|
|
385
|
+
return [];
|
|
386
|
+
const idx = this.selectedIndex >= 0 ? this.selectedIndex : this.scrollOffset;
|
|
387
|
+
const member = this.state_.members[idx];
|
|
388
|
+
if (!member)
|
|
389
|
+
return [];
|
|
390
|
+
const bottom = `\u2514${"\u2500".repeat(safeWidth - 2)}\u2518`;
|
|
391
|
+
lines.push(` \u250C${"\u2500".repeat(safeWidth - 2)}\u2510`);
|
|
392
|
+
lines.push(` \u2502 ${padRight(`Agent #${String(member.index).padStart(2, "0")}`, safeWidth - 4)}\u2502`);
|
|
393
|
+
lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
|
|
394
|
+
const fields = [
|
|
395
|
+
{ label: "Status", value: shortPhaseLabel(member.phase) },
|
|
396
|
+
];
|
|
397
|
+
if (member.item) {
|
|
398
|
+
fields.push({ label: "Item", value: member.item });
|
|
399
|
+
}
|
|
400
|
+
if (member.currentTool) {
|
|
401
|
+
fields.push({ label: "Tool", value: member.currentTool });
|
|
402
|
+
}
|
|
403
|
+
if (member.activity) {
|
|
404
|
+
fields.push({ label: "Activity", value: member.activity });
|
|
405
|
+
}
|
|
406
|
+
if (member.usage) {
|
|
407
|
+
fields.push({
|
|
408
|
+
label: "Tokens",
|
|
409
|
+
value: `${Math.round(member.usage.input)}in / ${Math.round(member.usage.output)}out`,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (member.error) {
|
|
413
|
+
fields.push({ label: "Error", value: member.error });
|
|
414
|
+
}
|
|
415
|
+
for (const field of fields) {
|
|
416
|
+
const line = `${padRight(field.label + ":", 10)} ${truncateText(field.value, contentWidth - 12)}`;
|
|
417
|
+
lines.push(` \u2502 ${padRight(line, safeWidth - 4)}\u2502`);
|
|
418
|
+
}
|
|
419
|
+
lines.push(` \u2502 ${padRight("", safeWidth - 4)}\u2502`);
|
|
420
|
+
lines.push(` \u2502 ${padRight("Press q or Esc to close", safeWidth - 4)}\u2502`);
|
|
421
|
+
lines.push(bottom);
|
|
422
|
+
return lines;
|
|
423
|
+
}
|
|
424
|
+
// -------------------------------------------------------------------
|
|
425
|
+
// Scroll helpers
|
|
426
|
+
// -------------------------------------------------------------------
|
|
427
|
+
scrollDown(n) {
|
|
428
|
+
if (!this.state_)
|
|
429
|
+
return;
|
|
430
|
+
const memberCount = this.state_.members.length;
|
|
431
|
+
const maxOffset = Math.max(0, memberCount - 1);
|
|
432
|
+
this.selectedIndex = Math.min(maxOffset, Math.max(0, this.selectedIndex < 0 ? this.scrollOffset : this.selectedIndex) + n);
|
|
433
|
+
if (this.selectedIndex - this.scrollOffset >= VISIBLE_MEMBERS ||
|
|
434
|
+
this.selectedIndex < this.scrollOffset) {
|
|
435
|
+
this.scrollOffset = Math.max(0, this.selectedIndex - VISIBLE_MEMBERS + 1);
|
|
436
|
+
const maxScroll = Math.max(0, memberCount - VISIBLE_MEMBERS);
|
|
437
|
+
this.scrollOffset = Math.min(this.scrollOffset, maxScroll);
|
|
438
|
+
}
|
|
439
|
+
this.requestRender();
|
|
440
|
+
}
|
|
441
|
+
scrollUp(n) {
|
|
442
|
+
if (!this.state_)
|
|
443
|
+
return;
|
|
444
|
+
this.selectedIndex = Math.max(0, (this.selectedIndex < 0 ? this.scrollOffset : this.selectedIndex) - n);
|
|
445
|
+
if (this.selectedIndex < this.scrollOffset) {
|
|
446
|
+
this.scrollOffset = Math.max(0, this.selectedIndex);
|
|
447
|
+
}
|
|
448
|
+
this.requestRender();
|
|
449
|
+
}
|
|
450
|
+
// -------------------------------------------------------------------
|
|
451
|
+
// Render scheduling
|
|
452
|
+
// -------------------------------------------------------------------
|
|
453
|
+
/**
|
|
454
|
+
* Request a debounced render. Multiple calls within the debounce window
|
|
455
|
+
* coalesce into a single render pass. State changes also trigger immediate
|
|
456
|
+
* invalidation.
|
|
457
|
+
*/
|
|
458
|
+
requestRender() {
|
|
459
|
+
this.invalidate();
|
|
460
|
+
if (this.debounceTimer !== undefined) {
|
|
461
|
+
this.pendingInvalidate = true;
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
this.debounceTimer = setTimeout(() => {
|
|
465
|
+
this.debounceTimer = undefined;
|
|
466
|
+
this.pendingInvalidate = false;
|
|
467
|
+
this.onRequestRender?.();
|
|
468
|
+
this.updatePollingInterval();
|
|
469
|
+
}, DEBOUNCE_MS);
|
|
470
|
+
// Also trigger an immediate render for faster response to inputs
|
|
471
|
+
this.onRequestRender?.();
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Start the fallback polling timer. Falls back to polling when no
|
|
475
|
+
* state changes trigger requests, ensuring the spinner animates.
|
|
476
|
+
*/
|
|
477
|
+
startPolling() {
|
|
478
|
+
this.schedulePoll();
|
|
479
|
+
}
|
|
480
|
+
schedulePoll() {
|
|
481
|
+
const interval = this.hasActiveMembers ? ACTIVE_POLL_MS : IDLE_POLL_MS;
|
|
482
|
+
this.pollTimer = setTimeout(() => {
|
|
483
|
+
this.pollTimer = undefined;
|
|
484
|
+
// Tick the spinner frame even without state changes
|
|
485
|
+
if (this.state_) {
|
|
486
|
+
this.invalidate();
|
|
487
|
+
this.onRequestRender?.();
|
|
488
|
+
}
|
|
489
|
+
this.schedulePoll();
|
|
490
|
+
}, interval);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Adjust polling interval based on active member count.
|
|
494
|
+
*/
|
|
495
|
+
updatePollingInterval() {
|
|
496
|
+
// Will be picked up on next poll cycle
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Start the animation tick for the braille spinner.
|
|
500
|
+
*/
|
|
198
501
|
startAnimation() {
|
|
199
502
|
this.animationFrame = setInterval(() => {
|
|
200
|
-
this.
|
|
201
|
-
//
|
|
202
|
-
|
|
503
|
+
this.frameIndex = (this.frameIndex + 1) % BRAILLE_SPINNER.length;
|
|
504
|
+
// Only invalidate and re-render if there are active members (spinner visible)
|
|
505
|
+
// or if we're in an overlay that uses animation
|
|
506
|
+
if (this.hasActiveMembers || this.overlay) {
|
|
507
|
+
this.invalidate();
|
|
508
|
+
this.onRequestRender?.();
|
|
509
|
+
}
|
|
203
510
|
}, FRAME_INTERVAL_MS);
|
|
204
511
|
}
|
|
512
|
+
stopTimers() {
|
|
513
|
+
if (this.animationFrame !== undefined) {
|
|
514
|
+
clearInterval(this.animationFrame);
|
|
515
|
+
this.animationFrame = undefined;
|
|
516
|
+
}
|
|
517
|
+
if (this.debounceTimer !== undefined) {
|
|
518
|
+
clearTimeout(this.debounceTimer);
|
|
519
|
+
this.debounceTimer = undefined;
|
|
520
|
+
}
|
|
521
|
+
if (this.pollTimer !== undefined) {
|
|
522
|
+
clearTimeout(this.pollTimer);
|
|
523
|
+
this.pollTimer = undefined;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
205
526
|
}
|
|
206
527
|
// ---------------------------------------------------------------------------
|
|
207
528
|
// Rendering helpers
|
|
208
529
|
// ---------------------------------------------------------------------------
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
function resolveOverallStatus(state) {
|
|
225
|
-
if (state.queued === 0 && state.active === 0) {
|
|
226
|
-
if (state.failed > 0)
|
|
227
|
-
return FAILED_LABEL;
|
|
228
|
-
return COMPLETED_LABEL;
|
|
530
|
+
function eventIcon(type) {
|
|
531
|
+
switch (type) {
|
|
532
|
+
case "started":
|
|
533
|
+
return "\u25B6"; // ▶
|
|
534
|
+
case "completed":
|
|
535
|
+
return "\u2713"; // ✓
|
|
536
|
+
case "failed":
|
|
537
|
+
return "\u2717"; // ✗
|
|
538
|
+
case "tool_execution":
|
|
539
|
+
return "\u2699"; // ⚙
|
|
540
|
+
case "suspended":
|
|
541
|
+
return "\u23F3"; // ⏳
|
|
542
|
+
case "phase_change":
|
|
543
|
+
return "\u2192"; // →
|
|
229
544
|
}
|
|
230
|
-
if (state.active > 0)
|
|
231
|
-
return WORKING_LABEL;
|
|
232
|
-
return ORCHESTRATING_LABEL;
|
|
233
545
|
}
|
|
234
|
-
function
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
const remaining = "\u2501".repeat(remainingChars);
|
|
242
|
-
return done + remaining;
|
|
243
|
-
}
|
|
244
|
-
function renderMemberRow(member, width) {
|
|
245
|
-
const indexLabel = `#${String(member.index)}`;
|
|
246
|
-
const brailleBar = renderBrailleBar(member, BRAILLE_BAR_MAX_WIDTH);
|
|
247
|
-
const statusLabel = memberPhaseLabel(member.phase);
|
|
248
|
-
const itemLabel = (member.item ?? "").slice(0, TEXT_CELL_PREFERRED_WIDTH);
|
|
249
|
-
// Layout: #N [braille] Status item
|
|
250
|
-
const fixed = `${indexLabel} ${brailleBar} ${statusLabel}`;
|
|
546
|
+
function renderMemberRow(member, width, frameIndex, selected) {
|
|
547
|
+
const prefix = selected ? ">" : " ";
|
|
548
|
+
const indexLabel = `#${String(member.index).padStart(2, "0")}`;
|
|
549
|
+
const icon = memberIcon(member, frameIndex);
|
|
550
|
+
const statusLabel = shortPhaseLabel(member.phase);
|
|
551
|
+
// Layout with optional activity column
|
|
552
|
+
const fixed = `${prefix}${indexLabel} ${icon} ${statusLabel}`;
|
|
251
553
|
const fixedLen = visibleLen(fixed);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
554
|
+
let remaining = Math.max(0, width - fixedLen - 2);
|
|
555
|
+
// Show current tool if available
|
|
556
|
+
let activitySuffix = "";
|
|
557
|
+
if (member.currentTool && member.phase === "working") {
|
|
558
|
+
const toolInfo = member.activity
|
|
559
|
+
? `${member.currentTool}: ${member.activity}`
|
|
560
|
+
: member.currentTool;
|
|
561
|
+
if (toolInfo.length < remaining - 2) {
|
|
562
|
+
activitySuffix = ` ${truncateText(toolInfo, Math.min(30, remaining - 2))}`;
|
|
563
|
+
remaining -= visibleLen(activitySuffix);
|
|
564
|
+
}
|
|
255
565
|
}
|
|
256
|
-
const
|
|
257
|
-
const truncatedItem = truncateText(itemLabel,
|
|
258
|
-
return `${fixed} ${truncatedItem}`;
|
|
566
|
+
const itemLabel = member.item ?? "";
|
|
567
|
+
const truncatedItem = truncateText(itemLabel, Math.max(0, remaining));
|
|
568
|
+
return `${fixed} ${truncatedItem}${activitySuffix}`;
|
|
259
569
|
}
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
const elapsed = member.phaseStartedAt
|
|
263
|
-
? Date.now() - member.phaseStartedAt
|
|
264
|
-
: 0;
|
|
265
|
-
switch (phase) {
|
|
570
|
+
function memberIcon(member, frameIndex) {
|
|
571
|
+
switch (member.phase) {
|
|
266
572
|
case "completed":
|
|
267
|
-
return BRAILLE_FULL.repeat(
|
|
573
|
+
return BRAILLE_FULL.repeat(2);
|
|
268
574
|
case "failed":
|
|
269
575
|
case "cancelled":
|
|
576
|
+
return "\u2717 ";
|
|
270
577
|
case "pending":
|
|
271
578
|
case "queued":
|
|
272
|
-
return
|
|
579
|
+
return "\u25CB ";
|
|
273
580
|
case "prompting":
|
|
274
581
|
case "working":
|
|
275
582
|
case "suspended":
|
|
276
|
-
return
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
function animatedBrailleBar(maxWidth, elapsedMs) {
|
|
280
|
-
const cycleMs = 800; // One full animation cycle
|
|
281
|
-
const progress = (elapsedMs % cycleMs) / cycleMs; // 0..1
|
|
282
|
-
// Fill from left to right
|
|
283
|
-
const totalDots = maxWidth * 6; // 6 dots per braille char
|
|
284
|
-
const filledDots = Math.floor(progress * totalDots);
|
|
285
|
-
let result = "";
|
|
286
|
-
for (let i = 0; i < maxWidth; i += 1) {
|
|
287
|
-
const cellStart = i * 6;
|
|
288
|
-
const dotsInCell = Math.max(0, Math.min(6, filledDots - cellStart));
|
|
289
|
-
result += dotsInCell === 0 ? BRAILLE_EMPTY : BRAILLE_LEVELS[dotsInCell];
|
|
583
|
+
return BRAILLE_SPINNER[frameIndex % BRAILLE_SPINNER.length] + " ";
|
|
290
584
|
}
|
|
291
|
-
return result;
|
|
292
585
|
}
|
|
293
|
-
function
|
|
586
|
+
function shortPhaseLabel(phase) {
|
|
294
587
|
switch (phase) {
|
|
295
588
|
case "pending":
|
|
296
589
|
case "queued":
|
|
297
|
-
return
|
|
590
|
+
return "wait";
|
|
298
591
|
case "prompting":
|
|
299
|
-
return "
|
|
592
|
+
return "init";
|
|
300
593
|
case "working":
|
|
301
|
-
return
|
|
594
|
+
return "work";
|
|
302
595
|
case "completed":
|
|
303
|
-
return
|
|
596
|
+
return "done";
|
|
304
597
|
case "failed":
|
|
305
|
-
return
|
|
598
|
+
return "fail";
|
|
306
599
|
case "cancelled":
|
|
307
|
-
return
|
|
600
|
+
return "abort";
|
|
308
601
|
case "suspended":
|
|
309
|
-
return "
|
|
602
|
+
return "retry";
|
|
310
603
|
}
|
|
311
604
|
}
|
|
312
|
-
function
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
605
|
+
function buildFooter(state, width) {
|
|
606
|
+
const usage = state.totalUsage ?? {
|
|
607
|
+
input: 0,
|
|
608
|
+
output: 0,
|
|
609
|
+
cacheRead: 0,
|
|
610
|
+
cacheWrite: 0,
|
|
611
|
+
totalTokens: 0,
|
|
612
|
+
};
|
|
613
|
+
const counts = `${state.completed + state.failed}/${state.total} ag`;
|
|
614
|
+
const tokens = `${Math.round(usage.input)}in/${Math.round(usage.output)}out`;
|
|
615
|
+
const elapsed = formatElapsed(Date.now() - state.startedAt);
|
|
616
|
+
const parts = [counts, tokens, elapsed];
|
|
316
617
|
if (state.failed > 0)
|
|
317
|
-
parts.
|
|
618
|
+
parts.splice(1, 0, `${state.failed}fail`);
|
|
318
619
|
if (state.active > 0)
|
|
319
|
-
parts.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
620
|
+
parts.splice(1, 0, `${state.active}act`);
|
|
621
|
+
// ETA
|
|
622
|
+
if (state.estimatedRemainingMs !== undefined &&
|
|
623
|
+
state.estimatedRemainingMs > 0) {
|
|
624
|
+
parts.push(formatElapsed(state.estimatedRemainingMs));
|
|
625
|
+
}
|
|
626
|
+
const full = parts.join(" | ");
|
|
323
627
|
if (full.length <= width)
|
|
324
628
|
return full;
|
|
325
|
-
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`;
|
|
326
638
|
}
|
|
327
639
|
// ---------------------------------------------------------------------------
|
|
328
|
-
// Text utilities
|
|
640
|
+
// Text utilities
|
|
329
641
|
// ---------------------------------------------------------------------------
|
|
330
642
|
function visibleLen(text) {
|
|
331
|
-
// Simple implementation: count characters, stripping ANSI escapes
|
|
332
643
|
return text.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
333
644
|
}
|
|
334
645
|
function truncateText(text, maxLen) {
|
|
335
646
|
if (text.length <= maxLen)
|
|
336
647
|
return text;
|
|
648
|
+
if (maxLen <= 1)
|
|
649
|
+
return text.slice(0, 1);
|
|
337
650
|
return text.slice(0, Math.max(0, maxLen - 1)) + "\u2026";
|
|
338
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
|
+
}
|
|
339
657
|
//# sourceMappingURL=progress.js.map
|