@hncode/hncode 0.3.6 → 0.4.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/package.json +4 -2
- package/src/agent-task.js +154 -84
- package/src/agent.js +533 -145
- package/src/clipboard.js +5 -2
- package/src/colors.js +242 -238
- package/src/config.js +217 -17
- package/src/file-history.js +177 -0
- package/src/hooks.js +11 -1
- package/src/i18n.js +389 -0
- package/src/index.js +46 -3
- package/src/llm.js +139 -16
- package/src/permissions.js +120 -0
- package/src/session-hub.js +407 -0
- package/src/session.js +31 -0
- package/src/term.js +19 -3
- package/src/tool-result.js +236 -0
- package/src/tools/agent.js +11 -0
- package/src/tools/bash.js +84 -17
- package/src/tools/edit.js +192 -32
- package/src/tools/file-lines.js +5 -0
- package/src/tools/index.js +4 -0
- package/src/tools/memory.js +88 -0
- package/src/tools/read-media-file.js +85 -68
- package/src/tools/read.js +15 -1
- package/src/tools/utils.js +14 -0
- package/src/tools/write.js +35 -5
- package/src/tui.js +11665 -9179
- package/src/web-auth.js +98 -0
- package/src/web-daemon-cli.js +63 -0
- package/src/web-daemon-client.js +203 -0
- package/src/web-daemon.js +871 -0
- package/src/web-public/app.css +1509 -0
- package/src/web-public/app.js +2913 -0
- package/src/web-public/complete.js +154 -0
- package/src/web-public/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/src/web-public/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-100-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-101-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-102-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-103-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-104-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-105-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-106-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-107-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-108-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-109-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-110-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-111-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-112-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-113-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-114-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-115-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-116-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-117-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-118-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-119-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-21-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-22-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-23-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-24-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-25-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-26-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-27-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-28-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-29-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-30-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-31-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-32-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-33-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-34-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-35-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-36-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-37-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-38-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-39-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-4-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-40-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-41-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-42-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-43-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-44-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-45-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-46-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-47-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-48-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-49-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-5-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-50-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-51-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-52-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-53-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-54-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-55-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-56-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-57-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-58-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-59-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-6-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-60-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-61-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-62-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-63-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-64-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-65-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-66-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-67-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-68-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-69-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-70-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-71-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-72-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-73-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-74-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-75-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-76-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-77-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-78-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-79-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-80-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-81-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-82-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-83-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-84-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-85-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-86-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-87-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-88-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-89-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-90-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-91-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-97-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-98-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc-99-400-normal.woff2 +0 -0
- package/src/web-public/fonts/noto-sans-sc.css +775 -0
- package/src/web-public/fonts.css +31 -0
- package/src/web-public/home.css +222 -0
- package/src/web-public/home.html +33 -0
- package/src/web-public/home.js +160 -0
- package/src/web-public/i18n.js +983 -0
- package/src/web-public/index.html +196 -0
- package/src/web-public/login.css +123 -0
- package/src/web-public/login.html +25 -0
- package/src/web-public/login.js +46 -0
- package/src/web-public/settings.js +290 -0
- package/src/web-ws.js +226 -0
- package/src/web.js +380 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hncode/hncode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "hncode — A cyan-blue TUI AI coding agent with zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"preinstall": "node bin/preinstall.js",
|
|
20
|
-
"install": "node bin/install.js"
|
|
20
|
+
"install": "node bin/install.js",
|
|
21
|
+
"i18n": "node tools/sync-i18n.mjs",
|
|
22
|
+
"i18n:check": "node tools/sync-i18n.mjs --check"
|
|
21
23
|
},
|
|
22
24
|
"engines": {
|
|
23
25
|
"node": ">=20"
|
package/src/agent-task.js
CHANGED
|
@@ -32,13 +32,25 @@ export function newTaskId(prefix = 'task') {
|
|
|
32
32
|
|
|
33
33
|
// Decode a task's buffered chunks for display. Buffer chunks (Bash) are UTF-8;
|
|
34
34
|
// string chunks (Agent/AgentSwarm) are already text.
|
|
35
|
+
//
|
|
36
|
+
// Tolerant of a RESTORED task: after a session round-trip the chunks may be
|
|
37
|
+
// strings, Buffers, `{type:'Buffer',data:[…]}` objects (what JSON.stringify does
|
|
38
|
+
// to a Buffer), or nothing at all. Anything unrecognisable is stringified rather
|
|
39
|
+
// than thrown on — a display helper must never be able to take the TUI down, and
|
|
40
|
+
// the output panel is exactly where a half-restored task shows up.
|
|
35
41
|
function decodeTaskOutput(chunks) {
|
|
36
|
-
|
|
42
|
+
if (chunks == null) return '';
|
|
43
|
+
const list = Array.isArray(chunks) ? chunks : [chunks];
|
|
37
44
|
if (!list.length) return '';
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
const normalize = (c) => {
|
|
46
|
+
if (Buffer.isBuffer(c)) return c;
|
|
47
|
+
if (c && c.type === 'Buffer' && Array.isArray(c.data)) return Buffer.from(c.data);
|
|
48
|
+
if (typeof c === 'string') return c;
|
|
49
|
+
return String(c);
|
|
50
|
+
};
|
|
51
|
+
const items = list.map(normalize);
|
|
52
|
+
try { return Buffer.concat(items.map((c) => (Buffer.isBuffer(c) ? c : Buffer.from(c)))).toString('utf8'); }
|
|
53
|
+
catch { return items.map((c) => String(c)).join(''); }
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
// The task store lives on the agent ctx (`ctx.tasks`), so every tool and the TUI
|
|
@@ -98,12 +110,22 @@ export function createTask(ctx, kind, options = {}) {
|
|
|
98
110
|
// * a process appends Buffer chunks -> decode as UTF-8
|
|
99
111
|
// * an agent/swarm appends strings -> join
|
|
100
112
|
// Callers may override this getter when they need a different decoding.
|
|
113
|
+
defineOutputGetter(task);
|
|
114
|
+
tasks[task.taskId] = task;
|
|
115
|
+
return task;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Install the lazy `output` getter. Extracted so a RESTORED task can have it
|
|
119
|
+
// reinstalled: JSON.stringify resolves the getter into a plain string property, so
|
|
120
|
+
// a reloaded record carries a frozen snapshot instead of a live view (see
|
|
121
|
+
// appendTaskOutput, which repairs both `_chunks` and the getter). `configurable`
|
|
122
|
+
// must stay true or the reinstall would throw on an already-defined property.
|
|
123
|
+
export function defineOutputGetter(task) {
|
|
101
124
|
Object.defineProperty(task, 'output', {
|
|
102
125
|
enumerable: true,
|
|
103
126
|
configurable: true,
|
|
104
127
|
get() { return decodeTaskOutput(this._chunks); },
|
|
105
128
|
});
|
|
106
|
-
tasks[task.taskId] = task;
|
|
107
129
|
return task;
|
|
108
130
|
}
|
|
109
131
|
|
|
@@ -111,8 +133,56 @@ export function createTask(ctx, kind, options = {}) {
|
|
|
111
133
|
export const MAX_TASK_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
112
134
|
|
|
113
135
|
// Append raw bytes/chunks to a task, trimming the oldest past the cap.
|
|
136
|
+
//
|
|
137
|
+
// `_chunks` is repaired rather than assumed. The task store is reachable from
|
|
138
|
+
// three directions — created in-process, restored with a SESSION
|
|
139
|
+
// (`state.tasks`/`ctx.tasks` are copied verbatim into the session JSON and read
|
|
140
|
+
// back), and handed around between tools — so a record can arrive here without an
|
|
141
|
+
// array. A session round-trip also turns Buffers into `{type:'Buffer',data:[…]}`
|
|
142
|
+
// plain objects (JSON has no Buffer), and a record that never produced output can
|
|
143
|
+
// come back with no `_chunks` at all.
|
|
144
|
+
//
|
|
145
|
+
// This matters beyond tidiness: the only production callers are stdout/stderr
|
|
146
|
+
// 'data' listeners on a ChildProcess, and a throw inside an event listener is NOT
|
|
147
|
+
// caught by the surrounding tool. A missing array therefore surfaces as an
|
|
148
|
+
// unhandledRejection that kills the whole TUI mid-tool-call, with nothing printed
|
|
149
|
+
// (the exit handler wipes the alternate screen). Both observed shapes —
|
|
150
|
+
// "undefined.push" and "null.push" — come from exactly this line.
|
|
114
151
|
export function appendTaskOutput(task, chunk) {
|
|
115
152
|
if (!task || chunk == null) return;
|
|
153
|
+
if (!Array.isArray(task._chunks)) {
|
|
154
|
+
// Salvage whatever the old value held: a single chunk, an array-like, or a
|
|
155
|
+
// serialized Buffer object. A Buffer must be revived from its JSON form or the
|
|
156
|
+
// decoder would later try to concat a plain object.
|
|
157
|
+
const prev = task._chunks;
|
|
158
|
+
const revived = [];
|
|
159
|
+
if (prev != null) {
|
|
160
|
+
const items = Array.isArray(prev) ? prev : [prev];
|
|
161
|
+
for (const item of items) {
|
|
162
|
+
if (item && item.type === 'Buffer' && Array.isArray(item.data)) revived.push(Buffer.from(item.data));
|
|
163
|
+
else if (item != null) revived.push(item);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
task._chunks = revived;
|
|
167
|
+
// `_bytes` may be missing too on a restored record; recount from what we kept
|
|
168
|
+
// so the size cap works from here on instead of comparing against undefined.
|
|
169
|
+
task._bytes = revived.reduce((n, c) => n + (c.length || 0), 0);
|
|
170
|
+
}
|
|
171
|
+
// `JSON.stringify` does not preserve the `output` GETTER: it reads the getter and
|
|
172
|
+
// stores the resulting STRING as an ordinary own property. A restored task's
|
|
173
|
+
// `_chunks` is still an array, so the repair above does NOT run for it — but its
|
|
174
|
+
// `output` is a frozen snapshot that no longer tracks `_chunks`, and would keep
|
|
175
|
+
// reporting the pre-save text (or `undefined`) for the rest of the session.
|
|
176
|
+
// Reinstall the live getter whenever the property is a data value rather than an
|
|
177
|
+
// accessor. Checked on every append; after the first repair it is an accessor and
|
|
178
|
+
// this is a cheap no-op.
|
|
179
|
+
try {
|
|
180
|
+
const desc = Object.getOwnPropertyDescriptor(task, 'output');
|
|
181
|
+
if (!desc || !desc.get) {
|
|
182
|
+
if (desc) delete task.output;
|
|
183
|
+
defineOutputGetter(task);
|
|
184
|
+
}
|
|
185
|
+
} catch { /* a frozen record still appends correctly, only its display lags */ }
|
|
116
186
|
task._chunks.push(chunk);
|
|
117
187
|
task._bytes = (task._bytes || 0) + (chunk.length || 0);
|
|
118
188
|
while (task._bytes > MAX_TASK_OUTPUT_BYTES && task._chunks.length > 1) {
|
|
@@ -176,81 +246,81 @@ export function statusColor(status) {
|
|
|
176
246
|
if (status === 'completed') return 'gray';
|
|
177
247
|
return 'red';
|
|
178
248
|
}
|
|
179
|
-
|
|
180
|
-
// ---- background-task transcript card (mirrors kimi-code) --------------------
|
|
181
|
-
// A background task reports its lifecycle as a one-line card in the transcript:
|
|
182
|
-
//
|
|
183
|
-
// ● agent task completed in background (review the diff)
|
|
184
|
-
// ✗ bash task failed in background (npm test · exit 1)
|
|
185
|
-
//
|
|
186
|
-
// `phase` drives the bullet colour (started / completed / failed); `headline`
|
|
187
|
-
// and the dim `detail` carry the rest. Ported from kimi-code's
|
|
188
|
-
// formatBackgroundTaskTranscript (apps/kimi-code/src/tui/utils/background-task-status.ts)
|
|
189
|
-
// so the two tools read the same way.
|
|
190
|
-
|
|
191
|
-
const BG_DETAIL_MAX = 240;
|
|
192
|
-
|
|
193
|
-
function bgTruncate(value) {
|
|
194
|
-
if (value === undefined || value === null) return undefined;
|
|
195
|
-
const collapsed = String(value).trim().replace(/\s+/g, ' ');
|
|
196
|
-
if (!collapsed) return undefined;
|
|
197
|
-
return collapsed.length <= BG_DETAIL_MAX ? collapsed : collapsed.slice(0, BG_DETAIL_MAX - 1) + '…';
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function backgroundTaskPhase(status) {
|
|
201
|
-
if (status === 'running') return 'started';
|
|
202
|
-
if (status === 'completed') return 'completed';
|
|
203
|
-
return 'failed'; // failed / timed_out / killed / lost
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function bgSubject(kind) {
|
|
207
|
-
if (kind === 'agent') return 'agent task';
|
|
208
|
-
if (kind === 'question') return 'question task';
|
|
209
|
-
return 'bash task';
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function bgHeadline(status, kind) {
|
|
213
|
-
const subject = bgSubject(kind);
|
|
214
|
-
switch (status) {
|
|
215
|
-
case 'running': return `${subject} started in background`;
|
|
216
|
-
case 'completed': return `${subject} completed in background`;
|
|
217
|
-
case 'failed': return `${subject} failed in background`;
|
|
218
|
-
case 'timed_out': return `${subject} timed out`;
|
|
219
|
-
case 'killed': return `${subject} stopped`;
|
|
220
|
-
case 'lost': return `${subject} lost`;
|
|
221
|
-
default: return `${subject} ${status}`;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function bgDetail(task) {
|
|
226
|
-
const parts = [];
|
|
227
|
-
const description = bgTruncate(task.description);
|
|
228
|
-
if (description !== undefined) parts.push(description);
|
|
229
|
-
|
|
230
|
-
if (task.status === 'completed' || task.status === 'failed') {
|
|
231
|
-
if (task.kind === 'process' && task.exitCode !== null && task.exitCode !== undefined) {
|
|
232
|
-
parts.push(`exit ${task.exitCode}`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (task.status === 'killed') {
|
|
236
|
-
const reason = bgTruncate(task.stopReason);
|
|
237
|
-
parts.push(reason !== undefined ? `stopped — ${reason}` : 'stopped');
|
|
238
|
-
}
|
|
239
|
-
if (task.status === 'failed') {
|
|
240
|
-
const reason = bgTruncate(task.stopReason);
|
|
241
|
-
if (reason !== undefined) parts.push(reason);
|
|
242
|
-
}
|
|
243
|
-
if (task.status === 'timed_out') parts.push('timed out');
|
|
244
|
-
if (task.status === 'lost') parts.push('session restarted before completion');
|
|
245
|
-
|
|
246
|
-
return parts.length ? parts.join(' · ') : undefined;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Build the card payload for a task snapshot.
|
|
250
|
-
export function backgroundTaskCard(task) {
|
|
251
|
-
return {
|
|
252
|
-
phase: backgroundTaskPhase(task.status),
|
|
253
|
-
headline: bgHeadline(task.status, task.kind),
|
|
254
|
-
detail: bgDetail(task),
|
|
255
|
-
};
|
|
256
|
-
}
|
|
249
|
+
|
|
250
|
+
// ---- background-task transcript card (mirrors kimi-code) --------------------
|
|
251
|
+
// A background task reports its lifecycle as a one-line card in the transcript:
|
|
252
|
+
//
|
|
253
|
+
// ● agent task completed in background (review the diff)
|
|
254
|
+
// ✗ bash task failed in background (npm test · exit 1)
|
|
255
|
+
//
|
|
256
|
+
// `phase` drives the bullet colour (started / completed / failed); `headline`
|
|
257
|
+
// and the dim `detail` carry the rest. Ported from kimi-code's
|
|
258
|
+
// formatBackgroundTaskTranscript (apps/kimi-code/src/tui/utils/background-task-status.ts)
|
|
259
|
+
// so the two tools read the same way.
|
|
260
|
+
|
|
261
|
+
const BG_DETAIL_MAX = 240;
|
|
262
|
+
|
|
263
|
+
function bgTruncate(value) {
|
|
264
|
+
if (value === undefined || value === null) return undefined;
|
|
265
|
+
const collapsed = String(value).trim().replace(/\s+/g, ' ');
|
|
266
|
+
if (!collapsed) return undefined;
|
|
267
|
+
return collapsed.length <= BG_DETAIL_MAX ? collapsed : collapsed.slice(0, BG_DETAIL_MAX - 1) + '…';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function backgroundTaskPhase(status) {
|
|
271
|
+
if (status === 'running') return 'started';
|
|
272
|
+
if (status === 'completed') return 'completed';
|
|
273
|
+
return 'failed'; // failed / timed_out / killed / lost
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function bgSubject(kind) {
|
|
277
|
+
if (kind === 'agent') return 'agent task';
|
|
278
|
+
if (kind === 'question') return 'question task';
|
|
279
|
+
return 'bash task';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function bgHeadline(status, kind) {
|
|
283
|
+
const subject = bgSubject(kind);
|
|
284
|
+
switch (status) {
|
|
285
|
+
case 'running': return `${subject} started in background`;
|
|
286
|
+
case 'completed': return `${subject} completed in background`;
|
|
287
|
+
case 'failed': return `${subject} failed in background`;
|
|
288
|
+
case 'timed_out': return `${subject} timed out`;
|
|
289
|
+
case 'killed': return `${subject} stopped`;
|
|
290
|
+
case 'lost': return `${subject} lost`;
|
|
291
|
+
default: return `${subject} ${status}`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function bgDetail(task) {
|
|
296
|
+
const parts = [];
|
|
297
|
+
const description = bgTruncate(task.description);
|
|
298
|
+
if (description !== undefined) parts.push(description);
|
|
299
|
+
|
|
300
|
+
if (task.status === 'completed' || task.status === 'failed') {
|
|
301
|
+
if (task.kind === 'process' && task.exitCode !== null && task.exitCode !== undefined) {
|
|
302
|
+
parts.push(`exit ${task.exitCode}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (task.status === 'killed') {
|
|
306
|
+
const reason = bgTruncate(task.stopReason);
|
|
307
|
+
parts.push(reason !== undefined ? `stopped — ${reason}` : 'stopped');
|
|
308
|
+
}
|
|
309
|
+
if (task.status === 'failed') {
|
|
310
|
+
const reason = bgTruncate(task.stopReason);
|
|
311
|
+
if (reason !== undefined) parts.push(reason);
|
|
312
|
+
}
|
|
313
|
+
if (task.status === 'timed_out') parts.push('timed out');
|
|
314
|
+
if (task.status === 'lost') parts.push('session restarted before completion');
|
|
315
|
+
|
|
316
|
+
return parts.length ? parts.join(' · ') : undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Build the card payload for a task snapshot.
|
|
320
|
+
export function backgroundTaskCard(task) {
|
|
321
|
+
return {
|
|
322
|
+
phase: backgroundTaskPhase(task.status),
|
|
323
|
+
headline: bgHeadline(task.status, task.kind),
|
|
324
|
+
detail: bgDetail(task),
|
|
325
|
+
};
|
|
326
|
+
}
|