@wrongstack/tui 0.1.8 → 0.1.10

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 CHANGED
@@ -1,13 +1,222 @@
1
- import React, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
2
1
  import { render, useApp, Box, useStdout, Static, Text, useInput } from 'ink';
2
+ import React, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
3
+ import * as fs from 'fs/promises';
3
4
  import * as path3 from 'path';
4
- import * as fs2 from 'fs/promises';
5
- import { InputBuilder } from '@wrongstack/core';
6
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { InputBuilder, formatTodosList } from '@wrongstack/core';
7
6
  import { spawn } from 'child_process';
8
7
  import * as os from 'os';
8
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
9
 
10
10
  // src/run-tui.ts
11
+ var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
12
+ async function readClipboardImage() {
13
+ const platform = process.platform;
14
+ if (platform === "win32") return readWindows();
15
+ if (platform === "darwin") return readDarwin();
16
+ if (platform === "linux") return readLinux();
17
+ return null;
18
+ }
19
+ async function readWindows() {
20
+ const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
21
+ const ps = [
22
+ "Add-Type -AssemblyName System.Windows.Forms",
23
+ "Add-Type -AssemblyName System.Drawing",
24
+ "$img = [System.Windows.Forms.Clipboard]::GetImage()",
25
+ 'if ($img -eq $null) { Write-Output "NO_IMAGE"; exit 0 }',
26
+ `$img.Save('${tmp.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png)`,
27
+ 'Write-Output "OK"'
28
+ ].join("; ");
29
+ const out = await runCmd("powershell", ["-NoProfile", "-Command", ps]);
30
+ if (!out || out.trim() === "NO_IMAGE") return null;
31
+ if (!out.includes("OK")) return null;
32
+ return readPngFile(tmp);
33
+ }
34
+ async function readDarwin() {
35
+ const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
36
+ const script = [
37
+ "try",
38
+ ` set the_file to (open for access POSIX file "${tmp}" with write permission)`,
39
+ " write (the clipboard as \xABclass PNGf\xBB) to the_file",
40
+ " close access the_file",
41
+ "on error",
42
+ " try",
43
+ ' close access POSIX file "' + tmp + '"',
44
+ " end try",
45
+ ' return "NO_IMAGE"',
46
+ "end try",
47
+ 'return "OK"'
48
+ ].join("\n");
49
+ const out = await runCmd("osascript", ["-e", script]);
50
+ if (!out || out.trim() !== "OK") return null;
51
+ return readPngFile(tmp);
52
+ }
53
+ async function readLinux() {
54
+ const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
55
+ const tries = [
56
+ ["wl-paste", ["--type", "image/png"]],
57
+ ["xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]]
58
+ ];
59
+ for (const [cmd, args] of tries) {
60
+ const ok = await runCmdToFile(cmd, args, tmp).catch(() => false);
61
+ if (ok) return readPngFile(tmp);
62
+ }
63
+ return null;
64
+ }
65
+ async function readPngFile(p) {
66
+ try {
67
+ const buf = await fs.readFile(p);
68
+ if (buf.length === 0) {
69
+ await fs.unlink(p).catch(() => void 0);
70
+ return null;
71
+ }
72
+ if (buf.length > MAX_IMAGE_BYTES) {
73
+ await fs.unlink(p).catch(() => void 0);
74
+ throw new Error(`Clipboard image exceeds ${MAX_IMAGE_BYTES / 1024 / 1024}MB limit`);
75
+ }
76
+ if (buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71) {
77
+ await fs.unlink(p).catch(() => void 0);
78
+ return null;
79
+ }
80
+ await fs.unlink(p).catch(() => void 0);
81
+ return { base64: buf.toString("base64"), mediaType: "image/png", bytes: buf.length };
82
+ } catch (err) {
83
+ if (err.code === "ENOENT") return null;
84
+ throw err;
85
+ }
86
+ }
87
+ function runCmd(cmd, args) {
88
+ return new Promise((resolve) => {
89
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
90
+ let out = "";
91
+ child.stdout.on("data", (c) => {
92
+ out += String(c);
93
+ });
94
+ child.on("error", () => resolve(null));
95
+ child.on("exit", (code) => resolve(code === 0 ? out : null));
96
+ });
97
+ }
98
+ function runCmdToFile(cmd, args, outPath) {
99
+ return new Promise((resolve) => {
100
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
101
+ const chunks = [];
102
+ child.stdout.on("data", (c) => chunks.push(c));
103
+ child.on("error", () => resolve(false));
104
+ child.on("exit", async (code) => {
105
+ if (code !== 0 || chunks.length === 0) return resolve(false);
106
+ try {
107
+ await fs.writeFile(outPath, Buffer.concat(chunks));
108
+ resolve(true);
109
+ } catch {
110
+ resolve(false);
111
+ }
112
+ });
113
+ });
114
+ }
115
+ function stringifyInput(input) {
116
+ if (!input || typeof input !== "object") return "";
117
+ const obj = input;
118
+ return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${truncate(JSON.stringify(v), 80)}`).join(" ");
119
+ }
120
+ function truncate(s, max) {
121
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
122
+ }
123
+ function hasDiff(input) {
124
+ return Boolean(
125
+ input && typeof input === "object" && "diff" in input
126
+ );
127
+ }
128
+ function renderDiffLine(line) {
129
+ const prefix = line.startsWith("+") ? "green" : line.startsWith("-") ? "red" : line.startsWith("@@") ? "cyan" : void 0;
130
+ return /* @__PURE__ */ jsxs(Text, { color: prefix, children: [
131
+ line,
132
+ "\n"
133
+ ] }, line);
134
+ }
135
+ function renderDiff(diff) {
136
+ const lines = diff.split("\n").filter((l) => l.length > 0).slice(0, 20);
137
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 2, children: lines.map((l) => renderDiffLine(l)) });
138
+ }
139
+ function ConfirmPrompt({
140
+ toolName,
141
+ input,
142
+ suggestedPattern,
143
+ onDecision
144
+ }) {
145
+ useInput((_, key) => {
146
+ if (key.return) {
147
+ onDecision("yes");
148
+ } else if (key.escape) {
149
+ onDecision("no");
150
+ } else if (key.ctrl && _.toLowerCase() === "a") {
151
+ onDecision("always");
152
+ } else if (key.ctrl && _.toLowerCase() === "d") {
153
+ onDecision("deny");
154
+ }
155
+ });
156
+ const inputSummary = stringifyInput(input);
157
+ const showDiff = hasDiff(input);
158
+ const inp = input;
159
+ const diff = typeof inp?.diff === "string" ? inp.diff : "";
160
+ return /* @__PURE__ */ jsxs(
161
+ Box,
162
+ {
163
+ flexDirection: "column",
164
+ borderStyle: "single",
165
+ borderTop: false,
166
+ borderLeft: false,
167
+ borderRight: false,
168
+ borderBottom: false,
169
+ paddingX: 1,
170
+ children: [
171
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
172
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
173
+ /* @__PURE__ */ jsx(Text, { children: " " }),
174
+ /* @__PURE__ */ jsx(Text, { bold: true, children: toolName })
175
+ ] }),
176
+ inputSummary ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: inputSummary }) : null,
177
+ showDiff && diff ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: renderDiff(diff) }) : null,
178
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
179
+ /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
180
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
181
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
182
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
183
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
184
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
185
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
186
+ " always (",
187
+ suggestedPattern,
188
+ ") "
189
+ ] }),
190
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
191
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
192
+ ] }) })
193
+ ]
194
+ }
195
+ );
196
+ }
197
+ function FilePicker({ query, matches, selected }) {
198
+ if (matches.length === 0) {
199
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
200
+ "@",
201
+ query,
202
+ " \u2014 no matches"
203
+ ] }) });
204
+ }
205
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
206
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
207
+ "@",
208
+ query || "\u2026",
209
+ " \u2014 \u2191/\u2193 select, Enter attach, Esc cancel"
210
+ ] }),
211
+ matches.map((m, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
212
+ i === selected ? "\u203A " : " ",
213
+ highlight(m)
214
+ ] }, m))
215
+ ] });
216
+ }
217
+ function highlight(path4, query) {
218
+ return path4;
219
+ }
11
220
 
12
221
  // src/markdown-table.ts
13
222
  function renderMarkdownTables(text, maxWidth) {
@@ -198,13 +407,53 @@ function History({ entries, streamingText, toolStream }) {
198
407
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "> " }),
199
408
  /* @__PURE__ */ jsx(Text, { children: tail })
200
409
  ] }) : null,
201
- toolTail ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
202
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: `\u25C6 ${toolStream.name} ` }),
203
- /* @__PURE__ */ jsx(Text, { children: toolTail })
204
- ] }) : null
410
+ toolTail ? /* @__PURE__ */ jsx(
411
+ ToolStreamBox,
412
+ {
413
+ name: toolStream.name,
414
+ text: toolTail,
415
+ startedAt: toolStream.startedAt,
416
+ termWidth
417
+ }
418
+ ) : null
205
419
  ] });
206
420
  }
207
421
  var MAX_STREAM_DISPLAY_CHARS = 480;
422
+ var MAX_STREAM_LINES = 8;
423
+ function ToolStreamBox({
424
+ name,
425
+ text,
426
+ startedAt,
427
+ termWidth
428
+ }) {
429
+ const [tick, setTick] = useState(0);
430
+ useEffect(() => {
431
+ const t = setInterval(() => setTick((n) => n + 1), 500);
432
+ return () => clearInterval(t);
433
+ }, []);
434
+ const elapsedMs = Date.now() - startedAt;
435
+ const lines = text.split("\n");
436
+ const totalLines = lines.length;
437
+ const hidden = Math.max(0, totalLines - MAX_STREAM_LINES);
438
+ const visible = hidden > 0 ? lines.slice(hidden) : lines;
439
+ const contentWidth = Math.max(20, Math.min(termWidth - 4, 100));
440
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 0, children: [
441
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
442
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25C6 " }),
443
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: name }),
444
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u23F1 ${fmtDuration(elapsedMs)}` }),
445
+ hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${totalLines} lines, showing last ${MAX_STREAM_LINES})` }) : null
446
+ ] }),
447
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [
448
+ hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: ` \u2026 ${hidden} more line${hidden === 1 ? "" : "s"} above` }) : null,
449
+ visible.map((line, i) => {
450
+ const key = i;
451
+ const trimmed = line.length > contentWidth ? `${line.slice(0, contentWidth - 1)}\u2026` : line;
452
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: trimmed || " " }, key);
453
+ })
454
+ ] })
455
+ ] });
456
+ }
208
457
  function tailForDisplay(text, maxChars) {
209
458
  if (text.length <= maxChars) return text;
210
459
  const cut = text.length - maxChars;
@@ -234,7 +483,10 @@ function DiffBlock({ rows, hidden }) {
234
483
  hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: ` \u2026 ${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
235
484
  ] });
236
485
  }
237
- function Entry({ entry, termWidth }) {
486
+ function Entry({
487
+ entry,
488
+ termWidth
489
+ }) {
238
490
  switch (entry.kind) {
239
491
  case "user":
240
492
  return /* @__PURE__ */ jsxs(Text, { children: [
@@ -247,8 +499,28 @@ function Entry({ entry, termWidth }) {
247
499
  return /* @__PURE__ */ jsx(Text, { children: renderMarkdownTables(entry.text, termWidth) });
248
500
  case "tool": {
249
501
  const argSummary = formatToolArgs(entry.name, entry.input);
250
- const outLines = formatToolOutput(entry.name, entry.output, entry.ok);
502
+ const outLines = formatToolOutput(
503
+ entry.name,
504
+ entry.output,
505
+ entry.ok,
506
+ entry.outputBytes,
507
+ entry.outputLines
508
+ );
251
509
  const diff = entry.ok ? extractDiffPreview(entry.name, entry.output) : void 0;
510
+ const sizeChip = (() => {
511
+ if (!entry.ok) return "";
512
+ const parts = [];
513
+ if (entry.outputLines !== void 0 && entry.outputLines > 0) {
514
+ parts.push(`${entry.outputLines} L`);
515
+ }
516
+ if (entry.outputBytes && entry.outputBytes > 0) {
517
+ parts.push(fmtBytes(entry.outputBytes));
518
+ }
519
+ if (entry.outputTokens && entry.outputTokens > 0) {
520
+ parts.push(`\u2248${fmtTok(entry.outputTokens)} tok`);
521
+ }
522
+ return parts.join(" \xB7 ");
523
+ })();
252
524
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
253
525
  /* @__PURE__ */ jsxs(Text, { children: [
254
526
  /* @__PURE__ */ jsx(Text, { color: entry.ok ? "green" : "red", children: entry.ok ? "\u25CF" : "\u2717" }),
@@ -258,13 +530,21 @@ function Entry({ entry, termWidth }) {
258
530
  /* @__PURE__ */ jsx(Text, { children: " " }),
259
531
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: argSummary })
260
532
  ] }) : null,
261
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${fmtDuration(entry.durationMs)}` })
533
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${fmtDuration(entry.durationMs)}` }),
534
+ sizeChip ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${sizeChip}` }) : null
262
535
  ] }),
263
536
  outLines.map((line, i) => (
264
537
  // biome-ignore lint/suspicious/noArrayIndexKey: tool output lines are static, index is stable
265
538
  /* @__PURE__ */ jsxs(Text, { children: [
266
539
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: i === outLines.length - 1 && !diff ? " \u2514\u2500 " : " \u251C\u2500 " }),
267
- /* @__PURE__ */ jsx(Text, { color: !entry.ok || line.startsWith("!") ? "red" : void 0, dimColor: entry.ok && !line.startsWith("!"), children: line })
540
+ /* @__PURE__ */ jsx(
541
+ Text,
542
+ {
543
+ color: !entry.ok || line.startsWith("!") ? "red" : void 0,
544
+ dimColor: entry.ok && !line.startsWith("!"),
545
+ children: line
546
+ }
547
+ )
268
548
  ] }, i)
269
549
  )),
270
550
  diff ? /* @__PURE__ */ jsx(DiffBlock, { rows: diff.rows, hidden: diff.hidden }) : null
@@ -279,29 +559,41 @@ function Entry({ entry, termWidth }) {
279
559
  case "turn-summary":
280
560
  return /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.text });
281
561
  case "confirm":
282
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderBottom: false, paddingX: 1, children: [
283
- /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
284
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
285
- /* @__PURE__ */ jsx(Text, { children: " " }),
286
- /* @__PURE__ */ jsx(Text, { bold: true, children: entry.toolName })
287
- ] }),
288
- entry.input && typeof entry.input === "object" ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: Object.entries(entry.input).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${String(v).slice(0, 80)}`).join(" ") }) : null,
289
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
290
- /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
291
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
292
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
293
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
294
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
295
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
296
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
297
- " always (",
298
- entry.suggestedPattern,
299
- ") "
300
- ] }),
301
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
302
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
303
- ] }) })
304
- ] });
562
+ return /* @__PURE__ */ jsxs(
563
+ Box,
564
+ {
565
+ flexDirection: "column",
566
+ borderStyle: "single",
567
+ borderTop: false,
568
+ borderLeft: false,
569
+ borderRight: false,
570
+ borderBottom: false,
571
+ paddingX: 1,
572
+ children: [
573
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
574
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
575
+ /* @__PURE__ */ jsx(Text, { children: " " }),
576
+ /* @__PURE__ */ jsx(Text, { bold: true, children: entry.toolName })
577
+ ] }),
578
+ entry.input && typeof entry.input === "object" ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: Object.entries(entry.input).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${String(v).slice(0, 80)}`).join(" ") }) : null,
579
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
580
+ /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
581
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
582
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
583
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
584
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
585
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
586
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
587
+ " always (",
588
+ entry.suggestedPattern,
589
+ ") "
590
+ ] }),
591
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
592
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
593
+ ] }) })
594
+ ]
595
+ }
596
+ );
305
597
  case "banner":
306
598
  return /* @__PURE__ */ jsx(Banner, { entry });
307
599
  }
@@ -310,55 +602,51 @@ function Banner({
310
602
  entry
311
603
  }) {
312
604
  const cwdShort = shortenPath(entry.cwd, 48);
313
- return /* @__PURE__ */ jsxs(
314
- Box,
315
- {
316
- flexDirection: "column",
317
- borderStyle: "round",
318
- borderColor: "magenta",
319
- paddingX: 2,
320
- paddingY: 0,
321
- children: [
322
- /* @__PURE__ */ jsxs(Text, { children: [
323
- /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: " \u259F\u259B " }),
324
- /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "WrongStack" }),
325
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " v" }),
326
- /* @__PURE__ */ jsx(Text, { children: entry.version })
327
- ] }),
328
- /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: " Built on the wrong stack. Shipped anyway." }),
329
- /* @__PURE__ */ jsxs(Text, { children: [
330
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: " provider " }),
331
- /* @__PURE__ */ jsxs(Text, { children: [
332
- entry.provider,
333
- "/",
334
- entry.model
335
- ] })
336
- ] }),
337
- entry.family ? /* @__PURE__ */ jsxs(Text, { children: [
338
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: " family " }),
339
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.family })
340
- ] }) : null,
341
- entry.keyTail ? /* @__PURE__ */ jsxs(Text, { children: [
342
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: " key " }),
343
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u25CF\u25CF\u25CF\u2026" }),
344
- /* @__PURE__ */ jsx(Text, { children: entry.keyTail })
345
- ] }) : null,
346
- /* @__PURE__ */ jsxs(Text, { children: [
347
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: " cwd " }),
348
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: cwdShort })
349
- ] }),
350
- /* @__PURE__ */ jsxs(Text, { children: [
351
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: " hints " }),
352
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/help \xB7 /init \xB7 /memory \xB7 /queue \xB7 /exit" })
353
- ] })
354
- ]
355
- }
356
- );
605
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 2, paddingY: 0, children: [
606
+ /* @__PURE__ */ jsxs(Text, { children: [
607
+ /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: " \u259F\u259B " }),
608
+ /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "WrongStack" }),
609
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " v" }),
610
+ /* @__PURE__ */ jsx(Text, { children: entry.version })
611
+ ] }),
612
+ /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: " Built on the wrong stack. Shipped anyway." }),
613
+ /* @__PURE__ */ jsxs(Text, { children: [
614
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " provider " }),
615
+ /* @__PURE__ */ jsxs(Text, { children: [
616
+ entry.provider,
617
+ "/",
618
+ entry.model
619
+ ] })
620
+ ] }),
621
+ entry.family ? /* @__PURE__ */ jsxs(Text, { children: [
622
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " family " }),
623
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.family })
624
+ ] }) : null,
625
+ entry.keyTail ? /* @__PURE__ */ jsxs(Text, { children: [
626
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " key " }),
627
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u25CF\u25CF\u25CF\u2026" }),
628
+ /* @__PURE__ */ jsx(Text, { children: entry.keyTail })
629
+ ] }) : null,
630
+ /* @__PURE__ */ jsxs(Text, { children: [
631
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " cwd " }),
632
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: cwdShort })
633
+ ] }),
634
+ /* @__PURE__ */ jsxs(Text, { children: [
635
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " hints " }),
636
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/help \xB7 /init \xB7 /memory \xB7 /queue \xB7 /exit" })
637
+ ] })
638
+ ] });
357
639
  }
358
640
  function shortenPath(p, max) {
359
641
  if (p.length <= max) return p;
360
642
  return `\u2026${p.slice(p.length - (max - 1))}`;
361
643
  }
644
+ function fmtTok(n) {
645
+ if (!Number.isFinite(n) || n <= 0) return "0";
646
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
647
+ if (n >= 1e3) return `${(n / 1e3).toFixed(n >= 1e4 ? 0 : 1)}k`;
648
+ return String(n);
649
+ }
362
650
  function fmtDuration(ms) {
363
651
  if (ms < 1e3) return `${ms}ms`;
364
652
  if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
@@ -476,7 +764,7 @@ function formatToolArgs(toolName, input) {
476
764
  return "";
477
765
  }
478
766
  }
479
- function formatToolOutput(toolName, output, ok) {
767
+ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
480
768
  if (!output) return ok ? [] : ["failed"];
481
769
  const text = output.trim();
482
770
  if (!text) return ok ? [] : ["failed"];
@@ -541,12 +829,14 @@ function formatToolOutput(toolName, output, ok) {
541
829
  if (!diff) return [files && files.length === 0 ? "no changes" : "empty diff"];
542
830
  const head = [];
543
831
  if (mode) head.push(mode);
544
- if (files && files.length > 0) head.push(`${files.length} file${files.length === 1 ? "" : "s"}`);
832
+ if (files && files.length > 0)
833
+ head.push(`${files.length} file${files.length === 1 ? "" : "s"}`);
545
834
  if (truncated) head.push("truncated");
546
835
  return head.length > 0 ? [head.join(" \xB7 ")] : [];
547
836
  }
548
837
  }
549
838
  if (toolName === "read") {
839
+ if (outputLines !== void 0) return [];
550
840
  if (json && typeof json === "object") {
551
841
  const o = json;
552
842
  const bytes = numOf(o["bytes"]);
@@ -983,6 +1273,72 @@ function Input({
983
1273
  hint ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint }) : null
984
1274
  ] });
985
1275
  }
1276
+ function ModelPicker({
1277
+ step,
1278
+ providerOptions,
1279
+ modelOptions,
1280
+ selected,
1281
+ pickedProviderId,
1282
+ hint
1283
+ }) {
1284
+ if (step === "provider") {
1285
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1286
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u2501\u2501 Switch model \u2014 Step 1/2: Pick provider \u2501\u2501" }),
1287
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc cancel" }),
1288
+ providerOptions.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no providers with keys \u2014 add one via `wstack auth`)" }) : providerOptions.map((p, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1289
+ i === selected ? "\u203A " : " ",
1290
+ /* @__PURE__ */ jsx(Text, { bold: true, children: p.id.padEnd(28) }),
1291
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1292
+ " [",
1293
+ p.family,
1294
+ "]"
1295
+ ] }),
1296
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1297
+ " ",
1298
+ p.models.length,
1299
+ " model",
1300
+ p.models.length === 1 ? "" : "s"
1301
+ ] })
1302
+ ] }, p.id)),
1303
+ hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
1304
+ ] });
1305
+ }
1306
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1307
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
1308
+ "\u2501\u2501 Switch model \u2014 Step 2/2: Pick model (",
1309
+ pickedProviderId,
1310
+ ") \u2501\u2501"
1311
+ ] }),
1312
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc back \xB7 Ctrl-C cancel" }),
1313
+ modelOptions.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no models known for this provider)" }) : modelOptions.map((id, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1314
+ i === selected ? "\u203A " : " ",
1315
+ id
1316
+ ] }, id)),
1317
+ hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
1318
+ ] });
1319
+ }
1320
+ function SlashMenu({ query, matches, selected }) {
1321
+ const placeholder = query ? `/${query}` : "/";
1322
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1323
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1324
+ placeholder || "/",
1325
+ " \u2014 \u2191/\u2193 select, Enter dispatch, Tab autocomplete, Esc close"
1326
+ ] }),
1327
+ matches.map((m, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1328
+ i === selected ? "\u203A " : " ",
1329
+ /* @__PURE__ */ jsx(Text, { bold: true, children: m.name }),
1330
+ m.argsHint ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1331
+ " ",
1332
+ m.argsHint
1333
+ ] }) : null,
1334
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1335
+ " \u2014 ",
1336
+ m.description
1337
+ ] })
1338
+ ] }, m.name)),
1339
+ matches.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No matching commands" })
1340
+ ] });
1341
+ }
986
1342
  function StatusBar({
987
1343
  model,
988
1344
  state,
@@ -1028,11 +1384,12 @@ function StatusBar({
1028
1384
  usage ? /* @__PURE__ */ jsxs(Fragment, { children: [
1029
1385
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1030
1386
  /* @__PURE__ */ jsxs(Text, { children: [
1031
- "\u2191 ",
1032
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: fmtTok(usage.input) }),
1033
- " \u2193",
1387
+ "\u2191",
1034
1388
  " ",
1035
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: fmtTok(usage.output) })
1389
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: fmtTok2(usage.input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0)) }),
1390
+ " ",
1391
+ "\u2193 ",
1392
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: fmtTok2(usage.output) })
1036
1393
  ] })
1037
1394
  ] }) : null,
1038
1395
  cache2 && cache2.hitRatio > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1134,208 +1491,52 @@ function StatusBar({
1134
1491
  }
1135
1492
  function ContextChip({ ctx }) {
1136
1493
  const ratio = Math.max(0, Math.min(1, ctx.used / ctx.max));
1137
- const pct = Math.round(ratio * 100);
1138
- const color = ratio >= 0.85 ? "red" : ratio >= 0.65 ? "yellow" : "cyan";
1139
- return /* @__PURE__ */ jsxs(Text, { children: [
1140
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "ctx " }),
1141
- /* @__PURE__ */ jsx(Text, { color, children: renderProgress(ratio, 10) }),
1142
- /* @__PURE__ */ jsxs(Text, { color, children: [
1143
- " ",
1144
- pct,
1145
- "%"
1146
- ] }),
1147
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1148
- " ",
1149
- "(",
1150
- fmtTok(ctx.used),
1151
- "/",
1152
- fmtTok(ctx.max),
1153
- ")"
1154
- ] })
1155
- ] });
1156
- }
1157
- var FILLED = "\u2588";
1158
- var EMPTY = "\u2591";
1159
- function renderProgress(ratio, width) {
1160
- const clamped = Math.max(0, Math.min(1, ratio));
1161
- const filled = clamped === 0 ? 0 : Math.max(1, Math.round(clamped * width));
1162
- const capped = Math.min(width, filled);
1163
- return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
1164
- }
1165
- function fmtTok(n) {
1166
- if (n < 1e3) return String(n);
1167
- if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
1168
- return `${(n / 1e6).toFixed(1)}M`;
1169
- }
1170
- function fmtElapsed(ms) {
1171
- const totalSec = Math.floor(ms / 1e3);
1172
- const h = Math.floor(totalSec / 3600);
1173
- const m = Math.floor(totalSec % 3600 / 60);
1174
- const s = totalSec % 60;
1175
- if (h > 0) {
1176
- return `${h}:${pad2(m)}:${pad2(s)}`;
1177
- }
1178
- return `${pad2(m)}:${pad2(s)}`;
1179
- }
1180
- function pad2(n) {
1181
- return n < 10 ? `0${n}` : String(n);
1182
- }
1183
- function FilePicker({ query, matches, selected }) {
1184
- if (matches.length === 0) {
1185
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1186
- "@",
1187
- query,
1188
- " \u2014 no matches"
1189
- ] }) });
1190
- }
1191
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1192
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1193
- "@",
1194
- query || "\u2026",
1195
- " \u2014 \u2191/\u2193 select, Enter attach, Esc cancel"
1196
- ] }),
1197
- matches.map((m, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1198
- i === selected ? "\u203A " : " ",
1199
- highlight(m)
1200
- ] }, m))
1201
- ] });
1202
- }
1203
- function highlight(path4, query) {
1204
- return path4;
1205
- }
1206
- function SlashMenu({ query, matches, selected }) {
1207
- const placeholder = query ? `/${query}` : "/";
1208
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1209
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1210
- placeholder || "/",
1211
- " \u2014 \u2191/\u2193 select, Enter dispatch, Tab autocomplete, Esc close"
1212
- ] }),
1213
- matches.map((m, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1214
- i === selected ? "\u203A " : " ",
1215
- /* @__PURE__ */ jsx(Text, { bold: true, children: m.name }),
1216
- m.argsHint ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1217
- " ",
1218
- m.argsHint
1219
- ] }) : null,
1220
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1221
- " \u2014 ",
1222
- m.description
1223
- ] })
1224
- ] }, m.name)),
1225
- matches.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No matching commands" })
1226
- ] });
1227
- }
1228
- function ModelPicker({
1229
- step,
1230
- providerOptions,
1231
- modelOptions,
1232
- selected,
1233
- pickedProviderId,
1234
- hint
1235
- }) {
1236
- if (step === "provider") {
1237
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1238
- /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u2501\u2501 Switch model \u2014 Step 1/2: Pick provider \u2501\u2501" }),
1239
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc cancel" }),
1240
- providerOptions.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no providers with keys \u2014 add one via `wstack auth`)" }) : providerOptions.map((p, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1241
- i === selected ? "\u203A " : " ",
1242
- /* @__PURE__ */ jsx(Text, { bold: true, children: p.id.padEnd(28) }),
1243
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1244
- " [",
1245
- p.family,
1246
- "]"
1247
- ] }),
1248
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1249
- " ",
1250
- p.models.length,
1251
- " model",
1252
- p.models.length === 1 ? "" : "s"
1253
- ] })
1254
- ] }, p.id)),
1255
- hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
1256
- ] });
1257
- }
1258
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1259
- /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
1260
- "\u2501\u2501 Switch model \u2014 Step 2/2: Pick model",
1261
- " ",
1262
- "(",
1263
- pickedProviderId,
1264
- ") \u2501\u2501"
1265
- ] }),
1266
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc back \xB7 Ctrl-C cancel" }),
1267
- modelOptions.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no models known for this provider)" }) : modelOptions.map((id, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1268
- i === selected ? "\u203A " : " ",
1269
- id
1270
- ] }, id)),
1271
- hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
1272
- ] });
1273
- }
1274
- function stringifyInput(input) {
1275
- if (!input || typeof input !== "object") return "";
1276
- const obj = input;
1277
- return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${truncate(JSON.stringify(v), 80)}`).join(" ");
1278
- }
1279
- function truncate(s, max) {
1280
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
1281
- }
1282
- function hasDiff(input) {
1283
- return Boolean(
1284
- input && typeof input === "object" && "diff" in input
1285
- );
1286
- }
1287
- function renderDiffLine(line) {
1288
- const prefix = line.startsWith("+") ? "green" : line.startsWith("-") ? "red" : line.startsWith("@@") ? "cyan" : void 0;
1289
- return /* @__PURE__ */ jsxs(Text, { color: prefix, children: [
1290
- line,
1291
- "\n"
1292
- ] }, line);
1293
- }
1294
- function renderDiff(diff) {
1295
- const lines = diff.split("\n").filter((l) => l.length > 0).slice(0, 20);
1296
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 2, children: lines.map((l) => renderDiffLine(l)) });
1297
- }
1298
- function ConfirmPrompt({ toolName, input, suggestedPattern, onDecision }) {
1299
- useInput((_, key) => {
1300
- if (key.return) {
1301
- onDecision("yes");
1302
- } else if (key.escape) {
1303
- onDecision("no");
1304
- } else if (key.ctrl && _.toLowerCase() === "a") {
1305
- onDecision("always");
1306
- } else if (key.ctrl && _.toLowerCase() === "d") {
1307
- onDecision("deny");
1308
- }
1309
- });
1310
- const inputSummary = stringifyInput(input);
1311
- const showDiff = hasDiff(input);
1312
- const inp = input;
1313
- const diff = typeof inp?.diff === "string" ? inp.diff : "";
1314
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderBottom: false, paddingX: 1, children: [
1315
- /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
1316
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
1317
- /* @__PURE__ */ jsx(Text, { children: " " }),
1318
- /* @__PURE__ */ jsx(Text, { bold: true, children: toolName })
1494
+ const pct = Math.round(ratio * 100);
1495
+ const color = ratio >= 0.85 ? "red" : ratio >= 0.65 ? "yellow" : "cyan";
1496
+ return /* @__PURE__ */ jsxs(Text, { children: [
1497
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "ctx " }),
1498
+ /* @__PURE__ */ jsx(Text, { color, children: renderProgress(ratio, 10) }),
1499
+ /* @__PURE__ */ jsxs(Text, { color, children: [
1500
+ " ",
1501
+ pct,
1502
+ "%"
1319
1503
  ] }),
1320
- inputSummary ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: inputSummary }) : null,
1321
- showDiff && diff ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: renderDiff(diff) }) : null,
1322
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
1323
- /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
1324
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
1325
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
1326
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
1327
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
1328
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
1329
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1330
- " always (",
1331
- suggestedPattern,
1332
- ") "
1333
- ] }),
1334
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
1335
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
1336
- ] }) })
1504
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1505
+ " ",
1506
+ "(",
1507
+ fmtTok2(ctx.used),
1508
+ "/",
1509
+ fmtTok2(ctx.max),
1510
+ ")"
1511
+ ] })
1337
1512
  ] });
1338
1513
  }
1514
+ var FILLED = "\u2588";
1515
+ var EMPTY = "\u2591";
1516
+ function renderProgress(ratio, width) {
1517
+ const clamped = Math.max(0, Math.min(1, ratio));
1518
+ const filled = clamped === 0 ? 0 : Math.max(1, Math.round(clamped * width));
1519
+ const capped = Math.min(width, filled);
1520
+ return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
1521
+ }
1522
+ function fmtTok2(n) {
1523
+ if (n < 1e3) return String(n);
1524
+ if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
1525
+ return `${(n / 1e6).toFixed(1)}M`;
1526
+ }
1527
+ function fmtElapsed(ms) {
1528
+ const totalSec = Math.floor(ms / 1e3);
1529
+ const h = Math.floor(totalSec / 3600);
1530
+ const m = Math.floor(totalSec % 3600 / 60);
1531
+ const s = totalSec % 60;
1532
+ if (h > 0) {
1533
+ return `${h}:${pad2(m)}:${pad2(s)}`;
1534
+ }
1535
+ return `${pad2(m)}:${pad2(s)}`;
1536
+ }
1537
+ function pad2(n) {
1538
+ return n < 10 ? `0${n}` : String(n);
1539
+ }
1339
1540
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
1340
1541
  "node_modules",
1341
1542
  ".git",
@@ -1369,7 +1570,7 @@ async function walk(root, rel, depth, out) {
1369
1570
  const dir = rel ? path3.join(root, rel) : root;
1370
1571
  let entries;
1371
1572
  try {
1372
- entries = await fs2.readdir(dir, { withFileTypes: true });
1573
+ entries = await fs.readdir(dir, { withFileTypes: true });
1373
1574
  } catch {
1374
1575
  return;
1375
1576
  }
@@ -1414,108 +1615,56 @@ async function searchFiles(root, query, limit = 8) {
1414
1615
  scored.sort((a, b) => a.score - b.score);
1415
1616
  return scored.slice(0, limit).map((x) => x.path);
1416
1617
  }
1417
- var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
1418
- async function readClipboardImage() {
1419
- const platform = process.platform;
1420
- if (platform === "win32") return readWindows();
1421
- if (platform === "darwin") return readDarwin();
1422
- if (platform === "linux") return readLinux();
1423
- return null;
1424
- }
1425
- async function readWindows() {
1426
- const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
1427
- const ps = [
1428
- "Add-Type -AssemblyName System.Windows.Forms",
1429
- "Add-Type -AssemblyName System.Drawing",
1430
- "$img = [System.Windows.Forms.Clipboard]::GetImage()",
1431
- 'if ($img -eq $null) { Write-Output "NO_IMAGE"; exit 0 }',
1432
- `$img.Save('${tmp.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png)`,
1433
- 'Write-Output "OK"'
1434
- ].join("; ");
1435
- const out = await runCmd("powershell", ["-NoProfile", "-Command", ps]);
1436
- if (!out || out.trim() === "NO_IMAGE") return null;
1437
- if (!out.includes("OK")) return null;
1438
- return readPngFile(tmp);
1439
- }
1440
- async function readDarwin() {
1441
- const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
1442
- const script = [
1443
- "try",
1444
- ` set the_file to (open for access POSIX file "${tmp}" with write permission)`,
1445
- " write (the clipboard as \xABclass PNGf\xBB) to the_file",
1446
- " close access the_file",
1447
- "on error",
1448
- " try",
1449
- ' close access POSIX file "' + tmp + '"',
1450
- " end try",
1451
- ' return "NO_IMAGE"',
1452
- "end try",
1453
- 'return "OK"'
1454
- ].join("\n");
1455
- const out = await runCmd("osascript", ["-e", script]);
1456
- if (!out || out.trim() !== "OK") return null;
1457
- return readPngFile(tmp);
1458
- }
1459
- async function readLinux() {
1460
- const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
1461
- const tries = [
1462
- ["wl-paste", ["--type", "image/png"]],
1463
- ["xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]]
1464
- ];
1465
- for (const [cmd, args] of tries) {
1466
- const ok = await runCmdToFile(cmd, args, tmp).catch(() => false);
1467
- if (ok) return readPngFile(tmp);
1618
+ async function readGitInfo(cwd) {
1619
+ const [branchRes, numstatRes, statusRes] = await Promise.all([
1620
+ runGit(cwd, ["branch", "--show-current"]),
1621
+ runGit(cwd, ["diff", "HEAD", "--numstat"]),
1622
+ runGit(cwd, ["status", "--porcelain"])
1623
+ ]);
1624
+ if (!branchRes.ok || !numstatRes.ok || !statusRes.ok) return null;
1625
+ const branch = branchRes.stdout.trim();
1626
+ const branchLabel = branch || await detachedShortSha(cwd) || "detached";
1627
+ let added = 0;
1628
+ let deleted = 0;
1629
+ for (const line of numstatRes.stdout.split("\n")) {
1630
+ if (!line) continue;
1631
+ const [a, d] = line.split(" ");
1632
+ if (a && a !== "-") added += Number.parseInt(a, 10) || 0;
1633
+ if (d && d !== "-") deleted += Number.parseInt(d, 10) || 0;
1468
1634
  }
1469
- return null;
1470
- }
1471
- async function readPngFile(p) {
1472
- try {
1473
- const buf = await fs2.readFile(p);
1474
- if (buf.length === 0) {
1475
- await fs2.unlink(p).catch(() => void 0);
1476
- return null;
1477
- }
1478
- if (buf.length > MAX_IMAGE_BYTES) {
1479
- await fs2.unlink(p).catch(() => void 0);
1480
- throw new Error(`Clipboard image exceeds ${MAX_IMAGE_BYTES / 1024 / 1024}MB limit`);
1481
- }
1482
- if (buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71) {
1483
- await fs2.unlink(p).catch(() => void 0);
1484
- return null;
1485
- }
1486
- await fs2.unlink(p).catch(() => void 0);
1487
- return { base64: buf.toString("base64"), mediaType: "image/png", bytes: buf.length };
1488
- } catch (err) {
1489
- if (err.code === "ENOENT") return null;
1490
- throw err;
1635
+ let untracked = 0;
1636
+ for (const line of statusRes.stdout.split("\n")) {
1637
+ if (line.startsWith("?? ")) untracked++;
1491
1638
  }
1639
+ return { branch: branchLabel, added, deleted, untracked };
1492
1640
  }
1493
- function runCmd(cmd, args) {
1494
- return new Promise((resolve) => {
1495
- const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1496
- let out = "";
1497
- child.stdout.on("data", (c) => {
1498
- out += String(c);
1499
- });
1500
- child.on("error", () => resolve(null));
1501
- child.on("exit", (code) => resolve(code === 0 ? out : null));
1502
- });
1641
+ async function detachedShortSha(cwd) {
1642
+ const res = await runGit(cwd, ["rev-parse", "--short", "HEAD"]);
1643
+ return res.ok ? res.stdout.trim() : null;
1503
1644
  }
1504
- function runCmdToFile(cmd, args, outPath) {
1645
+ function runGit(cwd, args) {
1505
1646
  return new Promise((resolve) => {
1506
- const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1507
- const chunks = [];
1508
- child.stdout.on("data", (c) => chunks.push(c));
1509
- child.on("error", () => resolve(false));
1510
- child.on("exit", async (code) => {
1511
- if (code !== 0 || chunks.length === 0) return resolve(false);
1512
- try {
1513
- await fs2.writeFile(outPath, Buffer.concat(chunks));
1514
- resolve(true);
1515
- } catch {
1516
- resolve(false);
1517
- }
1518
- });
1647
+ let stdout = "";
1648
+ try {
1649
+ const child = spawn("git", args, {
1650
+ cwd,
1651
+ // Inherit stderr (silent) we don't care about git's noise.
1652
+ stdio: ["ignore", "pipe", "ignore"],
1653
+ // Don't let a slow git hang the TUI.
1654
+ timeout: 3e3,
1655
+ windowsHide: true
1656
+ });
1657
+ child.stdout.setEncoding("utf8");
1658
+ child.stdout.on("data", (chunk) => {
1659
+ stdout += chunk;
1660
+ });
1661
+ child.on("error", () => resolve({ ok: false, stdout: "" }));
1662
+ child.on("close", (code) => {
1663
+ resolve({ ok: code === 0, stdout });
1664
+ });
1665
+ } catch {
1666
+ resolve({ ok: false, stdout: "" });
1667
+ }
1519
1668
  });
1520
1669
  }
1521
1670
 
@@ -1567,7 +1716,8 @@ function handleQueueCommand(args, deps) {
1567
1716
  if (uniqueValid.length === 0) {
1568
1717
  const parts2 = ["No valid positions to delete."];
1569
1718
  if (invalid.length > 0) parts2.push(`Invalid: ${invalid.join(", ")}.`);
1570
- if (outOfRange.length > 0) parts2.push(`Out of range (queue has ${queue.length}): ${outOfRange.join(", ")}.`);
1719
+ if (outOfRange.length > 0)
1720
+ parts2.push(`Out of range (queue has ${queue.length}): ${outOfRange.join(", ")}.`);
1571
1721
  return parts2.join(" ");
1572
1722
  }
1573
1723
  deps.deleteAt(uniqueValid);
@@ -1596,65 +1746,10 @@ function oneLine(s, max) {
1596
1746
  const collapsed = s.replace(/\s+/g, " ").trim();
1597
1747
  return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 1)}\u2026`;
1598
1748
  }
1599
- async function readGitInfo(cwd) {
1600
- const [branchRes, numstatRes, statusRes] = await Promise.all([
1601
- runGit(cwd, ["branch", "--show-current"]),
1602
- runGit(cwd, ["diff", "HEAD", "--numstat"]),
1603
- runGit(cwd, ["status", "--porcelain"])
1604
- ]);
1605
- if (!branchRes.ok || !numstatRes.ok || !statusRes.ok) return null;
1606
- const branch = branchRes.stdout.trim();
1607
- const branchLabel = branch || await detachedShortSha(cwd) || "detached";
1608
- let added = 0;
1609
- let deleted = 0;
1610
- for (const line of numstatRes.stdout.split("\n")) {
1611
- if (!line) continue;
1612
- const [a, d] = line.split(" ");
1613
- if (a && a !== "-") added += Number.parseInt(a, 10) || 0;
1614
- if (d && d !== "-") deleted += Number.parseInt(d, 10) || 0;
1615
- }
1616
- let untracked = 0;
1617
- for (const line of statusRes.stdout.split("\n")) {
1618
- if (line.startsWith("?? ")) untracked++;
1619
- }
1620
- return { branch: branchLabel, added, deleted, untracked };
1621
- }
1622
- async function detachedShortSha(cwd) {
1623
- const res = await runGit(cwd, ["rev-parse", "--short", "HEAD"]);
1624
- return res.ok ? res.stdout.trim() : null;
1625
- }
1626
- function runGit(cwd, args) {
1627
- return new Promise((resolve) => {
1628
- let stdout = "";
1629
- try {
1630
- const child = spawn("git", args, {
1631
- cwd,
1632
- // Inherit stderr (silent) — we don't care about git's noise.
1633
- stdio: ["ignore", "pipe", "ignore"],
1634
- // Don't let a slow git hang the TUI.
1635
- timeout: 3e3,
1636
- windowsHide: true
1637
- });
1638
- child.stdout.setEncoding("utf8");
1639
- child.stdout.on("data", (chunk) => {
1640
- stdout += chunk;
1641
- });
1642
- child.on("error", () => resolve({ ok: false, stdout: "" }));
1643
- child.on("close", (code) => {
1644
- resolve({ ok: code === 0, stdout });
1645
- });
1646
- } catch {
1647
- resolve({ ok: false, stdout: "" });
1648
- }
1649
- });
1650
- }
1651
1749
  function reducer(state, action) {
1652
1750
  switch (action.type) {
1653
1751
  case "addEntry": {
1654
- const appended = [
1655
- ...state.entries,
1656
- { ...action.entry, id: state.nextId }
1657
- ];
1752
+ const appended = [...state.entries, { ...action.entry, id: state.nextId }];
1658
1753
  return { ...state, entries: appended, nextId: state.nextId + 1 };
1659
1754
  }
1660
1755
  case "setBuffer":
@@ -1748,14 +1843,20 @@ function reducer(state, action) {
1748
1843
  }
1749
1844
  return {
1750
1845
  ...state,
1751
- toolStream: { toolUseId: action.toolUseId, name: action.name, text: action.text }
1846
+ toolStream: {
1847
+ toolUseId: action.toolUseId,
1848
+ name: action.name,
1849
+ text: action.text,
1850
+ startedAt: action.startedAt
1851
+ }
1752
1852
  };
1753
1853
  }
1754
1854
  case "toolStreamClear": {
1755
1855
  if (state.toolStream === null) return state;
1756
1856
  const t = state.toolStream;
1757
1857
  if (action.toolUseId !== void 0 && action.toolUseId !== t.toolUseId) return state;
1758
- if (action.name !== void 0 && action.toolUseId === void 0 && action.name !== t.name) return state;
1858
+ if (action.name !== void 0 && action.toolUseId === void 0 && action.name !== t.name)
1859
+ return state;
1759
1860
  return { ...state, toolStream: null };
1760
1861
  }
1761
1862
  case "enqueue": {
@@ -1986,20 +2087,18 @@ function App({
1986
2087
  const [lastInputTokens, setLastInputTokens] = React.useState(0);
1987
2088
  useEffect(() => {
1988
2089
  const off = events.on("provider.response", (e) => {
1989
- setLastInputTokens(e.usage.input);
2090
+ const total = (e.usage.input ?? 0) + (e.usage.cacheRead ?? 0) + (e.usage.cacheWrite ?? 0);
2091
+ setLastInputTokens(total);
1990
2092
  });
1991
2093
  return () => {
1992
2094
  off();
1993
2095
  };
1994
2096
  }, [events]);
1995
2097
  const maxContext = effectiveMaxContext ?? agent.ctx.provider.capabilities.maxContext;
1996
- const contextWindow = useMemo(
1997
- () => {
1998
- void state.contextChipVersion;
1999
- return lastInputTokens > 0 && maxContext > 0 ? { used: lastInputTokens, max: maxContext } : void 0;
2000
- },
2001
- [lastInputTokens, maxContext, state.contextChipVersion]
2002
- );
2098
+ const contextWindow = useMemo(() => {
2099
+ void state.contextChipVersion;
2100
+ return lastInputTokens > 0 && maxContext > 0 ? { used: lastInputTokens, max: maxContext } : void 0;
2101
+ }, [lastInputTokens, maxContext, state.contextChipVersion]);
2003
2102
  const todos = useMemo(() => {
2004
2103
  const counts = { pending: 0, inProgress: 0, completed: 0 };
2005
2104
  for (const t of agent.ctx.todos) {
@@ -2095,7 +2194,7 @@ function App({
2095
2194
  }
2096
2195
  const absPath = path3.isAbsolute(picked) ? picked : path3.join(projectRoot, picked);
2097
2196
  try {
2098
- const data = await fs2.readFile(absPath, "utf8");
2197
+ const data = await fs.readFile(absPath, "utf8");
2099
2198
  const placeholder = await builder.appendFile({
2100
2199
  kind: "file",
2101
2200
  data,
@@ -2113,7 +2212,10 @@ function App({
2113
2212
  } catch (err) {
2114
2213
  dispatch({
2115
2214
  type: "addEntry",
2116
- entry: { kind: "error", text: `Attach failed: ${err instanceof Error ? err.message : String(err)}` }
2215
+ entry: {
2216
+ kind: "error",
2217
+ text: `Attach failed: ${err instanceof Error ? err.message : String(err)}`
2218
+ }
2117
2219
  });
2118
2220
  dispatch({ type: "pickerClose" });
2119
2221
  }
@@ -2206,7 +2308,8 @@ function App({
2206
2308
  type: "toolStreamAppend",
2207
2309
  toolUseId: e.id,
2208
2310
  name: e.name,
2209
- text: e.event.text
2311
+ text: e.event.text,
2312
+ startedAt: Date.now()
2210
2313
  });
2211
2314
  });
2212
2315
  const offTool = events.on("tool.executed", (e) => {
@@ -2218,11 +2321,23 @@ function App({
2218
2321
  durationMs: e.durationMs,
2219
2322
  ok: e.ok,
2220
2323
  input: e.input,
2221
- output: e.output
2324
+ output: e.output,
2325
+ // Real model-visible sizes — forwarded so the size chip beside
2326
+ // the tool header can show what the model paid for instead of
2327
+ // the misleading preview-byte count we used to surface.
2328
+ outputBytes: e.outputBytes,
2329
+ outputTokens: e.outputTokens,
2330
+ outputLines: e.outputLines
2222
2331
  }
2223
2332
  });
2224
2333
  dispatch({ type: "toolEnded", name: e.name });
2225
2334
  dispatch({ type: "toolStreamClear", name: e.name });
2335
+ if (e.ok && e.name === "todo") {
2336
+ dispatch({
2337
+ type: "addEntry",
2338
+ entry: { kind: "info", text: formatTodosList(agent.ctx.todos) }
2339
+ });
2340
+ }
2226
2341
  });
2227
2342
  const offRetry = events.on("provider.retry", (e) => {
2228
2343
  const secs = (e.delayMs / 1e3).toFixed(e.delayMs >= 1e3 ? 1 : 2);
@@ -2237,6 +2352,19 @@ function App({
2237
2352
  entry: { kind: "error", text: e.description }
2238
2353
  });
2239
2354
  });
2355
+ const offProvResp = events.on("provider.response", () => {
2356
+ const text = streamingTextRef.current;
2357
+ streamingTextRef.current = "";
2358
+ pendingDeltaRef.current = "";
2359
+ if (flushTimerRef.current) {
2360
+ clearTimeout(flushTimerRef.current);
2361
+ flushTimerRef.current = null;
2362
+ }
2363
+ dispatch({ type: "streamReset" });
2364
+ if (text.trim()) {
2365
+ dispatch({ type: "addEntry", entry: { kind: "assistant", text } });
2366
+ }
2367
+ });
2240
2368
  const offConfirmNeeded = events.on("tool.confirm_needed", (e) => {
2241
2369
  dispatch({
2242
2370
  type: "addEntry",
@@ -2265,6 +2393,7 @@ function App({
2265
2393
  offTool();
2266
2394
  offRetry();
2267
2395
  offProvErr();
2396
+ offProvResp();
2268
2397
  offConfirmNeeded();
2269
2398
  if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
2270
2399
  };
@@ -2439,7 +2568,8 @@ function App({
2439
2568
  dispatch({ type: "setBuffer", buffer, cursor: target });
2440
2569
  return;
2441
2570
  }
2442
- if (state.cursor > 0) dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor - 1 });
2571
+ if (state.cursor > 0)
2572
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor - 1 });
2443
2573
  return;
2444
2574
  }
2445
2575
  if (key.rightArrow) {
@@ -2452,7 +2582,8 @@ function App({
2452
2582
  dispatch({ type: "setBuffer", buffer, cursor: target });
2453
2583
  return;
2454
2584
  }
2455
- if (state.cursor < state.buffer.length) dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor + 1 });
2585
+ if (state.cursor < state.buffer.length)
2586
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor + 1 });
2456
2587
  return;
2457
2588
  }
2458
2589
  if (key.upArrow) {
@@ -2520,10 +2651,9 @@ function App({
2520
2651
  const before = tokenCounter?.total();
2521
2652
  const costBefore = tokenCounter?.estimateCost().total ?? 0;
2522
2653
  const result = await agent.run(blocks, { signal: ctrl.signal });
2523
- const streamed = streamingTextRef.current;
2524
- const text = result.status === "done" && result.finalText ? result.finalText : streamed;
2525
- if (text?.trim()) {
2526
- dispatch({ type: "addEntry", entry: { kind: "assistant", text } });
2654
+ const lingering = streamingTextRef.current;
2655
+ if (lingering.trim()) {
2656
+ dispatch({ type: "addEntry", entry: { kind: "assistant", text: lingering } });
2527
2657
  }
2528
2658
  streamingTextRef.current = "";
2529
2659
  pendingDeltaRef.current = "";
@@ -2536,10 +2666,10 @@ function App({
2536
2666
  dispatch({ type: "addEntry", entry: { kind: "warn", text: "Aborted." } });
2537
2667
  } else if (result.status === "failed") {
2538
2668
  const err = result.error;
2539
- const text2 = err ? `Failed [${err.severity}${err.recoverable ? ", recoverable" : ""}]: ${err.describe()}` : "Failed.";
2669
+ const text = err ? `Failed [${err.severity}${err.recoverable ? ", recoverable" : ""}]: ${err.describe()}` : "Failed.";
2540
2670
  dispatch({
2541
2671
  type: "addEntry",
2542
- entry: { kind: "error", text: text2 }
2672
+ entry: { kind: "error", text }
2543
2673
  });
2544
2674
  } else if (result.status === "max_iterations") {
2545
2675
  dispatch({
@@ -2554,7 +2684,7 @@ function App({
2554
2684
  type: "addEntry",
2555
2685
  entry: {
2556
2686
  kind: "turn-summary",
2557
- 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]`
2687
+ text: `[in: ${fmtTok3(after.input - before.input)} out: ${fmtTok3(after.output - before.output)} iters: ${result.iterations} cost: ${(costAfter - costBefore).toFixed(4)} ${((Date.now() - startedAt) / 1e3).toFixed(1)}s]`
2558
2688
  }
2559
2689
  });
2560
2690
  }
@@ -2633,7 +2763,14 @@ function App({
2633
2763
  return "";
2634
2764
  }, [state.buffer, state.status, state.picker.open]);
2635
2765
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2636
- /* @__PURE__ */ jsx(History, { entries: state.entries, streamingText: state.streamingText, toolStream: state.toolStream }),
2766
+ /* @__PURE__ */ jsx(
2767
+ History,
2768
+ {
2769
+ entries: state.entries,
2770
+ streamingText: state.streamingText,
2771
+ toolStream: state.toolStream
2772
+ }
2773
+ ),
2637
2774
  /* @__PURE__ */ jsx(
2638
2775
  Input,
2639
2776
  {
@@ -2725,7 +2862,7 @@ function detectAtToken(buffer, cursor) {
2725
2862
  }
2726
2863
  return null;
2727
2864
  }
2728
- function fmtTok2(n) {
2865
+ function fmtTok3(n) {
2729
2866
  if (n < 1e3) return String(n);
2730
2867
  if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
2731
2868
  return `${(n / 1e6).toFixed(1)}M`;