atom-agent 1.2.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +13 -4
  3. package/atom.example.json +11 -0
  4. package/dist/App.js +923 -200
  5. package/dist/adapters.js +82 -13
  6. package/dist/agent/goal-evaluator.js +69 -0
  7. package/dist/agent/loop.js +517 -76
  8. package/dist/cli.js +11 -3
  9. package/dist/compact.js +41 -15
  10. package/dist/config.js +43 -7
  11. package/dist/context-manager.js +16 -198
  12. package/dist/context-windows.js +4 -2
  13. package/dist/env-block.js +5 -5
  14. package/dist/extension-commands.js +196 -0
  15. package/dist/extension-ui.js +153 -0
  16. package/dist/extensions.js +1571 -0
  17. package/dist/goal.js +583 -0
  18. package/dist/project-trust.js +96 -0
  19. package/dist/providers.js +6 -6
  20. package/dist/scheduler.js +74 -36
  21. package/dist/session.js +23 -5
  22. package/dist/sessions.js +25 -6
  23. package/dist/telemetry-dashboard.js +28 -0
  24. package/dist/telemetry.js +39 -0
  25. package/dist/tools/compaction-hooks.js +165 -0
  26. package/dist/tools/custom.js +189 -0
  27. package/dist/tools/intercept.js +145 -0
  28. package/dist/tools/overrides.js +105 -0
  29. package/dist/tools/provider-hooks.js +224 -0
  30. package/dist/tools/registry.js +246 -17
  31. package/dist/tools.js +44 -0
  32. package/dist/ui/diff-panel.js +5 -5
  33. package/dist/ui/diff-view.js +3 -2
  34. package/dist/ui/diff.js +7 -52
  35. package/dist/ui/modals.js +5 -5
  36. package/dist/ui/palette.js +1 -1
  37. package/dist/ui/side-by-side.js +7 -5
  38. package/dist/ui/status-bar.js +80 -5
  39. package/dist/ui/transcript.js +3 -3
  40. package/dist/zen.js +305 -75
  41. package/documentation/architecture.md +114 -0
  42. package/documentation/cli.md +82 -0
  43. package/documentation/compaction.md +50 -0
  44. package/documentation/configuration.md +111 -0
  45. package/documentation/development.md +62 -0
  46. package/documentation/extensions.md +160 -0
  47. package/documentation/getting-started.md +63 -0
  48. package/documentation/goals.md +41 -0
  49. package/documentation/index.md +41 -0
  50. package/documentation/observability.md +70 -0
  51. package/documentation/permissions.md +66 -0
  52. package/documentation/providers.md +78 -0
  53. package/documentation/sessions.md +92 -0
  54. package/documentation/skills.md +57 -0
  55. package/documentation/tools.md +94 -0
  56. package/documentation/troubleshooting.md +54 -0
  57. package/examples/extensions/01-audit-gate.js +24 -0
  58. package/examples/extensions/02-notes-tool.js +32 -0
  59. package/examples/extensions/03-custom-command.js +32 -0
  60. package/package.json +6 -2
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;
@@ -9,7 +9,7 @@ import { Box, Text } from "ink";
9
9
  import { PickerMoreAbove, PickerMoreBelow, pickerWindow } from "./pickers.js";
10
10
  import { theme } from "./theme.js";
11
11
  export const PALETTE_WINDOW = 12;
12
- export const PALETTE_CATEGORY_ORDER = ["Model", "Session", "Tools", "Skills", "Flow", "Help"];
12
+ export const PALETTE_CATEGORY_ORDER = ["Model", "Session", "Tools", "Skills", "Flow", "Extensions", "Help"];
13
13
  const PALETTE_CATEGORIES = {
14
14
  "/model": "Model",
15
15
  "/provider": "Model",
@@ -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)
@@ -12,6 +12,47 @@ import React from "react";
12
12
  import { Box, Text } from "ink";
13
13
  import { formatTokenSegment } from "../context-windows.js";
14
14
  import { theme } from "./theme.js";
15
+ // Default objective budget for the goal segment: compact enough to share the
16
+ // line with the pinned model/token/mode segments at 100 columns.
17
+ export const GOAL_STATUS_OBJECTIVE_CHARS = 32;
18
+ // Truncate an objective to n chars max (`…` tail keeps the start, which
19
+ // carries the verb). n < 4 yields "" (the caller drops the segment instead).
20
+ export function truncateGoalObjective(objective, max = GOAL_STATUS_OBJECTIVE_CHARS) {
21
+ const text = typeof objective === "string" ? objective : "";
22
+ if (text.length <= max)
23
+ return text;
24
+ if (max < 4)
25
+ return "";
26
+ return `${text.slice(0, max - 1)}…`;
27
+ }
28
+ // Full goal segment at the default budget, or null when no goal is live.
29
+ // Paused reads distinct from active (`[paused]` vs `[active]`).
30
+ export function formatGoalSegment(goal, max = GOAL_STATUS_OBJECTIVE_CHARS) {
31
+ if (!goal || typeof goal.objective !== "string" || goal.objective.length === 0)
32
+ return null;
33
+ const state = goal.active === true ? "active" : "paused";
34
+ return `goal: ${truncateGoalObjective(goal.objective, max)} [${state}]`;
35
+ }
36
+ // Fit the goal segment into `room` chars (the width left after every other
37
+ // segment): full text when it fits, a shorter truncation when it almost
38
+ // fits, null (drop the segment) when even a stub would displace the line.
39
+ // Never throws; never returns "".
40
+ export function fitGoalSegment(goal, room) {
41
+ if (!goal || typeof goal.objective !== "string" || goal.objective.length === 0)
42
+ return null;
43
+ if (typeof room !== "number" || !Number.isFinite(room) || room <= 0)
44
+ return null;
45
+ const state = goal.active === true ? "active" : "paused";
46
+ const full = `goal: ${goal.objective} [${state}]`;
47
+ if (full.length <= room)
48
+ return full;
49
+ // Room for at least 4 objective chars plus the fixed framing, else drop.
50
+ const overhead = `goal: [${state}]`.length + 1;
51
+ const allow = Math.floor(room - overhead);
52
+ if (allow < 4)
53
+ return null;
54
+ return `goal: ${truncateGoalObjective(goal.objective, allow)} [${state}]`;
55
+ }
15
56
  // ~/… collapse + tail-cut: informative, never a full scroll of nesting.
16
57
  // Further shrinking for tight widths goes through shrinkTo below (the bar
17
58
  // measures first and only renders what fits).
@@ -34,9 +75,13 @@ export function shrinkTo(s, n) {
34
75
  // render (same-props parent churn — token paints, keystrokes, unrelated
35
76
  // ticks — must skip it; only changed props repaint).
36
77
  export const statusBarRenderProbe = { count: 0 };
37
- export const StatusBar = React.memo(function StatusBar({ provider, model, usageTotals, contextLoad, reasoningDisplay, mode, trustAll, busy, activity, phaseLabel, elapsedSecs, stalled, approvalPending, cwd, branch, columns = 100, }) {
78
+ export const StatusBar = React.memo(function StatusBar({ provider, model, usageTotals, contextLoad, reasoningDisplay, mode, trustAll, busy, activity, phaseLabel, elapsedSecs, stalled, approvalPending, cwd, branch, columns = 100, extensionStatus, goal, }) {
38
79
  statusBarRenderProbe.count += 1;
39
80
  const bar = theme.symbol.bar;
81
+ // Extension guest slot (ticket 10): pre-budgeted text renders only when
82
+ // the full line still fits — the fixed-width contract above. The `+ 3`
83
+ // is the ` ${bar} ` separator the segment carries with it.
84
+ const hasExt = typeof extensionStatus === "string" && extensionStatus.length > 0;
40
85
  if (!busy) {
41
86
  const token = formatTokenSegment(usageTotals, model, contextLoad);
42
87
  const trust = trustAll && mode !== "plan" ? "+trust" : "";
@@ -46,7 +91,8 @@ export const StatusBar = React.memo(function StatusBar({ provider, model, usageT
46
91
  // order: branch → cwd tail → the whole segment.
47
92
  const tail = `reasoning: ${reasoningDisplay} ${bar} mode: ${mode}${trust}`;
48
93
  const baseLen = `${provider}/${model} ${bar} ${token} ${bar} ${bar} ${tail}`.length;
49
- const avail = columns - baseLen;
94
+ const showExt = hasExt && baseLen + extensionStatus.length + 3 + 2 <= columns;
95
+ const avail = columns - baseLen - (showExt ? extensionStatus.length + 3 : 0);
50
96
  let loc = null;
51
97
  if (cwd) {
52
98
  const branchPart = branch ? ` : ${branch}` : "";
@@ -61,15 +107,44 @@ export const StatusBar = React.memo(function StatusBar({ provider, model, usageT
61
107
  loc = shrunk ? shrunk : null;
62
108
  }
63
109
  }
64
- return (_jsx(Box, { marginTop: theme.spacing.statusMarginTop, children: _jsxs(Text, { dimColor: true, children: [provider, "/", model, " ", bar, " ", token, loc ? (_jsxs(_Fragment, { children: [" ", bar, " ", loc] })) : null, " ", bar, " reasoning: ", reasoningDisplay, " ", bar, " mode: ", mode, trust ? "+trust" : null] }) }));
110
+ // Goal segment (ticket 09): lowest-priority builtin it takes only the
111
+ // width left after every other segment and drops whole rather than push
112
+ // the line past `columns`. Hidden entirely with no goal.
113
+ const lineSoFar = baseLen + (showExt ? extensionStatus.length + 3 : 0) + (loc ? loc.length + 3 : 0);
114
+ const goalSeg = fitGoalSegment(goal ?? null, columns - lineSoFar - 2);
115
+ return (_jsx(Box, { marginTop: theme.spacing.statusMarginTop, children: _jsxs(Text, { dimColor: true, children: [provider, "/", model, " ", bar, " ", token, showExt ? (_jsxs(_Fragment, { children: [" ", bar, " ", extensionStatus] })) : null, loc ? (_jsxs(_Fragment, { children: [" ", bar, " ", loc] })) : null, " ", bar, " reasoning: ", reasoningDisplay, " ", bar, " mode: ", mode, trust ? "+trust" : null, goalSeg ? (_jsxs(_Fragment, { children: [" ", bar, " ", goalSeg] })) : null] }) }));
65
116
  }
66
117
  // Busy layout prioritizes activity + clock + interrupt hint; the mode
67
118
  // stays pinned (it used to vanish while working), and the reasoning
68
119
  // effort stays visible (it used to vanish while working). The activity text
69
120
  // shrinks to fit so `esc stops` never wraps away.
70
121
  const busyTrust = trustAll && mode !== "plan" ? "+trust" : "";
71
- const busyFixed = ` ${bar} ${elapsedSecs}s ${bar} ${formatTokenSegment(usageTotals, model, contextLoad)} ${bar} reasoning: ${reasoningDisplay} ${bar} mode: ${mode}${busyTrust} ${bar} esc stops`;
122
+ const busyToken = formatTokenSegment(usageTotals, model, contextLoad);
123
+ // Goal segment (ticket 09): a guest in the fixed part — capped at 48
124
+ // chars and rendered only when the FULL activity text still fits beside
125
+ // it. Otherwise the goal drops whole and every existing segment renders
126
+ // exactly as with no goal (the goal never displaces, same precedent as
127
+ // the extension guest above). The clock, token, mode, and esc-hint
128
+ // segments never move for it either way.
129
+ const busyGoalSeg = fitGoalSegment(goal ?? null, 48);
130
+ const busyGoalCandidate = busyGoalSeg ? ` ${bar} ${busyGoalSeg}` : "";
131
+ const activityFull = activity ?? phaseLabel;
132
+ const busyCore = ` ${bar} ${elapsedSecs}s ${bar} ${busyToken} ${bar} reasoning: ${reasoningDisplay} ${bar} mode: ${mode}${busyTrust}`;
133
+ const busyTail = ` ${bar} esc stops`;
134
+ const busyExtCandidate = hasExt && `${busyCore}${busyGoalCandidate}${busyTail}`.length + extensionStatus.length + 3 + 2 <= columns
135
+ ? ` ${bar} ${extensionStatus}`
136
+ : "";
137
+ const withGoalFixed = `${busyCore}${busyExtCandidate}${busyGoalCandidate}${busyTail}`;
138
+ // Room check against the unfitted activity text: when it no longer fits
139
+ // whole with the goal aboard, the goal yields (drop whole, recompute).
140
+ const busyGoalPart = busyGoalCandidate !== "" &&
141
+ withGoalFixed.length + activityFull.length + 2 <= columns
142
+ ? busyGoalCandidate
143
+ : "";
144
+ const busyNoExt = `${busyCore}${busyGoalPart}${busyTail}`;
145
+ const showBusyExt = hasExt && busyNoExt.length + extensionStatus.length + 3 + 2 <= columns;
146
+ const busyFixed = ` ${bar} ${elapsedSecs}s${showBusyExt ? ` ${bar} ${extensionStatus}` : ""} ${bar} ${busyToken} ${bar} reasoning: ${reasoningDisplay} ${bar} mode: ${mode}${busyTrust}${busyGoalPart} ${bar} esc stops`;
72
147
  const busyAvail = columns - busyFixed.length - 2;
73
- const activityText = shrinkTo(activity ?? phaseLabel, Math.max(0, busyAvail));
148
+ const activityText = shrinkTo(activityFull, Math.max(0, busyAvail));
74
149
  return (_jsx(Box, { marginTop: theme.spacing.statusMarginTop, children: _jsxs(Text, { dimColor: true, children: [_jsxs(Text, { color: theme.color.activity, children: [theme.symbol.workTool, " ", activityText] }), busyFixed, stalled && !approvalPending ? ` ${bar} waiting${theme.symbol.ellipsis}` : null, approvalPending ? (_jsxs(Text, { color: theme.color.warning, children: [" ", bar, " waiting approval"] })) : null] }) }));
75
150
  });
@@ -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
  }