mixdog 0.9.0 → 0.9.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/package.json +3 -3
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/src/headless-role.mjs +1 -1
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +2 -2
- package/src/mixdog-session-runtime.mjs +0 -1
- package/src/repl.mjs +0 -2
- package/src/runtime/agent/orchestrator/internal-roles.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +27 -49
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +5 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +8 -27
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +52 -182
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +8 -30
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +258 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/channels/index.mjs +0 -30
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +1 -10
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +3 -24
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +9 -7
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/standalone/seeds.mjs +1 -11
- package/src/tui/App.jsx +35 -12
- package/src/tui/components/PromptInput.jsx +63 -2
- package/src/tui/components/ToolExecution.jsx +7 -2
- package/src/tui/components/tool-output-format.mjs +156 -22
- package/src/tui/components/tool-output-format.test.mjs +93 -1
- package/src/tui/dist/index.mjs +473 -116
- package/src/tui/markdown/format-token.mjs +267 -108
- package/src/tui/markdown/format-token.test.mjs +105 -9
- package/src/tui/theme.mjs +10 -0
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/vendor/ink/build/ink.js +54 -8
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
|
@@ -21,6 +21,7 @@ import stripAnsi from 'strip-ansi';
|
|
|
21
21
|
import {
|
|
22
22
|
extraColorizers,
|
|
23
23
|
highlightCodeLine,
|
|
24
|
+
highlightCodeBlockToLines,
|
|
24
25
|
colorizeDiffLine,
|
|
25
26
|
looksLikeUnifiedDiff,
|
|
26
27
|
LANG_FAMILY,
|
|
@@ -35,13 +36,72 @@ const DEFAULT_MARKDOWN_WIDTH = 80;
|
|
|
35
36
|
// Hard ceilings so a pathological tool result can never lock the render loop.
|
|
36
37
|
// CC uses ~MAX_LINES*width*4 for the collapsed fold; for the EXPANDED body we
|
|
37
38
|
// cap total characters processed and total lines kept, with an explicit marker.
|
|
38
|
-
const MAX_EXPANDED_CHARS = 256 * 1024; // 256 KB of text gets per-line processing
|
|
39
|
-
const MAX_EXPANDED_LINES =
|
|
39
|
+
export const MAX_EXPANDED_CHARS = 256 * 1024; // 256 KB of text gets per-line processing
|
|
40
|
+
const MAX_EXPANDED_LINES = 4000; // logical-line safety ceiling; physical mount cap is separate
|
|
40
41
|
const MAX_JSON_FORMAT_LENGTH = 10_000; // mirror CC's tryJsonFormatContent cap
|
|
41
42
|
const MAX_HIGHLIGHT_LINE_CHARS = 2000; // skip token-scan on absurdly long lines
|
|
42
43
|
// Lockstep with ToolExecution collapsed fit budget (MIN_RESULT_LINE_CHARS).
|
|
43
44
|
const MIN_EXPANDED_BODY_COLS = 24;
|
|
44
45
|
|
|
46
|
+
// Hard cap on the number of PHYSICAL (wrapped) rows a single expanded tool /
|
|
47
|
+
// script body may mount. MAX_EXPANDED_LINES bounds LOGICAL lines, but a single
|
|
48
|
+
// very long line with no newlines (minified JSON, a 256 KB single-line blob)
|
|
49
|
+
// wraps into thousands of physical rows — and EACH physical row is a mounted
|
|
50
|
+
// Ink <Text> node that ink re-serializes every frame even while clipped off-
|
|
51
|
+
// screen (clipping only trims write coords, not the serialize pass). So a lone
|
|
52
|
+
// huge expanded item could mount thousands of nodes and stall typing/drag even
|
|
53
|
+
// though it is the only visible transcript item. This cap bounds the mounted
|
|
54
|
+
// node count regardless of line shape; the omitted tail gets a clear marker.
|
|
55
|
+
// It is set generously above what any viewport needs so normal multi-line
|
|
56
|
+
// output (already capped at MAX_EXPANDED_LINES logical lines) is byte-for-byte
|
|
57
|
+
// unchanged — only pathological wide/unwrapped blobs are trimmed. Env-tunable
|
|
58
|
+
// via MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES (0 disables); legacy
|
|
59
|
+
// MIXDOG_TUI_EXPANDED_MAX_ROWS is still honored when the primary var is unset.
|
|
60
|
+
export function resolveToolOutputMaxRenderLines() {
|
|
61
|
+
const raw = process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES;
|
|
62
|
+
if (raw !== undefined && String(raw).trim() !== '') {
|
|
63
|
+
const v = Number(raw);
|
|
64
|
+
if (Number.isFinite(v) && v <= 0) return 0;
|
|
65
|
+
if (Number.isFinite(v) && v > 0) return Math.floor(v);
|
|
66
|
+
}
|
|
67
|
+
const legacy = process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
|
|
68
|
+
if (legacy !== undefined && String(legacy).trim() !== '') {
|
|
69
|
+
const v = Number(legacy);
|
|
70
|
+
if (Number.isFinite(v) && v > 0) return Math.floor(v);
|
|
71
|
+
}
|
|
72
|
+
return 600;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function omittedPhysicalRowsMarker(omitted, isShell) {
|
|
76
|
+
if (isShell) {
|
|
77
|
+
const n = Math.max(1, Math.floor(Number(omitted) || 0));
|
|
78
|
+
return `\u2026 [${n} line${n === 1 ? '' : 's'} omitted above \u2014 showing newest output below]`;
|
|
79
|
+
}
|
|
80
|
+
return '\u2026 [output truncated for display \u2014 collapse (ctrl+o) or re-read a narrower range]';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function shellLogicalOmittedMarker({ omittedLines = 0, omittedChars = false }, c) {
|
|
84
|
+
if (omittedLines > 0) {
|
|
85
|
+
return c.synComment(omittedPhysicalRowsMarker(omittedLines, true));
|
|
86
|
+
}
|
|
87
|
+
if (omittedChars) {
|
|
88
|
+
return c.synComment('\u2026 [earlier output omitted above \u2014 showing newest output below]');
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function finalizeShellPhysicalCap(buffer, omitted, maxRows) {
|
|
94
|
+
const bodySlots = Math.max(0, maxRows - 1);
|
|
95
|
+
if (omitted <= 0) {
|
|
96
|
+
return buffer.length > 0 ? buffer : [' '];
|
|
97
|
+
}
|
|
98
|
+
const tail = bodySlots > 0 ? buffer.slice(-bodySlots) : [];
|
|
99
|
+
const extraHidden = Math.max(0, buffer.length - bodySlots);
|
|
100
|
+
const totalOmitted = omitted + extraHidden;
|
|
101
|
+
const out = [omittedPhysicalRowsMarker(totalOmitted, true), ...tail];
|
|
102
|
+
return out.length > 0 ? out.slice(0, maxRows) : [' '];
|
|
103
|
+
}
|
|
104
|
+
|
|
45
105
|
// `<n>→<content>` (read) OR `<n>:<content>` / `<path>:<n>:<content>` (grep).
|
|
46
106
|
const READ_LINE_RE = /^(\s*)(\d+)(\u2192)(.*)$/;
|
|
47
107
|
// Gutter ends with `:<line>:`. Windows absolute paths use `X:\...` so the drive
|
|
@@ -198,9 +258,12 @@ export function formatExpandedResult(text, { pathArg = '', isShell = false } = {
|
|
|
198
258
|
|
|
199
259
|
// Oversized guard: slice before any O(n) per-line work, append a marker.
|
|
200
260
|
let truncatedChars = false;
|
|
261
|
+
let shellOmittedLines = 0;
|
|
262
|
+
let shellOmittedChars = false;
|
|
201
263
|
if (src.length > MAX_EXPANDED_CHARS) {
|
|
202
|
-
src = src.slice(0, MAX_EXPANDED_CHARS);
|
|
203
|
-
|
|
264
|
+
src = isShell ? src.slice(-MAX_EXPANDED_CHARS) : src.slice(0, MAX_EXPANDED_CHARS);
|
|
265
|
+
if (isShell) shellOmittedChars = true;
|
|
266
|
+
else truncatedChars = true;
|
|
204
267
|
}
|
|
205
268
|
|
|
206
269
|
const carriesAnsi = hasAnsi(src);
|
|
@@ -210,8 +273,9 @@ export function formatExpandedResult(text, { pathArg = '', isShell = false } = {
|
|
|
210
273
|
let lines = src.split('\n');
|
|
211
274
|
let truncatedLines = false;
|
|
212
275
|
if (lines.length > MAX_EXPANDED_LINES) {
|
|
213
|
-
|
|
214
|
-
|
|
276
|
+
if (isShell) shellOmittedLines = lines.length - MAX_EXPANDED_LINES;
|
|
277
|
+
lines = isShell ? lines.slice(-MAX_EXPANDED_LINES) : lines.slice(0, MAX_EXPANDED_LINES);
|
|
278
|
+
if (!isShell) truncatedLines = true;
|
|
215
279
|
}
|
|
216
280
|
|
|
217
281
|
const c = extraColorizers();
|
|
@@ -223,29 +287,31 @@ export function formatExpandedResult(text, { pathArg = '', isShell = false } = {
|
|
|
223
287
|
if (shouldRenderExpandedMarkdown({ src, lines, carriesAnsi, isShell, diffMode, family })) {
|
|
224
288
|
out = formatExpandedMarkdownLines(src);
|
|
225
289
|
if (out.length > MAX_EXPANDED_LINES) {
|
|
226
|
-
|
|
227
|
-
|
|
290
|
+
if (isShell) shellOmittedLines += out.length - MAX_EXPANDED_LINES;
|
|
291
|
+
out = isShell ? out.slice(-MAX_EXPANDED_LINES) : out.slice(0, MAX_EXPANDED_LINES);
|
|
292
|
+
if (!isShell) truncatedLines = true;
|
|
228
293
|
}
|
|
229
294
|
} else {
|
|
230
|
-
|
|
295
|
+
if (family && family !== 'md' && !diffMode && !carriesAnsi && !isShell) {
|
|
296
|
+
out = formatSyntaxBlock(lines, { c, family });
|
|
297
|
+
} else {
|
|
298
|
+
out = lines.map((line) => formatLine(line, { c, family, diffMode, carriesAnsi, isShell }));
|
|
299
|
+
}
|
|
231
300
|
}
|
|
232
301
|
|
|
302
|
+
if (isShell) {
|
|
303
|
+
const marker = shellLogicalOmittedMarker({ omittedLines: shellOmittedLines, omittedChars: shellOmittedChars }, c);
|
|
304
|
+
if (marker) return [marker, ...out];
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
233
307
|
if (truncatedChars || truncatedLines) {
|
|
234
308
|
out.push(c.synComment('… [output truncated for display — re-read a narrower range]'));
|
|
235
309
|
}
|
|
236
310
|
return out;
|
|
237
311
|
}
|
|
238
312
|
|
|
239
|
-
/**
|
|
240
|
-
function
|
|
241
|
-
// Shell / already-colored output: keep ANSI verbatim, only fix underline leaks
|
|
242
|
-
// and linkify URLs. No gutter split (shell has no <n>→ prefix).
|
|
243
|
-
if (carriesAnsi || isShell) {
|
|
244
|
-
return linkifyUrls(stripUnderlineAnsi(line));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Split a read (`<n>→`) or grep (`<file>:<n>:` / `<n>:`) gutter into a dim
|
|
248
|
-
// column so the body can be highlighted independently.
|
|
313
|
+
/** Split read/grep line-number gutter from body (shared by per-line and block highlight). */
|
|
314
|
+
function splitGutter(line) {
|
|
249
315
|
let indent = '';
|
|
250
316
|
let gutter = '';
|
|
251
317
|
let body = line;
|
|
@@ -262,6 +328,35 @@ function formatLine(line, { c, family, diffMode, carriesAnsi, isShell }) {
|
|
|
262
328
|
body = gm[4];
|
|
263
329
|
}
|
|
264
330
|
}
|
|
331
|
+
return { indent, gutter, body };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Block syntax highlight for non-diff code bodies (multi-line tokens color correctly). */
|
|
335
|
+
function formatSyntaxBlock(lines, { c, family }) {
|
|
336
|
+
const parts = lines.map(splitGutter);
|
|
337
|
+
const bodies = parts.map((p) =>
|
|
338
|
+
p.body.length > MAX_HIGHLIGHT_LINE_CHARS ? '' : p.body,
|
|
339
|
+
);
|
|
340
|
+
const highlighted = highlightCodeBlockToLines(bodies.join('\n'), family, c);
|
|
341
|
+
return parts.map((p, i) => {
|
|
342
|
+
const rawBody = p.body;
|
|
343
|
+
const bodyOut = rawBody.length > MAX_HIGHLIGHT_LINE_CHARS
|
|
344
|
+
? c.body(rawBody)
|
|
345
|
+
: (highlighted[i] ?? '');
|
|
346
|
+
const linked = linkifyUrls(bodyOut);
|
|
347
|
+
return p.gutter ? `${p.indent}${c.synComment(p.gutter)}${linked}` : linked;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Format ONE line: split line-number gutter, then highlight/linkify the body. */
|
|
352
|
+
function formatLine(line, { c, family, diffMode, carriesAnsi, isShell }) {
|
|
353
|
+
// Shell / already-colored output: keep ANSI verbatim, only fix underline leaks
|
|
354
|
+
// and linkify URLs. No gutter split (shell has no <n>→ prefix).
|
|
355
|
+
if (carriesAnsi || isShell) {
|
|
356
|
+
return linkifyUrls(stripUnderlineAnsi(line));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const { indent, gutter, body } = splitGutter(line);
|
|
265
360
|
|
|
266
361
|
const coloredBody = highlightBody(body, { c, family, diffMode });
|
|
267
362
|
const linked = linkifyUrls(coloredBody);
|
|
@@ -354,12 +449,51 @@ function wrapOneExpandedLogicalLine(line, maxWidth) {
|
|
|
354
449
|
* Turn logical expanded lines into physical rows that fit the body column.
|
|
355
450
|
* One output row per left-rail row in ToolExecution (lockstep with App row est.).
|
|
356
451
|
*/
|
|
357
|
-
export function wrapExpandedResultLines(logicalLines, columns = 80) {
|
|
452
|
+
export function wrapExpandedResultLines(logicalLines, columns = 80, { isShell = false } = {}) {
|
|
453
|
+
const maxRows = resolveToolOutputMaxRenderLines();
|
|
454
|
+
const capOn = maxRows > 0;
|
|
358
455
|
const maxWidth = expandedResultBodyWidth(columns);
|
|
359
456
|
const lines = Array.isArray(logicalLines) ? logicalLines : [];
|
|
360
457
|
const out = [];
|
|
361
|
-
|
|
362
|
-
|
|
458
|
+
|
|
459
|
+
if (!capOn) {
|
|
460
|
+
for (const line of lines) {
|
|
461
|
+
for (const row of wrapOneExpandedLogicalLine(line, maxWidth)) out.push(row);
|
|
462
|
+
}
|
|
463
|
+
return out.length > 0 ? out : [' '];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (isShell) {
|
|
467
|
+
let omitted = 0;
|
|
468
|
+
for (const line of lines) {
|
|
469
|
+
for (const row of wrapOneExpandedLogicalLine(line, maxWidth)) {
|
|
470
|
+
out.push(row);
|
|
471
|
+
if (out.length > maxRows) {
|
|
472
|
+
out.shift();
|
|
473
|
+
omitted += 1;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return finalizeShellPhysicalCap(out, omitted, maxRows);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Non-shell surfaces keep the head of the expanded body (read/grep/json).
|
|
481
|
+
let truncated = false;
|
|
482
|
+
outer: for (const line of lines) {
|
|
483
|
+
for (const row of wrapOneExpandedLogicalLine(line, maxWidth)) {
|
|
484
|
+
if (out.length < maxRows) {
|
|
485
|
+
out.push(row);
|
|
486
|
+
} else {
|
|
487
|
+
truncated = true;
|
|
488
|
+
break outer;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (truncated) {
|
|
493
|
+
const bodySlots = Math.max(0, maxRows - 1);
|
|
494
|
+
if (out.length > bodySlots) out.length = bodySlots;
|
|
495
|
+
if (maxRows > 0) out.push(omittedPhysicalRowsMarker(1, false));
|
|
496
|
+
return out.length > 0 ? out.slice(0, maxRows) : [' '];
|
|
363
497
|
}
|
|
364
498
|
return out.length > 0 ? out : [' '];
|
|
365
499
|
}
|
|
@@ -13,6 +13,21 @@ import stringWidth from 'string-width';
|
|
|
13
13
|
|
|
14
14
|
const stripAnsi = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b\]8;;[^\x07]*\x07/g, '');
|
|
15
15
|
|
|
16
|
+
function withRenderLineCap(cap, fn) {
|
|
17
|
+
const prev = process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES;
|
|
18
|
+
const legacy = process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
|
|
19
|
+
process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES = String(cap);
|
|
20
|
+
delete process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
|
|
21
|
+
try {
|
|
22
|
+
return fn();
|
|
23
|
+
} finally {
|
|
24
|
+
if (prev === undefined) delete process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES;
|
|
25
|
+
else process.env.MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES = prev;
|
|
26
|
+
if (legacy === undefined) delete process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS;
|
|
27
|
+
else process.env.MIXDOG_TUI_EXPANDED_MAX_ROWS = legacy;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
test('inferLangFamily maps known extensions, null otherwise', () => {
|
|
17
32
|
assert.equal(inferLangFamily('src/x.mjs'), 'js');
|
|
18
33
|
assert.equal(inferLangFamily('a.py'), 'py');
|
|
@@ -31,6 +46,22 @@ test('read line-number gutter is split into a dim column and body is highlighted
|
|
|
31
46
|
assert.ok(out[0].includes('\x1b['), 'first line carries color escapes');
|
|
32
47
|
});
|
|
33
48
|
|
|
49
|
+
test('block syntax highlight skips tokenizer for over-long lines (index aligned)', () => {
|
|
50
|
+
// MAX_HIGHLIGHT_LINE_CHARS is 2000 in tool-output-format.mjs
|
|
51
|
+
const giant = 'x'.repeat(2001);
|
|
52
|
+
const read = `1\u2192const a = 1;\n2\u2192${giant}\n3\u2192const b = 2;`;
|
|
53
|
+
const out = formatExpandedResult(read, { pathArg: 'a.mjs' });
|
|
54
|
+
assert.equal(out.length, 3);
|
|
55
|
+
assert.ok(out[0].includes('\x1b['), 'short line before giant is highlighted');
|
|
56
|
+
assert.ok(out[2].includes('\x1b['), 'short line after giant is highlighted');
|
|
57
|
+
const plainMid = stripAnsi(out[1]);
|
|
58
|
+
assert.ok(plainMid.startsWith('2\u2192'), 'gutter preserved on giant line');
|
|
59
|
+
assert.ok(plainMid.endsWith(giant), 'giant body preserved');
|
|
60
|
+
const hljsSpans = (out[1].match(/\x1b\[[0-9;]*m/g) || []).length;
|
|
61
|
+
const shortSpans = (out[0].match(/\x1b\[[0-9;]*m/g) || []).length;
|
|
62
|
+
assert.ok(hljsSpans <= shortSpans + 2, 'giant line not fully tokenized like normal code');
|
|
63
|
+
});
|
|
64
|
+
|
|
34
65
|
test('grep file:line: gutter is split too', () => {
|
|
35
66
|
const grep = 'src/a.mjs:5581: value: foo,';
|
|
36
67
|
const out = formatExpandedResult(grep, { pathArg: 'src/a.mjs' });
|
|
@@ -105,9 +136,13 @@ test('unified diff is colored by line class', () => {
|
|
|
105
136
|
test('oversize output is capped with a truncation marker', () => {
|
|
106
137
|
const many = Array.from({ length: 5000 }, (_, i) => `line${i}`).join('\n');
|
|
107
138
|
const out = formatExpandedResult(many, {});
|
|
108
|
-
assert.ok(out.length <=
|
|
139
|
+
assert.ok(out.length <= 4001, 'logical cap keeps at most MAX_EXPANDED_LINES plus marker');
|
|
109
140
|
assert.ok(/truncated/.test(stripAnsi(out[out.length - 1])));
|
|
110
141
|
|
|
142
|
+
const sixHundred = Array.from({ length: 600 }, (_, i) => `row${i}`).join('\n');
|
|
143
|
+
const out600 = formatExpandedResult(sixHundred, {});
|
|
144
|
+
assert.equal(out600.length, 600, 'normal expanded output is not truncated at 80 logical lines');
|
|
145
|
+
|
|
111
146
|
const huge = 'x'.repeat(300 * 1024);
|
|
112
147
|
const out2 = formatExpandedResult(huge, {});
|
|
113
148
|
assert.ok(/truncated/.test(stripAnsi(out2[out2.length - 1])));
|
|
@@ -218,3 +253,60 @@ test('read gutter lines skip markdown mode and keep syntax highlight', () => {
|
|
|
218
253
|
assert.ok(visible.some((l) => l.includes('const y = 2;')), 'read body preserved');
|
|
219
254
|
assert.ok(out.some((l) => stripAnsi(l).includes('const y = 2;') && l.includes('\x1b[')), 'code line still highlighted');
|
|
220
255
|
});
|
|
256
|
+
|
|
257
|
+
test('shell physical cap keeps newest rows across logical lines', () => {
|
|
258
|
+
withRenderLineCap(5, () => {
|
|
259
|
+
const long = 'W'.repeat(240);
|
|
260
|
+
const logical = formatExpandedResult(`${long}\nNEWEST_TAIL_LINE`, { isShell: true });
|
|
261
|
+
const physical = wrapExpandedResultLines(logical, 32, { isShell: true });
|
|
262
|
+
const visible = physical.map(stripAnsi);
|
|
263
|
+
assert.ok(visible.some((l) => l.includes('NEWEST_TAIL_LINE')), 'newest logical line survives rolling cap');
|
|
264
|
+
assert.ok(physical.length <= 5);
|
|
265
|
+
assert.ok(visible.some((l) => /omitted above/i.test(l)));
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('exact physical cap row count is not truncated', () => {
|
|
270
|
+
withRenderLineCap(10, () => {
|
|
271
|
+
const logical = Array.from({ length: 10 }, (_, i) => `row-${i}`).map((l) => formatExpandedResult(l, { isShell: true })[0]);
|
|
272
|
+
const physical = wrapExpandedResultLines(logical, 80, { isShell: true });
|
|
273
|
+
assert.equal(physical.length, 10);
|
|
274
|
+
assert.ok(!physical.some((l) => /omitted above/i.test(stripAnsi(l))));
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('physical cap of 1 never returns more than one row', () => {
|
|
279
|
+
withRenderLineCap(1, () => {
|
|
280
|
+
const physical = wrapExpandedResultLines(formatExpandedResult('a\nb\nc', { isShell: true }), 80, { isShell: true });
|
|
281
|
+
assert.ok(physical.length <= 1);
|
|
282
|
+
const physicalNonShell = wrapExpandedResultLines(formatExpandedResult('a\nb\nc', {}), 80, { isShell: false });
|
|
283
|
+
assert.ok(physicalNonShell.length <= 1);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('shell physical cap omitted count includes marker slot', () => {
|
|
288
|
+
withRenderLineCap(5, () => {
|
|
289
|
+
const logical = ['r0', 'r1', 'r2', 'r3', 'r4', 'r5'].map((l) => formatExpandedResult(l, { isShell: true })[0]);
|
|
290
|
+
const physical = wrapExpandedResultLines(logical, 80, { isShell: true });
|
|
291
|
+
const visible = physical.map(stripAnsi);
|
|
292
|
+
assert.equal(physical.length, 5);
|
|
293
|
+
assert.match(visible[0], /2 lines omitted above/);
|
|
294
|
+
assert.deepEqual(visible.slice(1), ['r2', 'r3', 'r4', 'r5']);
|
|
295
|
+
});
|
|
296
|
+
withRenderLineCap(1, () => {
|
|
297
|
+
const logical = Array.from({ length: 6 }, (_, i) => `row${i}`).map((l) => formatExpandedResult(l, { isShell: true })[0]);
|
|
298
|
+
const physical = wrapExpandedResultLines(logical, 80, { isShell: true });
|
|
299
|
+
assert.equal(physical.length, 1);
|
|
300
|
+
assert.match(stripAnsi(physical[0]), /6 lines omitted above/);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('shell logical truncation marker precedes retained tail', () => {
|
|
305
|
+
const many = Array.from({ length: 5000 }, (_, i) => `line${i}`).join('\n');
|
|
306
|
+
const out = formatExpandedResult(many, { isShell: true });
|
|
307
|
+
const visible = out.map(stripAnsi);
|
|
308
|
+
assert.ok(visible.length <= 4001);
|
|
309
|
+
assert.ok(/omitted above/i.test(visible[0]), 'shell marker is first row');
|
|
310
|
+
assert.ok(visible[visible.length - 1].includes('line4999'), 'newest logical line is last');
|
|
311
|
+
assert.ok(!visible[visible.length - 1].includes('re-read a narrower range'));
|
|
312
|
+
});
|