@oxbit/cli 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1954 @@
1
+ #!/usr/bin/env node
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { renderToString, Box, Text, useAnimation, measureElement, useInput, useWindowSize, usePaste, Static, render } from "ink";
4
+ import { createContext, useContext, useMemo, useState, useRef, useEffect, useSyncExternalStore } from "react";
5
+ import { M as MODES, w as walkFiles } from "../index.js";
6
+ import { mkdirSync, appendFileSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { dirname } from "node:path";
8
+ import "node:util";
9
+ import "node:readline";
10
+ import "node:fs/promises";
11
+ import "node:crypto";
12
+ import "node:os";
13
+ import "node:child_process";
14
+ import "node:http";
15
+ const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^\s`]*)/;
16
+ const HEADING = /^ {0,3}(#{1,6})(?:\s+(.*?))?\s*#*\s*$/;
17
+ const RULE = /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/;
18
+ const LIST = /^(\s*)([-*+]|\d{1,9}[.)])(?:\s+(.*))?$/;
19
+ const QUOTE = /^ {0,3}>\s?(.*)$/;
20
+ const TABLE_SEPARATOR = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
21
+ const cells = (line) => line.trim().replace(/^\|/, "").replace(/(?<!\\)\|$/, "").split(/(?<!\\)\|/).map((cell) => cell.trim().replace(/\\\|/g, "|"));
22
+ const closes = (line, fence) => line.trim().startsWith(fence) && /^[`~]+$/.test(line.trim()) && line.trim()[0] === fence[0];
23
+ const startsBlock = (line, next) => FENCE.test(line) || HEADING.test(line) || RULE.test(line) || LIST.test(line) || QUOTE.test(line) || line.includes("|") && next !== void 0 && TABLE_SEPARATOR.test(next) && next.includes("-");
24
+ function parseBlocks(text) {
25
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
26
+ const blocks = [];
27
+ let index = 0;
28
+ while (index < lines.length) {
29
+ const line = lines[index];
30
+ if (!line.trim()) {
31
+ index++;
32
+ continue;
33
+ }
34
+ const fence = FENCE.exec(line);
35
+ if (fence) {
36
+ const body2 = [];
37
+ const indent = line.length - line.trimStart().length;
38
+ index++;
39
+ while (index < lines.length && !closes(lines[index], fence[1])) body2.push(lines[index++].replace(new RegExp(`^ {0,${indent}}`), ""));
40
+ blocks.push({ type: "code", lang: fence[2] ?? "", lines: body2, open: index >= lines.length });
41
+ index++;
42
+ continue;
43
+ }
44
+ const heading = HEADING.exec(line);
45
+ if (heading) {
46
+ blocks.push({ type: "heading", level: heading[1].length, text: heading[2] ?? "" });
47
+ index++;
48
+ continue;
49
+ }
50
+ if (RULE.test(line)) {
51
+ blocks.push({ type: "rule" });
52
+ index++;
53
+ continue;
54
+ }
55
+ if (QUOTE.test(line)) {
56
+ const body2 = [];
57
+ while (index < lines.length && lines[index].trim()) {
58
+ const quoted = QUOTE.exec(lines[index]);
59
+ if (!quoted && startsBlock(lines[index])) break;
60
+ body2.push(quoted ? quoted[1] : lines[index].trim());
61
+ index++;
62
+ }
63
+ blocks.push({ type: "quote", blocks: parseBlocks(body2.join("\n")) });
64
+ continue;
65
+ }
66
+ if (line.includes("|") && index + 1 < lines.length && TABLE_SEPARATOR.test(lines[index + 1]) && lines[index + 1].includes("-")) {
67
+ const head = cells(line);
68
+ const align = cells(lines[index + 1]).map((cell) => cell.endsWith(":") ? cell.startsWith(":") ? "center" : "right" : "left");
69
+ const rows = [];
70
+ index += 2;
71
+ while (index < lines.length && lines[index].includes("|") && lines[index].trim()) rows.push(cells(lines[index++]));
72
+ blocks.push({ type: "table", head, align, rows });
73
+ continue;
74
+ }
75
+ if (LIST.test(line)) {
76
+ index = parseList(lines, index, blocks);
77
+ continue;
78
+ }
79
+ const body = [line.trim()];
80
+ index++;
81
+ while (index < lines.length && lines[index].trim() && !startsBlock(lines[index], lines[index + 1])) {
82
+ if (/^ {0,3}=+\s*$/.test(lines[index]) || /^ {0,3}-+\s*$/.test(lines[index])) break;
83
+ body.push(lines[index++].trim());
84
+ }
85
+ const setext = lines[index]?.trim();
86
+ if (setext && /^(=+|-+)$/.test(setext)) {
87
+ blocks.push({ type: "heading", level: setext[0] === "=" ? 1 : 2, text: body.join(" ") });
88
+ index++;
89
+ } else blocks.push({ type: "paragraph", text: body.join("\n") });
90
+ }
91
+ return blocks;
92
+ }
93
+ function parseList(lines, start, blocks) {
94
+ const items = [];
95
+ const indents = [];
96
+ const counters = [];
97
+ let index = start;
98
+ while (index < lines.length) {
99
+ const line = lines[index];
100
+ const match = LIST.exec(line);
101
+ if (match && !RULE.test(line)) {
102
+ const indent = match[1].length;
103
+ while (indents.length && indent < indents.at(-1)) {
104
+ indents.pop();
105
+ counters.pop();
106
+ }
107
+ if (!indents.length || indent > indents.at(-1) + 1) {
108
+ indents.push(indent);
109
+ counters.push(0);
110
+ }
111
+ const depth = indents.length - 1;
112
+ const ordered = /\d/.test(match[2]);
113
+ counters[depth] = ordered ? (counters[depth] || Number.parseInt(match[2], 10) - 1) + 1 : 0;
114
+ const task = /^\[([ xX])\]\s+(.*)$/.exec(match[3] ?? "");
115
+ items.push({
116
+ depth,
117
+ marker: ordered ? `${counters[depth]}.` : "-",
118
+ text: task ? task[2] : match[3] ?? "",
119
+ ...task ? { checked: task[1] !== " " } : {}
120
+ });
121
+ index++;
122
+ } else if (line.trim() && items.length && !startsBlock(line)) {
123
+ items.at(-1).text += `
124
+ ${line.trim()}`;
125
+ index++;
126
+ } else if (!line.trim() && index + 1 < lines.length && (LIST.test(lines[index + 1]) || /^\s{2,}\S/.test(lines[index + 1]))) index++;
127
+ else break;
128
+ }
129
+ blocks.push({ type: "list", items });
130
+ return index;
131
+ }
132
+ const flanked = (text, at, size) => text[at + size] !== void 0 && !/\s/.test(text[at + size]);
133
+ const merge = (spans) => spans.reduce((out, span) => {
134
+ const last = out.at(-1);
135
+ const { text: _a, ...left2 } = last ?? { text: "" };
136
+ const { text: _b, ...right2 } = span;
137
+ if (last && JSON.stringify(left2) === JSON.stringify(right2)) last.text += span.text;
138
+ else if (span.text) out.push({ ...span });
139
+ return out;
140
+ }, []);
141
+ function parseInline(text, style = {}) {
142
+ const spans = [];
143
+ let plain = "";
144
+ const push = (...items) => {
145
+ if (plain) spans.push({ ...style, text: plain });
146
+ plain = "";
147
+ spans.push(...items);
148
+ };
149
+ let index = 0;
150
+ while (index < text.length) {
151
+ const char = text[index];
152
+ const rest = text.slice(index);
153
+ if (char === "\\" && /[\\`*_{}[\]()#+\-.!|~<>]/.test(text[index + 1] ?? "")) {
154
+ plain += text[index + 1];
155
+ index += 2;
156
+ continue;
157
+ }
158
+ if (char === "`") {
159
+ const run2 = /^`+/.exec(rest)[0];
160
+ const end2 = text.indexOf(run2, index + run2.length);
161
+ if (end2 > 0 && text[end2 + run2.length] !== "`") {
162
+ const body = text.slice(index + run2.length, end2);
163
+ push({ ...style, code: true, text: /^ .* $/.test(body) ? body.slice(1, -1) : body });
164
+ index = end2 + run2.length;
165
+ continue;
166
+ }
167
+ plain += run2;
168
+ index += run2.length;
169
+ continue;
170
+ }
171
+ const strong = /^(\*\*|__|~~)/.exec(rest)?.[1];
172
+ if (strong && flanked(text, index, 2)) {
173
+ const end2 = text.indexOf(strong, index + 2);
174
+ if (end2 > index + 2 && !/\s/.test(text[end2 - 1])) {
175
+ push(...parseInline(text.slice(index + 2, end2), { ...style, ...strong === "~~" ? { strike: true } : { bold: true } }));
176
+ index = end2 + 2;
177
+ continue;
178
+ }
179
+ }
180
+ if ((char === "*" || char === "_") && text[index + 1] !== char && flanked(text, index, 1) && !(char === "_" && /\w/.test(text[index - 1] ?? ""))) {
181
+ let end2 = index + 1;
182
+ while ((end2 = text.indexOf(char, end2)) > 0 && (text[end2 + 1] === char || /\s/.test(text[end2 - 1]) || char === "_" && /\w/.test(text[end2 + 1] ?? "")))
183
+ end2 += text[end2 + 1] === char ? 2 : 1;
184
+ if (end2 > index + 1) {
185
+ push(...parseInline(text.slice(index + 1, end2), { ...style, italic: true }));
186
+ index = end2 + 1;
187
+ continue;
188
+ }
189
+ }
190
+ const link = /^\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/.exec(rest);
191
+ if (link) {
192
+ push(...parseInline(link[1], { ...style, link: link[2] }));
193
+ index += link[0].length;
194
+ continue;
195
+ }
196
+ const auto = /^<(https?:\/\/[^>\s]+)>/.exec(rest) ?? (/[\s(]|^$/.test(text[index - 1] ?? "") ? /^(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/.exec(rest) : null);
197
+ if (auto) {
198
+ push({ ...style, link: auto[1], text: auto[1] });
199
+ index += auto[0].length;
200
+ continue;
201
+ }
202
+ plain += char;
203
+ index++;
204
+ }
205
+ push();
206
+ return merge(spans);
207
+ }
208
+ const plainText = (text) => parseInline(text).map((span) => span.text).join("");
209
+ function stableLength(text) {
210
+ let fence;
211
+ let offset = 0;
212
+ let cut = 0;
213
+ let seen = false;
214
+ const lines = text.split("\n");
215
+ for (let index = 0; index < lines.length - 1; index++) {
216
+ const line = lines[index];
217
+ offset += line.length + 1;
218
+ if (fence) {
219
+ if (closes(line, fence)) {
220
+ fence = void 0;
221
+ cut = offset;
222
+ }
223
+ continue;
224
+ }
225
+ const open = FENCE.exec(line);
226
+ if (open) fence = open[1];
227
+ else if (line.trim()) seen = true;
228
+ else if (seen) cut = offset;
229
+ }
230
+ return cut;
231
+ }
232
+ function tailLines(text, count) {
233
+ const lines = text.split("\n");
234
+ if (lines.length <= count) return { text, hidden: 0 };
235
+ const hidden = lines.length - count;
236
+ let fence;
237
+ for (const line of lines.slice(0, hidden)) {
238
+ if (fence) {
239
+ if (closes(line, fence.match(/[`~]+/)[0])) fence = void 0;
240
+ } else if (FENCE.test(line)) fence = line;
241
+ }
242
+ return { text: [...fence ? [fence] : [], ...lines.slice(hidden)].join("\n"), hidden };
243
+ }
244
+ const KEYWORDS = new Set("abstract as async await break case catch class const continue def default defer del do elif else enum export extends false final finally fn for from func function go if impl import in interface is lambda let loop match mod module mut new nil none None null package pass private protected pub public raise return self static struct super switch this throw throws trait true True False try type typeof undefined unless use val var void where while with yield echo then fi esac done local readonly".split(" "));
245
+ const HASH = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "shell", "console", "py", "python", "rb", "ruby", "yaml", "yml", "toml", "ini", "conf", "dockerfile", "make", "makefile", "r", "perl", "pl", "nix"]);
246
+ const SLASH = /(\/\/.*$|\/\*.*?(?:\*\/|$))/;
247
+ const POUND = /((?:^|\s)#.*$)/;
248
+ const COMMON = /("(?:\\.|[^"\\])*"?|'(?:\\.|[^'\\])*'?|`(?:\\.|[^`\\])*`?|\b\d[\d_]*(?:\.\d+)?(?:e[+-]?\d+)?\b|\b0x[\da-f]+\b|[A-Za-z_$][\w$]*)/i;
249
+ const NAMES = { js: "javascript", ts: "typescript", tsx: "tsx", jsx: "jsx", py: "python", rb: "ruby", sh: "shell", rs: "rust", md: "markdown", yml: "yaml" };
250
+ const languageLabel = (lang) => NAMES[lang.toLowerCase()] ?? lang.toLowerCase();
251
+ function highlight(line, lang) {
252
+ const name = lang.toLowerCase();
253
+ if (name === "diff" || name === "patch") {
254
+ const kind = line.startsWith("+") ? "added" : line.startsWith("-") ? "removed" : line.startsWith("@@") ? "meta" : void 0;
255
+ return [{ text: line, ...kind ? { kind } : {} }];
256
+ }
257
+ if (!name || name === "text" || name === "txt" || name === "markdown" || name === "md") return [{ text: line }];
258
+ const comment = HASH.has(name) ? POUND : name === "json" ? void 0 : SLASH;
259
+ const pattern = new RegExp(`${comment ? `${comment.source}|` : ""}${COMMON.source}`, "gi");
260
+ const tokens = [];
261
+ let last = 0;
262
+ for (const match of line.matchAll(pattern)) {
263
+ const text = match[0];
264
+ const at = match.index;
265
+ if (at > last) tokens.push({ text: line.slice(last, at) });
266
+ const first = text.trimStart()[0];
267
+ const kind = comment && match[1] !== void 0 ? "comment" : first === '"' || first === "'" || first === "`" ? "string" : /^\d/.test(text) ? "number" : KEYWORDS.has(text) ? "keyword" : void 0;
268
+ tokens.push({ text, ...kind ? { kind } : {} });
269
+ last = at + text.length;
270
+ }
271
+ if (last < line.length) tokens.push({ text: line.slice(last) });
272
+ return tokens;
273
+ }
274
+ const THEMES = {
275
+ dark: {
276
+ name: "dark",
277
+ accent: "#8ab4f8",
278
+ muted: "gray",
279
+ user: "#c8d3f5",
280
+ success: "#9ece6a",
281
+ error: "#f7768e",
282
+ warning: "#e0af68",
283
+ link: "#7dcfff",
284
+ code: "#e0af68",
285
+ keyword: "#bb9af7",
286
+ string: "#9ece6a",
287
+ number: "#ff9e64",
288
+ added: "#9ece6a",
289
+ removed: "#f7768e",
290
+ addedBg: "#1d3325",
291
+ removedBg: "#3b1f27",
292
+ border: "gray",
293
+ modes: { ask: "gray", "accept-edits": "#bb9af7", "full-auto": "#e0af68" }
294
+ },
295
+ light: {
296
+ name: "light",
297
+ accent: "#1d4ed8",
298
+ muted: "gray",
299
+ user: "#1e293b",
300
+ success: "#15803d",
301
+ error: "#b91c1c",
302
+ warning: "#b45309",
303
+ link: "#0369a1",
304
+ code: "#9a3412",
305
+ keyword: "#7c3aed",
306
+ string: "#15803d",
307
+ number: "#c2410c",
308
+ added: "#166534",
309
+ removed: "#991b1b",
310
+ addedBg: "#dcfce7",
311
+ removedBg: "#fee2e2",
312
+ border: "gray",
313
+ modes: { ask: "gray", "accept-edits": "#7c3aed", "full-auto": "#b45309" }
314
+ }
315
+ };
316
+ function resolveTheme(choice = "auto", env = process.env) {
317
+ if (choice !== "auto") return THEMES[choice];
318
+ const background = Number(env.COLORFGBG?.split(";").at(-1));
319
+ return THEMES[Number.isInteger(background) && (background === 7 || background >= 9) ? "light" : "dark"];
320
+ }
321
+ const ThemeContext = createContext(THEMES.dark);
322
+ const useTheme = () => useContext(ThemeContext);
323
+ const segmenter = new Intl.Segmenter();
324
+ const ANSI = /\x1b\[[0-9;:?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
325
+ const ZERO = /^[\p{Mn}\p{Me}\p{Cf}\p{Cc}]+$/u;
326
+ const EMOJI = new RegExp("\\p{Emoji_Presentation}|\\p{Extended_Pictographic}️|\\p{Regional_Indicator}", "u");
327
+ const WIDE = [
328
+ [4352, 4447],
329
+ [11904, 12350],
330
+ [12353, 13311],
331
+ [13312, 19903],
332
+ [19968, 40959],
333
+ [40960, 42191],
334
+ [44032, 55203],
335
+ [63744, 64255],
336
+ [65072, 65103],
337
+ [65280, 65376],
338
+ [65504, 65510],
339
+ [127744, 128591],
340
+ [129280, 129535],
341
+ [131072, 262141]
342
+ ];
343
+ const stripAnsi = (text) => text.replace(ANSI, "");
344
+ const graphemes = (text) => Array.from(segmenter.segment(text), (part) => part.segment);
345
+ function cellWidth(grapheme) {
346
+ if (!grapheme || ZERO.test(grapheme)) return 0;
347
+ if (EMOJI.test(grapheme)) return 2;
348
+ const code = grapheme.codePointAt(0);
349
+ return WIDE.some(([low, high]) => code >= low && code <= high) ? 2 : 1;
350
+ }
351
+ function width(text) {
352
+ let total = 0;
353
+ for (const grapheme of graphemes(stripAnsi(text))) total += cellWidth(grapheme);
354
+ return total;
355
+ }
356
+ function truncate(text, max) {
357
+ if (max <= 0) return "";
358
+ if (width(text) <= max) return text;
359
+ let out = "";
360
+ let used = 0;
361
+ for (const grapheme of graphemes(text)) {
362
+ const size = cellWidth(grapheme);
363
+ if (used + size > max - 1) break;
364
+ out += grapheme;
365
+ used += size;
366
+ }
367
+ return `${out}…`;
368
+ }
369
+ function truncateStart(text, max) {
370
+ if (width(text) <= max) return text;
371
+ const parts = graphemes(text);
372
+ let out = "";
373
+ let used = 0;
374
+ for (let index = parts.length - 1; index >= 0; index--) {
375
+ const size = cellWidth(parts[index]);
376
+ if (used + size > max - 1) break;
377
+ out = parts[index] + out;
378
+ used += size;
379
+ }
380
+ return `…${out}`;
381
+ }
382
+ const BULLETS = ["•", "◦", "▪"];
383
+ const JUSTIFY = { left: "flex-start", center: "center", right: "flex-end" };
384
+ function spanNodes(spans, theme) {
385
+ return spans.flatMap((span, index) => {
386
+ const node = /* @__PURE__ */ jsx(
387
+ Text,
388
+ {
389
+ bold: span.bold,
390
+ italic: span.italic,
391
+ strikethrough: span.strike,
392
+ underline: !!span.link,
393
+ color: span.code ? theme.code : span.link ? theme.link : void 0,
394
+ children: span.text
395
+ },
396
+ index
397
+ );
398
+ return span.link && span.link !== span.text && !span.link.startsWith("#") ? [node, /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
399
+ " (",
400
+ span.link,
401
+ ")"
402
+ ] }, `${index}-url`)] : [node];
403
+ });
404
+ }
405
+ function Inline({ text, color, dim, bold }) {
406
+ const theme = useTheme();
407
+ return /* @__PURE__ */ jsx(Text, { color, dimColor: dim, bold, wrap: "wrap", children: spanNodes(parseInline(text), theme) });
408
+ }
409
+ function tokenColor$1(kind, theme) {
410
+ if (!kind) return void 0;
411
+ if (kind === "comment" || kind === "meta") return theme.muted;
412
+ if (kind === "added") return theme.added;
413
+ if (kind === "removed") return theme.removed;
414
+ return theme[kind];
415
+ }
416
+ function Code({ lang, lines, open }) {
417
+ const theme = useTheme();
418
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
419
+ /* @__PURE__ */ jsxs(Text, { color: theme.border, children: [
420
+ "╭─",
421
+ lang ? /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
422
+ " ",
423
+ languageLabel(lang)
424
+ ] }) : null
425
+ ] }),
426
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", borderStyle: "single", borderTop: false, borderRight: false, borderBottom: false, borderColor: theme.border, paddingLeft: 1, children: (lines.length ? lines : [""]).map((line, index) => /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: line ? highlight(line, lang).map((token, at) => /* @__PURE__ */ jsx(Text, { color: tokenColor$1(token.kind, theme), italic: token.kind === "comment", children: token.text }, at)) : " " }, index)) }),
427
+ open ? null : /* @__PURE__ */ jsx(Text, { color: theme.border, children: "╰─" })
428
+ ] });
429
+ }
430
+ function List({ items }) {
431
+ const theme = useTheme();
432
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
433
+ const marker = item.checked !== void 0 ? item.checked ? "☒" : "☐" : item.marker === "-" ? BULLETS[item.depth % BULLETS.length] : item.marker;
434
+ return /* @__PURE__ */ jsxs(Box, { marginLeft: item.depth * 2, children: [
435
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: width(marker) + 1, children: /* @__PURE__ */ jsx(Text, { color: theme.accent, children: marker }) }),
436
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx(Inline, { text: item.text, dim: item.checked }) })
437
+ ] }, index);
438
+ }) });
439
+ }
440
+ function fitColumns(natural, available) {
441
+ const widths = natural.map((value) => Math.max(1, value));
442
+ if (widths.length * 3 > available) return void 0;
443
+ while (widths.reduce((sum, value) => sum + value, 0) > available) {
444
+ const widest = widths.indexOf(Math.max(...widths));
445
+ widths[widest]--;
446
+ }
447
+ return widths;
448
+ }
449
+ function Table({ head, align, rows, width: width$1 }) {
450
+ const theme = useTheme();
451
+ const count = Math.max(head.length, ...rows.map((row2) => row2.length));
452
+ const grid = [head, ...rows].map((row2) => Array.from({ length: count }, (_, index) => row2[index] ?? ""));
453
+ const natural = Array.from({ length: count }, (_, column) => Math.max(...grid.map((row2) => width(plainText(row2[column])))));
454
+ const widths = fitColumns(natural, width$1 - count * 3 - 1);
455
+ if (!widths) return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: rows.map((row2, index) => /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: index ? 1 : 0, children: head.map((name, column) => /* @__PURE__ */ jsx(Inline, { text: `**${name}:** ${row2[column] ?? ""}` }, column)) }, index)) });
456
+ const line = (left2, middle2, right2) => /* @__PURE__ */ jsxs(Text, { color: theme.border, children: [
457
+ left2,
458
+ widths.map((size) => "─".repeat(size + 2)).join(middle2),
459
+ right2
460
+ ] });
461
+ const row = (values, header) => /* @__PURE__ */ jsx(Box, { children: values.map((value, column) => /* @__PURE__ */ jsx(
462
+ Box,
463
+ {
464
+ width: widths[column] + 3 + (column === count - 1 ? 1 : 0),
465
+ borderStyle: "single",
466
+ borderTop: false,
467
+ borderBottom: false,
468
+ borderRight: column === count - 1,
469
+ borderColor: theme.border,
470
+ paddingX: 1,
471
+ justifyContent: JUSTIFY[align[column] ?? "left"],
472
+ children: /* @__PURE__ */ jsx(Inline, { text: value, bold: header })
473
+ },
474
+ column
475
+ )) });
476
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
477
+ line("┌", "┬", "┐"),
478
+ row(grid[0], true),
479
+ line("├", "┼", "┤"),
480
+ grid.slice(1).map((values, index) => /* @__PURE__ */ jsx(Box, { children: row(values, false) }, index)),
481
+ line("└", "┴", "┘")
482
+ ] });
483
+ }
484
+ function BlockView({ block, width: width2, dim }) {
485
+ const theme = useTheme();
486
+ switch (block.type) {
487
+ case "heading":
488
+ return /* @__PURE__ */ jsx(Text, { bold: true, underline: block.level === 1, color: block.level <= 2 ? theme.accent : void 0, wrap: "wrap", children: spanNodes(parseInline(block.text), theme) });
489
+ case "paragraph":
490
+ return /* @__PURE__ */ jsx(Inline, { text: block.text, dim });
491
+ case "code":
492
+ return /* @__PURE__ */ jsx(Code, { ...block });
493
+ case "list":
494
+ return /* @__PURE__ */ jsx(List, { items: block.items });
495
+ case "rule":
496
+ return /* @__PURE__ */ jsx(Text, { color: theme.border, children: "─".repeat(Math.max(1, Math.min(width2, 80))) });
497
+ case "table":
498
+ return /* @__PURE__ */ jsx(Table, { ...block, width: width2 });
499
+ case "quote":
500
+ return /* @__PURE__ */ jsx(Box, { borderStyle: "bold", borderTop: false, borderRight: false, borderBottom: false, borderColor: theme.border, paddingLeft: 1, flexDirection: "column", children: block.blocks.map((inner, index) => /* @__PURE__ */ jsx(Box, { marginTop: index ? 1 : 0, children: /* @__PURE__ */ jsx(BlockView, { block: inner, width: width2 - 2, dim: true }) }, index)) });
501
+ }
502
+ }
503
+ function Markdown({ text, width: width2, tail, dim }) {
504
+ const theme = useTheme();
505
+ const cut = tail ? tailLines(text, tail) : { text, hidden: 0 };
506
+ const blocks = parseBlocks(cut.text);
507
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: width2, children: [
508
+ cut.hidden ? /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
509
+ "… ",
510
+ cut.hidden,
511
+ " lines above"
512
+ ] }) : null,
513
+ blocks.map((block, index) => /* @__PURE__ */ jsx(Box, { marginTop: index ? 1 : 0, flexDirection: "column", children: /* @__PURE__ */ jsx(BlockView, { block, width: width2, dim }) }, index))
514
+ ] });
515
+ }
516
+ function markdownToAnsi(text, columns = 80, theme = THEMES.dark) {
517
+ return renderToString(/* @__PURE__ */ jsx(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx(Markdown, { text, width: columns }) }), { columns });
518
+ }
519
+ const MAX_CELLS = 4e6;
520
+ const split = (text) => {
521
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
522
+ if (lines.at(-1) === "") lines.pop();
523
+ return lines;
524
+ };
525
+ function middle(a, b) {
526
+ if (!a.length || !b.length || a.length * b.length > MAX_CELLS)
527
+ return [...a.map(() => "-"), ...b.map(() => "+")];
528
+ const columns = b.length + 1;
529
+ const table = new Uint32Array((a.length + 1) * columns);
530
+ for (let i2 = a.length - 1; i2 >= 0; i2--)
531
+ for (let j2 = b.length - 1; j2 >= 0; j2--)
532
+ table[i2 * columns + j2] = a[i2] === b[j2] ? table[(i2 + 1) * columns + j2 + 1] + 1 : Math.max(table[(i2 + 1) * columns + j2], table[i2 * columns + j2 + 1]);
533
+ const ops = [];
534
+ let i = 0;
535
+ let j = 0;
536
+ while (i < a.length && j < b.length) {
537
+ if (a[i] === b[j]) {
538
+ ops.push("=");
539
+ i++;
540
+ j++;
541
+ } else if (table[(i + 1) * columns + j] >= table[i * columns + j + 1]) {
542
+ ops.push("-");
543
+ i++;
544
+ } else {
545
+ ops.push("+");
546
+ j++;
547
+ }
548
+ }
549
+ while (i++ < a.length) ops.push("-");
550
+ while (j++ < b.length) ops.push("+");
551
+ return ops;
552
+ }
553
+ function diffLines(oldText, newText, context = 3) {
554
+ const a = oldText ? split(oldText) : [];
555
+ const b = split(newText);
556
+ let head = 0;
557
+ while (head < a.length && head < b.length && a[head] === b[head]) head++;
558
+ let tail = 0;
559
+ while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) tail++;
560
+ const ops = [...Array(head).fill("="), ...middle(a.slice(head, a.length - tail), b.slice(head, b.length - tail)), ...Array(tail).fill("=")];
561
+ const all = [];
562
+ let oldNo = 0;
563
+ let newNo = 0;
564
+ for (const op of ops) {
565
+ if (op === "=") all.push({ type: "context", text: b[newNo], oldNo: ++oldNo, newNo: ++newNo });
566
+ else if (op === "-") all.push({ type: "removed", text: a[oldNo], oldNo: ++oldNo });
567
+ else all.push({ type: "added", text: b[newNo], newNo: ++newNo });
568
+ }
569
+ const near = all.map(() => false);
570
+ all.forEach((line, index) => {
571
+ if (line.type === "context") return;
572
+ for (let k = Math.max(0, index - context); k <= Math.min(all.length - 1, index + context); k++) near[k] = true;
573
+ });
574
+ const lines = [];
575
+ let skipped = 0;
576
+ all.forEach((line, index) => {
577
+ if (near[index]) {
578
+ if (skipped) lines.push({ type: "gap", count: skipped });
579
+ skipped = 0;
580
+ lines.push(line);
581
+ } else skipped++;
582
+ });
583
+ if (skipped && lines.length) lines.push({ type: "gap", count: skipped });
584
+ return { lines, added: all.filter((line) => line.type === "added").length, removed: all.filter((line) => line.type === "removed").length };
585
+ }
586
+ const ICONS = { read: "→", edit: "✎", delete: "✗", move: "⇄", search: "⌕", execute: "$", think: "∴", fetch: "⇣", other: "◆" };
587
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
588
+ const SUFFIX = { pending: "waiting for approval", failed: "failed", cancelled: "interrupted" };
589
+ const relative = (cwd, text) => cwd === "/" ? text : text.split(`${cwd}/`).join("");
590
+ function Gutter({ children }) {
591
+ const theme = useTheme();
592
+ return /* @__PURE__ */ jsxs(Box, { children: [
593
+ /* @__PURE__ */ jsx(Box, { width: 5, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: " ⎿ " }) }),
594
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children })
595
+ ] });
596
+ }
597
+ function More({ count, unit = "line" }) {
598
+ const theme = useTheme();
599
+ return count > 0 ? /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
600
+ "… +",
601
+ count,
602
+ " ",
603
+ unit,
604
+ count === 1 ? "" : "s",
605
+ " (ctrl+o to expand)"
606
+ ] }) : null;
607
+ }
608
+ const extension = (path) => /\.([a-z0-9]+)$/i.exec(path)?.[1] ?? "";
609
+ function DiffView({ diff, cwd, limit }) {
610
+ const theme = useTheme();
611
+ const { lines, added, removed } = useMemo(() => diffLines(diff.oldText, diff.newText), [diff]);
612
+ const path = relative(cwd, diff.path);
613
+ const numbers = lines.flatMap((line) => line.type === "gap" ? [] : [line.newNo ?? line.oldNo ?? 0]);
614
+ const size = String(Math.max(1, ...numbers)).length;
615
+ const lang = extension(path);
616
+ const summary = diff.oldText === null ? `Created ${path} with ${added} ${added === 1 ? "line" : "lines"}` : `Updated ${path}: ${added} added, ${removed} removed`;
617
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
618
+ /* @__PURE__ */ jsx(Text, { color: theme.muted, children: summary }),
619
+ lines.slice(0, limit).map((line, index) => {
620
+ if (line.type === "gap") return /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
621
+ " ".repeat(size),
622
+ " ⋮ ",
623
+ line.count,
624
+ " unchanged"
625
+ ] }, index);
626
+ const sign = line.type === "added" ? "+" : line.type === "removed" ? "-" : " ";
627
+ const background = line.type === "added" ? theme.addedBg : line.type === "removed" ? theme.removedBg : void 0;
628
+ const color = line.type === "added" ? theme.added : line.type === "removed" ? theme.removed : theme.muted;
629
+ return /* @__PURE__ */ jsxs(Box, { backgroundColor: background, children: [
630
+ /* @__PURE__ */ jsx(Box, { width: size + 1, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: String(line.type === "removed" ? line.oldNo : line.newNo).padStart(size) }) }),
631
+ /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color, bold: true, children: sign }) }),
632
+ /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: line.text ? highlight(line.text, lang).map((token, at) => /* @__PURE__ */ jsx(Text, { color: tokenColor(token.kind, theme), children: token.text }, at)) : " " })
633
+ ] }, index);
634
+ }),
635
+ /* @__PURE__ */ jsx(More, { count: lines.length - limit })
636
+ ] });
637
+ }
638
+ function tokenColor(kind, theme) {
639
+ return kind === "keyword" ? theme.keyword : kind === "string" ? theme.string : kind === "number" ? theme.number : kind === "comment" ? theme.muted : void 0;
640
+ }
641
+ function Output({ tool: tool2, expanded }) {
642
+ const theme = useTheme();
643
+ const lines = (tool2.output ?? "").replace(/\s+$/, "").split("\n");
644
+ if (!tool2.output?.trim()) return tool2.status === "in_progress" && tool2.terminal ? /* @__PURE__ */ jsx(Text, { color: theme.muted, children: "Running…" }) : null;
645
+ if (tool2.status === "completed" && tool2.kind === "read" && !expanded)
646
+ return /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
647
+ lines.length,
648
+ " ",
649
+ lines.length === 1 ? "line" : "lines"
650
+ ] });
651
+ const limit = expanded ? 200 : tool2.status === "failed" ? 8 : 5;
652
+ const tail = tool2.kind === "execute" && tool2.status !== "failed";
653
+ const shown = tail ? lines.slice(-limit) : lines.slice(0, limit);
654
+ const hidden = lines.length - shown.length;
655
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
656
+ tail && hidden > 0 ? /* @__PURE__ */ jsx(More, { count: hidden }) : null,
657
+ shown.map((line, index) => /* @__PURE__ */ jsx(Text, { wrap: "wrap", color: tool2.status === "failed" ? theme.error : void 0, children: line || " " }, index)),
658
+ !tail && hidden > 0 ? /* @__PURE__ */ jsx(More, { count: hidden }) : null
659
+ ] });
660
+ }
661
+ function Icon({ tool: tool2, live }) {
662
+ const theme = useTheme();
663
+ const running = live && tool2.status === "in_progress";
664
+ const { frame } = useAnimation({ interval: 80, isActive: running });
665
+ const color = { completed: theme.success, failed: theme.error, cancelled: theme.muted, pending: theme.warning }[tool2.status] ?? theme.accent;
666
+ return /* @__PURE__ */ jsx(Text, { color, children: running ? SPINNER[frame % SPINNER.length] : ICONS[tool2.kind] ?? ICONS.other });
667
+ }
668
+ function ToolCard({ tool: view, live, expanded, cwd, awaiting }) {
669
+ const theme = useTheme();
670
+ const tool2 = awaiting && view.status === "in_progress" ? { ...view, status: "pending" } : view;
671
+ if (tool2.kind === "think" && tool2.title === "Update plan" && tool2.status !== "failed") return null;
672
+ const title = tool2.kind === "execute" ? tool2.title.replace(/^\$\s*/, "") : relative(cwd, tool2.title);
673
+ const suffix = SUFFIX[tool2.status];
674
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
675
+ /* @__PURE__ */ jsxs(Box, { children: [
676
+ /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx(Icon, { tool: tool2, live }) }),
677
+ /* @__PURE__ */ jsxs(Text, { wrap: "wrap", children: [
678
+ /* @__PURE__ */ jsx(Text, { bold: true, children: title }),
679
+ suffix ? /* @__PURE__ */ jsxs(Text, { color: tool2.status === "failed" ? theme.error : theme.muted, children: [
680
+ " · ",
681
+ suffix
682
+ ] }) : null
683
+ ] })
684
+ ] }),
685
+ tool2.diffs.length || tool2.output?.trim() || tool2.terminal && tool2.status === "in_progress" ? /* @__PURE__ */ jsxs(Gutter, { children: [
686
+ tool2.diffs.map((diff, index) => /* @__PURE__ */ jsx(DiffView, { diff, cwd, limit: expanded ? 400 : 14 }, index)),
687
+ tool2.diffs.length && tool2.status === "completed" ? null : /* @__PURE__ */ jsx(Output, { tool: tool2, expanded })
688
+ ] }) : null
689
+ ] });
690
+ }
691
+ const home$1 = (path) => {
692
+ const user = process.env.HOME;
693
+ return user && (path === user || path.startsWith(`${user}/`)) ? `~${path.slice(user.length)}` : path;
694
+ };
695
+ function Banner({ width: width2, cwd, version }) {
696
+ const theme = useTheme();
697
+ const size = Math.min(width2, 64);
698
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, width: size, children: [
699
+ /* @__PURE__ */ jsxs(Text, { children: [
700
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, children: "✻" }),
701
+ " ",
702
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Oxbit Agent" }),
703
+ version ? /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
704
+ " ",
705
+ version
706
+ ] }) : null
707
+ ] }),
708
+ /* @__PURE__ */ jsx(Text, { color: theme.muted, children: truncateStart(home$1(cwd), size - 4) }),
709
+ /* @__PURE__ */ jsx(Text, { color: theme.muted, children: "/help for help · shift+tab to switch modes" })
710
+ ] });
711
+ }
712
+ const PLAN_ICONS = { completed: "☒", in_progress: "▶", pending: "☐" };
713
+ function Plan({ entries, expanded }) {
714
+ const theme = useTheme();
715
+ const done = entries.filter((entry) => entry.status === "completed").length;
716
+ const limit = expanded ? entries.length : 12;
717
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
718
+ /* @__PURE__ */ jsxs(Text, { children: [
719
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, children: "☰" }),
720
+ " ",
721
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Plan" }),
722
+ /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
723
+ " · ",
724
+ done,
725
+ "/",
726
+ entries.length,
727
+ " done"
728
+ ] })
729
+ ] }),
730
+ /* @__PURE__ */ jsxs(Gutter, { children: [
731
+ entries.slice(0, limit).map((entry, index) => {
732
+ const active = entry.status === "in_progress";
733
+ const finished = entry.status === "completed";
734
+ return /* @__PURE__ */ jsxs(Box, { children: [
735
+ /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: active ? theme.accent : finished ? theme.success : theme.muted, children: PLAN_ICONS[entry.status] ?? "☐" }) }),
736
+ /* @__PURE__ */ jsx(Text, { wrap: "wrap", bold: active, dimColor: finished, strikethrough: finished, children: entry.content })
737
+ ] }, index);
738
+ }),
739
+ /* @__PURE__ */ jsx(More, { count: entries.length - limit, unit: "step" })
740
+ ] })
741
+ ] });
742
+ }
743
+ function Thought({ text, live, expanded, width: width2 }) {
744
+ const theme = useTheme();
745
+ const lines = text.trim().split("\n").filter((line) => line.trim());
746
+ const shown = expanded ? lines : live ? lines.slice(-3) : [];
747
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, width: width2, children: [
748
+ /* @__PURE__ */ jsxs(Text, { color: theme.muted, italic: true, children: [
749
+ "∴ ",
750
+ live ? "Thinking…" : "Thought",
751
+ !expanded && !live && lines.length ? " (ctrl+o to expand)" : ""
752
+ ] }),
753
+ shown.length ? /* @__PURE__ */ jsx(Gutter, { children: shown.map((line, index) => /* @__PURE__ */ jsx(Text, { color: theme.muted, italic: true, wrap: "wrap", children: line }, index)) }) : null
754
+ ] });
755
+ }
756
+ function ItemView({ item, live = false, context }) {
757
+ const theme = useTheme();
758
+ const { width: width2, rows, expanded, cwd } = context;
759
+ switch (item.type) {
760
+ case "banner":
761
+ return /* @__PURE__ */ jsx(Banner, { ...context });
762
+ case "user": {
763
+ const lines = item.text.split("\n");
764
+ const shown = item.continuation && !expanded ? lines.slice(0, 3) : lines;
765
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 1, width: width2, children: [
766
+ /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: item.continuation ? theme.muted : theme.accent, bold: true, children: item.continuation ? "↻" : "›" }) }),
767
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: [
768
+ /* @__PURE__ */ jsx(Text, { color: item.continuation ? theme.muted : theme.user, wrap: "wrap", children: shown.join("\n") }),
769
+ /* @__PURE__ */ jsx(More, { count: lines.length - shown.length })
770
+ ] })
771
+ ] });
772
+ }
773
+ case "assistant": {
774
+ const text = item.text.replace(/^\n+|\s+$/g, "");
775
+ if (!text) return null;
776
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 1, width: width2, children: [
777
+ /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: theme.accent, children: item.part ? " " : "●" }) }),
778
+ /* @__PURE__ */ jsx(Markdown, { text, width: Math.max(10, width2 - 2), tail: live ? Math.max(6, rows - 14) : void 0 })
779
+ ] });
780
+ }
781
+ case "thought":
782
+ return item.text.trim() ? /* @__PURE__ */ jsx(Thought, { text: item.text, live: live && !item.done, expanded, width: width2 }) : null;
783
+ case "tool":
784
+ return /* @__PURE__ */ jsx(ToolCard, { tool: item.tool, live, expanded, cwd, awaiting: context.awaiting === item.tool.toolCallId });
785
+ case "plan":
786
+ return /* @__PURE__ */ jsx(Plan, { entries: item.entries, expanded });
787
+ case "notice":
788
+ return item.tone === "error" ? /* @__PURE__ */ jsxs(Box, { marginTop: 1, width: width2, children: [
789
+ /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: theme.error, children: "✗" }) }),
790
+ /* @__PURE__ */ jsx(Text, { color: theme.error, wrap: "wrap", children: item.text })
791
+ ] }) : /* @__PURE__ */ jsx(Box, { width: width2, children: /* @__PURE__ */ jsx(Gutter, { children: /* @__PURE__ */ jsx(Text, { color: item.tone === "warn" ? theme.warning : theme.muted, wrap: "wrap", children: item.text }) }) });
792
+ case "divider": {
793
+ const label = ` ${item.text} `;
794
+ return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: theme.muted, wrap: "truncate-end", children: [
795
+ "──",
796
+ label,
797
+ "─".repeat(Math.max(0, width2 - label.length - 2))
798
+ ] }) });
799
+ }
800
+ }
801
+ }
802
+ const emptyTranscript = () => ({ flushed: [{ id: 0, type: "banner" }], live: [], next: 1 });
803
+ const STREAMING = /* @__PURE__ */ new Set(["assistant", "thought", "plan"]);
804
+ const FINAL = /* @__PURE__ */ new Set(["banner", "user", "notice", "divider"]);
805
+ const isDone = (item) => FINAL.has(item.type) || !!item.done;
806
+ const textOf = (content) => {
807
+ const block = content;
808
+ if (block?.type === "text" && typeof block.text === "string") return block.text;
809
+ return block?.type === "image" ? "[image]" : block?.type === "resource_link" ? `[${block.name ?? block.uri}]` : "";
810
+ };
811
+ const outputOf = (value) => value === void 0 || value === null ? void 0 : typeof value === "string" ? value : JSON.stringify(value, null, 2);
812
+ function flush(state) {
813
+ let { flushed, live, next } = state;
814
+ let count = 0;
815
+ while (count < live.length && isDone(live[count])) count++;
816
+ if (count) {
817
+ flushed = [...flushed, ...live.slice(0, count)];
818
+ live = live.slice(count);
819
+ }
820
+ const head = live[0];
821
+ if (head?.type === "assistant" && !head.done) {
822
+ const cut = stableLength(head.text);
823
+ if (cut > 0 && head.text.slice(cut).trim()) {
824
+ flushed = [...flushed, { ...head, text: head.text.slice(0, cut), done: true }];
825
+ live = [{ ...head, id: next++, text: head.text.slice(cut), part: true }, ...live.slice(1)];
826
+ }
827
+ }
828
+ return flushed === state.flushed && live === state.live ? state : { flushed, live, next };
829
+ }
830
+ function append(state, item) {
831
+ const live = state.live.map((entry) => STREAMING.has(entry.type) && !isDone(entry) ? { ...entry, done: true } : entry);
832
+ return flush({ ...state, live: [...live, { ...item, id: state.next }], next: state.next + 1 });
833
+ }
834
+ const replace = (state, index, item) => flush({ ...state, live: state.live.map((entry, at) => at === index ? item : entry) });
835
+ function stream(state, type, text) {
836
+ if (!text) return state;
837
+ const index = state.live.length - 1;
838
+ const last = state.live[index];
839
+ if (last?.type === type && !last.done) return replace(state, index, { ...last, text: last.text + text });
840
+ return append(state, { type, text: type === "assistant" ? text.replace(/^\n+/, "") : text });
841
+ }
842
+ function tool(state, update) {
843
+ const index = state.live.findIndex((item) => item.type === "tool" && item.tool.toolCallId === update.toolCallId);
844
+ const current = index >= 0 ? state.live[index].tool : void 0;
845
+ const content = Array.isArray(update.content) ? update.content : void 0;
846
+ const diffs = content?.filter((block) => block.type === "diff").map((block) => ({ path: String(block.path ?? ""), oldText: block.oldText ?? null, newText: String(block.newText ?? "") }));
847
+ const text = content?.filter((block) => block.type === "content").map((block) => textOf(block.content)).join("\n");
848
+ const next = {
849
+ toolCallId: String(update.toolCallId ?? ""),
850
+ title: typeof update.title === "string" ? update.title : current?.title ?? "Tool",
851
+ kind: typeof update.kind === "string" ? update.kind : current?.kind ?? "other",
852
+ status: typeof update.status === "string" ? update.status : current?.status ?? "pending",
853
+ rawInput: "rawInput" in update ? update.rawInput : current?.rawInput,
854
+ output: outputOf(update.rawOutput) ?? (text || current?.output),
855
+ diffs: diffs?.length ? diffs : current?.diffs ?? [],
856
+ terminal: current?.terminal || !!content?.some((block) => block.type === "terminal")
857
+ };
858
+ const done = next.status === "completed" || next.status === "failed";
859
+ if (index < 0 && state.flushed.slice(-50).some((item) => item.type === "tool" && item.tool.toolCallId === next.toolCallId)) return state;
860
+ if (index < 0) return append(state, { type: "tool", tool: next, done });
861
+ return replace(state, index, { id: state.live[index].id, type: "tool", tool: next, done });
862
+ }
863
+ function applyUpdate(state, update) {
864
+ switch (update.sessionUpdate) {
865
+ case "agent_message_chunk":
866
+ return stream(state, "assistant", textOf(update.content));
867
+ case "agent_thought_chunk":
868
+ return stream(state, "thought", textOf(update.content));
869
+ case "user_message_chunk": {
870
+ const text = textOf(update.content);
871
+ return text ? append(state, { type: "user", text, continuation: !!update._meta?.oxbit?.continuation }) : state;
872
+ }
873
+ case "tool_call":
874
+ case "tool_call_update":
875
+ return tool(state, update);
876
+ case "plan": {
877
+ const entries = Array.isArray(update.entries) ? update.entries : [];
878
+ const index = state.live.length - 1;
879
+ const last = state.live[index];
880
+ if (last?.type === "plan" && !last.done) return replace(state, index, { ...last, entries });
881
+ return append(state, { type: "plan", entries });
882
+ }
883
+ }
884
+ return state;
885
+ }
886
+ function endTurn(state) {
887
+ const live = state.live.map((item) => item.type === "tool" && !item.done ? { ...item, done: true, tool: { ...item.tool, status: item.tool.status === "failed" ? "failed" : "cancelled" } } : FINAL.has(item.type) ? item : { ...item, done: true });
888
+ return flush({ ...state, live });
889
+ }
890
+ function expandable(item) {
891
+ if (item.type === "thought") return !!item.text.trim();
892
+ if (item.type === "tool") return !!item.tool.output?.trim() || item.tool.diffs.length > 0;
893
+ return item.type === "user" && !!item.continuation && item.text.split("\n").length > 3;
894
+ }
895
+ function Details({ id, transcript, context }) {
896
+ const theme = useTheme();
897
+ const items = [...transcript.flushed, ...transcript.live].filter(expandable);
898
+ const at = Math.max(0, items.findIndex((entry) => entry.id === id));
899
+ const item = items[at];
900
+ const height = Math.max(4, context.rows - 9);
901
+ const [scroll, setScroll] = useState(0);
902
+ const [total, setTotal] = useState(0);
903
+ const content = useRef(null);
904
+ useEffect(() => {
905
+ if (content.current) setTotal(measureElement(content.current).height);
906
+ });
907
+ const max = Math.max(0, total - height);
908
+ const top = Math.min(scroll, max);
909
+ useInput((_input, key) => {
910
+ const page = height - 1;
911
+ const step = key.upArrow ? -1 : key.downArrow ? 1 : key.pageUp ? -page : key.pageDown ? page : key.home ? -total : key.end ? total : 0;
912
+ if (step) setScroll(Math.min(max, Math.max(0, top + step)));
913
+ });
914
+ if (!item) return null;
915
+ const live = transcript.live.includes(item);
916
+ const position = max ? ` · lines ${top + 1}-${Math.min(total, top + height)} of ${total}` : "";
917
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, width: context.width, marginTop: 1, children: [
918
+ /* @__PURE__ */ jsxs(Text, { bold: true, wrap: "truncate-end", children: [
919
+ "Details ",
920
+ /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
921
+ at + 1,
922
+ "/",
923
+ items.length,
924
+ position
925
+ ] })
926
+ ] }),
927
+ /* @__PURE__ */ jsx(Box, { height: total ? Math.min(height, total) : height, overflow: "hidden", flexDirection: "column", children: /* @__PURE__ */ jsx(Box, { ref: content, flexDirection: "column", flexShrink: 0, marginTop: -top, children: /* @__PURE__ */ jsx(ItemView, { item, live, context: { ...context, width: context.width - 4, expanded: true } }) }) }),
928
+ /* @__PURE__ */ jsxs(Text, { color: theme.muted, wrap: "truncate-end", children: [
929
+ max ? "↑↓ pgup pgdn scroll · " : "",
930
+ "←→ earlier/later · esc close"
931
+ ] })
932
+ ] });
933
+ }
934
+ function commandToken({ text, cursor }) {
935
+ if (!text.startsWith("/")) return void 0;
936
+ const space = text.search(/\s/);
937
+ const end2 = space < 0 ? text.length : space;
938
+ return cursor <= end2 ? { query: text.slice(1, end2), start: 0, end: end2 } : void 0;
939
+ }
940
+ function fileToken({ text, cursor }) {
941
+ const match = /(?:^|\s)@([^\s@]*)$/.exec(text.slice(0, cursor));
942
+ if (!match) return void 0;
943
+ let end2 = cursor;
944
+ while (end2 < text.length && !/\s/.test(text[end2])) end2++;
945
+ return { query: match[1], start: cursor - match[1].length - 1, end: end2 };
946
+ }
947
+ function score(candidate, query) {
948
+ if (!query) return 0;
949
+ const text = candidate.toLowerCase();
950
+ const wanted = query.toLowerCase();
951
+ if (text.startsWith(wanted)) return 3e3 - text.length;
952
+ const at = text.indexOf(wanted);
953
+ if (at >= 0) return 2e3 - at;
954
+ let position = -1;
955
+ let gaps = 0;
956
+ for (const char of wanted) {
957
+ const next = text.indexOf(char, position + 1);
958
+ if (next < 0) return void 0;
959
+ gaps += next - position - 1;
960
+ position = next;
961
+ }
962
+ return 1e3 - gaps;
963
+ }
964
+ function rank(items, key, query, limit = 50) {
965
+ return items.map((item) => ({ item, score: score(key(item), query) })).filter((entry) => entry.score !== void 0).sort((a, b) => b.score - a.score).slice(0, limit).map((entry) => entry.item);
966
+ }
967
+ function commandItems(commands, query) {
968
+ return rank(commands, (command) => command.name, query).map((command) => ({
969
+ value: `/${command.name}`,
970
+ label: `/${command.name}${command.hint ? ` ${command.hint}` : ""}`,
971
+ detail: command.description
972
+ }));
973
+ }
974
+ function fileItems(paths, query) {
975
+ const base = (path) => path.replace(/\/$/, "").split("/").pop() ?? path;
976
+ const scored = rank(paths, (path) => query.includes("/") ? path : base(path), query, 50);
977
+ const extra = query.includes("/") ? [] : rank(paths.filter((path) => !scored.includes(path)), (path) => path, query, 50 - scored.length);
978
+ return [...scored, ...extra].map((path) => ({ value: `@${path}`, label: path }));
979
+ }
980
+ function accept(draft, completion, item) {
981
+ const value = item.value.endsWith("/") ? item.value : `${item.value} `;
982
+ const rest = draft.text.slice(completion.end).replace(/^ /, "");
983
+ return { text: draft.text.slice(0, completion.start) + value + rest, cursor: completion.start + value.length };
984
+ }
985
+ const emptyDraft = { text: "", cursor: 0 };
986
+ const draftOf = (text) => ({ text, cursor: text.length });
987
+ const lineStartOf = (text, at) => text.lastIndexOf("\n", at - 1) + 1;
988
+ const lineEndOf = (text, at) => {
989
+ const end2 = text.indexOf("\n", at);
990
+ return end2 < 0 ? text.length : end2;
991
+ };
992
+ const previous = (text, at) => at - (graphemes(text.slice(Math.max(0, at - 16), at)).at(-1)?.length ?? 0);
993
+ const following = (text, at) => at + (graphemes(text.slice(at, at + 16))[0]?.length ?? 0);
994
+ const insert = ({ text, cursor }, value) => ({ text: text.slice(0, cursor) + value + text.slice(cursor), cursor: cursor + value.length });
995
+ const backspace = ({ text, cursor }) => {
996
+ const at = previous(text, cursor);
997
+ return { text: text.slice(0, at) + text.slice(cursor), cursor: at };
998
+ };
999
+ const deleteForward = ({ text, cursor }) => ({ text: text.slice(0, cursor) + text.slice(following(text, cursor)), cursor });
1000
+ const left = ({ text, cursor }) => ({ text, cursor: previous(text, cursor) });
1001
+ const right = ({ text, cursor }) => ({ text, cursor: following(text, cursor) });
1002
+ const home = ({ text, cursor }) => ({ text, cursor: lineStartOf(text, cursor) });
1003
+ const end = ({ text, cursor }) => ({ text, cursor: lineEndOf(text, cursor) });
1004
+ function wordLeft({ text, cursor }) {
1005
+ let at = cursor;
1006
+ while (at > 0 && /\s/.test(text[at - 1])) at--;
1007
+ while (at > 0 && !/\s/.test(text[at - 1])) at--;
1008
+ return { text, cursor: at };
1009
+ }
1010
+ function wordRight({ text, cursor }) {
1011
+ let at = cursor;
1012
+ while (at < text.length && /\s/.test(text[at])) at++;
1013
+ while (at < text.length && !/\s/.test(text[at])) at++;
1014
+ return { text, cursor: at };
1015
+ }
1016
+ const deleteWordLeft = (draft) => {
1017
+ const at = wordLeft(draft).cursor;
1018
+ return { text: draft.text.slice(0, at) + draft.text.slice(draft.cursor), cursor: at };
1019
+ };
1020
+ const killLineLeft = ({ text, cursor }) => {
1021
+ const start = lineStartOf(text, cursor);
1022
+ const at = start === cursor && cursor > 0 ? cursor - 1 : start;
1023
+ return { text: text.slice(0, at) + text.slice(cursor), cursor: at };
1024
+ };
1025
+ const killLineRight = ({ text, cursor }) => {
1026
+ const stop = lineEndOf(text, cursor);
1027
+ return { text: text.slice(0, cursor) + text.slice(stop === cursor ? cursor + 1 : stop), cursor };
1028
+ };
1029
+ function vertical({ text, cursor }, direction) {
1030
+ const start = lineStartOf(text, cursor);
1031
+ const column = graphemes(text.slice(start, cursor)).length;
1032
+ const target = direction < 0 ? start - 1 : lineEndOf(text, cursor) + 1;
1033
+ if (target < 0 || target > text.length || direction > 0 && lineEndOf(text, cursor) === text.length) return void 0;
1034
+ const lineStart = lineStartOf(text, target);
1035
+ const line = graphemes(text.slice(lineStart, lineEndOf(text, lineStart)));
1036
+ return { text, cursor: lineStart + line.slice(0, column).join("").length };
1037
+ }
1038
+ function cursorLines({ text, cursor }) {
1039
+ const lines = text.split("\n");
1040
+ let offset = 0;
1041
+ return lines.map((line) => {
1042
+ const start = offset;
1043
+ offset += line.length + 1;
1044
+ if (cursor < start || cursor > start + line.length) return { before: line };
1045
+ const at = cursor - start;
1046
+ const under = graphemes(line.slice(at, at + 16))[0] ?? "";
1047
+ return { before: line.slice(0, at), under: under || " ", after: line.slice(at + under.length) };
1048
+ });
1049
+ }
1050
+ const DECISIONS = ["once", "session", "deny"];
1051
+ const option = (options, kinds, ids) => options.find((item) => kinds.includes(item.kind)) ?? options.find((item) => ids.includes(item.optionId));
1052
+ const selected = (optionId) => ({ outcome: { outcome: "selected", optionId } });
1053
+ const cancelled = { outcome: { outcome: "cancelled" } };
1054
+ function commandOf(request) {
1055
+ const input = request.toolCall.rawInput;
1056
+ return typeof input?.command === "string" ? input.command : request.toolCall.title ?? "";
1057
+ }
1058
+ function ruleKey(request) {
1059
+ const kind = request.toolCall.kind ?? "other";
1060
+ if (kind !== "execute") return kind;
1061
+ return `execute:${commandOf(request).trim().split(/\s+/)[0] ?? ""}`;
1062
+ }
1063
+ function decisionLabel(request, decision) {
1064
+ if (decision === "once") return "Yes";
1065
+ if (decision === "deny") return "No, tell the agent what to do instead";
1066
+ const kind = request.toolCall.kind ?? "other";
1067
+ if (kind === "execute") return `Yes, and allow \`${ruleKey(request).slice(8) || "these"}\` commands for this session`;
1068
+ if (kind === "edit") return "Yes, and allow edits for this session";
1069
+ return `Yes, and allow ${kind} tools for this session`;
1070
+ }
1071
+ function question(request) {
1072
+ const kind = request.toolCall.kind ?? "other";
1073
+ if (kind === "execute") return "Run this command?";
1074
+ if (kind === "edit") return "Apply this edit?";
1075
+ if (kind === "delete") return "Delete this?";
1076
+ return `Allow ${request.toolCall.title ?? "this tool call"}?`;
1077
+ }
1078
+ function outcomeOf(request, decision) {
1079
+ const once = option(request.options, ["allow_once"], ["allow"]);
1080
+ const always = option(request.options, ["allow_always"], []);
1081
+ const reject = option(request.options, ["reject_once", "reject_always"], ["reject", "deny"]);
1082
+ if (decision === "deny") return { outcome: reject ? selected(reject.optionId) : cancelled, remember: false };
1083
+ if (decision === "session" && always) return { outcome: selected(always.optionId), remember: false };
1084
+ const allow = once ?? always;
1085
+ return { outcome: allow ? selected(allow.optionId) : cancelled, remember: decision === "session" };
1086
+ }
1087
+ const cancelOutcome = () => cancelled;
1088
+ const emptyStatus = () => ({ mode: "ask", models: [], commands: [] });
1089
+ function fromConfig(options) {
1090
+ if (!Array.isArray(options)) return {};
1091
+ const list = options;
1092
+ const model = list.find((option2) => option2.id === "model" || option2.category === "model");
1093
+ const reasoning = list.find((option2) => option2.id === "reasoning" || option2.category === "thought_level");
1094
+ const mode = list.find((option2) => option2.id === "mode" || option2.category === "mode");
1095
+ return {
1096
+ ...model ? { model: model.currentValue, models: model.options.map(({ value, name }) => ({ value, name })) } : {},
1097
+ ...reasoning ? { reasoning } : {},
1098
+ ...mode ? { mode: mode.currentValue } : {}
1099
+ };
1100
+ }
1101
+ function fromSession(session) {
1102
+ const models = session.models?.availableModels.map((model) => ({ value: model.modelId, name: model.name })) ?? [];
1103
+ return {
1104
+ ...emptyStatus(),
1105
+ sessionId: session.sessionId,
1106
+ mode: session.modes?.currentModeId ?? "ask",
1107
+ model: session.models?.currentModelId,
1108
+ models,
1109
+ ...fromConfig(session.configOptions)
1110
+ };
1111
+ }
1112
+ const commandsOf = (value) => Array.isArray(value) ? value.filter((item) => typeof item?.name === "string").map((item) => ({
1113
+ name: item.name.replace(/^\//, ""),
1114
+ description: typeof item.description === "string" ? item.description : "",
1115
+ ...typeof item.input?.hint === "string" ? { hint: item.input.hint } : {}
1116
+ })) : [];
1117
+ function applyStatus(status, update) {
1118
+ let next = status;
1119
+ switch (update.sessionUpdate) {
1120
+ case "available_commands_update":
1121
+ next = { ...status, commands: commandsOf(update.availableCommands) };
1122
+ break;
1123
+ case "current_mode_update":
1124
+ if (typeof update.currentModeId === "string") next = { ...status, mode: update.currentModeId };
1125
+ break;
1126
+ case "session_info_update":
1127
+ if (typeof update.title === "string") next = { ...status, title: update.title };
1128
+ break;
1129
+ case "config_option_update":
1130
+ next = { ...status, ...fromConfig(update.configOptions) };
1131
+ break;
1132
+ case "usage_update":
1133
+ if (typeof update.used === "number" && typeof update.size === "number" && update.size > 0) next = { ...status, usage: { used: update.used, size: update.size } };
1134
+ break;
1135
+ }
1136
+ const meta = update._meta?.oxbit;
1137
+ if (meta && "goal" in meta) next = { ...next, goal: meta.goal && typeof meta.goal.status === "string" ? meta.goal : void 0 };
1138
+ if (meta && "loop" in meta) next = { ...next, loop: meta.loop ?? void 0 };
1139
+ return next;
1140
+ }
1141
+ const modelName = (status) => status.models.find((model) => model.value === status.model)?.name ?? status.model;
1142
+ const STOP_NOTES = {
1143
+ cancelled: "Interrupted",
1144
+ max_tokens: "The response reached the output token limit",
1145
+ max_turn_requests: "Stopped after the maximum number of model requests",
1146
+ refusal: "The model declined to answer"
1147
+ };
1148
+ const message = (error) => error instanceof Error ? error.message : String(error);
1149
+ class SessionController {
1150
+ constructor(connection, cwd) {
1151
+ this.connection = connection;
1152
+ this.cwd = cwd;
1153
+ this.state = {
1154
+ cwd,
1155
+ transcript: emptyTranscript(),
1156
+ draft: emptyDraft,
1157
+ running: false,
1158
+ queued: [],
1159
+ status: emptyStatus(),
1160
+ info: connection.info()
1161
+ };
1162
+ this.stop = [connection.onUpdate(this.onUpdate), connection.onPermission(this.onPermission)];
1163
+ }
1164
+ connection;
1165
+ cwd;
1166
+ state;
1167
+ listeners = /* @__PURE__ */ new Set();
1168
+ permissions = [];
1169
+ rules = /* @__PURE__ */ new Set();
1170
+ timer;
1171
+ hintTimer;
1172
+ early;
1173
+ stop;
1174
+ onExit;
1175
+ subscribe = (listener) => {
1176
+ this.listeners.add(listener);
1177
+ return () => void this.listeners.delete(listener);
1178
+ };
1179
+ getState = () => this.state;
1180
+ notify() {
1181
+ clearTimeout(this.timer);
1182
+ this.timer = void 0;
1183
+ for (const listener of this.listeners) listener();
1184
+ }
1185
+ /** `soon` coalesces streaming updates into one render per 16 ms. */
1186
+ set(patch, soon = false) {
1187
+ this.state = { ...this.state, ...patch };
1188
+ if (!soon) this.notify();
1189
+ else this.timer ??= setTimeout(() => this.notify(), 16);
1190
+ }
1191
+ transcript(change) {
1192
+ this.set({ transcript: change(this.state.transcript) });
1193
+ }
1194
+ notice(text, tone = "info") {
1195
+ this.transcript((transcript) => append(transcript, { type: "notice", text, tone }));
1196
+ }
1197
+ flash(hint) {
1198
+ clearTimeout(this.hintTimer);
1199
+ this.set({ hint });
1200
+ this.hintTimer = setTimeout(() => this.state.hint === hint && this.set({ hint: void 0 }), 2e3);
1201
+ }
1202
+ onUpdate = (sessionId, update) => {
1203
+ if (this.early) return void this.early.push([sessionId, update]);
1204
+ if (sessionId !== this.state.status.sessionId) return;
1205
+ this.set({ transcript: applyUpdate(this.state.transcript, update), status: applyStatus(this.state.status, update), info: this.connection.info() }, true);
1206
+ if (update.sessionUpdate === "available_commands_update") this.commandsChanged();
1207
+ };
1208
+ commandsChanged() {
1209
+ }
1210
+ onPermission = (request) => new Promise((resolve) => {
1211
+ if (this.rules.has(ruleKey(request))) return resolve(outcomeOf(request, "once").outcome);
1212
+ this.permissions.push({ request, resolve, index: 0 });
1213
+ if (this.permissions.length === 1) this.set({ permission: this.permissions[0] });
1214
+ });
1215
+ /** Opens `sessionId` (or a new session) and replays updates that arrived before the response. */
1216
+ async open(sessionId, label) {
1217
+ if (this.state.running) return this.notice("Wait for the agent to finish, or press Esc to stop it", "warn");
1218
+ this.rules.clear();
1219
+ if (label) this.transcript((transcript) => append(transcript, { type: "divider", text: label }));
1220
+ this.early = [];
1221
+ let session;
1222
+ try {
1223
+ session = sessionId ? await this.connection.loadSession(sessionId, this.cwd) : await this.connection.newSession(this.cwd);
1224
+ } catch (error) {
1225
+ this.notice(message(error), "error");
1226
+ }
1227
+ const early = this.early;
1228
+ this.early = void 0;
1229
+ if (!session) return;
1230
+ this.set({ status: fromSession(session), info: this.connection.info() });
1231
+ for (const [id, update] of early) this.onUpdate(id, update);
1232
+ this.transcript(endTurn);
1233
+ }
1234
+ async send(text) {
1235
+ const sessionId = this.state.status.sessionId;
1236
+ if (!sessionId) return this.notice("There is no open session. Run /new to start one.", "error");
1237
+ this.set({ transcript: append(this.state.transcript, { type: "user", text }), running: true, startedAt: Date.now() });
1238
+ let failed;
1239
+ let stopReason = "end_turn";
1240
+ try {
1241
+ stopReason = (await this.connection.prompt(sessionId, [{ type: "text", text }])).stopReason;
1242
+ } catch (error) {
1243
+ failed = message(error);
1244
+ }
1245
+ let transcript = endTurn(this.state.transcript);
1246
+ if (STOP_NOTES[stopReason]) transcript = append(transcript, { type: "notice", text: STOP_NOTES[stopReason], tone: "warn" });
1247
+ if (failed) transcript = append(transcript, { type: "notice", text: failed, tone: "error" });
1248
+ const [next, ...queued] = this.state.queued;
1249
+ const draft = failed && !this.state.draft.text ? draftOf(text) : this.state.draft;
1250
+ this.set({ transcript, running: false, startedAt: void 0, info: this.connection.info(), draft, queued: failed || !next ? this.state.queued : queued });
1251
+ if (next && !failed) void this.send(next);
1252
+ }
1253
+ /** Stops the running turn; queued prompts return to the input. */
1254
+ cancel() {
1255
+ const sessionId = this.state.status.sessionId;
1256
+ for (const pending of this.permissions.splice(0)) pending.resolve(cancelOutcome());
1257
+ const queued = this.state.queued.join("\n\n");
1258
+ this.set({ permission: void 0, queued: [], ...queued && !this.state.draft.text ? { draft: draftOf(queued) } : {} });
1259
+ if (sessionId && this.state.running) void Promise.resolve(this.connection.cancel(sessionId)).catch(() => {
1260
+ });
1261
+ }
1262
+ decide(decision) {
1263
+ const pending = this.permissions.shift();
1264
+ if (!pending) return;
1265
+ const { outcome, remember } = outcomeOf(pending.request, decision);
1266
+ if (remember) this.rules.add(ruleKey(pending.request));
1267
+ pending.resolve(outcome);
1268
+ this.set({ permission: this.permissions[0] });
1269
+ }
1270
+ cycleMode() {
1271
+ const ids = MODES.map((mode2) => mode2.id);
1272
+ const mode = ids[(ids.indexOf(this.state.status.mode) + 1) % ids.length];
1273
+ const sessionId = this.state.status.sessionId;
1274
+ this.set({ status: { ...this.state.status, mode } });
1275
+ if (sessionId) this.connection.setMode(sessionId, mode).catch((error) => this.notice(message(error), "error"));
1276
+ }
1277
+ openModels() {
1278
+ const { models, model } = this.state.status;
1279
+ if (!models.length) return this.notice("The model list is empty. Check the provider settings.", "warn");
1280
+ this.set({ overlay: { kind: "models", filter: "", index: Math.max(0, models.findIndex((item) => item.value === model)) } });
1281
+ }
1282
+ async chooseModel(modelId) {
1283
+ const { status } = this.state;
1284
+ this.set({ overlay: void 0, status: { ...status, model: modelId } });
1285
+ if (!status.sessionId) return;
1286
+ try {
1287
+ const result = await this.connection.setModel(status.sessionId, modelId);
1288
+ if (result?.configOptions) this.set({ status: { ...this.state.status, ...fromConfig(result.configOptions) } });
1289
+ } catch (error) {
1290
+ this.set({ status: { ...this.state.status, model: status.model } });
1291
+ this.notice(message(error), "error");
1292
+ }
1293
+ }
1294
+ async cycleReasoning(step) {
1295
+ const { reasoning, sessionId } = this.state.status;
1296
+ if (!reasoning || !sessionId || !this.connection.setConfigOption) return;
1297
+ const values = reasoning.options.map((option2) => option2.value);
1298
+ const value = values[(values.indexOf(reasoning.currentValue) + step + values.length) % values.length];
1299
+ this.set({ status: { ...this.state.status, reasoning: { ...reasoning, currentValue: value } } });
1300
+ try {
1301
+ const result = await this.connection.setConfigOption(sessionId, reasoning.id, value);
1302
+ if (result?.configOptions) this.set({ status: { ...this.state.status, ...fromConfig(result.configOptions) } });
1303
+ } catch (error) {
1304
+ this.notice(message(error), "error");
1305
+ }
1306
+ }
1307
+ async openSessions() {
1308
+ try {
1309
+ const sessions = await this.connection.listSessions?.(this.cwd) ?? [];
1310
+ if (!sessions.length) return this.notice("There are no saved sessions for this folder.");
1311
+ this.set({ overlay: { kind: "sessions", filter: "", index: 0, sessions } });
1312
+ } catch (error) {
1313
+ this.notice(message(error), "error");
1314
+ }
1315
+ }
1316
+ commands() {
1317
+ return this.state.status.commands;
1318
+ }
1319
+ exit() {
1320
+ if (this.state.running) this.cancel();
1321
+ this.dispose();
1322
+ this.onExit?.();
1323
+ }
1324
+ dispose() {
1325
+ for (const stop of this.stop.splice(0)) stop();
1326
+ for (const pending of this.permissions.splice(0)) pending.resolve(cancelOutcome());
1327
+ clearTimeout(this.timer);
1328
+ clearTimeout(this.hintTimer);
1329
+ }
1330
+ liveTool(toolCallId) {
1331
+ const item = this.state.transcript.live.find((entry) => entry.type === "tool" && entry.tool.toolCallId === toolCallId);
1332
+ return item?.tool;
1333
+ }
1334
+ }
1335
+ const LOCAL_COMMANDS = [
1336
+ { name: "model", description: "Choose the model and reasoning effort", hint: "[model]" },
1337
+ { name: "new", description: "Start a new session" },
1338
+ { name: "resume", description: "Resume a saved session" },
1339
+ { name: "help", description: "Show keys and commands" },
1340
+ { name: "exit", description: "Quit" }
1341
+ ];
1342
+ const picker = (overlay) => overlay?.kind === "models" || overlay?.kind === "sessions" ? overlay : void 0;
1343
+ function overlayItems(state) {
1344
+ const overlay = state.overlay;
1345
+ if (overlay?.kind === "models")
1346
+ return rank(state.status.models, (model) => `${model.name} ${model.value}`, overlay.filter, 200).map((model) => ({ value: model.value, label: model.name, ...model.name !== model.value ? { detail: model.value } : {} }));
1347
+ if (overlay?.kind === "sessions")
1348
+ return rank(overlay.sessions, (session) => session.title ?? session.sessionId, overlay.filter, 200).map((session) => ({ value: session.sessionId, label: session.title || session.sessionId, detail: session.updatedAt?.slice(0, 16).replace("T", " ") }));
1349
+ return [];
1350
+ }
1351
+ class Controller extends SessionController {
1352
+ constructor(connection, options) {
1353
+ super(connection, options.cwd);
1354
+ this.options = options;
1355
+ this.history = options.history?.load() ?? [];
1356
+ }
1357
+ options;
1358
+ history;
1359
+ historyIndex = -1;
1360
+ stash = emptyDraft;
1361
+ dismissed;
1362
+ fileTimer;
1363
+ files = /* @__PURE__ */ new Map();
1364
+ lastInterrupt = 0;
1365
+ commands() {
1366
+ const local = LOCAL_COMMANDS.filter((command) => command.name !== "resume" || this.connection.listSessions);
1367
+ return [...this.state.status.commands.filter((command) => !local.some((item) => item.name === command.name)), ...local];
1368
+ }
1369
+ submit() {
1370
+ const text = this.state.draft.text.replace(/\s+$/, "");
1371
+ if (!text.trim()) return;
1372
+ if (this.history.at(-1) !== text) {
1373
+ this.history.push(text);
1374
+ this.options.history?.save(text);
1375
+ }
1376
+ this.historyIndex = -1;
1377
+ this.dismissed = void 0;
1378
+ this.set({ draft: emptyDraft, completion: void 0 });
1379
+ if (text.startsWith("/") && this.local(text)) return;
1380
+ if (this.state.running) this.set({ queued: [...this.state.queued, text] });
1381
+ else void this.send(text);
1382
+ }
1383
+ local(text) {
1384
+ const [, name, rest] = /^\/(\S+)\s*([\s\S]*)$/.exec(text) ?? [];
1385
+ const args = rest?.trim() ?? "";
1386
+ switch (name) {
1387
+ case "model":
1388
+ if (args) void this.chooseModel(args);
1389
+ else this.openModels();
1390
+ return true;
1391
+ case "new":
1392
+ void this.open(void 0, "New session");
1393
+ return true;
1394
+ case "resume":
1395
+ if (!this.connection.listSessions && !args) return false;
1396
+ if (args) void this.open(args, "Resumed session");
1397
+ else void this.openSessions();
1398
+ return true;
1399
+ case "help":
1400
+ this.set({ overlay: { kind: "help" } });
1401
+ return true;
1402
+ case "exit":
1403
+ case "quit":
1404
+ this.exit();
1405
+ return true;
1406
+ }
1407
+ return false;
1408
+ }
1409
+ commandsChanged() {
1410
+ if (commandToken(this.state.draft)) this.set({ completion: this.completionFor(this.state.draft) }, true);
1411
+ }
1412
+ /** Sets the draft and recomputes slash and @file completion. */
1413
+ setDraft(draft) {
1414
+ this.set({ draft, completion: this.completionFor(draft) });
1415
+ }
1416
+ paste(text) {
1417
+ const clean = text.replace(/\r\n?/g, "\n");
1418
+ const overlay = this.state.overlay;
1419
+ if (picker(overlay)) return this.filter(picker(overlay).filter + clean.replace(/\n/g, " "));
1420
+ if (overlay) return;
1421
+ this.setDraft(insert(this.state.draft, clean));
1422
+ }
1423
+ completionFor(draft) {
1424
+ if (this.dismissed === draft.text) return void 0;
1425
+ this.dismissed = void 0;
1426
+ const command = commandToken(draft);
1427
+ if (command) {
1428
+ const items2 = commandItems(this.commands(), command.query);
1429
+ return items2.length ? { kind: "command", items: items2, index: 0, ...command } : void 0;
1430
+ }
1431
+ const file = fileToken(draft);
1432
+ if (!file || !this.connection.listFiles) return void 0;
1433
+ const known = this.files.get(file.query);
1434
+ if (!known) this.lookup(file.query);
1435
+ const items = fileItems(known ?? [...new Set([...this.files.values()].flat())], file.query);
1436
+ return items.length ? { kind: "file", items, index: 0, ...file } : void 0;
1437
+ }
1438
+ lookup(query) {
1439
+ clearTimeout(this.fileTimer);
1440
+ this.fileTimer = setTimeout(() => {
1441
+ void this.connection.listFiles(query, this.options.cwd).then((paths) => {
1442
+ this.files.set(query, paths);
1443
+ if (fileToken(this.state.draft)?.query === query) this.set({ completion: this.completionFor(this.state.draft) });
1444
+ }, () => {
1445
+ });
1446
+ }, 50);
1447
+ }
1448
+ moveCompletion(step) {
1449
+ const completion = this.state.completion;
1450
+ if (completion) this.set({ completion: { ...completion, index: (completion.index + step + completion.items.length) % completion.items.length } });
1451
+ }
1452
+ dismissCompletion() {
1453
+ this.dismissed = this.state.draft.text;
1454
+ this.set({ completion: void 0 });
1455
+ }
1456
+ /** Inserts the selected item; with `run`, a command that takes no arguments is sent at once. */
1457
+ acceptCompletion(run2) {
1458
+ const completion = this.state.completion;
1459
+ if (!completion) return;
1460
+ const item = completion.items[completion.index];
1461
+ const command = completion.kind === "command" ? this.commands().find((entry) => `/${entry.name}` === item.value) : void 0;
1462
+ if (run2 && command && !command.hint) {
1463
+ this.set({ draft: draftOf(item.value) });
1464
+ return this.submit();
1465
+ }
1466
+ this.setDraft(accept(this.state.draft, completion, item));
1467
+ }
1468
+ /** Walks prompt history; false when there is nothing to recall, so the caller can move the cursor instead. */
1469
+ recall(step) {
1470
+ if (!this.history.length || this.historyIndex < 0 && step > 0) return false;
1471
+ if (this.historyIndex < 0) this.stash = this.state.draft;
1472
+ const index = this.historyIndex < 0 ? this.history.length - 1 : this.historyIndex + step;
1473
+ if (index < 0) return true;
1474
+ this.historyIndex = index >= this.history.length ? -1 : index;
1475
+ this.setDraft(this.historyIndex < 0 ? this.stash : draftOf(this.history[index]));
1476
+ return true;
1477
+ }
1478
+ filter(text) {
1479
+ const overlay = picker(this.state.overlay);
1480
+ if (overlay) this.set({ overlay: { ...overlay, filter: text, index: 0 } });
1481
+ }
1482
+ moveOverlay(step) {
1483
+ const overlay = picker(this.state.overlay);
1484
+ const count = overlayItems(this.state).length;
1485
+ if (overlay && count) this.set({ overlay: { ...overlay, index: (overlay.index + step + count) % count } });
1486
+ }
1487
+ confirmOverlay() {
1488
+ const overlay = picker(this.state.overlay);
1489
+ const item = overlay ? overlayItems(this.state)[overlay.index] : void 0;
1490
+ this.set({ overlay: void 0 });
1491
+ if (item && overlay?.kind === "models") void this.chooseModel(item.value);
1492
+ if (item && overlay?.kind === "sessions") void this.open(item.value, `Resumed ${item.label}`);
1493
+ }
1494
+ /** Ctrl+O: shows the latest thought, tool output or diff in full; `step` moves to an earlier or later one. */
1495
+ showDetails(step = 0) {
1496
+ const { transcript, overlay } = this.state;
1497
+ const items = [...transcript.flushed, ...transcript.live].filter(expandable);
1498
+ if (!items.length) return this.flash("Nothing to expand yet");
1499
+ const at = overlay?.kind === "details" ? items.findIndex((item) => item.id === overlay.id) : items.length;
1500
+ const next = items[Math.min(items.length - 1, Math.max(0, at + step - (overlay?.kind === "details" ? 0 : 1)))];
1501
+ this.set({ overlay: { kind: "details", id: next.id } });
1502
+ }
1503
+ /** Ctrl+C: stop the turn, close or clear, and exit on a second press within two seconds. */
1504
+ interrupt() {
1505
+ const now = Date.now();
1506
+ if (now - this.lastInterrupt < 2e3) return this.exit();
1507
+ this.lastInterrupt = now;
1508
+ if (this.state.running || this.state.permission) this.cancel();
1509
+ else if (this.state.overlay) this.set({ overlay: void 0 });
1510
+ else if (this.state.draft.text) this.setDraft(emptyDraft);
1511
+ this.flash("Press Ctrl+C again to exit");
1512
+ }
1513
+ dispose() {
1514
+ super.dispose();
1515
+ clearTimeout(this.fileTimer);
1516
+ }
1517
+ }
1518
+ const KEYS = [
1519
+ ["enter", "send"],
1520
+ ["shift+enter, alt+enter, ctrl+j, \\ enter", "new line"],
1521
+ ["↑ ↓", "history, or move between lines"],
1522
+ ["esc", "stop the agent, close a menu"],
1523
+ ["ctrl+c", "stop, then press again to exit"],
1524
+ ["shift+tab", "switch the mode"],
1525
+ ["ctrl+o", "show tool output, diffs and thinking in full"],
1526
+ ["ctrl+p, /model", "choose the model"],
1527
+ ["/ and @", "complete commands and files"],
1528
+ ["ctrl+a e u k w", "line editing"]
1529
+ ];
1530
+ function windowOf(items, index, size) {
1531
+ const start = Math.min(Math.max(0, index - size + 1), Math.max(0, items.length - size));
1532
+ return { start, rows: items.slice(start, start + size) };
1533
+ }
1534
+ function PickList({ items, index, width: width$1, size = 8 }) {
1535
+ const theme = useTheme();
1536
+ const { start, rows } = windowOf(items, index, size);
1537
+ const label = Math.min(Math.max(...rows.map((item) => width(item.label)), 8) + 3, Math.floor(width$1 * 0.5));
1538
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: width$1, children: [
1539
+ start > 0 ? /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
1540
+ " ↑ ",
1541
+ start,
1542
+ " more"
1543
+ ] }) : null,
1544
+ rows.map((item, at) => {
1545
+ const selected2 = start + at === index;
1546
+ return /* @__PURE__ */ jsxs(Box, { children: [
1547
+ /* @__PURE__ */ jsx(Box, { width: label, flexShrink: 0, children: /* @__PURE__ */ jsxs(Text, { color: selected2 ? theme.accent : void 0, bold: selected2, wrap: "truncate-end", children: [
1548
+ selected2 ? "❯ " : " ",
1549
+ item.label
1550
+ ] }) }),
1551
+ item.detail ? /* @__PURE__ */ jsx(Box, { flexShrink: 1, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, wrap: "truncate-end", children: item.detail }) }) : null
1552
+ ] }, item.value);
1553
+ }),
1554
+ start + rows.length < items.length ? /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
1555
+ " ↓ ",
1556
+ items.length - start - rows.length,
1557
+ " more"
1558
+ ] }) : null
1559
+ ] });
1560
+ }
1561
+ function PermissionDialog({ pending, controller, width: width2, cwd }) {
1562
+ const theme = useTheme();
1563
+ const { request, index } = pending;
1564
+ const call = request.toolCall;
1565
+ const diffs = (call.content ?? []).filter((block) => block.type === "diff").map((block) => ({ path: String(block.path ?? ""), oldText: block.oldText ?? null, newText: String(block.newText ?? "") }));
1566
+ const shown = controller.liveTool(call.toolCallId)?.diffs.length ? [] : diffs;
1567
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.warning, paddingX: 1, width: width2, marginTop: 1, children: [
1568
+ /* @__PURE__ */ jsx(Text, { bold: true, color: theme.warning, children: question(request) }),
1569
+ /* @__PURE__ */ jsxs(Box, { marginY: 1, flexDirection: "column", children: [
1570
+ call.kind === "execute" ? /* @__PURE__ */ jsxs(Text, { wrap: "wrap", children: [
1571
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, children: "$ " }),
1572
+ commandOf(request)
1573
+ ] }) : /* @__PURE__ */ jsx(Text, { bold: true, wrap: "wrap", children: relative(cwd, call.title ?? "Tool call") }),
1574
+ shown.map((diff, at) => /* @__PURE__ */ jsx(DiffView, { diff, cwd, limit: 10 }, at))
1575
+ ] }),
1576
+ DECISIONS.map((decision, at) => /* @__PURE__ */ jsxs(Text, { color: at === index ? theme.accent : void 0, bold: at === index, wrap: "wrap", children: [
1577
+ at === index ? "❯" : " ",
1578
+ " ",
1579
+ at + 1,
1580
+ ". ",
1581
+ decisionLabel(request, decision)
1582
+ ] }, decision)),
1583
+ /* @__PURE__ */ jsx(Text, { color: theme.muted, children: "↑↓ select · enter confirm · 1-3 · esc deny" })
1584
+ ] });
1585
+ }
1586
+ function OverlayView({ state, width: width2 }) {
1587
+ const theme = useTheme();
1588
+ const overlay = state.overlay;
1589
+ if (overlay.kind === "help") {
1590
+ const key = Math.min(40, Math.floor(width2 * 0.45));
1591
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, width: width2, marginTop: 1, children: [
1592
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Keys" }),
1593
+ KEYS.map(([keys, action]) => /* @__PURE__ */ jsxs(Box, { children: [
1594
+ /* @__PURE__ */ jsx(Box, { width: key, flexShrink: 0, paddingRight: 1, children: /* @__PURE__ */ jsx(Text, { color: theme.accent, wrap: "wrap", children: keys }) }),
1595
+ /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: action })
1596
+ ] }, keys)),
1597
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
1598
+ "\n",
1599
+ "Modes"
1600
+ ] }),
1601
+ MODES.map((mode) => /* @__PURE__ */ jsxs(Text, { wrap: "wrap", children: [
1602
+ /* @__PURE__ */ jsx(Text, { color: theme.modes[mode.id], children: mode.name }),
1603
+ /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
1604
+ " · ",
1605
+ mode.description
1606
+ ] })
1607
+ ] }, mode.id)),
1608
+ /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
1609
+ "\n",
1610
+ "Type / to list commands. esc closes this help."
1611
+ ] })
1612
+ ] });
1613
+ }
1614
+ if (overlay.kind === "details") return null;
1615
+ const items = overlayItems(state);
1616
+ const reasoning = overlay.kind === "models" ? state.status.reasoning : void 0;
1617
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, width: width2, marginTop: 1, children: [
1618
+ /* @__PURE__ */ jsx(Text, { bold: true, children: overlay.kind === "models" ? "Choose a model" : "Resume a session" }),
1619
+ /* @__PURE__ */ jsxs(Text, { children: [
1620
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, children: "› " }),
1621
+ overlay.filter,
1622
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: " " }),
1623
+ overlay.filter ? null : /* @__PURE__ */ jsx(Text, { color: theme.muted, children: "type to filter" })
1624
+ ] }),
1625
+ items.length ? /* @__PURE__ */ jsx(PickList, { items, index: overlay.index, width: width2 - 4, size: 10 }) : /* @__PURE__ */ jsx(Text, { color: theme.muted, children: "No matches" }),
1626
+ reasoning ? /* @__PURE__ */ jsxs(Text, { children: [
1627
+ "Reasoning ",
1628
+ /* @__PURE__ */ jsx(Text, { color: theme.muted, children: "◂ " }),
1629
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, children: reasoning.options.find((option2) => option2.value === reasoning.currentValue)?.name ?? reasoning.currentValue }),
1630
+ /* @__PURE__ */ jsx(Text, { color: theme.muted, children: " ▸" })
1631
+ ] }) : null,
1632
+ /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
1633
+ "↑↓ select · enter choose",
1634
+ reasoning ? " · ←→ reasoning" : "",
1635
+ " · esc close"
1636
+ ] })
1637
+ ] });
1638
+ }
1639
+ function permissionKey(controller, input, key) {
1640
+ const pending = controller.getState().permission;
1641
+ const choose = (index) => controller.decide(DECISIONS[index]);
1642
+ if (key.upArrow || key.downArrow) {
1643
+ const index = (pending.index + (key.upArrow ? -1 : 1) + DECISIONS.length) % DECISIONS.length;
1644
+ return controller.set({ permission: { ...pending, index } });
1645
+ }
1646
+ if (key.return) return choose(pending.index);
1647
+ if (key.escape) return controller.decide("deny");
1648
+ const shortcut = { 1: 0, 2: 1, 3: 2, y: 0, a: 1, n: 2 }[input.toLowerCase()];
1649
+ if (shortcut !== void 0) choose(shortcut);
1650
+ }
1651
+ function overlayKey(controller, input, key) {
1652
+ const overlay = controller.getState().overlay;
1653
+ if (overlay.kind === "details") {
1654
+ if (key.escape || key.return || input === "q" || key.ctrl && input === "o") controller.set({ overlay: void 0 });
1655
+ else if (key.leftArrow || key.rightArrow) controller.showDetails(key.leftArrow ? -1 : 1);
1656
+ return;
1657
+ }
1658
+ if (overlay.kind === "help") {
1659
+ if (key.escape || key.return || input === "q") controller.set({ overlay: void 0 });
1660
+ return;
1661
+ }
1662
+ if (key.escape) return controller.set({ overlay: void 0 });
1663
+ if (key.upArrow || key.downArrow) return controller.moveOverlay(key.upArrow ? -1 : 1);
1664
+ if (key.return) return controller.confirmOverlay();
1665
+ if (overlay.kind === "models" && (key.leftArrow || key.rightArrow)) return void controller.cycleReasoning(key.leftArrow ? -1 : 1);
1666
+ if (key.backspace || key.delete) return controller.filter(overlay.filter.slice(0, -1));
1667
+ if (input && !key.ctrl && !key.meta && !key.tab) controller.filter(overlay.filter + input);
1668
+ }
1669
+ function handleKey(controller, input, key) {
1670
+ const state = controller.getState();
1671
+ const draft = state.draft;
1672
+ const edit = controller.setDraft.bind(controller);
1673
+ if (key.ctrl && input === "c") return controller.interrupt();
1674
+ if (state.permission) return permissionKey(controller, input, key);
1675
+ if (state.overlay) return overlayKey(controller, input, key);
1676
+ if (key.tab && key.shift) return controller.cycleMode();
1677
+ if (key.ctrl && input === "o") return controller.showDetails();
1678
+ if (key.ctrl && input === "p") return controller.openModels();
1679
+ if (key.ctrl && input === "d") return draft.text ? edit(deleteForward(draft)) : controller.exit();
1680
+ if (key.escape) {
1681
+ if (state.completion) return controller.dismissCompletion();
1682
+ if (state.running) return controller.cancel();
1683
+ return;
1684
+ }
1685
+ if (state.completion) {
1686
+ if (key.upArrow || key.downArrow) return controller.moveCompletion(key.upArrow ? -1 : 1);
1687
+ if (key.tab) return controller.acceptCompletion(false);
1688
+ if (key.return && !key.shift && !key.meta) return controller.acceptCompletion(true);
1689
+ }
1690
+ if (key.return) {
1691
+ if (key.shift || key.meta) return edit(insert(draft, "\n"));
1692
+ if (draft.text[draft.cursor - 1] === "\\") return edit(insert(backspace(draft), "\n"));
1693
+ return controller.submit();
1694
+ }
1695
+ if (input === "\n" || key.ctrl && input === "j") return edit(insert(draft, "\n"));
1696
+ if (key.upArrow || key.downArrow) {
1697
+ const step = key.upArrow ? -1 : 1;
1698
+ const moved = vertical(draft, step);
1699
+ if (moved) return edit(moved);
1700
+ return void controller.recall(step);
1701
+ }
1702
+ if (key.leftArrow) return edit(key.meta || key.ctrl ? wordLeft(draft) : left(draft));
1703
+ if (key.rightArrow) return edit(key.meta || key.ctrl ? wordRight(draft) : right(draft));
1704
+ if (key.home || key.ctrl && input === "a") return edit(home(draft));
1705
+ if (key.end || key.ctrl && input === "e") return edit(end(draft));
1706
+ if (key.backspace) return edit(key.meta || key.ctrl ? deleteWordLeft(draft) : backspace(draft));
1707
+ if (key.delete) return edit(deleteForward(draft));
1708
+ if (key.ctrl && input === "w") return edit(deleteWordLeft(draft));
1709
+ if (key.ctrl && input === "u") return edit(killLineLeft(draft));
1710
+ if (key.ctrl && input === "k") return edit(killLineRight(draft));
1711
+ if (key.meta && input === "b") return edit(wordLeft(draft));
1712
+ if (key.meta && input === "f") return edit(wordRight(draft));
1713
+ if (input && !key.ctrl && !key.meta && !key.tab) edit(insert(draft, input.replace(/\r\n?/g, "\n")));
1714
+ }
1715
+ const MODE_LABELS = { ask: "◇ ask", "accept-edits": "⏵⏵ accept edits", "full-auto": "⏵⏵⏵ full auto" };
1716
+ function PromptBox({ state, width: width2 }) {
1717
+ const theme = useTheme();
1718
+ const empty = !state.draft.text;
1719
+ const placeholder = state.running ? "Queue a message · esc to stop the agent" : "Ask anything · / for commands · @ for files";
1720
+ return /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: theme.border, paddingX: 1, width: width2, children: [
1721
+ /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: theme.accent, bold: true, children: "›" }) }),
1722
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: empty ? /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
1723
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: " " }),
1724
+ /* @__PURE__ */ jsx(Text, { color: theme.muted, children: placeholder })
1725
+ ] }) : cursorLines(state.draft).map((line, index) => /* @__PURE__ */ jsxs(Text, { wrap: "wrap", children: [
1726
+ line.before,
1727
+ line.under === void 0 ? line.before ? "" : " " : /* @__PURE__ */ jsx(Text, { inverse: true, children: line.under }),
1728
+ line.after ?? ""
1729
+ ] }, index)) })
1730
+ ] });
1731
+ }
1732
+ const elapsed = (ms) => {
1733
+ const seconds = Math.floor(ms / 1e3);
1734
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
1735
+ };
1736
+ function Working({ state }) {
1737
+ const theme = useTheme();
1738
+ const { frame } = useAnimation({ interval: 100 });
1739
+ const last = state.transcript.live.at(-1);
1740
+ const label = last?.type === "thought" && !last.done ? "Thinking" : last?.type === "tool" && !last.done ? "Running" : "Working";
1741
+ return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
1742
+ /* @__PURE__ */ jsxs(Text, { color: theme.accent, children: [
1743
+ SPINNER[frame % SPINNER.length],
1744
+ " ",
1745
+ label,
1746
+ "…"
1747
+ ] }),
1748
+ /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
1749
+ " (",
1750
+ elapsed(Date.now() - (state.startedAt ?? Date.now())),
1751
+ " · esc to interrupt)"
1752
+ ] })
1753
+ ] }) });
1754
+ }
1755
+ function fitSegments(segments, budget, separator = " · ") {
1756
+ const kept = /* @__PURE__ */ new Set();
1757
+ let used = 0;
1758
+ for (const segment of [...segments].sort((a, b) => a.priority - b.priority)) {
1759
+ const size = width(segment.text) + (kept.size ? width(separator) : 0);
1760
+ if (used + size <= budget) {
1761
+ kept.add(segment);
1762
+ used += size;
1763
+ }
1764
+ }
1765
+ return segments.filter((segment) => kept.has(segment));
1766
+ }
1767
+ function StatusLine({ state, width: width$1 }) {
1768
+ const theme = useTheme();
1769
+ const { status, info, hint } = state;
1770
+ const usage = status.usage ? Math.min(100, Math.round(status.usage.used / status.usage.size * 100)) : void 0;
1771
+ const effort = status.reasoning && status.reasoning.currentValue !== "default" ? ` (${status.reasoning.currentValue})` : "";
1772
+ const model = modelName(status);
1773
+ const budget = Math.max(10, width$1 - 2);
1774
+ const loopText = status.loop && [
1775
+ `↻ loop`,
1776
+ status.loop.iteration ? ` ${status.loop.iteration}${status.loop.max ? `/${status.loop.max}` : ""}` : "",
1777
+ status.loop.status ? ` ${status.loop.status}` : "",
1778
+ status.loop.interval ? ` · every ${status.loop.interval}` : ""
1779
+ ].join("");
1780
+ const left2 = fitSegments([
1781
+ hint ? { text: hint, color: theme.warning, priority: 0 } : { text: `${MODE_LABELS[status.mode] ?? status.mode}${width$1 >= 80 ? " (shift+tab)" : ""}`, color: theme.modes[status.mode], priority: 0 },
1782
+ ...status.goal ? [{ text: `◎ goal ${status.goal.status}`, color: theme.accent, priority: 1 }] : [],
1783
+ ...loopText ? [{ text: loopText, color: theme.accent, priority: 2 }] : []
1784
+ ], budget, " ");
1785
+ const beside = budget - width(left2.map((segment) => segment.text).join(" ")) - 2;
1786
+ const segments = [
1787
+ ...status.title ? [{ text: truncate(status.title, 32), priority: 4 }] : [],
1788
+ ...model ? [{ text: truncate(`${model}${effort}`, 40), priority: 0 }] : [],
1789
+ ...info.usingChatGPTPlan ? [{ text: "Using ChatGPT plan", color: theme.success, priority: 1 }] : info.provider ? [{ text: info.provider, priority: 3 }] : [],
1790
+ ...usage !== void 0 ? [{ text: `${usage}% context`, color: usage >= 90 ? theme.error : usage >= 70 ? theme.warning : void 0, priority: 2 }] : [],
1791
+ { text: truncateStart(state.cwd.replace(process.env.HOME ?? "\0", "~"), 30), priority: 5 }
1792
+ ];
1793
+ const inline = fitSegments(segments, beside);
1794
+ const right2 = inline.length >= segments.filter((segment) => segment.priority < 5).length ? inline : fitSegments(segments, budget);
1795
+ return /* @__PURE__ */ jsxs(Box, { width: width$1, paddingX: 1, flexWrap: "wrap", justifyContent: "space-between", columnGap: 2, children: [
1796
+ /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: left2.map((segment, index) => /* @__PURE__ */ jsxs(Text, { color: segment.color, children: [
1797
+ index ? " " : "",
1798
+ segment.text
1799
+ ] }, index)) }),
1800
+ /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", color: theme.muted, children: right2.map((segment, index) => /* @__PURE__ */ jsxs(Text, { color: segment.color, children: [
1801
+ index ? /* @__PURE__ */ jsx(Text, { color: theme.muted, children: " · " }) : null,
1802
+ segment.text
1803
+ ] }, index)) })
1804
+ ] });
1805
+ }
1806
+ function Screen({ controller, version }) {
1807
+ const state = useSyncExternalStore(controller.subscribe, controller.getState);
1808
+ const theme = useTheme();
1809
+ const { columns, rows } = useWindowSize();
1810
+ const width2 = Math.max(20, columns);
1811
+ useInput((input, key) => handleKey(controller, input, key));
1812
+ usePaste((text) => controller.paste(text));
1813
+ const context = { width: width2, rows, expanded: false, cwd: state.cwd, version, awaiting: state.permission?.request.toolCall.toolCallId };
1814
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: width2, children: [
1815
+ /* @__PURE__ */ jsx(Static, { items: state.transcript.flushed, children: (item) => /* @__PURE__ */ jsx(Box, { width: width2, flexDirection: "column", children: /* @__PURE__ */ jsx(ItemView, { item, context }) }, item.id) }),
1816
+ state.transcript.live.map((item) => /* @__PURE__ */ jsx(ItemView, { item, live: true, context }, item.id)),
1817
+ state.permission ? /* @__PURE__ */ jsx(PermissionDialog, { pending: state.permission, controller, width: width2, cwd: state.cwd }) : state.overlay?.kind === "details" ? /* @__PURE__ */ jsx(Details, { id: state.overlay.id, transcript: state.transcript, context }, state.overlay.id) : state.overlay ? /* @__PURE__ */ jsx(OverlayView, { state, width: width2 }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1818
+ state.running ? /* @__PURE__ */ jsx(Working, { state }) : null,
1819
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1820
+ state.queued.map((text, index) => /* @__PURE__ */ jsxs(Text, { color: theme.muted, wrap: "truncate-end", children: [
1821
+ " ⏎ queued: ",
1822
+ truncate(text.replace(/\n/g, " "), width2 - 14)
1823
+ ] }, index)),
1824
+ /* @__PURE__ */ jsx(PromptBox, { state, width: width2 })
1825
+ ] }),
1826
+ state.completion ? /* @__PURE__ */ jsx(PickList, { items: state.completion.items, index: state.completion.index, width: width2 }) : null
1827
+ ] }),
1828
+ /* @__PURE__ */ jsx(StatusLine, { state, width: width2 })
1829
+ ] });
1830
+ }
1831
+ function App({ controller, theme, version }) {
1832
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx(Screen, { controller, version }) });
1833
+ }
1834
+ const FILE_LIST_TTL_MS = 1e4;
1835
+ const FILE_MATCHES = 2e3;
1836
+ const HISTORY_LIMIT = 500;
1837
+ const subsequence = (query, path) => {
1838
+ let at = 0;
1839
+ for (const char of query) {
1840
+ at = path.indexOf(char, at) + 1;
1841
+ if (!at) return false;
1842
+ }
1843
+ return true;
1844
+ };
1845
+ function memoryTuiConnection(connection, options = {}) {
1846
+ const listed = /* @__PURE__ */ new Map();
1847
+ const files = (cwd) => {
1848
+ const cached = listed.get(cwd);
1849
+ if (cached && Date.now() - cached.at < FILE_LIST_TTL_MS) return cached.files;
1850
+ const entry = { at: Date.now(), files: walkFiles(cwd).catch(() => []) };
1851
+ listed.set(cwd, entry);
1852
+ return entry.files;
1853
+ };
1854
+ return {
1855
+ info: () => ({ provider: options.provider, usingChatGPTPlan: options.usingChatGPTPlan }),
1856
+ async newSession(cwd) {
1857
+ const state = await connection.newSession({ cwd });
1858
+ if (!options.mode || !state.modes || state.modes.currentModeId === options.mode) return state;
1859
+ await connection.setMode(state.sessionId, options.mode);
1860
+ return { ...state, modes: { ...state.modes, currentModeId: options.mode } };
1861
+ },
1862
+ loadSession: async (sessionId, cwd) => await connection.loadSession(sessionId, { cwd }),
1863
+ listSessions: async (cwd) => (await connection.listSessions(cwd)).sessions,
1864
+ prompt: (sessionId, prompt) => connection.prompt(sessionId, prompt),
1865
+ cancel: (sessionId) => connection.cancel(sessionId),
1866
+ setMode: (sessionId, modeId) => connection.setMode(sessionId, modeId),
1867
+ setModel: async (sessionId, modelId) => await connection.setModel(sessionId, modelId),
1868
+ setConfigOption: async (sessionId, configId, value) => await connection.setConfigOption(sessionId, configId, value),
1869
+ onUpdate: (listener) => connection.onUpdate((sessionId, update) => listener(sessionId, update)),
1870
+ onPermission: (handler) => connection.onPermission(async (request) => (await handler(request)).outcome),
1871
+ async listFiles(query, cwd) {
1872
+ const term = query.toLowerCase();
1873
+ return (await files(cwd)).filter((path) => subsequence(term, path.toLowerCase())).slice(0, FILE_MATCHES);
1874
+ }
1875
+ };
1876
+ }
1877
+ function fileHistory(path) {
1878
+ return {
1879
+ load() {
1880
+ try {
1881
+ return readFileSync(path, "utf8").split("\n").flatMap((line) => {
1882
+ try {
1883
+ const entry = JSON.parse(line);
1884
+ return typeof entry === "string" ? [entry] : [];
1885
+ } catch {
1886
+ return [];
1887
+ }
1888
+ }).slice(-HISTORY_LIMIT);
1889
+ } catch {
1890
+ return [];
1891
+ }
1892
+ },
1893
+ save(entry) {
1894
+ try {
1895
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
1896
+ appendFileSync(path, `${JSON.stringify(entry)}
1897
+ `, { mode: 384 });
1898
+ const lines = readFileSync(path, "utf8").split("\n").filter(Boolean);
1899
+ if (lines.length > HISTORY_LIMIT * 2) writeFileSync(path, `${lines.slice(-HISTORY_LIMIT).join("\n")}
1900
+ `, { mode: 384 });
1901
+ } catch {
1902
+ }
1903
+ }
1904
+ };
1905
+ }
1906
+ async function run(connection, options) {
1907
+ const controller = new Controller(connection, { cwd: options.cwd, history: options.history });
1908
+ const exited = new Promise((resolve) => {
1909
+ controller.onExit = resolve;
1910
+ });
1911
+ const streams = Object.fromEntries(Object.entries({ stdin: options.stdin, stdout: options.stdout, stderr: options.stderr }).filter(([, value]) => value));
1912
+ const instance = render(/* @__PURE__ */ jsx(App, { controller, theme: resolveTheme(options.theme), version: options.version }), {
1913
+ ...streams,
1914
+ exitOnCtrlC: false,
1915
+ ...options.debug ? { debug: true } : { kittyKeyboard: { mode: "auto" } }
1916
+ });
1917
+ void controller.open(options.sessionId).then(() => {
1918
+ if (!options.prompt) return;
1919
+ controller.setDraft(draftOf(options.prompt));
1920
+ controller.submit();
1921
+ });
1922
+ try {
1923
+ await Promise.race([exited, instance.waitUntilExit()]);
1924
+ } finally {
1925
+ controller.dispose();
1926
+ instance.unmount();
1927
+ }
1928
+ return { sessionId: controller.getState().status.sessionId };
1929
+ }
1930
+ async function runTui(options) {
1931
+ const connection = memoryTuiConnection(
1932
+ options.connection,
1933
+ { provider: options.providerLabel, usingChatGPTPlan: options.usingChatGPTPlan, mode: options.mode }
1934
+ );
1935
+ const { sessionId } = await run(connection, {
1936
+ cwd: options.cwd,
1937
+ sessionId: options.sessionId,
1938
+ prompt: options.prompt,
1939
+ version: options.version,
1940
+ history: options.historyPath ? fileHistory(options.historyPath) : void 0,
1941
+ stdin: options.stdin,
1942
+ stdout: options.stdout,
1943
+ stderr: options.stderr
1944
+ });
1945
+ if (sessionId) (options.stderr ?? process.stderr).write(`Resume this session with: oxbit-agent -r ${sessionId}
1946
+ `);
1947
+ return 0;
1948
+ }
1949
+ export {
1950
+ markdownToAnsi,
1951
+ run,
1952
+ runTui
1953
+ };
1954
+ //# sourceMappingURL=index-BrBnidfI.js.map