@wrongstack/tui 0.3.3 → 0.3.7

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,5 +1,5 @@
1
1
  import { render, useApp, Box, useStdout, Static, Text, useInput, useStdin } from 'ink';
2
- import React, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
2
+ import React4, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
3
3
  import * as fs2 from 'fs/promises';
4
4
  import * as path2 from 'path';
5
5
  import { InputBuilder, formatTodosList } from '@wrongstack/core';
@@ -39,14 +39,19 @@ function ConfirmPrompt({
39
39
  suggestedPattern,
40
40
  onDecision
41
41
  }) {
42
- useInput((_, key) => {
43
- if (key.return) {
42
+ React4.useEffect(() => {
43
+ process.stdout.write("\x07");
44
+ }, []);
45
+ useInput((input2, key) => {
46
+ if (!input2 || input2 === "\r" || input2 === "\n") return;
47
+ const ch = input2.toLowerCase();
48
+ if (ch === "y") {
44
49
  onDecision("yes");
45
- } else if (key.escape) {
50
+ } else if (ch === "n") {
46
51
  onDecision("no");
47
- } else if (key.ctrl && _.toLowerCase() === "a") {
52
+ } else if (ch === "a") {
48
53
  onDecision("always");
49
- } else if (key.ctrl && _.toLowerCase() === "d") {
54
+ } else if (ch === "d") {
50
55
  onDecision("deny");
51
56
  }
52
57
  });
@@ -58,34 +63,32 @@ function ConfirmPrompt({
58
63
  Box,
59
64
  {
60
65
  flexDirection: "column",
61
- borderStyle: "single",
62
- borderTop: false,
63
- borderLeft: false,
64
- borderRight: false,
65
- borderBottom: false,
66
+ borderStyle: "round",
67
+ borderColor: "yellow",
66
68
  paddingX: 1,
69
+ marginY: 1,
67
70
  children: [
68
71
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
69
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
72
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 APPROVAL REQUIRED" }),
70
73
  /* @__PURE__ */ jsx(Text, { children: " " }),
71
- /* @__PURE__ */ jsx(Text, { bold: true, children: toolName })
74
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: toolName })
72
75
  ] }),
73
76
  inputSummary ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: inputSummary }) : null,
74
77
  showDiff && diff ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: renderDiff(diff) }) : null,
75
78
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
76
79
  /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
77
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
78
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
79
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
80
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
81
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
80
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[y]" }),
81
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "es " }),
82
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[n]" }),
83
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "o " }),
84
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[a]" }),
82
85
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
83
- " always (",
86
+ "lways (",
84
87
  suggestedPattern,
85
88
  ") "
86
89
  ] }),
87
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
88
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
90
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[d]" }),
91
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "eny" })
89
92
  ] }) })
90
93
  ]
91
94
  }
@@ -256,81 +259,6 @@ function FleetPanel({ entries, totalCost, roster }) {
256
259
  }
257
260
  );
258
261
  }
259
- function fmtElapsed(ms) {
260
- if (ms < 1e3) return `${ms}ms`;
261
- if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
262
- const m = Math.floor(ms / 6e4);
263
- const s = Math.floor(ms % 6e4 / 1e3);
264
- return `${m}m${s.toString().padStart(2, "0")}s`;
265
- }
266
- function fmtBytes2(n) {
267
- if (n < 1024) return `${n}B`;
268
- return `${(n / 1024).toFixed(1)}KB`;
269
- }
270
- function fmtRecentTool2(tool) {
271
- const status = tool.ok === false ? "fail" : "ok";
272
- const name = tool.name.length > 18 ? `${tool.name.slice(0, 17)}...` : tool.name;
273
- const parts = [status, name];
274
- if (typeof tool.durationMs === "number") parts.push(fmtElapsed(tool.durationMs));
275
- if (typeof tool.outputBytes === "number" && tool.outputBytes > 0) parts.push(fmtBytes2(tool.outputBytes));
276
- if (typeof tool.outputLines === "number" && tool.outputLines > 0) parts.push(`${tool.outputLines}L`);
277
- return parts.join(" ");
278
- }
279
- function fmtRecentMessage2(message) {
280
- const text = message.text.replace(/\s+/g, " ");
281
- return text.length > 48 ? `${text.slice(0, 47)}...` : text;
282
- }
283
- function LiveActivityStrip({
284
- entries,
285
- nowTick,
286
- maxRows = 4
287
- }) {
288
- const running = Object.values(entries).filter((e) => e.status === "running").sort((a, b) => a.startedAt - b.startedAt).slice(0, maxRows);
289
- if (running.length === 0) return null;
290
- const now = Date.now();
291
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
292
- running.map((e) => {
293
- const toolElapsed = e.currentTool ? now - e.currentTool.startedAt : 0;
294
- const taskElapsed = now - e.startedAt;
295
- const toolSeg = e.currentTool ? `\u2192 ${e.currentTool.name} (${fmtElapsed(toolElapsed)})` : "idle between tools";
296
- const recentTools = (e.recentTools ?? []).slice(-2).map(fmtRecentTool2).join(" | ");
297
- const messageText = e.streamingText.trim() || (e.recentMessages ?? []).slice(-1).map(fmtRecentMessage2).join("");
298
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
299
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CF" }),
300
- /* @__PURE__ */ jsx(Text, { children: e.name.slice(0, 14).padEnd(14) }),
301
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
302
- /* @__PURE__ */ jsx(Text, { color: e.currentTool ? "green" : "yellow", children: toolSeg }),
303
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
304
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
305
- e.iterations,
306
- "it ",
307
- e.toolCalls,
308
- "tc \xB7 ",
309
- fmtElapsed(taskElapsed)
310
- ] }),
311
- recentTools ? /* @__PURE__ */ jsxs(Fragment, { children: [
312
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
313
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
314
- "last: ",
315
- recentTools
316
- ] })
317
- ] }) : null,
318
- messageText ? /* @__PURE__ */ jsxs(Fragment, { children: [
319
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
320
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
321
- "msg: ",
322
- fmtRecentMessage2({ text: messageText})
323
- ] })
324
- ] }) : null
325
- ] }, e.id);
326
- }),
327
- Object.values(entries).filter((e) => e.status === "running").length > maxRows ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
328
- "\u2026+",
329
- Object.values(entries).filter((e) => e.status === "running").length - maxRows,
330
- " more"
331
- ] }) }) : null
332
- ] });
333
- }
334
262
 
335
263
  // src/markdown-table.ts
336
264
  function renderMarkdownTables(text, maxWidth) {
@@ -641,7 +569,7 @@ function Entry({
641
569
  parts.push(`${entry.outputLines} L`);
642
570
  }
643
571
  if (entry.outputBytes && entry.outputBytes > 0) {
644
- parts.push(fmtBytes3(entry.outputBytes));
572
+ parts.push(fmtBytes2(entry.outputBytes));
645
573
  }
646
574
  if (entry.outputTokens && entry.outputTokens > 0) {
647
575
  parts.push(`\u2248${fmtTok(entry.outputTokens)} tok`);
@@ -686,41 +614,13 @@ function Entry({
686
614
  case "turn-summary":
687
615
  return /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.text });
688
616
  case "confirm":
689
- return /* @__PURE__ */ jsxs(
690
- Box,
691
- {
692
- flexDirection: "column",
693
- borderStyle: "single",
694
- borderTop: false,
695
- borderLeft: false,
696
- borderRight: false,
697
- borderBottom: false,
698
- paddingX: 1,
699
- children: [
700
- /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
701
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
702
- /* @__PURE__ */ jsx(Text, { children: " " }),
703
- /* @__PURE__ */ jsx(Text, { bold: true, children: entry.toolName })
704
- ] }),
705
- 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,
706
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
707
- /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
708
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
709
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
710
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
711
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
712
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
713
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
714
- " always (",
715
- entry.suggestedPattern,
716
- ") "
717
- ] }),
718
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
719
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
720
- ] }) })
721
- ]
722
- }
723
- );
617
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1, children: [
618
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
619
+ "\u26A0 Confirm: ",
620
+ entry.toolName
621
+ ] }),
622
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Waiting for y / n / a / d..." })
623
+ ] });
724
624
  case "banner":
725
625
  return /* @__PURE__ */ jsx(Banner, { entry });
726
626
  case "subagent": {
@@ -925,7 +825,7 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
925
825
  const bytes = numOf(o["bytes_written"]) ?? numOf(o["bytes"]);
926
826
  const created = o["created"] === true;
927
827
  const tag = created ? "created" : "updated";
928
- if (bytes !== void 0) return [`${tag} \xB7 ${fmtBytes3(bytes)}`];
828
+ if (bytes !== void 0) return [`${tag} \xB7 ${fmtBytes2(bytes)}`];
929
829
  return [tag];
930
830
  }
931
831
  }
@@ -990,17 +890,17 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
990
890
  if (json && typeof json === "object") {
991
891
  const o = json;
992
892
  const bytes = numOf(o["bytes"]);
993
- if (bytes !== void 0) return [`${fmtBytes3(bytes)} read`];
893
+ if (bytes !== void 0) return [`${fmtBytes2(bytes)} read`];
994
894
  }
995
895
  const range = scanNumberedRange(text);
996
896
  if (range.count > 0 && range.first !== void 0 && range.last !== void 0) {
997
897
  if (range.first === range.last) {
998
- return [`L${range.first} \xB7 ${fmtBytes3(text.length)}`];
898
+ return [`L${range.first} \xB7 ${fmtBytes2(text.length)}`];
999
899
  }
1000
900
  const contiguous = range.count === range.last - range.first + 1;
1001
901
  const head = `L${range.first}\u2013${range.last}`;
1002
902
  const tail = contiguous ? `${range.count} line${range.count === 1 ? "" : "s"}` : `${range.count} lines (gaps)`;
1003
- return [`${head} \xB7 ${tail} \xB7 ${fmtBytes3(text.length)}`];
903
+ return [`${head} \xB7 ${tail} \xB7 ${fmtBytes2(text.length)}`];
1004
904
  }
1005
905
  }
1006
906
  if (toolName === "grep" || toolName === "glob") {
@@ -1058,7 +958,7 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
1058
958
  const head = [];
1059
959
  if (status !== void 0) head.push(`HTTP ${status}`);
1060
960
  if (ct) head.push(ct.split(";")[0] ?? ct);
1061
- if (content) head.push(fmtBytes3(Buffer.byteLength(content, "utf8")));
961
+ if (content) head.push(fmtBytes2(Buffer.byteLength(content, "utf8")));
1062
962
  const lines = [];
1063
963
  if (head.length > 0) lines.push(head.join(" \xB7 "));
1064
964
  if (url && status !== void 0 && (status < 200 || status >= 400)) {
@@ -1434,7 +1334,7 @@ function countLines(text) {
1434
1334
  if (!text) return 0;
1435
1335
  return text.replace(/\n$/, "").split("\n").length;
1436
1336
  }
1437
- function fmtBytes3(n) {
1337
+ function fmtBytes2(n) {
1438
1338
  if (n < 1024) return `${n}B`;
1439
1339
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
1440
1340
  return `${(n / (1024 * 1024)).toFixed(1)}MB`;
@@ -1515,6 +1415,81 @@ function Input({
1515
1415
  hint ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint }) : null
1516
1416
  ] });
1517
1417
  }
1418
+ function fmtElapsed(ms) {
1419
+ if (ms < 1e3) return `${ms}ms`;
1420
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
1421
+ const m = Math.floor(ms / 6e4);
1422
+ const s = Math.floor(ms % 6e4 / 1e3);
1423
+ return `${m}m${s.toString().padStart(2, "0")}s`;
1424
+ }
1425
+ function fmtBytes3(n) {
1426
+ if (n < 1024) return `${n}B`;
1427
+ return `${(n / 1024).toFixed(1)}KB`;
1428
+ }
1429
+ function fmtRecentTool2(tool) {
1430
+ const status = tool.ok === false ? "fail" : "ok";
1431
+ const name = tool.name.length > 18 ? `${tool.name.slice(0, 17)}...` : tool.name;
1432
+ const parts = [status, name];
1433
+ if (typeof tool.durationMs === "number") parts.push(fmtElapsed(tool.durationMs));
1434
+ if (typeof tool.outputBytes === "number" && tool.outputBytes > 0) parts.push(fmtBytes3(tool.outputBytes));
1435
+ if (typeof tool.outputLines === "number" && tool.outputLines > 0) parts.push(`${tool.outputLines}L`);
1436
+ return parts.join(" ");
1437
+ }
1438
+ function fmtRecentMessage2(message) {
1439
+ const text = message.text.replace(/\s+/g, " ");
1440
+ return text.length > 48 ? `${text.slice(0, 47)}...` : text;
1441
+ }
1442
+ function LiveActivityStrip({
1443
+ entries,
1444
+ nowTick,
1445
+ maxRows = 4
1446
+ }) {
1447
+ const running = Object.values(entries).filter((e) => e.status === "running").sort((a, b) => a.startedAt - b.startedAt).slice(0, maxRows);
1448
+ if (running.length === 0) return null;
1449
+ const now = Date.now();
1450
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1451
+ running.map((e) => {
1452
+ const toolElapsed = e.currentTool ? now - e.currentTool.startedAt : 0;
1453
+ const taskElapsed = now - e.startedAt;
1454
+ const toolSeg = e.currentTool ? `\u2192 ${e.currentTool.name} (${fmtElapsed(toolElapsed)})` : "idle between tools";
1455
+ const recentTools = (e.recentTools ?? []).slice(-2).map(fmtRecentTool2).join(" | ");
1456
+ const messageText = e.streamingText.trim() || (e.recentMessages ?? []).slice(-1).map(fmtRecentMessage2).join("");
1457
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
1458
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CF" }),
1459
+ /* @__PURE__ */ jsx(Text, { children: e.name.slice(0, 14).padEnd(14) }),
1460
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
1461
+ /* @__PURE__ */ jsx(Text, { color: e.currentTool ? "green" : "yellow", children: toolSeg }),
1462
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
1463
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1464
+ e.iterations,
1465
+ "it ",
1466
+ e.toolCalls,
1467
+ "tc \xB7 ",
1468
+ fmtElapsed(taskElapsed)
1469
+ ] }),
1470
+ recentTools ? /* @__PURE__ */ jsxs(Fragment, { children: [
1471
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
1472
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1473
+ "last: ",
1474
+ recentTools
1475
+ ] })
1476
+ ] }) : null,
1477
+ messageText ? /* @__PURE__ */ jsxs(Fragment, { children: [
1478
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
1479
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1480
+ "msg: ",
1481
+ fmtRecentMessage2({ text: messageText})
1482
+ ] })
1483
+ ] }) : null
1484
+ ] }, e.id);
1485
+ }),
1486
+ Object.values(entries).filter((e) => e.status === "running").length > maxRows ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1487
+ "\u2026+",
1488
+ Object.values(entries).filter((e) => e.status === "running").length - maxRows,
1489
+ " more"
1490
+ ] }) }) : null
1491
+ ] });
1492
+ }
1518
1493
  function ModelPicker({
1519
1494
  step,
1520
1495
  providerOptions,
@@ -1526,7 +1501,7 @@ function ModelPicker({
1526
1501
  if (step === "provider") {
1527
1502
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1528
1503
  /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u2501\u2501 Switch model \u2014 Step 1/2: Pick provider \u2501\u2501" }),
1529
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc cancel" }),
1504
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc cancel \xB7 Ctrl+C exit" }),
1530
1505
  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: [
1531
1506
  i === selected ? "\u203A " : " ",
1532
1507
  /* @__PURE__ */ jsx(Text, { bold: true, children: p.id.padEnd(28) }),
@@ -1551,7 +1526,7 @@ function ModelPicker({
1551
1526
  pickedProviderId,
1552
1527
  ") \u2501\u2501"
1553
1528
  ] }),
1554
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc back \xB7 Ctrl-C cancel" }),
1529
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc back \xB7 Ctrl+C exit" }),
1555
1530
  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: [
1556
1531
  i === selected ? "\u203A " : " ",
1557
1532
  id
@@ -2050,6 +2025,11 @@ function oneLine(s, max) {
2050
2025
  const collapsed = s.replace(/\s+/g, " ").trim();
2051
2026
  return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 1)}\u2026`;
2052
2027
  }
2028
+ function selectedSlashCommandLine(picker) {
2029
+ if (!picker.open || picker.matches.length === 0) return null;
2030
+ const picked = picker.matches[picker.selected];
2031
+ return picked ? `/${picked.name}` : null;
2032
+ }
2053
2033
  function reducer(state, action) {
2054
2034
  switch (action.type) {
2055
2035
  case "addEntry": {
@@ -2066,6 +2046,7 @@ function reducer(state, action) {
2066
2046
  buffer: "",
2067
2047
  cursor: 0,
2068
2048
  placeholders: [],
2049
+ historyIndex: 0,
2069
2050
  picker: { open: false, query: "", matches: [], selected: 0 },
2070
2051
  slashPicker: { open: false, query: "", matches: [], selected: 0 }
2071
2052
  };
@@ -2285,9 +2266,9 @@ function reducer(state, action) {
2285
2266
  modelPicker: { ...state.modelPicker, hint: action.text }
2286
2267
  };
2287
2268
  case "confirmOpen":
2288
- return { ...state, confirm: action.info };
2269
+ return { ...state, confirmQueue: [...state.confirmQueue, action.info] };
2289
2270
  case "confirmClose":
2290
- return { ...state, confirm: null };
2271
+ return { ...state, confirmQueue: state.confirmQueue.slice(1) };
2291
2272
  case "resetContextChip":
2292
2273
  return { ...state, contextChipVersion: state.contextChipVersion + 1 };
2293
2274
  // --- Fleet ---
@@ -2626,7 +2607,7 @@ function App({
2626
2607
  modelOptions: [],
2627
2608
  selected: 0
2628
2609
  },
2629
- confirm: null,
2610
+ confirmQueue: [],
2630
2611
  contextChipVersion: 0,
2631
2612
  fleet: {},
2632
2613
  fleetCost: 0,
@@ -2640,7 +2621,7 @@ function App({
2640
2621
  const inputGateRef = useRef(false);
2641
2622
  const lastEnterAtRef = useRef(0);
2642
2623
  const projectRoot = agent.ctx.projectRoot;
2643
- const projectName = React.useMemo(() => {
2624
+ const projectName = React4.useMemo(() => {
2644
2625
  const base = path2.basename(projectRoot);
2645
2626
  return base && base !== path2.sep ? base : void 0;
2646
2627
  }, [projectRoot]);
@@ -2649,14 +2630,24 @@ function App({
2649
2630
  const flushTimerRef = useRef(null);
2650
2631
  const stateRef = useRef(state);
2651
2632
  stateRef.current = state;
2633
+ const draftRef = useRef({ buffer: state.buffer, cursor: state.cursor });
2634
+ draftRef.current = { buffer: state.buffer, cursor: state.cursor };
2635
+ const setDraft = (buffer, cursor) => {
2636
+ draftRef.current = { buffer, cursor };
2637
+ dispatch({ type: "setBuffer", buffer, cursor });
2638
+ };
2639
+ const clearDraft = () => {
2640
+ draftRef.current = { buffer: "", cursor: 0 };
2641
+ dispatch({ type: "clearInput" });
2642
+ };
2652
2643
  const startedAtRef = useRef(Date.now());
2653
- const [nowTick, setNowTick] = React.useState(Date.now());
2644
+ const [nowTick, setNowTick] = React4.useState(Date.now());
2654
2645
  useEffect(() => {
2655
2646
  const t = setInterval(() => setNowTick(Date.now()), 1e3);
2656
2647
  return () => clearInterval(t);
2657
2648
  }, []);
2658
2649
  const elapsedMs = nowTick - startedAtRef.current;
2659
- const [gitInfo, setGitInfo] = React.useState(null);
2650
+ const [gitInfo, setGitInfo] = React4.useState(null);
2660
2651
  useEffect(() => {
2661
2652
  let cancelled = false;
2662
2653
  const refresh = () => {
@@ -2671,7 +2662,7 @@ function App({
2671
2662
  clearInterval(t);
2672
2663
  };
2673
2664
  }, [agent.ctx.cwd]);
2674
- const [lastInputTokens, setLastInputTokens] = React.useState(0);
2665
+ const [lastInputTokens, setLastInputTokens] = React4.useState(0);
2675
2666
  useEffect(() => {
2676
2667
  const off = events.on("provider.response", (e) => {
2677
2668
  const total = (e.usage.input ?? 0) + (e.usage.cacheRead ?? 0) + (e.usage.cacheWrite ?? 0);
@@ -2782,7 +2773,7 @@ function App({
2782
2773
  const prevAnyOverlayOpen = useRef(false);
2783
2774
  const prevEntriesCount = useRef(0);
2784
2775
  useEffect(() => {
2785
- const anyOpenNow = state.picker.open || state.slashPicker.open || state.modelPicker.open || !!state.confirm;
2776
+ const anyOpenNow = state.picker.open || state.slashPicker.open || state.modelPicker.open || state.confirmQueue.length > 0;
2786
2777
  const overlayClosed = prevAnyOverlayOpen.current && !anyOpenNow;
2787
2778
  const newEntryCommitted = state.entries.length > prevEntriesCount.current;
2788
2779
  prevAnyOverlayOpen.current = anyOpenNow;
@@ -2797,7 +2788,7 @@ function App({
2797
2788
  state.picker.open,
2798
2789
  state.slashPicker.open,
2799
2790
  state.modelPicker.open,
2800
- state.confirm,
2791
+ state.confirmQueue.length,
2801
2792
  state.entries.length
2802
2793
  ]);
2803
2794
  useEffect(() => {
@@ -2838,7 +2829,7 @@ function App({
2838
2829
  }).slice(0, 12).map(({ cmd, owner }) => ({
2839
2830
  name: cmd.name,
2840
2831
  description: cmd.description,
2841
- argsHint: void 0,
2832
+ argsHint: cmd.argsHint,
2842
2833
  isBuiltin: owner === "core"
2843
2834
  }));
2844
2835
  if (!state.slashPicker.open) {
@@ -2879,7 +2870,8 @@ function App({
2879
2870
  if (!picked) return;
2880
2871
  const builder = builderRef.current;
2881
2872
  if (!builder) return;
2882
- const tok = detectAtToken(state.buffer, state.cursor);
2873
+ const draft = draftRef.current;
2874
+ const tok = detectAtToken(draft.buffer, draft.cursor);
2883
2875
  if (!tok) {
2884
2876
  dispatch({ type: "pickerClose" });
2885
2877
  return;
@@ -2892,14 +2884,10 @@ function App({
2892
2884
  data,
2893
2885
  meta: { filename: picked, label: picked }
2894
2886
  });
2895
- const before = state.buffer.slice(0, tok.start);
2896
- const after = state.buffer.slice(tok.end);
2887
+ const before = draft.buffer.slice(0, tok.start);
2888
+ const after = draft.buffer.slice(tok.end);
2897
2889
  const next = `${before}${placeholder}${after}`;
2898
- dispatch({
2899
- type: "setBuffer",
2900
- buffer: next,
2901
- cursor: tok.start + placeholder.length
2902
- });
2890
+ setDraft(next, tok.start + placeholder.length);
2903
2891
  dispatch({ type: "pickerClose" });
2904
2892
  } catch (err) {
2905
2893
  dispatch({
@@ -2918,7 +2906,7 @@ function App({
2918
2906
  const picked = matches[selected];
2919
2907
  if (!picked) return;
2920
2908
  const cmd = picked.argsHint !== void 0 ? `/${picked.name} ` : `/${picked.name}`;
2921
- dispatch({ type: "setBuffer", buffer: cmd, cursor: cmd.length });
2909
+ setDraft(cmd, cmd.length);
2922
2910
  dispatch({ type: "slashPickerClose" });
2923
2911
  };
2924
2912
  useEffect(() => {
@@ -3180,15 +3168,6 @@ function App({
3180
3168
  }
3181
3169
  });
3182
3170
  const offConfirmNeeded = events.on("tool.confirm_needed", (e) => {
3183
- dispatch({
3184
- type: "addEntry",
3185
- entry: {
3186
- kind: "confirm",
3187
- toolName: e.tool.name,
3188
- input: e.input,
3189
- suggestedPattern: e.suggestedPattern
3190
- }
3191
- });
3192
3171
  dispatch({
3193
3172
  type: "confirmOpen",
3194
3173
  info: {
@@ -3200,6 +3179,17 @@ function App({
3200
3179
  }
3201
3180
  });
3202
3181
  });
3182
+ const offTrustPersisted = events.on("trust.persisted", (e) => {
3183
+ const icon = e.decision === "always" ? "\u2713" : "\u2717";
3184
+ const label = e.decision === "always" ? "always allowed" : "denied";
3185
+ dispatch({
3186
+ type: "addEntry",
3187
+ entry: {
3188
+ kind: "info",
3189
+ text: `${icon} ${label}: ${e.tool}(${e.pattern})`
3190
+ }
3191
+ });
3192
+ });
3203
3193
  return () => {
3204
3194
  offDelta();
3205
3195
  offToolStart();
@@ -3209,6 +3199,7 @@ function App({
3209
3199
  offProvErr();
3210
3200
  offProvResp();
3211
3201
  offConfirmNeeded();
3202
+ offTrustPersisted();
3212
3203
  if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
3213
3204
  };
3214
3205
  }, [events, agent.ctx.todos]);
@@ -3461,25 +3452,35 @@ function App({
3461
3452
  }, [director]);
3462
3453
  useEffect(() => {
3463
3454
  const onSigint = () => {
3464
- if (state.interrupts >= 1) {
3465
- if (state.interrupts >= 2) {
3455
+ const current = stateRef.current;
3456
+ if (current.interrupts >= 1) {
3457
+ if (current.interrupts >= 2) {
3466
3458
  process.exit(130);
3467
3459
  }
3468
3460
  try {
3469
- exit();
3470
- onExit(130);
3461
+ process.exit(130);
3471
3462
  } catch {
3472
3463
  }
3473
- setTimeout(() => {
3474
- try {
3475
- process.exit(130);
3476
- } catch {
3477
- }
3478
- }, 500).unref?.();
3479
3464
  dispatch({ type: "interrupt" });
3480
3465
  return;
3481
3466
  }
3482
3467
  dispatch({ type: "interrupt" });
3468
+ if (current.modelPicker.open) {
3469
+ dispatch({ type: "modelPickerClose" });
3470
+ dispatch({
3471
+ type: "addEntry",
3472
+ entry: { kind: "warn", text: "Model picker cancelled." }
3473
+ });
3474
+ return;
3475
+ }
3476
+ if (current.slashPicker.open) {
3477
+ dispatch({ type: "slashPickerClose" });
3478
+ dispatch({
3479
+ type: "addEntry",
3480
+ entry: { kind: "warn", text: "Cancelled." }
3481
+ });
3482
+ return;
3483
+ }
3483
3484
  if (activeCtrlRef.current) {
3484
3485
  activeCtrlRef.current.abort();
3485
3486
  dispatch({ type: "status", status: "aborting" });
@@ -3520,9 +3521,10 @@ function App({
3520
3521
  return () => {
3521
3522
  process.off("SIGINT", onSigint);
3522
3523
  };
3523
- }, [state.interrupts, exit, onExit, director]);
3524
+ }, [exit, onExit, director]);
3524
3525
  const handleKey = async (input, key) => {
3525
- if (state.status === "aborting") return;
3526
+ if (state.status === "aborting" && state.interrupts === 0) return;
3527
+ if (state.confirmQueue.length > 0) return;
3526
3528
  if (inputGateRef.current) return;
3527
3529
  const isEnter = key.return || input === "\r" || input === "\n";
3528
3530
  if (state.modelPicker.open) {
@@ -3591,15 +3593,23 @@ function App({
3591
3593
  return;
3592
3594
  }
3593
3595
  if (isEnter) {
3596
+ const now = Date.now();
3597
+ if (now - lastEnterAtRef.current < 50) return;
3598
+ lastEnterAtRef.current = now;
3594
3599
  inputGateRef.current = true;
3595
- acceptSlashPickerSelection();
3600
+ const line = selectedSlashCommandLine(state.slashPicker);
3601
+ if (line) {
3602
+ void submit(line);
3603
+ } else {
3604
+ acceptSlashPickerSelection();
3605
+ }
3596
3606
  inputGateRef.current = false;
3597
3607
  return;
3598
3608
  }
3599
3609
  if (key.tab && state.slashPicker.matches.length > 0) {
3600
3610
  const sel = state.slashPicker.matches[state.slashPicker.selected];
3601
3611
  if (sel) {
3602
- dispatch({ type: "setBuffer", buffer: `/${sel.name} `, cursor: sel.name.length + 2 });
3612
+ setDraft(`/${sel.name} `, sel.name.length + 2);
3603
3613
  dispatch({ type: "slashPickerClose" });
3604
3614
  }
3605
3615
  return;
@@ -3628,7 +3638,7 @@ function App({
3628
3638
  return;
3629
3639
  }
3630
3640
  }
3631
- if (key.escape && state.status !== "idle" && !state.confirm) {
3641
+ if (key.escape && state.status !== "idle" && state.confirmQueue.length === 0) {
3632
3642
  const runningTools = Array.from(state.runningTools.values()).map((t) => t.name);
3633
3643
  const subagents = Object.values(state.fleet).filter((e) => e.status === "running").map((e) => ({
3634
3644
  label: e.name,
@@ -3675,64 +3685,60 @@ function App({
3675
3685
  void submit();
3676
3686
  return;
3677
3687
  }
3688
+ const { buffer, cursor } = draftRef.current;
3678
3689
  if (key.backspace || key.delete) {
3679
3690
  if (key.ctrl) {
3680
- const { cursor, buffer } = state;
3681
3691
  if (key.backspace) {
3682
3692
  if (cursor === 0) return;
3683
3693
  const beforeCursor = buffer.slice(0, cursor);
3684
3694
  const lastWordStart = beforeCursor.lastIndexOf(" ") + 1;
3685
3695
  const next3 = buffer.slice(0, lastWordStart) + buffer.slice(cursor);
3686
- dispatch({ type: "setBuffer", buffer: next3, cursor: lastWordStart });
3696
+ setDraft(next3, lastWordStart);
3687
3697
  } else {
3688
3698
  if (cursor >= buffer.length) return;
3689
3699
  const afterCursor = buffer.slice(cursor);
3690
3700
  const nextWordStart = afterCursor.indexOf(" ");
3691
3701
  const end = nextWordStart === -1 ? buffer.length : cursor + nextWordStart + 1;
3692
3702
  const next3 = buffer.slice(0, cursor) + buffer.slice(end);
3693
- dispatch({ type: "setBuffer", buffer: next3, cursor });
3703
+ setDraft(next3, cursor);
3694
3704
  }
3695
3705
  return;
3696
3706
  }
3697
- if (state.cursor === 0) return;
3698
- const next2 = state.buffer.slice(0, state.cursor - 1) + state.buffer.slice(state.cursor);
3699
- dispatch({ type: "setBuffer", buffer: next2, cursor: state.cursor - 1 });
3707
+ if (cursor === 0) return;
3708
+ const next2 = buffer.slice(0, cursor - 1) + buffer.slice(cursor);
3709
+ setDraft(next2, cursor - 1);
3700
3710
  return;
3701
3711
  }
3702
3712
  if (key.leftArrow) {
3703
3713
  if (key.ctrl) {
3704
- const { cursor, buffer } = state;
3705
3714
  if (cursor === 0) return;
3706
3715
  const beforeCursor = buffer.slice(0, cursor);
3707
3716
  const prevWordStart = beforeCursor.lastIndexOf(" ");
3708
3717
  const target = prevWordStart === -1 ? 0 : prevWordStart + 1;
3709
- dispatch({ type: "setBuffer", buffer, cursor: target });
3718
+ setDraft(buffer, target);
3710
3719
  return;
3711
3720
  }
3712
- if (state.cursor > 0)
3713
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor - 1 });
3721
+ if (cursor > 0) setDraft(buffer, cursor - 1);
3714
3722
  return;
3715
3723
  }
3716
3724
  if (key.rightArrow) {
3717
3725
  if (key.ctrl) {
3718
- const { cursor, buffer } = state;
3719
3726
  if (cursor >= buffer.length) return;
3720
3727
  const afterCursor = buffer.slice(cursor);
3721
3728
  const nextWordStart = afterCursor.indexOf(" ");
3722
3729
  const target = nextWordStart === -1 ? buffer.length : cursor + nextWordStart + 1;
3723
- dispatch({ type: "setBuffer", buffer, cursor: target });
3730
+ setDraft(buffer, target);
3724
3731
  return;
3725
3732
  }
3726
- if (state.cursor < state.buffer.length)
3727
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor + 1 });
3733
+ if (cursor < buffer.length) setDraft(buffer, cursor + 1);
3728
3734
  return;
3729
3735
  }
3730
3736
  if (key.home) {
3731
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: 0 });
3737
+ setDraft(buffer, 0);
3732
3738
  return;
3733
3739
  }
3734
3740
  if (key.end) {
3735
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.buffer.length });
3741
+ setDraft(buffer, buffer.length);
3736
3742
  return;
3737
3743
  }
3738
3744
  if (key.upArrow) {
@@ -3744,24 +3750,23 @@ function App({
3744
3750
  return;
3745
3751
  }
3746
3752
  if (key.ctrl && input === "a") {
3747
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: 0 });
3753
+ setDraft(buffer, 0);
3748
3754
  return;
3749
3755
  }
3750
3756
  if (key.ctrl && input === "e") {
3751
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.buffer.length });
3757
+ setDraft(buffer, buffer.length);
3752
3758
  return;
3753
3759
  }
3754
3760
  if (key.ctrl && input === "u") {
3755
- dispatch({ type: "setBuffer", buffer: "", cursor: 0 });
3761
+ setDraft("", 0);
3756
3762
  return;
3757
3763
  }
3758
3764
  if (key.ctrl && input === "w") {
3759
- const { cursor, buffer } = state;
3760
3765
  if (cursor === 0) return;
3761
3766
  const beforeCursor = buffer.slice(0, cursor);
3762
3767
  const lastWordStart = beforeCursor.lastIndexOf(" ") + 1;
3763
3768
  const next2 = buffer.slice(0, lastWordStart) + buffer.slice(cursor);
3764
- dispatch({ type: "setBuffer", buffer: next2, cursor: lastWordStart });
3769
+ setDraft(next2, lastWordStart);
3765
3770
  return;
3766
3771
  }
3767
3772
  if (key.meta && input === "v") {
@@ -3783,13 +3788,13 @@ function App({
3783
3788
  const lineCount = cleanInput.split("\n").length;
3784
3789
  dispatch({ type: "addPlaceholder", ph: `${ph} (${lineCount} lines)` });
3785
3790
  } else {
3786
- const next2 = state.buffer.slice(0, state.cursor) + cleanInput + state.buffer.slice(state.cursor);
3787
- dispatch({ type: "setBuffer", buffer: next2, cursor: state.cursor + cleanInput.length });
3791
+ const next2 = buffer.slice(0, cursor) + cleanInput + buffer.slice(cursor);
3792
+ setDraft(next2, cursor + cleanInput.length);
3788
3793
  }
3789
3794
  return;
3790
3795
  }
3791
- const next = state.buffer.slice(0, state.cursor) + cleanInput + state.buffer.slice(state.cursor);
3792
- dispatch({ type: "setBuffer", buffer: next, cursor: state.cursor + cleanInput.length });
3796
+ const next = buffer.slice(0, cursor) + cleanInput + buffer.slice(cursor);
3797
+ setDraft(next, cursor + cleanInput.length);
3793
3798
  };
3794
3799
  const runBlocks = async (blocks) => {
3795
3800
  const ctrl = new AbortController();
@@ -3871,21 +3876,24 @@ function App({
3871
3876
  };
3872
3877
  const runBlocksRef = useRef(runBlocks);
3873
3878
  runBlocksRef.current = runBlocks;
3874
- const submit = async () => {
3875
- const raw = state.buffer;
3879
+ const submit = async (overrideRaw) => {
3880
+ const raw = overrideRaw ?? draftRef.current.buffer;
3876
3881
  const trimmed = raw.trim();
3877
3882
  if (!trimmed && state.placeholders.length === 0) return;
3878
3883
  dispatch({ type: "resetInterrupts" });
3884
+ const pushSubmittedHistory = () => {
3885
+ if (trimmed) dispatch({ type: "historyPush", text: trimmed });
3886
+ };
3879
3887
  if (trimmed === "/image" || trimmed === "/paste-image") {
3880
- dispatch({ type: "clearInput" });
3888
+ pushSubmittedHistory();
3889
+ clearDraft();
3881
3890
  await pasteClipboardImage();
3882
- if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
3883
3891
  return;
3884
3892
  }
3885
3893
  if (trimmed.startsWith("/")) {
3886
3894
  dispatch({ type: "addEntry", entry: { kind: "user", text: trimmed } });
3887
- if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
3888
- dispatch({ type: "clearInput" });
3895
+ pushSubmittedHistory();
3896
+ clearDraft();
3889
3897
  try {
3890
3898
  const res = await slashRegistry.dispatch(trimmed, agent.ctx);
3891
3899
  if (res?.message) {
@@ -3931,20 +3939,19 @@ function App({
3931
3939
  builder.appendText(toAppend);
3932
3940
  }
3933
3941
  if (steering) dispatch({ type: "steerConsume" });
3934
- const blocks = await builder.submit();
3935
3942
  const displayText = trimmed ? steering ? `\u21AF ${trimmed}` : trimmed : "(attachments only)";
3936
- dispatch({ type: "clearInput" });
3943
+ pushSubmittedHistory();
3944
+ clearDraft();
3945
+ const blocks = await builder.submit();
3937
3946
  if (state.status !== "idle") {
3938
3947
  dispatch({
3939
3948
  type: "addEntry",
3940
3949
  entry: { kind: "user", text: displayText, queued: true }
3941
3950
  });
3942
3951
  dispatch({ type: "enqueue", item: { displayText, blocks } });
3943
- if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
3944
3952
  return;
3945
3953
  }
3946
3954
  dispatch({ type: "addEntry", entry: { kind: "user", text: displayText } });
3947
- if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
3948
3955
  await runBlocks(blocks);
3949
3956
  };
3950
3957
  const bootInjectedRef = useRef(false);
@@ -3999,7 +4006,7 @@ function App({
3999
4006
  value: state.buffer,
4000
4007
  cursor: state.cursor,
4001
4008
  placeholders: state.placeholders,
4002
- disabled: state.status === "aborting",
4009
+ disabled: state.status === "aborting" || state.confirmQueue.length > 0,
4003
4010
  hint: inputHint,
4004
4011
  onKey: handleKey
4005
4012
  }
@@ -4031,15 +4038,24 @@ function App({
4031
4038
  hint: state.modelPicker.hint
4032
4039
  }
4033
4040
  ) : null,
4034
- state.confirm ? /* @__PURE__ */ jsx(
4035
- ConfirmPrompt,
4036
- {
4037
- toolName: state.confirm.toolName,
4038
- input: state.confirm.input,
4039
- suggestedPattern: state.confirm.suggestedPattern,
4040
- onDecision: state.confirm.resolve
4041
- }
4042
- ) : null,
4041
+ state.confirmQueue.length > 0 && (() => {
4042
+ const head = state.confirmQueue[0];
4043
+ let resolved = false;
4044
+ return /* @__PURE__ */ jsx(
4045
+ ConfirmPrompt,
4046
+ {
4047
+ toolName: head.toolName,
4048
+ input: head.input,
4049
+ suggestedPattern: head.suggestedPattern,
4050
+ onDecision: (decision) => {
4051
+ if (resolved) return;
4052
+ resolved = true;
4053
+ head.resolve(decision);
4054
+ dispatch({ type: "confirmClose" });
4055
+ }
4056
+ }
4057
+ );
4058
+ })(),
4043
4059
  /* @__PURE__ */ jsx(
4044
4060
  StatusBar,
4045
4061
  {
@@ -4171,7 +4187,7 @@ async function runTui(opts) {
4171
4187
  let instance;
4172
4188
  try {
4173
4189
  instance = render(
4174
- React.createElement(App, {
4190
+ React4.createElement(App, {
4175
4191
  agent: opts.agent,
4176
4192
  slashRegistry: opts.slashRegistry,
4177
4193
  attachments: opts.attachments,