atom-agent 1.0.0 → 1.1.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 +31 -2
- package/README.md +12 -12
- package/dist/App.js +297 -23
- package/dist/adapters.js +84 -6
- package/dist/agent/loop-guard.js +184 -0
- package/dist/agent/loop.js +609 -300
- package/dist/agent/normalize.js +144 -0
- package/dist/cli.js +1 -1
- package/dist/system.js +1 -0
- package/dist/telemetry-dashboard.js +19 -1
- package/dist/telemetry.js +55 -0
- package/dist/tools/dir-cache.js +207 -0
- package/dist/tools/filesystem.js +40 -1
- package/dist/tools/read-cache.js +160 -0
- package/dist/tools/registry.js +79 -0
- package/dist/tools/search.js +66 -60
- package/dist/tools/shell.js +19 -0
- package/dist/tools/todo.js +1 -1
- package/dist/tools.js +2 -0
- package/dist/ui/diff-panel.js +55 -0
- package/dist/ui/diff-view.js +112 -0
- package/dist/ui/diff.js +422 -0
- package/dist/ui/highlight.js +120 -0
- package/dist/ui/live-tail.js +2 -2
- package/dist/ui/modals.js +22 -5
- package/dist/ui/palette.js +10 -2
- package/dist/ui/side-by-side.js +144 -0
- package/dist/ui/status-bar.js +20 -4
- package/dist/ui/theme.js +6 -0
- package/dist/ui/todo-panel.js +10 -2
- package/dist/ui/transcript.js +16 -4
- package/dist/zen.js +33 -9
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Side-by-side diff view for write/edit results: BEFORE pane (left) vs
|
|
3
|
+
// AFTER pane (right), aligned rows, preserved line numbers.
|
|
4
|
+
//
|
|
5
|
+
// Requirements it answers (presentation only — data comes from the
|
|
6
|
+
// existing approve-time preview / commit slot, tool behavior untouched):
|
|
7
|
+
// - left = original, right = result; additions right, removals left,
|
|
8
|
+
// context on both sides; paired changes share ONE row so corresponding
|
|
9
|
+
// lines align; changed regions pop via the existing word-background +
|
|
10
|
+
// add/del line-number treatment; syntax colors reused per cell.
|
|
11
|
+
// - hunks only (configurable context in the engine) — never whole files.
|
|
12
|
+
// - width-aware: panes split the measured terminal (useStdout, same
|
|
13
|
+
// pattern as App's termColumns); long lines truncate per pane with …
|
|
14
|
+
// (code-point safe); below NARROW_COLUMNS the view degrades to the
|
|
15
|
+
// stacked unified DiffView instead of destroying the layout.
|
|
16
|
+
// - computed once per mount (useMemo, keyed on inputs + pane width) and
|
|
17
|
+
// capped (maxRows + trailer) — never recomputed per tick, never floods.
|
|
18
|
+
// All paint comes from ui/theme tokens.
|
|
19
|
+
import React from "react";
|
|
20
|
+
import { Box, Text } from "ink";
|
|
21
|
+
import { useStdout } from "ink";
|
|
22
|
+
import { computeSideBySide, wordRuns } from "./diff.js";
|
|
23
|
+
import { DiffView, LineBody } from "./diff-view.js";
|
|
24
|
+
import { theme } from "./theme.js";
|
|
25
|
+
// Below this width two panes cannot breathe — stack unified instead.
|
|
26
|
+
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;
|
|
31
|
+
function truncateTo(s, width) {
|
|
32
|
+
const chars = [...s];
|
|
33
|
+
if (chars.length <= width)
|
|
34
|
+
return s;
|
|
35
|
+
if (width < 4)
|
|
36
|
+
return "";
|
|
37
|
+
return chars.slice(0, width - 1).join("") + theme.symbol.ellipsis;
|
|
38
|
+
}
|
|
39
|
+
// Fit engine rows to a pane content width: truncate cell texts (with …)
|
|
40
|
+
// and re-derive word runs on the truncated pair so offsets always tile
|
|
41
|
+
// the displayed text. Runs once per mount/width — never per tick.
|
|
42
|
+
function fitRows(rows, contentW) {
|
|
43
|
+
return rows.map((r) => {
|
|
44
|
+
if (r.kind === "context") {
|
|
45
|
+
return {
|
|
46
|
+
kind: "context",
|
|
47
|
+
left: { no: r.oldNo, text: truncateTo(r.text, contentW) },
|
|
48
|
+
right: { no: r.newNo, text: truncateTo(r.text, contentW) },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const tOld = r.oldText !== null ? truncateTo(r.oldText, contentW) : null;
|
|
52
|
+
const tNew = r.newText !== null ? truncateTo(r.newText, contentW) : null;
|
|
53
|
+
let oldRuns = [];
|
|
54
|
+
let newRuns = [];
|
|
55
|
+
if (tOld !== null && tNew !== null) {
|
|
56
|
+
const w = wordRuns(tOld, tNew);
|
|
57
|
+
oldRuns = w.del;
|
|
58
|
+
newRuns = w.add;
|
|
59
|
+
}
|
|
60
|
+
else if (tOld !== null) {
|
|
61
|
+
oldRuns = tOld === "" ? [] : [{ text: tOld, changed: false }];
|
|
62
|
+
}
|
|
63
|
+
else if (tNew !== null) {
|
|
64
|
+
newRuns = tNew === "" ? [] : [{ text: tNew, changed: false }];
|
|
65
|
+
}
|
|
66
|
+
const changed = oldRuns.some((x) => x.changed) ||
|
|
67
|
+
newRuns.some((x) => x.changed) ||
|
|
68
|
+
(r.oldText === null) !== (r.newText === null);
|
|
69
|
+
return {
|
|
70
|
+
kind: "change",
|
|
71
|
+
changed,
|
|
72
|
+
left: tOld !== null ? { no: r.oldNo, text: tOld, runs: oldRuns } : null,
|
|
73
|
+
right: tNew !== null ? { no: r.newNo, text: tNew, runs: newRuns } : null,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function padEnd(s, width) {
|
|
78
|
+
const len = [...s].length;
|
|
79
|
+
if (len >= width)
|
|
80
|
+
return s;
|
|
81
|
+
return s + " ".repeat(width - len);
|
|
82
|
+
}
|
|
83
|
+
function SideBySideInner({ oldText, newText, lang = null, maxRows = Infinity, columns, }) {
|
|
84
|
+
let stdoutCols;
|
|
85
|
+
try {
|
|
86
|
+
stdoutCols = useStdout()?.stdout?.columns;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
stdoutCols = undefined;
|
|
90
|
+
}
|
|
91
|
+
const totalW = columns ?? stdoutCols ?? 100;
|
|
92
|
+
const sbs = React.useMemo(() => computeSideBySide(oldText, newText), [oldText, newText]);
|
|
93
|
+
if (sbs.kind === "same") {
|
|
94
|
+
return _jsx(Text, { dimColor: true, children: "(no changes \u2014 file unchanged)" });
|
|
95
|
+
}
|
|
96
|
+
if (sbs.kind === "binary") {
|
|
97
|
+
return _jsx(Text, { dimColor: true, children: "binary file changed" });
|
|
98
|
+
}
|
|
99
|
+
if (sbs.kind === "skipped") {
|
|
100
|
+
return _jsx(Text, { dimColor: true, children: sbs.reason });
|
|
101
|
+
}
|
|
102
|
+
// Graceful narrow-terminal degrade: stacked unified keeps every char
|
|
103
|
+
// instead of crushing two panes into unreadable slivers.
|
|
104
|
+
if (totalW < SBS_NARROW_COLUMNS) {
|
|
105
|
+
return _jsx(DiffView, { oldText: oldText, newText: newText, lang: lang, maxLines: maxRows });
|
|
106
|
+
}
|
|
107
|
+
const sep = ` ${theme.symbol.bar} `;
|
|
108
|
+
const paneW = Math.max(20, Math.floor((totalW - sep.length) / 2));
|
|
109
|
+
let maxNo = 0;
|
|
110
|
+
for (const r of sbs.rows) {
|
|
111
|
+
if (r.kind === "context")
|
|
112
|
+
maxNo = Math.max(maxNo, r.oldNo, r.newNo);
|
|
113
|
+
else {
|
|
114
|
+
if (r.oldNo !== null)
|
|
115
|
+
maxNo = Math.max(maxNo, r.oldNo);
|
|
116
|
+
if (r.newNo !== null)
|
|
117
|
+
maxNo = Math.max(maxNo, r.newNo);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const numW = String(Math.max(maxNo, 1)).length;
|
|
121
|
+
const contentW = Math.max(8, paneW - numW - 1);
|
|
122
|
+
const view = React.useMemo(() => fitRows(sbs.rows, contentW),
|
|
123
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
124
|
+
[sbs, contentW]);
|
|
125
|
+
const shown = view.slice(0, maxRows);
|
|
126
|
+
const overflow = Math.max(0, view.length - shown.length);
|
|
127
|
+
const renderCell = (no, body, opts) => {
|
|
128
|
+
const num = no === null ? " ".repeat(numW) : padEnd(String(no), numW);
|
|
129
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { color: opts.numColor, dimColor: opts.numColor === undefined || opts.dim, children: [num, " "] }), body] }));
|
|
130
|
+
};
|
|
131
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [sbs.isNewFile ? "new file " : "", "+", sbs.adds, " \u2212", sbs.dels] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: padEnd("BEFORE", paneW) }), _jsx(Text, { dimColor: true, children: sep }), _jsx(Text, { bold: true, children: "AFTER" })] }), shown.map((r, k) => {
|
|
132
|
+
if (r.kind === "context") {
|
|
133
|
+
return (_jsxs(Text, { children: [renderCell(r.left.no, _jsx(Text, { dimColor: true, children: r.left.text }), { dim: true }), _jsx(Text, { dimColor: true, children: sep }), renderCell(r.right.no, _jsx(Text, { dimColor: true, children: r.right.text }), { dim: true })] }, k));
|
|
134
|
+
}
|
|
135
|
+
const leftNumColor = r.left !== null && r.changed ? theme.color.toolError : undefined;
|
|
136
|
+
const rightNumColor = r.right !== null && r.changed ? theme.color.success : undefined;
|
|
137
|
+
return (_jsxs(Text, { children: [r.left !== null
|
|
138
|
+
? renderCell(r.left.no, _jsx(LineBody, { lineText: r.left.text, runs: r.left.runs, base: "del", lang: lang }), { numColor: leftNumColor, dim: !r.changed })
|
|
139
|
+
: renderCell(null, _jsx(Text, { children: padEnd("", contentW) }), { dim: true }), _jsx(Text, { dimColor: true, children: sep }), r.right !== null
|
|
140
|
+
? renderCell(r.right.no, _jsx(LineBody, { lineText: r.right.text, runs: r.right.runs, base: "add", lang: lang }), { numColor: rightNumColor, dim: !r.changed })
|
|
141
|
+
: renderCell(null, _jsx(Text, { children: padEnd("", contentW) }), { dim: true })] }, k));
|
|
142
|
+
}), overflow > 0 ? _jsxs(Text, { dimColor: true, children: ["\u2026 ", overflow, " more row", overflow === 1 ? "" : "s"] }) : null, sbs.truncated ? _jsx(Text, { dimColor: true, children: "(diff truncated at 400 changed lines)" }) : null] }));
|
|
143
|
+
}
|
|
144
|
+
export const SideBySideDiffView = React.memo(SideBySideInner);
|
package/dist/ui/status-bar.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Status bar leaf: the sole info bar, state-prioritized and quiet.
|
|
3
|
+
// - idle: provider/model │ token │ cwd[:branch] │ reasoning │ mode.
|
|
4
|
+
// Labels are positional (no `provider:` prefixes); mode/trust show always
|
|
5
|
+
// (pinned), cwd shortens, branch only for git repos.
|
|
6
|
+
// - busy: activity │ elapsed │ token │ reasoning │ mode │ esc-hint (+waiting/approval flags).
|
|
7
|
+
// Provider/model/cwd drop while working — the activity, the
|
|
8
|
+
// clock, context pressure, effort, and the pinned mode are what matter mid-turn.
|
|
9
|
+
// All paint comes from ui/theme tokens. The token segment formatter lives
|
|
10
|
+
// in context-windows (its only surface).
|
|
11
|
+
import React from "react";
|
|
2
12
|
import { Box, Text } from "ink";
|
|
3
13
|
import { formatTokenSegment } from "../context-windows.js";
|
|
4
14
|
import { theme } from "./theme.js";
|
|
@@ -20,7 +30,12 @@ export function shrinkTo(s, n) {
|
|
|
20
30
|
return "";
|
|
21
31
|
return `…/${s.slice(-(n - 3))}`;
|
|
22
32
|
}
|
|
23
|
-
|
|
33
|
+
// Render-count probe for the flicker tests: incremented on every StatusBar
|
|
34
|
+
// render (same-props parent churn — token paints, keystrokes, unrelated
|
|
35
|
+
// ticks — must skip it; only changed props repaint).
|
|
36
|
+
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, }) {
|
|
38
|
+
statusBarRenderProbe.count += 1;
|
|
24
39
|
const bar = theme.symbol.bar;
|
|
25
40
|
if (!busy) {
|
|
26
41
|
const token = formatTokenSegment(usageTotals, model, contextLoad);
|
|
@@ -49,11 +64,12 @@ export function StatusBar({ provider, model, usageTotals, contextLoad, reasoning
|
|
|
49
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] }) }));
|
|
50
65
|
}
|
|
51
66
|
// Busy layout prioritizes activity + clock + interrupt hint; the mode
|
|
52
|
-
// stays pinned (it used to vanish while working)
|
|
67
|
+
// stays pinned (it used to vanish while working), and the reasoning
|
|
68
|
+
// effort stays visible (it used to vanish while working). The activity text
|
|
53
69
|
// shrinks to fit so `esc stops` never wraps away.
|
|
54
70
|
const busyTrust = trustAll && mode !== "plan" ? "+trust" : "";
|
|
55
|
-
const busyFixed = ` ${bar} ${elapsedSecs}s ${bar} ${formatTokenSegment(usageTotals, model, contextLoad)} ${bar} mode: ${mode}${busyTrust} ${bar} esc stops`;
|
|
71
|
+
const busyFixed = ` ${bar} ${elapsedSecs}s ${bar} ${formatTokenSegment(usageTotals, model, contextLoad)} ${bar} reasoning: ${reasoningDisplay} ${bar} mode: ${mode}${busyTrust} ${bar} esc stops`;
|
|
56
72
|
const busyAvail = columns - busyFixed.length - 2;
|
|
57
73
|
const activityText = shrinkTo(activity ?? phaseLabel, Math.max(0, busyAvail));
|
|
58
74
|
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] }) }));
|
|
59
|
-
}
|
|
75
|
+
});
|
package/dist/ui/theme.js
CHANGED
|
@@ -54,6 +54,12 @@ export const theme = {
|
|
|
54
54
|
code: "green",
|
|
55
55
|
link: "cyan",
|
|
56
56
|
heading: undefined, // bold, no hue
|
|
57
|
+
// Diff-body syntax colors (ui/highlight): Monokai-ish hues that read
|
|
58
|
+
// on dark and light terminals. Comments stay dim (no hue — same rule
|
|
59
|
+
// as muted text); plain code inherits the line paint.
|
|
60
|
+
synKeyword: "magenta",
|
|
61
|
+
synString: "yellow",
|
|
62
|
+
synNumber: "cyan",
|
|
57
63
|
},
|
|
58
64
|
border: {
|
|
59
65
|
style: "round",
|
package/dist/ui/todo-panel.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Live session checklist panel (TodoWrite mirror). Prop-driven; returns
|
|
3
|
+
// null when empty. Mounted below the transcript, fed by a checklist snapshot.
|
|
4
|
+
// Paint from ui/theme tokens — no literal colors or glyphs here.
|
|
5
|
+
import React from "react";
|
|
2
6
|
import { Box, Text } from "ink";
|
|
3
7
|
import { theme } from "./theme.js";
|
|
4
8
|
// Live session checklist (Claude-Code-style TodoWrite panel). Mounted in
|
|
@@ -6,7 +10,11 @@ import { theme } from "./theme.js";
|
|
|
6
10
|
// by a snapshot the loop refreshes after every todowrite/todo_update call,
|
|
7
11
|
// so the in-progress row — shown with its activeForm when present — always
|
|
8
12
|
// answers "what is the model doing right now". Returns null when empty.
|
|
9
|
-
|
|
13
|
+
// Render-count probe for the flicker tests: same-props parent churn must
|
|
14
|
+
// skip the panel (it only changes when the loop commits todo activity).
|
|
15
|
+
export const todoPanelRenderProbe = { count: 0 };
|
|
16
|
+
export const TodoPanel = React.memo(function TodoPanel({ items }) {
|
|
17
|
+
todoPanelRenderProbe.count += 1;
|
|
10
18
|
if (items.length === 0)
|
|
11
19
|
return null;
|
|
12
20
|
const done = items.filter((t) => t.status === "completed").length;
|
|
@@ -19,4 +27,4 @@ export function TodoPanel({ items }) {
|
|
|
19
27
|
const label = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
|
|
20
28
|
return (_jsxs(Text, { dimColor: t.status === "completed", children: [mark, " ", label, t.priority ? ` (${t.priority})` : ""] }, `${i}-${t.content}`));
|
|
21
29
|
})] }));
|
|
22
|
-
}
|
|
30
|
+
});
|
package/dist/ui/transcript.js
CHANGED
|
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
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, Text } from "ink";
|
|
8
|
+
import { SideBySideDiffView, TRANSCRIPT_DIFF_MAX_LINES } from "./side-by-side.js";
|
|
8
9
|
import { ErrorCard, classifyToolError } from "./errors.js";
|
|
9
10
|
import { MarkdownText, ToolLine } from "./markdown.js";
|
|
10
11
|
import { theme } from "./theme.js";
|
|
@@ -54,6 +55,11 @@ export function renderTranscriptItem(item) {
|
|
|
54
55
|
return _jsx(StartupBanner, {}, item.id);
|
|
55
56
|
const t = item.turn;
|
|
56
57
|
const i = item.id;
|
|
58
|
+
// Committed thinking blocks read as quiet annotations (never confused
|
|
59
|
+
// with answers): dim label plus the raw reasoning text, verbatim.
|
|
60
|
+
if (t.thinking === true) {
|
|
61
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [theme.symbol.thinking, " thinking"] }), _jsx(Text, { dimColor: true, children: t.content })] }, i));
|
|
62
|
+
}
|
|
57
63
|
// Conversation turns (user/assistant) breathe: one blank line after each,
|
|
58
64
|
// so the eye lands on the next turn. Tool/status lines stay dense — they
|
|
59
65
|
// read as lightweight annotations woven between turns, not blocks.
|
|
@@ -65,17 +71,20 @@ export function renderTranscriptItem(item) {
|
|
|
65
71
|
if (classified) {
|
|
66
72
|
// Paired cards keep the verbatim audit line above the card (pinned
|
|
67
73
|
// `⚙ name target` text for tests/scanning) and name the failure in
|
|
68
|
-
// the card title. Lone details render the card alone.
|
|
69
|
-
|
|
74
|
+
// the card title. Lone details render the card alone. A successful
|
|
75
|
+
// write/edit label swallowed by pairing (success line immediately
|
|
76
|
+
// followed by an error line) keeps its committed diff above the card.
|
|
77
|
+
const labelDiff = item.label?.diff;
|
|
78
|
+
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));
|
|
70
79
|
}
|
|
71
|
-
return _jsx(ToolLine, { content: t.content, error: t.error, ms: t.ms }, i);
|
|
80
|
+
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));
|
|
72
81
|
}
|
|
73
82
|
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));
|
|
74
83
|
}
|
|
75
84
|
// Render-count probe for the timer-isolation test: incremented on every
|
|
76
85
|
// TranscriptView render (a 1s timer tick must leave it unchanged).
|
|
77
86
|
export const transcriptRenderProbe = { count: 0 };
|
|
78
|
-
export const TranscriptView = React.memo(function TranscriptView({ turns, clearGen, renderItem, end, windowSize, held, }) {
|
|
87
|
+
export const TranscriptView = React.memo(function TranscriptView({ turns, clearGen, renderItem, end, windowSize, held, showThinking = true, }) {
|
|
79
88
|
transcriptRenderProbe.count += 1;
|
|
80
89
|
const render = renderItem ?? renderTranscriptItem;
|
|
81
90
|
const win = windowSize ?? SCROLLBACK_WINDOW;
|
|
@@ -83,9 +92,12 @@ export const TranscriptView = React.memo(function TranscriptView({ turns, clearG
|
|
|
83
92
|
// Pairing ([audit label, error detail] → one card) runs over the VISIBLE
|
|
84
93
|
// slice only — pairing is positional, and off-window turns never mount.
|
|
85
94
|
// Keys stay global (`turn-${idx}`) so scrolling never remounts rows.
|
|
95
|
+
// Hidden thinking turns are skipped in place (same index stability).
|
|
86
96
|
const body = [];
|
|
87
97
|
for (let idx = vp.start; idx < vp.end; idx++) {
|
|
88
98
|
const turn = turns[idx];
|
|
99
|
+
if (turn.thinking === true && !showThinking)
|
|
100
|
+
continue;
|
|
89
101
|
const next = idx + 1 < vp.end ? turns[idx + 1] : undefined;
|
|
90
102
|
if (isAuditLabel(turn) && next !== undefined && next.role === "tool" && next.error === true) {
|
|
91
103
|
body.push({ id: `turn-${idx}`, turn: next, label: turn });
|
package/dist/zen.js
CHANGED
|
@@ -8,7 +8,8 @@ import * as path from "node:path";
|
|
|
8
8
|
import { MAX_TOOL_STEPS, TOOL_DEFINITIONS, } from "./tools.js";
|
|
9
9
|
import { chatEndpointFor, getProvider, isLocalProviderId, modelsUrlForProvider, providerLabel, } from "./providers.js";
|
|
10
10
|
import { discoverLocalProvider } from "./local-discovery.js";
|
|
11
|
-
import { ANTHROPIC_VERSION, anthropicHeaders, buildAnthropicBody, buildGeminiBody, geminiChatUrl, geminiGenerateUrl, geminiHeaders, parseAnthropicJson, parseAnthropicModelsList, parseGeminiJson, parseGeminiModelsList, parseOpenAIModelsList, readAnthropicSSEMessage, readGeminiSSEMessage, } from "./adapters.js";
|
|
11
|
+
import { ANTHROPIC_VERSION, anthropicHeaders, buildAnthropicBody, buildGeminiBody, geminiChatUrl, geminiGenerateUrl, geminiHeaders, parseAnthropicJson, parseAnthropicModelsList, parseGeminiJson, parseGeminiModelsList, parseOpenAIModelsList, readAnthropicSSEMessage, readGeminiSSEMessage, isStallError, readWithStall, } from "./adapters.js";
|
|
12
|
+
export { isStallError, readWithStall, sseStallTimeoutMs } from "./adapters.js";
|
|
12
13
|
import { KILO_FALLBACK_MODELS, fetchKiloModelsWithStatus, normalizeKiloChatError, } from "./kilo.js";
|
|
13
14
|
import { splitSystemHead } from "./prompt-cache.js";
|
|
14
15
|
import { SYSTEM_PROMPT } from "./system.js";
|
|
@@ -336,6 +337,11 @@ export async function fetchModels(endpoint, apiKey) {
|
|
|
336
337
|
// - Slots with an id but no name at [DONE] are dropped with an onWarning
|
|
337
338
|
// message and never returned (keeps assistant/tool pairing valid).
|
|
338
339
|
// - A stream that ends without [DONE] throws a truncation error.
|
|
340
|
+
// - A stream silent longer than the stall budget (env ATOM_STALL_TIMEOUT_MS,
|
|
341
|
+
// default 60s; the clock resets on every received chunk) throws a
|
|
342
|
+
// Truncated-stream stall error — permanent, never retried, same contract
|
|
343
|
+
// as a dead connection (verified live: free-tier routers can stall a
|
|
344
|
+
// 200-OK stream mid-generation for minutes).
|
|
339
345
|
// - A stream with zero "data:" lines is treated as a non-SSE JSON payload
|
|
340
346
|
// (tolerance for bodies that are really single-shot JSON) and parsed as
|
|
341
347
|
// choices[0].message like the non-streaming fallback.
|
|
@@ -513,9 +519,11 @@ export async function readSSEMessage(res, opts) {
|
|
|
513
519
|
for (;;) {
|
|
514
520
|
let chunk;
|
|
515
521
|
try {
|
|
516
|
-
chunk = await reader.read();
|
|
522
|
+
chunk = await readWithStall(() => reader.read());
|
|
517
523
|
}
|
|
518
524
|
catch (e) {
|
|
525
|
+
if (isStallError(e))
|
|
526
|
+
throw e;
|
|
519
527
|
throw new Error(`Truncated stream from model (connection aborted: ${e instanceof Error ? e.message : String(e)}).`);
|
|
520
528
|
}
|
|
521
529
|
if (chunk.done)
|
|
@@ -550,13 +558,28 @@ export async function readSSEMessage(res, opts) {
|
|
|
550
558
|
}
|
|
551
559
|
}
|
|
552
560
|
else if (typeof body[Symbol.asyncIterator] === "function") {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
561
|
+
const it = body[Symbol.asyncIterator]();
|
|
562
|
+
try {
|
|
563
|
+
for (;;) {
|
|
564
|
+
const step = await readWithStall(() => it.next());
|
|
565
|
+
if (step.done)
|
|
566
|
+
break;
|
|
567
|
+
const v = step.value;
|
|
568
|
+
const text = typeof v === "string" ? v : decoder.decode(v, { stream: true });
|
|
569
|
+
rawText += text;
|
|
570
|
+
buffer += text;
|
|
571
|
+
drainBuffer();
|
|
572
|
+
if (sawDone)
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
finally {
|
|
577
|
+
try {
|
|
578
|
+
await it.return?.();
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
// ignore — the stream is over either way
|
|
582
|
+
}
|
|
560
583
|
}
|
|
561
584
|
if (!sawDone && buffer.length > 0) {
|
|
562
585
|
processLine(buffer);
|
|
@@ -1167,6 +1190,7 @@ export function planToolBatches(calls) {
|
|
|
1167
1190
|
}
|
|
1168
1191
|
import { runLoopWithChat } from "./agent/loop.js";
|
|
1169
1192
|
export { runLoopWithChat } from "./agent/loop.js";
|
|
1193
|
+
export { DEFAULT_MAX_TOTAL_TOOL_CALLS, DEFAULT_TOOL_TIMEOUT_MS, executeWithTimeout, resolveMaxTotalToolCalls, resolveToolTimeoutMs, } from "./agent/loop.js";
|
|
1170
1194
|
export async function runAgenticLoopForProvider(provider, apiKey, model, history, opts) {
|
|
1171
1195
|
return runLoopWithChat((h, o) => chatCompletionForProvider(provider, apiKey, model, h, {
|
|
1172
1196
|
onToken: o?.onToken,
|
package/package.json
CHANGED