@wrongstack/tui 0.10.3 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -234,6 +234,15 @@ interface RunTuiOptions {
234
234
  mode: 'off' | 'suggest' | 'auto';
235
235
  delayMs: number;
236
236
  }) => string | null | Promise<string | null>;
237
+ /**
238
+ * Predict likely next steps after a completed turn. The CLI wires this from
239
+ * the session provider and the `/next` toggle; it returns [] when prediction
240
+ * is disabled or autonomy isn't 'off'. Display-only — never executed.
241
+ */
242
+ predictNext?: (input: {
243
+ userRequest: string;
244
+ assistantSummary: string;
245
+ }) => Promise<string[]>;
237
246
  }
238
247
  declare function runTui(opts: RunTuiOptions): Promise<number>;
239
248
 
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { PassThrough } from 'stream';
1
+ import { Readable } from 'stream';
2
2
  import { Box, Text, render, useApp, useStdout, measureElement, Static, useInput, useStdin } from 'ink';
3
- import React4, { useState, useEffect, useReducer, useRef, useMemo, useLayoutEffect } from 'react';
3
+ import React4, { useState, useEffect, useReducer, useRef, useMemo, useCallback, useLayoutEffect } from 'react';
4
4
  import * as fs2 from 'fs/promises';
5
5
  import * as path2 from 'path';
6
6
  import { InputBuilder, DefaultSessionRewinder, formatTodosList, buildGoalPreamble, buildChildEnv } from '@wrongstack/core';
@@ -882,21 +882,58 @@ function AgentsMonitor({
882
882
  ] });
883
883
  }
884
884
  var AUTONOMY_OPTIONS = [
885
- { mode: "off", label: "OFF", description: "Agent stops after each turn (normal interactive mode)", color: "green" },
886
- { mode: "suggest", label: "SUGGEST", description: "Shows next-step suggestions after each turn", color: "cyan" },
887
- { mode: "auto", label: "AUTO", description: "Self-driving \u2014 agent picks next step and continues", color: "yellow" },
888
- { mode: "eternal", label: "ETERNAL", description: "Goal-driven loop \u2014 requires /goal set first", color: "red" },
889
- { mode: "eternal-parallel", label: "PARALLEL", description: "Fan-out 4\u20138 subagents per tick \u2014 requires /goal", color: "magenta" }
885
+ {
886
+ mode: "off",
887
+ label: "OFF",
888
+ description: "Agent stops after each turn (normal interactive mode)",
889
+ color: "green"
890
+ },
891
+ {
892
+ mode: "suggest",
893
+ label: "SUGGEST",
894
+ description: "Shows next-step suggestions after each turn",
895
+ color: "cyan"
896
+ },
897
+ {
898
+ mode: "auto",
899
+ label: "AUTO",
900
+ description: "Self-driving \u2014 agent picks next step and continues",
901
+ color: "yellow"
902
+ },
903
+ {
904
+ mode: "eternal",
905
+ label: "ETERNAL",
906
+ description: "Goal-driven loop \u2014 requires /goal set first",
907
+ color: "red"
908
+ },
909
+ {
910
+ mode: "eternal-parallel",
911
+ label: "PARALLEL",
912
+ description: "Fan-out 4\u20138 subagents per tick \u2014 requires /goal",
913
+ color: "magenta"
914
+ }
890
915
  ];
891
- function AutonomyPicker({ options, selected, hint }) {
916
+ function AutonomyPicker({
917
+ options,
918
+ selected,
919
+ hint
920
+ }) {
892
921
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
893
922
  /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u2501\u2501 Autonomy Mode \u2501\u2501" }),
894
923
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc cancel \xB7 Ctrl+C exit" }),
895
- options.map((opt, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? opt.color : void 0, inverse: i === selected, children: [
896
- i === selected ? "\u203A " : " ",
897
- /* @__PURE__ */ jsx(Text, { bold: true, children: opt.label.padEnd(12) }),
898
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: opt.description })
899
- ] }, opt.mode)),
924
+ options.map((opt, i) => /* @__PURE__ */ jsxs(
925
+ Text,
926
+ {
927
+ color: i === selected ? opt.color : void 0,
928
+ inverse: i === selected,
929
+ children: [
930
+ i === selected ? "\u203A " : " ",
931
+ /* @__PURE__ */ jsx(Text, { bold: true, children: opt.label.padEnd(12) }),
932
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: opt.description })
933
+ ]
934
+ },
935
+ opt.mode
936
+ )),
900
937
  hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
901
938
  ] });
902
939
  }
@@ -934,7 +971,8 @@ function CheckpointTimeline({
934
971
  new Date(cp.ts).toLocaleTimeString()
935
972
  ] }),
936
973
  cp.fileCount > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
937
- " \xB7 ",
974
+ " ",
975
+ "\xB7 ",
938
976
  cp.fileCount,
939
977
  " file",
940
978
  cp.fileCount !== 1 ? "s" : ""
@@ -1055,7 +1093,11 @@ function FilePicker({ query, matches, selected }) {
1055
1093
  function highlight(path3, _query) {
1056
1094
  return path3;
1057
1095
  }
1058
- function FleetPanel({ entries, totalCost, collabSession }) {
1096
+ function FleetPanel({
1097
+ entries,
1098
+ totalCost,
1099
+ collabSession
1100
+ }) {
1059
1101
  const { stdout } = useStdout();
1060
1102
  const [termWidth, setTermWidth] = useState(stdout?.columns ?? 90);
1061
1103
  useEffect(() => {
@@ -1580,7 +1622,8 @@ function tokenizePython(line, carry) {
1580
1622
  }
1581
1623
  function tokenizeDiff(line) {
1582
1624
  if (line.startsWith("@@")) return [{ text: line, color: C.diffMeta }];
1583
- if (line.startsWith("+++") || line.startsWith("---")) return [{ text: line, color: C.diffMeta, dim: true }];
1625
+ if (line.startsWith("+++") || line.startsWith("---"))
1626
+ return [{ text: line, color: C.diffMeta, dim: true }];
1584
1627
  if (line.startsWith("+")) return [{ text: line, color: C.diffAdd }];
1585
1628
  if (line.startsWith("-")) return [{ text: line, color: C.diffDel }];
1586
1629
  return [{ text: line, dim: true }];
@@ -1996,7 +2039,7 @@ function MarkdownView({
1996
2039
  if (quote && line.startsWith(">")) {
1997
2040
  rows.push(
1998
2041
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
1999
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502 " }),
2042
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
2000
2043
  /* @__PURE__ */ jsx(InlineLine, { tokens: parseInline(quote[1] ?? ""), dim: true })
2001
2044
  ] }, `q${key++}`)
2002
2045
  );
@@ -2121,13 +2164,13 @@ function splitFencedBlocks(text) {
2121
2164
  function CodeBlock({
2122
2165
  code,
2123
2166
  lang,
2124
- termWidth
2167
+ contentWidth
2125
2168
  }) {
2126
2169
  let lines = code.replace(/\n+$/, "").split("\n");
2127
2170
  const hidden = Math.max(0, lines.length - MAX_CODE_LINES);
2128
2171
  if (hidden > 0) lines = lines.slice(0, MAX_CODE_LINES);
2129
2172
  const gutterW = String(lines.length).length;
2130
- const maxW = Math.max(20, Math.min(termWidth - 8 - gutterW - 1, 120));
2173
+ const maxW = Math.max(20, Math.min(contentWidth - 6 - gutterW - 1, 120));
2131
2174
  let carry = {};
2132
2175
  const rows = lines.map((raw) => {
2133
2176
  const display = raw.length > maxW ? `${raw.slice(0, maxW - 1)}\u2026` : raw;
@@ -2135,20 +2178,31 @@ function CodeBlock({
2135
2178
  carry = r.carry;
2136
2179
  return r.tokens;
2137
2180
  });
2138
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 2, marginY: 0, borderStyle: "round", borderColor: theme.borderDefault, paddingX: 1, children: [
2139
- lang !== "plain" ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: lang }) : null,
2140
- rows.map((tokens, i) => (
2141
- // biome-ignore lint/suspicious/noArrayIndexKey: code lines are positional
2142
- /* @__PURE__ */ jsxs(Text, { children: [
2143
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${String(i + 1).padStart(gutterW, " ")} ` }),
2144
- tokens.length === 0 ? " " : tokens.map((t, j) => (
2145
- // biome-ignore lint/suspicious/noArrayIndexKey: token order is stable per line
2146
- /* @__PURE__ */ jsx(Text, { color: t.color, dimColor: t.dim, bold: t.bold, children: t.text }, j)
2147
- ))
2148
- ] }, i)
2149
- )),
2150
- hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: `\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
2151
- ] });
2181
+ return /* @__PURE__ */ jsxs(
2182
+ Box,
2183
+ {
2184
+ flexDirection: "column",
2185
+ marginLeft: 2,
2186
+ marginY: 0,
2187
+ borderStyle: "round",
2188
+ borderColor: theme.borderDefault,
2189
+ paddingX: 1,
2190
+ children: [
2191
+ lang !== "plain" ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: lang }) : null,
2192
+ rows.map((tokens, i) => (
2193
+ // biome-ignore lint/suspicious/noArrayIndexKey: code lines are positional
2194
+ /* @__PURE__ */ jsxs(Text, { children: [
2195
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${String(i + 1).padStart(gutterW, " ")} ` }),
2196
+ tokens.length === 0 ? " " : tokens.map((t, j) => (
2197
+ // biome-ignore lint/suspicious/noArrayIndexKey: token order is stable per line
2198
+ /* @__PURE__ */ jsx(Text, { color: t.color, dimColor: t.dim, bold: t.bold, children: t.text }, j)
2199
+ ))
2200
+ ] }, i)
2201
+ )),
2202
+ hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: `\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
2203
+ ]
2204
+ }
2205
+ );
2152
2206
  }
2153
2207
  function AssistantBody({
2154
2208
  text,
@@ -2160,7 +2214,7 @@ function AssistantBody({
2160
2214
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: segments.map(
2161
2215
  (seg, i) => seg.type === "code" ? (
2162
2216
  // biome-ignore lint/suspicious/noArrayIndexKey: segment order is stable
2163
- /* @__PURE__ */ jsx(CodeBlock, { code: seg.text, lang: seg.lang ?? "plain", termWidth }, i)
2217
+ /* @__PURE__ */ jsx(CodeBlock, { code: seg.text, lang: seg.lang ?? "plain", contentWidth: inner }, i)
2164
2218
  ) : (
2165
2219
  // biome-ignore lint/suspicious/noArrayIndexKey: segment order is stable
2166
2220
  /* @__PURE__ */ jsx(MarkdownView, { text: seg.text, termWidth: inner }, i)
@@ -2289,14 +2343,7 @@ var Entry = React4.memo(function Entry2({
2289
2343
  paddingLeft: 1,
2290
2344
  children: [
2291
2345
  /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.assistant, children: "ASSISTANT" }) }),
2292
- /* @__PURE__ */ jsx(
2293
- AssistantBody,
2294
- {
2295
- text: entry.text,
2296
- termWidth,
2297
- contentWidth
2298
- }
2299
- )
2346
+ /* @__PURE__ */ jsx(AssistantBody, { text: entry.text, termWidth, contentWidth })
2300
2347
  ]
2301
2348
  }
2302
2349
  );
@@ -3411,8 +3458,10 @@ function fmtRecentTool(tool) {
3411
3458
  const name = tool.name.length > 18 ? `${tool.name.slice(0, 17)}...` : tool.name;
3412
3459
  const parts = [status, name];
3413
3460
  if (typeof tool.durationMs === "number") parts.push(fmtElapsed2(tool.durationMs));
3414
- if (typeof tool.outputBytes === "number" && tool.outputBytes > 0) parts.push(fmtBytes2(tool.outputBytes));
3415
- if (typeof tool.outputLines === "number" && tool.outputLines > 0) parts.push(`${tool.outputLines}L`);
3461
+ if (typeof tool.outputBytes === "number" && tool.outputBytes > 0)
3462
+ parts.push(fmtBytes2(tool.outputBytes));
3463
+ if (typeof tool.outputLines === "number" && tool.outputLines > 0)
3464
+ parts.push(`${tool.outputLines}L`);
3416
3465
  return parts.join(" ");
3417
3466
  }
3418
3467
  function fmtRecentMessage(message) {
@@ -3545,7 +3594,9 @@ function PhaseMonitor({
3545
3594
  if (key.escape) onClose();
3546
3595
  });
3547
3596
  const phaseList = Object.values(phases);
3548
- const running = phaseList.filter((p) => runningPhaseIds.includes(Object.keys(phases).find((k) => phases[k] === p) ?? ""));
3597
+ const running = phaseList.filter(
3598
+ (p) => runningPhaseIds.includes(Object.keys(phases).find((k) => phases[k] === p) ?? "")
3599
+ );
3549
3600
  const done = phaseList.filter((p) => p.status === "completed" || p.status === "skipped");
3550
3601
  const failed = phaseList.filter((p) => p.status === "failed");
3551
3602
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
@@ -3756,7 +3807,7 @@ function ScrollableHistory({
3756
3807
  lastReported.current = height;
3757
3808
  onMeasure(height);
3758
3809
  }
3759
- });
3810
+ }, [onMeasure]);
3760
3811
  const vp = Math.max(1, viewportRows);
3761
3812
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
3762
3813
  /* @__PURE__ */ jsx(
@@ -3992,7 +4043,9 @@ function WorktreePanel({
3992
4043
  }) {
3993
4044
  const list = Object.values(worktrees);
3994
4045
  if (list.length === 0) return null;
3995
- const active = list.filter((w) => w.status === "active" || w.status === "committing" || w.status === "merging").length;
4046
+ const active = list.filter(
4047
+ (w) => w.status === "active" || w.status === "committing" || w.status === "merging"
4048
+ ).length;
3996
4049
  const merged = list.filter((w) => w.status === "merged").length;
3997
4050
  const failed = list.filter((w) => w.status === "failed" || w.status === "needs-review").length;
3998
4051
  return /* @__PURE__ */ jsxs(
@@ -4036,7 +4089,8 @@ function WorktreePanel({
4036
4089
  /* @__PURE__ */ jsx(Text, { children: w.branch.replace(/^wstack\/ap\//, "").slice(0, 18).padEnd(18) }),
4037
4090
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: w.ownerLabel.slice(0, 12) }),
4038
4091
  conflict ? /* @__PURE__ */ jsx(Text, { color: "magenta", children: " CONFLICT" }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
4039
- " +",
4092
+ " ",
4093
+ "+",
4040
4094
  w.insertions,
4041
4095
  "/-",
4042
4096
  w.deletions,
@@ -4200,13 +4254,18 @@ function createKillSlashCommand() {
4200
4254
  if (sub === "all") {
4201
4255
  const pids = getProcessRegistry().killAll();
4202
4256
  if (pids.length === 0) return { message: "No processes to kill." };
4203
- return { message: `Killed ${pids.length} process${pids.length === 1 ? "" : "es"}: ${pids.join(", ")}` };
4257
+ return {
4258
+ message: `Killed ${pids.length} process${pids.length === 1 ? "" : "es"}: ${pids.join(", ")}`
4259
+ };
4204
4260
  }
4205
4261
  if (sub === "force") {
4206
4262
  getProcessRegistry().forceBreakerOpen();
4207
4263
  const pids = getProcessRegistry().killAll({ force: true });
4208
- if (pids.length === 0) return { message: "Circuit breaker forced open. No processes to kill." };
4209
- return { message: `Force-killed ${pids.length} process${pids.length === 1 ? "" : "es"}: ${pids.join(", ")}` };
4264
+ if (pids.length === 0)
4265
+ return { message: "Circuit breaker forced open. No processes to kill." };
4266
+ return {
4267
+ message: `Force-killed ${pids.length} process${pids.length === 1 ? "" : "es"}: ${pids.join(", ")}`
4268
+ };
4210
4269
  }
4211
4270
  if (sub === "reset") {
4212
4271
  getProcessRegistry().forceBreakerReset();
@@ -5375,6 +5434,7 @@ function App({
5375
5434
  switchProviderAndModel,
5376
5435
  getSettings,
5377
5436
  saveSettings,
5437
+ predictNext,
5378
5438
  switchAutonomy,
5379
5439
  effectiveMaxContext,
5380
5440
  onExit,
@@ -5400,8 +5460,12 @@ function App({
5400
5460
  const [hiddenItems, setHiddenItems] = useState(statuslineHiddenItems);
5401
5461
  const { stdout } = useStdout();
5402
5462
  const [termRows, setTermRows] = useState(stdout?.rows ?? 24);
5463
+ const [termCols, setTermCols] = useState(stdout?.columns ?? 80);
5403
5464
  useEffect(() => {
5404
- const onResize = () => setTermRows(process.stdout.rows ?? 24);
5465
+ const onResize = () => {
5466
+ setTermRows(process.stdout.rows ?? 24);
5467
+ setTermCols(process.stdout.columns ?? 80);
5468
+ };
5405
5469
  process.stdout.on("resize", onResize);
5406
5470
  return () => {
5407
5471
  process.stdout.off("resize", onResize);
@@ -5551,7 +5615,7 @@ function App({
5551
5615
  if (vp !== s2.viewportRows) {
5552
5616
  dispatch({ type: "setViewportRows", rows: vp });
5553
5617
  }
5554
- });
5618
+ }, [managedLive, termRows]);
5555
5619
  const handleKeyRef = useRef(null);
5556
5620
  const pendingClickConfirmRef = useRef(null);
5557
5621
  const openModelPickerRef = useRef(null);
@@ -5599,7 +5663,7 @@ function App({
5599
5663
  }
5600
5664
  return;
5601
5665
  }
5602
- const cols = process.stdout.columns ?? 0;
5666
+ const cols = termCols || 80;
5603
5667
  const onScrollbar = cols > 0 && ev.x >= cols - 2 && ev.y >= 1 && ev.y <= rows;
5604
5668
  if (onScrollbar && s2.totalLines > rows) {
5605
5669
  scrollbarDragRef.current = true;
@@ -5687,7 +5751,7 @@ function App({
5687
5751
  if (!picker || picker.count === 0) {
5688
5752
  const inputDisabled = s2.status === "aborting" && !s2.steeringPending;
5689
5753
  if (!inputDisabled) {
5690
- const cols = process.stdout.columns ?? 80;
5754
+ const cols = termCols || 80;
5691
5755
  const inputTop = s2.viewportRows + affordance + liveStripRowsRef.current + 1;
5692
5756
  const inputRows = layoutInputRows(INPUT_PROMPT, s2.buffer, s2.cursor, cols).length;
5693
5757
  const rowIdx = ev.y - inputTop;
@@ -5730,7 +5794,8 @@ function App({
5730
5794
  }
5731
5795
  },
5732
5796
  // dispatch is stable (useReducer); refs are mutable — no reactive deps.
5733
- []
5797
+ // termCols is stable (useState + resize effect).
5798
+ [termCols]
5734
5799
  );
5735
5800
  useEffect(() => {
5736
5801
  if (!subscribeMouse) return;
@@ -5899,12 +5964,12 @@ function App({
5899
5964
  }, [agent.ctx.meta]);
5900
5965
  const prevAnyOverlayOpen = useRef(false);
5901
5966
  const prevEntriesCount = useRef(0);
5902
- const eraseLiveRegion = () => {
5967
+ const eraseLiveRegion = useCallback(() => {
5903
5968
  try {
5904
5969
  process.stdout.write("\x1B[J");
5905
5970
  } catch {
5906
5971
  }
5907
- };
5972
+ }, []);
5908
5973
  useEffect(() => {
5909
5974
  const anyOpenNow = state.picker.open || state.slashPicker.open || state.modelPicker.open || state.autonomyPicker.open || state.settingsPicker.open || state.confirmQueue.length > 0;
5910
5975
  const overlayClosed = prevAnyOverlayOpen.current && !anyOpenNow;
@@ -5921,7 +5986,8 @@ function App({
5921
5986
  state.autonomyPicker.open,
5922
5987
  state.settingsPicker.open,
5923
5988
  state.confirmQueue.length,
5924
- state.entries.length
5989
+ state.entries.length,
5990
+ eraseLiveRegion
5925
5991
  ]);
5926
5992
  useEffect(() => {
5927
5993
  const handleResize = () => eraseLiveRegion();
@@ -5929,7 +5995,7 @@ function App({
5929
5995
  return () => {
5930
5996
  process.stdout.off("resize", handleResize);
5931
5997
  };
5932
- }, []);
5998
+ }, [eraseLiveRegion]);
5933
5999
  useEffect(() => {
5934
6000
  const detected = detectAtToken(state.buffer, state.cursor);
5935
6001
  if (!detected) {
@@ -7903,6 +7969,22 @@ function App({
7903
7969
  }
7904
7970
  });
7905
7971
  }
7972
+ if (result.status === "done" && predictNext) {
7973
+ try {
7974
+ const userRequest = blocks.filter((b) => b.type === "text").map((b) => b.text).join(" ").trim();
7975
+ const predictions = await predictNext({
7976
+ userRequest,
7977
+ assistantSummary: result.finalText ?? ""
7978
+ });
7979
+ if (predictions.length > 0) {
7980
+ const text = ["\u21B3 likely next:", ...predictions.map((p, i) => ` ${i + 1}. ${p}`)].join(
7981
+ "\n"
7982
+ );
7983
+ dispatch({ type: "addEntry", entry: { kind: "turn-summary", text } });
7984
+ }
7985
+ } catch {
7986
+ }
7987
+ }
7906
7988
  } catch (err) {
7907
7989
  dispatch({
7908
7990
  type: "addEntry",
@@ -8546,8 +8628,48 @@ async function runTui(opts) {
8546
8628
  let inkStdin = stdin;
8547
8629
  let detachMouse = null;
8548
8630
  if (useMouse) {
8549
- const proxy = new PassThrough();
8550
- const p = proxy;
8631
+ class KeyboardReadable extends Readable {
8632
+ pendingChunks = [];
8633
+ // eslint-disable-next-line no-useless-constructor
8634
+ constructor() {
8635
+ super({ encoding: "utf8", highWaterMark: 64 * 1024 });
8636
+ }
8637
+ _read(_size) {
8638
+ this.flushPending();
8639
+ }
8640
+ flushPending() {
8641
+ while (this.pendingChunks.length > 0) {
8642
+ const chunk = this.pendingChunks[0];
8643
+ const ok = this.push(chunk);
8644
+ this.pendingChunks.shift();
8645
+ if (!ok) {
8646
+ break;
8647
+ }
8648
+ }
8649
+ }
8650
+ /** Called by the stdin data handler when keyboard bytes are available. */
8651
+ doPush(chunk) {
8652
+ if (chunk.length === 0) return;
8653
+ const ok = this.push(chunk);
8654
+ if (ok) {
8655
+ if (this.pendingChunks.length > 0) {
8656
+ this.flushPending();
8657
+ }
8658
+ } else {
8659
+ if (this.pendingChunks.length >= 100) {
8660
+ this.pendingChunks.shift();
8661
+ }
8662
+ this.pendingChunks.push(chunk);
8663
+ }
8664
+ }
8665
+ /** Called on shutdown so the stream closes cleanly. */
8666
+ doEnd() {
8667
+ this.pendingChunks = [];
8668
+ this.push(null);
8669
+ }
8670
+ }
8671
+ const keyboardStream = new KeyboardReadable();
8672
+ const p = keyboardStream;
8551
8673
  p.isTTY = true;
8552
8674
  p.setRawMode = (mode) => {
8553
8675
  try {
@@ -8578,10 +8700,13 @@ async function runTui(opts) {
8578
8700
  }
8579
8701
  }
8580
8702
  const rest = stripSgrMouse(chunk);
8581
- if (rest.length > 0) proxy.write(rest);
8703
+ keyboardStream.doPush(rest);
8582
8704
  };
8583
8705
  stdin.on("data", onData);
8584
- detachMouse = () => stdin.off("data", onData);
8706
+ detachMouse = () => {
8707
+ stdin.off("data", onData);
8708
+ keyboardStream.doEnd();
8709
+ };
8585
8710
  inkStdin = p;
8586
8711
  }
8587
8712
  const subscribeMouse = useMouse ? (fn) => {
@@ -8700,6 +8825,7 @@ async function runTui(opts) {
8700
8825
  projectRoot: opts.projectRoot,
8701
8826
  getSettings: opts.getSettings,
8702
8827
  saveSettings: opts.saveSettings,
8828
+ predictNext: opts.predictNext,
8703
8829
  mouse: useMouse,
8704
8830
  subscribeMouse,
8705
8831
  // Managed viewport (in-app scroll + collapsibility) follows