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.
@@ -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);
@@ -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
- export function StatusBar({ provider, model, usageTotals, contextLoad, reasoningDisplay, mode, trustAll, busy, activity, phaseLabel, elapsedSecs, stalled, approvalPending, cwd, branch, columns = 100, }) {
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). The activity text
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",
@@ -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
- export function TodoPanel({ items }) {
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
+ });
@@ -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
- return (_jsxs(React.Fragment, { children: [item.label ? _jsx(ToolLine, { content: item.label.content, ms: item.label.ms }) : null, _jsx(ErrorCard, { classified: classified })] }, i));
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
- for await (const v of body) {
554
- const text = typeof v === "string" ? v : decoder.decode(v, { stream: true });
555
- rawText += text;
556
- buffer += text;
557
- drainBuffer();
558
- if (sawDone)
559
- break;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atom-agent",
3
- "version": "1.0.0",
3
+ "version": "1.1.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",