@wrongstack/tui 0.3.3 → 0.3.4

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
@@ -256,81 +256,6 @@ function FleetPanel({ entries, totalCost, roster }) {
256
256
  }
257
257
  );
258
258
  }
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
259
 
335
260
  // src/markdown-table.ts
336
261
  function renderMarkdownTables(text, maxWidth) {
@@ -641,7 +566,7 @@ function Entry({
641
566
  parts.push(`${entry.outputLines} L`);
642
567
  }
643
568
  if (entry.outputBytes && entry.outputBytes > 0) {
644
- parts.push(fmtBytes3(entry.outputBytes));
569
+ parts.push(fmtBytes2(entry.outputBytes));
645
570
  }
646
571
  if (entry.outputTokens && entry.outputTokens > 0) {
647
572
  parts.push(`\u2248${fmtTok(entry.outputTokens)} tok`);
@@ -925,7 +850,7 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
925
850
  const bytes = numOf(o["bytes_written"]) ?? numOf(o["bytes"]);
926
851
  const created = o["created"] === true;
927
852
  const tag = created ? "created" : "updated";
928
- if (bytes !== void 0) return [`${tag} \xB7 ${fmtBytes3(bytes)}`];
853
+ if (bytes !== void 0) return [`${tag} \xB7 ${fmtBytes2(bytes)}`];
929
854
  return [tag];
930
855
  }
931
856
  }
@@ -990,17 +915,17 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
990
915
  if (json && typeof json === "object") {
991
916
  const o = json;
992
917
  const bytes = numOf(o["bytes"]);
993
- if (bytes !== void 0) return [`${fmtBytes3(bytes)} read`];
918
+ if (bytes !== void 0) return [`${fmtBytes2(bytes)} read`];
994
919
  }
995
920
  const range = scanNumberedRange(text);
996
921
  if (range.count > 0 && range.first !== void 0 && range.last !== void 0) {
997
922
  if (range.first === range.last) {
998
- return [`L${range.first} \xB7 ${fmtBytes3(text.length)}`];
923
+ return [`L${range.first} \xB7 ${fmtBytes2(text.length)}`];
999
924
  }
1000
925
  const contiguous = range.count === range.last - range.first + 1;
1001
926
  const head = `L${range.first}\u2013${range.last}`;
1002
927
  const tail = contiguous ? `${range.count} line${range.count === 1 ? "" : "s"}` : `${range.count} lines (gaps)`;
1003
- return [`${head} \xB7 ${tail} \xB7 ${fmtBytes3(text.length)}`];
928
+ return [`${head} \xB7 ${tail} \xB7 ${fmtBytes2(text.length)}`];
1004
929
  }
1005
930
  }
1006
931
  if (toolName === "grep" || toolName === "glob") {
@@ -1058,7 +983,7 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
1058
983
  const head = [];
1059
984
  if (status !== void 0) head.push(`HTTP ${status}`);
1060
985
  if (ct) head.push(ct.split(";")[0] ?? ct);
1061
- if (content) head.push(fmtBytes3(Buffer.byteLength(content, "utf8")));
986
+ if (content) head.push(fmtBytes2(Buffer.byteLength(content, "utf8")));
1062
987
  const lines = [];
1063
988
  if (head.length > 0) lines.push(head.join(" \xB7 "));
1064
989
  if (url && status !== void 0 && (status < 200 || status >= 400)) {
@@ -1434,7 +1359,7 @@ function countLines(text) {
1434
1359
  if (!text) return 0;
1435
1360
  return text.replace(/\n$/, "").split("\n").length;
1436
1361
  }
1437
- function fmtBytes3(n) {
1362
+ function fmtBytes2(n) {
1438
1363
  if (n < 1024) return `${n}B`;
1439
1364
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
1440
1365
  return `${(n / (1024 * 1024)).toFixed(1)}MB`;
@@ -1515,6 +1440,81 @@ function Input({
1515
1440
  hint ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint }) : null
1516
1441
  ] });
1517
1442
  }
1443
+ function fmtElapsed(ms) {
1444
+ if (ms < 1e3) return `${ms}ms`;
1445
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
1446
+ const m = Math.floor(ms / 6e4);
1447
+ const s = Math.floor(ms % 6e4 / 1e3);
1448
+ return `${m}m${s.toString().padStart(2, "0")}s`;
1449
+ }
1450
+ function fmtBytes3(n) {
1451
+ if (n < 1024) return `${n}B`;
1452
+ return `${(n / 1024).toFixed(1)}KB`;
1453
+ }
1454
+ function fmtRecentTool2(tool) {
1455
+ const status = tool.ok === false ? "fail" : "ok";
1456
+ const name = tool.name.length > 18 ? `${tool.name.slice(0, 17)}...` : tool.name;
1457
+ const parts = [status, name];
1458
+ if (typeof tool.durationMs === "number") parts.push(fmtElapsed(tool.durationMs));
1459
+ if (typeof tool.outputBytes === "number" && tool.outputBytes > 0) parts.push(fmtBytes3(tool.outputBytes));
1460
+ if (typeof tool.outputLines === "number" && tool.outputLines > 0) parts.push(`${tool.outputLines}L`);
1461
+ return parts.join(" ");
1462
+ }
1463
+ function fmtRecentMessage2(message) {
1464
+ const text = message.text.replace(/\s+/g, " ");
1465
+ return text.length > 48 ? `${text.slice(0, 47)}...` : text;
1466
+ }
1467
+ function LiveActivityStrip({
1468
+ entries,
1469
+ nowTick,
1470
+ maxRows = 4
1471
+ }) {
1472
+ const running = Object.values(entries).filter((e) => e.status === "running").sort((a, b) => a.startedAt - b.startedAt).slice(0, maxRows);
1473
+ if (running.length === 0) return null;
1474
+ const now = Date.now();
1475
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1476
+ running.map((e) => {
1477
+ const toolElapsed = e.currentTool ? now - e.currentTool.startedAt : 0;
1478
+ const taskElapsed = now - e.startedAt;
1479
+ const toolSeg = e.currentTool ? `\u2192 ${e.currentTool.name} (${fmtElapsed(toolElapsed)})` : "idle between tools";
1480
+ const recentTools = (e.recentTools ?? []).slice(-2).map(fmtRecentTool2).join(" | ");
1481
+ const messageText = e.streamingText.trim() || (e.recentMessages ?? []).slice(-1).map(fmtRecentMessage2).join("");
1482
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
1483
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CF" }),
1484
+ /* @__PURE__ */ jsx(Text, { children: e.name.slice(0, 14).padEnd(14) }),
1485
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
1486
+ /* @__PURE__ */ jsx(Text, { color: e.currentTool ? "green" : "yellow", children: toolSeg }),
1487
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
1488
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1489
+ e.iterations,
1490
+ "it ",
1491
+ e.toolCalls,
1492
+ "tc \xB7 ",
1493
+ fmtElapsed(taskElapsed)
1494
+ ] }),
1495
+ recentTools ? /* @__PURE__ */ jsxs(Fragment, { children: [
1496
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
1497
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1498
+ "last: ",
1499
+ recentTools
1500
+ ] })
1501
+ ] }) : null,
1502
+ messageText ? /* @__PURE__ */ jsxs(Fragment, { children: [
1503
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
1504
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1505
+ "msg: ",
1506
+ fmtRecentMessage2({ text: messageText})
1507
+ ] })
1508
+ ] }) : null
1509
+ ] }, e.id);
1510
+ }),
1511
+ Object.values(entries).filter((e) => e.status === "running").length > maxRows ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1512
+ "\u2026+",
1513
+ Object.values(entries).filter((e) => e.status === "running").length - maxRows,
1514
+ " more"
1515
+ ] }) }) : null
1516
+ ] });
1517
+ }
1518
1518
  function ModelPicker({
1519
1519
  step,
1520
1520
  providerOptions,
@@ -1526,7 +1526,7 @@ function ModelPicker({
1526
1526
  if (step === "provider") {
1527
1527
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1528
1528
  /* @__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" }),
1529
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc cancel \xB7 Ctrl+C exit" }),
1530
1530
  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
1531
  i === selected ? "\u203A " : " ",
1532
1532
  /* @__PURE__ */ jsx(Text, { bold: true, children: p.id.padEnd(28) }),
@@ -1551,7 +1551,7 @@ function ModelPicker({
1551
1551
  pickedProviderId,
1552
1552
  ") \u2501\u2501"
1553
1553
  ] }),
1554
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc back \xB7 Ctrl-C cancel" }),
1554
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc back \xB7 Ctrl+C exit" }),
1555
1555
  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
1556
  i === selected ? "\u203A " : " ",
1557
1557
  id
@@ -2050,6 +2050,11 @@ function oneLine(s, max) {
2050
2050
  const collapsed = s.replace(/\s+/g, " ").trim();
2051
2051
  return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 1)}\u2026`;
2052
2052
  }
2053
+ function selectedSlashCommandLine(picker) {
2054
+ if (!picker.open || picker.matches.length === 0) return null;
2055
+ const picked = picker.matches[picker.selected];
2056
+ return picked ? `/${picked.name}` : null;
2057
+ }
2053
2058
  function reducer(state, action) {
2054
2059
  switch (action.type) {
2055
2060
  case "addEntry": {
@@ -2066,6 +2071,7 @@ function reducer(state, action) {
2066
2071
  buffer: "",
2067
2072
  cursor: 0,
2068
2073
  placeholders: [],
2074
+ historyIndex: 0,
2069
2075
  picker: { open: false, query: "", matches: [], selected: 0 },
2070
2076
  slashPicker: { open: false, query: "", matches: [], selected: 0 }
2071
2077
  };
@@ -2649,6 +2655,16 @@ function App({
2649
2655
  const flushTimerRef = useRef(null);
2650
2656
  const stateRef = useRef(state);
2651
2657
  stateRef.current = state;
2658
+ const draftRef = useRef({ buffer: state.buffer, cursor: state.cursor });
2659
+ draftRef.current = { buffer: state.buffer, cursor: state.cursor };
2660
+ const setDraft = (buffer, cursor) => {
2661
+ draftRef.current = { buffer, cursor };
2662
+ dispatch({ type: "setBuffer", buffer, cursor });
2663
+ };
2664
+ const clearDraft = () => {
2665
+ draftRef.current = { buffer: "", cursor: 0 };
2666
+ dispatch({ type: "clearInput" });
2667
+ };
2652
2668
  const startedAtRef = useRef(Date.now());
2653
2669
  const [nowTick, setNowTick] = React.useState(Date.now());
2654
2670
  useEffect(() => {
@@ -2838,7 +2854,7 @@ function App({
2838
2854
  }).slice(0, 12).map(({ cmd, owner }) => ({
2839
2855
  name: cmd.name,
2840
2856
  description: cmd.description,
2841
- argsHint: void 0,
2857
+ argsHint: cmd.argsHint,
2842
2858
  isBuiltin: owner === "core"
2843
2859
  }));
2844
2860
  if (!state.slashPicker.open) {
@@ -2879,7 +2895,8 @@ function App({
2879
2895
  if (!picked) return;
2880
2896
  const builder = builderRef.current;
2881
2897
  if (!builder) return;
2882
- const tok = detectAtToken(state.buffer, state.cursor);
2898
+ const draft = draftRef.current;
2899
+ const tok = detectAtToken(draft.buffer, draft.cursor);
2883
2900
  if (!tok) {
2884
2901
  dispatch({ type: "pickerClose" });
2885
2902
  return;
@@ -2892,14 +2909,10 @@ function App({
2892
2909
  data,
2893
2910
  meta: { filename: picked, label: picked }
2894
2911
  });
2895
- const before = state.buffer.slice(0, tok.start);
2896
- const after = state.buffer.slice(tok.end);
2912
+ const before = draft.buffer.slice(0, tok.start);
2913
+ const after = draft.buffer.slice(tok.end);
2897
2914
  const next = `${before}${placeholder}${after}`;
2898
- dispatch({
2899
- type: "setBuffer",
2900
- buffer: next,
2901
- cursor: tok.start + placeholder.length
2902
- });
2915
+ setDraft(next, tok.start + placeholder.length);
2903
2916
  dispatch({ type: "pickerClose" });
2904
2917
  } catch (err) {
2905
2918
  dispatch({
@@ -2918,7 +2931,7 @@ function App({
2918
2931
  const picked = matches[selected];
2919
2932
  if (!picked) return;
2920
2933
  const cmd = picked.argsHint !== void 0 ? `/${picked.name} ` : `/${picked.name}`;
2921
- dispatch({ type: "setBuffer", buffer: cmd, cursor: cmd.length });
2934
+ setDraft(cmd, cmd.length);
2922
2935
  dispatch({ type: "slashPickerClose" });
2923
2936
  };
2924
2937
  useEffect(() => {
@@ -3461,8 +3474,9 @@ function App({
3461
3474
  }, [director]);
3462
3475
  useEffect(() => {
3463
3476
  const onSigint = () => {
3464
- if (state.interrupts >= 1) {
3465
- if (state.interrupts >= 2) {
3477
+ const current = stateRef.current;
3478
+ if (current.interrupts >= 1) {
3479
+ if (current.interrupts >= 2) {
3466
3480
  process.exit(130);
3467
3481
  }
3468
3482
  try {
@@ -3480,6 +3494,22 @@ function App({
3480
3494
  return;
3481
3495
  }
3482
3496
  dispatch({ type: "interrupt" });
3497
+ if (current.modelPicker.open) {
3498
+ dispatch({ type: "modelPickerClose" });
3499
+ dispatch({
3500
+ type: "addEntry",
3501
+ entry: { kind: "warn", text: "Model picker cancelled." }
3502
+ });
3503
+ return;
3504
+ }
3505
+ if (current.slashPicker.open) {
3506
+ dispatch({ type: "slashPickerClose" });
3507
+ dispatch({
3508
+ type: "addEntry",
3509
+ entry: { kind: "warn", text: "Cancelled." }
3510
+ });
3511
+ return;
3512
+ }
3483
3513
  if (activeCtrlRef.current) {
3484
3514
  activeCtrlRef.current.abort();
3485
3515
  dispatch({ type: "status", status: "aborting" });
@@ -3520,7 +3550,7 @@ function App({
3520
3550
  return () => {
3521
3551
  process.off("SIGINT", onSigint);
3522
3552
  };
3523
- }, [state.interrupts, exit, onExit, director]);
3553
+ }, [exit, onExit, director]);
3524
3554
  const handleKey = async (input, key) => {
3525
3555
  if (state.status === "aborting") return;
3526
3556
  if (inputGateRef.current) return;
@@ -3591,15 +3621,23 @@ function App({
3591
3621
  return;
3592
3622
  }
3593
3623
  if (isEnter) {
3624
+ const now = Date.now();
3625
+ if (now - lastEnterAtRef.current < 50) return;
3626
+ lastEnterAtRef.current = now;
3594
3627
  inputGateRef.current = true;
3595
- acceptSlashPickerSelection();
3628
+ const line = selectedSlashCommandLine(state.slashPicker);
3629
+ if (line) {
3630
+ void submit(line);
3631
+ } else {
3632
+ acceptSlashPickerSelection();
3633
+ }
3596
3634
  inputGateRef.current = false;
3597
3635
  return;
3598
3636
  }
3599
3637
  if (key.tab && state.slashPicker.matches.length > 0) {
3600
3638
  const sel = state.slashPicker.matches[state.slashPicker.selected];
3601
3639
  if (sel) {
3602
- dispatch({ type: "setBuffer", buffer: `/${sel.name} `, cursor: sel.name.length + 2 });
3640
+ setDraft(`/${sel.name} `, sel.name.length + 2);
3603
3641
  dispatch({ type: "slashPickerClose" });
3604
3642
  }
3605
3643
  return;
@@ -3675,64 +3713,60 @@ function App({
3675
3713
  void submit();
3676
3714
  return;
3677
3715
  }
3716
+ const { buffer, cursor } = draftRef.current;
3678
3717
  if (key.backspace || key.delete) {
3679
3718
  if (key.ctrl) {
3680
- const { cursor, buffer } = state;
3681
3719
  if (key.backspace) {
3682
3720
  if (cursor === 0) return;
3683
3721
  const beforeCursor = buffer.slice(0, cursor);
3684
3722
  const lastWordStart = beforeCursor.lastIndexOf(" ") + 1;
3685
3723
  const next3 = buffer.slice(0, lastWordStart) + buffer.slice(cursor);
3686
- dispatch({ type: "setBuffer", buffer: next3, cursor: lastWordStart });
3724
+ setDraft(next3, lastWordStart);
3687
3725
  } else {
3688
3726
  if (cursor >= buffer.length) return;
3689
3727
  const afterCursor = buffer.slice(cursor);
3690
3728
  const nextWordStart = afterCursor.indexOf(" ");
3691
3729
  const end = nextWordStart === -1 ? buffer.length : cursor + nextWordStart + 1;
3692
3730
  const next3 = buffer.slice(0, cursor) + buffer.slice(end);
3693
- dispatch({ type: "setBuffer", buffer: next3, cursor });
3731
+ setDraft(next3, cursor);
3694
3732
  }
3695
3733
  return;
3696
3734
  }
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 });
3735
+ if (cursor === 0) return;
3736
+ const next2 = buffer.slice(0, cursor - 1) + buffer.slice(cursor);
3737
+ setDraft(next2, cursor - 1);
3700
3738
  return;
3701
3739
  }
3702
3740
  if (key.leftArrow) {
3703
3741
  if (key.ctrl) {
3704
- const { cursor, buffer } = state;
3705
3742
  if (cursor === 0) return;
3706
3743
  const beforeCursor = buffer.slice(0, cursor);
3707
3744
  const prevWordStart = beforeCursor.lastIndexOf(" ");
3708
3745
  const target = prevWordStart === -1 ? 0 : prevWordStart + 1;
3709
- dispatch({ type: "setBuffer", buffer, cursor: target });
3746
+ setDraft(buffer, target);
3710
3747
  return;
3711
3748
  }
3712
- if (state.cursor > 0)
3713
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor - 1 });
3749
+ if (cursor > 0) setDraft(buffer, cursor - 1);
3714
3750
  return;
3715
3751
  }
3716
3752
  if (key.rightArrow) {
3717
3753
  if (key.ctrl) {
3718
- const { cursor, buffer } = state;
3719
3754
  if (cursor >= buffer.length) return;
3720
3755
  const afterCursor = buffer.slice(cursor);
3721
3756
  const nextWordStart = afterCursor.indexOf(" ");
3722
3757
  const target = nextWordStart === -1 ? buffer.length : cursor + nextWordStart + 1;
3723
- dispatch({ type: "setBuffer", buffer, cursor: target });
3758
+ setDraft(buffer, target);
3724
3759
  return;
3725
3760
  }
3726
- if (state.cursor < state.buffer.length)
3727
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor + 1 });
3761
+ if (cursor < buffer.length) setDraft(buffer, cursor + 1);
3728
3762
  return;
3729
3763
  }
3730
3764
  if (key.home) {
3731
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: 0 });
3765
+ setDraft(buffer, 0);
3732
3766
  return;
3733
3767
  }
3734
3768
  if (key.end) {
3735
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.buffer.length });
3769
+ setDraft(buffer, buffer.length);
3736
3770
  return;
3737
3771
  }
3738
3772
  if (key.upArrow) {
@@ -3744,24 +3778,23 @@ function App({
3744
3778
  return;
3745
3779
  }
3746
3780
  if (key.ctrl && input === "a") {
3747
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: 0 });
3781
+ setDraft(buffer, 0);
3748
3782
  return;
3749
3783
  }
3750
3784
  if (key.ctrl && input === "e") {
3751
- dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.buffer.length });
3785
+ setDraft(buffer, buffer.length);
3752
3786
  return;
3753
3787
  }
3754
3788
  if (key.ctrl && input === "u") {
3755
- dispatch({ type: "setBuffer", buffer: "", cursor: 0 });
3789
+ setDraft("", 0);
3756
3790
  return;
3757
3791
  }
3758
3792
  if (key.ctrl && input === "w") {
3759
- const { cursor, buffer } = state;
3760
3793
  if (cursor === 0) return;
3761
3794
  const beforeCursor = buffer.slice(0, cursor);
3762
3795
  const lastWordStart = beforeCursor.lastIndexOf(" ") + 1;
3763
3796
  const next2 = buffer.slice(0, lastWordStart) + buffer.slice(cursor);
3764
- dispatch({ type: "setBuffer", buffer: next2, cursor: lastWordStart });
3797
+ setDraft(next2, lastWordStart);
3765
3798
  return;
3766
3799
  }
3767
3800
  if (key.meta && input === "v") {
@@ -3783,13 +3816,13 @@ function App({
3783
3816
  const lineCount = cleanInput.split("\n").length;
3784
3817
  dispatch({ type: "addPlaceholder", ph: `${ph} (${lineCount} lines)` });
3785
3818
  } 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 });
3819
+ const next2 = buffer.slice(0, cursor) + cleanInput + buffer.slice(cursor);
3820
+ setDraft(next2, cursor + cleanInput.length);
3788
3821
  }
3789
3822
  return;
3790
3823
  }
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 });
3824
+ const next = buffer.slice(0, cursor) + cleanInput + buffer.slice(cursor);
3825
+ setDraft(next, cursor + cleanInput.length);
3793
3826
  };
3794
3827
  const runBlocks = async (blocks) => {
3795
3828
  const ctrl = new AbortController();
@@ -3871,21 +3904,24 @@ function App({
3871
3904
  };
3872
3905
  const runBlocksRef = useRef(runBlocks);
3873
3906
  runBlocksRef.current = runBlocks;
3874
- const submit = async () => {
3875
- const raw = state.buffer;
3907
+ const submit = async (overrideRaw) => {
3908
+ const raw = overrideRaw ?? draftRef.current.buffer;
3876
3909
  const trimmed = raw.trim();
3877
3910
  if (!trimmed && state.placeholders.length === 0) return;
3878
3911
  dispatch({ type: "resetInterrupts" });
3912
+ const pushSubmittedHistory = () => {
3913
+ if (trimmed) dispatch({ type: "historyPush", text: trimmed });
3914
+ };
3879
3915
  if (trimmed === "/image" || trimmed === "/paste-image") {
3880
- dispatch({ type: "clearInput" });
3916
+ pushSubmittedHistory();
3917
+ clearDraft();
3881
3918
  await pasteClipboardImage();
3882
- if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
3883
3919
  return;
3884
3920
  }
3885
3921
  if (trimmed.startsWith("/")) {
3886
3922
  dispatch({ type: "addEntry", entry: { kind: "user", text: trimmed } });
3887
- if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
3888
- dispatch({ type: "clearInput" });
3923
+ pushSubmittedHistory();
3924
+ clearDraft();
3889
3925
  try {
3890
3926
  const res = await slashRegistry.dispatch(trimmed, agent.ctx);
3891
3927
  if (res?.message) {
@@ -3931,20 +3967,19 @@ function App({
3931
3967
  builder.appendText(toAppend);
3932
3968
  }
3933
3969
  if (steering) dispatch({ type: "steerConsume" });
3934
- const blocks = await builder.submit();
3935
3970
  const displayText = trimmed ? steering ? `\u21AF ${trimmed}` : trimmed : "(attachments only)";
3936
- dispatch({ type: "clearInput" });
3971
+ pushSubmittedHistory();
3972
+ clearDraft();
3973
+ const blocks = await builder.submit();
3937
3974
  if (state.status !== "idle") {
3938
3975
  dispatch({
3939
3976
  type: "addEntry",
3940
3977
  entry: { kind: "user", text: displayText, queued: true }
3941
3978
  });
3942
3979
  dispatch({ type: "enqueue", item: { displayText, blocks } });
3943
- if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
3944
3980
  return;
3945
3981
  }
3946
3982
  dispatch({ type: "addEntry", entry: { kind: "user", text: displayText } });
3947
- if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
3948
3983
  await runBlocks(blocks);
3949
3984
  };
3950
3985
  const bootInjectedRef = useRef(false);