@wrongstack/tui 0.10.3 → 0.31.1

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';
@@ -554,11 +554,12 @@ function FleetMonitor({
554
554
  events.push({ at: e.startedAt, icon: "\u25CF", color: "cyan", text: `${e.name} spawned` });
555
555
  if (e.status !== "running" && e.status !== "idle") {
556
556
  const s2 = STATUS[e.status];
557
+ const reason = e.failureReason ? ` [${e.failureReason}]` : "";
557
558
  events.push({
558
559
  at: e.lastEventAt,
559
560
  icon: s2.icon,
560
561
  color: s2.color,
561
- text: `${e.name} ${e.status} (${e.toolCalls}t)`
562
+ text: `${e.name} ${e.status} (${e.toolCalls}t)${reason}`
562
563
  });
563
564
  }
564
565
  if (e.budgetWarning) {
@@ -873,6 +874,10 @@ function AgentsMonitor({
873
874
  e.budgetWarning.limit,
874
875
  " \u2014 extending"
875
876
  ] }) }) : null,
877
+ e.failureReason && e.status !== "success" ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
878
+ "\u2717 ",
879
+ e.failureReason
880
+ ] }) }) : null,
876
881
  e.ctxPct !== void 0 ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, children: [
877
882
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "ctx " }),
878
883
  /* @__PURE__ */ jsx(ContextBar, { pct: e.ctxPct, tokens: e.ctxTokens, maxTokens: e.ctxMaxTokens })
@@ -882,21 +887,58 @@ function AgentsMonitor({
882
887
  ] });
883
888
  }
884
889
  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" }
890
+ {
891
+ mode: "off",
892
+ label: "OFF",
893
+ description: "Agent stops after each turn (normal interactive mode)",
894
+ color: "green"
895
+ },
896
+ {
897
+ mode: "suggest",
898
+ label: "SUGGEST",
899
+ description: "Shows next-step suggestions after each turn",
900
+ color: "cyan"
901
+ },
902
+ {
903
+ mode: "auto",
904
+ label: "AUTO",
905
+ description: "Self-driving \u2014 agent picks next step and continues",
906
+ color: "yellow"
907
+ },
908
+ {
909
+ mode: "eternal",
910
+ label: "ETERNAL",
911
+ description: "Goal-driven loop \u2014 requires /goal set first",
912
+ color: "red"
913
+ },
914
+ {
915
+ mode: "eternal-parallel",
916
+ label: "PARALLEL",
917
+ description: "Fan-out 4\u20138 subagents per tick \u2014 requires /goal",
918
+ color: "magenta"
919
+ }
890
920
  ];
891
- function AutonomyPicker({ options, selected, hint }) {
921
+ function AutonomyPicker({
922
+ options,
923
+ selected,
924
+ hint
925
+ }) {
892
926
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
893
927
  /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u2501\u2501 Autonomy Mode \u2501\u2501" }),
894
928
  /* @__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)),
929
+ options.map((opt, i) => /* @__PURE__ */ jsxs(
930
+ Text,
931
+ {
932
+ color: i === selected ? opt.color : void 0,
933
+ inverse: i === selected,
934
+ children: [
935
+ i === selected ? "\u203A " : " ",
936
+ /* @__PURE__ */ jsx(Text, { bold: true, children: opt.label.padEnd(12) }),
937
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: opt.description })
938
+ ]
939
+ },
940
+ opt.mode
941
+ )),
900
942
  hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
901
943
  ] });
902
944
  }
@@ -934,7 +976,8 @@ function CheckpointTimeline({
934
976
  new Date(cp.ts).toLocaleTimeString()
935
977
  ] }),
936
978
  cp.fileCount > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
937
- " \xB7 ",
979
+ " ",
980
+ "\xB7 ",
938
981
  cp.fileCount,
939
982
  " file",
940
983
  cp.fileCount !== 1 ? "s" : ""
@@ -1055,7 +1098,11 @@ function FilePicker({ query, matches, selected }) {
1055
1098
  function highlight(path3, _query) {
1056
1099
  return path3;
1057
1100
  }
1058
- function FleetPanel({ entries, totalCost, collabSession }) {
1101
+ function FleetPanel({
1102
+ entries,
1103
+ totalCost,
1104
+ collabSession
1105
+ }) {
1059
1106
  const { stdout } = useStdout();
1060
1107
  const [termWidth, setTermWidth] = useState(stdout?.columns ?? 90);
1061
1108
  useEffect(() => {
@@ -1077,8 +1124,8 @@ function FleetPanel({ entries, totalCost, collabSession }) {
1077
1124
  const costLabel = totalCost > 0 ? ` \xB7 ${totalCost.toFixed(3)}` : "";
1078
1125
  const collabLabel = hasCollab && collabSession.sessionId ? ` \xB7 collab(${collabSession.bugCount}b/${collabSession.planCount}p/${collabSession.evalCount}e)` : "";
1079
1126
  const summaryLine = runningCount > 0 ? `${runningCount} running${costLabel}${collabLabel}` : `idle${costLabel}${collabLabel}`;
1080
- const shown = running.slice(0, 3);
1081
- const overflow = running.length > 3 ? running.length - 3 : 0;
1127
+ const shown = running.slice(0, 5);
1128
+ const overflow = running.length > 5 ? running.length - 5 : 0;
1082
1129
  const leaderTool = hasCollab ? "waiting for agents" : leader?.currentTool?.name ?? (leader?.status === "running" ? "running" : "\u2014");
1083
1130
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1084
1131
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
@@ -1102,13 +1149,17 @@ function FleetPanel({ entries, totalCost, collabSession }) {
1102
1149
  /* @__PURE__ */ jsx(Text, { color: "cyan", children: tool })
1103
1150
  ] }, entry.id);
1104
1151
  }),
1105
- overflow > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1106
- " ",
1107
- "+",
1108
- overflow,
1109
- ": ",
1110
- running[3]?.name?.slice(0, nameMaxLen - 2) ?? "agent",
1111
- "\u2026"
1152
+ overflow > 0 ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
1153
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1154
+ "+",
1155
+ overflow,
1156
+ ":"
1157
+ ] }),
1158
+ running.slice(5, 7).map((e) => /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1159
+ e.name?.split(" ")[0]?.slice(0, nameMaxLen - 2) ?? "agent",
1160
+ ","
1161
+ ] }, e.id)),
1162
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2026" })
1112
1163
  ] }) : null
1113
1164
  ] });
1114
1165
  }
@@ -1580,7 +1631,8 @@ function tokenizePython(line, carry) {
1580
1631
  }
1581
1632
  function tokenizeDiff(line) {
1582
1633
  if (line.startsWith("@@")) return [{ text: line, color: C.diffMeta }];
1583
- if (line.startsWith("+++") || line.startsWith("---")) return [{ text: line, color: C.diffMeta, dim: true }];
1634
+ if (line.startsWith("+++") || line.startsWith("---"))
1635
+ return [{ text: line, color: C.diffMeta, dim: true }];
1584
1636
  if (line.startsWith("+")) return [{ text: line, color: C.diffAdd }];
1585
1637
  if (line.startsWith("-")) return [{ text: line, color: C.diffDel }];
1586
1638
  return [{ text: line, dim: true }];
@@ -1996,7 +2048,7 @@ function MarkdownView({
1996
2048
  if (quote && line.startsWith(">")) {
1997
2049
  rows.push(
1998
2050
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
1999
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502 " }),
2051
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
2000
2052
  /* @__PURE__ */ jsx(InlineLine, { tokens: parseInline(quote[1] ?? ""), dim: true })
2001
2053
  ] }, `q${key++}`)
2002
2054
  );
@@ -2121,13 +2173,13 @@ function splitFencedBlocks(text) {
2121
2173
  function CodeBlock({
2122
2174
  code,
2123
2175
  lang,
2124
- termWidth
2176
+ contentWidth
2125
2177
  }) {
2126
2178
  let lines = code.replace(/\n+$/, "").split("\n");
2127
2179
  const hidden = Math.max(0, lines.length - MAX_CODE_LINES);
2128
2180
  if (hidden > 0) lines = lines.slice(0, MAX_CODE_LINES);
2129
2181
  const gutterW = String(lines.length).length;
2130
- const maxW = Math.max(20, Math.min(termWidth - 8 - gutterW - 1, 120));
2182
+ const maxW = Math.max(20, Math.min(contentWidth - 6 - gutterW - 1, 120));
2131
2183
  let carry = {};
2132
2184
  const rows = lines.map((raw) => {
2133
2185
  const display = raw.length > maxW ? `${raw.slice(0, maxW - 1)}\u2026` : raw;
@@ -2135,20 +2187,31 @@ function CodeBlock({
2135
2187
  carry = r.carry;
2136
2188
  return r.tokens;
2137
2189
  });
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
- ] });
2190
+ return /* @__PURE__ */ jsxs(
2191
+ Box,
2192
+ {
2193
+ flexDirection: "column",
2194
+ marginLeft: 2,
2195
+ marginY: 0,
2196
+ borderStyle: "round",
2197
+ borderColor: theme.borderDefault,
2198
+ paddingX: 1,
2199
+ children: [
2200
+ lang !== "plain" ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: lang }) : null,
2201
+ rows.map((tokens, i) => (
2202
+ // biome-ignore lint/suspicious/noArrayIndexKey: code lines are positional
2203
+ /* @__PURE__ */ jsxs(Text, { children: [
2204
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${String(i + 1).padStart(gutterW, " ")} ` }),
2205
+ tokens.length === 0 ? " " : tokens.map((t, j) => (
2206
+ // biome-ignore lint/suspicious/noArrayIndexKey: token order is stable per line
2207
+ /* @__PURE__ */ jsx(Text, { color: t.color, dimColor: t.dim, bold: t.bold, children: t.text }, j)
2208
+ ))
2209
+ ] }, i)
2210
+ )),
2211
+ hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: `\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
2212
+ ]
2213
+ }
2214
+ );
2152
2215
  }
2153
2216
  function AssistantBody({
2154
2217
  text,
@@ -2160,7 +2223,7 @@ function AssistantBody({
2160
2223
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: segments.map(
2161
2224
  (seg, i) => seg.type === "code" ? (
2162
2225
  // biome-ignore lint/suspicious/noArrayIndexKey: segment order is stable
2163
- /* @__PURE__ */ jsx(CodeBlock, { code: seg.text, lang: seg.lang ?? "plain", termWidth }, i)
2226
+ /* @__PURE__ */ jsx(CodeBlock, { code: seg.text, lang: seg.lang ?? "plain", contentWidth: inner }, i)
2164
2227
  ) : (
2165
2228
  // biome-ignore lint/suspicious/noArrayIndexKey: segment order is stable
2166
2229
  /* @__PURE__ */ jsx(MarkdownView, { text: seg.text, termWidth: inner }, i)
@@ -2289,14 +2352,7 @@ var Entry = React4.memo(function Entry2({
2289
2352
  paddingLeft: 1,
2290
2353
  children: [
2291
2354
  /* @__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
- )
2355
+ /* @__PURE__ */ jsx(AssistantBody, { text: entry.text, termWidth, contentWidth })
2300
2356
  ]
2301
2357
  }
2302
2358
  );
@@ -3411,8 +3467,10 @@ function fmtRecentTool(tool) {
3411
3467
  const name = tool.name.length > 18 ? `${tool.name.slice(0, 17)}...` : tool.name;
3412
3468
  const parts = [status, name];
3413
3469
  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`);
3470
+ if (typeof tool.outputBytes === "number" && tool.outputBytes > 0)
3471
+ parts.push(fmtBytes2(tool.outputBytes));
3472
+ if (typeof tool.outputLines === "number" && tool.outputLines > 0)
3473
+ parts.push(`${tool.outputLines}L`);
3416
3474
  return parts.join(" ");
3417
3475
  }
3418
3476
  function fmtRecentMessage(message) {
@@ -3545,7 +3603,9 @@ function PhaseMonitor({
3545
3603
  if (key.escape) onClose();
3546
3604
  });
3547
3605
  const phaseList = Object.values(phases);
3548
- const running = phaseList.filter((p) => runningPhaseIds.includes(Object.keys(phases).find((k) => phases[k] === p) ?? ""));
3606
+ const running = phaseList.filter(
3607
+ (p) => runningPhaseIds.includes(Object.keys(phases).find((k) => phases[k] === p) ?? "")
3608
+ );
3549
3609
  const done = phaseList.filter((p) => p.status === "completed" || p.status === "skipped");
3550
3610
  const failed = phaseList.filter((p) => p.status === "failed");
3551
3611
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
@@ -3756,7 +3816,7 @@ function ScrollableHistory({
3756
3816
  lastReported.current = height;
3757
3817
  onMeasure(height);
3758
3818
  }
3759
- });
3819
+ }, [onMeasure]);
3760
3820
  const vp = Math.max(1, viewportRows);
3761
3821
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
3762
3822
  /* @__PURE__ */ jsx(
@@ -3992,7 +4052,9 @@ function WorktreePanel({
3992
4052
  }) {
3993
4053
  const list = Object.values(worktrees);
3994
4054
  if (list.length === 0) return null;
3995
- const active = list.filter((w) => w.status === "active" || w.status === "committing" || w.status === "merging").length;
4055
+ const active = list.filter(
4056
+ (w) => w.status === "active" || w.status === "committing" || w.status === "merging"
4057
+ ).length;
3996
4058
  const merged = list.filter((w) => w.status === "merged").length;
3997
4059
  const failed = list.filter((w) => w.status === "failed" || w.status === "needs-review").length;
3998
4060
  return /* @__PURE__ */ jsxs(
@@ -4036,7 +4098,8 @@ function WorktreePanel({
4036
4098
  /* @__PURE__ */ jsx(Text, { children: w.branch.replace(/^wstack\/ap\//, "").slice(0, 18).padEnd(18) }),
4037
4099
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: w.ownerLabel.slice(0, 12) }),
4038
4100
  conflict ? /* @__PURE__ */ jsx(Text, { color: "magenta", children: " CONFLICT" }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
4039
- " +",
4101
+ " ",
4102
+ "+",
4040
4103
  w.insertions,
4041
4104
  "/-",
4042
4105
  w.deletions,
@@ -4200,13 +4263,18 @@ function createKillSlashCommand() {
4200
4263
  if (sub === "all") {
4201
4264
  const pids = getProcessRegistry().killAll();
4202
4265
  if (pids.length === 0) return { message: "No processes to kill." };
4203
- return { message: `Killed ${pids.length} process${pids.length === 1 ? "" : "es"}: ${pids.join(", ")}` };
4266
+ return {
4267
+ message: `Killed ${pids.length} process${pids.length === 1 ? "" : "es"}: ${pids.join(", ")}`
4268
+ };
4204
4269
  }
4205
4270
  if (sub === "force") {
4206
4271
  getProcessRegistry().forceBreakerOpen();
4207
4272
  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(", ")}` };
4273
+ if (pids.length === 0)
4274
+ return { message: "Circuit breaker forced open. No processes to kill." };
4275
+ return {
4276
+ message: `Force-killed ${pids.length} process${pids.length === 1 ? "" : "es"}: ${pids.join(", ")}`
4277
+ };
4210
4278
  }
4211
4279
  if (sub === "reset") {
4212
4280
  getProcessRegistry().forceBreakerReset();
@@ -4735,10 +4803,24 @@ function reducer(state, action) {
4735
4803
  return { ...state, fleet: seeded, fleetCost: action.cost };
4736
4804
  }
4737
4805
  case "fleetSpawn": {
4738
- if (state.fleet[action.id]) return state;
4806
+ const existing = state.fleet[action.id];
4807
+ const incomingName = action.name ?? action.id.slice(0, 8);
4808
+ const isPlaceholderName = (name) => name === "adhoc" || name === "subagent" || name === "generic" || name.startsWith("slot-") || name === action.id.slice(0, 8);
4809
+ if (existing) {
4810
+ if (isPlaceholderName(existing.name) && !isPlaceholderName(incomingName) && incomingName !== existing.name) {
4811
+ return {
4812
+ ...state,
4813
+ fleet: {
4814
+ ...state.fleet,
4815
+ [action.id]: { ...existing, name: incomingName }
4816
+ }
4817
+ };
4818
+ }
4819
+ return state;
4820
+ }
4739
4821
  const entry = {
4740
4822
  id: action.id,
4741
- name: action.name ?? action.id.slice(0, 8),
4823
+ name: incomingName,
4742
4824
  provider: action.provider,
4743
4825
  model: action.model,
4744
4826
  status: "idle",
@@ -4876,7 +4958,8 @@ function reducer(state, action) {
4876
4958
  currentTool: void 0,
4877
4959
  budgetWarning: void 0,
4878
4960
  // clear on done/restart
4879
- lastEventAt: Date.now()
4961
+ lastEventAt: Date.now(),
4962
+ failureReason: action.failureReason
4880
4963
  }
4881
4964
  }
4882
4965
  };
@@ -5375,6 +5458,7 @@ function App({
5375
5458
  switchProviderAndModel,
5376
5459
  getSettings,
5377
5460
  saveSettings,
5461
+ predictNext,
5378
5462
  switchAutonomy,
5379
5463
  effectiveMaxContext,
5380
5464
  onExit,
@@ -5400,8 +5484,12 @@ function App({
5400
5484
  const [hiddenItems, setHiddenItems] = useState(statuslineHiddenItems);
5401
5485
  const { stdout } = useStdout();
5402
5486
  const [termRows, setTermRows] = useState(stdout?.rows ?? 24);
5487
+ const [termCols, setTermCols] = useState(stdout?.columns ?? 80);
5403
5488
  useEffect(() => {
5404
- const onResize = () => setTermRows(process.stdout.rows ?? 24);
5489
+ const onResize = () => {
5490
+ setTermRows(process.stdout.rows ?? 24);
5491
+ setTermCols(process.stdout.columns ?? 80);
5492
+ };
5405
5493
  process.stdout.on("resize", onResize);
5406
5494
  return () => {
5407
5495
  process.stdout.off("resize", onResize);
@@ -5551,7 +5639,7 @@ function App({
5551
5639
  if (vp !== s2.viewportRows) {
5552
5640
  dispatch({ type: "setViewportRows", rows: vp });
5553
5641
  }
5554
- });
5642
+ }, [managedLive, termRows]);
5555
5643
  const handleKeyRef = useRef(null);
5556
5644
  const pendingClickConfirmRef = useRef(null);
5557
5645
  const openModelPickerRef = useRef(null);
@@ -5599,7 +5687,7 @@ function App({
5599
5687
  }
5600
5688
  return;
5601
5689
  }
5602
- const cols = process.stdout.columns ?? 0;
5690
+ const cols = termCols || 80;
5603
5691
  const onScrollbar = cols > 0 && ev.x >= cols - 2 && ev.y >= 1 && ev.y <= rows;
5604
5692
  if (onScrollbar && s2.totalLines > rows) {
5605
5693
  scrollbarDragRef.current = true;
@@ -5687,7 +5775,7 @@ function App({
5687
5775
  if (!picker || picker.count === 0) {
5688
5776
  const inputDisabled = s2.status === "aborting" && !s2.steeringPending;
5689
5777
  if (!inputDisabled) {
5690
- const cols = process.stdout.columns ?? 80;
5778
+ const cols = termCols || 80;
5691
5779
  const inputTop = s2.viewportRows + affordance + liveStripRowsRef.current + 1;
5692
5780
  const inputRows = layoutInputRows(INPUT_PROMPT, s2.buffer, s2.cursor, cols).length;
5693
5781
  const rowIdx = ev.y - inputTop;
@@ -5730,7 +5818,8 @@ function App({
5730
5818
  }
5731
5819
  },
5732
5820
  // dispatch is stable (useReducer); refs are mutable — no reactive deps.
5733
- []
5821
+ // termCols is stable (useState + resize effect).
5822
+ [termCols]
5734
5823
  );
5735
5824
  useEffect(() => {
5736
5825
  if (!subscribeMouse) return;
@@ -5899,12 +5988,12 @@ function App({
5899
5988
  }, [agent.ctx.meta]);
5900
5989
  const prevAnyOverlayOpen = useRef(false);
5901
5990
  const prevEntriesCount = useRef(0);
5902
- const eraseLiveRegion = () => {
5991
+ const eraseLiveRegion = useCallback(() => {
5903
5992
  try {
5904
5993
  process.stdout.write("\x1B[J");
5905
5994
  } catch {
5906
5995
  }
5907
- };
5996
+ }, []);
5908
5997
  useEffect(() => {
5909
5998
  const anyOpenNow = state.picker.open || state.slashPicker.open || state.modelPicker.open || state.autonomyPicker.open || state.settingsPicker.open || state.confirmQueue.length > 0;
5910
5999
  const overlayClosed = prevAnyOverlayOpen.current && !anyOpenNow;
@@ -5921,7 +6010,8 @@ function App({
5921
6010
  state.autonomyPicker.open,
5922
6011
  state.settingsPicker.open,
5923
6012
  state.confirmQueue.length,
5924
- state.entries.length
6013
+ state.entries.length,
6014
+ eraseLiveRegion
5925
6015
  ]);
5926
6016
  useEffect(() => {
5927
6017
  const handleResize = () => eraseLiveRegion();
@@ -5929,7 +6019,7 @@ function App({
5929
6019
  return () => {
5930
6020
  process.stdout.off("resize", handleResize);
5931
6021
  };
5932
- }, []);
6022
+ }, [eraseLiveRegion]);
5933
6023
  useEffect(() => {
5934
6024
  const detected = detectAtToken(state.buffer, state.cursor);
5935
6025
  if (!detected) {
@@ -6534,15 +6624,16 @@ function App({
6534
6624
  });
6535
6625
  const offCompleted = events.on("subagent.task_completed", (e) => {
6536
6626
  const lbl = labelFor(e.subagentId);
6627
+ const errKind = e.error?.kind;
6537
6628
  dispatch({
6538
6629
  type: "fleetDone",
6539
6630
  id: e.subagentId,
6540
6631
  status: e.status,
6541
6632
  iterations: e.iterations,
6542
- toolCalls: e.toolCalls
6633
+ toolCalls: e.toolCalls,
6634
+ failureReason: errKind
6543
6635
  });
6544
6636
  const icon = e.status === "success" ? "\u2713" : e.status === "timeout" ? "\u23F1" : e.status === "stopped" ? "\u2298" : "\u2717";
6545
- const errKind = e.error?.kind;
6546
6637
  const errMsg = e.error?.message;
6547
6638
  const errMsgTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 100)}${errMsg.length > 100 ? "\u2026" : ""}` : "";
6548
6639
  const errChip = errKind ? ` [${errKind}]` : "";
@@ -7903,6 +7994,22 @@ function App({
7903
7994
  }
7904
7995
  });
7905
7996
  }
7997
+ if (result.status === "done" && predictNext) {
7998
+ try {
7999
+ const userRequest = blocks.filter((b) => b.type === "text").map((b) => b.text).join(" ").trim();
8000
+ const predictions = await predictNext({
8001
+ userRequest,
8002
+ assistantSummary: result.finalText ?? ""
8003
+ });
8004
+ if (predictions.length > 0) {
8005
+ const text = ["\u21B3 likely next:", ...predictions.map((p, i) => ` ${i + 1}. ${p}`)].join(
8006
+ "\n"
8007
+ );
8008
+ dispatch({ type: "addEntry", entry: { kind: "turn-summary", text } });
8009
+ }
8010
+ } catch {
8011
+ }
8012
+ }
7906
8013
  } catch (err) {
7907
8014
  dispatch({
7908
8015
  type: "addEntry",
@@ -8546,8 +8653,48 @@ async function runTui(opts) {
8546
8653
  let inkStdin = stdin;
8547
8654
  let detachMouse = null;
8548
8655
  if (useMouse) {
8549
- const proxy = new PassThrough();
8550
- const p = proxy;
8656
+ class KeyboardReadable extends Readable {
8657
+ pendingChunks = [];
8658
+ // eslint-disable-next-line no-useless-constructor
8659
+ constructor() {
8660
+ super({ encoding: "utf8", highWaterMark: 64 * 1024 });
8661
+ }
8662
+ _read(_size) {
8663
+ this.flushPending();
8664
+ }
8665
+ flushPending() {
8666
+ while (this.pendingChunks.length > 0) {
8667
+ const chunk = this.pendingChunks[0];
8668
+ const ok = this.push(chunk);
8669
+ this.pendingChunks.shift();
8670
+ if (!ok) {
8671
+ break;
8672
+ }
8673
+ }
8674
+ }
8675
+ /** Called by the stdin data handler when keyboard bytes are available. */
8676
+ doPush(chunk) {
8677
+ if (chunk.length === 0) return;
8678
+ const ok = this.push(chunk);
8679
+ if (ok) {
8680
+ if (this.pendingChunks.length > 0) {
8681
+ this.flushPending();
8682
+ }
8683
+ } else {
8684
+ if (this.pendingChunks.length >= 100) {
8685
+ this.pendingChunks.shift();
8686
+ }
8687
+ this.pendingChunks.push(chunk);
8688
+ }
8689
+ }
8690
+ /** Called on shutdown so the stream closes cleanly. */
8691
+ doEnd() {
8692
+ this.pendingChunks = [];
8693
+ this.push(null);
8694
+ }
8695
+ }
8696
+ const keyboardStream = new KeyboardReadable();
8697
+ const p = keyboardStream;
8551
8698
  p.isTTY = true;
8552
8699
  p.setRawMode = (mode) => {
8553
8700
  try {
@@ -8578,10 +8725,13 @@ async function runTui(opts) {
8578
8725
  }
8579
8726
  }
8580
8727
  const rest = stripSgrMouse(chunk);
8581
- if (rest.length > 0) proxy.write(rest);
8728
+ keyboardStream.doPush(rest);
8582
8729
  };
8583
8730
  stdin.on("data", onData);
8584
- detachMouse = () => stdin.off("data", onData);
8731
+ detachMouse = () => {
8732
+ stdin.off("data", onData);
8733
+ keyboardStream.doEnd();
8734
+ };
8585
8735
  inkStdin = p;
8586
8736
  }
8587
8737
  const subscribeMouse = useMouse ? (fn) => {
@@ -8700,6 +8850,7 @@ async function runTui(opts) {
8700
8850
  projectRoot: opts.projectRoot,
8701
8851
  getSettings: opts.getSettings,
8702
8852
  saveSettings: opts.saveSettings,
8853
+ predictNext: opts.predictNext,
8703
8854
  mouse: useMouse,
8704
8855
  subscribeMouse,
8705
8856
  // Managed viewport (in-app scroll + collapsibility) follows