@wrongstack/tui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2421 @@
1
+ import React, { useReducer, useRef, useEffect, useMemo } from 'react';
2
+ import { render, useApp, Box, useStdout, Static, Text, useInput } from 'ink';
3
+ import * as path2 from 'path';
4
+ import * as fs2 from 'fs/promises';
5
+ import { InputBuilder } from '@wrongstack/core';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
+ import { spawn } from 'child_process';
8
+ import * as os from 'os';
9
+
10
+ // src/run-tui.ts
11
+
12
+ // src/markdown-table.ts
13
+ function renderMarkdownTables(text, maxWidth) {
14
+ if (!text.includes("|")) return text;
15
+ const lines = text.split("\n");
16
+ const out = [];
17
+ let i = 0;
18
+ while (i < lines.length) {
19
+ const end = detectTable(lines, i);
20
+ if (end > i) {
21
+ out.push(renderTable(lines.slice(i, end), Math.max(20, maxWidth)));
22
+ i = end;
23
+ } else {
24
+ out.push(lines[i] ?? "");
25
+ i++;
26
+ }
27
+ }
28
+ return out.join("\n");
29
+ }
30
+ var ROW_RE = /^\s*\|.*\|\s*$/;
31
+ var SEP_RE = /^\s*\|[\s\-:|]+\|\s*$/;
32
+ function detectTable(lines, start) {
33
+ if (start + 1 >= lines.length) return start;
34
+ if (!ROW_RE.test(lines[start] ?? "")) return start;
35
+ const sep = lines[start + 1] ?? "";
36
+ if (!SEP_RE.test(sep) || !/-/.test(sep)) return start;
37
+ let end = start + 2;
38
+ while (end < lines.length && ROW_RE.test(lines[end] ?? "")) end++;
39
+ return end;
40
+ }
41
+ function parseCells(line) {
42
+ const inner = line.trim().replace(/^\||\|$/g, "");
43
+ const parts = [];
44
+ let buf = "";
45
+ for (let i = 0; i < inner.length; i++) {
46
+ const ch = inner[i];
47
+ if (ch === "\\" && inner[i + 1] === "|") {
48
+ buf += "|";
49
+ i++;
50
+ continue;
51
+ }
52
+ if (ch === "|") {
53
+ parts.push(buf);
54
+ buf = "";
55
+ continue;
56
+ }
57
+ buf += ch;
58
+ }
59
+ parts.push(buf);
60
+ return parts.map((c) => c.trim());
61
+ }
62
+ function parseAlign(sep) {
63
+ const t = sep.trim();
64
+ const left = t.startsWith(":");
65
+ const right = t.endsWith(":");
66
+ if (left && right) return "center";
67
+ if (right) return "right";
68
+ return "left";
69
+ }
70
+ function renderTable(tableLines, maxWidth) {
71
+ const header = parseCells(tableLines[0] ?? "");
72
+ const sepCells = parseCells(tableLines[1] ?? "");
73
+ const cols = header.length;
74
+ const aligns = [];
75
+ for (let c = 0; c < cols; c++) {
76
+ aligns.push(parseAlign(sepCells[c] ?? ""));
77
+ }
78
+ const dataRows = tableLines.slice(2).map(parseCells);
79
+ for (const row of dataRows) {
80
+ while (row.length < cols) row.push("");
81
+ row.length = cols;
82
+ }
83
+ const widths = computeWidths([header, ...dataRows], cols, maxWidth);
84
+ const lines = [];
85
+ lines.push(border("\u250C", "\u252C", "\u2510", widths));
86
+ lines.push(...renderRow(header, widths, aligns));
87
+ lines.push(border("\u251C", "\u253C", "\u2524", widths));
88
+ for (const row of dataRows) {
89
+ lines.push(...renderRow(row, widths, aligns));
90
+ }
91
+ lines.push(border("\u2514", "\u2534", "\u2518", widths));
92
+ return lines.join("\n");
93
+ }
94
+ function computeWidths(allRows, cols, maxWidth) {
95
+ const overhead = 3 * cols + 1;
96
+ const avail = Math.max(cols * MIN_COL_WIDTH, maxWidth - overhead);
97
+ const natural = new Array(cols).fill(0);
98
+ for (const row of allRows) {
99
+ for (let c = 0; c < cols; c++) {
100
+ const cell = row[c] ?? "";
101
+ const w = longestWord(cell);
102
+ const total = cell.length;
103
+ natural[c] = Math.max(natural[c], total);
104
+ if (w > natural[c]) natural[c] = Math.min(total + 1, w);
105
+ }
106
+ }
107
+ const sumNatural = natural.reduce((s, n) => s + n, 0);
108
+ if (sumNatural <= avail) return natural;
109
+ const widths = natural.slice();
110
+ let sum = sumNatural;
111
+ while (sum > avail) {
112
+ let maxIdx = -1;
113
+ let maxVal = MIN_COL_WIDTH;
114
+ for (let i = 0; i < cols; i++) {
115
+ const w = widths[i];
116
+ if (w > maxVal) {
117
+ maxVal = w;
118
+ maxIdx = i;
119
+ }
120
+ }
121
+ if (maxIdx < 0) break;
122
+ widths[maxIdx]--;
123
+ sum--;
124
+ }
125
+ return widths;
126
+ }
127
+ var MIN_COL_WIDTH = 4;
128
+ function longestWord(s) {
129
+ let max = 0;
130
+ for (const w of s.split(/\s+/)) if (w.length > max) max = w.length;
131
+ return max;
132
+ }
133
+ function border(left, mid, right, widths) {
134
+ return left + widths.map((w) => "\u2500".repeat(w + 2)).join(mid) + right;
135
+ }
136
+ function renderRow(cells, widths, aligns) {
137
+ const wrapped = cells.map((c, i) => wrapCell(c, widths[i] ?? MIN_COL_WIDTH));
138
+ const height = Math.max(1, ...wrapped.map((w) => w.length));
139
+ const out = [];
140
+ for (let line = 0; line < height; line++) {
141
+ const parts = [];
142
+ for (let c = 0; c < widths.length; c++) {
143
+ const w = widths[c] ?? MIN_COL_WIDTH;
144
+ const text = wrapped[c]?.[line] ?? "";
145
+ parts.push(padCell(text, w, aligns[c] ?? "left"));
146
+ }
147
+ out.push("\u2502 " + parts.join(" \u2502 ") + " \u2502");
148
+ }
149
+ return out;
150
+ }
151
+ function wrapCell(text, width) {
152
+ if (text.length <= width) return [text];
153
+ const out = [];
154
+ const words = text.split(/(\s+)/);
155
+ let cur = "";
156
+ for (const word of words) {
157
+ if (!word) continue;
158
+ if (cur.length + word.length <= width) {
159
+ cur += word;
160
+ continue;
161
+ }
162
+ if (cur) {
163
+ out.push(cur.trimEnd());
164
+ cur = "";
165
+ }
166
+ if (word.length > width) {
167
+ let rest = word;
168
+ while (rest.length > width) {
169
+ out.push(rest.slice(0, width));
170
+ rest = rest.slice(width);
171
+ }
172
+ cur = rest;
173
+ } else if (!/^\s+$/.test(word)) {
174
+ cur = word;
175
+ }
176
+ }
177
+ if (cur) out.push(cur.trimEnd());
178
+ return out.length === 0 ? [""] : out;
179
+ }
180
+ function padCell(text, width, align) {
181
+ if (text.length >= width) return text.slice(0, width);
182
+ const pad = width - text.length;
183
+ if (align === "right") return " ".repeat(pad) + text;
184
+ if (align === "center") {
185
+ const l = Math.floor(pad / 2);
186
+ return " ".repeat(l) + text + " ".repeat(pad - l);
187
+ }
188
+ return text + " ".repeat(pad);
189
+ }
190
+ function History({ entries, streamingText }) {
191
+ const { stdout } = useStdout();
192
+ const termWidth = stdout?.columns ?? 80;
193
+ const tail = streamingText ? tailForDisplay(streamingText, MAX_STREAM_DISPLAY_CHARS) : "";
194
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
195
+ /* @__PURE__ */ jsx(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx(Box, { marginBottom: entry.kind === "turn-summary" ? 1 : 0, children: /* @__PURE__ */ jsx(Entry, { entry, termWidth }) }, entry.id) }),
196
+ tail ? /* @__PURE__ */ jsxs(Box, { children: [
197
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "> " }),
198
+ /* @__PURE__ */ jsx(Text, { children: tail })
199
+ ] }) : null
200
+ ] });
201
+ }
202
+ var MAX_STREAM_DISPLAY_CHARS = 480;
203
+ function tailForDisplay(text, maxChars) {
204
+ if (text.length <= maxChars) return text;
205
+ const cut = text.length - maxChars;
206
+ const nl = text.indexOf("\n", cut);
207
+ if (nl !== -1 && nl < cut + 80) {
208
+ return `\u2026 ${text.slice(nl + 1)}`;
209
+ }
210
+ return `\u2026 ${text.slice(cut)}`;
211
+ }
212
+ function DiffBlock({ rows, hidden }) {
213
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 4, marginTop: 0, children: [
214
+ rows.map((row, i) => {
215
+ if (row.kind === "hunk") {
216
+ return /* @__PURE__ */ jsx(Text, { color: "cyan", dimColor: true, children: row.text }, i);
217
+ }
218
+ if (row.kind === "meta") {
219
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: row.text }, i);
220
+ }
221
+ if (row.kind === "ctx") {
222
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: row.text }, i);
223
+ }
224
+ const bg = row.kind === "add" ? "green" : "red";
225
+ const fg = row.kind === "add" ? "black" : "white";
226
+ return /* @__PURE__ */ jsx(Text, { backgroundColor: bg, color: fg, children: row.text }, i);
227
+ }),
228
+ hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: ` \u2026 ${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
229
+ ] });
230
+ }
231
+ function Entry({ entry, termWidth }) {
232
+ switch (entry.kind) {
233
+ case "user":
234
+ return /* @__PURE__ */ jsxs(Text, { children: [
235
+ /* @__PURE__ */ jsx(Text, { color: entry.queued ? "yellow" : "cyan", children: entry.queued ? "\u231B" : "\u203A" }),
236
+ " ",
237
+ /* @__PURE__ */ jsx(Text, { dimColor: entry.queued ?? false, children: entry.text }),
238
+ entry.queued ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (queued)" }) : null
239
+ ] });
240
+ case "assistant":
241
+ return /* @__PURE__ */ jsx(Text, { children: renderMarkdownTables(entry.text, termWidth) });
242
+ case "tool": {
243
+ const argSummary = formatToolArgs(entry.name, entry.input);
244
+ const outLines = formatToolOutput(entry.name, entry.output, entry.ok);
245
+ const diff = entry.ok ? extractDiffPreview(entry.name, entry.output) : void 0;
246
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
247
+ /* @__PURE__ */ jsxs(Text, { children: [
248
+ /* @__PURE__ */ jsx(Text, { color: entry.ok ? "green" : "red", children: entry.ok ? "\u25CF" : "\u2717" }),
249
+ " ",
250
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: entry.name }),
251
+ argSummary ? /* @__PURE__ */ jsxs(Fragment, { children: [
252
+ /* @__PURE__ */ jsx(Text, { children: " " }),
253
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: argSummary })
254
+ ] }) : null,
255
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${fmtDuration(entry.durationMs)}` })
256
+ ] }),
257
+ outLines.map((line, i) => {
258
+ const isLast = i === outLines.length - 1 && !diff;
259
+ const prefix = isLast ? " \u2514\u2500 " : " \u251C\u2500 ";
260
+ const errish = !entry.ok || line.startsWith("!");
261
+ return /* @__PURE__ */ jsxs(Text, { children: [
262
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: prefix }),
263
+ /* @__PURE__ */ jsx(Text, { color: errish ? "red" : void 0, dimColor: !errish, children: line })
264
+ ] }, i);
265
+ }),
266
+ diff ? /* @__PURE__ */ jsx(DiffBlock, { rows: diff.rows, hidden: diff.hidden }) : null
267
+ ] });
268
+ }
269
+ case "info":
270
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.text });
271
+ case "warn":
272
+ return /* @__PURE__ */ jsx(Text, { color: "yellow", children: entry.text });
273
+ case "error":
274
+ return /* @__PURE__ */ jsx(Text, { color: "red", children: entry.text });
275
+ case "turn-summary":
276
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.text });
277
+ case "banner":
278
+ return /* @__PURE__ */ jsx(Banner, { entry });
279
+ }
280
+ }
281
+ function Banner({
282
+ entry
283
+ }) {
284
+ const cwdShort = shortenPath(entry.cwd, 48);
285
+ return /* @__PURE__ */ jsxs(
286
+ Box,
287
+ {
288
+ flexDirection: "column",
289
+ borderStyle: "round",
290
+ borderColor: "magenta",
291
+ paddingX: 2,
292
+ paddingY: 0,
293
+ children: [
294
+ /* @__PURE__ */ jsxs(Text, { children: [
295
+ /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: " \u259F\u259B " }),
296
+ /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "WrongStack" }),
297
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " v" }),
298
+ /* @__PURE__ */ jsx(Text, { children: entry.version })
299
+ ] }),
300
+ /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: " Built on the wrong stack. Shipped anyway." }),
301
+ /* @__PURE__ */ jsxs(Text, { children: [
302
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " provider " }),
303
+ /* @__PURE__ */ jsxs(Text, { children: [
304
+ entry.provider,
305
+ "/",
306
+ entry.model
307
+ ] })
308
+ ] }),
309
+ /* @__PURE__ */ jsxs(Text, { children: [
310
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " cwd " }),
311
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: cwdShort })
312
+ ] }),
313
+ /* @__PURE__ */ jsxs(Text, { children: [
314
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " hints " }),
315
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/help \xB7 /init \xB7 /memory \xB7 /queue \xB7 /exit" })
316
+ ] })
317
+ ]
318
+ }
319
+ );
320
+ }
321
+ function shortenPath(p, max) {
322
+ if (p.length <= max) return p;
323
+ return `\u2026${p.slice(p.length - (max - 1))}`;
324
+ }
325
+ function fmtDuration(ms) {
326
+ if (ms < 1e3) return `${ms}ms`;
327
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
328
+ const totalSec = Math.floor(ms / 1e3);
329
+ return `${Math.floor(totalSec / 60)}m${totalSec % 60}s`;
330
+ }
331
+ var ARG_BUDGET = 60;
332
+ var OUT_BUDGET = 80;
333
+ function formatToolArgs(toolName, input) {
334
+ if (!input || typeof input !== "object") return "";
335
+ const obj = input;
336
+ switch (toolName) {
337
+ case "read":
338
+ case "write":
339
+ case "edit":
340
+ case "patch":
341
+ case "document":
342
+ case "list_dir":
343
+ case "ls":
344
+ case "tree": {
345
+ const p = stringOf(obj["path"]) ?? stringOf(obj["file"]);
346
+ return p ? shortenPath(p, ARG_BUDGET) : "";
347
+ }
348
+ case "grep":
349
+ case "search":
350
+ case "replace": {
351
+ const pat = stringOf(obj["pattern"]) ?? stringOf(obj["query"]);
352
+ const scope = stringOf(obj["path"]) ?? stringOf(obj["glob"]);
353
+ const head = pat ? `"${truncMid(pat, 36)}"` : "";
354
+ const tail = scope ? ` in ${shortenPath(scope, 28)}` : "";
355
+ return `${head}${tail}` || (stringOf(obj["command"]) ?? "");
356
+ }
357
+ case "glob": {
358
+ const pat = stringOf(obj["pattern"]) ?? stringOf(obj["glob"]);
359
+ return pat ? `"${truncMid(pat, ARG_BUDGET - 2)}"` : "";
360
+ }
361
+ case "bash":
362
+ case "shell":
363
+ case "exec":
364
+ case "install":
365
+ case "git": {
366
+ const cmd = stringOf(obj["command"]) ?? stringOf(obj["args"]);
367
+ return cmd ? truncMid(cmd, ARG_BUDGET) : "";
368
+ }
369
+ case "diff": {
370
+ const files = Array.isArray(obj["files"]) ? obj["files"] : void 0;
371
+ if (files && files.length > 0) {
372
+ const head = stringOf(files[0]) ?? "";
373
+ const rest = files.length > 1 ? ` (+${files.length - 1})` : "";
374
+ return head ? `${shortenPath(head, 50)}${rest}` : "";
375
+ }
376
+ const mode = stringOf(obj["mode"]);
377
+ return mode ? `mode: ${mode}` : "";
378
+ }
379
+ case "fetch":
380
+ case "webfetch":
381
+ case "web_fetch": {
382
+ const u = stringOf(obj["url"]);
383
+ return u ? truncMid(u, ARG_BUDGET) : "";
384
+ }
385
+ case "todo": {
386
+ const list = obj["todos"];
387
+ if (Array.isArray(list)) return `${list.length} item${list.length === 1 ? "" : "s"}`;
388
+ return "";
389
+ }
390
+ case "lint":
391
+ case "format":
392
+ case "typecheck":
393
+ case "test":
394
+ case "audit":
395
+ case "outdated": {
396
+ const files = obj["files"];
397
+ if (Array.isArray(files) && files.length > 0) {
398
+ const first = stringOf(files[0]);
399
+ const more = files.length > 1 ? ` (+${files.length - 1})` : "";
400
+ return first ? `${shortenPath(first, 50)}${more}` : `${files.length} files`;
401
+ }
402
+ const filter = stringOf(obj["filter"]) ?? stringOf(obj["pattern"]);
403
+ return filter ? `"${truncMid(filter, ARG_BUDGET - 2)}"` : "";
404
+ }
405
+ case "json": {
406
+ const file = stringOf(obj["file"]);
407
+ const q = stringOf(obj["query"]);
408
+ if (file) return q ? `${shortenPath(file, 40)} ${q}` : shortenPath(file, ARG_BUDGET);
409
+ return q ? truncMid(q, ARG_BUDGET) : "";
410
+ }
411
+ case "scaffold": {
412
+ const tmpl = stringOf(obj["template"]) ?? stringOf(obj["type"]);
413
+ const name = stringOf(obj["name"]);
414
+ if (tmpl && name) return `${tmpl} \u2192 ${truncMid(name, ARG_BUDGET - tmpl.length - 4)}`;
415
+ return name ?? tmpl ?? "";
416
+ }
417
+ case "remember":
418
+ case "forget":
419
+ case "memory": {
420
+ const key = stringOf(obj["key"]) ?? stringOf(obj["name"]);
421
+ return key ? truncMid(key, ARG_BUDGET) : "";
422
+ }
423
+ case "mode": {
424
+ const m = stringOf(obj["mode"]) ?? stringOf(obj["name"]);
425
+ return m ? truncMid(m, ARG_BUDGET) : "";
426
+ }
427
+ case "logs": {
428
+ const target = stringOf(obj["target"]) ?? stringOf(obj["service"]) ?? stringOf(obj["path"]);
429
+ return target ? truncMid(target, ARG_BUDGET) : "";
430
+ }
431
+ }
432
+ for (const key of ["path", "file", "url", "name", "query", "pattern", "command"]) {
433
+ const v = stringOf(obj[key]);
434
+ if (v) return truncMid(v, ARG_BUDGET);
435
+ }
436
+ try {
437
+ return truncMid(JSON.stringify(obj), ARG_BUDGET);
438
+ } catch {
439
+ return "";
440
+ }
441
+ }
442
+ function formatToolOutput(toolName, output, ok) {
443
+ if (!output) return ok ? [] : ["failed"];
444
+ const text = output.trim();
445
+ if (!text) return ok ? [] : ["failed"];
446
+ const json = tryParseJson(text);
447
+ if (toolName === "write") {
448
+ if (json && typeof json === "object") {
449
+ const o = json;
450
+ const bytes = numOf(o["bytes_written"]) ?? numOf(o["bytes"]);
451
+ const created = o["created"] === true;
452
+ const tag = created ? "created" : "updated";
453
+ if (bytes !== void 0) return [`${tag} \xB7 ${fmtBytes(bytes)}`];
454
+ return [tag];
455
+ }
456
+ }
457
+ if (toolName === "edit") {
458
+ if (json && typeof json === "object") {
459
+ const o = json;
460
+ const reps = numOf(o["replacements"]);
461
+ if (reps !== void 0) return [`${reps} replacement${reps === 1 ? "" : "s"}`];
462
+ }
463
+ }
464
+ if (toolName === "patch") {
465
+ if (json && typeof json === "object") {
466
+ const o = json;
467
+ const applied = numOf(o["applied"]);
468
+ const rejected = numOf(o["rejected"]);
469
+ const files = Array.isArray(o["files"]) ? o["files"] : void 0;
470
+ const lines = [];
471
+ if (applied !== void 0 || rejected !== void 0) {
472
+ const parts = [];
473
+ if (applied !== void 0) parts.push(`${applied} applied`);
474
+ if (rejected !== void 0 && rejected > 0) parts.push(`${rejected} rejected`);
475
+ lines.push(parts.join(" \xB7 "));
476
+ }
477
+ if (files && files.length > 0) {
478
+ const first = stringOf(files[0]) ?? "";
479
+ const more = files.length > 1 ? ` (+${files.length - 1})` : "";
480
+ lines.push(`${shortenPath(first, 60)}${more}`);
481
+ }
482
+ if (lines.length > 0) return lines;
483
+ }
484
+ }
485
+ if (toolName === "replace") {
486
+ if (json && typeof json === "object") {
487
+ const o = json;
488
+ const files = numOf(o["files_modified"]);
489
+ const reps = numOf(o["total_replacements"]);
490
+ if (files !== void 0 && reps !== void 0) {
491
+ return [
492
+ `${reps} replacement${reps === 1 ? "" : "s"} in ${files} file${files === 1 ? "" : "s"}`
493
+ ];
494
+ }
495
+ }
496
+ }
497
+ if (toolName === "diff") {
498
+ if (json && typeof json === "object") {
499
+ const o = json;
500
+ const files = Array.isArray(o["files"]) ? o["files"] : void 0;
501
+ const truncated = o["truncated"] === true;
502
+ const mode = stringOf(o["mode"]);
503
+ const diff = stringOf(o["diff"]);
504
+ if (!diff) return [files && files.length === 0 ? "no changes" : "empty diff"];
505
+ const head = [];
506
+ if (mode) head.push(mode);
507
+ if (files && files.length > 0) head.push(`${files.length} file${files.length === 1 ? "" : "s"}`);
508
+ if (truncated) head.push("truncated");
509
+ return head.length > 0 ? [head.join(" \xB7 ")] : [];
510
+ }
511
+ }
512
+ if (toolName === "read") {
513
+ if (json && typeof json === "object") {
514
+ const o = json;
515
+ const bytes = numOf(o["bytes"]);
516
+ if (bytes !== void 0) return [`${fmtBytes(bytes)} read`];
517
+ }
518
+ const range = scanNumberedRange(text);
519
+ if (range.count > 0 && range.first !== void 0 && range.last !== void 0) {
520
+ if (range.first === range.last) {
521
+ return [`L${range.first} \xB7 ${fmtBytes(text.length)}`];
522
+ }
523
+ const contiguous = range.count === range.last - range.first + 1;
524
+ const head = `L${range.first}\u2013${range.last}`;
525
+ const tail = contiguous ? `${range.count} line${range.count === 1 ? "" : "s"}` : `${range.count} lines (gaps)`;
526
+ return [`${head} \xB7 ${tail} \xB7 ${fmtBytes(text.length)}`];
527
+ }
528
+ }
529
+ if (toolName === "grep" || toolName === "glob") {
530
+ if (json && typeof json === "object") {
531
+ const o = json;
532
+ const matches = Array.isArray(o["matches"]) ? o["matches"] : void 0;
533
+ const count = numOf(o["count"]) ?? matches?.length;
534
+ const truncated = o["truncated"] === true;
535
+ if (count !== void 0) {
536
+ if (count === 0) return ["no matches"];
537
+ const lines = [
538
+ `${count} match${count === 1 ? "" : "es"}${truncated ? " (truncated)" : ""}`
539
+ ];
540
+ const firstHit = matches && matches.length > 0 ? formatMatchHit(matches[0]) : void 0;
541
+ if (firstHit) lines.push(firstHit);
542
+ return lines;
543
+ }
544
+ }
545
+ }
546
+ if (toolName === "bash" || toolName === "shell") {
547
+ if (json && typeof json === "object") {
548
+ const o = json;
549
+ const exit = numOf(o["exit_code"]) ?? numOf(o["exitCode"]);
550
+ const stdout = stringOf(o["stdout"]) ?? "";
551
+ const stderr = stringOf(o["stderr"]) ?? "";
552
+ const stdoutLines = countLines(stdout);
553
+ const stderrLines = countLines(stderr);
554
+ const head = [];
555
+ if (exit !== void 0) head.push(`exit ${exit}`);
556
+ const lineParts = [];
557
+ if (stdoutLines > 0) lineParts.push(`${stdoutLines} out`);
558
+ if (stderrLines > 0) lineParts.push(`${stderrLines} err`);
559
+ if (lineParts.length > 0) head.push(lineParts.join(" \xB7 "));
560
+ const lines = [];
561
+ if (head.length > 0) lines.push(head.join(" \xB7 "));
562
+ const stdoutPreview = firstNonEmpty(stdout);
563
+ const stderrPreview = firstNonEmpty(stderr);
564
+ if (stdoutPreview) lines.push(`"${truncMid(stdoutPreview, 70)}"`);
565
+ if (stderrPreview && stderrPreview !== stdoutPreview) {
566
+ lines.push(`! "${truncMid(stderrPreview, 70)}"`);
567
+ }
568
+ if (lines.length > 0) return lines;
569
+ }
570
+ }
571
+ if (toolName === "todo") {
572
+ return ok ? [] : [text.split("\n")[0] ?? ""];
573
+ }
574
+ if (toolName === "fetch" || toolName === "webfetch" || toolName === "web_fetch") {
575
+ if (json && typeof json === "object") {
576
+ const o = json;
577
+ const status = numOf(o["status"]);
578
+ const ct = stringOf(o["content_type"]);
579
+ const url = stringOf(o["url"]);
580
+ const content = stringOf(o["content"]);
581
+ const head = [];
582
+ if (status !== void 0) head.push(`HTTP ${status}`);
583
+ if (ct) head.push(ct.split(";")[0] ?? ct);
584
+ if (content) head.push(fmtBytes(Buffer.byteLength(content, "utf8")));
585
+ const lines = [];
586
+ if (head.length > 0) lines.push(head.join(" \xB7 "));
587
+ if (url && status !== void 0 && (status < 200 || status >= 400)) {
588
+ lines.push(shortenPath(url, 70));
589
+ }
590
+ if (lines.length > 0) return lines;
591
+ }
592
+ }
593
+ if (toolName === "git") {
594
+ if (json && typeof json === "object") {
595
+ const o = json;
596
+ const exit = numOf(o["exitCode"]) ?? numOf(o["exit_code"]);
597
+ const stdout = stringOf(o["stdout"]) ?? "";
598
+ const stderr = stringOf(o["stderr"]) ?? "";
599
+ const head = [];
600
+ if (exit !== void 0) head.push(`exit ${exit}`);
601
+ const stdoutLines = countLines(stdout);
602
+ const stderrLines = countLines(stderr);
603
+ const lparts = [];
604
+ if (stdoutLines > 0) lparts.push(`${stdoutLines} out`);
605
+ if (stderrLines > 0) lparts.push(`${stderrLines} err`);
606
+ if (lparts.length > 0) head.push(lparts.join(" \xB7 "));
607
+ const lines = [];
608
+ if (head.length > 0) lines.push(head.join(" \xB7 "));
609
+ const preview = firstNonEmpty(stdout) ?? firstNonEmpty(stderr);
610
+ if (preview) lines.push(`"${truncMid(preview, 70)}"`);
611
+ if (lines.length > 0) return lines;
612
+ }
613
+ }
614
+ if (toolName === "lint") {
615
+ if (json && typeof json === "object") {
616
+ const o = json;
617
+ const linter = stringOf(o["linter"]);
618
+ const files = numOf(o["files_checked"]);
619
+ const errors = numOf(o["errors"]) ?? 0;
620
+ const warnings = numOf(o["warnings"]) ?? 0;
621
+ const fix = o["fix_applied"] === true;
622
+ const head = [];
623
+ if (linter && linter !== "none") head.push(linter);
624
+ head.push(`${errors} error${errors === 1 ? "" : "s"}`);
625
+ head.push(`${warnings} warning${warnings === 1 ? "" : "s"}`);
626
+ if (files !== void 0) head.push(`${files} file${files === 1 ? "" : "s"}`);
627
+ if (fix) head.push("fixed");
628
+ return [head.join(" \xB7 ")];
629
+ }
630
+ }
631
+ if (toolName === "format") {
632
+ if (json && typeof json === "object") {
633
+ const o = json;
634
+ const fixer = stringOf(o["fixer"]);
635
+ const checked = numOf(o["files_checked"]);
636
+ const changed = numOf(o["files_changed"]);
637
+ const head = [];
638
+ if (fixer && fixer !== "none") head.push(fixer);
639
+ if (changed !== void 0 && checked !== void 0) {
640
+ head.push(`${changed}/${checked} changed`);
641
+ } else if (changed !== void 0) {
642
+ head.push(`${changed} changed`);
643
+ }
644
+ return head.length > 0 ? [head.join(" \xB7 ")] : [];
645
+ }
646
+ }
647
+ if (toolName === "typecheck") {
648
+ if (json && typeof json === "object") {
649
+ const o = json;
650
+ const exit = numOf(o["exit_code"]) ?? numOf(o["exitCode"]);
651
+ const errors = numOf(o["errors"]);
652
+ const head = [];
653
+ if (errors !== void 0) head.push(`${errors} error${errors === 1 ? "" : "s"}`);
654
+ if (exit !== void 0) head.push(`exit ${exit}`);
655
+ const stdout = stringOf(o["output"]) ?? stringOf(o["stdout"]) ?? "";
656
+ const lines = [];
657
+ if (head.length > 0) lines.push(head.join(" \xB7 "));
658
+ const preview = firstNonEmpty(stdout);
659
+ if (preview && (!errors || errors > 0)) lines.push(`"${truncMid(preview, 70)}"`);
660
+ if (lines.length > 0) return lines;
661
+ }
662
+ }
663
+ if (toolName === "test") {
664
+ if (json && typeof json === "object") {
665
+ const o = json;
666
+ const runner = stringOf(o["runner"]);
667
+ const total = numOf(o["tests_run"]) ?? 0;
668
+ const passed = numOf(o["passed"]) ?? 0;
669
+ const failed = numOf(o["failed"]) ?? 0;
670
+ const duration = numOf(o["duration_ms"]);
671
+ const head = [];
672
+ if (runner && runner !== "none") head.push(runner);
673
+ head.push(`${passed}/${total} passed`);
674
+ if (failed > 0) head.push(`${failed} failed`);
675
+ if (duration !== void 0) head.push(fmtDuration(duration));
676
+ return [head.join(" \xB7 ")];
677
+ }
678
+ }
679
+ if (toolName === "audit") {
680
+ if (json && typeof json === "object") {
681
+ const o = json;
682
+ const total = numOf(o["total"]) ?? 0;
683
+ const summary = stringOf(o["summary"]);
684
+ if (total === 0) return ["no vulnerabilities"];
685
+ const head = `${total} vulnerabilit${total === 1 ? "y" : "ies"}`;
686
+ return summary && summary.toLowerCase() !== head.toLowerCase() ? [head, truncMid(summary, OUT_BUDGET)] : [head];
687
+ }
688
+ }
689
+ if (toolName === "outdated") {
690
+ if (json && typeof json === "object") {
691
+ const o = json;
692
+ const total = numOf(o["total"]) ?? 0;
693
+ const pkgs = Array.isArray(o["packages"]) ? o["packages"] : void 0;
694
+ if (total === 0) return ["all up to date"];
695
+ const lines = [`${total} outdated`];
696
+ if (pkgs && pkgs.length > 0) {
697
+ const first = pkgs[0];
698
+ if (first && typeof first === "object") {
699
+ const p = first;
700
+ const name = stringOf(p["name"]) ?? stringOf(p["package"]);
701
+ const cur = stringOf(p["current"]);
702
+ const wanted = stringOf(p["wanted"]) ?? stringOf(p["latest"]);
703
+ if (name && cur && wanted) lines.push(`${name}: ${cur} \u2192 ${wanted}`);
704
+ else if (name) lines.push(name);
705
+ }
706
+ }
707
+ return lines;
708
+ }
709
+ }
710
+ if (toolName === "tree") {
711
+ if (json && typeof json === "object") {
712
+ const o = json;
713
+ const files = numOf(o["total_files"]);
714
+ const dirs = numOf(o["total_dirs"]);
715
+ const truncated = o["truncated"] === true;
716
+ const parts = [];
717
+ if (files !== void 0) parts.push(`${files} file${files === 1 ? "" : "s"}`);
718
+ if (dirs !== void 0) parts.push(`${dirs} dir${dirs === 1 ? "" : "s"}`);
719
+ if (truncated) parts.push("truncated");
720
+ return parts.length > 0 ? [parts.join(" \xB7 ")] : [];
721
+ }
722
+ }
723
+ if (toolName === "json") {
724
+ if (json && typeof json === "object") {
725
+ const o = json;
726
+ const err = stringOf(o["error"]);
727
+ if (err) return [truncMid(err, OUT_BUDGET)];
728
+ const type = stringOf(o["type"]);
729
+ const keys = Array.isArray(o["keys"]) ? o["keys"] : void 0;
730
+ const parts = [];
731
+ if (type) parts.push(type);
732
+ if (keys) parts.push(`${keys.length} key${keys.length === 1 ? "" : "s"}`);
733
+ return parts.length > 0 ? [parts.join(" \xB7 ")] : [];
734
+ }
735
+ }
736
+ if (toolName === "install") {
737
+ if (json && typeof json === "object") {
738
+ const o = json;
739
+ const exit = numOf(o["exit_code"]) ?? numOf(o["exitCode"]);
740
+ const added = numOf(o["added"]);
741
+ const removed = numOf(o["removed"]);
742
+ const head = [];
743
+ if (exit !== void 0) head.push(`exit ${exit}`);
744
+ if (added !== void 0) head.push(`+${added}`);
745
+ if (removed !== void 0) head.push(`-${removed}`);
746
+ const stdout = stringOf(o["stdout"]) ?? stringOf(o["output"]) ?? "";
747
+ const lines = [];
748
+ if (head.length > 0) lines.push(head.join(" \xB7 "));
749
+ const preview = firstNonEmpty(stdout);
750
+ if (preview) lines.push(`"${truncMid(preview, 70)}"`);
751
+ if (lines.length > 0) return lines;
752
+ }
753
+ }
754
+ if (toolName === "scaffold") {
755
+ if (json && typeof json === "object") {
756
+ const o = json;
757
+ const created = Array.isArray(o["created"]) ? o["created"] : void 0;
758
+ const skipped = Array.isArray(o["skipped"]) ? o["skipped"] : void 0;
759
+ const parts = [];
760
+ if (created !== void 0) parts.push(`${created.length} created`);
761
+ if (skipped !== void 0 && skipped.length > 0) parts.push(`${skipped.length} skipped`);
762
+ if (parts.length > 0) return [parts.join(" \xB7 ")];
763
+ }
764
+ }
765
+ if (toolName === "remember" || toolName === "forget" || toolName === "memory") {
766
+ return ok ? [toolName === "forget" ? "removed" : "saved"] : [text.split("\n")[0] ?? ""];
767
+ }
768
+ if (toolName === "mode") {
769
+ if (json && typeof json === "object") {
770
+ const o = json;
771
+ const mode = stringOf(o["mode"]) ?? stringOf(o["active"]) ?? stringOf(o["name"]);
772
+ if (mode) return [`mode: ${mode}`];
773
+ }
774
+ }
775
+ if (toolName === "search") {
776
+ if (json && typeof json === "object") {
777
+ const o = json;
778
+ const matches = Array.isArray(o["matches"]) ? o["matches"] : Array.isArray(o["results"]) ? o["results"] : void 0;
779
+ const count = numOf(o["count"]) ?? matches?.length;
780
+ if (count !== void 0) {
781
+ if (count === 0) return ["no results"];
782
+ const lines = [`${count} result${count === 1 ? "" : "s"}`];
783
+ const firstHit = matches && matches.length > 0 ? formatMatchHit(matches[0]) : void 0;
784
+ if (firstHit) lines.push(firstHit);
785
+ return lines;
786
+ }
787
+ }
788
+ }
789
+ if (toolName === "logs") {
790
+ const lines = text.split("\n").filter((l) => l.trim());
791
+ if (lines.length === 0) return [];
792
+ const head = `${lines.length} line${lines.length === 1 ? "" : "s"}`;
793
+ const lastLine = lines[lines.length - 1];
794
+ return lastLine ? [head, `"${truncMid(lastLine.trim(), 70)}"`] : [head];
795
+ }
796
+ const firstLine = text.split("\n").find((l) => l.trim()) ?? text;
797
+ return [truncMid(firstLine.replace(/\s+/g, " "), OUT_BUDGET)];
798
+ }
799
+ function firstNonEmpty(text) {
800
+ if (!text) return void 0;
801
+ const line = text.split("\n").find((l) => l.trim());
802
+ return line ? line.replace(/\s+/g, " ").trim() : void 0;
803
+ }
804
+ function formatMatchHit(hit) {
805
+ if (typeof hit === "string") return truncMid(hit, 70);
806
+ if (hit && typeof hit === "object") {
807
+ const o = hit;
808
+ const file = stringOf(o["file"]) ?? stringOf(o["path"]);
809
+ const line = numOf(o["line"]) ?? numOf(o["lineNumber"]);
810
+ const snippet = stringOf(o["text"]) ?? stringOf(o["match"]) ?? stringOf(o["preview"]);
811
+ if (file) {
812
+ const head = line !== void 0 ? `${shortenPath(file, 40)}:${line}` : shortenPath(file, 50);
813
+ return snippet ? `${head} ${truncMid(snippet.replace(/\s+/g, " "), 40)}` : head;
814
+ }
815
+ if (snippet) return truncMid(snippet, 70);
816
+ }
817
+ return void 0;
818
+ }
819
+ var DIFF_MAX_LINES = 8;
820
+ function extractDiffPreview(toolName, output) {
821
+ if (!output) return void 0;
822
+ const text = output.trim();
823
+ if (!text) return void 0;
824
+ let diff;
825
+ if (toolName === "edit" || toolName === "diff") {
826
+ const parsed = tryParseJson(text);
827
+ if (parsed && typeof parsed === "object") {
828
+ diff = stringOf(parsed["diff"]);
829
+ }
830
+ } else if (toolName === "patch") {
831
+ const parsed = tryParseJson(text);
832
+ if (parsed && typeof parsed === "object") {
833
+ diff = stringOf(parsed["diff"]) ?? stringOf(parsed["stdout"]);
834
+ } else if (text.includes("@@") || text.startsWith("---")) {
835
+ diff = text;
836
+ }
837
+ }
838
+ if (!diff || !diff.trim() || diff.startsWith("(no-op")) return void 0;
839
+ return parseUnifiedDiff(diff, DIFF_MAX_LINES);
840
+ }
841
+ function parseUnifiedDiff(diff, maxLines) {
842
+ const all = [];
843
+ for (const raw of diff.split("\n")) {
844
+ const line = raw.replace(/\r$/, "");
845
+ if (line.startsWith("+++") || line.startsWith("---")) continue;
846
+ if (line.startsWith("diff --git") || line.startsWith("index ")) continue;
847
+ if (line.startsWith("@@")) {
848
+ all.push({ kind: "hunk", text: truncMid(line, 60) });
849
+ continue;
850
+ }
851
+ if (line.startsWith("+")) {
852
+ all.push({ kind: "add", text: truncMid(line, 100) });
853
+ continue;
854
+ }
855
+ if (line.startsWith("-")) {
856
+ all.push({ kind: "del", text: truncMid(line, 100) });
857
+ continue;
858
+ }
859
+ if (line.startsWith("\\ No newline")) {
860
+ all.push({ kind: "meta", text: line });
861
+ continue;
862
+ }
863
+ if (line.length === 0) continue;
864
+ all.push({ kind: "ctx", text: truncMid(line, 100) });
865
+ }
866
+ if (all.length === 0) return { rows: [], hidden: 0 };
867
+ if (all.length <= maxLines) return { rows: all, hidden: 0 };
868
+ return { rows: all.slice(0, maxLines), hidden: all.length - maxLines };
869
+ }
870
+ function stringOf(v) {
871
+ return typeof v === "string" && v.length > 0 ? v : void 0;
872
+ }
873
+ function numOf(v) {
874
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
875
+ }
876
+ function tryParseJson(s) {
877
+ const t = s.trimStart();
878
+ if (!t.startsWith("{") && !t.startsWith("[")) return void 0;
879
+ try {
880
+ return JSON.parse(s);
881
+ } catch {
882
+ return void 0;
883
+ }
884
+ }
885
+ function scanNumberedRange(text) {
886
+ let first;
887
+ let last;
888
+ let count = 0;
889
+ for (const line of text.split("\n")) {
890
+ const m = line.match(/^\s*(\d+)→/);
891
+ if (m && m[1]) {
892
+ const n = Number.parseInt(m[1], 10);
893
+ if (Number.isFinite(n)) {
894
+ if (first === void 0) first = n;
895
+ last = n;
896
+ count++;
897
+ }
898
+ }
899
+ }
900
+ return { first, last, count };
901
+ }
902
+ function countLines(text) {
903
+ if (!text) return 0;
904
+ return text.replace(/\n$/, "").split("\n").length;
905
+ }
906
+ function fmtBytes(n) {
907
+ if (n < 1024) return `${n}B`;
908
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
909
+ return `${(n / (1024 * 1024)).toFixed(1)}MB`;
910
+ }
911
+ function truncMid(s, max) {
912
+ if (s.length <= max) return s;
913
+ return `${s.slice(0, max - 1)}\u2026`;
914
+ }
915
+ function Input({
916
+ prompt = "\u203A ",
917
+ value,
918
+ cursor,
919
+ placeholders,
920
+ disabled,
921
+ hint,
922
+ onKey
923
+ }) {
924
+ useInput((input, key) => {
925
+ if (disabled) return;
926
+ onKey(input, key);
927
+ });
928
+ const before = value.slice(0, cursor);
929
+ const at = value.slice(cursor, cursor + 1) || " ";
930
+ const after = value.slice(cursor + 1);
931
+ const borderColor = disabled ? "red" : value.length > 0 ? "cyan" : "gray";
932
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
933
+ placeholders.map((p, i) => /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
934
+ " \u21B3 ",
935
+ p
936
+ ] }, i)),
937
+ /* @__PURE__ */ jsx(
938
+ Box,
939
+ {
940
+ borderStyle: "round",
941
+ borderColor,
942
+ paddingX: 1,
943
+ width: "100%",
944
+ children: /* @__PURE__ */ jsxs(Text, { children: [
945
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: prompt }),
946
+ before,
947
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: at }),
948
+ after
949
+ ] })
950
+ }
951
+ ),
952
+ hint ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint }) : null
953
+ ] });
954
+ }
955
+ function StatusBar({
956
+ model,
957
+ state,
958
+ tokenCounter,
959
+ hint,
960
+ queueCount = 0,
961
+ yolo = false,
962
+ elapsedMs,
963
+ todos,
964
+ git,
965
+ subagentCount = 0,
966
+ context
967
+ }) {
968
+ const usage = tokenCounter?.total();
969
+ const cost = tokenCounter?.estimateCost();
970
+ const cache2 = tokenCounter?.cacheStats();
971
+ const stateColor = state === "idle" ? "cyan" : state === "aborting" ? "yellow" : "green";
972
+ const stateLabel = state === "idle" ? "idle" : state === "aborting" ? "aborting\u2026" : "thinking\u2026";
973
+ const hasSecondLine = yolo || elapsedMs !== void 0 || todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || git !== null && git !== void 0 || subagentCount > 0;
974
+ return /* @__PURE__ */ jsxs(
975
+ Box,
976
+ {
977
+ flexDirection: "column",
978
+ paddingX: 1,
979
+ borderStyle: "single",
980
+ borderTop: true,
981
+ borderBottom: false,
982
+ borderLeft: false,
983
+ borderRight: false,
984
+ children: [
985
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
986
+ /* @__PURE__ */ jsxs(Text, { color: stateColor, children: [
987
+ "\u25CF ",
988
+ stateLabel
989
+ ] }),
990
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
991
+ /* @__PURE__ */ jsx(Text, { color: "magenta", children: model }),
992
+ context && context.max > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
993
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
994
+ /* @__PURE__ */ jsx(ContextChip, { ctx: context })
995
+ ] }) : null,
996
+ usage ? /* @__PURE__ */ jsxs(Fragment, { children: [
997
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
998
+ /* @__PURE__ */ jsxs(Text, { children: [
999
+ "\u2191 ",
1000
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: fmtTok(usage.input) }),
1001
+ " \u2193",
1002
+ " ",
1003
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: fmtTok(usage.output) })
1004
+ ] })
1005
+ ] }) : null,
1006
+ cache2 && cache2.hitRatio > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1007
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1008
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1009
+ "cache ",
1010
+ (cache2.hitRatio * 100).toFixed(0),
1011
+ "%"
1012
+ ] })
1013
+ ] }) : null,
1014
+ cost && cost.total > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1015
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1016
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1017
+ "$",
1018
+ cost.total.toFixed(4)
1019
+ ] })
1020
+ ] }) : null,
1021
+ queueCount > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1022
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1023
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
1024
+ "\u231B queued: ",
1025
+ queueCount
1026
+ ] })
1027
+ ] }) : null,
1028
+ hint ? /* @__PURE__ */ jsxs(Fragment, { children: [
1029
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1030
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint })
1031
+ ] }) : null
1032
+ ] }),
1033
+ hasSecondLine ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
1034
+ yolo ? /* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: "\u26A0 YOLO" }) : null,
1035
+ elapsedMs !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1036
+ yolo ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1037
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1038
+ "\u23F1 ",
1039
+ fmtElapsed(elapsedMs)
1040
+ ] })
1041
+ ] }) : null,
1042
+ git ? /* @__PURE__ */ jsxs(Fragment, { children: [
1043
+ yolo || elapsedMs !== void 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1044
+ /* @__PURE__ */ jsxs(Text, { children: [
1045
+ /* @__PURE__ */ jsxs(Text, { color: "magenta", children: [
1046
+ "\u2387 ",
1047
+ git.branch
1048
+ ] }),
1049
+ git.added > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1050
+ " +",
1051
+ git.added
1052
+ ] }) : null,
1053
+ git.deleted > 0 ? /* @__PURE__ */ jsxs(Text, { color: "red", children: [
1054
+ " -",
1055
+ git.deleted
1056
+ ] }) : null,
1057
+ git.untracked > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1058
+ " ?",
1059
+ git.untracked
1060
+ ] }) : null
1061
+ ] })
1062
+ ] }) : null,
1063
+ todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) ? /* @__PURE__ */ jsxs(Fragment, { children: [
1064
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1065
+ /* @__PURE__ */ jsxs(Text, { children: [
1066
+ todos.inProgress > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1067
+ "\u231B ",
1068
+ todos.inProgress
1069
+ ] }) : null,
1070
+ todos.inProgress > 0 && (todos.pending > 0 || todos.completed > 0) ? " " : "",
1071
+ todos.pending > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1072
+ "\u2610 ",
1073
+ todos.pending
1074
+ ] }) : null,
1075
+ todos.pending > 0 && todos.completed > 0 ? " " : "",
1076
+ todos.completed > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1077
+ "\u2713 ",
1078
+ todos.completed
1079
+ ] }) : null
1080
+ ] })
1081
+ ] }) : null,
1082
+ subagentCount > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1083
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1084
+ /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
1085
+ "\u{1F310} ",
1086
+ subagentCount,
1087
+ " agent",
1088
+ subagentCount === 1 ? "" : "s"
1089
+ ] })
1090
+ ] }) : null
1091
+ ] }) : null
1092
+ ]
1093
+ }
1094
+ );
1095
+ }
1096
+ function ContextChip({ ctx }) {
1097
+ const ratio = Math.max(0, Math.min(1, ctx.used / ctx.max));
1098
+ const pct = Math.round(ratio * 100);
1099
+ const color = ratio >= 0.85 ? "red" : ratio >= 0.65 ? "yellow" : "cyan";
1100
+ return /* @__PURE__ */ jsxs(Text, { children: [
1101
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "ctx " }),
1102
+ /* @__PURE__ */ jsx(Text, { color, children: renderProgress(ratio, 10) }),
1103
+ /* @__PURE__ */ jsxs(Text, { color, children: [
1104
+ " ",
1105
+ pct,
1106
+ "%"
1107
+ ] }),
1108
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1109
+ " ",
1110
+ "(",
1111
+ fmtTok(ctx.used),
1112
+ "/",
1113
+ fmtTok(ctx.max),
1114
+ ")"
1115
+ ] })
1116
+ ] });
1117
+ }
1118
+ var FILLED = "\u2588";
1119
+ var EMPTY = "\u2591";
1120
+ function renderProgress(ratio, width) {
1121
+ const clamped = Math.max(0, Math.min(1, ratio));
1122
+ const filled = clamped === 0 ? 0 : Math.max(1, Math.round(clamped * width));
1123
+ const capped = Math.min(width, filled);
1124
+ return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
1125
+ }
1126
+ function fmtTok(n) {
1127
+ if (n < 1e3) return String(n);
1128
+ if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
1129
+ return `${(n / 1e6).toFixed(1)}M`;
1130
+ }
1131
+ function fmtElapsed(ms) {
1132
+ const totalSec = Math.floor(ms / 1e3);
1133
+ const h = Math.floor(totalSec / 3600);
1134
+ const m = Math.floor(totalSec % 3600 / 60);
1135
+ const s = totalSec % 60;
1136
+ if (h > 0) {
1137
+ return `${h}:${pad2(m)}:${pad2(s)}`;
1138
+ }
1139
+ return `${pad2(m)}:${pad2(s)}`;
1140
+ }
1141
+ function pad2(n) {
1142
+ return n < 10 ? `0${n}` : String(n);
1143
+ }
1144
+ function FilePicker({ query, matches, selected }) {
1145
+ if (matches.length === 0) {
1146
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1147
+ "@",
1148
+ query,
1149
+ " \u2014 no matches"
1150
+ ] }) });
1151
+ }
1152
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1153
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1154
+ "@",
1155
+ query || "\u2026",
1156
+ " \u2014 \u2191/\u2193 select, Enter attach, Esc cancel"
1157
+ ] }),
1158
+ matches.map((m, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1159
+ i === selected ? "\u203A " : " ",
1160
+ highlight(m)
1161
+ ] }, m))
1162
+ ] });
1163
+ }
1164
+ function highlight(path4, query) {
1165
+ return path4;
1166
+ }
1167
+ function SlashMenu({ query, matches, selected }) {
1168
+ const placeholder = query ? `/${query}` : "/";
1169
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1170
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1171
+ placeholder || "/",
1172
+ " \u2014 \u2191/\u2193 select, Enter dispatch, Tab autocomplete, Esc close"
1173
+ ] }),
1174
+ matches.map((m, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1175
+ i === selected ? "\u203A " : " ",
1176
+ /* @__PURE__ */ jsx(Text, { bold: true, children: m.name }),
1177
+ m.argsHint ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1178
+ " ",
1179
+ m.argsHint
1180
+ ] }) : null,
1181
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1182
+ " \u2014 ",
1183
+ m.description
1184
+ ] })
1185
+ ] }, m.name)),
1186
+ matches.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No matching commands" })
1187
+ ] });
1188
+ }
1189
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
1190
+ "node_modules",
1191
+ ".git",
1192
+ "dist",
1193
+ "build",
1194
+ ".next",
1195
+ ".turbo",
1196
+ ".cache",
1197
+ "coverage",
1198
+ ".idea",
1199
+ ".vscode"
1200
+ ]);
1201
+ var MAX_FILES_INDEXED = 5e3;
1202
+ var MAX_DEPTH = 8;
1203
+ var cache = null;
1204
+ var CACHE_TTL_MS = 3e4;
1205
+ async function loadIndex(root) {
1206
+ const now = Date.now();
1207
+ if (cache && cache.root === root && now - cache.loadedAt < CACHE_TTL_MS) {
1208
+ return cache.files;
1209
+ }
1210
+ const files = [];
1211
+ await walk(root, "", 0, files);
1212
+ files.sort();
1213
+ cache = { root, files, loadedAt: now };
1214
+ return files;
1215
+ }
1216
+ async function walk(root, rel, depth, out) {
1217
+ if (out.length >= MAX_FILES_INDEXED) return;
1218
+ if (depth > MAX_DEPTH) return;
1219
+ const dir = rel ? path2.join(root, rel) : root;
1220
+ let entries;
1221
+ try {
1222
+ entries = await fs2.readdir(dir, { withFileTypes: true });
1223
+ } catch {
1224
+ return;
1225
+ }
1226
+ for (const e of entries) {
1227
+ if (out.length >= MAX_FILES_INDEXED) return;
1228
+ if (e.name.startsWith(".") && e.name !== ".env.example") continue;
1229
+ if (IGNORED_DIRS.has(e.name)) continue;
1230
+ const next = rel ? `${rel}/${e.name}` : e.name;
1231
+ if (e.isDirectory()) {
1232
+ await walk(root, next, depth + 1, out);
1233
+ } else if (e.isFile()) {
1234
+ out.push(next);
1235
+ }
1236
+ }
1237
+ }
1238
+ function score(s, query) {
1239
+ if (!query) return s.length;
1240
+ const ql = query.toLowerCase();
1241
+ const sl = s.toLowerCase();
1242
+ let si = 0;
1243
+ let firstHit = -1;
1244
+ let lastHit = -1;
1245
+ for (let qi = 0; qi < ql.length; qi++) {
1246
+ const c = ql.charCodeAt(qi);
1247
+ while (si < sl.length && sl.charCodeAt(si) !== c) si++;
1248
+ if (si >= sl.length) return null;
1249
+ if (firstHit < 0) firstHit = si;
1250
+ lastHit = si;
1251
+ si++;
1252
+ }
1253
+ const span = lastHit - firstHit;
1254
+ return span * 100 + firstHit * 2 + s.length;
1255
+ }
1256
+ async function searchFiles(root, query, limit = 8) {
1257
+ const all = await loadIndex(root);
1258
+ if (!query) return all.slice(0, limit);
1259
+ const scored = [];
1260
+ for (const f of all) {
1261
+ const sc = score(f, query);
1262
+ if (sc !== null) scored.push({ path: f, score: sc });
1263
+ }
1264
+ scored.sort((a, b) => a.score - b.score);
1265
+ return scored.slice(0, limit).map((x) => x.path);
1266
+ }
1267
+ var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
1268
+ async function readClipboardImage() {
1269
+ const platform = process.platform;
1270
+ if (platform === "win32") return readWindows();
1271
+ if (platform === "darwin") return readDarwin();
1272
+ if (platform === "linux") return readLinux();
1273
+ return null;
1274
+ }
1275
+ async function readWindows() {
1276
+ const tmp = path2.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
1277
+ const ps = [
1278
+ "Add-Type -AssemblyName System.Windows.Forms",
1279
+ "Add-Type -AssemblyName System.Drawing",
1280
+ "$img = [System.Windows.Forms.Clipboard]::GetImage()",
1281
+ 'if ($img -eq $null) { Write-Output "NO_IMAGE"; exit 0 }',
1282
+ `$img.Save('${tmp.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png)`,
1283
+ 'Write-Output "OK"'
1284
+ ].join("; ");
1285
+ const out = await runCmd("powershell", ["-NoProfile", "-Command", ps]);
1286
+ if (!out || out.trim() === "NO_IMAGE") return null;
1287
+ if (!out.includes("OK")) return null;
1288
+ return readPngFile(tmp);
1289
+ }
1290
+ async function readDarwin() {
1291
+ const tmp = path2.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
1292
+ const script = [
1293
+ "try",
1294
+ ` set the_file to (open for access POSIX file "${tmp}" with write permission)`,
1295
+ " write (the clipboard as \xABclass PNGf\xBB) to the_file",
1296
+ " close access the_file",
1297
+ "on error",
1298
+ " try",
1299
+ ' close access POSIX file "' + tmp + '"',
1300
+ " end try",
1301
+ ' return "NO_IMAGE"',
1302
+ "end try",
1303
+ 'return "OK"'
1304
+ ].join("\n");
1305
+ const out = await runCmd("osascript", ["-e", script]);
1306
+ if (!out || out.trim() !== "OK") return null;
1307
+ return readPngFile(tmp);
1308
+ }
1309
+ async function readLinux() {
1310
+ const tmp = path2.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
1311
+ const tries = [
1312
+ ["wl-paste", ["--type", "image/png"]],
1313
+ ["xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]]
1314
+ ];
1315
+ for (const [cmd, args] of tries) {
1316
+ const ok = await runCmdToFile(cmd, args, tmp).catch(() => false);
1317
+ if (ok) return readPngFile(tmp);
1318
+ }
1319
+ return null;
1320
+ }
1321
+ async function readPngFile(p) {
1322
+ try {
1323
+ const buf = await fs2.readFile(p);
1324
+ if (buf.length === 0) {
1325
+ await fs2.unlink(p).catch(() => void 0);
1326
+ return null;
1327
+ }
1328
+ if (buf.length > MAX_IMAGE_BYTES) {
1329
+ await fs2.unlink(p).catch(() => void 0);
1330
+ throw new Error(`Clipboard image exceeds ${MAX_IMAGE_BYTES / 1024 / 1024}MB limit`);
1331
+ }
1332
+ if (buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71) {
1333
+ await fs2.unlink(p).catch(() => void 0);
1334
+ return null;
1335
+ }
1336
+ await fs2.unlink(p).catch(() => void 0);
1337
+ return { base64: buf.toString("base64"), mediaType: "image/png", bytes: buf.length };
1338
+ } catch (err) {
1339
+ if (err.code === "ENOENT") return null;
1340
+ throw err;
1341
+ }
1342
+ }
1343
+ function runCmd(cmd, args) {
1344
+ return new Promise((resolve) => {
1345
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1346
+ let out = "";
1347
+ child.stdout.on("data", (c) => out += String(c));
1348
+ child.on("error", () => resolve(null));
1349
+ child.on("exit", (code) => resolve(code === 0 ? out : null));
1350
+ });
1351
+ }
1352
+ function runCmdToFile(cmd, args, outPath) {
1353
+ return new Promise((resolve) => {
1354
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1355
+ const chunks = [];
1356
+ child.stdout.on("data", (c) => chunks.push(c));
1357
+ child.on("error", () => resolve(false));
1358
+ child.on("exit", async (code) => {
1359
+ if (code !== 0 || chunks.length === 0) return resolve(false);
1360
+ try {
1361
+ await fs2.writeFile(outPath, Buffer.concat(chunks));
1362
+ resolve(true);
1363
+ } catch {
1364
+ resolve(false);
1365
+ }
1366
+ });
1367
+ });
1368
+ }
1369
+
1370
+ // src/queue-slash.ts
1371
+ var USAGE = "Usage:\n /queue \u2014 list pending messages\n /queue list \u2014 same as /queue\n /queue clear \u2014 drop all pending messages\n /queue delete N M\u2026 \u2014 drop messages at 1-based positions";
1372
+ function createQueueSlashCommand(deps) {
1373
+ return {
1374
+ name: "queue",
1375
+ description: "List, clear, or delete pending messages typed while the agent was running.",
1376
+ async run(args) {
1377
+ const out = handleQueueCommand(args, deps);
1378
+ return { message: out };
1379
+ }
1380
+ };
1381
+ }
1382
+ function handleQueueCommand(args, deps) {
1383
+ const trimmed = args.trim();
1384
+ const [sub, ...rest] = trimmed.split(/\s+/);
1385
+ const subcommand = sub?.toLowerCase() ?? "";
1386
+ if (subcommand === "" || subcommand === "list") {
1387
+ return renderList(deps.getQueue());
1388
+ }
1389
+ if (subcommand === "clear") {
1390
+ const before = deps.getQueue().length;
1391
+ if (before === 0) return "Queue is already empty.";
1392
+ deps.clear();
1393
+ return `Cleared ${before} queued message${before === 1 ? "" : "s"}.`;
1394
+ }
1395
+ if (subcommand === "delete" || subcommand === "del" || subcommand === "rm") {
1396
+ if (rest.length === 0) return "Usage: /queue delete <position> [<position>\u2026]";
1397
+ const queue = deps.getQueue();
1398
+ if (queue.length === 0) return "Queue is empty \u2014 nothing to delete.";
1399
+ const parsed = [];
1400
+ const invalid = [];
1401
+ const outOfRange = [];
1402
+ for (const tok of rest) {
1403
+ if (!/^\d+$/.test(tok)) {
1404
+ invalid.push(tok);
1405
+ continue;
1406
+ }
1407
+ const n = Number.parseInt(tok, 10);
1408
+ if (n < 1 || n > queue.length) {
1409
+ outOfRange.push(n);
1410
+ continue;
1411
+ }
1412
+ parsed.push(n);
1413
+ }
1414
+ const uniqueValid = [...new Set(parsed)];
1415
+ if (uniqueValid.length === 0) {
1416
+ const parts2 = ["No valid positions to delete."];
1417
+ if (invalid.length > 0) parts2.push(`Invalid: ${invalid.join(", ")}.`);
1418
+ if (outOfRange.length > 0) parts2.push(`Out of range (queue has ${queue.length}): ${outOfRange.join(", ")}.`);
1419
+ return parts2.join(" ");
1420
+ }
1421
+ deps.deleteAt(uniqueValid);
1422
+ const parts = [
1423
+ `Deleted ${uniqueValid.length} of ${queue.length} (positions ${uniqueValid.sort((a, b) => a - b).join(", ")}).`
1424
+ ];
1425
+ if (invalid.length > 0) parts.push(`Ignored invalid: ${invalid.join(", ")}.`);
1426
+ if (outOfRange.length > 0) parts.push(`Ignored out of range: ${outOfRange.join(", ")}.`);
1427
+ return parts.join(" ");
1428
+ }
1429
+ return `Unknown subcommand "${sub}".
1430
+ ${USAGE}`;
1431
+ }
1432
+ function renderList(queue) {
1433
+ if (queue.length === 0) return "Queue is empty.";
1434
+ const lines = [`Queue (${queue.length}):`];
1435
+ for (let i = 0; i < queue.length; i++) {
1436
+ const item = queue[i];
1437
+ if (!item) continue;
1438
+ const preview = oneLine(item.displayText, 100);
1439
+ lines.push(` ${i + 1}. ${preview}`);
1440
+ }
1441
+ return lines.join("\n");
1442
+ }
1443
+ function oneLine(s, max) {
1444
+ const collapsed = s.replace(/\s+/g, " ").trim();
1445
+ return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 1)}\u2026`;
1446
+ }
1447
+ async function readGitInfo(cwd) {
1448
+ const [branchRes, numstatRes, statusRes] = await Promise.all([
1449
+ runGit(cwd, ["branch", "--show-current"]),
1450
+ runGit(cwd, ["diff", "HEAD", "--numstat"]),
1451
+ runGit(cwd, ["status", "--porcelain"])
1452
+ ]);
1453
+ if (!branchRes.ok || !numstatRes.ok || !statusRes.ok) return null;
1454
+ const branch = branchRes.stdout.trim();
1455
+ const branchLabel = branch || await detachedShortSha(cwd) || "detached";
1456
+ let added = 0;
1457
+ let deleted = 0;
1458
+ for (const line of numstatRes.stdout.split("\n")) {
1459
+ if (!line) continue;
1460
+ const [a, d] = line.split(" ");
1461
+ if (a && a !== "-") added += Number.parseInt(a, 10) || 0;
1462
+ if (d && d !== "-") deleted += Number.parseInt(d, 10) || 0;
1463
+ }
1464
+ let untracked = 0;
1465
+ for (const line of statusRes.stdout.split("\n")) {
1466
+ if (line.startsWith("?? ")) untracked++;
1467
+ }
1468
+ return { branch: branchLabel, added, deleted, untracked };
1469
+ }
1470
+ async function detachedShortSha(cwd) {
1471
+ const res = await runGit(cwd, ["rev-parse", "--short", "HEAD"]);
1472
+ return res.ok ? res.stdout.trim() : null;
1473
+ }
1474
+ function runGit(cwd, args) {
1475
+ return new Promise((resolve) => {
1476
+ let stdout = "";
1477
+ try {
1478
+ const child = spawn("git", args, {
1479
+ cwd,
1480
+ // Inherit stderr (silent) — we don't care about git's noise.
1481
+ stdio: ["ignore", "pipe", "ignore"],
1482
+ // Don't let a slow git hang the TUI.
1483
+ timeout: 3e3,
1484
+ windowsHide: true
1485
+ });
1486
+ child.stdout.setEncoding("utf8");
1487
+ child.stdout.on("data", (chunk) => {
1488
+ stdout += chunk;
1489
+ });
1490
+ child.on("error", () => resolve({ ok: false, stdout: "" }));
1491
+ child.on("close", (code) => {
1492
+ resolve({ ok: code === 0, stdout });
1493
+ });
1494
+ } catch {
1495
+ resolve({ ok: false, stdout: "" });
1496
+ }
1497
+ });
1498
+ }
1499
+ function reducer(state, action) {
1500
+ switch (action.type) {
1501
+ case "addEntry": {
1502
+ const appended = [
1503
+ ...state.entries,
1504
+ { ...action.entry, id: state.nextId }
1505
+ ];
1506
+ return { ...state, entries: appended, nextId: state.nextId + 1 };
1507
+ }
1508
+ case "setBuffer":
1509
+ return { ...state, buffer: action.buffer, cursor: action.cursor };
1510
+ case "addPlaceholder":
1511
+ return { ...state, placeholders: [...state.placeholders, action.ph] };
1512
+ case "clearInput":
1513
+ return {
1514
+ ...state,
1515
+ buffer: "",
1516
+ cursor: 0,
1517
+ placeholders: [],
1518
+ picker: { open: false, query: "", matches: [], selected: 0 },
1519
+ slashPicker: { open: false, query: "", matches: [], selected: 0 }
1520
+ };
1521
+ case "streamDelta":
1522
+ return { ...state, streamingText: state.streamingText + action.delta };
1523
+ case "streamReset":
1524
+ return { ...state, streamingText: "" };
1525
+ case "status":
1526
+ return { ...state, status: action.status };
1527
+ case "interrupt":
1528
+ return { ...state, interrupts: state.interrupts + 1 };
1529
+ case "resetInterrupts":
1530
+ return { ...state, interrupts: 0 };
1531
+ case "hint":
1532
+ return { ...state, hint: action.text };
1533
+ case "pickerOpen":
1534
+ return {
1535
+ ...state,
1536
+ picker: { open: true, query: action.query, matches: state.picker.matches, selected: 0 }
1537
+ };
1538
+ case "pickerClose":
1539
+ return {
1540
+ ...state,
1541
+ picker: { open: false, query: "", matches: [], selected: 0 }
1542
+ };
1543
+ case "pickerSetMatches":
1544
+ if (!state.picker.open || state.picker.query !== action.query) return state;
1545
+ return {
1546
+ ...state,
1547
+ picker: {
1548
+ ...state.picker,
1549
+ matches: action.matches,
1550
+ selected: Math.min(state.picker.selected, Math.max(0, action.matches.length - 1))
1551
+ }
1552
+ };
1553
+ case "pickerMove": {
1554
+ const n = state.picker.matches.length;
1555
+ if (n === 0) return state;
1556
+ const next = (state.picker.selected + action.delta + n) % n;
1557
+ return { ...state, picker: { ...state.picker, selected: next } };
1558
+ }
1559
+ case "toolStarted": {
1560
+ const next = new Map(state.runningTools);
1561
+ next.set(action.id, { name: action.name, startedAt: Date.now() });
1562
+ return { ...state, runningTools: next };
1563
+ }
1564
+ case "toolEnded": {
1565
+ const next = new Map(state.runningTools);
1566
+ if (action.id !== void 0 && next.has(action.id)) {
1567
+ next.delete(action.id);
1568
+ return { ...state, runningTools: next };
1569
+ }
1570
+ if (action.name !== void 0) {
1571
+ for (const [id, info] of next) {
1572
+ if (info.name === action.name) {
1573
+ next.delete(id);
1574
+ return { ...state, runningTools: next };
1575
+ }
1576
+ }
1577
+ }
1578
+ return state;
1579
+ }
1580
+ case "enqueue": {
1581
+ const item = { ...action.item, id: state.nextQueueId };
1582
+ return {
1583
+ ...state,
1584
+ queue: [...state.queue, item],
1585
+ nextQueueId: state.nextQueueId + 1
1586
+ };
1587
+ }
1588
+ case "dequeueFirst": {
1589
+ if (state.queue.length === 0) return state;
1590
+ return { ...state, queue: state.queue.slice(1) };
1591
+ }
1592
+ case "queueClear": {
1593
+ if (state.queue.length === 0) return state;
1594
+ return { ...state, queue: [] };
1595
+ }
1596
+ case "queueDelete": {
1597
+ if (state.queue.length === 0 || action.positions.length === 0) return state;
1598
+ const drop = new Set(action.positions.map((p) => p - 1).filter((i) => i >= 0));
1599
+ const filtered = state.queue.filter((_, i) => !drop.has(i));
1600
+ if (filtered.length === state.queue.length) return state;
1601
+ return { ...state, queue: filtered };
1602
+ }
1603
+ case "slashPickerOpen":
1604
+ return {
1605
+ ...state,
1606
+ slashPicker: { open: true, query: action.query, matches: action.matches, selected: 0 }
1607
+ };
1608
+ case "slashPickerClose":
1609
+ return {
1610
+ ...state,
1611
+ slashPicker: { open: false, query: "", matches: [], selected: 0 }
1612
+ };
1613
+ case "slashPickerMove": {
1614
+ const n = state.slashPicker.matches.length;
1615
+ if (n === 0) return state;
1616
+ const next = (state.slashPicker.selected + action.delta + n) % n;
1617
+ return { ...state, slashPicker: { ...state.slashPicker, selected: next } };
1618
+ }
1619
+ case "historyPush": {
1620
+ if (action.text === "" || action.text === state.inputHistory[0]) return state;
1621
+ return { ...state, inputHistory: [action.text, ...state.inputHistory].slice(0, 100) };
1622
+ }
1623
+ case "historyUp": {
1624
+ if (state.inputHistory.length === 0) return state;
1625
+ const next = Math.min(state.historyIndex + 1, state.inputHistory.length);
1626
+ const entry = state.inputHistory[next - 1] ?? "";
1627
+ return { ...state, historyIndex: next, buffer: entry, cursor: entry.length };
1628
+ }
1629
+ case "historyDown": {
1630
+ if (state.historyIndex === 0) return state;
1631
+ const next = state.historyIndex - 1;
1632
+ const entry = next === 0 ? "" : state.inputHistory[next - 1] ?? "";
1633
+ return { ...state, historyIndex: next, buffer: entry, cursor: entry.length };
1634
+ }
1635
+ }
1636
+ }
1637
+ var PASTE_THRESHOLD_CHARS = 200;
1638
+ function App({
1639
+ agent,
1640
+ slashRegistry,
1641
+ attachments,
1642
+ events,
1643
+ tokenCounter,
1644
+ model,
1645
+ banner = true,
1646
+ queueStore,
1647
+ yolo = false,
1648
+ appVersion,
1649
+ provider,
1650
+ effectiveMaxContext,
1651
+ onExit
1652
+ }) {
1653
+ const { exit } = useApp();
1654
+ const [state, dispatch] = useReducer(reducer, {
1655
+ entries: banner ? [
1656
+ {
1657
+ id: 0,
1658
+ kind: "banner",
1659
+ version: appVersion ?? "dev",
1660
+ provider: provider ?? "agent",
1661
+ model,
1662
+ cwd: agent.ctx.cwd
1663
+ }
1664
+ ] : [],
1665
+ buffer: "",
1666
+ cursor: 0,
1667
+ placeholders: [],
1668
+ streamingText: "",
1669
+ status: "idle",
1670
+ interrupts: 0,
1671
+ hint: "",
1672
+ nextId: 1,
1673
+ picker: { open: false, query: "", matches: [], selected: 0 },
1674
+ slashPicker: { open: false, query: "", matches: [], selected: 0 },
1675
+ runningTools: /* @__PURE__ */ new Map(),
1676
+ queue: [],
1677
+ nextQueueId: 1,
1678
+ inputHistory: [],
1679
+ historyIndex: 0
1680
+ });
1681
+ const builderRef = useRef(null);
1682
+ if (builderRef.current === null) {
1683
+ builderRef.current = new InputBuilder({ store: attachments });
1684
+ }
1685
+ const activeCtrlRef = useRef(null);
1686
+ const projectRoot = agent.ctx.projectRoot;
1687
+ const streamingTextRef = useRef("");
1688
+ const pendingDeltaRef = useRef("");
1689
+ const flushTimerRef = useRef(null);
1690
+ const stateRef = useRef(state);
1691
+ stateRef.current = state;
1692
+ const startedAtRef = useRef(Date.now());
1693
+ const [nowTick, setNowTick] = React.useState(Date.now());
1694
+ useEffect(() => {
1695
+ const t = setInterval(() => setNowTick(Date.now()), 1e3);
1696
+ return () => clearInterval(t);
1697
+ }, []);
1698
+ const elapsedMs = nowTick - startedAtRef.current;
1699
+ const [gitInfo, setGitInfo] = React.useState(null);
1700
+ useEffect(() => {
1701
+ let cancelled = false;
1702
+ const refresh = () => {
1703
+ readGitInfo(agent.ctx.cwd).then((info) => {
1704
+ if (!cancelled) setGitInfo(info);
1705
+ }).catch(() => void 0);
1706
+ };
1707
+ refresh();
1708
+ const t = setInterval(refresh, 5e3);
1709
+ return () => {
1710
+ cancelled = true;
1711
+ clearInterval(t);
1712
+ };
1713
+ }, [agent.ctx.cwd]);
1714
+ const [lastInputTokens, setLastInputTokens] = React.useState(0);
1715
+ useEffect(() => {
1716
+ const off = events.on("provider.response", (e) => {
1717
+ setLastInputTokens(e.usage.input);
1718
+ });
1719
+ return () => {
1720
+ off();
1721
+ };
1722
+ }, [events]);
1723
+ const maxContext = effectiveMaxContext ?? agent.ctx.provider.capabilities.maxContext;
1724
+ const contextWindow = useMemo(
1725
+ () => lastInputTokens > 0 && maxContext > 0 ? { used: lastInputTokens, max: maxContext } : void 0,
1726
+ [lastInputTokens, maxContext]
1727
+ );
1728
+ const todos = useMemo(() => {
1729
+ const counts = { pending: 0, inProgress: 0, completed: 0 };
1730
+ for (const t of agent.ctx.todos) {
1731
+ if (t.status === "pending") counts.pending++;
1732
+ else if (t.status === "in_progress") counts.inProgress++;
1733
+ else if (t.status === "completed") counts.completed++;
1734
+ }
1735
+ return counts;
1736
+ }, [nowTick, agent.ctx.todos]);
1737
+ useEffect(() => {
1738
+ const detected = detectAtToken(state.buffer, state.cursor);
1739
+ if (!detected) {
1740
+ if (state.picker.open) dispatch({ type: "pickerClose" });
1741
+ return;
1742
+ }
1743
+ if (!state.picker.open || state.picker.query !== detected.query) {
1744
+ dispatch({ type: "pickerOpen", query: detected.query });
1745
+ }
1746
+ let cancelled = false;
1747
+ searchFiles(projectRoot, detected.query, 8).then((matches) => {
1748
+ if (!cancelled) {
1749
+ dispatch({ type: "pickerSetMatches", query: detected.query, matches });
1750
+ }
1751
+ }).catch(() => void 0);
1752
+ return () => {
1753
+ cancelled = true;
1754
+ };
1755
+ }, [state.buffer, state.cursor, projectRoot]);
1756
+ useEffect(() => {
1757
+ const trimmed = state.buffer.trimStart();
1758
+ if (!trimmed.startsWith("/")) {
1759
+ if (state.slashPicker.open) dispatch({ type: "slashPickerClose" });
1760
+ return;
1761
+ }
1762
+ const query = (trimmed.slice(1).split(/\s/)[0] ?? "").toLowerCase();
1763
+ const allCommands = slashRegistry.listWithOwner();
1764
+ const matches = allCommands.filter(({ cmd }) => {
1765
+ const name = cmd.name.toLowerCase();
1766
+ const aliases = cmd.aliases ?? [];
1767
+ return name.includes(query) || aliases.some((a) => a.toLowerCase().includes(query));
1768
+ }).slice(0, 12).map(({ cmd, owner }) => ({
1769
+ name: cmd.name,
1770
+ description: cmd.description,
1771
+ argsHint: void 0,
1772
+ isBuiltin: owner === "core"
1773
+ }));
1774
+ if (!state.slashPicker.open) {
1775
+ dispatch({ type: "slashPickerOpen", query, matches });
1776
+ } else if (state.slashPicker.query !== query) {
1777
+ dispatch({ type: "slashPickerOpen", query, matches });
1778
+ }
1779
+ }, [state.buffer, slashRegistry]);
1780
+ const pasteClipboardImage = async () => {
1781
+ const builder = builderRef.current;
1782
+ if (!builder) return;
1783
+ try {
1784
+ const img = await readClipboardImage();
1785
+ if (!img) {
1786
+ dispatch({
1787
+ type: "addEntry",
1788
+ entry: { kind: "info", text: "No image on the clipboard." }
1789
+ });
1790
+ return;
1791
+ }
1792
+ const placeholder = await builder.appendImage(img.base64, img.mediaType);
1793
+ const kb = (img.bytes / 1024).toFixed(0);
1794
+ dispatch({ type: "addPlaceholder", ph: `${placeholder} (PNG ${kb}KB)` });
1795
+ } catch (err) {
1796
+ dispatch({
1797
+ type: "addEntry",
1798
+ entry: {
1799
+ kind: "error",
1800
+ text: `Clipboard image error: ${err instanceof Error ? err.message : String(err)}`
1801
+ }
1802
+ });
1803
+ }
1804
+ };
1805
+ const acceptPickerSelection = async () => {
1806
+ const { open, matches, selected } = state.picker;
1807
+ if (!open || matches.length === 0) return;
1808
+ const picked = matches[selected];
1809
+ if (!picked) return;
1810
+ const builder = builderRef.current;
1811
+ if (!builder) return;
1812
+ const tok = detectAtToken(state.buffer, state.cursor);
1813
+ if (!tok) {
1814
+ dispatch({ type: "pickerClose" });
1815
+ return;
1816
+ }
1817
+ const absPath = path2.isAbsolute(picked) ? picked : path2.join(projectRoot, picked);
1818
+ try {
1819
+ const data = await fs2.readFile(absPath, "utf8");
1820
+ const placeholder = await builder.appendFile({
1821
+ kind: "file",
1822
+ data,
1823
+ meta: { filename: picked, label: picked }
1824
+ });
1825
+ const before = state.buffer.slice(0, tok.start);
1826
+ const after = state.buffer.slice(tok.end);
1827
+ const next = `${before}${placeholder}${after}`;
1828
+ dispatch({
1829
+ type: "setBuffer",
1830
+ buffer: next,
1831
+ cursor: tok.start + placeholder.length
1832
+ });
1833
+ dispatch({ type: "pickerClose" });
1834
+ } catch (err) {
1835
+ dispatch({
1836
+ type: "addEntry",
1837
+ entry: { kind: "error", text: `Attach failed: ${err instanceof Error ? err.message : String(err)}` }
1838
+ });
1839
+ dispatch({ type: "pickerClose" });
1840
+ }
1841
+ };
1842
+ const acceptSlashPickerSelection = () => {
1843
+ const { open, matches, selected } = state.slashPicker;
1844
+ if (!open || matches.length === 0) return;
1845
+ const picked = matches[selected];
1846
+ if (!picked) return;
1847
+ const cmd = picked.argsHint !== void 0 ? `/${picked.name} ` : `/${picked.name}`;
1848
+ dispatch({ type: "setBuffer", buffer: cmd, cursor: cmd.length });
1849
+ dispatch({ type: "slashPickerClose" });
1850
+ };
1851
+ useEffect(() => {
1852
+ if (!queueStore) return;
1853
+ let cancelled = false;
1854
+ queueStore.read().then((items) => {
1855
+ if (cancelled || items.length === 0) return;
1856
+ for (const item of items) {
1857
+ dispatch({
1858
+ type: "enqueue",
1859
+ item: { displayText: item.displayText, blocks: item.blocks }
1860
+ });
1861
+ }
1862
+ dispatch({
1863
+ type: "addEntry",
1864
+ entry: {
1865
+ kind: "info",
1866
+ text: `Restored ${items.length} queued message${items.length === 1 ? "" : "s"} from a previous run.`
1867
+ }
1868
+ });
1869
+ }).catch(() => void 0);
1870
+ return () => {
1871
+ cancelled = true;
1872
+ };
1873
+ }, [queueStore]);
1874
+ useEffect(() => {
1875
+ if (!queueStore) return;
1876
+ queueStore.write(state.queue.map(({ displayText, blocks }) => ({ displayText, blocks }))).catch(() => void 0);
1877
+ }, [state.queue, queueStore]);
1878
+ useEffect(() => {
1879
+ const cmd = createQueueSlashCommand({
1880
+ getQueue: () => stateRef.current.queue,
1881
+ clear: () => dispatch({ type: "queueClear" }),
1882
+ deleteAt: (positions) => dispatch({ type: "queueDelete", positions })
1883
+ });
1884
+ slashRegistry.register(cmd);
1885
+ return () => {
1886
+ slashRegistry.unregister("queue");
1887
+ };
1888
+ }, [slashRegistry]);
1889
+ useEffect(() => {
1890
+ const FLUSH_MS = 100;
1891
+ const flush = () => {
1892
+ if (pendingDeltaRef.current) {
1893
+ dispatch({ type: "streamDelta", delta: pendingDeltaRef.current });
1894
+ pendingDeltaRef.current = "";
1895
+ }
1896
+ flushTimerRef.current = null;
1897
+ };
1898
+ const offDelta = events.on("provider.text_delta", (e) => {
1899
+ const text = e.text.replace(/\x1b\[200~|\x1b\[201~/g, "");
1900
+ streamingTextRef.current += text;
1901
+ pendingDeltaRef.current += text;
1902
+ if (!flushTimerRef.current) flushTimerRef.current = setTimeout(flush, FLUSH_MS);
1903
+ });
1904
+ const offToolStart = events.on("tool.started", (e) => {
1905
+ dispatch({ type: "toolStarted", id: e.id, name: e.name });
1906
+ });
1907
+ const offTool = events.on("tool.executed", (e) => {
1908
+ dispatch({
1909
+ type: "addEntry",
1910
+ entry: {
1911
+ kind: "tool",
1912
+ name: e.name,
1913
+ durationMs: e.durationMs,
1914
+ ok: e.ok,
1915
+ input: e.input,
1916
+ output: e.output
1917
+ }
1918
+ });
1919
+ dispatch({ type: "toolEnded", name: e.name });
1920
+ });
1921
+ const offRetry = events.on("provider.retry", (e) => {
1922
+ const secs = (e.delayMs / 1e3).toFixed(e.delayMs >= 1e3 ? 1 : 2);
1923
+ dispatch({
1924
+ type: "addEntry",
1925
+ entry: { kind: "warn", text: `\u27F3 retry ${e.attempt} in ${secs}s \u2014 ${e.description}` }
1926
+ });
1927
+ });
1928
+ const offProvErr = events.on("provider.error", (e) => {
1929
+ dispatch({
1930
+ type: "addEntry",
1931
+ entry: { kind: "error", text: e.description }
1932
+ });
1933
+ });
1934
+ return () => {
1935
+ offDelta();
1936
+ offToolStart();
1937
+ offTool();
1938
+ offRetry();
1939
+ offProvErr();
1940
+ if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
1941
+ };
1942
+ }, [events]);
1943
+ useEffect(() => {
1944
+ const onSigint = () => {
1945
+ if (state.interrupts >= 1 && state.status === "idle") {
1946
+ exit();
1947
+ onExit(130);
1948
+ return;
1949
+ }
1950
+ dispatch({ type: "interrupt" });
1951
+ if (activeCtrlRef.current) {
1952
+ activeCtrlRef.current.abort();
1953
+ dispatch({ type: "status", status: "aborting" });
1954
+ const droppedCount = stateRef.current.queue.length;
1955
+ if (droppedCount > 0) {
1956
+ dispatch({ type: "queueClear" });
1957
+ dispatch({
1958
+ type: "addEntry",
1959
+ entry: {
1960
+ kind: "warn",
1961
+ text: `Iteration cancelled. Dropped ${droppedCount} queued message${droppedCount === 1 ? "" : "s"}. Press Ctrl+C again to exit.`
1962
+ }
1963
+ });
1964
+ } else {
1965
+ dispatch({
1966
+ type: "addEntry",
1967
+ entry: { kind: "warn", text: "Iteration cancelled. Press Ctrl+C again to exit." }
1968
+ });
1969
+ }
1970
+ } else {
1971
+ dispatch({
1972
+ type: "addEntry",
1973
+ entry: { kind: "warn", text: "Press Ctrl+C again to exit." }
1974
+ });
1975
+ }
1976
+ };
1977
+ process.on("SIGINT", onSigint);
1978
+ return () => {
1979
+ process.off("SIGINT", onSigint);
1980
+ };
1981
+ }, [state.interrupts, state.status, exit, onExit]);
1982
+ const handleKey = async (input, key) => {
1983
+ if (state.status === "aborting") return;
1984
+ if (state.slashPicker.open) {
1985
+ if (key.escape) {
1986
+ dispatch({ type: "slashPickerClose" });
1987
+ return;
1988
+ }
1989
+ if (key.upArrow) {
1990
+ dispatch({ type: "slashPickerMove", delta: -1 });
1991
+ return;
1992
+ }
1993
+ if (key.downArrow) {
1994
+ dispatch({ type: "slashPickerMove", delta: 1 });
1995
+ return;
1996
+ }
1997
+ if (key.return) {
1998
+ await acceptSlashPickerSelection();
1999
+ return;
2000
+ }
2001
+ if (key.tab && state.slashPicker.matches.length > 0) {
2002
+ const sel = state.slashPicker.matches[state.slashPicker.selected];
2003
+ if (sel) {
2004
+ dispatch({ type: "setBuffer", buffer: `/${sel.name} `, cursor: sel.name.length + 2 });
2005
+ dispatch({ type: "slashPickerClose" });
2006
+ }
2007
+ return;
2008
+ }
2009
+ }
2010
+ if (state.picker.open) {
2011
+ if (key.escape) {
2012
+ dispatch({ type: "pickerClose" });
2013
+ return;
2014
+ }
2015
+ if (key.upArrow) {
2016
+ dispatch({ type: "pickerMove", delta: -1 });
2017
+ return;
2018
+ }
2019
+ if (key.downArrow) {
2020
+ dispatch({ type: "pickerMove", delta: 1 });
2021
+ return;
2022
+ }
2023
+ if (key.return) {
2024
+ await acceptPickerSelection();
2025
+ return;
2026
+ }
2027
+ }
2028
+ if (key.return) {
2029
+ await submit();
2030
+ return;
2031
+ }
2032
+ if (key.backspace || key.delete) {
2033
+ if (key.ctrl) {
2034
+ const { cursor, buffer } = state;
2035
+ if (key.backspace) {
2036
+ if (cursor === 0) return;
2037
+ const beforeCursor = buffer.slice(0, cursor);
2038
+ const lastWordStart = beforeCursor.lastIndexOf(" ") + 1;
2039
+ const next3 = buffer.slice(0, lastWordStart) + buffer.slice(cursor);
2040
+ dispatch({ type: "setBuffer", buffer: next3, cursor: lastWordStart });
2041
+ } else {
2042
+ if (cursor >= buffer.length) return;
2043
+ const afterCursor = buffer.slice(cursor);
2044
+ const nextWordStart = afterCursor.indexOf(" ");
2045
+ const end = nextWordStart === -1 ? buffer.length : cursor + nextWordStart + 1;
2046
+ const next3 = buffer.slice(0, cursor) + buffer.slice(end);
2047
+ dispatch({ type: "setBuffer", buffer: next3, cursor });
2048
+ }
2049
+ return;
2050
+ }
2051
+ if (state.cursor === 0) return;
2052
+ const next2 = state.buffer.slice(0, state.cursor - 1) + state.buffer.slice(state.cursor);
2053
+ dispatch({ type: "setBuffer", buffer: next2, cursor: state.cursor - 1 });
2054
+ return;
2055
+ }
2056
+ if (key.leftArrow) {
2057
+ if (key.ctrl) {
2058
+ const { cursor, buffer } = state;
2059
+ if (cursor === 0) return;
2060
+ const beforeCursor = buffer.slice(0, cursor);
2061
+ const prevWordStart = beforeCursor.lastIndexOf(" ");
2062
+ const target = prevWordStart === -1 ? 0 : prevWordStart + 1;
2063
+ dispatch({ type: "setBuffer", buffer, cursor: target });
2064
+ return;
2065
+ }
2066
+ if (state.cursor > 0) dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor - 1 });
2067
+ return;
2068
+ }
2069
+ if (key.rightArrow) {
2070
+ if (key.ctrl) {
2071
+ const { cursor, buffer } = state;
2072
+ if (cursor >= buffer.length) return;
2073
+ const afterCursor = buffer.slice(cursor);
2074
+ const nextWordStart = afterCursor.indexOf(" ");
2075
+ const target = nextWordStart === -1 ? buffer.length : cursor + nextWordStart + 1;
2076
+ dispatch({ type: "setBuffer", buffer, cursor: target });
2077
+ return;
2078
+ }
2079
+ if (state.cursor < state.buffer.length) dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor + 1 });
2080
+ return;
2081
+ }
2082
+ if (key.upArrow) {
2083
+ if (state.inputHistory.length > 0) dispatch({ type: "historyUp" });
2084
+ return;
2085
+ }
2086
+ if (key.downArrow) {
2087
+ if (state.historyIndex > 0) dispatch({ type: "historyDown" });
2088
+ return;
2089
+ }
2090
+ if (key.ctrl && input === "a") {
2091
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: 0 });
2092
+ return;
2093
+ }
2094
+ if (key.ctrl && input === "e") {
2095
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.buffer.length });
2096
+ return;
2097
+ }
2098
+ if (key.ctrl && input === "u") {
2099
+ dispatch({ type: "setBuffer", buffer: "", cursor: 0 });
2100
+ return;
2101
+ }
2102
+ if (key.ctrl && input === "w") {
2103
+ const { cursor, buffer } = state;
2104
+ if (cursor === 0) return;
2105
+ const beforeCursor = buffer.slice(0, cursor);
2106
+ const lastWordStart = beforeCursor.lastIndexOf(" ") + 1;
2107
+ const next2 = buffer.slice(0, lastWordStart) + buffer.slice(cursor);
2108
+ dispatch({ type: "setBuffer", buffer: next2, cursor: lastWordStart });
2109
+ return;
2110
+ }
2111
+ if (key.meta && input === "v") {
2112
+ await pasteClipboardImage();
2113
+ return;
2114
+ }
2115
+ if (!input || key.ctrl || key.meta) return;
2116
+ let bracketedPaste = false;
2117
+ if (input.includes("\x1B[200~") || input.includes("\x1B[201~")) {
2118
+ input = input.replace(/\x1b\[200~/g, "").replace(/\x1b\[201~/g, "");
2119
+ bracketedPaste = true;
2120
+ }
2121
+ if (bracketedPaste || input.length > PASTE_THRESHOLD_CHARS || input.includes("\n")) {
2122
+ const builder = builderRef.current;
2123
+ if (!builder) return;
2124
+ const ph = await builder.appendPaste(input);
2125
+ if (ph) {
2126
+ const lineCount = input.split("\n").length;
2127
+ dispatch({ type: "addPlaceholder", ph: `${ph} (${lineCount} lines)` });
2128
+ } else {
2129
+ const next2 = state.buffer.slice(0, state.cursor) + input + state.buffer.slice(state.cursor);
2130
+ dispatch({ type: "setBuffer", buffer: next2, cursor: state.cursor + input.length });
2131
+ }
2132
+ return;
2133
+ }
2134
+ const next = state.buffer.slice(0, state.cursor) + input + state.buffer.slice(state.cursor);
2135
+ dispatch({ type: "setBuffer", buffer: next, cursor: state.cursor + input.length });
2136
+ };
2137
+ const runBlocks = async (blocks) => {
2138
+ const ctrl = new AbortController();
2139
+ activeCtrlRef.current = ctrl;
2140
+ dispatch({ type: "status", status: "running" });
2141
+ try {
2142
+ const startedAt = Date.now();
2143
+ const before = tokenCounter?.total();
2144
+ const costBefore = tokenCounter?.estimateCost().total ?? 0;
2145
+ const result = await agent.run(blocks, { signal: ctrl.signal });
2146
+ const streamed = streamingTextRef.current;
2147
+ const text = result.status === "done" && result.finalText ? result.finalText : streamed;
2148
+ if (text && text.trim()) {
2149
+ dispatch({ type: "addEntry", entry: { kind: "assistant", text } });
2150
+ }
2151
+ streamingTextRef.current = "";
2152
+ pendingDeltaRef.current = "";
2153
+ if (flushTimerRef.current) {
2154
+ clearTimeout(flushTimerRef.current);
2155
+ flushTimerRef.current = null;
2156
+ }
2157
+ dispatch({ type: "streamReset" });
2158
+ if (result.status === "aborted") {
2159
+ dispatch({ type: "addEntry", entry: { kind: "warn", text: "Aborted." } });
2160
+ } else if (result.status === "failed") {
2161
+ dispatch({
2162
+ type: "addEntry",
2163
+ entry: {
2164
+ kind: "error",
2165
+ text: `Failed: ${result.error instanceof Error ? result.error.message : String(result.error)}`
2166
+ }
2167
+ });
2168
+ } else if (result.status === "max_iterations") {
2169
+ dispatch({
2170
+ type: "addEntry",
2171
+ entry: { kind: "warn", text: `Hit max iterations (${result.iterations}).` }
2172
+ });
2173
+ }
2174
+ if (tokenCounter && before) {
2175
+ const after = tokenCounter.total();
2176
+ const costAfter = tokenCounter.estimateCost().total;
2177
+ dispatch({
2178
+ type: "addEntry",
2179
+ entry: {
2180
+ kind: "turn-summary",
2181
+ text: `[in: ${fmtTok2(after.input - before.input)} out: ${fmtTok2(after.output - before.output)} iters: ${result.iterations} cost: ${(costAfter - costBefore).toFixed(4)} ${((Date.now() - startedAt) / 1e3).toFixed(1)}s]`
2182
+ }
2183
+ });
2184
+ }
2185
+ } catch (err) {
2186
+ dispatch({
2187
+ type: "addEntry",
2188
+ entry: { kind: "error", text: err instanceof Error ? err.message : String(err) }
2189
+ });
2190
+ } finally {
2191
+ activeCtrlRef.current = null;
2192
+ dispatch({ type: "status", status: "idle" });
2193
+ }
2194
+ const head = stateRef.current.queue[0];
2195
+ if (head) {
2196
+ dispatch({ type: "dequeueFirst" });
2197
+ await runBlocks(head.blocks);
2198
+ }
2199
+ };
2200
+ const submit = async () => {
2201
+ const raw = state.buffer;
2202
+ const trimmed = raw.trim();
2203
+ if (!trimmed && state.placeholders.length === 0) return;
2204
+ dispatch({ type: "resetInterrupts" });
2205
+ if (trimmed.startsWith("/")) {
2206
+ dispatch({ type: "addEntry", entry: { kind: "user", text: trimmed } });
2207
+ if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
2208
+ dispatch({ type: "clearInput" });
2209
+ try {
2210
+ const res = await slashRegistry.dispatch(trimmed, agent.ctx);
2211
+ if (res?.message) {
2212
+ dispatch({ type: "addEntry", entry: { kind: "info", text: res.message } });
2213
+ }
2214
+ if (res?.exit) {
2215
+ exit();
2216
+ onExit(0);
2217
+ }
2218
+ } catch (err) {
2219
+ dispatch({
2220
+ type: "addEntry",
2221
+ entry: { kind: "error", text: err instanceof Error ? err.message : String(err) }
2222
+ });
2223
+ }
2224
+ return;
2225
+ }
2226
+ const builder = builderRef.current;
2227
+ if (!builder) return;
2228
+ if (trimmed) builder.appendText(trimmed);
2229
+ const blocks = await builder.submit();
2230
+ const displayText = trimmed || "(attachments only)";
2231
+ dispatch({ type: "clearInput" });
2232
+ if (state.status !== "idle") {
2233
+ dispatch({
2234
+ type: "addEntry",
2235
+ entry: { kind: "user", text: displayText, queued: true }
2236
+ });
2237
+ dispatch({ type: "enqueue", item: { displayText, blocks } });
2238
+ if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
2239
+ return;
2240
+ }
2241
+ dispatch({ type: "addEntry", entry: { kind: "user", text: displayText } });
2242
+ if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
2243
+ await runBlocks(blocks);
2244
+ };
2245
+ const inputHint = useMemo(() => {
2246
+ if (state.status !== "idle") return "";
2247
+ if (state.buffer.startsWith("/")) return "slash command \u2014 Enter to dispatch";
2248
+ if (state.picker.open) return "";
2249
+ return "";
2250
+ }, [state.buffer, state.status, state.picker.open]);
2251
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2252
+ /* @__PURE__ */ jsx(History, { entries: state.entries, streamingText: state.streamingText }),
2253
+ /* @__PURE__ */ jsx(
2254
+ Input,
2255
+ {
2256
+ value: state.buffer,
2257
+ cursor: state.cursor,
2258
+ placeholders: state.placeholders,
2259
+ disabled: state.status === "aborting",
2260
+ hint: inputHint,
2261
+ onKey: handleKey
2262
+ }
2263
+ ),
2264
+ state.picker.open ? /* @__PURE__ */ jsx(
2265
+ FilePicker,
2266
+ {
2267
+ query: state.picker.query,
2268
+ matches: state.picker.matches,
2269
+ selected: state.picker.selected
2270
+ }
2271
+ ) : null,
2272
+ state.slashPicker.open ? /* @__PURE__ */ jsx(
2273
+ SlashMenu,
2274
+ {
2275
+ query: state.slashPicker.query,
2276
+ matches: state.slashPicker.matches,
2277
+ selected: state.slashPicker.selected
2278
+ }
2279
+ ) : null,
2280
+ /* @__PURE__ */ jsx(
2281
+ StatusBar,
2282
+ {
2283
+ model,
2284
+ state: state.status,
2285
+ tokenCounter,
2286
+ hint: renderRunningTools(state.runningTools) || state.hint,
2287
+ queueCount: state.queue.length,
2288
+ yolo,
2289
+ elapsedMs,
2290
+ todos,
2291
+ git: gitInfo,
2292
+ context: contextWindow
2293
+ }
2294
+ )
2295
+ ] });
2296
+ }
2297
+ function renderRunningTools(running) {
2298
+ if (running.size === 0) return "";
2299
+ let oldest = null;
2300
+ for (const info of running.values()) {
2301
+ if (!oldest || info.startedAt < oldest.startedAt) oldest = info;
2302
+ }
2303
+ if (!oldest) return "";
2304
+ const elapsedSec = ((Date.now() - oldest.startedAt) / 1e3).toFixed(1);
2305
+ const more = running.size > 1 ? ` (+${running.size - 1})` : "";
2306
+ return `running: ${oldest.name} ${elapsedSec}s${more}`;
2307
+ }
2308
+ function detectAtToken(buffer, cursor) {
2309
+ let i = cursor - 1;
2310
+ while (i >= 0) {
2311
+ const ch = buffer.charCodeAt(i);
2312
+ if (ch === 64) {
2313
+ if (i === 0 || /\s/.test(buffer[i - 1] ?? "")) {
2314
+ return { start: i, end: cursor, query: buffer.slice(i + 1, cursor) };
2315
+ }
2316
+ return null;
2317
+ }
2318
+ if (ch === 32 || ch === 9 || ch === 10) return null;
2319
+ i--;
2320
+ }
2321
+ return null;
2322
+ }
2323
+ function fmtTok2(n) {
2324
+ if (n < 1e3) return String(n);
2325
+ if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
2326
+ return `${(n / 1e6).toFixed(1)}M`;
2327
+ }
2328
+
2329
+ // src/run-tui.ts
2330
+ var BRACKETED_PASTE_ON = "\x1B[?2004h";
2331
+ var BRACKETED_PASTE_OFF = "\x1B[?2004l";
2332
+ var ALT_SCREEN_ON = "\x1B[?1049h";
2333
+ var ALT_SCREEN_OFF = "\x1B[?1049l";
2334
+ var CURSOR_HOME = "\x1B[H";
2335
+ async function runTui(opts) {
2336
+ const stdout = process.stdout;
2337
+ const stdin = process.stdin;
2338
+ if (!stdout.isTTY || !stdin.isTTY) {
2339
+ process.stderr.write(
2340
+ "wstack: --tui requires an interactive terminal on both stdin and stdout.\n Drop the flag (use the plain REPL) or run wstack directly without piping.\n"
2341
+ );
2342
+ return 2;
2343
+ }
2344
+ const useAltScreen = opts.altScreen === true;
2345
+ if (useAltScreen) {
2346
+ stdout.write(ALT_SCREEN_ON);
2347
+ stdout.write(CURSOR_HOME);
2348
+ }
2349
+ stdout.write(BRACKETED_PASTE_ON);
2350
+ let cleaned = false;
2351
+ const cleanup = () => {
2352
+ if (cleaned) return;
2353
+ cleaned = true;
2354
+ try {
2355
+ stdout.write(BRACKETED_PASTE_OFF);
2356
+ if (useAltScreen) {
2357
+ stdout.write(ALT_SCREEN_OFF);
2358
+ }
2359
+ } catch {
2360
+ }
2361
+ };
2362
+ const signals = ["SIGTERM", "SIGHUP", "SIGINT"];
2363
+ const signalHandler = () => cleanup();
2364
+ const exitHandler = () => cleanup();
2365
+ for (const s of signals) process.on(s, signalHandler);
2366
+ process.on("exit", exitHandler);
2367
+ const detachListeners = () => {
2368
+ for (const s of signals) process.off(s, signalHandler);
2369
+ process.off("exit", exitHandler);
2370
+ };
2371
+ return new Promise((resolve) => {
2372
+ let exitCode = 0;
2373
+ const onExit = (code) => {
2374
+ exitCode = code;
2375
+ };
2376
+ const settle = (code) => {
2377
+ cleanup();
2378
+ detachListeners();
2379
+ if (useAltScreen && opts.onAfterExit) {
2380
+ try {
2381
+ opts.onAfterExit();
2382
+ } catch {
2383
+ }
2384
+ }
2385
+ resolve(code);
2386
+ };
2387
+ let instance;
2388
+ try {
2389
+ instance = render(
2390
+ React.createElement(App, {
2391
+ agent: opts.agent,
2392
+ slashRegistry: opts.slashRegistry,
2393
+ attachments: opts.attachments,
2394
+ events: opts.events,
2395
+ tokenCounter: opts.tokenCounter,
2396
+ model: opts.model,
2397
+ banner: opts.banner ?? true,
2398
+ queueStore: opts.queueStore,
2399
+ yolo: opts.yolo,
2400
+ appVersion: opts.appVersion,
2401
+ provider: opts.provider,
2402
+ effectiveMaxContext: opts.effectiveMaxContext,
2403
+ onExit
2404
+ }),
2405
+ { exitOnCtrlC: false }
2406
+ );
2407
+ } catch (err) {
2408
+ process.stderr.write(
2409
+ `wstack: TUI failed to start: ${err instanceof Error ? err.message : String(err)}
2410
+ `
2411
+ );
2412
+ settle(1);
2413
+ return;
2414
+ }
2415
+ instance.waitUntilExit().then(() => settle(exitCode)).catch(() => settle(1));
2416
+ });
2417
+ }
2418
+
2419
+ export { runTui };
2420
+ //# sourceMappingURL=index.js.map
2421
+ //# sourceMappingURL=index.js.map