@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
package/dist/tui/progress.js
CHANGED
|
@@ -1,38 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tui/progress — AgentSwarm live progress panel.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Vertical panel layout with fixed-width tool-call-based braille progress
|
|
5
|
+
* bars and inline activity text. Each agent renders as a single line:
|
|
6
|
+
* 001 [braille bar] read: src/lib.rs lines 42-99
|
|
7
|
+
* Bar width is fixed (5 cells) so tool labels align across agents.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
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
|
|
9
|
+
* Progress is driven by actual tool calls / activity events (progressTick),
|
|
10
|
+
* not wall-clock time. An agent that makes more progress fills faster.
|
|
15
11
|
*
|
|
16
|
-
*
|
|
12
|
+
* For 5+ agents, switches to a 2-column compact grid (3-cell bars).
|
|
17
13
|
*/
|
|
18
|
-
import { matchesKey, Key, isKeyRepeat } from "@earendil-works/pi-tui";
|
|
19
14
|
// ---------------------------------------------------------------------------
|
|
20
15
|
// Constants
|
|
21
16
|
// ---------------------------------------------------------------------------
|
|
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. */
|
|
36
17
|
const BRAILLE_LEVELS = [
|
|
37
18
|
"\u28C0",
|
|
38
19
|
"\u28C4",
|
|
@@ -42,36 +23,21 @@ const BRAILLE_LEVELS = [
|
|
|
42
23
|
"\u28F7",
|
|
43
24
|
"\u28FF",
|
|
44
25
|
];
|
|
45
|
-
const BRAILLE_EMPTY =
|
|
46
|
-
const BRAILLE_FULL = BRAILLE_LEVELS[6];
|
|
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. */
|
|
26
|
+
const BRAILLE_EMPTY = "\u28C0"; // baseline empty (bottom dots), so bar track is always visible
|
|
53
27
|
const DEBOUNCE_MS = 75;
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
28
|
+
const POLL_MS = 800;
|
|
29
|
+
const MAX_VISIBLE_MEMBERS = 20;
|
|
30
|
+
const ID_WIDTH = 3;
|
|
31
|
+
// Fixed-width braille bar — all agents share the same width so labels align.
|
|
32
|
+
// Vertical mode: 5 cells, grid mode: 3 cells.
|
|
33
|
+
const FIXED_BAR_CELLS = 5;
|
|
34
|
+
const GRID_BAR_CELLS = 3;
|
|
35
|
+
// Layout: agents shown vertically when count <= this
|
|
36
|
+
const VERTICAL_LAYOUT_MAX = 4;
|
|
60
37
|
// ---------------------------------------------------------------------------
|
|
61
38
|
// Snapshot conversion
|
|
62
39
|
// ---------------------------------------------------------------------------
|
|
63
40
|
export function snapshotToProgressState(snapshot, title) {
|
|
64
|
-
const now = Date.now();
|
|
65
|
-
const members = snapshot.members.map((m) => ({
|
|
66
|
-
index: m.index,
|
|
67
|
-
phase: mapMemberPhase(m.phase),
|
|
68
|
-
item: m.item,
|
|
69
|
-
error: m.error,
|
|
70
|
-
phaseStartedAt: isAnimatedPhase(m.phase) ? now : undefined,
|
|
71
|
-
currentTool: m.currentTool,
|
|
72
|
-
activity: m.activity,
|
|
73
|
-
usage: m.usage,
|
|
74
|
-
}));
|
|
75
41
|
return {
|
|
76
42
|
title,
|
|
77
43
|
total: snapshot.total,
|
|
@@ -79,11 +45,19 @@ export function snapshotToProgressState(snapshot, title) {
|
|
|
79
45
|
failed: snapshot.failed,
|
|
80
46
|
active: snapshot.active,
|
|
81
47
|
queued: snapshot.queued,
|
|
82
|
-
members
|
|
48
|
+
members: snapshot.members.map((m) => ({
|
|
49
|
+
index: m.index,
|
|
50
|
+
phase: mapMemberPhase(m.phase),
|
|
51
|
+
item: m.item,
|
|
52
|
+
error: m.error,
|
|
53
|
+
currentTool: m.currentTool,
|
|
54
|
+
activity: m.activity,
|
|
55
|
+
usage: m.usage,
|
|
56
|
+
progressTick: m.progressTick,
|
|
57
|
+
startedAt: m.startedAt,
|
|
58
|
+
})),
|
|
83
59
|
totalUsage: snapshot.totalUsage,
|
|
84
|
-
startedAt: snapshot.startedAt ?? now,
|
|
85
|
-
estimatedRemainingMs: snapshot.estimatedRemainingMs,
|
|
86
|
-
eventLog: snapshot.eventLog ? [...snapshot.eventLog] : undefined,
|
|
60
|
+
startedAt: snapshot.startedAt ?? Date.now(),
|
|
87
61
|
};
|
|
88
62
|
}
|
|
89
63
|
function mapMemberPhase(phase) {
|
|
@@ -100,44 +74,23 @@ function mapMemberPhase(phase) {
|
|
|
100
74
|
return "suspended";
|
|
101
75
|
}
|
|
102
76
|
}
|
|
103
|
-
function isAnimatedPhase(phase) {
|
|
104
|
-
return phase === "working" || phase === "suspended";
|
|
105
|
-
}
|
|
106
77
|
// ---------------------------------------------------------------------------
|
|
107
78
|
// Component
|
|
108
79
|
// ---------------------------------------------------------------------------
|
|
109
80
|
export class AgentSwarmProgressComponent {
|
|
110
81
|
state_ = null;
|
|
111
|
-
renderedWidth;
|
|
112
|
-
cachedLines;
|
|
113
82
|
onRequestRender;
|
|
114
|
-
// Render scheduler
|
|
115
|
-
animationFrame;
|
|
116
|
-
frameIndex = 0;
|
|
117
83
|
pollTimer;
|
|
118
84
|
debounceTimer;
|
|
119
|
-
pendingInvalidate = false;
|
|
120
|
-
hasActiveMembers = false;
|
|
121
|
-
// Input / UI state
|
|
122
|
-
scrollOffset = 0;
|
|
123
|
-
selectedIndex = -1;
|
|
124
|
-
activePanel = "members";
|
|
125
|
-
overlay = null;
|
|
126
85
|
constructor(onRequestRender) {
|
|
127
86
|
this.onRequestRender = onRequestRender;
|
|
128
87
|
this.startPolling();
|
|
129
|
-
this.startAnimation();
|
|
130
88
|
}
|
|
131
89
|
// -------------------------------------------------------------------
|
|
132
90
|
// Public API
|
|
133
91
|
// -------------------------------------------------------------------
|
|
134
92
|
update(state) {
|
|
135
93
|
this.state_ = state;
|
|
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
94
|
this.requestRender();
|
|
142
95
|
}
|
|
143
96
|
complete() {
|
|
@@ -147,14 +100,12 @@ export class AgentSwarmProgressComponent {
|
|
|
147
100
|
m.phase !== "failed" &&
|
|
148
101
|
m.phase !== "cancelled") {
|
|
149
102
|
m.phase = "completed";
|
|
150
|
-
m.phaseStartedAt = Date.now();
|
|
151
103
|
}
|
|
152
104
|
}
|
|
153
105
|
this.state_.active = 0;
|
|
154
106
|
this.state_.queued = 0;
|
|
155
107
|
this.state_.completed = this.state_.total - this.state_.failed;
|
|
156
108
|
}
|
|
157
|
-
this.hasActiveMembers = false;
|
|
158
109
|
this.requestRender();
|
|
159
110
|
}
|
|
160
111
|
dispose() {
|
|
@@ -162,358 +113,261 @@ export class AgentSwarmProgressComponent {
|
|
|
162
113
|
this.onRequestRender = undefined;
|
|
163
114
|
}
|
|
164
115
|
invalidate() {
|
|
165
|
-
|
|
166
|
-
this.cachedLines = undefined;
|
|
116
|
+
/* no-op */
|
|
167
117
|
}
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
}
|
|
118
|
+
handleInput(_data) {
|
|
119
|
+
/* minimal: no keyboard */
|
|
250
120
|
}
|
|
251
121
|
// -------------------------------------------------------------------
|
|
252
122
|
// Rendering
|
|
253
123
|
// -------------------------------------------------------------------
|
|
254
124
|
render(width) {
|
|
255
125
|
const safeWidth = Math.max(20, width);
|
|
256
|
-
if (this.
|
|
257
|
-
return
|
|
258
|
-
}
|
|
259
|
-
if (!this.state_ || this.state_.members.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
|
-
}
|
|
126
|
+
if (!this.state_ || this.state_.members.length === 0)
|
|
127
|
+
return [];
|
|
269
128
|
const state = this.state_;
|
|
270
|
-
const contentWidth = safeWidth - 4;
|
|
271
129
|
const lines = [];
|
|
272
|
-
// Header:
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
lines
|
|
285
|
-
if (this.activePanel === "members") {
|
|
286
|
-
this.renderMembersPanel(lines, state, contentWidth);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
this.renderEventsPanel(lines, state, contentWidth);
|
|
290
|
-
}
|
|
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}`);
|
|
297
|
-
// Bottom border
|
|
298
|
-
const bottom = `\u2514${"\u2500".repeat(contentWidth)}\u2518`;
|
|
299
|
-
lines.push(bottom);
|
|
300
|
-
this.cachedLines = lines;
|
|
301
|
-
this.renderedWidth = safeWidth;
|
|
302
|
-
return this.cachedLines;
|
|
130
|
+
// Header: ─ Agent Swarm ─ <title> ──────────
|
|
131
|
+
lines.push(this.renderHeader(safeWidth, state));
|
|
132
|
+
lines.push("");
|
|
133
|
+
// Agent panels (vertical or compact grid)
|
|
134
|
+
const memberLines = this.renderAgentPanels(safeWidth - 2, state);
|
|
135
|
+
lines.push(...memberLines);
|
|
136
|
+
lines.push("");
|
|
137
|
+
// Bottom separator
|
|
138
|
+
const sepWidth = safeWidth - 2;
|
|
139
|
+
lines.push(truncateText(repeatStr("\u2500", sepWidth), sepWidth));
|
|
140
|
+
// Status line: Working... N/M (P%) elapsed
|
|
141
|
+
lines.push(this.renderStatusLine(safeWidth, state));
|
|
142
|
+
return lines;
|
|
303
143
|
}
|
|
304
144
|
// -------------------------------------------------------------------
|
|
305
|
-
//
|
|
145
|
+
// Header: ─ Agent Swarm ─ <desc> ──────────
|
|
306
146
|
// -------------------------------------------------------------------
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
}
|
|
147
|
+
renderHeader(width, state) {
|
|
148
|
+
const mode = state.mailbox ? "Swarm Team" : "Agent Swarm";
|
|
149
|
+
const desc = state.title ?? "";
|
|
150
|
+
const mailboxInfo = state.mailboxCount && state.mailboxCount > 0
|
|
151
|
+
? ` | Mailbox: ${state.mailboxCount}`
|
|
152
|
+
: "";
|
|
153
|
+
const prefix = `\u2500 ${mode}`;
|
|
154
|
+
const content = desc ? ` \u2500 ${desc}` : "";
|
|
155
|
+
const label = `${prefix}${content}${mailboxInfo}`;
|
|
156
|
+
const suffixLen = Math.max(0, width - visibleLen(label) - 2);
|
|
157
|
+
const suffix = suffixLen > 0 ? ` \u2500${repeatStr("\u2500", suffixLen)}` : "";
|
|
158
|
+
return truncateText(`${label}${suffix}`, width);
|
|
328
159
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const detail = truncateText(event.detail, contentWidth - 12);
|
|
340
|
-
lines.push(` ${icon} ${time} ${detail}`);
|
|
341
|
-
}
|
|
160
|
+
// -------------------------------------------------------------------
|
|
161
|
+
// Status line
|
|
162
|
+
// -------------------------------------------------------------------
|
|
163
|
+
renderStatusLine(width, state) {
|
|
164
|
+
const done = state.completed + state.failed;
|
|
165
|
+
const total = state.total;
|
|
166
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
167
|
+
const elapsed = formatElapsed(Date.now() - state.startedAt);
|
|
168
|
+
const label = state.active > 0 ? "Working..." : "Completed";
|
|
169
|
+
return truncateText(`${label} ${done}/${total} (${pct}%) ${elapsed}`, width);
|
|
342
170
|
}
|
|
343
171
|
// -------------------------------------------------------------------
|
|
344
|
-
//
|
|
172
|
+
// Agent panel layout selection
|
|
345
173
|
// -------------------------------------------------------------------
|
|
346
|
-
|
|
347
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Render all agents. Uses vertical panels for small counts
|
|
176
|
+
* and a compact 2-column grid for larger batches.
|
|
177
|
+
*/
|
|
178
|
+
renderAgentPanels(width, state) {
|
|
179
|
+
const count = Math.min(state.members.length, MAX_VISIBLE_MEMBERS);
|
|
180
|
+
if (count <= 0)
|
|
348
181
|
return [];
|
|
349
|
-
if (
|
|
350
|
-
return this.
|
|
182
|
+
if (count <= VERTICAL_LAYOUT_MAX) {
|
|
183
|
+
return this.renderVerticalPanels(width, state);
|
|
351
184
|
}
|
|
352
|
-
return this.
|
|
185
|
+
return this.renderCompactGrid(width, state);
|
|
353
186
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
187
|
+
// -------------------------------------------------------------------
|
|
188
|
+
// Vertical panel layout (1-4 agents)
|
|
189
|
+
//
|
|
190
|
+
// Each agent is a single line with live activity inline:
|
|
191
|
+
// 001 [▓▓▓▓▓░░░] read: src/lib.rs lines 42-99
|
|
192
|
+
// (blank line between agents)
|
|
193
|
+
// -------------------------------------------------------------------
|
|
194
|
+
renderVerticalPanels(width, state) {
|
|
195
|
+
const count = Math.min(state.members.length, MAX_VISIBLE_MEMBERS);
|
|
357
196
|
const lines = [];
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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`);
|
|
197
|
+
for (let i = 0; i < count; i++) {
|
|
198
|
+
const member = state.members[i];
|
|
199
|
+
lines.push(this.renderAgentLine(member, width));
|
|
200
|
+
if (i < count - 1) {
|
|
201
|
+
lines.push(""); // blank line separator between agents
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (state.members.length > MAX_VISIBLE_MEMBERS) {
|
|
205
|
+
lines.push(` ... ${state.members.length - MAX_VISIBLE_MEMBERS} more`);
|
|
374
206
|
}
|
|
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
207
|
return lines;
|
|
379
208
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Render one agent as a single line with fixed-width progress bar.
|
|
211
|
+
*
|
|
212
|
+
* Format: `001 [braille bar] read: src/lib.rs lines 42-99`
|
|
213
|
+
* Bar is always FIXED_BAR_CELLS (5) wide so tool labels align across agents.
|
|
214
|
+
*
|
|
215
|
+
* Progress is driven by progressTick (actual tool calls / activity events),
|
|
216
|
+
* not wall-clock time. Each tick fills one level of the braille bar.
|
|
217
|
+
*/
|
|
218
|
+
renderAgentLine(member, width) {
|
|
219
|
+
const id = String(member.index).padStart(ID_WIDTH, "0");
|
|
220
|
+
// Fixed bar width — ensures all agent labels align
|
|
221
|
+
const barCells = FIXED_BAR_CELLS;
|
|
222
|
+
// Label gets the remaining space after id + bar + fixed gaps
|
|
223
|
+
const labelWidth = Math.max(4, width - ID_WIDTH - barCells - 4);
|
|
224
|
+
const bar = this.renderBrailleBar(member, barCells);
|
|
225
|
+
const label = this.renderCellLabel(member, labelWidth);
|
|
226
|
+
return truncateText(`${id} ${bar} ${label}`, width);
|
|
227
|
+
}
|
|
228
|
+
// -------------------------------------------------------------------
|
|
229
|
+
// Compact grid layout (5+ agents)
|
|
230
|
+
//
|
|
231
|
+
// 2 columns, single-line cells:
|
|
232
|
+
// 001 [▓▓▓] task 002 [▓▓▓] task
|
|
233
|
+
// 003 [▓▓▓] task 004 [▓▓▓] task
|
|
234
|
+
// -------------------------------------------------------------------
|
|
235
|
+
renderCompactGrid(width, state) {
|
|
236
|
+
const cols = 2;
|
|
237
|
+
const count = Math.min(state.members.length, MAX_VISIBLE_MEMBERS);
|
|
238
|
+
const gap = 3; // spaces between columns
|
|
239
|
+
const cellWidth = Math.floor((width - gap) / cols);
|
|
240
|
+
const rows = Math.ceil(count / cols);
|
|
383
241
|
const lines = [];
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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 });
|
|
242
|
+
for (let row = 0; row < rows; row++) {
|
|
243
|
+
let line = "";
|
|
244
|
+
for (let col = 0; col < cols; col++) {
|
|
245
|
+
const idx = row * cols + col;
|
|
246
|
+
if (idx >= count)
|
|
247
|
+
break;
|
|
248
|
+
const member = state.members[idx];
|
|
249
|
+
const cw = col < cols - 1 ? cellWidth : width - line.length;
|
|
250
|
+
const cell = this.renderGridCell(member, cw);
|
|
251
|
+
line += cell;
|
|
252
|
+
if (col < cols - 1) {
|
|
253
|
+
// Pad to fill the remaining cell width + gap
|
|
254
|
+
const padLen = Math.max(0, cellWidth - visibleLen(cell) + gap);
|
|
255
|
+
line += " ".repeat(padLen);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
lines.push(truncateText(line, width));
|
|
414
259
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
lines.push(` \u2502 ${padRight(line, safeWidth - 4)}\u2502`);
|
|
260
|
+
if (state.members.length > MAX_VISIBLE_MEMBERS) {
|
|
261
|
+
lines.push(` ... ${state.members.length - MAX_VISIBLE_MEMBERS} more`);
|
|
418
262
|
}
|
|
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
263
|
return lines;
|
|
423
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Compact single-line grid cell.
|
|
267
|
+
* Format: `001 [▓▓] label`
|
|
268
|
+
*/
|
|
269
|
+
renderGridCell(member, width) {
|
|
270
|
+
const id = String(member.index).padStart(ID_WIDTH, "0");
|
|
271
|
+
// Compact grid uses fewer bar cells
|
|
272
|
+
const barCells = GRID_BAR_CELLS;
|
|
273
|
+
const bar = this.renderBrailleBar(member, barCells);
|
|
274
|
+
const labelWidth = Math.max(2, width - ID_WIDTH - barCells - 4);
|
|
275
|
+
const label = this.renderCellLabel(member, labelWidth);
|
|
276
|
+
return `${id} ${bar} ${label}`;
|
|
277
|
+
}
|
|
424
278
|
// -------------------------------------------------------------------
|
|
425
|
-
//
|
|
279
|
+
// Braille progress bar
|
|
280
|
+
//
|
|
281
|
+
// Tool-call-based progress for working agents:
|
|
282
|
+
// - Each progressTick (tool call / activity event) fills one level.
|
|
283
|
+
// - Bar fills up as the agent works; capped at 85% so completed
|
|
284
|
+
// agents (full bar) are visually distinguishable from working ones.
|
|
285
|
+
// - Empty cells show the baseline character so the bar track is always visible.
|
|
286
|
+
//
|
|
287
|
+
// Completed agents: full bar
|
|
288
|
+
// Failed agents: half bar
|
|
289
|
+
// Queued/suspended: empty bar (baseline only)
|
|
426
290
|
// -------------------------------------------------------------------
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
return;
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
291
|
+
renderBrailleBar(member, width) {
|
|
292
|
+
if (width <= 0)
|
|
293
|
+
return "";
|
|
294
|
+
const capacity = width * BRAILLE_LEVELS.length;
|
|
295
|
+
let ticks;
|
|
296
|
+
if (member.phase === "completed") {
|
|
297
|
+
ticks = capacity;
|
|
298
|
+
}
|
|
299
|
+
else if (member.phase === "failed") {
|
|
300
|
+
ticks = Math.floor(capacity / 2);
|
|
301
|
+
}
|
|
302
|
+
else if (member.phase === "working" || member.phase === "prompting") {
|
|
303
|
+
// Fill based on actual tool-call progress.
|
|
304
|
+
// Each tool execution or model output event increments progressTick by 1.
|
|
305
|
+
// Cap at 85% so "almost done" is visually distinct from "completed".
|
|
306
|
+
ticks = Math.min(member.progressTick ?? 0, Math.floor(capacity * 0.85));
|
|
438
307
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
308
|
+
else {
|
|
309
|
+
ticks = 0;
|
|
310
|
+
}
|
|
311
|
+
const fullBars = Math.floor(ticks / BRAILLE_LEVELS.length);
|
|
312
|
+
const partial = ticks % BRAILLE_LEVELS.length;
|
|
313
|
+
const partialChar = partial > 0 ? BRAILLE_LEVELS[partial - 1] : "";
|
|
314
|
+
let bar = "";
|
|
315
|
+
for (let i = 0; i < width; i++) {
|
|
316
|
+
if (i < fullBars) {
|
|
317
|
+
bar += BRAILLE_LEVELS[BRAILLE_LEVELS.length - 1]; // Full cell
|
|
318
|
+
}
|
|
319
|
+
else if (i === fullBars && partialChar) {
|
|
320
|
+
bar += partialChar;
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
bar += BRAILLE_EMPTY;
|
|
324
|
+
}
|
|
447
325
|
}
|
|
448
|
-
|
|
326
|
+
return bar;
|
|
327
|
+
}
|
|
328
|
+
// -------------------------------------------------------------------
|
|
329
|
+
// Cell label rendering
|
|
330
|
+
// -------------------------------------------------------------------
|
|
331
|
+
renderCellLabel(member, width) {
|
|
332
|
+
if (width <= 0)
|
|
333
|
+
return "";
|
|
334
|
+
if (member.phase === "working" || member.phase === "prompting") {
|
|
335
|
+
// Show tool: activity_text (scrolling model output / shell command)
|
|
336
|
+
const toolPart = member.currentTool ? `${member.currentTool}: ` : "";
|
|
337
|
+
const activityText = member.activity ?? member.item ?? "";
|
|
338
|
+
const text = toolPart + activityText;
|
|
339
|
+
return truncateText(text, width);
|
|
340
|
+
}
|
|
341
|
+
if (member.phase === "completed")
|
|
342
|
+
return truncateText("ok", width);
|
|
343
|
+
if (member.phase === "failed" && member.error)
|
|
344
|
+
return truncateText(member.error, Math.min(width, 20));
|
|
345
|
+
if (member.phase === "queued" || member.phase === "suspended")
|
|
346
|
+
return truncateText(member.item ?? "...", width);
|
|
347
|
+
return "";
|
|
449
348
|
}
|
|
450
349
|
// -------------------------------------------------------------------
|
|
451
350
|
// Render scheduling
|
|
452
351
|
// -------------------------------------------------------------------
|
|
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
352
|
requestRender() {
|
|
459
|
-
this.invalidate();
|
|
460
353
|
if (this.debounceTimer !== undefined) {
|
|
461
|
-
this.pendingInvalidate = true;
|
|
462
354
|
return;
|
|
463
355
|
}
|
|
464
356
|
this.debounceTimer = setTimeout(() => {
|
|
465
357
|
this.debounceTimer = undefined;
|
|
466
|
-
this.pendingInvalidate = false;
|
|
467
358
|
this.onRequestRender?.();
|
|
468
|
-
this.updatePollingInterval();
|
|
469
359
|
}, DEBOUNCE_MS);
|
|
470
|
-
// Also trigger an immediate render for faster response to inputs
|
|
471
360
|
this.onRequestRender?.();
|
|
472
361
|
}
|
|
473
|
-
/**
|
|
474
|
-
* Start the fallback polling timer. Falls back to polling when no
|
|
475
|
-
* state changes trigger requests, ensuring the spinner animates.
|
|
476
|
-
*/
|
|
477
362
|
startPolling() {
|
|
478
|
-
this.schedulePoll();
|
|
479
|
-
}
|
|
480
|
-
schedulePoll() {
|
|
481
|
-
const interval = this.hasActiveMembers ? ACTIVE_POLL_MS : IDLE_POLL_MS;
|
|
482
363
|
this.pollTimer = setTimeout(() => {
|
|
483
364
|
this.pollTimer = undefined;
|
|
484
|
-
|
|
485
|
-
if (this.state_) {
|
|
486
|
-
this.invalidate();
|
|
365
|
+
if (this.state_)
|
|
487
366
|
this.onRequestRender?.();
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
*/
|
|
501
|
-
startAnimation() {
|
|
502
|
-
this.animationFrame = setInterval(() => {
|
|
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
|
-
}
|
|
510
|
-
}, FRAME_INTERVAL_MS);
|
|
367
|
+
this.startPolling();
|
|
368
|
+
}, POLL_MS);
|
|
511
369
|
}
|
|
512
370
|
stopTimers() {
|
|
513
|
-
if (this.animationFrame !== undefined) {
|
|
514
|
-
clearInterval(this.animationFrame);
|
|
515
|
-
this.animationFrame = undefined;
|
|
516
|
-
}
|
|
517
371
|
if (this.debounceTimer !== undefined) {
|
|
518
372
|
clearTimeout(this.debounceTimer);
|
|
519
373
|
this.debounceTimer = undefined;
|
|
@@ -525,108 +379,13 @@ export class AgentSwarmProgressComponent {
|
|
|
525
379
|
}
|
|
526
380
|
}
|
|
527
381
|
// ---------------------------------------------------------------------------
|
|
528
|
-
//
|
|
382
|
+
// Text utilities
|
|
529
383
|
// ---------------------------------------------------------------------------
|
|
530
|
-
function
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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"; // →
|
|
544
|
-
}
|
|
545
|
-
}
|
|
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}`;
|
|
553
|
-
const fixedLen = visibleLen(fixed);
|
|
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
|
-
}
|
|
565
|
-
}
|
|
566
|
-
const itemLabel = member.item ?? "";
|
|
567
|
-
const truncatedItem = truncateText(itemLabel, Math.max(0, remaining));
|
|
568
|
-
return `${fixed} ${truncatedItem}${activitySuffix}`;
|
|
569
|
-
}
|
|
570
|
-
function memberIcon(member, frameIndex) {
|
|
571
|
-
switch (member.phase) {
|
|
572
|
-
case "completed":
|
|
573
|
-
return BRAILLE_FULL.repeat(2);
|
|
574
|
-
case "failed":
|
|
575
|
-
case "cancelled":
|
|
576
|
-
return "\u2717 ";
|
|
577
|
-
case "pending":
|
|
578
|
-
case "queued":
|
|
579
|
-
return "\u25CB ";
|
|
580
|
-
case "prompting":
|
|
581
|
-
case "working":
|
|
582
|
-
case "suspended":
|
|
583
|
-
return BRAILLE_SPINNER[frameIndex % BRAILLE_SPINNER.length] + " ";
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
function shortPhaseLabel(phase) {
|
|
587
|
-
switch (phase) {
|
|
588
|
-
case "pending":
|
|
589
|
-
case "queued":
|
|
590
|
-
return "wait";
|
|
591
|
-
case "prompting":
|
|
592
|
-
return "init";
|
|
593
|
-
case "working":
|
|
594
|
-
return "work";
|
|
595
|
-
case "completed":
|
|
596
|
-
return "done";
|
|
597
|
-
case "failed":
|
|
598
|
-
return "fail";
|
|
599
|
-
case "cancelled":
|
|
600
|
-
return "abort";
|
|
601
|
-
case "suspended":
|
|
602
|
-
return "retry";
|
|
603
|
-
}
|
|
604
|
-
}
|
|
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];
|
|
617
|
-
if (state.failed > 0)
|
|
618
|
-
parts.splice(1, 0, `${state.failed}fail`);
|
|
619
|
-
if (state.active > 0)
|
|
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(" | ");
|
|
627
|
-
if (full.length <= width)
|
|
628
|
-
return full;
|
|
629
|
-
return truncateText(full, width);
|
|
384
|
+
function repeatStr(ch, count) {
|
|
385
|
+
let out = "";
|
|
386
|
+
for (let i = 0; i < count; i++)
|
|
387
|
+
out += ch;
|
|
388
|
+
return out;
|
|
630
389
|
}
|
|
631
390
|
function formatElapsed(ms) {
|
|
632
391
|
const totalSec = Math.floor(ms / 1000);
|
|
@@ -634,24 +393,21 @@ function formatElapsed(ms) {
|
|
|
634
393
|
return `${totalSec}s`;
|
|
635
394
|
const m = Math.floor(totalSec / 60);
|
|
636
395
|
const s = totalSec % 60;
|
|
637
|
-
|
|
396
|
+
if (m < 60)
|
|
397
|
+
return `${m}m ${s}s`;
|
|
398
|
+
const h = Math.floor(m / 60);
|
|
399
|
+
return `${h}h ${m % 60}m ${s}s`;
|
|
638
400
|
}
|
|
639
|
-
// ---------------------------------------------------------------------------
|
|
640
|
-
// Text utilities
|
|
641
|
-
// ---------------------------------------------------------------------------
|
|
642
401
|
function visibleLen(text) {
|
|
643
402
|
return text.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
644
403
|
}
|
|
645
404
|
function truncateText(text, maxLen) {
|
|
405
|
+
if (maxLen <= 0)
|
|
406
|
+
return "";
|
|
646
407
|
if (text.length <= maxLen)
|
|
647
408
|
return text;
|
|
648
409
|
if (maxLen <= 1)
|
|
649
410
|
return text.slice(0, 1);
|
|
650
|
-
return text.slice(0,
|
|
651
|
-
}
|
|
652
|
-
function padRight(text, width) {
|
|
653
|
-
if (text.length >= width)
|
|
654
|
-
return text.slice(0, width);
|
|
655
|
-
return text + " ".repeat(width - text.length);
|
|
411
|
+
return text.slice(0, maxLen - 1) + "\u2026";
|
|
656
412
|
}
|
|
657
413
|
//# sourceMappingURL=progress.js.map
|