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,525 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { theme } from "./theme.js";
|
|
4
|
+
// Split `s` on inline-code spans first (code content is never formatted),
|
|
5
|
+
// then links, then bold/italic/strikethrough inside the remaining text.
|
|
6
|
+
export function parseInline(s) {
|
|
7
|
+
const out = [];
|
|
8
|
+
const parts = s.split(/(`[^`]*`)/g);
|
|
9
|
+
for (const part of parts) {
|
|
10
|
+
if (!part)
|
|
11
|
+
continue;
|
|
12
|
+
if (part.startsWith("`") && part.endsWith("`") && part.length >= 2) {
|
|
13
|
+
out.push({ kind: "code", text: part.slice(1, -1) });
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
out.push(...parseInlineRich(part));
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function parseInlineRich(s) {
|
|
21
|
+
const out = [];
|
|
22
|
+
// Links: [text](url). Autolinks (bare https://…) paint as plain text.
|
|
23
|
+
const re = /\[([^\]]*)\]\(([^)\s]+)\)|(\*\*[^*]+?\*\*)|(__[^_]+?__)|(~~[^~\n]+?~~)|(\*[^*\n]+?\*)|(_[^_\n]+?_)/g;
|
|
24
|
+
let last = 0;
|
|
25
|
+
let m;
|
|
26
|
+
const pushText = (t) => {
|
|
27
|
+
if (t)
|
|
28
|
+
out.push({ kind: "text", text: t });
|
|
29
|
+
};
|
|
30
|
+
// Single-char markers must not fire intra-word (`my_var`, `2*3*4` stay
|
|
31
|
+
// literal — CommonMark flanking, simplified to word boundaries).
|
|
32
|
+
const isWord = (c) => c !== undefined && /[\w]/.test(c);
|
|
33
|
+
while ((m = re.exec(s)) !== null) {
|
|
34
|
+
const raw = m[0];
|
|
35
|
+
const single = raw.startsWith("~~") || (raw.startsWith("*") && !raw.startsWith("**")) || (raw.startsWith("_") && !raw.startsWith("__"));
|
|
36
|
+
if (single) {
|
|
37
|
+
const before = m.index > 0 ? s[m.index - 1] : undefined;
|
|
38
|
+
const after = m.index + raw.length < s.length ? s[m.index + raw.length] : undefined;
|
|
39
|
+
if (isWord(before) || isWord(after)) {
|
|
40
|
+
pushText(s.slice(last, m.index + raw.length));
|
|
41
|
+
last = m.index + raw.length;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
pushText(s.slice(last, m.index));
|
|
46
|
+
last = m.index + raw.length;
|
|
47
|
+
if (m[1] !== undefined && m[2] !== undefined) {
|
|
48
|
+
out.push({ kind: "link", text: m[1], url: m[2] });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const inner = raw.startsWith("**") || raw.startsWith("__")
|
|
52
|
+
? raw.slice(2, -2)
|
|
53
|
+
: raw.slice(1, -1);
|
|
54
|
+
out.push({
|
|
55
|
+
kind: "text",
|
|
56
|
+
text: inner,
|
|
57
|
+
bold: (raw.startsWith("**") || raw.startsWith("__")) || undefined,
|
|
58
|
+
italic: (!raw.startsWith("**") && !raw.startsWith("__") && !raw.startsWith("~~")) || undefined,
|
|
59
|
+
strike: raw.startsWith("~~") || undefined,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
pushText(s.slice(last));
|
|
64
|
+
// Coalesce adjacent plain runs (literal fallbacks split them) — fewer
|
|
65
|
+
// nodes, and plain text stays one contiguous run.
|
|
66
|
+
const merged = [];
|
|
67
|
+
for (const r of out) {
|
|
68
|
+
const prev = merged[merged.length - 1];
|
|
69
|
+
if (r.kind === "text" && !r.bold && !r.italic && !r.strike &&
|
|
70
|
+
prev?.kind === "text" && !prev.bold && !prev.italic && !prev.strike) {
|
|
71
|
+
prev.text += r.text;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
merged.push(r.kind === "text" ? { ...r } : r);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return merged;
|
|
78
|
+
}
|
|
79
|
+
function expandTabs(line) {
|
|
80
|
+
return line.replace(/\t/g, " ");
|
|
81
|
+
}
|
|
82
|
+
// --- Tables (GFM, terminal-quiet) ---------------------------------------
|
|
83
|
+
// Tables render where feasible: header + dim separator + left-aligned rows,
|
|
84
|
+
// columns joined with `│`. Column widths come from cell content, capped so
|
|
85
|
+
// one long cell (a path, URL, stack line) never blows out an 80-col
|
|
86
|
+
// terminal; over-long cells truncate with `…`. No outer borders: nothing
|
|
87
|
+
// extra lands on copy/paste beyond the cell text itself.
|
|
88
|
+
export const TABLE_MAX_COL = 40;
|
|
89
|
+
function splitTableRow(line) {
|
|
90
|
+
if (!line.includes("|"))
|
|
91
|
+
return null;
|
|
92
|
+
// Split on unescaped pipes; `\|` stays a literal pipe inside the cell.
|
|
93
|
+
const cells = [];
|
|
94
|
+
let cur = "";
|
|
95
|
+
for (let k = 0; k < line.length; k++) {
|
|
96
|
+
const c = line[k];
|
|
97
|
+
if (c === "\\" && line[k + 1] === "|") {
|
|
98
|
+
cur += "|";
|
|
99
|
+
k += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (c === "|") {
|
|
103
|
+
cells.push(cur);
|
|
104
|
+
cur = "";
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
cur += c;
|
|
108
|
+
}
|
|
109
|
+
cells.push(cur);
|
|
110
|
+
// Drop the empty caps from leading/trailing pipes: `| a | b |` -> [a, b].
|
|
111
|
+
if (cells.length > 0 && cells[0].trim() === "")
|
|
112
|
+
cells.shift();
|
|
113
|
+
if (cells.length > 0 && cells[cells.length - 1].trim() === "")
|
|
114
|
+
cells.pop();
|
|
115
|
+
if (cells.length === 0)
|
|
116
|
+
return null;
|
|
117
|
+
return cells.map((c) => c.trim());
|
|
118
|
+
}
|
|
119
|
+
function isTableDelimiter(line) {
|
|
120
|
+
const cells = splitTableRow(line);
|
|
121
|
+
if (!cells || cells.length === 0)
|
|
122
|
+
return false;
|
|
123
|
+
return cells.every((c) => /^:?-{1,}:?$/.test(c));
|
|
124
|
+
}
|
|
125
|
+
// Rendered plain width of a cell: links read as `text (url)`, code as bare
|
|
126
|
+
// text — the width must match what Ink actually paints.
|
|
127
|
+
function cellPlain(runs) {
|
|
128
|
+
return runs
|
|
129
|
+
.map((r) => {
|
|
130
|
+
if (r.kind === "code")
|
|
131
|
+
return r.text;
|
|
132
|
+
if (r.kind === "link") {
|
|
133
|
+
return r.text && r.text !== r.url ? `${r.text} (${r.url})` : r.text || r.url;
|
|
134
|
+
}
|
|
135
|
+
return r.text;
|
|
136
|
+
})
|
|
137
|
+
.join("");
|
|
138
|
+
}
|
|
139
|
+
function truncatePlain(s, max) {
|
|
140
|
+
if ([...s].length <= max)
|
|
141
|
+
return s;
|
|
142
|
+
return [...s].slice(0, Math.max(0, max - 1)).join("") + theme.symbol.ellipsis;
|
|
143
|
+
}
|
|
144
|
+
// ATX closing hashes (`## Title ##`) are decoration, not content.
|
|
145
|
+
function stripAtxClose(s) {
|
|
146
|
+
return s.replace(/\s+#+$/, "").trim();
|
|
147
|
+
}
|
|
148
|
+
// Line-based block parser. Unclosed fences run to end of input (streaming
|
|
149
|
+
// cutoffs still render). Lists keep nesting as 2-space indents (capped —
|
|
150
|
+
// terminal width is scarce, deep nesting is re-indented, not tree-rendered).
|
|
151
|
+
// Tables parse only with a valid delimiter row; anything malformed stays a
|
|
152
|
+
// plain paragraph so streaming partials never flash a broken grid.
|
|
153
|
+
export function parseMarkdown(src) {
|
|
154
|
+
const lines = src.replace(/\r\n?/g, "\n").split("\n");
|
|
155
|
+
const blocks = [];
|
|
156
|
+
let para = [];
|
|
157
|
+
const flushPara = () => {
|
|
158
|
+
if (para.length === 0)
|
|
159
|
+
return;
|
|
160
|
+
blocks.push({ kind: "paragraph", runs: parseInline(para.join(" ")) });
|
|
161
|
+
para = [];
|
|
162
|
+
};
|
|
163
|
+
let i = 0;
|
|
164
|
+
while (i < lines.length) {
|
|
165
|
+
const line = lines[i];
|
|
166
|
+
const fence = line.match(/^(\s*)(`{3,}|~{3,})\s*(\S*)\s*$/);
|
|
167
|
+
if (fence) {
|
|
168
|
+
flushPara();
|
|
169
|
+
const marker = fence[2].startsWith("`") ? "`" : "~";
|
|
170
|
+
const fenceRe = marker === "`" ? /^\s*`{3,}\s*$/ : /^\s*~{3,}\s*$/;
|
|
171
|
+
const lang = fence[3] ?? "";
|
|
172
|
+
const body = [];
|
|
173
|
+
i += 1;
|
|
174
|
+
while (i < lines.length && !fenceRe.test(lines[i])) {
|
|
175
|
+
body.push(expandTabs(lines[i]));
|
|
176
|
+
i += 1;
|
|
177
|
+
}
|
|
178
|
+
i += 1; // consume closing fence (or run past end when unclosed)
|
|
179
|
+
blocks.push({ kind: "code", lang, lines: body });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
183
|
+
if (heading) {
|
|
184
|
+
flushPara();
|
|
185
|
+
const text = stripAtxClose(heading[2] ?? "");
|
|
186
|
+
blocks.push({
|
|
187
|
+
kind: "heading",
|
|
188
|
+
level: heading[1].length,
|
|
189
|
+
runs: parseInline(text || " "),
|
|
190
|
+
});
|
|
191
|
+
i += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
// Setext headings: `Text\n===` (H1) and `Text\n---` (H2). A `---` with
|
|
195
|
+
// no pending paragraph stays a horizontal rule (spacing carries it).
|
|
196
|
+
if (/^=+\s*$/.test(line) && para.length > 0) {
|
|
197
|
+
blocks.push({ kind: "heading", level: 1, runs: parseInline(para.join(" ")) });
|
|
198
|
+
para = [];
|
|
199
|
+
i += 1;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (/^-{3,}\s*$/.test(line) && para.length > 0 && !line.includes("|")) {
|
|
203
|
+
blocks.push({ kind: "heading", level: 2, runs: parseInline(para.join(" ")) });
|
|
204
|
+
para = [];
|
|
205
|
+
i += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const hr = /^\s*([-*_]\s*){3,}$/.test(line);
|
|
209
|
+
if (hr) {
|
|
210
|
+
flushPara();
|
|
211
|
+
i += 1;
|
|
212
|
+
continue; // spacing around neighbors carries the separation; no chrome
|
|
213
|
+
}
|
|
214
|
+
// GFM table: header row + delimiter row, then 0+ body rows. A header
|
|
215
|
+
// without a delimiter is an ordinary paragraph (streaming partials).
|
|
216
|
+
if (line.includes("|") && i + 1 < lines.length && isTableDelimiter(lines[i + 1])) {
|
|
217
|
+
const headerCells = splitTableRow(line);
|
|
218
|
+
if (headerCells) {
|
|
219
|
+
flushPara();
|
|
220
|
+
const headers = headerCells.map((c) => parseInline(c));
|
|
221
|
+
const rows = [];
|
|
222
|
+
i += 2;
|
|
223
|
+
while (i < lines.length) {
|
|
224
|
+
const rowLine = lines[i];
|
|
225
|
+
if (!rowLine.includes("|") || /^\s*$/.test(rowLine))
|
|
226
|
+
break;
|
|
227
|
+
if (isTableDelimiter(rowLine))
|
|
228
|
+
break;
|
|
229
|
+
// A new block (fence/heading/quote/list) ends the table.
|
|
230
|
+
if (/^(\s*)(`{3,}|~{3,})/.test(rowLine))
|
|
231
|
+
break;
|
|
232
|
+
if (/^(#{1,6})\s+/.test(rowLine))
|
|
233
|
+
break;
|
|
234
|
+
if (/^>\s?/.test(rowLine))
|
|
235
|
+
break;
|
|
236
|
+
if (/^(\s*)([-*+]|\d+[.)])\s+/.test(rowLine))
|
|
237
|
+
break;
|
|
238
|
+
const cells = splitTableRow(rowLine);
|
|
239
|
+
if (!cells)
|
|
240
|
+
break;
|
|
241
|
+
while (cells.length < headerCells.length)
|
|
242
|
+
cells.push("");
|
|
243
|
+
rows.push(cells.slice(0, headerCells.length).map((c) => parseInline(c)));
|
|
244
|
+
i += 1;
|
|
245
|
+
}
|
|
246
|
+
blocks.push({ kind: "table", headers, rows });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const quote = line.match(/^>\s?(.*)$/);
|
|
251
|
+
if (quote) {
|
|
252
|
+
flushPara();
|
|
253
|
+
const cited = [quote[1] ?? ""];
|
|
254
|
+
i += 1;
|
|
255
|
+
while (i < lines.length) {
|
|
256
|
+
const cont = lines[i].match(/^>\s?(.*)$/);
|
|
257
|
+
if (!cont)
|
|
258
|
+
break;
|
|
259
|
+
cited.push(cont[1] ?? "");
|
|
260
|
+
i += 1;
|
|
261
|
+
}
|
|
262
|
+
blocks.push({ kind: "quote", runs: parseInline(cited.join(" ")) });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const item = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
|
266
|
+
if (item) {
|
|
267
|
+
flushPara();
|
|
268
|
+
const items = [];
|
|
269
|
+
let raws = [];
|
|
270
|
+
const pushItem = () => {
|
|
271
|
+
const prev = items[items.length - 1];
|
|
272
|
+
if (!prev)
|
|
273
|
+
return;
|
|
274
|
+
prev.runs = parseInline(raws.join(" "));
|
|
275
|
+
raws = [];
|
|
276
|
+
};
|
|
277
|
+
while (i < lines.length) {
|
|
278
|
+
const it = lines[i].match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
|
279
|
+
if (it) {
|
|
280
|
+
if (items.length > 0)
|
|
281
|
+
pushItem();
|
|
282
|
+
const indentSpaces = expandTabs(it[1] ?? "").length;
|
|
283
|
+
const indent = Math.min(Math.floor(indentSpaces / 2), 4);
|
|
284
|
+
const ordered = /^\d/.test(it[2]);
|
|
285
|
+
let content = it[3] ?? "";
|
|
286
|
+
let marker = ordered ? `${it[2]}` : theme.symbol.bullet;
|
|
287
|
+
// Task lists: `- [ ] todo` / `- [x] done` read as ballot boxes.
|
|
288
|
+
const task = content.match(/^\[([ xX])\]\s+(.*)$/);
|
|
289
|
+
if (!ordered && task) {
|
|
290
|
+
marker = task[1].toLowerCase() === "x" ? `${theme.symbol.bullet} ☑` : `${theme.symbol.bullet} ☐`;
|
|
291
|
+
content = task[2] ?? "";
|
|
292
|
+
}
|
|
293
|
+
items.push({ marker, indent, runs: [] });
|
|
294
|
+
raws = [content];
|
|
295
|
+
i += 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
// Indented continuation lines join the current item (multi-line
|
|
299
|
+
// list bodies); anything else ends the list.
|
|
300
|
+
if (items.length > 0 && /^(\s+)\S/.test(lines[i]) && !/^\s*$/.test(lines[i])) {
|
|
301
|
+
raws.push(lines[i].trim());
|
|
302
|
+
i += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
if (items.length > 0)
|
|
308
|
+
pushItem();
|
|
309
|
+
blocks.push({ kind: "list", items });
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (/^\s*$/.test(line)) {
|
|
313
|
+
flushPara();
|
|
314
|
+
i += 1;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
para.push(line.trim());
|
|
318
|
+
i += 1;
|
|
319
|
+
}
|
|
320
|
+
flushPara();
|
|
321
|
+
return blocks;
|
|
322
|
+
}
|
|
323
|
+
// Bounded parse cache: Static items render once, but timer-tick re-renders,
|
|
324
|
+
// /resume restores, and tests re-render the same turns — never re-parse.
|
|
325
|
+
const PARSE_CACHE_CAP = 300;
|
|
326
|
+
const parseCache = new Map();
|
|
327
|
+
export function parseMarkdownCached(src) {
|
|
328
|
+
const hit = parseCache.get(src);
|
|
329
|
+
if (hit)
|
|
330
|
+
return hit;
|
|
331
|
+
const blocks = parseMarkdown(src);
|
|
332
|
+
parseCache.set(src, blocks);
|
|
333
|
+
if (parseCache.size > PARSE_CACHE_CAP) {
|
|
334
|
+
const oldest = parseCache.keys().next();
|
|
335
|
+
if (!oldest.done)
|
|
336
|
+
parseCache.delete(oldest.value);
|
|
337
|
+
}
|
|
338
|
+
return blocks;
|
|
339
|
+
}
|
|
340
|
+
function InlineRuns({ runs }) {
|
|
341
|
+
return (_jsx(_Fragment, { children: runs.map((r, k) => {
|
|
342
|
+
if (r.kind === "code") {
|
|
343
|
+
return (_jsx(Text, { color: theme.color.code, children: r.text }, k));
|
|
344
|
+
}
|
|
345
|
+
if (r.kind === "link") {
|
|
346
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.color.link, underline: true, children: r.text || r.url }), r.text && r.text !== r.url ? _jsxs(Text, { dimColor: true, children: [" (", r.url, ")"] }) : null] }, k));
|
|
347
|
+
}
|
|
348
|
+
return (_jsx(Text, { bold: r.bold, italic: r.italic, strikethrough: r.strike, children: r.text }, k));
|
|
349
|
+
}) }));
|
|
350
|
+
}
|
|
351
|
+
// Tables paint as plain aligned columns: bold header, one dim separator
|
|
352
|
+
// row, left-aligned body. No outer borders or boxes (copy/paste stays the
|
|
353
|
+
// cell text). Widths derive from truncated cell plains so a single long
|
|
354
|
+
// cell never pushes the grid off-screen; Ink wraps the row if the terminal
|
|
355
|
+
// is narrower still, preserving content over grid shape.
|
|
356
|
+
function TableView({ block, gap, }) {
|
|
357
|
+
const cols = block.headers.length;
|
|
358
|
+
const headPlains = block.headers.map((h) => truncatePlain(cellPlain(h), TABLE_MAX_COL));
|
|
359
|
+
const bodyPlains = block.rows.map((r) => Array.from({ length: cols }, (_, c) => truncatePlain(cellPlain(r[c] ?? []), TABLE_MAX_COL)));
|
|
360
|
+
const widths = Array.from({ length: cols }, (_, c) => {
|
|
361
|
+
let w = [...(headPlains[c] ?? "")].length;
|
|
362
|
+
for (const row of bodyPlains)
|
|
363
|
+
w = Math.max(w, [...(row[c] ?? "")].length);
|
|
364
|
+
return Math.max(w, 1);
|
|
365
|
+
});
|
|
366
|
+
const colSep = ` ${theme.symbol.quoteBar} `;
|
|
367
|
+
const joint = "┼";
|
|
368
|
+
const ruleRow = widths.map((w) => theme.symbol.rule.repeat(w)).join(`${theme.symbol.rule}${joint}${theme.symbol.rule}`);
|
|
369
|
+
// Long cells render as truncated plain text (shape over formatting);
|
|
370
|
+
// short cells keep their runs (bold/code/links) plus space padding.
|
|
371
|
+
const renderCell = (runs, plain, width, boldCell) => {
|
|
372
|
+
const full = cellPlain(runs);
|
|
373
|
+
if (full !== plain) {
|
|
374
|
+
return _jsx(Text, { bold: boldCell, children: plain + " ".repeat(Math.max(0, width - [...plain].length)) });
|
|
375
|
+
}
|
|
376
|
+
return (_jsxs(Text, { bold: boldCell, children: [_jsx(InlineRuns, { runs: runs }), width > [...plain].length ? " ".repeat(width - [...plain].length) : null] }));
|
|
377
|
+
};
|
|
378
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: gap ? 1 : 0, children: [_jsx(Text, { children: block.headers.map((h, c) => (_jsxs(Text, { children: [c > 0 ? colSep : null, renderCell(h, headPlains[c] ?? "", widths[c] ?? 1, true)] }, c))) }), _jsx(Text, { dimColor: true, children: ruleRow }), block.rows.map((r, k) => (_jsx(Text, { children: Array.from({ length: cols }, (_, c) => (_jsxs(Text, { children: [c > 0 ? colSep : null, renderCell(r[c] ?? [], bodyPlains[k]?.[c] ?? "", widths[c] ?? 1, false)] }, c))) }, k)))] }));
|
|
379
|
+
}
|
|
380
|
+
function BlockView({ block, gap }) {
|
|
381
|
+
const top = gap ? 1 : 0;
|
|
382
|
+
switch (block.kind) {
|
|
383
|
+
case "heading":
|
|
384
|
+
return (_jsx(Box, { marginTop: top, children: _jsx(Text, { bold: true, children: _jsx(InlineRuns, { runs: block.runs }) }) }));
|
|
385
|
+
case "list":
|
|
386
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: top, children: block.items.map((it, k) => (_jsxs(Text, { children: [" ".repeat(it.indent), it.marker, " ", _jsx(InlineRuns, { runs: it.runs })] }, k))) }));
|
|
387
|
+
case "table":
|
|
388
|
+
return _jsx(TableView, { block: block, gap: top > 0 });
|
|
389
|
+
case "quote":
|
|
390
|
+
return (_jsx(Box, { marginTop: top, children: _jsxs(Text, { dimColor: true, children: [theme.symbol.quoteBar, " ", _jsx(InlineRuns, { runs: block.runs })] }) }));
|
|
391
|
+
case "code":
|
|
392
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: top, children: [block.lang ? _jsx(Text, { dimColor: true, children: block.lang }) : null, block.lines.map((ln, k) => (_jsx(Text, { children: ln.length > 0 ? `${theme.spacing.codeIndent}${ln}` : " " }, k)))] }));
|
|
393
|
+
case "paragraph":
|
|
394
|
+
return (_jsx(Box, { marginTop: top, children: _jsx(Text, { children: _jsx(InlineRuns, { runs: block.runs }) }) }));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// --- Streaming variant --------------------------------------------------
|
|
398
|
+
// Mid-stream partials end with unclosed markers (`**bold`, `*italic`,
|
|
399
|
+
// `~~strike`, `` `code ``, `[text](url`). Committed rendering would leak
|
|
400
|
+
// the raw markers, so the streaming pass auto-closes a trailing opener
|
|
401
|
+
// before parsing — the paint then matches the final shape instead of
|
|
402
|
+
// flashing broken markdown. Closers apply only with markdown-flanking
|
|
403
|
+
// (opener glued to content, not `a ** b`; intra-word `my_var`/`2*3*4`
|
|
404
|
+
// stay literal), so literal asterisks never misfire mid-stream. Fences
|
|
405
|
+
// (```/~~~) need no help: the block parser already runs unclosed fences
|
|
406
|
+
// to end of input. Tables need no help either: a header without its
|
|
407
|
+
// delimiter row parses as plain text until the delimiter streams in.
|
|
408
|
+
function unclosedMarker(s, m) {
|
|
409
|
+
let count = 0;
|
|
410
|
+
let idx = -1;
|
|
411
|
+
for (;;) {
|
|
412
|
+
idx = s.indexOf(m, idx + 1);
|
|
413
|
+
if (idx === -1)
|
|
414
|
+
break;
|
|
415
|
+
count += 1;
|
|
416
|
+
}
|
|
417
|
+
if (count % 2 === 0)
|
|
418
|
+
return false;
|
|
419
|
+
const last = s.lastIndexOf(m);
|
|
420
|
+
const after = s[last + m.length];
|
|
421
|
+
const before = last > 0 ? s[last - 1] : undefined;
|
|
422
|
+
if (after === undefined || /\s/.test(after))
|
|
423
|
+
return false;
|
|
424
|
+
if (before !== undefined && /[\w]/.test(before) && /[\w]/.test(after))
|
|
425
|
+
return false;
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
// Single `*`/`_` closers ignore characters already paired as `**`/`__`
|
|
429
|
+
// doubles, then apply the same flanking + intra-word guards as the parser.
|
|
430
|
+
function unclosedSingle(s, ch, double) {
|
|
431
|
+
const withoutDoubles = s.split(double).join("");
|
|
432
|
+
let count = 0;
|
|
433
|
+
for (const c of withoutDoubles)
|
|
434
|
+
if (c === ch)
|
|
435
|
+
count += 1;
|
|
436
|
+
if (count % 2 === 0)
|
|
437
|
+
return false;
|
|
438
|
+
const last = withoutDoubles.lastIndexOf(ch);
|
|
439
|
+
const after = withoutDoubles[last + 1];
|
|
440
|
+
const before = last > 0 ? withoutDoubles[last - 1] : undefined;
|
|
441
|
+
if (after === undefined || /\s/.test(after))
|
|
442
|
+
return false;
|
|
443
|
+
// The appended closer lands at end-of-string (non-word), so the
|
|
444
|
+
// committed full match is literal only when the opener sits intra-word
|
|
445
|
+
// (`my_var`) — matching the parser's own single-marker guard.
|
|
446
|
+
if (before !== undefined && /[\w]/.test(before))
|
|
447
|
+
return false;
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
export function closeStreamingMarkers(s) {
|
|
451
|
+
// Code spans protect their contents from marker counting.
|
|
452
|
+
const fenceless = s.replace(/^(\s*)(`{3,}|~{3,}).*$/gm, "");
|
|
453
|
+
const stripped = fenceless.replace(/`[^`\n]*`/g, "");
|
|
454
|
+
let out = s;
|
|
455
|
+
if (unclosedMarker(stripped, "**"))
|
|
456
|
+
out += "**";
|
|
457
|
+
if (unclosedMarker(stripped, "__"))
|
|
458
|
+
out += "__";
|
|
459
|
+
if (unclosedMarker(stripped, "~~"))
|
|
460
|
+
out += "~~";
|
|
461
|
+
if (unclosedSingle(stripped, "*", "**"))
|
|
462
|
+
out += "*";
|
|
463
|
+
if (unclosedSingle(stripped, "_", "__"))
|
|
464
|
+
out += "_";
|
|
465
|
+
const ticks = stripped.split("").filter((c) => c === "`").length;
|
|
466
|
+
if (ticks % 2 === 1) {
|
|
467
|
+
const lastTick = stripped.lastIndexOf("`");
|
|
468
|
+
const after = stripped[lastTick + 1];
|
|
469
|
+
if (after !== undefined && !/\s/.test(after))
|
|
470
|
+
out += "`";
|
|
471
|
+
}
|
|
472
|
+
// Unclosed link destination: `[text](https://…` streams char-by-char;
|
|
473
|
+
// closing it renders the link shape instead of flashing raw brackets.
|
|
474
|
+
if (/\[[^\]\n]*\]\([^)\s]*$/.test(stripped))
|
|
475
|
+
out += ")";
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
// Streaming answer draft: same grammar as the committed body, but parsed
|
|
479
|
+
// fresh per paint (partials churn — caching them would evict committed
|
|
480
|
+
// turns' entries for zero hits) with transient markers closed and the
|
|
481
|
+
// block cursor riding the final run. Converges to MarkdownText byte-for-
|
|
482
|
+
// byte once the stream completes (cursor aside), so commit never visually
|
|
483
|
+
// jumps.
|
|
484
|
+
export function MarkdownStream({ text }) {
|
|
485
|
+
const blocks = parseMarkdown(closeStreamingMarkers(text) + theme.symbol.cursorBar);
|
|
486
|
+
return (_jsx(Box, { flexDirection: "column", children: blocks.map((b, k) => (_jsx(BlockView, { block: b, gap: k > 0 }, k))) }));
|
|
487
|
+
}
|
|
488
|
+
// Assistant body: full markdown when the text parses into structure,
|
|
489
|
+
// byte-identical plain text otherwise (a single paragraph paints its runs;
|
|
490
|
+
// with no formatting syntax those runs are the input verbatim).
|
|
491
|
+
export function MarkdownText({ text }) {
|
|
492
|
+
const blocks = parseMarkdownCached(text);
|
|
493
|
+
return (_jsx(Box, { flexDirection: "column", children: blocks.map((b, k) => (_jsx(BlockView, { block: b, gap: k > 0 }, k))) }));
|
|
494
|
+
}
|
|
495
|
+
// Compact tool/activity line. The call row keeps its loop-produced text
|
|
496
|
+
// byte-identical (`⚙ name target` is pinned by tests + help), and state
|
|
497
|
+
// reads from shape, not decoration:
|
|
498
|
+
// - success: dim call row, plus a `· Ns` suffix when the TUI measured a
|
|
499
|
+
// slow run (TOOL_SLOW_MS threshold — fast tools stay one clean line).
|
|
500
|
+
// - failed: the red `↳ detail` row the loop commits (error flag forces red
|
|
501
|
+
// on any shape, so unknown future shapes still read as failures).
|
|
502
|
+
// - warning (`⚠ `): warning color — the one hue escalation, marking "needs
|
|
503
|
+
// attention" without a box.
|
|
504
|
+
// - denied (`⊘ `), retry (`↻ `), cancel notices, boundaries, and multi-line
|
|
505
|
+
// outputs (todo checklists): full fidelity, dim, untouched.
|
|
506
|
+
// Results are deliberately NOT echoed on success (the model owns them, not
|
|
507
|
+
// the transcript — pinned by plan-mode tests); large outputs never dump.
|
|
508
|
+
// True folding needs interactive history (Static items freeze on commit);
|
|
509
|
+
// this component is that chunk's seam.
|
|
510
|
+
export const TOOL_SLOW_MS = 2000;
|
|
511
|
+
export function ToolLine({ content, error, ms }) {
|
|
512
|
+
if (error) {
|
|
513
|
+
return _jsx(Text, { color: theme.color.toolError, children: content });
|
|
514
|
+
}
|
|
515
|
+
if (content.startsWith("⚠ ")) {
|
|
516
|
+
return _jsx(Text, { color: theme.color.warning, children: content });
|
|
517
|
+
}
|
|
518
|
+
if (content.startsWith(`${theme.symbol.toolMark} `) &&
|
|
519
|
+
!content.includes("\n") &&
|
|
520
|
+
ms !== undefined &&
|
|
521
|
+
ms >= TOOL_SLOW_MS) {
|
|
522
|
+
return (_jsxs(Text, { color: theme.color.tool, dimColor: true, children: [content, " ", theme.symbol.separator, " ", Math.round(ms / 1000), "s"] }));
|
|
523
|
+
}
|
|
524
|
+
return (_jsx(Text, { color: theme.color.tool, dimColor: true, children: content }));
|
|
525
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Modal leaves: the tool-approval and ask_question dialogs. Prop-driven.
|
|
3
|
+
// All paint comes from ui/theme tokens. The approval tool-call description
|
|
4
|
+
// arrives pre-formatted (describeToolCall stays in App) so this module
|
|
5
|
+
// couples to no tool internals — the approval-redesign chunk owns it.
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
|
+
import { SideBySideDiffView } from "./side-by-side.js";
|
|
9
|
+
import { theme } from "./theme.js";
|
|
10
|
+
// Max diff body lines inside the approval modal (hunk headers excluded;
|
|
11
|
+
// the trailer names the remainder). Keeps the modal scannable while the
|
|
12
|
+
// 1s busy tick repaints around it.
|
|
13
|
+
export const APPROVAL_DIFF_MAX_LINES = 40;
|
|
14
|
+
export const APPROVAL_OPTIONS = ["once", "always", "trustAll", "no"];
|
|
15
|
+
// Command/file preview: the audit description minus its `⚙ name` prefix
|
|
16
|
+
// (the tool name already headlines above). Falls back to the full text
|
|
17
|
+
// when the shape is unexpected — never invents content.
|
|
18
|
+
export function approvalPreview(toolName, description) {
|
|
19
|
+
const prefix = `⚙ ${toolName} `;
|
|
20
|
+
if (description.startsWith(prefix))
|
|
21
|
+
return description.slice(prefix.length);
|
|
22
|
+
return description;
|
|
23
|
+
}
|
|
24
|
+
export function approvalTitle(toolName) {
|
|
25
|
+
return toolName.length > 0 ? toolName[0].toUpperCase() + toolName.slice(1) : toolName;
|
|
26
|
+
}
|
|
27
|
+
// Render-count probes for the flicker tests: the 1s busy tick and unrelated
|
|
28
|
+
// parent churn must skip both modals (only changed props repaint — nav
|
|
29
|
+
// selection still paints exactly once per keypress).
|
|
30
|
+
export const approvalRenderProbe = { count: 0 };
|
|
31
|
+
export const questionRenderProbe = { count: 0 };
|
|
32
|
+
export const ApprovalBox = React.memo(function ApprovalBox({ toolName, description, selected, diff }) {
|
|
33
|
+
approvalRenderProbe.count += 1;
|
|
34
|
+
const rows = [
|
|
35
|
+
// Labels keep the historical [y]/[a]/[t]/[n] shortcuts (pinned by tests
|
|
36
|
+
// + muscle memory): arrows are additive, shortcuts never move.
|
|
37
|
+
{ label: "[y]es once", option: "once" },
|
|
38
|
+
{ label: `[a]lways allow ${toolName} this session`, option: "always" },
|
|
39
|
+
{ label: "[t]rust all write/edit/bash this session", option: "trustAll" },
|
|
40
|
+
{ label: "[n]o — deny this call", option: "no" },
|
|
41
|
+
];
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.border.style, borderColor: theme.border.approval, paddingX: theme.spacing.pickerPadX, children: [_jsxs(Text, { bold: true, color: theme.color.warning, children: [theme.symbol.warningMark, " Atom permission \u2014 allow this tool?"] }), _jsx(Text, { bold: true, children: approvalTitle(toolName) }), _jsx(Text, { color: theme.color.code, children: approvalPreview(toolName, description) }), diff ? _jsx(SideBySideDiffView, { oldText: diff.oldText, newText: diff.newText, lang: diff.lang, maxRows: APPROVAL_DIFF_MAX_LINES }) : null, rows.map((r, i) => (_jsxs(Text, { color: i === selected ? theme.color.selection : undefined, children: [i === selected ? `${theme.symbol.select} ` : theme.spacing.rowIndent, r.label] }, r.option))), _jsx(Text, { dimColor: true, children: "\u2191/\u2193 + Enter selects \u00B7 y/a/t/n shortcuts \u00B7 Esc denies" })] }));
|
|
43
|
+
});
|
|
44
|
+
export const QuestionBox = React.memo(function QuestionBox({ question, options, allowCustom, askCustom, askSelIndex }) {
|
|
45
|
+
questionRenderProbe.count += 1;
|
|
46
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.border.style, borderColor: theme.border.question, paddingX: theme.spacing.pickerPadX, children: [_jsxs(Text, { bold: true, children: ["Atom question \u2014 ", question] }), options.map((o, i) => (_jsxs(Text, { color: i === askSelIndex ? theme.color.questionSelection : undefined, children: [i === askSelIndex ? `${theme.symbol.select} ` : theme.spacing.rowIndent, o] }, `${o}-${i}`))), allowCustom ? (_jsxs(Text, { dimColor: true, children: ["Type a custom answer + Enter to send it", askCustom ? `: ${askCustom}` : "", " \u00B7 \u2191/\u2193 + Enter picks \u00B7 Esc cancels"] })) : (_jsx(Text, { dimColor: true, children: "\u2191/\u2193 + Enter to pick \u00B7 Esc cancels" }))] }));
|
|
47
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Command-palette panel (Ctrl+P): grouped, searchable, keyboard-driven.
|
|
3
|
+
// Owns its display types (categories, hints); App builds the entries from
|
|
4
|
+
// the SLASH_COMMANDS registry (single command system) and owns the run
|
|
5
|
+
// gating. Windowed like every other popup; headers render for groups
|
|
6
|
+
// present in the window (plus the open group when sliced mid-way).
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { Box, Text } from "ink";
|
|
9
|
+
import { PickerMoreAbove, PickerMoreBelow, pickerWindow } from "./pickers.js";
|
|
10
|
+
import { theme } from "./theme.js";
|
|
11
|
+
export const PALETTE_WINDOW = 12;
|
|
12
|
+
export const PALETTE_CATEGORY_ORDER = ["Model", "Session", "Tools", "Skills", "Flow", "Help"];
|
|
13
|
+
const PALETTE_CATEGORIES = {
|
|
14
|
+
"/model": "Model",
|
|
15
|
+
"/provider": "Model",
|
|
16
|
+
"/effort": "Model",
|
|
17
|
+
"/compact": "Session",
|
|
18
|
+
"/clear": "Session",
|
|
19
|
+
"/new": "Session",
|
|
20
|
+
"/resume": "Session",
|
|
21
|
+
"/rewind": "Session",
|
|
22
|
+
"/context": "Session",
|
|
23
|
+
"/telemetry": "Session",
|
|
24
|
+
"/dashboard": "Session",
|
|
25
|
+
"/tools": "Tools",
|
|
26
|
+
"/mode": "Tools",
|
|
27
|
+
"/trust": "Tools",
|
|
28
|
+
"/allow": "Tools",
|
|
29
|
+
"/deny": "Tools",
|
|
30
|
+
"/rules": "Tools",
|
|
31
|
+
"/skills": "Skills",
|
|
32
|
+
"/skill": "Skills",
|
|
33
|
+
"/queue": "Flow",
|
|
34
|
+
"/steer": "Flow",
|
|
35
|
+
"/autoscroll": "Flow",
|
|
36
|
+
"/thinking": "Flow",
|
|
37
|
+
"/help": "Help",
|
|
38
|
+
"/exit": "Help",
|
|
39
|
+
"/quit": "Help",
|
|
40
|
+
};
|
|
41
|
+
export function paletteCategory(name) {
|
|
42
|
+
return PALETTE_CATEGORIES[name] ?? "Help";
|
|
43
|
+
}
|
|
44
|
+
// Real key bindings only — shown as row hints, never invented.
|
|
45
|
+
// (Mode switching lives on Tab alone now, so no command carries it.)
|
|
46
|
+
export const PALETTE_HINTS = {
|
|
47
|
+
"/exit": "Ctrl+C",
|
|
48
|
+
"/quit": "Ctrl+C",
|
|
49
|
+
};
|
|
50
|
+
export const PalettePanel = React.memo(function PalettePanel({ entries, index, filter }) {
|
|
51
|
+
const hi = entries.length === 0 ? 0 : Math.max(0, Math.min(index, entries.length - 1));
|
|
52
|
+
const win = pickerWindow(entries.length, hi, PALETTE_WINDOW);
|
|
53
|
+
const slice = entries.slice(win.start, win.end);
|
|
54
|
+
const rows = [];
|
|
55
|
+
let lastCat = null;
|
|
56
|
+
if (slice.length > 0 && win.start > 0) {
|
|
57
|
+
// Sliced mid-group: name the open group so rows never float headerless.
|
|
58
|
+
lastCat = slice[0].category;
|
|
59
|
+
rows.push(_jsx(Text, { dimColor: true, children: lastCat }, `cat-${lastCat}`));
|
|
60
|
+
}
|
|
61
|
+
slice.forEach((e, k) => {
|
|
62
|
+
const i = win.start + k;
|
|
63
|
+
if (e.category !== lastCat) {
|
|
64
|
+
lastCat = e.category;
|
|
65
|
+
rows.push(_jsx(Text, { dimColor: true, children: e.category }, `cat-${e.category}-${i}`));
|
|
66
|
+
}
|
|
67
|
+
rows.push(_jsxs(Text, { color: i === hi ? theme.color.menuSelection : undefined, children: [i === hi ? `${theme.symbol.select} ` : theme.spacing.rowIndent, e.name, e.description ? ` ${theme.symbol.descSeparator} ${e.description}` : "", e.hint ? _jsxs(Text, { dimColor: true, children: [" \u00B7 ", e.hint] }) : null] }, `${e.name}-${i}`));
|
|
68
|
+
});
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.border.style, borderColor: theme.border.menu, paddingX: theme.spacing.pickerPadX, children: [_jsx(Text, { bold: true, children: "Search commands \u2014 type to filter (\u2191/\u2193 + Enter to run, Esc closes):" }), _jsxs(Text, { children: [_jsxs(Text, { color: theme.color.inputPrompt, bold: true, children: [theme.symbol.inputPrompt, " "] }), filter, _jsx(Text, { color: theme.color.mutedPaint, children: theme.symbol.cursorBlock })] }), _jsx(PickerMoreAbove, { count: win.start }), rows, _jsx(PickerMoreBelow, { count: entries.length - win.end }), entries.length === 0 ? _jsx(Text, { dimColor: true, children: "No commands match \u2014 backspace to widen." }) : null] }));
|
|
70
|
+
});
|