@hncode/hncode 0.5.1 → 0.5.4
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/bin/install.cjs +24 -44
- package/bin/preinstall.cjs +0 -14
- package/package.json +2 -2
- package/src/agent-task.js +12 -1
- package/src/agent.js +279 -48
- package/src/cache.js +175 -130
- package/src/cell-buffer.js +5 -0
- package/src/clipboard.js +111 -8
- package/src/context-bar.js +46 -5
- package/src/doctor.js +7 -2
- package/src/git.js +35 -4
- package/src/index.js +29 -5
- package/src/keybindings.js +25 -3
- package/src/llm.js +60 -6
- package/src/mcp.js +419 -384
- package/src/permissions.js +29 -7
- package/src/plugin.js +95 -10
- package/src/registry-browser.js +21 -6
- package/src/remote.js +307 -289
- package/src/session-core.js +203 -0
- package/src/session-host.js +213 -0
- package/src/session-view.js +110 -0
- package/src/settings-schema.js +12 -9
- package/src/shell-safety.js +610 -473
- package/src/swarm-progress.js +348 -293
- package/src/tasks-browser.js +4 -1
- package/src/team/constants.js +20 -0
- package/src/team/context.js +89 -0
- package/src/team/mailbox.js +216 -0
- package/src/team/permission.js +121 -0
- package/src/team/protocol.js +167 -0
- package/src/team/runner.js +462 -0
- package/src/team/spawn.js +305 -0
- package/src/team/store.js +172 -0
- package/src/team/tasks.js +177 -0
- package/src/team/team.js +346 -0
- package/src/team-panel.js +624 -0
- package/src/term-caps.js +457 -410
- package/src/term.js +162 -19
- package/src/tool-result.js +70 -7
- package/src/tools/agent-swarm.js +114 -11
- package/src/tools/agent.js +464 -386
- package/src/tools/bash.js +53 -14
- package/src/tools/diagnostics.js +13 -0
- package/src/tools/edit.js +428 -50
- package/src/tools/git.js +15 -3
- package/src/tools/glob.js +5 -3
- package/src/tools/grep.js +19 -5
- package/src/tools/ignore.js +17 -3
- package/src/tools/index.js +19 -3
- package/src/tools/read.js +2 -1
- package/src/tools/shell.js +145 -134
- package/src/tools/tasks.js +214 -141
- package/src/tools/team.js +411 -0
- package/src/tools/utils.js +12 -3
- package/src/tools/web-search.js +53 -40
- package/src/tui.js +7496 -5366
- package/src/update.js +320 -115
- package/src/web-daemon.js +27 -4
- package/src/web-live.js +250 -0
package/src/swarm-progress.js
CHANGED
|
@@ -1,293 +1,348 @@
|
|
|
1
|
-
// AgentSwarm live progress block, mirroring kimi-code's AgentSwarmProgress.
|
|
2
|
-
//
|
|
3
|
-
// kimi does NOT render a swarm as a tool line plus a text dump. It renders a live
|
|
4
|
-
// BLOCK in the transcript:
|
|
5
|
-
//
|
|
6
|
-
// ─ Agent Swarm ─
|
|
7
|
-
//
|
|
8
|
-
// 001 [#---------------] Working… Inspecting src/tui.js
|
|
9
|
-
// 002 [###-------------] Queued…
|
|
10
|
-
// 003 [################] Completed.
|
|
11
|
-
//
|
|
12
|
-
// Completed. ━━━━━━━━━━━━━━━━━━━━━
|
|
13
|
-
//
|
|
14
|
-
// Structure (all of it from kimi's component):
|
|
15
|
-
// * header - "Agent Swarm" + " ─ " + description + " ─ " + model, padded with ─
|
|
16
|
-
// * grid - one CELL per subagent: an index, a progress bar, a phase label
|
|
17
|
-
// and the latest line of that subagent's output
|
|
18
|
-
// * status - the overall label plus a segmented "pip bar": one ━ per subagent,
|
|
19
|
-
// coloured by that subagent's phase, in STATUS_BAR_ORDER
|
|
20
|
-
//
|
|
21
|
-
// This module is PURE: it takes a snapshot and returns lines. No timers, no
|
|
22
|
-
// state — the TUI owns the member list and calls renderSwarmProgress().
|
|
23
|
-
|
|
24
|
-
import { C } from './colors.js';
|
|
25
|
-
import { visualWidth } from './term.js';
|
|
26
|
-
|
|
27
|
-
const ESC = '\x1b';
|
|
28
|
-
function visWidth(s) { return visualWidth(String(s).replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '')); }
|
|
29
|
-
const ELLIPSIS = '\u2026';
|
|
30
|
-
function col(text, c) { return c + String(text) + C.reset; }
|
|
31
|
-
|
|
32
|
-
// The order kimi lays the pip-bar segments in, so the bar always reads
|
|
33
|
-
// completed-first regardless of which subagent finished when.
|
|
34
|
-
const STATUS_BAR_ORDER = ['completed', 'working', 'suspended', 'queued', 'cancelled', 'failed'];
|
|
35
|
-
|
|
36
|
-
// phase -> (label, colour). kimi's labels, verbatim.
|
|
37
|
-
const PHASE = {
|
|
38
|
-
queued: { label: 'Queued\u2026', color: C.gray },
|
|
39
|
-
prompting: { label: 'Prompting\u2026', color: C.cyan },
|
|
40
|
-
working: { label: 'Working\u2026', color: C.cyan },
|
|
41
|
-
suspended: { label: 'Rate limited\u2026', color: C.yellow },
|
|
42
|
-
completed: { label: 'Completed.', color: C.green },
|
|
43
|
-
failed: { label: 'Failed.', color: C.red },
|
|
44
|
-
cancelled: { label: 'Cancelled.', color: C.gray },
|
|
45
|
-
};
|
|
46
|
-
export function phaseInfo(phase) { return PHASE[phase] || PHASE.working; }
|
|
47
|
-
|
|
48
|
-
const BAR_EMPTY = '-';
|
|
49
|
-
const BAR_FULL = '#';
|
|
50
|
-
const STATUS_BAR_CHAR = '\u2501'; // ━
|
|
51
|
-
const SUCCESS_MARK = '\u2714'; // ✔
|
|
52
|
-
const FAILURE_MARK = '\u2716'; // ✖
|
|
53
|
-
const CANCELLED_MARK = '\u2716';
|
|
54
|
-
|
|
55
|
-
// ---- helpers ---------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
function truncateToWidth(s, width) {
|
|
58
|
-
const str = String(s);
|
|
59
|
-
if (width <= 0) return '';
|
|
60
|
-
if (visWidth(str) <= width) return str;
|
|
61
|
-
let out = '';
|
|
62
|
-
let n = 0;
|
|
63
|
-
let i = 0;
|
|
64
|
-
while (i < str.length) {
|
|
65
|
-
if (str[i] === ESC) {
|
|
66
|
-
const m = /^\x1b\[[0-9;?]*[a-zA-Z]/.exec(str.slice(i));
|
|
67
|
-
if (m) { out += m[0]; i += m[0].length; continue; }
|
|
68
|
-
}
|
|
69
|
-
const cp = str.codePointAt(i);
|
|
70
|
-
const ch = String.fromCodePoint(cp);
|
|
71
|
-
const cw = visualWidth(ch);
|
|
72
|
-
if (n + cw > width - 1) break; // leave a cell for the ellipsis
|
|
73
|
-
n += cw;
|
|
74
|
-
out += ch;
|
|
75
|
-
i += cp > 0xffff ? 2 : 1;
|
|
76
|
-
}
|
|
77
|
-
return out + ELLIPSIS;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function padTo(s, width) {
|
|
81
|
-
const w = visWidth(s);
|
|
82
|
-
return w >= width ? s : s + ' '.repeat(width - w);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function collapseWhitespace(s) {
|
|
86
|
-
return String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ---- the model -> phases ---------------------------------------------------
|
|
90
|
-
|
|
91
|
-
// A background "agent" task carries the swarm in ctx: we track members from the
|
|
92
|
-
// progress lines the AgentSwarm tool appends (`[n/m] finished`) plus, when the
|
|
93
|
-
// caller has richer data, an explicit member list. Members are normalised here so
|
|
94
|
-
// the renderer never has to guess.
|
|
95
|
-
export function normalizeMember(m, i) {
|
|
96
|
-
const src = m || {}; // a half-built member must not crash the grid
|
|
97
|
-
const phase = PHASE[src.phase] ? src.phase : 'queued';
|
|
98
|
-
return {
|
|
99
|
-
index: i + 1,
|
|
100
|
-
phase,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
if (
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
1
|
+
// AgentSwarm live progress block, mirroring kimi-code's AgentSwarmProgress.
|
|
2
|
+
//
|
|
3
|
+
// kimi does NOT render a swarm as a tool line plus a text dump. It renders a live
|
|
4
|
+
// BLOCK in the transcript:
|
|
5
|
+
//
|
|
6
|
+
// ─ Agent Swarm ─ two subagents each replied hi ─ coder
|
|
7
|
+
//
|
|
8
|
+
// 001 [#---------------] Working… Inspecting src/tui.js
|
|
9
|
+
// 002 [###-------------] Queued…
|
|
10
|
+
// 003 [################] Completed.
|
|
11
|
+
//
|
|
12
|
+
// Completed. ━━━━━━━━━━━━━━━━━━━━━
|
|
13
|
+
//
|
|
14
|
+
// Structure (all of it from kimi's component):
|
|
15
|
+
// * header - "Agent Swarm" + " ─ " + description + " ─ " + model, padded with ─
|
|
16
|
+
// * grid - one CELL per subagent: an index, a progress bar, a phase label
|
|
17
|
+
// and the latest line of that subagent's output
|
|
18
|
+
// * status - the overall label plus a segmented "pip bar": one ━ per subagent,
|
|
19
|
+
// coloured by that subagent's phase, in STATUS_BAR_ORDER
|
|
20
|
+
//
|
|
21
|
+
// This module is PURE: it takes a snapshot and returns lines. No timers, no
|
|
22
|
+
// state — the TUI owns the member list and calls renderSwarmProgress().
|
|
23
|
+
|
|
24
|
+
import { C } from './colors.js';
|
|
25
|
+
import { visualWidth } from './term.js';
|
|
26
|
+
|
|
27
|
+
const ESC = '\x1b';
|
|
28
|
+
function visWidth(s) { return visualWidth(String(s).replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '')); }
|
|
29
|
+
const ELLIPSIS = '\u2026';
|
|
30
|
+
function col(text, c) { return c + String(text) + C.reset; }
|
|
31
|
+
|
|
32
|
+
// The order kimi lays the pip-bar segments in, so the bar always reads
|
|
33
|
+
// completed-first regardless of which subagent finished when.
|
|
34
|
+
const STATUS_BAR_ORDER = ['completed', 'working', 'suspended', 'queued', 'cancelled', 'failed'];
|
|
35
|
+
|
|
36
|
+
// phase -> (label, colour). kimi's labels, verbatim.
|
|
37
|
+
const PHASE = {
|
|
38
|
+
queued: { label: 'Queued\u2026', color: C.gray },
|
|
39
|
+
prompting: { label: 'Prompting\u2026', color: C.cyan },
|
|
40
|
+
working: { label: 'Working\u2026', color: C.cyan },
|
|
41
|
+
suspended: { label: 'Rate limited\u2026', color: C.yellow },
|
|
42
|
+
completed: { label: 'Completed.', color: C.green },
|
|
43
|
+
failed: { label: 'Failed.', color: C.red },
|
|
44
|
+
cancelled: { label: 'Cancelled.', color: C.gray },
|
|
45
|
+
};
|
|
46
|
+
export function phaseInfo(phase) { return PHASE[phase] || PHASE.working; }
|
|
47
|
+
|
|
48
|
+
const BAR_EMPTY = '-';
|
|
49
|
+
const BAR_FULL = '#';
|
|
50
|
+
const STATUS_BAR_CHAR = '\u2501'; // ━
|
|
51
|
+
const SUCCESS_MARK = '\u2714'; // ✔
|
|
52
|
+
const FAILURE_MARK = '\u2716'; // ✖
|
|
53
|
+
const CANCELLED_MARK = '\u2716';
|
|
54
|
+
|
|
55
|
+
// ---- helpers ---------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function truncateToWidth(s, width) {
|
|
58
|
+
const str = String(s);
|
|
59
|
+
if (width <= 0) return '';
|
|
60
|
+
if (visWidth(str) <= width) return str;
|
|
61
|
+
let out = '';
|
|
62
|
+
let n = 0;
|
|
63
|
+
let i = 0;
|
|
64
|
+
while (i < str.length) {
|
|
65
|
+
if (str[i] === ESC) {
|
|
66
|
+
const m = /^\x1b\[[0-9;?]*[a-zA-Z]/.exec(str.slice(i));
|
|
67
|
+
if (m) { out += m[0]; i += m[0].length; continue; }
|
|
68
|
+
}
|
|
69
|
+
const cp = str.codePointAt(i);
|
|
70
|
+
const ch = String.fromCodePoint(cp);
|
|
71
|
+
const cw = visualWidth(ch);
|
|
72
|
+
if (n + cw > width - 1) break; // leave a cell for the ellipsis
|
|
73
|
+
n += cw;
|
|
74
|
+
out += ch;
|
|
75
|
+
i += cp > 0xffff ? 2 : 1;
|
|
76
|
+
}
|
|
77
|
+
return out + ELLIPSIS;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function padTo(s, width) {
|
|
81
|
+
const w = visWidth(s);
|
|
82
|
+
return w >= width ? s : s + ' '.repeat(width - w);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function collapseWhitespace(s) {
|
|
86
|
+
return String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- the model -> phases ---------------------------------------------------
|
|
90
|
+
|
|
91
|
+
// A background "agent" task carries the swarm in ctx: we track members from the
|
|
92
|
+
// progress lines the AgentSwarm tool appends (`[n/m] finished`) plus, when the
|
|
93
|
+
// caller has richer data, an explicit member list. Members are normalised here so
|
|
94
|
+
// the renderer never has to guess.
|
|
95
|
+
export function normalizeMember(m, i) {
|
|
96
|
+
const src = m || {}; // a half-built member must not crash the grid
|
|
97
|
+
const phase = PHASE[src.phase] ? src.phase : 'queued';
|
|
98
|
+
return {
|
|
99
|
+
index: i + 1,
|
|
100
|
+
phase,
|
|
101
|
+
// Carried through, not dropped: `renderCell` reads it for the bar, and omitting it
|
|
102
|
+
// here meant every cell fell back to the placeholder ratio no matter what the agent
|
|
103
|
+
// reported — the wire carried a real figure and the renderer never saw one.
|
|
104
|
+
ratio: typeof src.ratio === 'number' && Number.isFinite(src.ratio) ? src.ratio : null,
|
|
105
|
+
latestText: collapseWhitespace(src.latestText || src.text || ''),
|
|
106
|
+
label: src.label || '',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
// Overall status of the swarm: failed wins over aborted over working/completed.
|
|
112
|
+
// `members` is normally an array, but this is also reachable from a rebuilt session
|
|
113
|
+
// where it can be null — guard rather than crash the whole render.
|
|
114
|
+
export function totalStatus(members, flags = {}) {
|
|
115
|
+
if (flags && flags.failed) return 'failed';
|
|
116
|
+
if (flags && flags.aborted) return 'aborted';
|
|
117
|
+
if (!Array.isArray(members) || !members.length) return 'working';
|
|
118
|
+
const phaseOf = (m) => (m && m.phase) || 'queued'; // a null member is "queued", not a crash
|
|
119
|
+
if (members.some((m) => phaseOf(m) === 'failed')) return 'failed';
|
|
120
|
+
if (members.every((m) => phaseOf(m) === 'completed')) return 'completed';
|
|
121
|
+
if (members.every((m) => ['completed', 'cancelled', 'failed'].includes(phaseOf(m)))) return 'completed';
|
|
122
|
+
return 'working';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const TOTAL_LABEL = {
|
|
126
|
+
working: 'Working\u2026',
|
|
127
|
+
completed: 'Completed.',
|
|
128
|
+
failed: 'Failed.',
|
|
129
|
+
aborted: 'Aborted.',
|
|
130
|
+
cancelled: 'Cancelled.',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// ---- grid layout (kimi's calculateAgentSwarmGridLayout, simplified) --------
|
|
134
|
+
|
|
135
|
+
export function computeGridLayout(input) {
|
|
136
|
+
const { width, height, count } = input || {};
|
|
137
|
+
// Every input is coerced: a caller that passes nothing (or a NaN width) would
|
|
138
|
+
// otherwise produce NaN rows/cells and a frame full of garbage.
|
|
139
|
+
const safeWidth = Number.isFinite(width) && width > 0 ? width : 1;
|
|
140
|
+
const safeCount = Number.isFinite(count) ? Math.max(1, Math.floor(count)) : 1;
|
|
141
|
+
const safeHeight = Number.isFinite(height) ? Math.max(0, height) : Number.POSITIVE_INFINITY;
|
|
142
|
+
// Try the widest sensible column count whose cells still hold a readable bar.
|
|
143
|
+
// Then clamp the ROWS back to the height budget: kimi falls back to COMPACT
|
|
144
|
+
// bars rather than spilling. We keep the text cells but cap the row count, so a
|
|
145
|
+
// 128-subagent swarm cannot push the transcript off screen.
|
|
146
|
+
const MIN_CELL = 22;
|
|
147
|
+
const GAP = 2;
|
|
148
|
+
let best = { columns: 1, rows: safeCount, cellWidth: safeWidth };
|
|
149
|
+
for (let columns = 1; columns <= safeCount; columns++) {
|
|
150
|
+
const cellWidth = Math.floor((safeWidth - GAP * (columns - 1)) / columns);
|
|
151
|
+
if (cellWidth < MIN_CELL) break;
|
|
152
|
+
best = { columns, rows: Math.ceil(safeCount / columns), cellWidth };
|
|
153
|
+
}
|
|
154
|
+
// Visible rows never exceed the budget. The caller adds one "+N more" line.
|
|
155
|
+
// Visible rows never exceed the budget. The caller adds one "+N more" line.
|
|
156
|
+
const maxRows = Number.isFinite(safeHeight) ? Math.max(1, safeHeight) : best.rows;
|
|
157
|
+
const shownRows = Math.min(best.rows, maxRows);
|
|
158
|
+
return {
|
|
159
|
+
columns: best.columns,
|
|
160
|
+
rows: shownRows,
|
|
161
|
+
cellWidth: best.cellWidth,
|
|
162
|
+
totalRows: best.rows,
|
|
163
|
+
truncated: best.rows > shownRows,
|
|
164
|
+
// How many cells actually get drawn, so the caller can report the rest.
|
|
165
|
+
visibleCount: Math.min(safeCount, shownRows * best.columns),
|
|
166
|
+
count: safeCount,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---- cells -----------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
function renderBar(ratio, cells, color) {
|
|
173
|
+
const n = Math.max(0, Math.round(ratio * cells));
|
|
174
|
+
return col(BAR_FULL.repeat(n), color) + col(BAR_EMPTY.repeat(Math.max(0, cells - n)), C.gray);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// One cell: `001 [####----] Working… <latest text>`
|
|
178
|
+
// The cell is truncated to EXACTLY `cellWidth` visible columns, so joining cells
|
|
179
|
+
// with a gap cannot overflow the row (kimi truncates every cell the same way).
|
|
180
|
+
function renderCell(member, cellWidth) {
|
|
181
|
+
const idx = String(member.index).padStart(3, '0');
|
|
182
|
+
const info = phaseInfo(member.phase);
|
|
183
|
+
const label = info.label;
|
|
184
|
+
// A working cell with no ratio from its agent shows a near-empty bar, not a confident
|
|
185
|
+
// half. `0.5` was a fixed placeholder: every running agent drew the same half-filled bar
|
|
186
|
+
// for its whole life, which read as "halfway done" and was simply untrue — a swarm of
|
|
187
|
+
// seven showed seven identical 50% bars and then jumped straight to 100%.
|
|
188
|
+
const ratio = member.phase === 'completed' ? 1
|
|
189
|
+
: member.phase === 'queued' ? 0
|
|
190
|
+
: (member.ratio != null ? Math.max(0, Math.min(1, member.ratio)) : 0.05);
|
|
191
|
+
// Budget: "001 [" + bar + "] " + label + optional " " + text.
|
|
192
|
+
const fixed = 3 + 2 + 2 + visWidth(label);
|
|
193
|
+
const barCells = Math.max(0, Math.min(20, cellWidth - fixed));
|
|
194
|
+
let cell = col(idx, C.gray) + ' [' + renderBar(ratio, barCells, info.color) + '] '
|
|
195
|
+
+ col(label, info.color);
|
|
196
|
+
const room = cellWidth - visWidth(cell);
|
|
197
|
+
if (room > 3 && member.latestText) {
|
|
198
|
+
cell += ' ' + col(truncateToWidth(member.latestText, room - 2), C.gray);
|
|
199
|
+
}
|
|
200
|
+
return padTo(truncateToWidth(cell, cellWidth), cellWidth);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---- the status pip bar ----------------------------------------------------
|
|
204
|
+
|
|
205
|
+
function renderPipBar(members, width) {
|
|
206
|
+
const counts = new Map();
|
|
207
|
+
for (const m of members) {
|
|
208
|
+
const p = STATUS_BAR_ORDER.includes(m.phase) ? m.phase : 'working';
|
|
209
|
+
counts.set(p, (counts.get(p) || 0) + 1);
|
|
210
|
+
}
|
|
211
|
+
const segs = STATUS_BAR_ORDER.filter((p) => counts.get(p)).map((p) => ({ p, n: counts.get(p) }));
|
|
212
|
+
if (!segs.length) return col(STATUS_BAR_CHAR.repeat(Math.max(1, width)), C.gray);
|
|
213
|
+
const total = segs.reduce((a, s) => a + s.n, 0);
|
|
214
|
+
let used = 0;
|
|
215
|
+
return segs.map((s, i) => {
|
|
216
|
+
const w = i === segs.length - 1 ? width - used : Math.round((s.n / total) * width);
|
|
217
|
+
used += w;
|
|
218
|
+
if (w <= 0) return '';
|
|
219
|
+
return col(STATUS_BAR_CHAR.repeat(w), phaseInfo(s.p).color);
|
|
220
|
+
}).join('');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---- public renderer -------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Render the swarm block. `snapshot`:
|
|
227
|
+
* { description, model, members: [{phase, ratio, latestText}], failed, aborted }
|
|
228
|
+
* Returns an array of already-coloured lines, ALREADY indented by `indent`.
|
|
229
|
+
*/
|
|
230
|
+
export function renderSwarmProgress(snapshot, width, opts = {}) {
|
|
231
|
+
const snap = snapshot || {};
|
|
232
|
+
const desc = collapseWhitespace(snap.description || '');
|
|
233
|
+
const model = collapseWhitespace(snap.model || '');
|
|
234
|
+
const members = (Array.isArray(snap.members) ? snap.members : []).map(normalizeMember);
|
|
235
|
+
const indent = opts.indent == null ? ' ' : opts.indent;
|
|
236
|
+
const innerW = Math.max(10, width - visWidth(indent));
|
|
237
|
+
const lines = [];
|
|
238
|
+
|
|
239
|
+
// ---- header: `─ Agent Swarm ─ desc ─ model ─────` ----
|
|
240
|
+
const title = col('Agent Swarm', C.spring + C.bold);
|
|
241
|
+
const suffix = [];
|
|
242
|
+
if (desc) suffix.push(col(' \u2500 ', C.spring) + col(desc, C.white));
|
|
243
|
+
if (model) suffix.push(col(' \u2500 ', C.spring) + col(model, C.gray));
|
|
244
|
+
const used = 3 + visWidth(title) + visWidth(suffix.join(''));
|
|
245
|
+
const tail = Math.max(0, innerW - used);
|
|
246
|
+
const header = col(' \u2500 ', C.spring) + title + suffix.join('') + (tail > 1 ? col(' ' + '\u2500'.repeat(tail - 1), C.spring) : '');
|
|
247
|
+
lines.push(indent + truncateToWidth(header, innerW));
|
|
248
|
+
|
|
249
|
+
// ---- optional blank + grid, exactly like kimi's [ '', header, '', grid, '', status, '' ] ----
|
|
250
|
+
lines.push(indent + '');
|
|
251
|
+
const gridH = Number.isFinite(opts.availableGridHeight) ? opts.availableGridHeight : undefined;
|
|
252
|
+
const layout = computeGridLayout({ width: innerW, height: gridH, count: members.length });
|
|
253
|
+
if (!members.length) {
|
|
254
|
+
lines.push(indent + col('Orchestrating\u2026', C.cyan));
|
|
255
|
+
} else {
|
|
256
|
+
for (let r = 0; r < layout.rows; r++) {
|
|
257
|
+
const cells = [];
|
|
258
|
+
for (let c = 0; c < layout.columns; c++) {
|
|
259
|
+
const m = members[r * layout.columns + c];
|
|
260
|
+
if (!m) continue;
|
|
261
|
+
cells.push(renderCell(m, layout.cellWidth));
|
|
262
|
+
}
|
|
263
|
+
if (cells.length) lines.push(indent + cells.join(' '));
|
|
264
|
+
}
|
|
265
|
+
// A swarm taller than the budget reports the remainder instead of spilling.
|
|
266
|
+
if (layout.truncated) {
|
|
267
|
+
lines.push(indent + col(`\u2026 +${layout.count - layout.visibleCount} more`, C.gray));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
lines.push(indent + '');
|
|
271
|
+
|
|
272
|
+
// ---- status: mark + label + pip bar ----
|
|
273
|
+
const status = totalStatus(members, { failed: snap.failed, aborted: snap.aborted });
|
|
274
|
+
const mark = status === 'completed' ? SUCCESS_MARK
|
|
275
|
+
: status === 'failed' ? FAILURE_MARK
|
|
276
|
+
: status === 'aborted' ? CANCELLED_MARK
|
|
277
|
+
: '';
|
|
278
|
+
const label = TOTAL_LABEL[status] || TOTAL_LABEL.working;
|
|
279
|
+
const lc = status === 'completed' ? C.green
|
|
280
|
+
: status === 'failed' ? C.red
|
|
281
|
+
: status === 'aborted' ? C.gray
|
|
282
|
+
: C.cyan;
|
|
283
|
+
const head = (mark ? col(mark + ' ', lc) : ' ') + col(label, lc);
|
|
284
|
+
const barW = Math.max(0, innerW - visWidth(head) - 2);
|
|
285
|
+
lines.push(indent + head + (barW > 0 ? ' ' + renderPipBar(members, barW) : ''));
|
|
286
|
+
lines.push(indent + '');
|
|
287
|
+
return lines;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Derive a member list from the progress lines the AgentSwarm tool appends to a
|
|
291
|
+
// task's output (`[n/m] finished`). Used as the fallback when the caller has no
|
|
292
|
+
// richer per-subagent data — e.g. rebuilding the block after a session reload.
|
|
293
|
+
export function membersFromOutput(output, total) {
|
|
294
|
+
const text = String(output || '');
|
|
295
|
+
// Each line names its agent, and the agent it names is the one it is about:
|
|
296
|
+
// `[#i] started` this agent began
|
|
297
|
+
// `[#i] progress <pct>` this agent is pct% through its own work
|
|
298
|
+
// `[#i] finished [n/m]` this agent, with a running total for readers that only count
|
|
299
|
+
//
|
|
300
|
+
// Reading a bare `[n/m]` as "the first n are done" was the earlier mistake: workers pull
|
|
301
|
+
// from a shared queue, so agent 3 can finish before agent 1, and the count says nothing
|
|
302
|
+
// about which. A `[n/m]`-only line is still accepted, for output written before this
|
|
303
|
+
// named its agent, and is counted but not attributed.
|
|
304
|
+
const started = new Set();
|
|
305
|
+
const doneSet = new Set();
|
|
306
|
+
const ratioOf = new Map();
|
|
307
|
+
let bareDone = 0;
|
|
308
|
+
for (const line of text.split('\n').map((l) => l.replace(/\r$/, ''))) {
|
|
309
|
+
let m = /^\[#(\d+)\] started$/.exec(line);
|
|
310
|
+
if (m) { started.add(Number(m[1])); continue; }
|
|
311
|
+
m = /^\[#(\d+)\] progress (\d+)$/.exec(line);
|
|
312
|
+
if (m) { ratioOf.set(Number(m[1]), Math.max(0, Math.min(100, Number(m[2]))) / 100); continue; }
|
|
313
|
+
m = /^\[#(\d+)\] finished(?: \[\d+\/\d+\])?$/.exec(line);
|
|
314
|
+
if (m) { doneSet.add(Number(m[1])); continue; }
|
|
315
|
+
if (/^\[\d+\/\d+\] finished$/.test(line)) bareDone++;
|
|
316
|
+
}
|
|
317
|
+
// A named finish also counts as "an agent started", so a log that lost its `started` lines
|
|
318
|
+
// still attributes the finish instead of leaving the cell stuck on "working" forever.
|
|
319
|
+
for (const idx of doneSet) started.add(idx);
|
|
320
|
+
// Bounded. `highest` comes off the wire, and one malformed line (`[#999999] progress 1`)
|
|
321
|
+
// would otherwise allocate an array that size and hang the render on every frame.
|
|
322
|
+
const MAX_MEMBERS = 128;
|
|
323
|
+
const highest = Math.min(MAX_MEMBERS, Math.max(0, ...started, ...doneSet, ...ratioOf.keys()));
|
|
324
|
+
const n = Math.min(MAX_MEMBERS, Math.max(total || 0, bareDone, highest));
|
|
325
|
+
// The lines no name claims are the OLD format's only evidence of who finished, so they are
|
|
326
|
+
// attributed to the agents that have not spoken for themselves — counting from the front
|
|
327
|
+
// and skipping any agent that announced itself, rather than refusing to attribute them at
|
|
328
|
+
// all whenever a single named line is present.
|
|
329
|
+
let unattributed = Math.max(0, bareDone - doneSet.size);
|
|
330
|
+
const legacyDone = new Set();
|
|
331
|
+
for (let idx = 1; idx <= n && unattributed > 0; idx++) {
|
|
332
|
+
if (started.has(idx) || doneSet.has(idx) || ratioOf.has(idx)) continue;
|
|
333
|
+
legacyDone.add(idx);
|
|
334
|
+
unattributed--;
|
|
335
|
+
}
|
|
336
|
+
return Array.from({ length: n }, (_, i) => {
|
|
337
|
+
const idx = i + 1;
|
|
338
|
+
const completed = doneSet.has(idx) || legacyDone.has(idx);
|
|
339
|
+
const phase = completed ? 'completed'
|
|
340
|
+
: started.has(idx) ? 'working'
|
|
341
|
+
: 'queued';
|
|
342
|
+
const out = { phase, latestText: '' };
|
|
343
|
+
// `ratioOf` only ever holds a finite value, so the extra `isFinite` test was dead — and
|
|
344
|
+
// it turned a genuine 0 into `null`, which the renderer reads as "no figure at all".
|
|
345
|
+
if (ratioOf.has(idx) && !completed) out.ratio = ratioOf.get(idx);
|
|
346
|
+
return out;
|
|
347
|
+
});
|
|
348
|
+
}
|