atom-agent 0.3.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 +82 -0
- package/README.md +83 -32
- package/dist/App.js +2178 -318
- package/dist/adapters.js +146 -15
- package/dist/agent/gates.js +153 -0
- package/dist/agent/loop-guard.js +184 -0
- package/dist/agent/loop.js +908 -0
- package/dist/agent/normalize.js +144 -0
- package/dist/agent/types.js +1 -0
- package/dist/auth.js +2 -1
- package/dist/cli.js +68 -6
- package/dist/compact.js +6 -48
- package/dist/config.js +171 -0
- package/dist/context-manager.js +564 -0
- package/dist/kilo.js +343 -0
- package/dist/local-discovery.js +308 -0
- package/dist/policy.js +286 -0
- package/dist/prompt-cache.js +99 -0
- package/dist/providers.js +183 -2
- package/dist/rollback.js +21 -0
- package/dist/scheduler.js +247 -0
- package/dist/session.js +35 -3
- package/dist/skills.js +214 -43
- package/dist/snapshots.js +57 -2
- package/dist/system.js +8 -1
- package/dist/telemetry-dashboard.js +589 -0
- package/dist/telemetry-server.js +301 -0
- package/dist/telemetry.js +1056 -0
- package/dist/tools/dir-cache.js +207 -0
- package/dist/tools/filesystem.js +149 -0
- package/dist/tools/fingerprints.js +33 -0
- package/dist/tools/overflow.js +76 -0
- package/dist/tools/read-cache.js +160 -0
- package/dist/tools/registry.js +802 -0
- package/dist/tools/search.js +242 -0
- package/dist/tools/shared.js +31 -0
- package/dist/tools/shell.js +273 -0
- package/dist/tools/todo.js +191 -0
- package/dist/tools/web.js +454 -0
- package/dist/tools.js +17 -1863
- package/dist/ui/activity.js +51 -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/errors.js +129 -0
- package/dist/ui/highlight.js +120 -0
- package/dist/ui/input-model.js +115 -0
- package/dist/ui/input.js +40 -0
- package/dist/ui/live-tail.js +15 -0
- package/dist/ui/markdown.js +525 -0
- package/dist/ui/modals.js +47 -0
- package/dist/ui/palette.js +70 -0
- package/dist/ui/pickers.js +32 -0
- package/dist/ui/side-by-side.js +144 -0
- package/dist/ui/status-bar.js +75 -0
- package/dist/ui/theme.js +128 -0
- package/dist/ui/todo-panel.js +30 -0
- package/dist/ui/tool-inspector.js +59 -0
- package/dist/ui/transcript.js +128 -0
- package/dist/zen.js +145 -666
- package/package.json +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { theme } from "./theme.js";
|
|
4
|
+
// The audit label grammar (`⚙ name target`) carries the tool identity the
|
|
5
|
+
// error detail alone lacks. Paired in TranscriptView (adjacent turns).
|
|
6
|
+
export function parseToolLabel(label) {
|
|
7
|
+
const mark = `${theme.symbol.toolMark} `;
|
|
8
|
+
const stripped = (label.startsWith(mark) ? label.slice(mark.length) : label).trim();
|
|
9
|
+
const space = stripped.indexOf(" ");
|
|
10
|
+
if (space === -1)
|
|
11
|
+
return { name: stripped, target: "" };
|
|
12
|
+
return { name: stripped.slice(0, space), target: stripped.slice(space + 1).trim() };
|
|
13
|
+
}
|
|
14
|
+
export function titleCase(name) {
|
|
15
|
+
return name.length > 0 ? name[0].toUpperCase() + name.slice(1) : name;
|
|
16
|
+
}
|
|
17
|
+
const NETWORK_RE = /HTTP (429|500|502|503|504)|connection reset|fetch failed|network|timed? ?out|ENOTFOUND|ECONN|EAI_AGAIN|socket hang up/i;
|
|
18
|
+
const MODEL_RE = /Empty reply|Truncated stream|malformed|unexpected payload|no .* usage|invalid response/i;
|
|
19
|
+
const CONFIG_RE = /Missing API key|no API key|not configured|invalid baseURL|auth/i;
|
|
20
|
+
// Classify a tool-role turn. Returns null when the turn is not an error
|
|
21
|
+
// presentation (normal audit lines, warnings, retries, boundaries, todo
|
|
22
|
+
// output, and the pinned `(cancelled)` line, which keeps its legacy look).
|
|
23
|
+
export function classifyToolError(turn, label) {
|
|
24
|
+
const content = turn.content;
|
|
25
|
+
if (content.startsWith("(cancelled)"))
|
|
26
|
+
return null;
|
|
27
|
+
if (!turn.error) {
|
|
28
|
+
if (/^Missing API key/i.test(content.trim())) {
|
|
29
|
+
return {
|
|
30
|
+
kind: "config",
|
|
31
|
+
title: "Setup needed",
|
|
32
|
+
detail: content.trim(),
|
|
33
|
+
hint: "Run /provider to paste a key, then resend.",
|
|
34
|
+
inspectable: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const detail = content.replace(/^[↳\s]+/, "");
|
|
40
|
+
if (/denied by user/i.test(detail)) {
|
|
41
|
+
const tool = detail.split(":").pop()?.trim() ?? "";
|
|
42
|
+
return {
|
|
43
|
+
kind: "denial",
|
|
44
|
+
title: tool ? `Denied — ${titleCase(tool)}` : "Denied",
|
|
45
|
+
detail,
|
|
46
|
+
hint: "Approve next time, or pre-approve with /allow.",
|
|
47
|
+
inspectable: false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (NETWORK_RE.test(detail)) {
|
|
51
|
+
return {
|
|
52
|
+
kind: "network",
|
|
53
|
+
title: "Network failed",
|
|
54
|
+
detail,
|
|
55
|
+
hint: "Auto-retried — check the connection, then resend.",
|
|
56
|
+
inspectable: false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (MODEL_RE.test(detail)) {
|
|
60
|
+
return {
|
|
61
|
+
kind: "model",
|
|
62
|
+
title: "Model failed",
|
|
63
|
+
detail,
|
|
64
|
+
hint: "Resend to retry the request.",
|
|
65
|
+
inspectable: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (CONFIG_RE.test(detail)) {
|
|
69
|
+
return {
|
|
70
|
+
kind: "config",
|
|
71
|
+
title: "Setup needed",
|
|
72
|
+
detail,
|
|
73
|
+
hint: "Run /provider to paste a key, then resend.",
|
|
74
|
+
inspectable: false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (/panic|invariant|unexpected|internal/i.test(detail)) {
|
|
78
|
+
return {
|
|
79
|
+
kind: "internal",
|
|
80
|
+
title: "Internal error",
|
|
81
|
+
detail,
|
|
82
|
+
hint: "Please report this with the transcript.",
|
|
83
|
+
inspectable: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Default: a failed tool call. The audit line above already names the
|
|
87
|
+
// tool + target (paired by TranscriptView), so the card title stays
|
|
88
|
+
// short; the full result waits in the inspector store.
|
|
89
|
+
if (label) {
|
|
90
|
+
const { name } = parseToolLabel(label.content);
|
|
91
|
+
return {
|
|
92
|
+
kind: "tool",
|
|
93
|
+
title: `${titleCase(name) || "Tool"} failed`,
|
|
94
|
+
detail,
|
|
95
|
+
hint: "Ctrl+O opens the full output in the inspector.",
|
|
96
|
+
inspectable: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
kind: "tool",
|
|
101
|
+
title: "Tool failed",
|
|
102
|
+
detail,
|
|
103
|
+
hint: "Ctrl+O opens the full output in the inspector.",
|
|
104
|
+
inspectable: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const KIND_GLYPH = {
|
|
108
|
+
tool: "✕",
|
|
109
|
+
denial: "⊘",
|
|
110
|
+
network: "⚠",
|
|
111
|
+
model: "✕",
|
|
112
|
+
cancelled: "",
|
|
113
|
+
config: "→",
|
|
114
|
+
internal: "‼",
|
|
115
|
+
};
|
|
116
|
+
const KIND_COLOR = {
|
|
117
|
+
tool: theme.color.toolError,
|
|
118
|
+
denial: theme.color.warning,
|
|
119
|
+
network: theme.color.warning,
|
|
120
|
+
model: theme.color.toolError,
|
|
121
|
+
cancelled: undefined,
|
|
122
|
+
config: theme.color.user,
|
|
123
|
+
internal: theme.color.toolError,
|
|
124
|
+
};
|
|
125
|
+
// Compact card: bold titled first line, one detail line, one dim hint line.
|
|
126
|
+
// No boxes, no walls — three lines max per error.
|
|
127
|
+
export function ErrorCard({ classified }) {
|
|
128
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: KIND_COLOR[classified.kind], children: [KIND_GLYPH[classified.kind], " ", classified.title] }), _jsxs(Text, { children: [" ", classified.detail] }), classified.hint ? _jsxs(Text, { dimColor: true, children: [" ", classified.hint] }) : null] }));
|
|
129
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const C_KEYWORDS = new Set("break case catch class const continue debugger default delete do else enum export extends finally for function if implements import interface let new return static super switch this throw try typeof var void while with yield async await".split(" "));
|
|
2
|
+
const PY_KEYWORDS = new Set("def class return if elif else for while in is not and or import from as try except finally with lambda pass break continue raise True False None async await".split(" "));
|
|
3
|
+
const SH_KEYWORDS = new Set("if then else elif fi for while do done case esac function return exit echo local export readonly".split(" "));
|
|
4
|
+
function keywordsFor(lang) {
|
|
5
|
+
if (lang === "c")
|
|
6
|
+
return C_KEYWORDS;
|
|
7
|
+
if (lang === "py")
|
|
8
|
+
return PY_KEYWORDS;
|
|
9
|
+
if (lang === "sh")
|
|
10
|
+
return SH_KEYWORDS;
|
|
11
|
+
return null; // "data": no keywords
|
|
12
|
+
}
|
|
13
|
+
function commentStyle(lang) {
|
|
14
|
+
if (lang === "c")
|
|
15
|
+
return "slash";
|
|
16
|
+
if (lang === "data")
|
|
17
|
+
return "hash";
|
|
18
|
+
return "hash"; // py + sh
|
|
19
|
+
}
|
|
20
|
+
// Master token pattern over the code part of a line: strings (with
|
|
21
|
+
// escapes, incl. unterminated tails so streaming/odd lines still paint),
|
|
22
|
+
// numbers, words, and single fallback chars.
|
|
23
|
+
const TOKEN_RE = /'(?:[^'\\\n]|\\.)*(?:'|$)|"(?:[^"\\\n]|\\.)*(?:"|$)|`(?:[^`\\]|\\.)*(?:`|$)|[0-9][0-9_]*(?:\.[0-9_]+)?\b|[A-Za-z_$][A-Za-z0-9_$]*|\s+|./g;
|
|
24
|
+
const NUMBER_RE = /^[0-9][0-9_]*(?:\.[0-9_]+)?\b$/;
|
|
25
|
+
const WORD_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
26
|
+
// Split off a trailing line comment, honoring string spans: `//` inside
|
|
27
|
+
// a string is code, and (for hash style) `#` inside a string is code.
|
|
28
|
+
// Single-line `/* … */` pairs are treated as comments when both halves
|
|
29
|
+
// sit on this line; an unterminated opener is left as code (multi-line
|
|
30
|
+
// state is the documented non-goal).
|
|
31
|
+
function splitComment(line, lang) {
|
|
32
|
+
const style = commentStyle(lang);
|
|
33
|
+
let inStr = null;
|
|
34
|
+
let escaped = false;
|
|
35
|
+
for (let i = 0; i < line.length; i++) {
|
|
36
|
+
const c = line[i];
|
|
37
|
+
if (inStr !== null) {
|
|
38
|
+
if (escaped)
|
|
39
|
+
escaped = false;
|
|
40
|
+
else if (c === "\\")
|
|
41
|
+
escaped = true;
|
|
42
|
+
else if (c === inStr)
|
|
43
|
+
inStr = null;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
47
|
+
inStr = c;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (style === "slash" && c === "/" && line[i + 1] === "/") {
|
|
51
|
+
return { code: line.slice(0, i), comment: line.slice(i) };
|
|
52
|
+
}
|
|
53
|
+
if (style === "hash" && c === "#") {
|
|
54
|
+
// Shebang or comment to end of line.
|
|
55
|
+
return { code: line.slice(0, i), comment: line.slice(i) };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (style === "slash") {
|
|
59
|
+
const open = line.indexOf("/*");
|
|
60
|
+
const close = open >= 0 ? line.indexOf("*/", open + 2) : -1;
|
|
61
|
+
if (open >= 0 && close > open) {
|
|
62
|
+
// Keep it simple: trailing block comment paints as comment; an
|
|
63
|
+
// embedded one splits code around it via the token pass below
|
|
64
|
+
// (rare — paint the whole tail as comment only when the opener
|
|
65
|
+
// starts after code we already keep plain).
|
|
66
|
+
return { code: line.slice(0, open), comment: line.slice(open) };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { code: line, comment: "" };
|
|
70
|
+
}
|
|
71
|
+
function highlightCode(code, lang, base) {
|
|
72
|
+
const runs = [];
|
|
73
|
+
const keywords = keywordsFor(lang);
|
|
74
|
+
TOKEN_RE.lastIndex = 0;
|
|
75
|
+
let m;
|
|
76
|
+
while ((m = TOKEN_RE.exec(code)) !== null) {
|
|
77
|
+
const text = m[0];
|
|
78
|
+
const start = base + (m.index ?? 0);
|
|
79
|
+
let kind = "plain";
|
|
80
|
+
const first = text[0];
|
|
81
|
+
if (first === "'" || first === '"' || first === "`")
|
|
82
|
+
kind = "string";
|
|
83
|
+
else if (NUMBER_RE.test(text))
|
|
84
|
+
kind = "number";
|
|
85
|
+
else if (keywords !== null && WORD_RE.test(text) && keywords.has(text))
|
|
86
|
+
kind = "keyword";
|
|
87
|
+
runs.push({ text, kind, start, end: start + text.length });
|
|
88
|
+
}
|
|
89
|
+
return runs;
|
|
90
|
+
}
|
|
91
|
+
// Bounded highlight cache: diff hunks re-render on busy ticks and the
|
|
92
|
+
// same lines repeat across hunks/turns — tokenize once per unique line.
|
|
93
|
+
const HIGHLIGHT_CACHE_CAP = 2000;
|
|
94
|
+
const highlightCache = new Map();
|
|
95
|
+
export function highlightLine(line, lang) {
|
|
96
|
+
if (lang !== "c" && lang !== "py" && lang !== "sh" && lang !== "data") {
|
|
97
|
+
return line === "" ? [] : [{ text: line, kind: "plain", start: 0, end: line.length }];
|
|
98
|
+
}
|
|
99
|
+
const key = `${lang} ${line}`;
|
|
100
|
+
const hit = highlightCache.get(key);
|
|
101
|
+
if (hit)
|
|
102
|
+
return hit;
|
|
103
|
+
const { code, comment } = splitComment(line, lang);
|
|
104
|
+
const runs = highlightCode(code, lang, 0);
|
|
105
|
+
if (comment) {
|
|
106
|
+
runs.push({ text: comment, kind: "comment", start: code.length, end: line.length });
|
|
107
|
+
}
|
|
108
|
+
const out = runs.length > 0 ? runs : [];
|
|
109
|
+
highlightCache.set(key, out);
|
|
110
|
+
if (highlightCache.size > HIGHLIGHT_CACHE_CAP) {
|
|
111
|
+
const oldest = highlightCache.keys().next();
|
|
112
|
+
if (!oldest.done)
|
|
113
|
+
highlightCache.delete(oldest.value);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
// Test seam: current cache size (eviction behavior).
|
|
118
|
+
export function highlightCacheSize() {
|
|
119
|
+
return highlightCache.size;
|
|
120
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Pure input-editing model: multiline cursor math, line/word kills, paste
|
|
2
|
+
// normalization, and history-browse index ops. No React, no Ink — the App
|
|
3
|
+
// wires these to state, and the unit tests pin them here.
|
|
4
|
+
export const INPUT_HISTORY_CAP = 100;
|
|
5
|
+
export function splitInputLines(text) {
|
|
6
|
+
return text.split("\n");
|
|
7
|
+
}
|
|
8
|
+
export function lineColOf(text, offset) {
|
|
9
|
+
const safe = Math.max(0, Math.min(offset, text.length));
|
|
10
|
+
const lines = splitInputLines(text);
|
|
11
|
+
let acc = 0;
|
|
12
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13
|
+
const len = lines[i].length;
|
|
14
|
+
if (safe <= acc + len || i === lines.length - 1) {
|
|
15
|
+
return { line: i, col: safe - acc };
|
|
16
|
+
}
|
|
17
|
+
acc += len + 1;
|
|
18
|
+
}
|
|
19
|
+
return { line: 0, col: 0 };
|
|
20
|
+
}
|
|
21
|
+
export function offsetOfLines(lines, line, col) {
|
|
22
|
+
const l = Math.max(0, Math.min(line, lines.length - 1));
|
|
23
|
+
const c = Math.max(0, Math.min(col, lines[l].length));
|
|
24
|
+
let acc = 0;
|
|
25
|
+
for (let i = 0; i < l; i++)
|
|
26
|
+
acc += lines[i].length + 1;
|
|
27
|
+
return acc + c;
|
|
28
|
+
}
|
|
29
|
+
// Vertical cursor motion. `edge` is true when already on the first/last
|
|
30
|
+
// line — the caller falls through to history browse instead of moving.
|
|
31
|
+
export function moveVertically(text, offset, dir) {
|
|
32
|
+
const lines = splitInputLines(text);
|
|
33
|
+
const { line, col } = lineColOf(text, offset);
|
|
34
|
+
const next = line + dir;
|
|
35
|
+
if (next < 0 || next >= lines.length)
|
|
36
|
+
return { offset, edge: true };
|
|
37
|
+
return { offset: offsetOfLines(lines, next, col), edge: false };
|
|
38
|
+
}
|
|
39
|
+
// Ctrl+K: delete to end of the current line; at line end (with more lines)
|
|
40
|
+
// delete the newline itself (join). Ctrl+U: delete to line start.
|
|
41
|
+
export function killToLineEnd(text, offset) {
|
|
42
|
+
const safe = Math.max(0, Math.min(offset, text.length));
|
|
43
|
+
const nl = text.indexOf("\n", safe);
|
|
44
|
+
if (nl === -1) {
|
|
45
|
+
if (safe >= text.length)
|
|
46
|
+
return { text, offset: safe };
|
|
47
|
+
return { text: text.slice(0, safe), offset: safe };
|
|
48
|
+
}
|
|
49
|
+
if (nl === safe)
|
|
50
|
+
return { text: text.slice(0, safe) + text.slice(safe + 1), offset: safe };
|
|
51
|
+
return { text: text.slice(0, safe) + text.slice(nl), offset: safe };
|
|
52
|
+
}
|
|
53
|
+
export function killToLineStart(text, offset) {
|
|
54
|
+
const safe = Math.max(0, Math.min(offset, text.length));
|
|
55
|
+
const { line, col } = lineColOf(text, safe);
|
|
56
|
+
if (col === 0)
|
|
57
|
+
return { text, offset: safe };
|
|
58
|
+
const lines = splitInputLines(text);
|
|
59
|
+
const start = offsetOfLines(lines, line, 0);
|
|
60
|
+
return { text: text.slice(0, start) + text.slice(safe), offset: start };
|
|
61
|
+
}
|
|
62
|
+
// Ctrl+W: delete the word before the cursor (word chars + the gap before
|
|
63
|
+
// it). Words are [A-Za-z0-9_]+ runs; anything else is a single-char step
|
|
64
|
+
// when no word precedes (punctuation never eats a whole run).
|
|
65
|
+
export function killWordBefore(text, offset) {
|
|
66
|
+
let at = Math.max(0, Math.min(offset, text.length));
|
|
67
|
+
if (at <= 0)
|
|
68
|
+
return { text, offset: 0 };
|
|
69
|
+
let start = at;
|
|
70
|
+
while (start > 0 && /\s/.test(text[start - 1]))
|
|
71
|
+
start -= 1;
|
|
72
|
+
// A skipped gap is the whole kill unit ("foo " → "foo", not "").
|
|
73
|
+
if (start < at)
|
|
74
|
+
return { text: text.slice(0, start) + text.slice(at), offset: start };
|
|
75
|
+
if (/[\w]/.test(text[start - 1])) {
|
|
76
|
+
while (start > 0 && /[\w]/.test(text[start - 1]))
|
|
77
|
+
start -= 1;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
start -= 1;
|
|
81
|
+
}
|
|
82
|
+
return { text: text.slice(0, start) + text.slice(at), offset: start };
|
|
83
|
+
}
|
|
84
|
+
// Bracketed-paste content arrives verbatim (including newlines) — only line
|
|
85
|
+
// endings normalize. Pasted text never submits, even when multiline.
|
|
86
|
+
export function normalizePaste(text) {
|
|
87
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
88
|
+
}
|
|
89
|
+
// History is in-memory only (never persisted: prompts may carry pasted
|
|
90
|
+
// secrets, and the session file warns as much). Blank + consecutive-duplicate
|
|
91
|
+
// submits are skipped; oldest drops past the cap.
|
|
92
|
+
export function pushInputHistory(hist, text) {
|
|
93
|
+
if (!text.trim())
|
|
94
|
+
return hist;
|
|
95
|
+
if (hist.length > 0 && hist[hist.length - 1] === text)
|
|
96
|
+
return hist;
|
|
97
|
+
const next = [...hist, text];
|
|
98
|
+
if (next.length > INPUT_HISTORY_CAP)
|
|
99
|
+
next.splice(0, next.length - INPUT_HISTORY_CAP);
|
|
100
|
+
return next;
|
|
101
|
+
}
|
|
102
|
+
export function historyOlderIndex(hist, index) {
|
|
103
|
+
if (hist.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
if (index === null)
|
|
106
|
+
return hist.length - 1;
|
|
107
|
+
return Math.max(0, index - 1);
|
|
108
|
+
}
|
|
109
|
+
// Past the newest entry → null (the caller restores the stashed draft).
|
|
110
|
+
export function historyNewerIndex(hist, index) {
|
|
111
|
+
if (index === null)
|
|
112
|
+
return null;
|
|
113
|
+
const next = index + 1;
|
|
114
|
+
return next >= hist.length ? null : next;
|
|
115
|
+
}
|
package/dist/ui/input.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Input leaf: the boxed prompt surface. Prop-driven + memoized on
|
|
3
|
+
// (input, cursor) so timer ticks never repaint it. Multiline aware: the
|
|
4
|
+
// cursor rides line/col and long lines wrap via Ink. Deliberately bare —
|
|
5
|
+
// no placeholder text, no hints; the box + cursor is the affordance.
|
|
6
|
+
// Paint from ui/theme tokens — no literal colors or glyphs here.
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { Box, Text } from "ink";
|
|
9
|
+
import { theme } from "./theme.js";
|
|
10
|
+
import { lineColOf, splitInputLines } from "./input-model.js";
|
|
11
|
+
// Render-count probe for the input-smoothness test: incremented on every
|
|
12
|
+
// InputBox render (one keystroke must paint the input exactly once; the 1s
|
|
13
|
+
// busy-tick must leave it unchanged while idle input sits still).
|
|
14
|
+
export const inputRenderProbe = { count: 0 };
|
|
15
|
+
// The input is the one boxed, prominent surface: a quiet gray frame sets it
|
|
16
|
+
// apart from the transcript above and the status line below. Memoized on
|
|
17
|
+
// (input, cursor) so elapsed-timer ticks, token paints, and unrelated App
|
|
18
|
+
// state churn never repaint it — keystrokes stay at exactly one paint each,
|
|
19
|
+
// which is what makes navigation feel instant instead of choppy.
|
|
20
|
+
export const InputBox = React.memo(function InputBox({ input, cursor }) {
|
|
21
|
+
inputRenderProbe.count += 1;
|
|
22
|
+
// Defensive clamp: the ref is the source of truth mid-tick and always
|
|
23
|
+
// stays in range, but state may lag it by one render.
|
|
24
|
+
const safeCursor = Math.max(0, Math.min(cursor, input.length));
|
|
25
|
+
const lines = splitInputLines(input);
|
|
26
|
+
const { line: cline, col: ccol } = lineColOf(input, safeCursor);
|
|
27
|
+
return (_jsxs(Box, { borderStyle: theme.border.style, borderColor: theme.border.input, paddingX: theme.spacing.pickerPadX, children: [_jsxs(Text, { color: theme.color.inputPrompt, bold: true, children: [theme.symbol.inputPrompt, " "] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: lines.map((ln, i) => {
|
|
28
|
+
if (i !== cline)
|
|
29
|
+
return _jsx(Text, { children: ln.length > 0 ? ln : " " }, i);
|
|
30
|
+
// See-through cursor: the character under the cursor renders in
|
|
31
|
+
// inverse video instead of inserting a block glyph beside it, so
|
|
32
|
+
// letters never shift aside as the cursor moves (the old block
|
|
33
|
+
// made the line wobble on every arrow-key step). At end of line
|
|
34
|
+
// (or on an empty line) an inverse space holds the cell.
|
|
35
|
+
const before = ln.slice(0, ccol);
|
|
36
|
+
const at = ln.slice(ccol, ccol + 1);
|
|
37
|
+
const after = ln.slice(ccol + 1);
|
|
38
|
+
return (_jsxs(Text, { children: [before, _jsx(Text, { inverse: true, children: at.length > 0 ? at : " " }), after] }, i));
|
|
39
|
+
}) })] }));
|
|
40
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { activityText } from "./activity.js";
|
|
4
|
+
import { MarkdownStream } from "./markdown.js";
|
|
5
|
+
import { theme } from "./theme.js";
|
|
6
|
+
export function LiveTail({ isEmpty, sessionHint, draft, thinking, busy, held, toolHint, toolElapsedSecs, elapsedSecs, showThinking = true }) {
|
|
7
|
+
// Held view (user scrolled up mid-turn): the growing draft/thinking blocks
|
|
8
|
+
// are replaced by one static line so the frame stops gaining terminal
|
|
9
|
+
// lines — the terminal stops yanking and scrollback stays readable. The
|
|
10
|
+
// turn keeps running underneath; End resumes the live view. One-line
|
|
11
|
+
// status (tool hint, thinking tick) keeps updating in place: same line,
|
|
12
|
+
// no growth, no yank.
|
|
13
|
+
const freezeLive = held === true && busy;
|
|
14
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: theme.spacing.liveTailMarginY, children: [isEmpty ? (_jsx(Text, { dimColor: true, children: "Say hi to Atom \u2014 or type / for commands, /provider to pick a provider + key, /model to switch models." })) : null, sessionHint && isEmpty ? (_jsx(Text, { dimColor: true, children: "(last session available \u2014 /resume to restore)" })) : null, freezeLive ? (_jsxs(Text, { dimColor: true, children: [theme.symbol.moreAbove, " held \u2014 turn running \u00B7 End to follow"] })) : null, !freezeLive && draft ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: _jsxs(Text, { color: theme.color.assistant, bold: true, children: [theme.symbol.speakerAssistant, " "] }) }), _jsx(MarkdownStream, { text: draft })] })) : null, !freezeLive && thinking && showThinking ? (_jsxs(Text, { dimColor: true, children: [theme.symbol.thinking, " ", thinking, _jsx(Text, { color: theme.color.mutedPaint, children: theme.symbol.cursorBar })] })) : null, busy && toolHint ? (_jsxs(Text, { dimColor: true, children: [theme.symbol.workTool, " ", activityText(toolHint), toolElapsedSecs !== null && toolElapsedSecs >= 2 ? (_jsxs(_Fragment, { children: [" ", theme.symbol.separator, " ", toolElapsedSecs, "s"] })) : (theme.symbol.ellipsis)] })) : null, !freezeLive && busy && !draft && !thinking && !toolHint ? (_jsxs(Text, { dimColor: true, children: [theme.symbol.workThinking, " Thinking", theme.symbol.ellipsis, " ", theme.symbol.separator, " ", elapsedSecs, "s"] })) : null] }));
|
|
15
|
+
}
|