@semalt-ai/code 1.8.4 → 1.8.5
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/.claude/settings.local.json +3 -1
- package/CLAUDE.md +4 -1
- package/TECHNICAL_DEBT.md +66 -0
- package/index.js +9 -2
- package/lib/agent.js +234 -87
- package/lib/api.js +95 -6
- package/lib/args.js +22 -0
- package/lib/commands.js +168 -18
- package/lib/config.js +13 -0
- package/lib/debug.js +106 -0
- package/lib/proc.js +96 -0
- package/lib/prompts.js +4 -3
- package/lib/tool_specs.js +14 -7
- package/lib/tools.js +287 -113
- package/lib/ui/chat-history.js +19 -1
- package/lib/ui/format.js +79 -5
- package/lib/ui/terminal.js +10 -4
- package/lib/ui/writer.js +7 -9
- package/package.json +1 -1
package/lib/ui/chat-history.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { RST, DIM, BOLD, FG_CYAN, FG_GREEN, FG_YELLOW, FG_RED, FG_DARK, FG_GRAY } = require('./ansi');
|
|
4
4
|
const { getCols, stripAnsi } = require('./utils');
|
|
5
5
|
const { UI_THEME, UI_ICONS, TOOL_CATEGORIES } = require('./theme');
|
|
6
|
+
const { summarizeToolResult } = require('./format');
|
|
6
7
|
const writer = require('./writer');
|
|
7
8
|
|
|
8
9
|
|
|
@@ -147,7 +148,24 @@ class ChatHistory {
|
|
|
147
148
|
// scrollback via endActivity — when a caller supplies an empty
|
|
148
149
|
// `content` they're signalling that the header is already present
|
|
149
150
|
// and only the expandable output body should render here.
|
|
150
|
-
|
|
151
|
+
//
|
|
152
|
+
// History-loaded tool messages arrive with the raw stored payload as
|
|
153
|
+
// `content` and no `output` — collapse them through summarizeToolResult
|
|
154
|
+
// to a single live-activity-style line. Callers that pass `output`
|
|
155
|
+
// (debug blocks, live-activity error pass-through) keep the legacy
|
|
156
|
+
// header chrome.
|
|
157
|
+
if (msg.content && !msg.output) {
|
|
158
|
+
const summary = safeContent(summarizeToolResult(msg.content));
|
|
159
|
+
if (summary) {
|
|
160
|
+
const indicator = msg.isError
|
|
161
|
+
? `${UI_THEME.error}${UI_ICONS.error}${RST}`
|
|
162
|
+
: `${UI_THEME.success}${UI_ICONS.success}${RST}`;
|
|
163
|
+
const sep = ` ${DIM}·${RST} `;
|
|
164
|
+
const styled = summary.split(' · ').map((p) => _dimPaths(p)).join(sep);
|
|
165
|
+
out = ` ${indicator} ${styled}\n`;
|
|
166
|
+
lineCount = 1;
|
|
167
|
+
}
|
|
168
|
+
} else if (content) {
|
|
151
169
|
const indicator = msg.isError
|
|
152
170
|
? `${UI_THEME.error}${UI_ICONS.error}${RST}`
|
|
153
171
|
: `${UI_THEME.success}${UI_ICONS.success}${RST}`;
|
package/lib/ui/format.js
CHANGED
|
@@ -95,11 +95,28 @@ function _categoryLabel(tag) {
|
|
|
95
95
|
return cat.length >= CATEGORY_WIDTH ? cat.slice(0, CATEGORY_WIDTH) : cat.padEnd(CATEGORY_WIDTH);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
// Display-only normalizer for tool argument text. Collapses every run of
|
|
99
|
+
// whitespace (including \n from heredocs and `\<NL>` line continuations)
|
|
100
|
+
// to a single space and trims the ends, so the summary line never spills
|
|
101
|
+
// across multiple physical rows. Pure: never mutates the value used for
|
|
102
|
+
// execution or sent to the model.
|
|
103
|
+
function normalizeCmdForDisplay(text) {
|
|
99
104
|
if (text == null) return '';
|
|
100
|
-
|
|
105
|
+
return String(text).replace(/\s+/g, ' ').trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Truncate to fit `max` visible chars, normalizing first so a multi-line
|
|
109
|
+
// command becomes a single visual line. When the cut would land mid-word,
|
|
110
|
+
// back up to the nearest space — but only if it doesn't sacrifice more
|
|
111
|
+
// than ~30% of the available width (otherwise prefer the harder cut).
|
|
112
|
+
function _truncate(text, max) {
|
|
113
|
+
const s = normalizeCmdForDisplay(text);
|
|
101
114
|
if (s.length <= max) return s;
|
|
102
|
-
|
|
115
|
+
const cap = Math.max(0, max - 1);
|
|
116
|
+
let cut = s.slice(0, cap);
|
|
117
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
118
|
+
if (lastSpace > cap * 0.7) cut = cut.slice(0, lastSpace);
|
|
119
|
+
return cut + '…';
|
|
103
120
|
}
|
|
104
121
|
|
|
105
122
|
// Verb + target string. Never more than ~80 chars visible — longer URLs,
|
|
@@ -199,6 +216,7 @@ function formatToolLine(args) {
|
|
|
199
216
|
durationMs,
|
|
200
217
|
meta,
|
|
201
218
|
error,
|
|
219
|
+
noDuration,
|
|
202
220
|
} = args || {};
|
|
203
221
|
|
|
204
222
|
let glyph;
|
|
@@ -218,7 +236,6 @@ function formatToolLine(args) {
|
|
|
218
236
|
const catColor = (UI_THEME.categories && UI_THEME.categories[_normalizeTag(tag) && (TOOL_CATEGORIES[_normalizeTag(tag)] || 'tool')]) || UI_THEME.accent;
|
|
219
237
|
|
|
220
238
|
const op = _operation(tag, arg, attrs);
|
|
221
|
-
const durStr = formatDuration(durationMs || 0) + (status === 'pending' ? '…' : '');
|
|
222
239
|
const metaParts = _metaParts(tag, meta, error);
|
|
223
240
|
|
|
224
241
|
// Segment-by-segment styling. Each fragment carries its own ANSI codes
|
|
@@ -232,16 +249,73 @@ function formatToolLine(args) {
|
|
|
232
249
|
const segments = [];
|
|
233
250
|
segments.push(` ${glyphColor}${glyph}${RST} ${catColor}${cat}${RST}`);
|
|
234
251
|
segments.push(`${UI_THEME.default}${op}${RST}`);
|
|
235
|
-
|
|
252
|
+
if (!noDuration) {
|
|
253
|
+
const durStr = formatDuration(durationMs || 0) + (status === 'pending' ? '…' : '');
|
|
254
|
+
segments.push(`${durColor}${durStr}${RST}`);
|
|
255
|
+
}
|
|
236
256
|
for (const m of metaParts) {
|
|
237
257
|
if (m) segments.push(`${metaColor}${m}${RST}`);
|
|
238
258
|
}
|
|
239
259
|
return segments.join(sep);
|
|
240
260
|
}
|
|
241
261
|
|
|
262
|
+
// Collapse a stored tool-result string to a single-line summary in the same
|
|
263
|
+
// shape as the live activity bubble ("net · GET https://x · 200 · 256 KB").
|
|
264
|
+
// Pure: no ANSI, no IO, no allocation beyond the returned string. Idempotent
|
|
265
|
+
// — a value that already looks like a summary is returned untouched so the
|
|
266
|
+
// helper survives double-application once a separate display field lands in
|
|
267
|
+
// storage.
|
|
268
|
+
function summarizeToolResult(content) {
|
|
269
|
+
if (typeof content !== 'string' || !content) return '';
|
|
270
|
+
const trimmed = content.trim();
|
|
271
|
+
if (!trimmed) return '';
|
|
272
|
+
|
|
273
|
+
if (!trimmed.includes('\n') && trimmed.includes(' · ') && trimmed.length < 200) {
|
|
274
|
+
return trimmed;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// HTTP: agent.js formats as `HTTP <VERB> <url> (<status>):\n<body>`.
|
|
278
|
+
const httpMatch = content.match(/^HTTP\s+(\w+)\s+(\S+)\s+\((\d+)\)/);
|
|
279
|
+
if (httpMatch) {
|
|
280
|
+
const parts = ['net', `${httpMatch[1]} ${httpMatch[2]}`, httpMatch[3]];
|
|
281
|
+
const bytes = formatBytes(Buffer.byteLength(content, 'utf8'));
|
|
282
|
+
if (bytes) parts.push(bytes);
|
|
283
|
+
return parts.join(' · ');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Exec: `Command \`<cmd>\`:\nExit code: <N>\n<output>`. The cmd may span
|
|
287
|
+
// multiple lines if the user passed a heredoc — non-greedy capture stops
|
|
288
|
+
// at the first `\`:` boundary, then we collapse whitespace for the preview.
|
|
289
|
+
const cmdMatch = content.match(/^Command `([\s\S]+?)`:/);
|
|
290
|
+
const exitMatch = content.match(/^Exit code: (-?\d+)$/m);
|
|
291
|
+
if (cmdMatch && exitMatch) {
|
|
292
|
+
const cmd = cmdMatch[1].replace(/\s+/g, ' ').trim();
|
|
293
|
+
const preview = cmd.length > 60 ? cmd.slice(0, 59) + '…' : cmd;
|
|
294
|
+
return `exec · ${preview} · exit ${exitMatch[1]}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const lines = content.split('\n');
|
|
298
|
+
if (lines.length <= 3 && /^(Wrote|Read|Created|Deleted|Moved|Renamed)\b/.test(lines[0])) {
|
|
299
|
+
return lines[0];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (lines.length > 1 || content.length > 120) {
|
|
303
|
+
const firstLine = lines[0] || '';
|
|
304
|
+
const preview = firstLine.length > 100 ? firstLine.slice(0, 99) + '…' : firstLine;
|
|
305
|
+
const parts = ['tool', preview];
|
|
306
|
+
const bytes = formatBytes(Buffer.byteLength(content, 'utf8'));
|
|
307
|
+
if (bytes) parts.push(bytes);
|
|
308
|
+
return parts.join(' · ');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return trimmed;
|
|
312
|
+
}
|
|
313
|
+
|
|
242
314
|
module.exports = {
|
|
243
315
|
formatDuration,
|
|
244
316
|
formatBytes,
|
|
245
317
|
formatHttpErrorTag,
|
|
246
318
|
formatToolLine,
|
|
319
|
+
summarizeToolResult,
|
|
320
|
+
normalizeCmdForDisplay,
|
|
247
321
|
};
|
package/lib/ui/terminal.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// the process dies.
|
|
8
8
|
|
|
9
9
|
const writer = require('./writer');
|
|
10
|
+
const dbg = require('../debug');
|
|
10
11
|
|
|
11
12
|
let _registered = false;
|
|
12
13
|
|
|
@@ -30,20 +31,24 @@ function registerTerminalCleanup() {
|
|
|
30
31
|
|
|
31
32
|
// Normal exit + process.exit(): fires synchronously, last thing Node does.
|
|
32
33
|
// Catches every path that doesn't already manually call teardown.
|
|
33
|
-
process.on('exit', () => {
|
|
34
|
+
process.on('exit', () => {
|
|
35
|
+
try { writer.teardown(); } catch {}
|
|
36
|
+
try { dbg.close(); } catch {}
|
|
37
|
+
});
|
|
34
38
|
|
|
35
39
|
// Signals that should terminate the app. Cleanup first, then exit with
|
|
36
40
|
// the conventional 128+signum code. In TUI raw mode, Ctrl+C is consumed
|
|
37
41
|
// at the byte level and SIGINT is not delivered, so this handler only
|
|
38
42
|
// trips in non-raw contexts (one-shot commands, readline prompts, etc.).
|
|
39
|
-
process.on('SIGINT', () => { try { writer.teardown(); } catch {} process.exit(130); });
|
|
40
|
-
process.on('SIGTERM', () => { try { writer.teardown(); } catch {} process.exit(143); });
|
|
41
|
-
process.on('SIGHUP', () => { try { writer.teardown(); } catch {} process.exit(129); });
|
|
43
|
+
process.on('SIGINT', () => { try { writer.teardown(); } catch {} try { dbg.close(); } catch {} process.exit(130); });
|
|
44
|
+
process.on('SIGTERM', () => { try { writer.teardown(); } catch {} try { dbg.close(); } catch {} process.exit(143); });
|
|
45
|
+
process.on('SIGHUP', () => { try { writer.teardown(); } catch {} try { dbg.close(); } catch {} process.exit(129); });
|
|
42
46
|
|
|
43
47
|
// Last-chance net: if something throws outside a try/catch, still
|
|
44
48
|
// restore terminal state before the stack trace prints.
|
|
45
49
|
process.on('uncaughtException', (err) => {
|
|
46
50
|
try { writer.teardown(); } catch {}
|
|
51
|
+
try { dbg.close(); } catch {}
|
|
47
52
|
// audit: allowed — crash handler stderr after writer teardown.
|
|
48
53
|
try { console.error(err && err.stack ? err.stack : err); } catch {}
|
|
49
54
|
process.exit(1);
|
|
@@ -51,6 +56,7 @@ function registerTerminalCleanup() {
|
|
|
51
56
|
|
|
52
57
|
process.on('unhandledRejection', (reason) => {
|
|
53
58
|
try { writer.teardown(); } catch {}
|
|
59
|
+
try { dbg.close(); } catch {}
|
|
54
60
|
// audit: allowed — crash handler stderr after writer teardown.
|
|
55
61
|
try { console.error(reason && reason.stack ? reason.stack : reason); } catch {}
|
|
56
62
|
process.exit(1);
|
package/lib/ui/writer.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
// can't interleave with another task's writes.
|
|
30
30
|
|
|
31
31
|
const { stripAnsi, termWidth, truncateVisible } = require('./utils');
|
|
32
|
+
const dbg = require('../debug');
|
|
32
33
|
|
|
33
34
|
let _queue = Promise.resolve();
|
|
34
35
|
let _liveLines = [];
|
|
@@ -228,16 +229,13 @@ function setLive(lines, caret) {
|
|
|
228
229
|
// The input row's visible position is relative to the bottom of the
|
|
229
230
|
// viewport; it stays still only while the live region keeps a stable
|
|
230
231
|
// height. If a code path sneaks in a different-height setLive call the
|
|
231
|
-
// input jumps one row. Log
|
|
232
|
-
// without leaking into the TUI itself.
|
|
233
|
-
if (
|
|
234
|
-
_previousLiveLineCount !== undefined &&
|
|
232
|
+
// input jumps one row. Log to the debug file (extended trace) so future
|
|
233
|
+
// drift is visible without leaking into the TUI itself.
|
|
234
|
+
if (_previousLiveLineCount !== undefined &&
|
|
235
235
|
_previousLiveLineCount !== _liveLines.length) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
);
|
|
240
|
-
} catch {}
|
|
236
|
+
dbg.logExtended(
|
|
237
|
+
`[writer] live region height changed: ${_previousLiveLineCount} → ${_liveLines.length}`
|
|
238
|
+
);
|
|
241
239
|
}
|
|
242
240
|
_previousLiveLineCount = _liveLines.length;
|
|
243
241
|
_writeSync(_HIDE + eraseSeq + _drawLiveSeq() + _positionCaretSeq() + _caretShowSeq());
|