@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.
@@ -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
- if (content) {
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
- function _truncate(text, max) {
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
- const s = String(text);
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
- return s.slice(0, Math.max(0, max - 1)) + '…';
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
- segments.push(`${durColor}${durStr}${RST}`);
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
  };
@@ -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', () => { try { writer.teardown(); } catch {} });
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 under DEBUG_TUI so future drift is visible
232
- // without leaking into the TUI itself.
233
- if (process.env.DEBUG_TUI &&
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
- try {
237
- process.stderr.write(
238
- `live region height changed: ${_previousLiveLineCount} → ${_liveLines.length}\n`
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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semalt-ai/code",
3
- "version": "1.8.4",
3
+ "version": "1.8.5",
4
4
  "description": "Self-hosted AI Coding Assistant CLI",
5
5
  "main": "index.js",
6
6
  "bin": {