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
package/dist/App.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Ink (React) TUI for the minimal Atom chatbot.
|
|
3
3
|
// Hand-rolled input + dropdowns via useInput (no extra deps).
|
|
4
|
+
import * as fs from "node:fs";
|
|
4
5
|
import * as os from "node:os";
|
|
5
|
-
import
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
6
8
|
import { Box, Text, useApp, useInput, usePaste, useStdout } from "ink";
|
|
7
9
|
import { DEFAULT_MODEL, EFFORT_OPTIONS, FALLBACK_MODELS, LoopCancelledError, REASONING_EFFORT_SUPPORTED_MODELS, buildSystemPrompt, fetchModelsForProviderWithStatus, fetchModelsWithStatus, historyChars, isEffortSupported, messageChars, openTodoNeedles, runAgenticLoopForProvider, } from "./zen.js";
|
|
8
10
|
import { createContextManager, trackHistory, } from "./context-manager.js";
|
|
9
11
|
import { assemblePrefix, providerCacheSupport, } from "./prompt-cache.js";
|
|
10
|
-
import { TOOL_DEFINITIONS, TOOL_ONE_LINERS, clearTodos, describeToolCall, executeTool, getTodos, needsApproval, providerSecrets } from "./tools.js";
|
|
12
|
+
import { TOOL_DEFINITIONS, TOOL_ONE_LINERS, APPROVAL_PREVIEW_MAX_BYTES, clearTodos, describeToolCall, executeTool, getTodos, needsApproval, previewDiffForApproval, providerSecrets } from "./tools.js";
|
|
11
13
|
import { classifyTurnOutcome, createTelemetryRecorder, loadTelemetrySessions, resolveTelemetryEnabled, summarizeTelemetry, telemetryDir, } from "./telemetry.js";
|
|
12
14
|
import { writeTelemetryDashboard } from "./telemetry-dashboard.js";
|
|
13
15
|
import { formatRules, parseRuleInput, } from "./permissions.js";
|
|
@@ -48,7 +50,7 @@ export const SLASH_COMMANDS = [
|
|
|
48
50
|
name: "/effort",
|
|
49
51
|
description: "Open the reasoning-effort picker (Default/Low/Medium/High/Max; top is Max, sent as max).",
|
|
50
52
|
},
|
|
51
|
-
{ name: "/tools", description: "List the
|
|
53
|
+
{ name: "/tools", description: "List the tools with one-line descriptions." },
|
|
52
54
|
{ name: "/skills", description: "List installed skills (project + global)." },
|
|
53
55
|
{ name: "/skill", description: "Invoke a skill by name (/skill:name; /skills lists)." },
|
|
54
56
|
{ name: "/mode", description: "Print the current permission mode (Tab cycles normal → yolo → plan)." },
|
|
@@ -62,6 +64,8 @@ export const SLASH_COMMANDS = [
|
|
|
62
64
|
{ name: "/context", description: "Show context usage by source (system, tools, history, skills)." },
|
|
63
65
|
{ name: "/queue", description: "List queued follow-ups (/queue clear wipes them)." },
|
|
64
66
|
{ name: "/steer", description: "Steer the running turn, or send when idle (/steer <text>)." },
|
|
67
|
+
{ name: "/autoscroll", description: "Follow new output as it arrives (/autoscroll on|off; off freezes the view mid-turn)." },
|
|
68
|
+
{ name: "/thinking", description: "Show or hide model thinking in the TUI (rendering only; the turn is untouched)." },
|
|
65
69
|
{ name: "/resume", description: "Restore the last saved session (turns, history, settings, usage)." },
|
|
66
70
|
{ name: "/telemetry", description: "Show the local observability summary (sessions, tokens, tools)." },
|
|
67
71
|
{ name: "/dashboard", description: "Write the local observability dashboard page and show its path." },
|
|
@@ -106,8 +110,18 @@ export const SKILL_USAGE = "usage: /skill:<name> — invoke a skill directly (li
|
|
|
106
110
|
export const RULE_USAGE = "usage: /allow <tool[:glob]> · /deny <tool[:glob]> · /rules · /rules clear (e.g. /allow bash:npm test*, /deny bash:rm *)";
|
|
107
111
|
export const QUEUE_USAGE = "usage: /queue (list) · /queue clear (wipe) · /steer <text> (steer the running turn, or send when idle)";
|
|
108
112
|
export const STEER_USAGE = "usage: /steer <text> — while busy, injects into the running turn at the next step boundary (the current action finishes first); when idle, sends as a normal turn";
|
|
113
|
+
export const AUTOSCROLL_USAGE = "usage: /autoscroll [on|off] — on (default) follows new output as it arrives; off freezes the view while a turn runs (a `↓ N new` indicator offers the jump back). Bare /autoscroll prints the current state.";
|
|
114
|
+
export const THINKING_USAGE = "usage: /thinking — toggles model-thinking visibility in the TUI (rendering only: shows or hides the committed thinking blocks; the turn, history, and telemetry are untouched).";
|
|
109
115
|
export function filterSlashCommands(prefix) {
|
|
110
116
|
const q = prefix.startsWith("/") ? prefix.slice(1) : prefix;
|
|
117
|
+
// Exact match wins outright: a fully-typed command collapses the menu
|
|
118
|
+
// to itself, so prefix-siblings (/skill vs /skills, /model vs /models)
|
|
119
|
+
// never read as duplicates and Enter stays deterministic. Partial
|
|
120
|
+
// input keeps the prefix-then-fuzzy tiers below untouched.
|
|
121
|
+
const full = `/${q}`;
|
|
122
|
+
const exact = SLASH_COMMANDS.find((c) => c.name === full);
|
|
123
|
+
if (exact)
|
|
124
|
+
return [exact];
|
|
111
125
|
const pre = [];
|
|
112
126
|
const fuzzy = [];
|
|
113
127
|
for (const c of SLASH_COMMANDS) {
|
|
@@ -163,10 +177,15 @@ export function paletteEntries(query) {
|
|
|
163
177
|
}));
|
|
164
178
|
}
|
|
165
179
|
// Busy-gate shared by the slash menu and the palette: /compact sets the
|
|
166
|
-
// pending flag for turn-end drain; /queue + /steer manage the running turn
|
|
180
|
+
// pending flag for turn-end drain; /queue + /steer manage the running turn;
|
|
181
|
+
// /autoscroll and /thinking only flip view flags (never touch the turn).
|
|
167
182
|
// Every other command waits idle.
|
|
168
183
|
export function slashRunsWhileBusy(name) {
|
|
169
|
-
return name === "/compact" ||
|
|
184
|
+
return (name === "/compact" ||
|
|
185
|
+
name === "/queue" ||
|
|
186
|
+
name === "/steer" ||
|
|
187
|
+
name === "/autoscroll" ||
|
|
188
|
+
name === "/thinking");
|
|
170
189
|
}
|
|
171
190
|
// Fuzzy subsequence match with gap/start/word-boundary scoring (lower is
|
|
172
191
|
// better; null = no match). Pure — shared by the command filter, the skill
|
|
@@ -816,6 +835,26 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
816
835
|
scrollEndRef.current = next;
|
|
817
836
|
setScrollEnd(next);
|
|
818
837
|
}
|
|
838
|
+
// Thinking visibility (the /thinking toggle, rendering-only, default
|
|
839
|
+
// hidden): committed thinking turns + the live thinking block show only
|
|
840
|
+
// while on. Never touches the turn, history, or telemetry — purely paint.
|
|
841
|
+
const [showThinking, setShowThinking] = useState(false);
|
|
842
|
+
const showThinkingRef = useRef(false);
|
|
843
|
+
function setShowThinkingBoth(next) {
|
|
844
|
+
showThinkingRef.current = next;
|
|
845
|
+
setShowThinking(next);
|
|
846
|
+
}
|
|
847
|
+
// /autoscroll (session-only, default on). On = today's behavior: a
|
|
848
|
+
// following view extends with every appended turn. Off = appends during a
|
|
849
|
+
// busy turn freeze a following view at its current end instead of yanking
|
|
850
|
+
// it (the `↓ N new` indicator offers the jump back; End resumes). Idle
|
|
851
|
+
// appends always follow — freezing only matters while output streams.
|
|
852
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
853
|
+
const autoScrollRef = useRef(true);
|
|
854
|
+
function setAutoScrollBoth(next) {
|
|
855
|
+
autoScrollRef.current = next;
|
|
856
|
+
setAutoScroll(next);
|
|
857
|
+
}
|
|
819
858
|
// Skill registry (cached metadata): one instance per App, scoped to the
|
|
820
859
|
// same dirs the suite injects via skillDirs. Every discovery path below
|
|
821
860
|
// reads through it — refresh() revalidates by stat (mtime+size) and only
|
|
@@ -840,6 +879,34 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
840
879
|
// menu keeps its previous snapshot (a hiccup must never break input)
|
|
841
880
|
}
|
|
842
881
|
}
|
|
882
|
+
// Pending transcript diff (display-only): the approve-time write/edit
|
|
883
|
+
// preview plus full-file BEFORE/AFTER capture, held for the matching
|
|
884
|
+
// onToolActivity commit. Single slot is exact — the scheduler never
|
|
885
|
+
// parallel-batches writes (writes conflict globally; parallel members
|
|
886
|
+
// never prompt), and every execution commits exactly one activity entry
|
|
887
|
+
// in call order. Lifetime ⊆ one turn: set in approve(),
|
|
888
|
+
// consumed-or-cleared by the matching activity, and cleared on
|
|
889
|
+
// deny/cancel/turn boundaries so a stale preview can never attach to
|
|
890
|
+
// a later call.
|
|
891
|
+
// - write: BEFORE reuses the preview's pre-read; AFTER is the new
|
|
892
|
+
// content arg (exactly what the tool writes) — zero extra reads.
|
|
893
|
+
// - edit: BEFORE is a best-effort full-file read here (pre-execution);
|
|
894
|
+
// AFTER is read at commit time. Two reads, each once, never in render.
|
|
895
|
+
const pendingDiffRef = useRef(null);
|
|
896
|
+
// Best-effort full-file read for diff capture: null on missing dir,
|
|
897
|
+
// oversize, or any I/O failure. Never throws — capture degrades to the
|
|
898
|
+
// arg-block preview pair instead of breaking approval.
|
|
899
|
+
function readFileForDiff(absPath) {
|
|
900
|
+
try {
|
|
901
|
+
const st = fs.statSync(absPath);
|
|
902
|
+
if (!st.isFile() || st.size > APPROVAL_PREVIEW_MAX_BYTES)
|
|
903
|
+
return null;
|
|
904
|
+
return fs.readFileSync(absPath, "utf8");
|
|
905
|
+
}
|
|
906
|
+
catch {
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
843
910
|
// Tool approval prompt (normal mode, write/edit/bash): the loop waits on
|
|
844
911
|
// the resolver until the user presses y/a/n. Ctrl+C aborts the whole turn
|
|
845
912
|
// (LoopCancelledError) instead of denying one call.
|
|
@@ -941,10 +1008,28 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
941
1008
|
// tool name before its execution line lands.
|
|
942
1009
|
const [draft, setDraft] = useState(null);
|
|
943
1010
|
// Thinking channel (onThinking): reasoning text streamed apart from the
|
|
944
|
-
// answer, rendered in its own dim block below.
|
|
945
|
-
// cleared on every turn boundary below —
|
|
946
|
-
// transcript
|
|
1011
|
+
// answer, rendered in its own dim block below. The live value is transient
|
|
1012
|
+
// like `draft` — cleared on every turn boundary below — but each completed
|
|
1013
|
+
// round commits to the transcript via commitThinking (stays in the TUI,
|
|
1014
|
+
// never the model history) instead of being replaced and lost.
|
|
947
1015
|
const [thinking, setThinking] = useState(null);
|
|
1016
|
+
const thinkingRef = useRef(null);
|
|
1017
|
+
// Move the accumulated round thinking into the transcript as a quiet
|
|
1018
|
+
// annotation turn (no-op when empty). Called when a new POST starts and at
|
|
1019
|
+
// turn end, so every round's reasoning stays visible; the /thinking toggle
|
|
1020
|
+
// only controls rendering, never this record.
|
|
1021
|
+
function commitThinking() {
|
|
1022
|
+
const text = thinkingRef.current;
|
|
1023
|
+
thinkingRef.current = null;
|
|
1024
|
+
setThinking(null);
|
|
1025
|
+
if (typeof text === "string" && text.length > 0) {
|
|
1026
|
+
appendTurns({ role: "assistant", content: text, thinking: true });
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function clearThinking() {
|
|
1030
|
+
thinkingRef.current = null;
|
|
1031
|
+
setThinking(null);
|
|
1032
|
+
}
|
|
948
1033
|
const [phase, setPhase] = useState("idle");
|
|
949
1034
|
const [phaseDetail, setPhaseDetail] = useState("");
|
|
950
1035
|
const [toolHint, setToolHint] = useState(null);
|
|
@@ -1552,6 +1637,13 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
1552
1637
|
setTurns(next);
|
|
1553
1638
|
}
|
|
1554
1639
|
function appendTurns(...items) {
|
|
1640
|
+
// Autoscroll off + busy + following: freeze the view at its current end
|
|
1641
|
+
// BEFORE appending, so streaming output accumulates below instead of
|
|
1642
|
+
// yanking the viewport (smooth-scroll hold). Idle appends and already-
|
|
1643
|
+
// held views pass through untouched.
|
|
1644
|
+
if (!autoScrollRef.current && busyRef.current && scrollEndRef.current === null) {
|
|
1645
|
+
setScrollEndBoth(turnsRef.current.length);
|
|
1646
|
+
}
|
|
1555
1647
|
const next = [...turnsRef.current, ...items];
|
|
1556
1648
|
turnsRef.current = next;
|
|
1557
1649
|
setTurns(next);
|
|
@@ -1729,7 +1821,10 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
1729
1821
|
// Snapshot the committed session (historyRef + turnsRef + settings refs)
|
|
1730
1822
|
// to ~/.atom/session.json. Disk errors are ignored (in-memory session
|
|
1731
1823
|
// still applies). Called only for committed state: completed turns and
|
|
1732
|
-
// clean exit — never for rolled-back (failed/cancelled) turns.
|
|
1824
|
+
// clean exit — never for rolled-back (failed/cancelled) turns. The
|
|
1825
|
+
// committed diff previews (Turn.diff) are display-only and never saved:
|
|
1826
|
+
// they can hold whole file contents (bloat) and would render stale
|
|
1827
|
+
// after later edits, so /resume restores label-only turns.
|
|
1733
1828
|
function persistSession() {
|
|
1734
1829
|
try {
|
|
1735
1830
|
saveSession({
|
|
@@ -1739,7 +1834,10 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
1739
1834
|
mode: modeRef.current,
|
|
1740
1835
|
usageTotals: usageRef.current,
|
|
1741
1836
|
history: historyRef.current,
|
|
1742
|
-
turns: turnsRef.current
|
|
1837
|
+
turns: turnsRef.current.map((t) => {
|
|
1838
|
+
const { diff: _dropped, ...rest } = t;
|
|
1839
|
+
return rest;
|
|
1840
|
+
}),
|
|
1743
1841
|
}, authHome);
|
|
1744
1842
|
}
|
|
1745
1843
|
catch {
|
|
@@ -2209,6 +2307,58 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2209
2307
|
}
|
|
2210
2308
|
pushInfo(QUEUE_USAGE);
|
|
2211
2309
|
}
|
|
2310
|
+
// /thinking: rendering-only visibility toggle for model thinking (both
|
|
2311
|
+
// the committed transcript blocks and the live thinking block). Pure
|
|
2312
|
+
// paint — safe while busy (never touches the turn, like /autoscroll).
|
|
2313
|
+
// Bare toggles; anything appended prints usage (there are no arguments).
|
|
2314
|
+
function runThinkingCommand(raw) {
|
|
2315
|
+
if (raw.trim() !== "/thinking") {
|
|
2316
|
+
pushInfo(THINKING_USAGE);
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
const next = !showThinkingRef.current;
|
|
2320
|
+
setShowThinkingBoth(next);
|
|
2321
|
+
pushInfo(next
|
|
2322
|
+
? "(thinking shown — model reasoning stays visible in the transcript)"
|
|
2323
|
+
: "(thinking hidden — reasoning still runs, it just isn't rendered)");
|
|
2324
|
+
}
|
|
2325
|
+
// /autoscroll [on|off]: follow switch for the scrollback viewport. View-
|
|
2326
|
+
// only state — safe while busy (never touches the turn, like /queue).
|
|
2327
|
+
// Bare prints the state; on jumps to the latest; off freezes a following
|
|
2328
|
+
// view at its current end (mid-turn appends then accumulate below).
|
|
2329
|
+
function runAutoScrollCommand(raw) {
|
|
2330
|
+
const arg = raw.trim() === "/autoscroll" ? "" : raw.trim().slice("/autoscroll".length).trim().toLowerCase();
|
|
2331
|
+
if (arg === "") {
|
|
2332
|
+
pushInfo(autoScrollRef.current
|
|
2333
|
+
? "(autoscroll on — following new output as it arrives)"
|
|
2334
|
+
: "(autoscroll off — the view freezes while a turn runs; End follows the latest)");
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
if (arg === "on") {
|
|
2338
|
+
if (autoScrollRef.current) {
|
|
2339
|
+
pushInfo("(autoscroll already on)");
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
setAutoScrollBoth(true);
|
|
2343
|
+
setScrollEndBoth(null);
|
|
2344
|
+
pushInfo("(autoscroll on — following the latest)");
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
if (arg === "off") {
|
|
2348
|
+
if (!autoScrollRef.current) {
|
|
2349
|
+
pushInfo("(autoscroll already off)");
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
// Confirm FIRST while still following (visible), then flip the switch:
|
|
2353
|
+
// flipping first would freeze this very confirm below the viewport
|
|
2354
|
+
// (appendTurns freezes busy appends once off). Already-held views stay
|
|
2355
|
+
// held; the next busy append freezes a following view via appendTurns.
|
|
2356
|
+
pushInfo("(autoscroll off — the view freezes while a turn runs; End follows the latest)");
|
|
2357
|
+
setAutoScrollBoth(false);
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
pushInfo(AUTOSCROLL_USAGE);
|
|
2361
|
+
}
|
|
2212
2362
|
// /models: local-discovery status + refresh. Bare `/models` reports the
|
|
2213
2363
|
// last snapshot (kicking a first probe when discovery never ran);
|
|
2214
2364
|
// `/models refresh` re-probes all three runtimes, then reports. Results
|
|
@@ -2275,7 +2425,7 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2275
2425
|
setClearGen((g) => g + 1);
|
|
2276
2426
|
setError(null);
|
|
2277
2427
|
setDraft(null);
|
|
2278
|
-
|
|
2428
|
+
clearThinking();
|
|
2279
2429
|
setToolHint(null);
|
|
2280
2430
|
setPhase("idle");
|
|
2281
2431
|
setPhaseDetail("");
|
|
@@ -2326,7 +2476,7 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2326
2476
|
setClearGen((g) => g + 1);
|
|
2327
2477
|
setError(null);
|
|
2328
2478
|
setDraft(null);
|
|
2329
|
-
|
|
2479
|
+
clearThinking();
|
|
2330
2480
|
setToolHint(null);
|
|
2331
2481
|
setPhase("idle");
|
|
2332
2482
|
setPhaseDetail("");
|
|
@@ -2412,6 +2562,12 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2412
2562
|
case "/steer":
|
|
2413
2563
|
runQueueCommand("/steer");
|
|
2414
2564
|
return;
|
|
2565
|
+
case "/thinking":
|
|
2566
|
+
runThinkingCommand("/thinking");
|
|
2567
|
+
return;
|
|
2568
|
+
case "/autoscroll":
|
|
2569
|
+
runAutoScrollCommand("/autoscroll");
|
|
2570
|
+
return;
|
|
2415
2571
|
case "/mode":
|
|
2416
2572
|
if (modeRef.current === "plan") {
|
|
2417
2573
|
pushInfo("mode: plan (read-only — write/edit/bash blocked with a replan note; Tab to approve + exit)");
|
|
@@ -2503,6 +2659,25 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2503
2659
|
async function approve(name, args) {
|
|
2504
2660
|
if (turnCancelRef.current?.signal.aborted)
|
|
2505
2661
|
throw new LoopCancelledError();
|
|
2662
|
+
// Stage the transcript-diff preview for write/edit (all outcomes):
|
|
2663
|
+
// the modal below reuses it, and onToolActivity consumes it when the
|
|
2664
|
+
// matching execution commits. Deny/cancel paths clear it (execution
|
|
2665
|
+
// never happens, so nothing must linger for a later call). Full-file
|
|
2666
|
+
// BEFORE is captured here (pre-execution); AFTER resolves at commit
|
|
2667
|
+
// (write content arg, or a post-execution disk read for edit).
|
|
2668
|
+
const stagedDiff = name === "write" || name === "edit" ? previewDiffForApproval(name, args) : null;
|
|
2669
|
+
if (name === "write" || name === "edit") {
|
|
2670
|
+
const toolPath = typeof args["path"] === "string" ? args["path"] : null;
|
|
2671
|
+
const beforeFull = name === "write"
|
|
2672
|
+
? (stagedDiff?.oldText ?? null) // preview already pre-read it: no second read
|
|
2673
|
+
: toolPath !== null
|
|
2674
|
+
? readFileForDiff(path.resolve(process.cwd(), toolPath))
|
|
2675
|
+
: null;
|
|
2676
|
+
const afterArg = name === "write" && typeof args["content"] === "string"
|
|
2677
|
+
? args["content"]
|
|
2678
|
+
: null;
|
|
2679
|
+
pendingDiffRef.current = { name, path: toolPath, beforeFull, afterArg, diff: stagedDiff };
|
|
2680
|
+
}
|
|
2506
2681
|
// Policy layer owns the decision order (deny → plan → allow → yolo →
|
|
2507
2682
|
// trust → always → skill grants → prompt); this function owns cancel
|
|
2508
2683
|
// handling and the interactive prompt plumbing around it.
|
|
@@ -2514,22 +2689,27 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2514
2689
|
skillGrants: skillGrantsRef.current,
|
|
2515
2690
|
approvalGated: needsApproval(name),
|
|
2516
2691
|
});
|
|
2517
|
-
if (outcome.kind === "deny")
|
|
2692
|
+
if (outcome.kind === "deny") {
|
|
2693
|
+
pendingDiffRef.current = null;
|
|
2518
2694
|
return "no";
|
|
2695
|
+
}
|
|
2519
2696
|
if (outcome.kind === "allow")
|
|
2520
2697
|
return "once";
|
|
2521
2698
|
const signal = turnCancelRef.current?.signal ?? null;
|
|
2522
|
-
if (signal?.aborted)
|
|
2699
|
+
if (signal?.aborted) {
|
|
2700
|
+
pendingDiffRef.current = null;
|
|
2523
2701
|
throw new LoopCancelledError();
|
|
2702
|
+
}
|
|
2524
2703
|
return new Promise((resolve, reject) => {
|
|
2525
2704
|
approvalResolveRef.current = { resolve, reject };
|
|
2526
2705
|
setApproveIndexBoth(0);
|
|
2527
|
-
setPendingApproval({ name, args });
|
|
2706
|
+
setPendingApproval({ name, args, diff: stagedDiff });
|
|
2528
2707
|
if (signal) {
|
|
2529
2708
|
const onAbort = () => {
|
|
2530
2709
|
const h = approvalResolveRef.current;
|
|
2531
2710
|
approvalResolveRef.current = null;
|
|
2532
2711
|
setPendingApproval(null);
|
|
2712
|
+
pendingDiffRef.current = null;
|
|
2533
2713
|
h?.reject(new LoopCancelledError());
|
|
2534
2714
|
};
|
|
2535
2715
|
if (signal.aborted)
|
|
@@ -2543,6 +2723,8 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2543
2723
|
if (decision === "always" && pendingApproval) {
|
|
2544
2724
|
alwaysAllowedRef.current.add(pendingApproval.name);
|
|
2545
2725
|
}
|
|
2726
|
+
if (decision === "no")
|
|
2727
|
+
pendingDiffRef.current = null;
|
|
2546
2728
|
const h = approvalResolveRef.current;
|
|
2547
2729
|
approvalResolveRef.current = null;
|
|
2548
2730
|
setPendingApproval(null);
|
|
@@ -2651,6 +2833,17 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2651
2833
|
runQueueCommand(text);
|
|
2652
2834
|
return;
|
|
2653
2835
|
}
|
|
2836
|
+
// /autoscroll and /thinking are view-only state (never touch the
|
|
2837
|
+
// turn), so they run while busy like /queue + /steer (see
|
|
2838
|
+
// slashRunsWhileBusy).
|
|
2839
|
+
if (text === "/autoscroll" || text.startsWith("/autoscroll ")) {
|
|
2840
|
+
runAutoScrollCommand(text);
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
if (text === "/thinking" || text.startsWith("/thinking ")) {
|
|
2844
|
+
runThinkingCommand(text);
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2654
2847
|
if (text.startsWith("/"))
|
|
2655
2848
|
return;
|
|
2656
2849
|
if (queueRef.current.length >= QUEUE_CAP) {
|
|
@@ -2667,6 +2860,17 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2667
2860
|
runQueueCommand(text);
|
|
2668
2861
|
return;
|
|
2669
2862
|
}
|
|
2863
|
+
// /autoscroll takes an optional subcommand (/autoscroll on|off), like the
|
|
2864
|
+
// /queue family — SLASH_NAMES only holds the exact command. /thinking
|
|
2865
|
+
// is bare-toggle-only; anything appended prints its usage.
|
|
2866
|
+
if (text === "/autoscroll" || text.startsWith("/autoscroll ")) {
|
|
2867
|
+
runAutoScrollCommand(text);
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
if (text === "/thinking" || text.startsWith("/thinking ")) {
|
|
2871
|
+
runThinkingCommand(text);
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
2670
2874
|
// Scoped rules (ticket 03): exact or free-text forms (/allow bash:x,
|
|
2671
2875
|
// /rules clear) route with args intact — SLASH_NAMES only holds exact
|
|
2672
2876
|
// commands, and the skill fallback below must not swallow these.
|
|
@@ -2731,7 +2935,7 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2731
2935
|
setBusy(true);
|
|
2732
2936
|
setError(null);
|
|
2733
2937
|
setDraft(null);
|
|
2734
|
-
|
|
2938
|
+
clearThinking();
|
|
2735
2939
|
// Fresh turn, fresh latch: the queue drain at the end auto-sends only
|
|
2736
2940
|
// when this turn was NOT cancelled (see the turn-end finally).
|
|
2737
2941
|
turnCancelledRef.current = false;
|
|
@@ -2753,6 +2957,9 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2753
2957
|
// Display-only tool clock: no tool is running at turn start, so any
|
|
2754
2958
|
// stale timestamp from a previous turn must not leak into this one.
|
|
2755
2959
|
toolStartRef.current = null;
|
|
2960
|
+
// Same for the transcript-diff slot: a previous turn's unconsumed
|
|
2961
|
+
// preview (cancelled mid-execution) must never attach to this turn.
|
|
2962
|
+
pendingDiffRef.current = null;
|
|
2756
2963
|
lastPartialRef.current = "";
|
|
2757
2964
|
refreshGitInfo();
|
|
2758
2965
|
// SUBMIT STAGE 2/4 — context-assembly (rollback scope: pre-rollbackTo,
|
|
@@ -2833,6 +3040,11 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2833
3040
|
// Local observability sink: the loop reports completed model/tool
|
|
2834
3041
|
// calls (iterations, durations, usage) into the open turn trace.
|
|
2835
3042
|
telemetry: telemetrySink,
|
|
3043
|
+
// Loop-harness rollup: per-turn LoopStats (cache hits, guard hits,
|
|
3044
|
+
// bottleneck, context growth) attach to the same open turn trace.
|
|
3045
|
+
// Fires once per turn — including failed/cancelled turns, whose
|
|
3046
|
+
// endTurn below still records the outcome alongside these stats.
|
|
3047
|
+
onLoopStats: (s) => telemetry.recordLoopStats(telemetryTurnId, s),
|
|
2836
3048
|
// Plan-mode read-only gate (ticket 04): mutations are refused here
|
|
2837
3049
|
// with a replan note; every other tool delegates to executeTool.
|
|
2838
3050
|
execute: guardedExecute,
|
|
@@ -2850,6 +3062,7 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2850
3062
|
noteTurnActivity();
|
|
2851
3063
|
},
|
|
2852
3064
|
onThinking: (partial) => {
|
|
3065
|
+
thinkingRef.current = partial;
|
|
2853
3066
|
setThinking(partial);
|
|
2854
3067
|
noteTurnActivity();
|
|
2855
3068
|
},
|
|
@@ -2858,8 +3071,10 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2858
3071
|
setPhaseDetail(detail ?? "");
|
|
2859
3072
|
noteTurnActivity();
|
|
2860
3073
|
if (p === "thinking") {
|
|
2861
|
-
// New POST:
|
|
2862
|
-
|
|
3074
|
+
// New POST: the previous round's thinking (if any) commits to
|
|
3075
|
+
// the transcript so it stays in the TUI instead of being
|
|
3076
|
+
// replaced and lost; the fresh round streams into the live block.
|
|
3077
|
+
commitThinking();
|
|
2863
3078
|
}
|
|
2864
3079
|
else if (p === "tool" && detail) {
|
|
2865
3080
|
setToolHint(detail);
|
|
@@ -2963,6 +3178,40 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2963
3178
|
else if (isTodo) {
|
|
2964
3179
|
items.push({ role: "tool", content: result });
|
|
2965
3180
|
}
|
|
3181
|
+
// Committed transcript diff: the approve-time capture for this
|
|
3182
|
+
// exact execution rides on the label turn. Consume-or-clear on
|
|
3183
|
+
// every matching activity (success or failure) so a stale
|
|
3184
|
+
// capture can never leak onto a later call; render only on
|
|
3185
|
+
// success with a real payload (failures keep the ↳ line only).
|
|
3186
|
+
// Full-file BEFORE→AFTER is preferred (aligned panes with
|
|
3187
|
+
// context); when either side is unavailable (unreadable file,
|
|
3188
|
+
// oversize), fall back to the arg-block preview pair.
|
|
3189
|
+
const slot = pendingDiffRef.current;
|
|
3190
|
+
if (slot !== null &&
|
|
3191
|
+
(label === `⚙ ${slot.name}` || label.startsWith(`⚙ ${slot.name} `))) {
|
|
3192
|
+
pendingDiffRef.current = null;
|
|
3193
|
+
if (!isError) {
|
|
3194
|
+
let afterFull = null;
|
|
3195
|
+
if (slot.name === "write") {
|
|
3196
|
+
afterFull = slot.afterArg;
|
|
3197
|
+
}
|
|
3198
|
+
else if (slot.path !== null) {
|
|
3199
|
+
afterFull = readFileForDiff(path.resolve(process.cwd(), slot.path));
|
|
3200
|
+
}
|
|
3201
|
+
const beforeFull = slot.beforeFull;
|
|
3202
|
+
if (beforeFull !== null && afterFull !== null) {
|
|
3203
|
+
items[0].diff = {
|
|
3204
|
+
oldText: beforeFull,
|
|
3205
|
+
newText: afterFull,
|
|
3206
|
+
lang: slot.diff?.lang ?? null,
|
|
3207
|
+
path: slot.path,
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
3210
|
+
else if (slot.diff !== null) {
|
|
3211
|
+
items[0].diff = slot.diff;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
2966
3215
|
appendTurns(...items);
|
|
2967
3216
|
noteTurnActivity();
|
|
2968
3217
|
},
|
|
@@ -2975,8 +3224,11 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
2975
3224
|
},
|
|
2976
3225
|
});
|
|
2977
3226
|
// Turn-end flush: any trailing throttled partial paints before the
|
|
2978
|
-
// commit replaces the draft (byte-exact via `reply` regardless).
|
|
3227
|
+
// commit replaces the draft (byte-exact via `reply` regardless). The
|
|
3228
|
+
// final round's thinking commits first (chronological: reasoning, then
|
|
3229
|
+
// the answer it produced).
|
|
2979
3230
|
flushDraft();
|
|
3231
|
+
commitThinking();
|
|
2980
3232
|
appendTurns({ role: "assistant", content: reply });
|
|
2981
3233
|
// The turn committed to history (final text, denial-as-result, or
|
|
2982
3234
|
// stop-notice) — persist the kill-safe save. Rolled-back turns (catch
|
|
@@ -3015,6 +3267,9 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
3015
3267
|
controller.signal.aborted;
|
|
3016
3268
|
historyRef.current.splice(rollbackTo); // don't keep the failed/cancelled turn
|
|
3017
3269
|
turnCancelledRef.current = cancelled;
|
|
3270
|
+
// The turn never happened: drop live thinking with it (a failed turn
|
|
3271
|
+
// commits nothing — same scope as the history rollback above).
|
|
3272
|
+
clearThinking();
|
|
3018
3273
|
// Local observability: failed/cancelled turns still record what was
|
|
3019
3274
|
// attempted (model/tool calls so far) with their outcome, then flush.
|
|
3020
3275
|
// Like the save above, the telemetry file only ever gains completed
|
|
@@ -3066,6 +3321,9 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
3066
3321
|
turnCancelRef.current = null;
|
|
3067
3322
|
approvalResolveRef.current = null;
|
|
3068
3323
|
setPendingApproval(null);
|
|
3324
|
+
// Safety net: the slot is normally consumed by onToolActivity or
|
|
3325
|
+
// cleared on deny/cancel — never let it cross a turn boundary.
|
|
3326
|
+
pendingDiffRef.current = null;
|
|
3069
3327
|
askResolveRef.current = null;
|
|
3070
3328
|
setPendingQuestion(null);
|
|
3071
3329
|
setAskCustomBoth("");
|
|
@@ -3083,7 +3341,7 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
3083
3341
|
// ignore
|
|
3084
3342
|
}
|
|
3085
3343
|
setDraft(null);
|
|
3086
|
-
|
|
3344
|
+
clearThinking();
|
|
3087
3345
|
setToolHint(null);
|
|
3088
3346
|
clearTurnTimer();
|
|
3089
3347
|
setStalled(false);
|
|
@@ -3599,7 +3857,7 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
3599
3857
|
}
|
|
3600
3858
|
else if (key.return || key.tab) {
|
|
3601
3859
|
const pick = matches[slashIndexRef.current % matches.length];
|
|
3602
|
-
// /compact, /queue, and /
|
|
3860
|
+
// /compact, /queue, /steer, and /autoscroll run while busy (see
|
|
3603
3861
|
// slashRunsWhileBusy); every other entry still waits idle.
|
|
3604
3862
|
if (pick && (slashRunsWhileBusy(pick.name) || !busyRef.current)) {
|
|
3605
3863
|
if (pick.skill) {
|
|
@@ -3623,6 +3881,14 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
3623
3881
|
setInputBoth("");
|
|
3624
3882
|
void runCompactCommand(focus);
|
|
3625
3883
|
}
|
|
3884
|
+
else if (pick.name === "/autoscroll" &&
|
|
3885
|
+
(inputRef.current === "/autoscroll" || inputRef.current.startsWith("/autoscroll "))) {
|
|
3886
|
+
// Preserve the on/off arg when the menu is open on a prefix
|
|
3887
|
+
// (bare highlighted name alone would drop it).
|
|
3888
|
+
const raw = inputRef.current;
|
|
3889
|
+
setInputBoth("");
|
|
3890
|
+
runAutoScrollCommand(raw);
|
|
3891
|
+
}
|
|
3626
3892
|
else if ((pick.name === "/allow" || pick.name === "/deny" || pick.name === "/rules") &&
|
|
3627
3893
|
inputRef.current.startsWith(pick.name)) {
|
|
3628
3894
|
// Preserve the typed rule args (e.g. "/allow bash:npm test*");
|
|
@@ -3957,6 +4223,14 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
3957
4223
|
const skillTitle = `Skills (${skillEntries.length}` +
|
|
3958
4224
|
(skillFilter ? ` of ${skillEntriesAll.length}, filter: "${skillFilter}"` : "") +
|
|
3959
4225
|
`) — type to filter, up/down + Enter, Esc cancels:`;
|
|
4226
|
+
// Memoized render derivations (flicker fix): these rebuild arrays on every
|
|
4227
|
+
// App render (token paints, keystrokes, 1s ticks), which defeats the memo
|
|
4228
|
+
// on the leaf panels below. Memoized, the leaves skip everything but real
|
|
4229
|
+
// changes. Checkpoint listing reads the snapshot dir — never per frame.
|
|
4230
|
+
const paletteEntriesMemo = useMemo(() => (paletteOpen ? paletteEntries(paletteFilter) : []), [paletteOpen, paletteFilter]);
|
|
4231
|
+
const checkpointListMemo = useMemo(() => (selectingRewind ? listCheckpoints() : []),
|
|
4232
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4233
|
+
[selectingRewind]);
|
|
3960
4234
|
// (The cursor clamp lives inside the memoized InputBox now, next to its
|
|
3961
4235
|
// only use — App body no longer reads cursor state for paint.)
|
|
3962
4236
|
// Status-line reasoning segment wired to the effort session state:
|
|
@@ -3977,7 +4251,7 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
3977
4251
|
const toolElapsedSecs = busy && toolHint && toolStartRef.current !== null
|
|
3978
4252
|
? elapsedSecsSince(toolStartRef.current, turnStartRef.current + elapsedSecs * 1000)
|
|
3979
4253
|
: null;
|
|
3980
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TranscriptView, { turns: turns, clearGen: clearGen, end: scrollEnd, held: scrollEnd !== null }), _jsx(LiveTail, { isEmpty: turns.length === 0, sessionHint: sessionHint, draft: draft, thinking: thinking, busy: busy, held: scrollEnd !== null, toolHint: toolHint, toolElapsedSecs: toolElapsedSecs, elapsedSecs: elapsedSecs }), error ? _jsxs(Text, { color: theme.color.error, children: ["error> ", error] }) : null, pendingApproval ? (_jsx(ApprovalBox, { toolName: pendingApproval.name, description: describeToolCall(pendingApproval.name, pendingApproval.args), selected: approveIndex })) : null, pendingQuestion ? (_jsx(QuestionBox, { question: pendingQuestion.question, options: pendingQuestion.options, allowCustom: pendingQuestion.allowCustom, askCustom: askCustom, askSelIndex: askSelIndex })) : null, _jsx(TodoPanel, { items: todoSnap }), steerPending ? _jsxs(Text, { dimColor: true, children: ["Steering: ", steerPending] }) : null, queue.length > 0 ? (_jsxs(Text, { dimColor: true, children: ["Queued (", queue.length, "): ", queue[0], queue.length > 1 ? ` +${queue.length - 1} more (/queue)` : ""] })) : null, paletteOpen ? (_jsx(PalettePanel, { entries:
|
|
4254
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TranscriptView, { turns: turns, clearGen: clearGen, end: scrollEnd, held: scrollEnd !== null, showThinking: showThinking }), _jsx(LiveTail, { isEmpty: turns.length === 0, sessionHint: sessionHint, draft: draft, thinking: thinking, busy: busy, held: scrollEnd !== null, toolHint: toolHint, toolElapsedSecs: toolElapsedSecs, elapsedSecs: elapsedSecs, showThinking: showThinking }), error ? _jsxs(Text, { color: theme.color.error, children: ["error> ", error] }) : null, pendingApproval ? (_jsx(ApprovalBox, { toolName: pendingApproval.name, description: describeToolCall(pendingApproval.name, pendingApproval.args), selected: approveIndex, diff: pendingApproval.diff ?? null })) : null, pendingQuestion ? (_jsx(QuestionBox, { question: pendingQuestion.question, options: pendingQuestion.options, allowCustom: pendingQuestion.allowCustom, askCustom: askCustom, askSelIndex: askSelIndex })) : null, _jsx(TodoPanel, { items: todoSnap }), steerPending ? _jsxs(Text, { dimColor: true, children: ["Steering: ", steerPending] }) : null, queue.length > 0 ? (_jsxs(Text, { dimColor: true, children: ["Queued (", queue.length, "): ", queue[0], queue.length > 1 ? ` +${queue.length - 1} more (/queue)` : ""] })) : null, paletteOpen ? (_jsx(PalettePanel, { entries: paletteEntriesMemo, index: paletteIndex, filter: paletteFilter })) : inspecting ? (_jsx(InspectorPanel, { records: toolLogRef.current, index: inspectIndex, expanded: inspectExpanded, scroll: inspectScroll })) : selecting ? (_jsxs(PickerShell, { title: modelTitle, children: [_jsx(PickerMoreAbove, { count: modelWin.start }), modelEntries.slice(modelWin.start, modelWin.end).map((e, k) => {
|
|
3981
4255
|
const i = modelWin.start + k;
|
|
3982
4256
|
const entryLocal = e.local === true;
|
|
3983
4257
|
const prevLocal = i === 0 ? null : modelEntries[i - 1]?.local === true;
|
|
@@ -4002,7 +4276,7 @@ export function App({ apiKey, endpoint, initialModel, initialModels, initialProv
|
|
|
4002
4276
|
? `${theme.symbol.descSeparator} key optional — free models need none`
|
|
4003
4277
|
: `${theme.symbol.descSeparator} no key`;
|
|
4004
4278
|
return (_jsxs(PickerRow, { highlighted: i === providerIndex, children: [p.name, " (", p.id, ") ", keyMark, p.id === provider ? " (current)" : ""] }, p.id));
|
|
4005
|
-
}) })) : keyPrompt ? (_jsxs(Box, { flexDirection: "column", borderStyle: theme.border.style, borderColor: theme.border.picker, paddingX: theme.spacing.pickerPadX, children: [_jsxs(Text, { bold: true, children: ["Atom \u2014 API key for ", keyPrompt.providerId, " (paste + Enter, Esc cancels):"] }), keyPrompt.consoleURL ? (_jsxs(Text, { dimColor: true, children: ["Get a key: ", keyPrompt.consoleURL] })) : null, keyPrompt.existingMasked ? (_jsxs(Text, { dimColor: true, children: ["key on file (", keyPrompt.existingMasked, ") \u2014 type a new key to replace, Esc keeps + switches"] })) : (_jsx(Text, { dimColor: true, children: "No key on file \u2014 paste once, validated then stored in ~/.atom/auth.json" })), keyPrompt.providerId === "kilo" ? (_jsx(Text, { dimColor: true, children: "Optional: free models work without a key \u2014 empty Enter continues anonymously" })) : null, _jsxs(Text, { children: ["key: ", theme.symbol.keyMask.repeat(keyPrompt.draft.length), _jsx(Text, { color: theme.color.mutedPaint, children: theme.symbol.cursorBlock })] }), keyPrompt.validating ? _jsxs(Text, { dimColor: true, children: ["validating", theme.symbol.ellipsis] }) : null, keyPrompt.error ? _jsx(Text, { color: theme.color.error, children: keyPrompt.error }) : null] })) : baseURLPrompt ? (_jsxs(Box, { flexDirection: "column", borderStyle: theme.border.style, borderColor: theme.border.picker, paddingX: theme.spacing.pickerPadX, children: [_jsx(Text, { bold: true, children: "Atom \u2014 baseURL for openai-compatible (http(s) URL + Enter, Esc cancels):" }), _jsxs(Text, { children: ["baseURL: ", baseURLPrompt.draft, _jsx(Text, { color: theme.color.mutedPaint, children: theme.symbol.cursorBlock })] }), baseURLPrompt.error ? _jsx(Text, { color: theme.color.error, children: baseURLPrompt.error }) : null] })) : selectingEffort ? (_jsxs(PickerShell, { title: "Atom \u2014 Select reasoning effort (up/down + Enter, Esc cancels):", children: [EFFORT_OPTIONS.map((o, i) => (_jsxs(PickerRow, { highlighted: i === effortIndex, children: [o === "default" ? "Default" : o === "max" ? "Max" : o[0]?.toUpperCase() + o.slice(1), o === effort ? " (current)" : ""] }, `${o}-${i}`))), _jsx(Text, { dimColor: true, children: "Top is Max (sent as max); xhigh is not a verified value." })] })) : selectingRewind ? (_jsxs(PickerShell, { title: "Atom \u2014 Rewind to checkpoint (up/down + Enter, Esc cancels):", children: [
|
|
4279
|
+
}) })) : keyPrompt ? (_jsxs(Box, { flexDirection: "column", borderStyle: theme.border.style, borderColor: theme.border.picker, paddingX: theme.spacing.pickerPadX, children: [_jsxs(Text, { bold: true, children: ["Atom \u2014 API key for ", keyPrompt.providerId, " (paste + Enter, Esc cancels):"] }), keyPrompt.consoleURL ? (_jsxs(Text, { dimColor: true, children: ["Get a key: ", keyPrompt.consoleURL] })) : null, keyPrompt.existingMasked ? (_jsxs(Text, { dimColor: true, children: ["key on file (", keyPrompt.existingMasked, ") \u2014 type a new key to replace, Esc keeps + switches"] })) : (_jsx(Text, { dimColor: true, children: "No key on file \u2014 paste once, validated then stored in ~/.atom/auth.json" })), keyPrompt.providerId === "kilo" ? (_jsx(Text, { dimColor: true, children: "Optional: free models work without a key \u2014 empty Enter continues anonymously" })) : null, _jsxs(Text, { children: ["key: ", theme.symbol.keyMask.repeat(keyPrompt.draft.length), _jsx(Text, { color: theme.color.mutedPaint, children: theme.symbol.cursorBlock })] }), keyPrompt.validating ? _jsxs(Text, { dimColor: true, children: ["validating", theme.symbol.ellipsis] }) : null, keyPrompt.error ? _jsx(Text, { color: theme.color.error, children: keyPrompt.error }) : null] })) : baseURLPrompt ? (_jsxs(Box, { flexDirection: "column", borderStyle: theme.border.style, borderColor: theme.border.picker, paddingX: theme.spacing.pickerPadX, children: [_jsx(Text, { bold: true, children: "Atom \u2014 baseURL for openai-compatible (http(s) URL + Enter, Esc cancels):" }), _jsxs(Text, { children: ["baseURL: ", baseURLPrompt.draft, _jsx(Text, { color: theme.color.mutedPaint, children: theme.symbol.cursorBlock })] }), baseURLPrompt.error ? _jsx(Text, { color: theme.color.error, children: baseURLPrompt.error }) : null] })) : selectingEffort ? (_jsxs(PickerShell, { title: "Atom \u2014 Select reasoning effort (up/down + Enter, Esc cancels):", children: [EFFORT_OPTIONS.map((o, i) => (_jsxs(PickerRow, { highlighted: i === effortIndex, children: [o === "default" ? "Default" : o === "max" ? "Max" : o[0]?.toUpperCase() + o.slice(1), o === effort ? " (current)" : ""] }, `${o}-${i}`))), _jsx(Text, { dimColor: true, children: "Top is Max (sent as max); xhigh is not a verified value." })] })) : selectingRewind ? (_jsxs(PickerShell, { title: "Atom \u2014 Rewind to checkpoint (up/down + Enter, Esc cancels):", children: [checkpointListMemo.map((c, i) => (_jsxs(PickerRow, { highlighted: i === rewindIndex, children: ["#", c.seq, " ", theme.symbol.separator, " ", c.label, " ", theme.symbol.separator, " ", c.files.length, " file(s)"] }, c.id))), _jsx(Text, { dimColor: true, children: "Restores exact bytes (hash-verified). Shell side effects (bash) are never snapshotted and cannot be undone." })] })) : selectingRewindScope ? (_jsxs(PickerShell, { title: "Atom \u2014 Rewind scope (up/down + Enter, Esc cancels):", children: [REWIND_SCOPES.map((s, i) => (_jsx(PickerRow, { highlighted: i === rewindScopeIndex, children: s }, s))), _jsx(Text, { dimColor: true, children: "Shell side effects (bash) are explicitly out of scope and cannot be undone." })] })) : (
|
|
4006
4280
|
// The input is the one boxed, prominent surface (see the memoized
|
|
4007
4281
|
// InputBox above): a quiet gray frame sets it apart from the
|
|
4008
4282
|
// transcript above and the status line below. Pickers and modals
|