atom-agent 1.3.0 → 1.5.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 +62 -0
- package/README.md +220 -224
- package/dist/App.js +922 -341
- package/dist/adapters.js +127 -14
- package/dist/agent/goal-evaluator.js +3 -0
- package/dist/agent/loop.js +211 -430
- package/dist/agent/tool-pipeline.js +398 -0
- package/dist/agent/turn-events.js +12 -0
- package/dist/cli.js +57 -8
- package/dist/compact.js +72 -8
- package/dist/config.js +19 -0
- package/dist/context-manager.js +6 -2
- package/dist/extensions.js +6 -0
- package/dist/file-diffs.js +108 -0
- package/dist/kilo.js +1 -1
- package/dist/local-discovery.js +2 -2
- package/dist/media.js +276 -0
- package/dist/overflow.js +140 -0
- package/dist/policy.js +8 -0
- package/dist/scheduler.js +38 -9
- package/dist/session-revert.js +125 -0
- package/dist/sessions.js +101 -0
- package/dist/snapshots.js +69 -0
- package/dist/system.js +2 -89
- package/dist/telemetry.js +26 -1
- package/dist/todos.js +241 -0
- package/dist/tools/filesystem.js +102 -22
- package/dist/tools/registry.js +184 -45
- package/dist/tools/ripgrep.js +7 -6
- package/dist/tools/search.js +172 -17
- package/dist/tools/shared.js +6 -0
- package/dist/tools.js +7 -39
- package/dist/ui/diff-panel.js +5 -5
- package/dist/ui/diff-view.js +16 -7
- package/dist/ui/diff.js +73 -51
- package/dist/ui/errors.js +20 -6
- package/dist/ui/input.js +24 -20
- package/dist/ui/live-tail.js +36 -1
- package/dist/ui/markdown.js +9 -4
- package/dist/ui/modals.js +6 -4
- package/dist/ui/paint-scheduler.js +120 -0
- package/dist/ui/palette.js +4 -2
- package/dist/ui/pickers.js +4 -1
- package/dist/ui/side-by-side.js +88 -27
- package/dist/ui/status-bar.js +63 -8
- package/dist/ui/stream-store.js +7 -0
- package/dist/ui/theme.js +23 -1
- package/dist/ui/todo-panel.js +5 -2
- package/dist/ui/tool-inspector.js +33 -4
- package/dist/ui/transcript.js +9 -6
- package/dist/web/events.js +93 -0
- package/dist/web/runtime.js +790 -0
- package/dist/web/server.js +570 -0
- package/dist/web/ui/app.js +1925 -0
- package/dist/web/ui/index.html +135 -0
- package/dist/web/ui/styles.css +515 -0
- package/dist/zen.js +115 -4
- package/documentation/cli.md +5 -5
- package/documentation/configuration.md +11 -6
- package/documentation/development.md +4 -3
- package/documentation/extensions.md +1 -1
- package/documentation/goals.md +1 -1
- package/documentation/index.md +4 -4
- package/documentation/providers.md +2 -3
- package/documentation/skills.md +3 -3
- package/documentation/tools.md +8 -3
- package/documentation/troubleshooting.md +1 -1
- package/examples/extensions/01-audit-gate.js +2 -2
- package/examples/extensions/02-notes-tool.js +2 -2
- package/examples/extensions/03-custom-command.js +2 -2
- package/package.json +3 -2
package/dist/tools.js
CHANGED
|
@@ -25,42 +25,10 @@ export * from "./tools/shared.js";
|
|
|
25
25
|
export * from "./tools/shell.js";
|
|
26
26
|
export * from "./tools/todo.js";
|
|
27
27
|
export * from "./tools/web.js";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"A \"complete\" lands only on genuinely finished work: verified checks and resolved todos. " +
|
|
36
|
-
"Checks you could not run go in \"unverified\" (recorded openly in the closing summary, never a gate). " +
|
|
37
|
-
"WHEN NOT to use: never outside a goal turn (it records nothing there); " +
|
|
38
|
-
"a turn with no report continues the goal.",
|
|
39
|
-
parameters: {
|
|
40
|
-
type: "object",
|
|
41
|
-
properties: {
|
|
42
|
-
status: {
|
|
43
|
-
type: "string",
|
|
44
|
-
enum: ["continue", "complete", "blocked"],
|
|
45
|
-
description: "Turn outcome: \"continue\" (keep working), \"complete\" (goal done), \"blocked\" (cannot proceed).",
|
|
46
|
-
},
|
|
47
|
-
next: {
|
|
48
|
-
type: "string",
|
|
49
|
-
description: "Next action (only with status \"continue\"; omit otherwise).",
|
|
50
|
-
},
|
|
51
|
-
reason: {
|
|
52
|
-
type: "string",
|
|
53
|
-
description: "Why the goal is done or stuck (required with \"complete\"/\"blocked\"; omit otherwise).",
|
|
54
|
-
},
|
|
55
|
-
unverified: {
|
|
56
|
-
type: "array",
|
|
57
|
-
items: { type: "string" },
|
|
58
|
-
description: "Checks that could not be run (only with status \"complete\"; omit otherwise). " +
|
|
59
|
-
"Recorded openly in the closing summary; at most 10 non-empty items of 200 characters each.",
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
required: ["status"],
|
|
63
|
-
additionalProperties: false,
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
};
|
|
28
|
+
// UPDATE_GOAL_TOOL_DEFINITION lives in src/tools/registry.ts beside its
|
|
29
|
+
// validator and executor (ticket 06: intercepted first-class registry
|
|
30
|
+
// entries) and re-exports through the `export *` barrel above, so every
|
|
31
|
+
// existing `from "./tools.js"` importer keeps working untouched. The loop
|
|
32
|
+
// dispatches it through the registry runner (never by name), and chat
|
|
33
|
+
// payloads carry it via allToolDefinitions — one list for model visibility
|
|
34
|
+
// and executability.
|
package/dist/ui/diff-panel.js
CHANGED
|
@@ -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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
export const DIFF_PANEL_MAX_LINES =
|
|
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: [
|
|
54
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SideBySideDiffView, { oldText: rec.oldText, newText: rec.newText, lang: rec.lang, path: rec.path }), _jsxs(Text, { dimColor: true, children: [theme.symbol.moreAbove, "/", theme.symbol.moreBelow, " prev/next file \u00B7 Enter collapses \u00B7 Esc closes"] })] }));
|
|
55
55
|
}
|
package/dist/ui/diff-view.js
CHANGED
|
@@ -6,13 +6,14 @@ 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)
|
|
10
|
-
//
|
|
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";
|
|
14
15
|
import { Box, Text } from "ink";
|
|
15
|
-
import { computeDiff } from "./diff.js";
|
|
16
|
+
import { computeDiff, hunksRange, rangeLabel } from "./diff.js";
|
|
16
17
|
import { highlightLine } from "./highlight.js";
|
|
17
18
|
import { theme } from "./theme.js";
|
|
18
19
|
function syntaxColor(kind) {
|
|
@@ -36,7 +37,7 @@ function syntaxColor(kind) {
|
|
|
36
37
|
// both the walk and the tokenize lookup.
|
|
37
38
|
export const LineBody = React.memo(function LineBody({ lineText, runs, base, lang, }) {
|
|
38
39
|
const baseColor = base === "add" ? theme.color.success : theme.color.toolError;
|
|
39
|
-
const hlBg = base === "add" ?
|
|
40
|
+
const hlBg = base === "add" ? theme.color.diffAddBg : theme.color.diffDelBg;
|
|
40
41
|
const langKnown = lang === "c" || lang === "py" || lang === "sh" || lang === "data";
|
|
41
42
|
const syn = langKnown ? highlightLine(lineText, lang) : [];
|
|
42
43
|
const nodes = [];
|
|
@@ -49,7 +50,7 @@ export const LineBody = React.memo(function LineBody({ lineText, runs, base, lan
|
|
|
49
50
|
if (r.changed) {
|
|
50
51
|
// Changed words keep the high-contrast background treatment —
|
|
51
52
|
// syntax hues would muddy the signal.
|
|
52
|
-
nodes.push(_jsx(Text, { backgroundColor: hlBg, color:
|
|
53
|
+
nodes.push(_jsx(Text, { backgroundColor: hlBg, color: theme.color.diffChangedFg, bold: true, children: r.text }, k));
|
|
53
54
|
// Advance the syntax cursor past this run so later runs align.
|
|
54
55
|
while (synIdx < syn.length && syn[synIdx].end <= runEnd)
|
|
55
56
|
synIdx += 1;
|
|
@@ -86,7 +87,15 @@ export const LineBody = React.memo(function LineBody({ lineText, runs, base, lan
|
|
|
86
87
|
});
|
|
87
88
|
return _jsx(Text, { color: langKnown ? undefined : baseColor, children: nodes });
|
|
88
89
|
});
|
|
89
|
-
|
|
90
|
+
// Ticket 02 signature header: ONE quiet dim line — change counts, path,
|
|
91
|
+
// line range — shared verbatim by the unified view, the side-by-side view
|
|
92
|
+
// (ui/side-by-side), the approval modal, the committed transcript, and the
|
|
93
|
+
// /diff review. Changed rows carry +/- color + word backgrounds; context
|
|
94
|
+
// rows stay dim so unchanged text reads subordinate.
|
|
95
|
+
export function DiffSummary({ adds, dels, isNewFile, path, range, }) {
|
|
96
|
+
return (_jsxs(Text, { dimColor: true, children: [isNewFile ? "new file " : "", "+", adds, " \u2212", dels, path ? ` ${theme.symbol.separator} ${path}` : "", range ? ` ${theme.symbol.separator} ${range}` : ""] }));
|
|
97
|
+
}
|
|
98
|
+
function DiffViewInner({ oldText, newText, lang = null, path = null, maxLines = Infinity }) {
|
|
90
99
|
const diff = React.useMemo(() => computeDiff(oldText, newText), [oldText, newText]);
|
|
91
100
|
if (diff.skipped) {
|
|
92
101
|
return _jsx(Text, { dimColor: true, children: diff.skipped });
|
|
@@ -101,7 +110,7 @@ function DiffViewInner({ oldText, newText, lang = null, maxLines = Infinity }) {
|
|
|
101
110
|
total += h.lines.length;
|
|
102
111
|
return Math.max(0, total - maxLines);
|
|
103
112
|
})();
|
|
104
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(DiffSummary, { adds: diff.adds, dels: diff.dels, isNewFile: diff.isNewFile, path: path, range: diff.isNewFile ? null : rangeLabel(hunksRange(diff.hunks)) }), diff.hunks.map((h, hi) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["@@ -", h.oldStart, ",", h.oldLines, " +", h.newStart, ",", h.newLines, " @@"] }), h.lines.map((ln, k) => {
|
|
105
114
|
bodyLines += 1;
|
|
106
115
|
if (bodyLines > maxLines)
|
|
107
116
|
return null;
|
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
|
-
|
|
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,33 +254,79 @@ 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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
262
|
+
return { hunks, adds, dels, truncated, skipped: null, isNewFile: oldText === null };
|
|
263
|
+
}
|
|
264
|
+
// Overall changed-line range for unified hunks (hunk context excluded —
|
|
265
|
+
// the header names the span that changed, the @@ lines keep per-hunk detail).
|
|
266
|
+
export function hunksRange(hunks) {
|
|
267
|
+
let oldMin = null;
|
|
268
|
+
let oldMax = null;
|
|
269
|
+
let newMin = null;
|
|
270
|
+
let newMax = null;
|
|
271
|
+
for (const h of hunks) {
|
|
272
|
+
// Walk hunk lines counting only changed lines for the range.
|
|
273
|
+
let ho = h.oldStart;
|
|
274
|
+
let hn = h.newStart;
|
|
275
|
+
for (const ln of h.lines) {
|
|
276
|
+
if (ln.kind === "context") {
|
|
277
|
+
ho += 1;
|
|
278
|
+
hn += 1;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (ln.kind === "del") {
|
|
282
|
+
oldMin = oldMin === null ? ho : Math.min(oldMin, ho);
|
|
283
|
+
oldMax = oldMax === null ? ho : Math.max(oldMax, ho);
|
|
284
|
+
ho += 1;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
newMin = newMin === null ? hn : Math.min(newMin, hn);
|
|
288
|
+
newMax = newMax === null ? hn : Math.max(newMax, hn);
|
|
289
|
+
hn += 1;
|
|
290
|
+
}
|
|
276
291
|
}
|
|
277
|
-
truncated = true;
|
|
278
|
-
return { hunks: out, adds, dels, truncated, skipped: null, isNewFile: oldText === null };
|
|
279
292
|
}
|
|
280
|
-
return {
|
|
293
|
+
return { oldMin, oldMax, newMin, newMax };
|
|
294
|
+
}
|
|
295
|
+
// Overall line range for side-by-side rows (change rows only — context
|
|
296
|
+
// excluded, same rule as hunksRange so both views agree).
|
|
297
|
+
export function sbsRange(rows) {
|
|
298
|
+
let oldMin = null;
|
|
299
|
+
let oldMax = null;
|
|
300
|
+
let newMin = null;
|
|
301
|
+
let newMax = null;
|
|
302
|
+
for (const r of rows) {
|
|
303
|
+
if (r.kind === "context")
|
|
304
|
+
continue;
|
|
305
|
+
if (r.oldNo !== null) {
|
|
306
|
+
oldMin = oldMin === null ? r.oldNo : Math.min(oldMin, r.oldNo);
|
|
307
|
+
oldMax = oldMax === null ? r.oldNo : Math.max(oldMax, r.oldNo);
|
|
308
|
+
}
|
|
309
|
+
if (r.newNo !== null) {
|
|
310
|
+
newMin = newMin === null ? r.newNo : Math.min(newMin, r.newNo);
|
|
311
|
+
newMax = newMax === null ? r.newNo : Math.max(newMax, r.newNo);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { oldMin, oldMax, newMin, newMax };
|
|
315
|
+
}
|
|
316
|
+
function span(min, max) {
|
|
317
|
+
if (min === null || max === null)
|
|
318
|
+
return null;
|
|
319
|
+
return min === max ? `L${min}` : `L${min}–${max}`;
|
|
320
|
+
}
|
|
321
|
+
// "L2 → L2", "L2–20 → L2–21", or the one-sided remainder for pure
|
|
322
|
+
// add/del blocks. Null when there is nothing to name (new files carry
|
|
323
|
+
// the `new file` marker instead of a range; callers skip null).
|
|
324
|
+
export function rangeLabel(r) {
|
|
325
|
+
const o = span(r.oldMin, r.oldMax);
|
|
326
|
+
const n = span(r.newMin, r.newMax);
|
|
327
|
+
if (o && n)
|
|
328
|
+
return o === n ? o : `${o} → ${n}`;
|
|
329
|
+
return o ?? n;
|
|
281
330
|
}
|
|
282
331
|
export function computeSideBySide(oldText, newText) {
|
|
283
332
|
if (isBinary(newText) || (oldText !== null && isBinary(oldText))) {
|
|
@@ -316,9 +365,7 @@ export function computeSideBySide(oldText, newText) {
|
|
|
316
365
|
const rows = [];
|
|
317
366
|
let adds = 0;
|
|
318
367
|
let dels = 0;
|
|
319
|
-
|
|
320
|
-
const changedSoFar = () => dels + adds;
|
|
321
|
-
// Returns false when the change budget is exhausted (stop windowing).
|
|
368
|
+
const truncated = false;
|
|
322
369
|
const flushSlice = (s, e) => {
|
|
323
370
|
const slice = raw.slice(s, e);
|
|
324
371
|
let k = 0;
|
|
@@ -376,12 +423,7 @@ export function computeSideBySide(oldText, newText) {
|
|
|
376
423
|
}
|
|
377
424
|
dels += delRun.length;
|
|
378
425
|
adds += addRun.length;
|
|
379
|
-
if (changedSoFar() >= MAX_CHANGED_LINES) {
|
|
380
|
-
truncated = true;
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
426
|
}
|
|
384
|
-
return true;
|
|
385
427
|
};
|
|
386
428
|
let hunkStart = Math.max(0, changeIdx[0] - DIFF_CONTEXT);
|
|
387
429
|
let hunkEnd = Math.min(raw.length, changeIdx[0] + DIFF_CONTEXT + 1);
|
|
@@ -392,31 +434,11 @@ export function computeSideBySide(oldText, newText) {
|
|
|
392
434
|
hunkEnd = Math.min(raw.length, cur + DIFF_CONTEXT + 1);
|
|
393
435
|
}
|
|
394
436
|
else {
|
|
395
|
-
|
|
396
|
-
return { kind: "diff", rows, adds, dels, truncated, isNewFile: oldText === null };
|
|
397
|
-
}
|
|
437
|
+
flushSlice(hunkStart, hunkEnd);
|
|
398
438
|
hunkStart = Math.max(0, cur - DIFF_CONTEXT);
|
|
399
439
|
hunkEnd = Math.min(raw.length, cur + DIFF_CONTEXT + 1);
|
|
400
440
|
}
|
|
401
441
|
}
|
|
402
442
|
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
443
|
return { kind: "diff", rows, adds, dels, truncated, isNewFile: oldText === null };
|
|
422
444
|
}
|
package/dist/ui/errors.js
CHANGED
|
@@ -14,6 +14,20 @@ export function parseToolLabel(label) {
|
|
|
14
14
|
export function titleCase(name) {
|
|
15
15
|
return name.length > 0 ? name[0].toUpperCase() + name.slice(1) : name;
|
|
16
16
|
}
|
|
17
|
+
// One-line guarantee (ticket 04): the card is a summary, never a wall. The
|
|
18
|
+
// loop already commits only the first error line, but the classifier must
|
|
19
|
+
// not trust its input — any multi-line turn committed directly (tests,
|
|
20
|
+
// future paths) collapses to its first line here, and pathological
|
|
21
|
+
// single-line output caps at MAX_DETAIL_CHARS. The full text always waits
|
|
22
|
+
// in the Ctrl+O inspector store (tool failures) or the model reply stream.
|
|
23
|
+
export const MAX_DETAIL_CHARS = 200;
|
|
24
|
+
export function summarizeDetail(raw) {
|
|
25
|
+
const first = raw
|
|
26
|
+
.replace(/^[↳\s]+/, "")
|
|
27
|
+
.split("\n", 1)[0]
|
|
28
|
+
.trim();
|
|
29
|
+
return first.length > MAX_DETAIL_CHARS ? `${first.slice(0, MAX_DETAIL_CHARS)}…` : first;
|
|
30
|
+
}
|
|
17
31
|
const NETWORK_RE = /HTTP (429|500|502|503|504)|connection reset|fetch failed|network|timed? ?out|ENOTFOUND|ECONN|EAI_AGAIN|socket hang up/i;
|
|
18
32
|
const MODEL_RE = /Empty reply|Truncated stream|malformed|unexpected payload|no .* usage|invalid response/i;
|
|
19
33
|
const CONFIG_RE = /Missing API key|no API key|not configured|invalid baseURL|auth/i;
|
|
@@ -29,14 +43,14 @@ export function classifyToolError(turn, label) {
|
|
|
29
43
|
return {
|
|
30
44
|
kind: "config",
|
|
31
45
|
title: "Setup needed",
|
|
32
|
-
detail: content
|
|
46
|
+
detail: summarizeDetail(content),
|
|
33
47
|
hint: "Run /provider to paste a key, then resend.",
|
|
34
48
|
inspectable: false,
|
|
35
49
|
};
|
|
36
50
|
}
|
|
37
51
|
return null;
|
|
38
52
|
}
|
|
39
|
-
const detail = content
|
|
53
|
+
const detail = summarizeDetail(content);
|
|
40
54
|
if (/denied by user/i.test(detail)) {
|
|
41
55
|
const tool = detail.split(":").pop()?.trim() ?? "";
|
|
42
56
|
return {
|
|
@@ -105,10 +119,10 @@ export function classifyToolError(turn, label) {
|
|
|
105
119
|
};
|
|
106
120
|
}
|
|
107
121
|
const KIND_GLYPH = {
|
|
108
|
-
tool:
|
|
109
|
-
denial:
|
|
110
|
-
network:
|
|
111
|
-
model:
|
|
122
|
+
tool: theme.symbol.toolFail,
|
|
123
|
+
denial: theme.symbol.toolDenied,
|
|
124
|
+
network: theme.symbol.warningMark,
|
|
125
|
+
model: theme.symbol.toolFail,
|
|
112
126
|
cancelled: "",
|
|
113
127
|
config: "→",
|
|
114
128
|
internal: "‼",
|
package/dist/ui/input.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
// Input leaf: the boxed prompt surface. Prop-driven + memoized on
|
|
3
|
-
// (input, cursor) so timer ticks never repaint it. Multiline aware: the
|
|
4
|
-
// cursor rides line/col and long lines wrap via Ink.
|
|
5
|
-
//
|
|
3
|
+
// (input, cursor, busy) so timer ticks never repaint it. Multiline aware: the
|
|
4
|
+
// cursor rides line/col and long lines wrap via Ink.
|
|
5
|
+
// Footer-cluster states: idle shows just the box + cursor; busy dims the
|
|
6
|
+
// whole surface and carries its own interrupt hint, so the input never
|
|
7
|
+
// vanishes mid-turn — Enter while busy queues, esc stops.
|
|
6
8
|
// Paint from ui/theme tokens — no literal colors or glyphs here.
|
|
7
9
|
import React from "react";
|
|
8
10
|
import { Box, Text } from "ink";
|
|
@@ -14,27 +16,29 @@ import { lineColOf, splitInputLines } from "./input-model.js";
|
|
|
14
16
|
export const inputRenderProbe = { count: 0 };
|
|
15
17
|
// The input is the one boxed, prominent surface: a quiet gray frame sets it
|
|
16
18
|
// apart from the transcript above and the status line below. Memoized on
|
|
17
|
-
// (input, cursor) so elapsed-timer ticks, token paints, and unrelated
|
|
18
|
-
// state churn never repaint it — keystrokes stay at exactly one paint
|
|
19
|
-
// which is what makes navigation feel instant instead of choppy.
|
|
20
|
-
|
|
19
|
+
// (input, cursor, busy) so elapsed-timer ticks, token paints, and unrelated
|
|
20
|
+
// App state churn never repaint it — keystrokes stay at exactly one paint
|
|
21
|
+
// each, which is what makes navigation feel instant instead of choppy.
|
|
22
|
+
// `busy` flips only at turn boundaries (never per tick/token), so the
|
|
23
|
+
// working state costs exactly one extra paint per turn edge.
|
|
24
|
+
export const InputBox = React.memo(function InputBox({ input, cursor, busy = false }) {
|
|
21
25
|
inputRenderProbe.count += 1;
|
|
22
26
|
// Defensive clamp: the ref is the source of truth mid-tick and always
|
|
23
27
|
// stays in range, but state may lag it by one render.
|
|
24
28
|
const safeCursor = Math.max(0, Math.min(cursor, input.length));
|
|
25
29
|
const lines = splitInputLines(input);
|
|
26
30
|
const { line: cline, col: ccol } = lineColOf(input, safeCursor);
|
|
27
|
-
return (_jsxs(Box, { borderStyle: theme.border.style, borderColor: theme.border.input, paddingX: theme.spacing.pickerPadX, children: [_jsxs(Text, { color: theme.color.inputPrompt, bold: true, children: [theme.symbol.inputPrompt, " "] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: lines.map((ln, i) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Box, { borderStyle: theme.border.style, borderColor: theme.border.input, paddingX: theme.spacing.pickerPadX, children: [_jsxs(Text, { color: theme.color.inputPrompt, bold: true, dimColor: busy, children: [theme.symbol.inputPrompt, " "] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: lines.map((ln, i) => {
|
|
32
|
+
if (i !== cline)
|
|
33
|
+
return (_jsx(Text, { dimColor: busy, children: ln.length > 0 ? ln : " " }, i));
|
|
34
|
+
// See-through cursor: the character under the cursor renders in
|
|
35
|
+
// inverse video instead of inserting a block glyph beside it, so
|
|
36
|
+
// letters never shift aside as the cursor moves (the old block
|
|
37
|
+
// made the line wobble on every arrow-key step). At end of line
|
|
38
|
+
// (or on an empty line) an inverse space holds the cell.
|
|
39
|
+
const before = ln.slice(0, ccol);
|
|
40
|
+
const at = ln.slice(ccol, ccol + 1);
|
|
41
|
+
const after = ln.slice(ccol + 1);
|
|
42
|
+
return (_jsxs(Text, { dimColor: busy, children: [before, _jsx(Text, { inverse: true, children: at.length > 0 ? at : " " }), after] }, i));
|
|
43
|
+
}) })] }), busy ? (_jsxs(Text, { dimColor: true, children: [theme.symbol.workTool, " working ", theme.symbol.separator, " esc stops ", theme.symbol.separator, " Enter queues"] })) : null] }));
|
|
40
44
|
});
|
package/dist/ui/live-tail.js
CHANGED
|
@@ -9,6 +9,12 @@ import { Box, Text } from "ink";
|
|
|
9
9
|
import { activityText } from "./activity.js";
|
|
10
10
|
import { MarkdownStream } from "./markdown.js";
|
|
11
11
|
import { theme } from "./theme.js";
|
|
12
|
+
// Live thinking window (render-stability): reasoning streams at token rate,
|
|
13
|
+
// and painting the full accumulated text every 64ms both churns frame
|
|
14
|
+
// height and re-lays-out an ever-growing block. The live view shows only
|
|
15
|
+
// the tail — the full text still commits to the transcript at the round
|
|
16
|
+
// boundary, so nothing is ever lost.
|
|
17
|
+
export const LIVE_THINKING_LINES = 8;
|
|
12
18
|
export const LiveTail = React.memo(function LiveTail({ isEmpty, sessionHint, emptySessionTitle, draft, thinking, busy, held, toolHint, toolElapsedSecs, elapsedSecs, showThinking = true }) {
|
|
13
19
|
// Held view (user scrolled up mid-turn): the growing draft/thinking blocks
|
|
14
20
|
// are replaced by one static line so the frame stops gaining terminal
|
|
@@ -17,5 +23,34 @@ export const LiveTail = React.memo(function LiveTail({ isEmpty, sessionHint, emp
|
|
|
17
23
|
// status (tool hint, thinking tick) keeps updating in place: same line,
|
|
18
24
|
// no growth, no yank.
|
|
19
25
|
const freezeLive = held === true && busy;
|
|
20
|
-
|
|
26
|
+
const thoughtLines = thinking !== null ? thinking.split("\n") : [];
|
|
27
|
+
const thoughtTail = thoughtLines.slice(-LIVE_THINKING_LINES);
|
|
28
|
+
const thoughtTruncated = thoughtLines.length > thoughtTail.length;
|
|
29
|
+
// Restraint (ticket 07): an empty live zone mounts nothing. The Box below
|
|
30
|
+
// carries marginY, which Ink paints as blank lines even with no children —
|
|
31
|
+
// without this guard every idle frame with history wasted two vertical
|
|
32
|
+
// lines between the transcript and the input. When the transcript is empty
|
|
33
|
+
// the hint lines always render, so only the non-empty, nothing-live case
|
|
34
|
+
// collapses. Mirror the JSX conditions below (falsy draft/toolHint render
|
|
35
|
+
// nothing there, so they count as nothing here too).
|
|
36
|
+
const showsDraft = !freezeLive && !!draft;
|
|
37
|
+
const showsThinking = !freezeLive && thinking !== null && showThinking;
|
|
38
|
+
const showsToolHint = busy && !!toolHint;
|
|
39
|
+
const showsThinkingGap = !freezeLive && busy && !draft && !thinking && !toolHint;
|
|
40
|
+
if (!isEmpty &&
|
|
41
|
+
!freezeLive &&
|
|
42
|
+
!showsDraft &&
|
|
43
|
+
!showsThinking &&
|
|
44
|
+
!showsToolHint &&
|
|
45
|
+
!showsThinkingGap) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: theme.spacing.liveTailMarginY, children: [isEmpty ? (_jsx(Text, { dimColor: true, children: "Say hi to Atom \u2014 or type / for commands, /provider to pick a provider + key, /model to switch models." })) : null, isEmpty && emptySessionTitle && emptySessionTitle.trim() ? (_jsxs(Text, { dimColor: true, children: ["Session: ", emptySessionTitle.trim()] })) : null, sessionHint && isEmpty ? (_jsx(Text, { dimColor: true, children: "(last session available \u2014 /resume to restore)" })) : null, freezeLive ? (_jsxs(Text, { dimColor: true, children: [theme.symbol.moreAbove, " held \u2014 turn running \u00B7 End to follow"] })) : null, !freezeLive && draft ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: _jsxs(Text, { color: theme.color.assistant, bold: true, children: [theme.symbol.speakerAssistant, " "] }) }), _jsx(MarkdownStream, { text: draft })] })) : null, !freezeLive && thinking && showThinking ? (
|
|
49
|
+
// Grouped thinking unit: dim labeled header + quoteBar-prefixed tail
|
|
50
|
+
// body reads as one quiet block, structurally separate from the
|
|
51
|
+
// answer draft above (magenta ATOM> + markdown). Tail-window behavior
|
|
52
|
+
// unchanged; divider lives inside this row's own box (no extra
|
|
53
|
+
// Static rows). Muted = dimColor per theme law, never gray paint
|
|
54
|
+
// (cursor glyph keeps the reserved mutedPaint shade).
|
|
55
|
+
_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [theme.symbol.thinking, " thinking", thoughtTruncated ? ` ${theme.symbol.ellipsis}` : ""] }), thoughtTail.map((line, idx) => (_jsxs(Text, { dimColor: true, children: [`${theme.symbol.quoteBar} ${line}`, idx === thoughtTail.length - 1 ? (_jsx(Text, { color: theme.color.mutedPaint, children: theme.symbol.cursorBar })) : null] }, idx)))] })) : null, busy && toolHint ? (_jsxs(Text, { dimColor: true, children: [theme.symbol.workTool, " ", activityText(toolHint), toolElapsedSecs !== null && toolElapsedSecs >= 2 ? (_jsxs(_Fragment, { children: [" ", theme.symbol.separator, " ", toolElapsedSecs, "s"] })) : (theme.symbol.ellipsis)] })) : null, !freezeLive && busy && !draft && !thinking && !toolHint ? (_jsxs(Text, { dimColor: true, children: [theme.symbol.workThinking, " Thinking", theme.symbol.ellipsis, " ", theme.symbol.separator, " ", elapsedSecs, "s"] })) : null] }));
|
|
21
56
|
});
|
package/dist/ui/markdown.js
CHANGED
|
@@ -532,9 +532,14 @@ export function MarkdownText({ text }) {
|
|
|
532
532
|
// True folding needs interactive history (Static items freeze on commit);
|
|
533
533
|
// this component is that chunk's seam.
|
|
534
534
|
export const TOOL_SLOW_MS = 2000;
|
|
535
|
-
export function ToolLine({ content, error, ms }) {
|
|
535
|
+
export function ToolLine({ content, error, ms, via }) {
|
|
536
|
+
// Approval provenance suffix (ticket 04): a dim `· via <token>` marker
|
|
537
|
+
// rendered OUTSIDE the label text, so `⚙ name target` stays byte-identical
|
|
538
|
+
// for the parsers/tests that read it (parseToolLabel/parseActivityHint)
|
|
539
|
+
// while what allowed the call stays visible on the audit line.
|
|
540
|
+
const suffix = via ? ` ${theme.symbol.separator} via ${via}` : "";
|
|
536
541
|
if (error) {
|
|
537
|
-
return
|
|
542
|
+
return _jsxs(Text, { color: theme.color.toolError, children: [content, suffix] });
|
|
538
543
|
}
|
|
539
544
|
if (content.startsWith("⚠ ")) {
|
|
540
545
|
return _jsx(Text, { color: theme.color.warning, children: content });
|
|
@@ -543,7 +548,7 @@ export function ToolLine({ content, error, ms }) {
|
|
|
543
548
|
!content.includes("\n") &&
|
|
544
549
|
ms !== undefined &&
|
|
545
550
|
ms >= TOOL_SLOW_MS) {
|
|
546
|
-
return (_jsxs(Text, { color: theme.color.tool, dimColor: true, children: [content, " ", theme.symbol.separator, " ", Math.round(ms / 1000), "s"] }));
|
|
551
|
+
return (_jsxs(Text, { color: theme.color.tool, dimColor: true, children: [content, suffix, " ", theme.symbol.separator, " ", Math.round(ms / 1000), "s"] }));
|
|
547
552
|
}
|
|
548
|
-
return (
|
|
553
|
+
return (_jsxs(Text, { color: theme.color.tool, dimColor: true, children: [content, suffix] }));
|
|
549
554
|
}
|
package/dist/ui/modals.js
CHANGED
|
@@ -7,9 +7,11 @@ 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
|
-
//
|
|
11
|
-
// the trailer names the remainder).
|
|
12
|
-
//
|
|
10
|
+
// Display-only window for the approval preview (hunk headers excluded;
|
|
11
|
+
// the trailer names the remainder). The diff engine stays uncapped and the
|
|
12
|
+
// transcript renders the full diff on approve — this window only keeps the
|
|
13
|
+
// modal (and its allow/deny options) on screen instead of pushing the frame
|
|
14
|
+
// into fullscreen full-clear territory on every large write.
|
|
13
15
|
export const APPROVAL_DIFF_MAX_LINES = 40;
|
|
14
16
|
export const APPROVAL_OPTIONS = ["once", "always", "trustAll", "no"];
|
|
15
17
|
// Command/file preview: the audit description minus its `⚙ name` prefix
|
|
@@ -39,7 +41,7 @@ export const ApprovalBox = React.memo(function ApprovalBox({ toolName, descripti
|
|
|
39
41
|
{ label: "[t]rust all write/edit/bash this session", option: "trustAll" },
|
|
40
42
|
{ label: "[n]o — deny this call", option: "no" },
|
|
41
43
|
];
|
|
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" })] }));
|
|
44
|
+
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, path: diff.path, maxRows: APPROVAL_DIFF_MAX_LINES }) : null, diff ? _jsx(Text, { dimColor: true, children: "Full diff renders in the transcript on approve." }) : 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
45
|
});
|
|
44
46
|
export const QuestionBox = React.memo(function QuestionBox({ question, options, allowCustom, askCustom, askSelIndex }) {
|
|
45
47
|
questionRenderProbe.count += 1;
|