atom-agent 1.3.0 → 1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0 — 2026-09-11
4
+
5
+ - Uncapped TUI diffs (`src/ui/diff.ts`, `src/ui/side-by-side.tsx`,
6
+ `src/ui/diff-view.tsx`, `src/ui/transcript.tsx`, `src/ui/modals.tsx`,
7
+ `src/ui/diff-panel.tsx`): the diff engine no longer truncates at 400
8
+ changed lines — `computeDiff` and `computeSideBySide` return the full
9
+ hunk/row list with `truncated: false`, and the transcript, approval
10
+ preview, and `/diff` panel render it whole (no more `… N more rows` or
11
+ `(diff truncated at 400 changed lines)` trailers). A 500-line write now
12
+ shows its complete before/after instead of a capped head. Smoothness is
13
+ preserved by the existing per-mount memoization (`SideBySideInner`,
14
+ `DiffViewInner`, `LineBody`, `TranscriptRow`), append-once `<Static>`
15
+ commits, and the engine's linear-time fallbacks (Myers prefix/suffix past
16
+ 1000 lines, flat word runs past 200 tokens/line). Safety caps stay
17
+ enforced: binary detect and the 1MB file skip. `MAX_CHANGED_LINES`,
18
+ `TRANSCRIPT_DIFF_MAX_LINES`, `APPROVAL_DIFF_MAX_LINES`, and
19
+ `DIFF_PANEL_MAX_LINES` are retained as compatibility exports (the row
20
+ caps now read `Infinity`); `maxRows`/`maxLines` remain as opt-in windows
21
+ for callers that want a collapsed tail. Known tradeoff: large approvals
22
+ grow the permission modal, pushing the `y/a/t/n` options further down —
23
+ the file on disk was and remains the whole truth
24
+
3
25
  ## 1.3.0 — 2026-09-11
4
26
 
5
27
  - Session goals (`src/goal.ts`, `src/App.tsx`, `src/agent/loop.ts`,
@@ -4,10 +4,10 @@ import { computeDiff } from "./diff.js";
4
4
  import { SideBySideDiffView } from "./side-by-side.js";
5
5
  import { theme } from "./theme.js";
6
6
  import { LIST_WINDOW, windowedList } from "./tool-inspector.js";
7
- // Cap for the panel detail (hunk headers excluded): full-file writes can
8
- // be large the head reviews, the trailer names the remainder, the file
9
- // on disk is the whole truth.
10
- export const DIFF_PANEL_MAX_LINES = 200;
7
+ // Retained for compatibility (no longer applied the panel renders the
8
+ // full preview; smoothness comes from the per-mount memo + word fallbacks,
9
+ // not from a row cap).
10
+ export const DIFF_PANEL_MAX_LINES = Infinity;
11
11
  // Group committed session previews by file: latest preview per path
12
12
  // wins (a later edit supersedes the earlier view of the same file),
13
13
  // first-seen path order kept. Previews without a path are ungroupable
@@ -51,5 +51,5 @@ export function DiffPanel({ files, index, expanded }) {
51
51
  return (_jsxs(Text, { color: hi ? theme.color.selection : undefined, children: [hi ? `${theme.symbol.select} ` : theme.spacing.rowIndent, "+", r.adds, " \u2212", r.dels, " ", r.path] }, r.path));
52
52
  }), win.below > 0 ? (_jsxs(Text, { dimColor: true, children: [theme.symbol.moreBelow, " ", win.below, " more"] })) : null, _jsxs(Text, { dimColor: true, children: [theme.symbol.moreAbove, "/", theme.symbol.moreBelow, " move \u00B7 Enter expands \u00B7 Esc closes"] })] }));
53
53
  }
54
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [rec.path, _jsxs(Text, { dimColor: true, children: [" ", theme.symbol.separator, " +", rec.adds, " \u2212", rec.dels] })] }), _jsx(Text, { dimColor: true, children: theme.symbol.rule.repeat(32) }), _jsx(SideBySideDiffView, { oldText: rec.oldText, newText: rec.newText, lang: rec.lang, maxRows: DIFF_PANEL_MAX_LINES }), _jsx(Text, { dimColor: true, children: theme.symbol.rule.repeat(32) }), _jsxs(Text, { dimColor: true, children: [theme.symbol.moreAbove, "/", theme.symbol.moreBelow, " prev/next file \u00B7 Enter collapses \u00B7 Esc closes"] })] }));
54
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [rec.path, _jsxs(Text, { dimColor: true, children: [" ", theme.symbol.separator, " +", rec.adds, " \u2212", rec.dels] })] }), _jsx(Text, { dimColor: true, children: theme.symbol.rule.repeat(32) }), _jsx(SideBySideDiffView, { oldText: rec.oldText, newText: rec.newText, lang: rec.lang }), _jsx(Text, { dimColor: true, children: theme.symbol.rule.repeat(32) }), _jsxs(Text, { dimColor: true, children: [theme.symbol.moreAbove, "/", theme.symbol.moreBelow, " prev/next file \u00B7 Enter collapses \u00B7 Esc closes"] })] }));
55
55
  }
@@ -6,8 +6,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  // changed-word backgrounds + syntax foregrounds (ui/highlight, zero-dep),
7
7
  // one <Text> per run (no per-character nodes), word diff + highlighting
8
8
  // memoized/cached so the 1s busy tick never recomputes. Long lines are
9
- // never truncated (Ink wraps; copy/paste stays intact) only hunk COUNT
10
- // is capped via maxLines so the modal stays compact. All paint comes from
9
+ // never truncated (Ink wraps; copy/paste stays intact); the full hunk list
10
+ // renders by default (an explicit maxLines windows it when a caller passes
11
+ // one). All paint comes from
11
12
  // ui/theme tokens (Ink supports color + backgroundColor on Text — verified
12
13
  // against the installed Ink 7 typings).
13
14
  import React from "react";
package/dist/ui/diff.js CHANGED
@@ -4,6 +4,10 @@
4
4
  export const CHANGE_THRESHOLD = 0.4;
5
5
  // Mirrors Claude's DiffDetailView guards.
6
6
  export const MAX_FILE_BYTES = 1_000_000;
7
+ // Retained for compatibility (no longer applied — the engine returns the
8
+ // full hunk list; views render it whole). Safety caps that remain enforced:
9
+ // MAX_FILE_BYTES, binary detect, MAX_MYERS_LINES fallback, MAX_WORD_TOKENS
10
+ // fallback.
7
11
  export const MAX_CHANGED_LINES = 400;
8
12
  export const DIFF_CONTEXT = 3;
9
13
  // Worst-case guards for the O(ND) Myers pass + O(w1*w2) word pass.
@@ -183,7 +187,7 @@ export function computeDiff(oldText, newText) {
183
187
  const hunks = [];
184
188
  let adds = 0;
185
189
  let dels = 0;
186
- let truncated = false;
190
+ const truncated = false;
187
191
  let hunkStart = Math.max(0, changeIdx[0] - DIFF_CONTEXT);
188
192
  let hunkEnd = Math.min(raw.length, changeIdx[0] + DIFF_CONTEXT + 1);
189
193
  const flush = (s, e) => {
@@ -242,7 +246,6 @@ export function computeDiff(oldText, newText) {
242
246
  }
243
247
  hunks.push({ oldStart, oldLines: oCount, newStart, newLines: nCount, lines });
244
248
  };
245
- let changed = 0;
246
249
  for (let c = 1; c < changeIdx.length; c++) {
247
250
  const prev = changeIdx[c - 1];
248
251
  const cur = changeIdx[c];
@@ -251,32 +254,11 @@ export function computeDiff(oldText, newText) {
251
254
  }
252
255
  else {
253
256
  flush(hunkStart, hunkEnd);
254
- changed = hunks.reduce((t, h) => t + h.lines.filter((l) => l.kind !== "context").length, 0);
255
- if (changed >= MAX_CHANGED_LINES) {
256
- truncated = true;
257
- return { hunks, adds, dels, truncated, skipped: null, isNewFile: oldText === null };
258
- }
259
257
  hunkStart = Math.max(0, cur - DIFF_CONTEXT);
260
258
  hunkEnd = Math.min(raw.length, cur + DIFF_CONTEXT + 1);
261
259
  }
262
260
  }
263
261
  flush(hunkStart, hunkEnd);
264
- changed = hunks.reduce((t, h) => t + h.lines.filter((l) => l.kind !== "context").length, 0);
265
- if (changed > MAX_CHANGED_LINES) {
266
- // Trim trailing hunks past the budget (keep the head — the user
267
- // reviews top-down; the notice names the remainder).
268
- let kept = 0;
269
- const out = [];
270
- for (const h of hunks) {
271
- const n = h.lines.filter((l) => l.kind !== "context").length;
272
- if (kept + n > MAX_CHANGED_LINES)
273
- break;
274
- out.push(h);
275
- kept += n;
276
- }
277
- truncated = true;
278
- return { hunks: out, adds, dels, truncated, skipped: null, isNewFile: oldText === null };
279
- }
280
262
  return { hunks, adds, dels, truncated, skipped: null, isNewFile: oldText === null };
281
263
  }
282
264
  export function computeSideBySide(oldText, newText) {
@@ -316,9 +298,7 @@ export function computeSideBySide(oldText, newText) {
316
298
  const rows = [];
317
299
  let adds = 0;
318
300
  let dels = 0;
319
- let truncated = false;
320
- const changedSoFar = () => dels + adds;
321
- // Returns false when the change budget is exhausted (stop windowing).
301
+ const truncated = false;
322
302
  const flushSlice = (s, e) => {
323
303
  const slice = raw.slice(s, e);
324
304
  let k = 0;
@@ -376,12 +356,7 @@ export function computeSideBySide(oldText, newText) {
376
356
  }
377
357
  dels += delRun.length;
378
358
  adds += addRun.length;
379
- if (changedSoFar() >= MAX_CHANGED_LINES) {
380
- truncated = true;
381
- return false;
382
- }
383
359
  }
384
- return true;
385
360
  };
386
361
  let hunkStart = Math.max(0, changeIdx[0] - DIFF_CONTEXT);
387
362
  let hunkEnd = Math.min(raw.length, changeIdx[0] + DIFF_CONTEXT + 1);
@@ -392,31 +367,11 @@ export function computeSideBySide(oldText, newText) {
392
367
  hunkEnd = Math.min(raw.length, cur + DIFF_CONTEXT + 1);
393
368
  }
394
369
  else {
395
- if (!flushSlice(hunkStart, hunkEnd)) {
396
- return { kind: "diff", rows, adds, dels, truncated, isNewFile: oldText === null };
397
- }
370
+ flushSlice(hunkStart, hunkEnd);
398
371
  hunkStart = Math.max(0, cur - DIFF_CONTEXT);
399
372
  hunkEnd = Math.min(raw.length, cur + DIFF_CONTEXT + 1);
400
373
  }
401
374
  }
402
375
  flushSlice(hunkStart, hunkEnd);
403
- if (dels + adds > MAX_CHANGED_LINES) {
404
- // Single-hunk overflow: the slice already pushed past the budget —
405
- // trim trailing rows past 400 changes (keep the head; the notice
406
- // names the remainder). Mirrors computeDiff's trailing-hunk trim.
407
- let kept = 0;
408
- let cut = rows.length;
409
- for (let i = 0; i < rows.length; i++) {
410
- if (rows[i].kind === "change") {
411
- kept += 1;
412
- if (kept > MAX_CHANGED_LINES) {
413
- cut = i;
414
- break;
415
- }
416
- }
417
- }
418
- rows.length = cut;
419
- truncated = true;
420
- }
421
376
  return { kind: "diff", rows, adds, dels, truncated, isNewFile: oldText === null };
422
377
  }
package/dist/ui/modals.js CHANGED
@@ -7,10 +7,10 @@ import React from "react";
7
7
  import { Box, Text } from "ink";
8
8
  import { SideBySideDiffView } from "./side-by-side.js";
9
9
  import { theme } from "./theme.js";
10
- // Max diff body lines inside the approval modal (hunk headers excluded;
11
- // the trailer names the remainder). Keeps the modal scannable while the
12
- // 1s busy tick repaints around it.
13
- export const APPROVAL_DIFF_MAX_LINES = 40;
10
+ // Retained for compatibility (no longer applied — the approval preview
11
+ // renders the full diff; smoothness comes from the per-mount memo + word
12
+ // fallbacks, not from a row cap).
13
+ export const APPROVAL_DIFF_MAX_LINES = Infinity;
14
14
  export const APPROVAL_OPTIONS = ["once", "always", "trustAll", "no"];
15
15
  // Command/file preview: the audit description minus its `⚙ name` prefix
16
16
  // (the tool name already headlines above). Falls back to the full text
@@ -39,7 +39,7 @@ export const ApprovalBox = React.memo(function ApprovalBox({ toolName, descripti
39
39
  { label: "[t]rust all write/edit/bash this session", option: "trustAll" },
40
40
  { label: "[n]o — deny this call", option: "no" },
41
41
  ];
42
- return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.border.style, borderColor: theme.border.approval, paddingX: theme.spacing.pickerPadX, children: [_jsxs(Text, { bold: true, color: theme.color.warning, children: [theme.symbol.warningMark, " Atom permission \u2014 allow this tool?"] }), _jsx(Text, { bold: true, children: approvalTitle(toolName) }), _jsx(Text, { color: theme.color.code, children: approvalPreview(toolName, description) }), diff ? _jsx(SideBySideDiffView, { oldText: diff.oldText, newText: diff.newText, lang: diff.lang, maxRows: APPROVAL_DIFF_MAX_LINES }) : null, rows.map((r, i) => (_jsxs(Text, { color: i === selected ? theme.color.selection : undefined, children: [i === selected ? `${theme.symbol.select} ` : theme.spacing.rowIndent, r.label] }, r.option))), _jsx(Text, { dimColor: true, children: "\u2191/\u2193 + Enter selects \u00B7 y/a/t/n shortcuts \u00B7 Esc denies" })] }));
42
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.border.style, borderColor: theme.border.approval, paddingX: theme.spacing.pickerPadX, children: [_jsxs(Text, { bold: true, color: theme.color.warning, children: [theme.symbol.warningMark, " Atom permission \u2014 allow this tool?"] }), _jsx(Text, { bold: true, children: approvalTitle(toolName) }), _jsx(Text, { color: theme.color.code, children: approvalPreview(toolName, description) }), diff ? _jsx(SideBySideDiffView, { oldText: diff.oldText, newText: diff.newText, lang: diff.lang }) : null, rows.map((r, i) => (_jsxs(Text, { color: i === selected ? theme.color.selection : undefined, children: [i === selected ? `${theme.symbol.select} ` : theme.spacing.rowIndent, r.label] }, r.option))), _jsx(Text, { dimColor: true, children: "\u2191/\u2193 + Enter selects \u00B7 y/a/t/n shortcuts \u00B7 Esc denies" })] }));
43
43
  });
44
44
  export const QuestionBox = React.memo(function QuestionBox({ question, options, allowCustom, askCustom, askSelIndex }) {
45
45
  questionRenderProbe.count += 1;
@@ -14,7 +14,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  // (code-point safe); below NARROW_COLUMNS the view degrades to the
15
15
  // stacked unified DiffView instead of destroying the layout.
16
16
  // - computed once per mount (useMemo, keyed on inputs + pane width) and
17
- // capped (maxRows + trailer) never recomputed per tick, never floods.
17
+ // rendered whole (an explicit maxRows windows it when a caller passes one)
18
+ // — never recomputed per tick, never floods via re-computation.
18
19
  // All paint comes from ui/theme tokens.
19
20
  import React from "react";
20
21
  import { Box, Text } from "ink";
@@ -24,10 +25,11 @@ import { DiffView, LineBody } from "./diff-view.js";
24
25
  import { theme } from "./theme.js";
25
26
  // Below this width two panes cannot breathe — stack unified instead.
26
27
  export const SBS_NARROW_COLUMNS = 70;
27
- // Committed-transcript cap (rows, context + change): the scrollback shows
28
- // the reviewable head; the file on disk is the whole truth. The engine
29
- // still truncates past 400 changed lines with its own notice.
30
- export const TRANSCRIPT_DIFF_MAX_LINES = 120;
28
+ // Uncapped: views render the full row list (smooth via per-mount useMemo +
29
+ // append-once Static + word-token/Myers fallbacks in the engine). maxRows
30
+ // remains as an opt-in window for callers/tests that want a collapsed tail.
31
+ // Retained for compatibility.
32
+ export const TRANSCRIPT_DIFF_MAX_LINES = Infinity;
31
33
  function truncateTo(s, width) {
32
34
  const chars = [...s];
33
35
  if (chars.length <= width)
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
5
5
  // All paint comes from ui/theme tokens — no literal colors or glyphs here.
6
6
  import React from "react";
7
7
  import { Box, Static, Text } from "ink";
8
- import { SideBySideDiffView, TRANSCRIPT_DIFF_MAX_LINES } from "./side-by-side.js";
8
+ import { SideBySideDiffView } from "./side-by-side.js";
9
9
  import { ErrorCard, classifyToolError } from "./errors.js";
10
10
  import { MarkdownText, ToolLine } from "./markdown.js";
11
11
  import { theme } from "./theme.js";
@@ -73,9 +73,9 @@ export function renderTranscriptItem(item) {
73
73
  // write/edit label swallowed by pairing (success line immediately
74
74
  // followed by an error line) keeps its committed diff above the card.
75
75
  const labelDiff = item.label?.diff;
76
- return (_jsxs(React.Fragment, { children: [item.label ? _jsx(ToolLine, { content: item.label.content, ms: item.label.ms }) : null, labelDiff && !item.label?.error ? (_jsx(SideBySideDiffView, { oldText: labelDiff.oldText, newText: labelDiff.newText, lang: labelDiff.lang, maxRows: TRANSCRIPT_DIFF_MAX_LINES })) : null, _jsx(ErrorCard, { classified: classified })] }, i));
76
+ return (_jsxs(React.Fragment, { children: [item.label ? _jsx(ToolLine, { content: item.label.content, ms: item.label.ms }) : null, labelDiff && !item.label?.error ? (_jsx(SideBySideDiffView, { oldText: labelDiff.oldText, newText: labelDiff.newText, lang: labelDiff.lang })) : null, _jsx(ErrorCard, { classified: classified })] }, i));
77
77
  }
78
- return (_jsxs(React.Fragment, { children: [_jsx(ToolLine, { content: t.content, error: t.error, ms: t.ms }), t.diff && !t.error ? (_jsx(SideBySideDiffView, { oldText: t.diff.oldText, newText: t.diff.newText, lang: t.diff.lang, maxRows: TRANSCRIPT_DIFF_MAX_LINES })) : null] }, i));
78
+ return (_jsxs(React.Fragment, { children: [_jsx(ToolLine, { content: t.content, error: t.error, ms: t.ms }), t.diff && !t.error ? (_jsx(SideBySideDiffView, { oldText: t.diff.oldText, newText: t.diff.newText, lang: t.diff.lang })) : null] }, i));
79
79
  }
80
80
  return (_jsxs(Box, { flexDirection: "column", marginBottom: theme.spacing.turnGap, children: [_jsx(Text, { children: _jsxs(Text, { color: theme.color.assistant, bold: true, children: [theme.symbol.speakerAssistant, " "] }) }), _jsx(MarkdownText, { text: t.content })] }, i));
81
81
  }
@@ -146,7 +146,7 @@ Exact `ExtensionAPI` methods — nothing else exists:
146
146
  | `isProjectTrusted()` | Whether this load is trusted (degrade gracefully when false) |
147
147
  | `setStatusSegment(text)` | One status-bar slot per extension (upsert by owner) |
148
148
  | `setWidget(def)` | Panel widget (`placement: "panel"`, keyed by owner + id) |
149
- | `notify(message)` | Transient `(name) message` transcript line |
149
+ | `notify(message)` | Transient `(name) message` transcript line (staged queue caps at 100, drop-oldest) |
150
150
  | `promptUser(question, options?, allowCustom?)` | Modal dialog; rejects headless, during activation, or while one is open |
151
151
 
152
152
  Stores behind the API: `src/tools/custom.ts`, `intercept.ts`, `overrides.ts`, `provider-hooks.ts`, `compaction-hooks.ts`, `src/extension-commands.ts`, `src/extension-ui.ts`, `src/project-trust.ts`.
@@ -4,8 +4,8 @@
4
4
  // as the normal model-visible result (approval is skipped, the turn
5
5
  // continues); anything this hook does not block runs untouched.
6
6
  //
7
- // Gallery sample for documentation/extensions.md — that guide references
8
- // this file by name and never duplicates it. Covered by
7
+ // Gallery sample for documentation/extensions.md — that guide excerpts
8
+ // this file and references it by name. Covered by
9
9
  // tests/extension-gallery.test.ts, which loads this exact file through the
10
10
  // real loadExtensions path.
11
11
  module.exports = function auditGate(api) {
@@ -5,8 +5,8 @@
5
5
  // `Error: invalid call: ...` result and the implementation never runs), and
6
6
  // dispatches through the shared loop.
7
7
  //
8
- // Gallery sample for documentation/extensions.md — that guide references
9
- // this file by name and never duplicates it. Covered by
8
+ // Gallery sample for documentation/extensions.md — that guide excerpts
9
+ // this file and references it by name. Covered by
10
10
  // tests/extension-gallery.test.ts, which loads this exact file through the
11
11
  // real loadExtensions path.
12
12
  const notes = [];
@@ -6,8 +6,8 @@
6
6
  // transcript) so the dialog flow is testable headless — tests drive the
7
7
  // command with a stub askUser and unit-test the helper directly.
8
8
  //
9
- // Gallery sample for documentation/extensions.md — that guide references
10
- // this file by name and never duplicates it. Covered by
9
+ // Gallery sample for documentation/extensions.md — that guide excerpts
10
+ // this file and references it by name. Covered by
11
11
  // tests/extension-gallery.test.ts, which loads this exact file through the
12
12
  // real loadExtensions path.
13
13
  function summarizeChoice(choice, extra) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atom-agent",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Agentic terminal coding assistant: multi-provider LLM loop with local file/shell/web tools in an Ink (React) TUI.",
5
5
  "type": "module",
6
6
  "license": "MIT",