@semalt-ai/code 1.8.5 → 1.20.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.
Files changed (192) hide show
  1. package/.claude/settings.local.json +7 -1
  2. package/.github/workflows/ci.yml +69 -0
  3. package/ARCHITECTURE.md +6 -95
  4. package/CLAUDE.md +196 -316
  5. package/README.md +148 -4
  6. package/docs/ARCHITECTURE.md +1321 -0
  7. package/docs/CONFIG.md +340 -0
  8. package/docs/HISTORY.md +245 -0
  9. package/examples/embed.js +74 -0
  10. package/index.js +251 -10
  11. package/lib/agent.js +856 -120
  12. package/lib/api.js +239 -50
  13. package/lib/args.js +74 -2
  14. package/lib/audit.js +23 -1
  15. package/lib/background.js +584 -0
  16. package/lib/checkpoints.js +757 -0
  17. package/lib/commands/auth.js +94 -0
  18. package/lib/commands/chat-session.js +489 -0
  19. package/lib/commands/chat-slash.js +415 -0
  20. package/lib/commands/chat-turn.js +669 -0
  21. package/lib/commands/chat.js +407 -0
  22. package/lib/commands/custom.js +157 -0
  23. package/lib/commands/history-utils.js +66 -0
  24. package/lib/commands/index.js +268 -0
  25. package/lib/commands/mcp.js +113 -0
  26. package/lib/commands/oneshot.js +193 -0
  27. package/lib/commands/registry.js +269 -0
  28. package/lib/commands/tasks.js +89 -0
  29. package/lib/compact.js +87 -0
  30. package/lib/config.js +360 -11
  31. package/lib/constants.js +401 -3
  32. package/lib/deny.js +199 -0
  33. package/lib/doctor.js +160 -0
  34. package/lib/headless.js +202 -0
  35. package/lib/hooks.js +286 -0
  36. package/lib/images.js +270 -0
  37. package/lib/internals.js +49 -0
  38. package/lib/mcp/boundary.js +131 -0
  39. package/lib/mcp/client.js +270 -0
  40. package/lib/mcp/oauth.js +134 -0
  41. package/lib/memory.js +209 -0
  42. package/lib/metrics.js +37 -2
  43. package/lib/payload.js +54 -0
  44. package/lib/permission-rules.js +401 -0
  45. package/lib/permissions.js +123 -26
  46. package/lib/pricing.js +67 -0
  47. package/lib/proc.js +62 -0
  48. package/lib/prompts.js +99 -8
  49. package/lib/sandbox.js +568 -0
  50. package/lib/sdk.js +328 -0
  51. package/lib/secrets.js +211 -0
  52. package/lib/skills.js +223 -0
  53. package/lib/subagents.js +516 -0
  54. package/lib/tool_registry.js +2862 -0
  55. package/lib/tool_specs.js +263 -9
  56. package/lib/tools.js +352 -1039
  57. package/lib/ui/anim.js +86 -0
  58. package/lib/ui/ansi.js +17 -27
  59. package/lib/ui/chat-history.js +253 -71
  60. package/lib/ui/create-ui.js +67 -24
  61. package/lib/ui/diff.js +90 -25
  62. package/lib/ui/file-activity.js +236 -0
  63. package/lib/ui/format.js +195 -29
  64. package/lib/ui/input-field.js +21 -11
  65. package/lib/ui/md-stream.js +234 -0
  66. package/lib/ui/render-operation.js +113 -0
  67. package/lib/ui/select.js +1 -4
  68. package/lib/ui/status-bar.js +146 -36
  69. package/lib/ui/stream.js +20 -13
  70. package/lib/ui/theme.js +190 -44
  71. package/lib/ui/tool-operation.js +190 -0
  72. package/lib/ui/utils.js +9 -5
  73. package/lib/ui/web-activity.js +270 -0
  74. package/lib/ui/writer.js +159 -45
  75. package/lib/ui.js +1 -1
  76. package/lib/verify.js +229 -0
  77. package/lib/web-extract.js +213 -0
  78. package/lib/web-summarize.js +68 -0
  79. package/package.json +19 -4
  80. package/scripts/lint.js +57 -0
  81. package/test/agent-loop.test.js +389 -0
  82. package/test/anim-driver.test.js +153 -0
  83. package/test/ask-user-display.test.js +226 -0
  84. package/test/ask-user-gate.test.js +231 -0
  85. package/test/background.test.js +414 -0
  86. package/test/chat-history-nocolor.test.js +155 -0
  87. package/test/chat-relogin.test.js +207 -0
  88. package/test/chat.test.js +114 -0
  89. package/test/checkpoints-agent.test.js +181 -0
  90. package/test/checkpoints.test.js +650 -0
  91. package/test/command-registry.test.js +160 -0
  92. package/test/compact.test.js +116 -0
  93. package/test/completion-lazy.test.js +52 -0
  94. package/test/config-merge.test.js +324 -0
  95. package/test/config-quarantine.test.js +128 -0
  96. package/test/config-write-guard-allow-anywhere.test.js +56 -0
  97. package/test/config-write-guard-skip.test.js +46 -0
  98. package/test/config-write-guard.test.js +153 -0
  99. package/test/context-split.test.js +215 -0
  100. package/test/cost-doctor.test.js +142 -0
  101. package/test/custom-commands-chat.test.js +106 -0
  102. package/test/custom-commands.test.js +230 -0
  103. package/test/defer-detail-band.test.js +403 -0
  104. package/test/deny-windows.test.js +120 -0
  105. package/test/deny.test.js +83 -0
  106. package/test/detail-band-tab-flatten.test.js +242 -0
  107. package/test/download-allow-anywhere.test.js +66 -0
  108. package/test/download-confine.test.js +153 -0
  109. package/test/exec-diff.test.js +268 -0
  110. package/test/executors.test.js +599 -0
  111. package/test/extract-tool-calls.test.js +349 -0
  112. package/test/fetch-url-validation.test.js +219 -0
  113. package/test/file-activity.test.js +522 -0
  114. package/test/fixtures/tool-calls.js +57 -0
  115. package/test/fixtures/web-page.js +91 -0
  116. package/test/git-tools.test.js +384 -0
  117. package/test/grep-glob-serialize.test.js +242 -0
  118. package/test/grep-glob.test.js +268 -0
  119. package/test/grep-path-target.test.js +227 -0
  120. package/test/harness/README.md +57 -0
  121. package/test/harness/chat-harness.js +143 -0
  122. package/test/harness/memwarn-headless-child.js +65 -0
  123. package/test/harness/mock-llm.js +120 -0
  124. package/test/harness/mock-mcp-server.js +142 -0
  125. package/test/harness/sse-server.js +69 -0
  126. package/test/headless.test.js +348 -0
  127. package/test/history-utils.test.js +88 -0
  128. package/test/hooks-agent.test.js +238 -0
  129. package/test/hooks-verify-sandbox.test.js +232 -0
  130. package/test/hooks.test.js +216 -0
  131. package/test/http-get-user-agent.test.js +142 -0
  132. package/test/images-api.test.js +208 -0
  133. package/test/images.test.js +238 -0
  134. package/test/input-field-ctrl-o.test.js +37 -0
  135. package/test/live-height-physical.test.js +281 -0
  136. package/test/max-iterations.test.js +218 -0
  137. package/test/mcp-boundary.test.js +57 -0
  138. package/test/mcp-client.test.js +267 -0
  139. package/test/mcp-oauth.test.js +86 -0
  140. package/test/md-stream.test.js +183 -0
  141. package/test/memory-truncation-warning.test.js +222 -0
  142. package/test/memory.test.js +198 -0
  143. package/test/native-dispatch.test.js +409 -0
  144. package/test/native-live-narration.test.js +254 -0
  145. package/test/output-chokepoint.test.js +188 -0
  146. package/test/output-heredoc-leak.test.js +195 -0
  147. package/test/output-preview.test.js +245 -0
  148. package/test/path-guards.test.js +134 -0
  149. package/test/payload.test.js +99 -0
  150. package/test/permission-rules-agent.test.js +210 -0
  151. package/test/permission-rules.test.js +297 -0
  152. package/test/permissions.test.js +362 -0
  153. package/test/plan-mode.test.js +167 -0
  154. package/test/read-paginate.test.js +275 -0
  155. package/test/readonly-tools.test.js +177 -0
  156. package/test/render-operation.test.js +317 -0
  157. package/test/replay-descriptor-xml.test.js +216 -0
  158. package/test/replay-descriptor.test.js +189 -0
  159. package/test/replay-web-aggregate.test.js +291 -0
  160. package/test/replay-web-persist.test.js +241 -0
  161. package/test/result-cap.test.js +233 -0
  162. package/test/running-glyph-anim.test.js +111 -0
  163. package/test/sandbox-agent.test.js +147 -0
  164. package/test/sandbox-integration.test.js +216 -0
  165. package/test/sandbox.test.js +408 -0
  166. package/test/sdk.test.js +234 -0
  167. package/test/shell-output-cap.test.js +181 -0
  168. package/test/skills-chat.test.js +110 -0
  169. package/test/skills.test.js +295 -0
  170. package/test/smoke.test.js +68 -0
  171. package/test/status-bar-driver.test.js +93 -0
  172. package/test/status-bar-pause.test.js +164 -0
  173. package/test/status-bar-resync.test.js +188 -0
  174. package/test/stream-parser.test.js +171 -0
  175. package/test/subagents-agent.test.js +178 -0
  176. package/test/subagents.test.js +222 -0
  177. package/test/theme-palette.test.js +166 -0
  178. package/test/tool-registry.test.js +85 -0
  179. package/test/trim-budget.test.js +101 -0
  180. package/test/truncate-visible.test.js +78 -0
  181. package/test/verify-agent.test.js +317 -0
  182. package/test/verify.test.js +141 -0
  183. package/test/view-image.test.js +199 -0
  184. package/test/web-activity-ordering.test.js +203 -0
  185. package/test/web-activity.test.js +207 -0
  186. package/test/web-data-extraction-guidance.test.js +71 -0
  187. package/test/web-extract.test.js +185 -0
  188. package/test/web-fetch-agent.test.js +291 -0
  189. package/test/web-fetch-mode.test.js +193 -0
  190. package/test/web-search.test.js +380 -0
  191. package/lib/commands.js +0 -1438
  192. package/path +0 -1
@@ -0,0 +1,268 @@
1
+ 'use strict';
2
+
3
+ // Execution-time file-edit diffs.
4
+ //
5
+ // The fix moves file-edit diff rendering OUT of the permission modal and INTO
6
+ // the execution-result path, so the diff renders for EVERY edit regardless of
7
+ // approval state (manual-approved, auto-approved) or entry mode (fresh /
8
+ // --resume / /history / /chats). These tests pin the three layers of that fix:
9
+ //
10
+ // 1. lib/ui/diff.js renderDiff — the changed-line cap (head+tail + notice for
11
+ // a large edit; full render for a small edit or a series of small edits).
12
+ // 2. lib/ui/diff.js buildExecutionDiff — the once-per-edit decision: renders
13
+ // when a payload is present, returns null on error / no payload (a loaded
14
+ // history turn carries none, so past turns are NOT replayed) / no-op edit.
15
+ // 3. The mutating executors (write/append/edit_file/replace_in_file) attach
16
+ // the before/after payload UNCONDITIONALLY — not gated by the permission
17
+ // modal — which is what makes an auto-approved edit show its diff.
18
+ //
19
+ // Home-based paths are redirected into a temp dir BEFORE any lib module loads,
20
+ // matching test/readonly-tools.test.js.
21
+
22
+ const os = require('node:os');
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+
26
+ const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'semalt-diff-home-'));
27
+ const PREV_HOME = process.env.HOME;
28
+ const PREV_USERPROFILE = process.env.USERPROFILE;
29
+ process.env.HOME = TMP_HOME;
30
+ process.env.USERPROFILE = TMP_HOME;
31
+
32
+ const { test, before, after } = require('node:test');
33
+ const assert = require('node:assert');
34
+
35
+ const ui = require('../lib/ui');
36
+ const { renderDiff, buildExecutionDiff } = require('../lib/ui/diff');
37
+ const { summarizeToolResult } = require('../lib/ui/format');
38
+ const { createPermissionManager } = require('../lib/permissions');
39
+ const { createToolExecutor } = require('../lib/tools');
40
+ const { DEFAULT_CONFIG } = require('../lib/constants');
41
+
42
+ // renderDiff styles with ANSI only when stdout.isTTY; under `node --test` it is
43
+ // false, so output is already plain. stripAnsi defends against a TTY harness.
44
+ const stripAnsi = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, '');
45
+
46
+ function mkExec({ config = {}, pmOpts = {} } = {}) {
47
+ const pm = createPermissionManager(ui, pmOpts);
48
+ return createToolExecutor(pm, ui, () => ({
49
+ max_file_size_kb: 512,
50
+ command_timeout_ms: 30000,
51
+ max_output_lines: 50,
52
+ ...config,
53
+ }));
54
+ }
55
+
56
+ let CWD;
57
+ let PREV_CWD;
58
+
59
+ before(() => {
60
+ PREV_CWD = process.cwd();
61
+ CWD = fs.mkdtempSync(path.join(os.tmpdir(), 'semalt-diff-cwd-'));
62
+ process.chdir(CWD);
63
+ });
64
+
65
+ after(() => {
66
+ process.chdir(PREV_CWD);
67
+ if (PREV_HOME === undefined) delete process.env.HOME; else process.env.HOME = PREV_HOME;
68
+ if (PREV_USERPROFILE === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = PREV_USERPROFILE;
69
+ });
70
+
71
+ const tmpFile = (name, content) => {
72
+ const p = path.join(CWD, name);
73
+ fs.writeFileSync(p, content);
74
+ return p;
75
+ };
76
+
77
+ // ── 1. renderDiff cap ───────────────────────────────────────────────────────
78
+
79
+ test('renderDiff: small edit renders fully with no truncation notice', () => {
80
+ const out = stripAnsi(renderDiff('a\nb\nc', 'a\nB\nc', 'f.txt', { maxLines: 50 }));
81
+ assert.match(out, /B/, 'the changed line is shown');
82
+ assert.doesNotMatch(out, /more changed lines/, 'no cap notice for a small edit');
83
+ });
84
+
85
+ test('renderDiff: edit exceeding diff_max_lines renders head+tail + notice', () => {
86
+ const before = Array.from({ length: 100 }, (_, i) => `old ${i}`).join('\n');
87
+ const after = Array.from({ length: 100 }, (_, i) => `new ${i}`).join('\n');
88
+ const out = stripAnsi(renderDiff(before, after, 'big.txt', { maxLines: 10 }));
89
+ // 100 removed + 100 added = 200 changed lines, capped at 10 (head 6 + tail 4).
90
+ assert.match(out, /…\s*190 more changed lines \(200 total\)/, 'notice reports elided + total changed lines');
91
+ // Head shows the first removed line; tail shows the last added line.
92
+ assert.match(out, /old 0/, 'head of the diff is present');
93
+ assert.match(out, /new 99/, 'tail of the diff is present');
94
+ // Bounded: head(6) + notice + tail(4) = 11 lines, far below the input.
95
+ assert.ok(out.split('\n').length <= 14, 'capped output is bounded');
96
+ });
97
+
98
+ test('renderDiff: a series of small edits each render fully (no spurious truncation)', () => {
99
+ for (let i = 0; i < 5; i++) {
100
+ const before = `line one\nline two\nline three`;
101
+ const after = `line one\nline two CHANGED ${i}\nline three`;
102
+ const out = stripAnsi(renderDiff(before, after, `s${i}.txt`, { maxLines: 50 }));
103
+ assert.match(out, new RegExp(`CHANGED ${i}`), `edit ${i} renders its change`);
104
+ assert.doesNotMatch(out, /more changed lines/, `edit ${i} is not truncated`);
105
+ }
106
+ });
107
+
108
+ test('renderDiff: a large NEW file is also capped on changed (added) lines', () => {
109
+ const after = Array.from({ length: 80 }, (_, i) => `gen ${i}`).join('\n');
110
+ const out = stripAnsi(renderDiff('', after, 'new.txt', { maxLines: 10 }));
111
+ assert.match(out, /more changed lines \(80 total\)/, 'new-file additions count as changed lines');
112
+ });
113
+
114
+ // ── 1b. D3 (Output Refactor Phase 2): the diff body no longer repeats the path ─
115
+ // The result line above the diff already states the file path (the descriptor's
116
+ // `target`). The diff body therefore drops its redundant path header — the path
117
+ // is stated once, on the result line. The @@ hunk ranges and the (new file)
118
+ // marker remain; only the bare path-header line is gone.
119
+
120
+ test('D3: an existing-file diff body does NOT restate the file path', () => {
121
+ const out = stripAnsi(renderDiff('a\nb\nc', 'a\nB\nc', 'unique/path/to/file.js', { maxLines: 50 }));
122
+ assert.doesNotMatch(out, /unique\/path\/to\/file\.js/, 'the path is not repeated in the diff body');
123
+ assert.match(out, /B/, 'the changed line is still shown');
124
+ assert.match(out, /@@/, 'the hunk range header remains');
125
+ });
126
+
127
+ test('D3: the capped diff body also omits the redundant path header', () => {
128
+ const before = Array.from({ length: 100 }, (_, i) => `old ${i}`).join('\n');
129
+ const after = Array.from({ length: 100 }, (_, i) => `new ${i}`).join('\n');
130
+ const out = stripAnsi(renderDiff(before, after, 'capped/unique-path.txt', { maxLines: 10 }));
131
+ assert.doesNotMatch(out, /capped\/unique-path\.txt/, 'no path header on the capped path');
132
+ // The cap notice + head + tail are unchanged.
133
+ assert.match(out, /…\s*190 more changed lines \(200 total\)/);
134
+ assert.match(out, /old 0/, 'head of the diff is present');
135
+ assert.match(out, /new 99/, 'tail of the diff is present');
136
+ });
137
+
138
+ test('D3: a new-file diff keeps its @@ (new file) marker but not a path header', () => {
139
+ const out = stripAnsi(renderDiff('', 'one\ntwo\nthree', 'brand/new-file.js', { maxLines: 50 }));
140
+ assert.doesNotMatch(out, /brand\/new-file\.js/, 'no path header for a new file');
141
+ assert.match(out, /\(new file\)/, 'the new-file hunk marker remains');
142
+ assert.match(out, /@@ -0,0 \+1,3 @@/, 'the new-file hunk range remains');
143
+ });
144
+
145
+ // ── 2. buildExecutionDiff (the once-per-edit decision) ───────────────────────
146
+
147
+ test('buildExecutionDiff: renders the diff when a payload is present', () => {
148
+ const out = buildExecutionDiff({ diff: { before: 'a\nb', after: 'a\nB', path: 'f.txt' }, maxLines: 50 });
149
+ assert.ok(out, 'a diff string is produced');
150
+ assert.match(stripAnsi(out), /B/);
151
+ });
152
+
153
+ test('buildExecutionDiff: returns null on a failed edit (no diff under an error)', () => {
154
+ const out = buildExecutionDiff({ diff: { before: 'a', after: 'b', path: 'f' }, error: { message: 'boom' }, maxLines: 50 });
155
+ assert.strictEqual(out, null);
156
+ });
157
+
158
+ test('buildExecutionDiff: returns null with no payload — loaded turns are not replayed', () => {
159
+ // A loaded-history tool result reaches the UI without a before/after payload
160
+ // (diffs are never persisted), so nothing replays for past turns.
161
+ assert.strictEqual(buildExecutionDiff({ diff: null, maxLines: 50 }), null);
162
+ assert.strictEqual(buildExecutionDiff({ maxLines: 50 }), null);
163
+ assert.strictEqual(buildExecutionDiff({ diff: { before: 'a' }, maxLines: 50 }), null);
164
+ });
165
+
166
+ test('buildExecutionDiff: returns null for a no-op edit (before === after)', () => {
167
+ assert.strictEqual(buildExecutionDiff({ diff: { before: 'x\ny', after: 'x\ny', path: 'f' }, maxLines: 50 }), null);
168
+ });
169
+
170
+ test('buildExecutionDiff: honors diff_max_lines for the cap', () => {
171
+ const before = Array.from({ length: 60 }, (_, i) => `o${i}`).join('\n');
172
+ const after = Array.from({ length: 60 }, (_, i) => `n${i}`).join('\n');
173
+ const out = stripAnsi(buildExecutionDiff({ diff: { before, after, path: 'p' }, maxLines: 8 }));
174
+ assert.match(out, /more changed lines \(120 total\)/);
175
+ });
176
+
177
+ // ── 3. Executors attach the before/after payload unconditionally ─────────────
178
+ // The payload is produced by the executor itself, NOT by the permission modal —
179
+ // so it is present whether the call was manual-approved, auto-approved, or run
180
+ // in any entry mode. (The agent loop forwards it to onToolEnd, which renders it
181
+ // exactly once — the modal no longer carries the full diff.)
182
+
183
+ test('write (new file): payload captures empty before → full content after', async () => {
184
+ const exec = mkExec();
185
+ const f = path.join(CWD, 'created.txt');
186
+ const r = await exec.agentExecFile('write', f, 'hello\nworld');
187
+ assert.strictEqual(r.status, 'ok');
188
+ assert.strictEqual(r._diffBefore, '');
189
+ assert.strictEqual(r._diffAfter, 'hello\nworld');
190
+ });
191
+
192
+ test('write (existing file): payload captures prior content as before', async () => {
193
+ const exec = mkExec();
194
+ const f = tmpFile('over.txt', 'old text');
195
+ const r = await exec.agentExecFile('write', f, 'new text');
196
+ assert.strictEqual(r._diffBefore, 'old text');
197
+ assert.strictEqual(r._diffAfter, 'new text');
198
+ });
199
+
200
+ test('append: payload after is prior + appended content', async () => {
201
+ const exec = mkExec();
202
+ const f = tmpFile('log.txt', 'line1\n');
203
+ const r = await exec.agentExecFile('append', f, 'line2\n');
204
+ assert.strictEqual(r._diffBefore, 'line1\n');
205
+ assert.strictEqual(r._diffAfter, 'line1\nline2\n');
206
+ });
207
+
208
+ test('edit_file (single line): payload reflects the one-line swap', async () => {
209
+ const exec = mkExec();
210
+ const f = tmpFile('e.txt', 'a\nb\nc');
211
+ const r = await exec.agentExecFile('edit_file', f, 2, 'B');
212
+ assert.strictEqual(r._diffBefore, 'a\nb\nc');
213
+ assert.strictEqual(r._diffAfter, 'a\nB\nc');
214
+ });
215
+
216
+ test('edit_file (line range): payload reflects the block replacement', async () => {
217
+ const exec = mkExec();
218
+ const f = tmpFile('r.txt', 'a\nb\nc\nd');
219
+ const r = await exec.agentExecFile('edit_file', f, 2, 'X\nY\nZ', 3);
220
+ assert.strictEqual(r._diffBefore, 'a\nb\nc\nd');
221
+ assert.strictEqual(r._diffAfter, 'a\nX\nY\nZ\nd');
222
+ });
223
+
224
+ test('replace_in_file: payload captures before/after of the replacement', async () => {
225
+ const exec = mkExec();
226
+ const f = tmpFile('rep.txt', 'foo bar foo');
227
+ const r = await exec.agentExecFile('replace_in_file', f, 'bar', 'baz', '');
228
+ assert.strictEqual(r._diffBefore, 'foo bar foo');
229
+ assert.strictEqual(r._diffAfter, 'foo baz foo');
230
+ });
231
+
232
+ test('a failed edit attaches NO payload (out-of-range line)', async () => {
233
+ const exec = mkExec();
234
+ const f = tmpFile('oor.txt', 'one\ntwo');
235
+ const r = await exec.agentExecFile('edit_file', f, 99, 'X');
236
+ assert.ok(r.error, 'the edit failed');
237
+ assert.strictEqual(r._diffBefore, undefined, 'no diff payload on a failed edit');
238
+ });
239
+
240
+ // End-to-end through buildExecutionDiff: the executor payload renders a real diff
241
+ // for an auto-approve-style call (no modal involved in this harness at all).
242
+ test('parity: an executor payload renders a diff with no modal in the loop', async () => {
243
+ const exec = mkExec();
244
+ const f = tmpFile('parity.txt', 'keep\nold\nkeep');
245
+ const r = await exec.agentExecFile('edit_file', f, 2, 'new');
246
+ const out = stripAnsi(buildExecutionDiff({
247
+ diff: { before: r._diffBefore, after: r._diffAfter, path: f },
248
+ maxLines: DEFAULT_CONFIG.diff_max_lines,
249
+ }));
250
+ assert.match(out, /new/, 'the new content shows in the rendered diff');
251
+ assert.match(out, /old/, 'the removed content shows in the rendered diff');
252
+ });
253
+
254
+ // ── 4. No replay: loaded history collapses to summaries, not diffs ───────────
255
+
256
+ test('loaded-history file results summarize to a one-liner (never a diff)', () => {
257
+ // displayLoadedMessages renders stored tool results through summarizeToolResult.
258
+ // A persisted write result is a short "Wrote …" line — there is no diff to replay.
259
+ const summary = summarizeToolResult('Wrote 42 bytes to /tmp/x.txt');
260
+ assert.strictEqual(summary, 'Wrote 42 bytes to /tmp/x.txt');
261
+ assert.doesNotMatch(summary, /more changed lines/);
262
+ });
263
+
264
+ // ── config default ───────────────────────────────────────────────────────────
265
+
266
+ test('diff_max_lines has a sane default', () => {
267
+ assert.strictEqual(DEFAULT_CONFIG.diff_max_lines, 50);
268
+ });