reasonix 0.3.0-alpha.6 → 0.3.2

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/cli/index.js CHANGED
@@ -684,6 +684,16 @@ var AppendOnlyLog = class {
684
684
  extend(messages) {
685
685
  for (const m of messages) this.append(m);
686
686
  }
687
+ /**
688
+ * Bulk-replace entries. Intentionally named to be hard to reach for —
689
+ * this is the one mutation path that breaks the log's append-only
690
+ * spirit, reserved for compaction flows (`/compact`) and recovery
691
+ * where the caller has consciously decided to drop old history. Any
692
+ * other use is almost certainly wrong; append() is what you want.
693
+ */
694
+ compactInPlace(replacement) {
695
+ this._entries = [...replacement];
696
+ }
687
697
  get entries() {
688
698
  return this._entries;
689
699
  }
@@ -962,7 +972,8 @@ import {
962
972
  readFileSync as readFileSync2,
963
973
  readdirSync,
964
974
  statSync,
965
- unlinkSync
975
+ unlinkSync,
976
+ writeFileSync as writeFileSync2
966
977
  } from "fs";
967
978
  import { homedir as homedir2 } from "os";
968
979
  import { dirname as dirname2, join as join2 } from "path";
@@ -1031,6 +1042,17 @@ function deleteSession(name) {
1031
1042
  return false;
1032
1043
  }
1033
1044
  }
1045
+ function rewriteSession(name, messages) {
1046
+ const path = sessionPath(name);
1047
+ mkdirSync2(dirname2(path), { recursive: true });
1048
+ const body = messages.map((m) => JSON.stringify(m)).join("\n");
1049
+ writeFileSync2(path, body ? `${body}
1050
+ ` : "", "utf8");
1051
+ try {
1052
+ chmodSync2(path, 384);
1053
+ } catch {
1054
+ }
1055
+ }
1034
1056
  function countLines(path) {
1035
1057
  try {
1036
1058
  const raw = readFileSync2(path, "utf8");
@@ -1046,6 +1068,11 @@ var DEEPSEEK_PRICING = {
1046
1068
  "deepseek-reasoner": { inputCacheHit: 0.14, inputCacheMiss: 0.55, output: 2.19 }
1047
1069
  };
1048
1070
  var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
1071
+ var DEEPSEEK_CONTEXT_TOKENS = {
1072
+ "deepseek-chat": 131072,
1073
+ "deepseek-reasoner": 131072
1074
+ };
1075
+ var DEFAULT_CONTEXT_TOKENS = 131072;
1049
1076
  function costUsd(model, usage) {
1050
1077
  const p = DEEPSEEK_PRICING[model];
1051
1078
  if (!p) return 0;
@@ -1089,12 +1116,14 @@ var SessionStats = class {
1089
1116
  return denom > 0 ? hit / denom : 0;
1090
1117
  }
1091
1118
  summary() {
1119
+ const last = this.turns[this.turns.length - 1];
1092
1120
  return {
1093
1121
  turns: this.turns.length,
1094
1122
  totalCostUsd: round(this.totalCost, 6),
1095
1123
  claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
1096
1124
  savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
1097
- cacheHitRatio: round(this.aggregateCacheHitRatio, 4)
1125
+ cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
1126
+ lastPromptTokens: last?.usage.promptTokens ?? 0
1098
1127
  };
1099
1128
  }
1100
1129
  };
@@ -1126,12 +1155,18 @@ var CacheFirstLoop = class {
1126
1155
  resumedMessageCount;
1127
1156
  _turn = 0;
1128
1157
  _streamPreference;
1158
+ /**
1159
+ * Set by {@link abort} to short-circuit the tool-call loop after the
1160
+ * current iteration. Reset at the start of each `step()` so an Esc
1161
+ * during one turn doesn't poison the next.
1162
+ */
1163
+ _aborted = false;
1129
1164
  constructor(opts) {
1130
1165
  this.client = opts.client;
1131
1166
  this.prefix = opts.prefix;
1132
1167
  this.tools = opts.tools ?? new ToolRegistry();
1133
1168
  this.model = opts.model ?? "deepseek-chat";
1134
- this.maxToolIters = opts.maxToolIters ?? 8;
1169
+ this.maxToolIters = opts.maxToolIters ?? 24;
1135
1170
  if (typeof opts.branch === "number") {
1136
1171
  this.branchOptions = { budget: opts.branch };
1137
1172
  } else if (opts.branch && typeof opts.branch === "object") {
@@ -1166,6 +1201,33 @@ var CacheFirstLoop = class {
1166
1201
  this.resumedMessageCount = 0;
1167
1202
  }
1168
1203
  }
1204
+ /**
1205
+ * Shrink the log by re-truncating oversized tool results to a tighter
1206
+ * cap, and persist the result back to disk so the next launch doesn't
1207
+ * re-inherit a fat session file. Returns a summary the TUI can
1208
+ * display.
1209
+ *
1210
+ * Only tool-role messages are touched (same rationale as
1211
+ * {@link healLoadedMessages}). User and assistant messages carry
1212
+ * authored intent we can't mechanically shrink without losing
1213
+ * meaning.
1214
+ */
1215
+ compact(tightCapChars = 4e3) {
1216
+ const before = this.log.toMessages();
1217
+ const { messages, healedCount, healedFrom } = healLoadedMessages(before, tightCapChars);
1218
+ const afterBytes = messages.filter((m) => m.role === "tool").reduce((s, m) => s + (typeof m.content === "string" ? m.content.length : 0), 0);
1219
+ const charsSaved = healedFrom - afterBytes;
1220
+ if (healedCount > 0) {
1221
+ this.log.compactInPlace(messages);
1222
+ if (this.sessionName) {
1223
+ try {
1224
+ rewriteSession(this.sessionName, messages);
1225
+ } catch {
1226
+ }
1227
+ }
1228
+ }
1229
+ return { healedCount, charsSaved };
1230
+ }
1169
1231
  appendAndPersist(message) {
1170
1232
  this.log.append(message);
1171
1233
  if (this.sessionName) {
@@ -1210,12 +1272,42 @@ var CacheFirstLoop = class {
1210
1272
  if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
1211
1273
  return msgs;
1212
1274
  }
1275
+ /**
1276
+ * Signal the currently-running {@link step} that the user wants to
1277
+ * stop exploring. Takes effect at the next iteration boundary — if a
1278
+ * tool call is mid-flight it will be allowed to finish, then the
1279
+ * loop diverts to the forced-summary path so the user gets an
1280
+ * answer instead of a cliff. Called by the TUI on Esc.
1281
+ */
1282
+ abort() {
1283
+ this._aborted = true;
1284
+ }
1213
1285
  async *step(userInput) {
1214
1286
  this._turn++;
1215
1287
  this.scratch.reset();
1288
+ this._aborted = false;
1216
1289
  let pendingUser = userInput;
1217
1290
  const toolSpecs = this.prefix.tools();
1291
+ const warnAt = Math.max(1, Math.floor(this.maxToolIters * 0.7));
1292
+ let warnedForIterBudget = false;
1218
1293
  for (let iter = 0; iter < this.maxToolIters; iter++) {
1294
+ if (this._aborted) {
1295
+ yield {
1296
+ turn: this._turn,
1297
+ role: "warning",
1298
+ content: `aborted at iter ${iter}/${this.maxToolIters} \u2014 forcing summary from what was gathered`
1299
+ };
1300
+ yield* this.forceSummaryAfterIterLimit({ reason: "aborted" });
1301
+ return;
1302
+ }
1303
+ if (!warnedForIterBudget && iter >= warnAt) {
1304
+ warnedForIterBudget = true;
1305
+ yield {
1306
+ turn: this._turn,
1307
+ role: "warning",
1308
+ content: `${iter}/${this.maxToolIters} tool calls used \u2014 approaching budget. Press Esc to force a summary now.`
1309
+ };
1310
+ }
1219
1311
  const messages = this.buildMessages(pendingUser);
1220
1312
  let assistantContent = "";
1221
1313
  let reasoningContent = "";
@@ -1403,7 +1495,40 @@ var CacheFirstLoop = class {
1403
1495
  };
1404
1496
  }
1405
1497
  }
1406
- yield { turn: this._turn, role: "done", content: "[max_tool_iters reached]" };
1498
+ yield* this.forceSummaryAfterIterLimit({ reason: "budget" });
1499
+ }
1500
+ async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
1501
+ try {
1502
+ const messages = this.buildMessages(null);
1503
+ const resp = await this.client.chat({
1504
+ model: this.model,
1505
+ messages
1506
+ // no tools → model is forced to answer in text
1507
+ });
1508
+ const summary = resp.content?.trim() || "(model returned no text; try a narrower question or raise --max-tool-iters)";
1509
+ const reasonPrefix = opts.reason === "aborted" ? "[aborted by user (Esc) \u2014 summarizing what I found so far]" : `[tool-call budget (${this.maxToolIters}) reached \u2014 forcing summary from what I found]`;
1510
+ const annotated = `${reasonPrefix}
1511
+
1512
+ ${summary}`;
1513
+ const summaryStats = this.stats.record(this._turn, this.model, resp.usage ?? new Usage());
1514
+ this.appendAndPersist({ role: "assistant", content: summary });
1515
+ yield {
1516
+ turn: this._turn,
1517
+ role: "assistant_final",
1518
+ content: annotated,
1519
+ stats: summaryStats
1520
+ };
1521
+ yield { turn: this._turn, role: "done", content: summary };
1522
+ } catch (err) {
1523
+ const label = opts.reason === "aborted" ? "aborted by user" : `tool-call budget (${this.maxToolIters}) reached`;
1524
+ yield {
1525
+ turn: this._turn,
1526
+ role: "error",
1527
+ content: "",
1528
+ error: `${label} and the fallback summary call failed: ${err.message}. Run /clear and retry with a narrower question, or raise --max-tool-iters.`
1529
+ };
1530
+ yield { turn: this._turn, role: "done", content: "" };
1531
+ }
1407
1532
  }
1408
1533
  async run(userInput, onEvent) {
1409
1534
  let final = "";
@@ -1644,12 +1769,14 @@ function summarizeTurns(turns) {
1644
1769
  }
1645
1770
  const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
1646
1771
  const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
1772
+ const lastTurn = turns[turns.length - 1];
1647
1773
  return {
1648
1774
  turns: turns.length,
1649
1775
  totalCostUsd: round2(totalCost, 6),
1650
1776
  claudeEquivalentUsd: round2(totalClaude, 6),
1651
1777
  savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
1652
- cacheHitRatio: round2(cacheHitRatio, 4)
1778
+ cacheHitRatio: round2(cacheHitRatio, 4),
1779
+ lastPromptTokens: lastTurn?.usage.promptTokens ?? 0
1653
1780
  };
1654
1781
  }
1655
1782
  function round2(n, digits) {
@@ -2444,14 +2571,14 @@ function parseMcpSpec(input) {
2444
2571
  }
2445
2572
 
2446
2573
  // src/index.ts
2447
- var VERSION = "0.3.0-alpha.6";
2574
+ var VERSION = "0.3.2";
2448
2575
 
2449
2576
  // src/cli/commands/chat.tsx
2450
2577
  import { render } from "ink";
2451
2578
  import React8, { useState as useState4 } from "react";
2452
2579
 
2453
2580
  // src/cli/ui/App.tsx
2454
- import { Box as Box6, Static, Text as Text6, useApp } from "ink";
2581
+ import { Box as Box6, Static, Text as Text6, useApp, useInput } from "ink";
2455
2582
  import React6, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
2456
2583
 
2457
2584
  // src/cli/ui/EventLog.tsx
@@ -2682,6 +2809,9 @@ var EventRow = React3.memo(function EventRow2({ event }) {
2682
2809
  if (event.role === "info") {
2683
2810
  return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, event.text));
2684
2811
  }
2812
+ if (event.role === "warning") {
2813
+ return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "\u25B8 "), /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, event.text));
2814
+ }
2685
2815
  return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, null, event.text));
2686
2816
  });
2687
2817
  function BranchBlock({ branch }) {
@@ -2772,7 +2902,15 @@ function StatsPanel({
2772
2902
  const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
2773
2903
  const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
2774
2904
  const branchOn = (branchBudget ?? 1) > 1;
2775
- return /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React5.createElement(Box5, { justifyContent: "space-between" }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, model), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cache hit "), /* @__PURE__ */ React5.createElement(Text5, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cost "), /* @__PURE__ */ React5.createElement(Text5, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React5.createElement(Text5, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "saving "), /* @__PURE__ */ React5.createElement(Text5, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%"))));
2905
+ const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
2906
+ const ctxRatio = summary.lastPromptTokens / ctxMax;
2907
+ const ctxColor = ctxRatio >= 0.8 ? "red" : ctxRatio >= 0.5 ? "yellow" : void 0;
2908
+ return /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React5.createElement(Box5, { justifyContent: "space-between" }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, model), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cache hit "), /* @__PURE__ */ React5.createElement(Text5, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cost "), /* @__PURE__ */ React5.createElement(Text5, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React5.createElement(Text5, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "saving "), /* @__PURE__ */ React5.createElement(Text5, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "ctx "), /* @__PURE__ */ React5.createElement(Text5, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React5.createElement(Text5, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null));
2909
+ }
2910
+ function formatTokens(n) {
2911
+ if (n < 1e3) return String(n);
2912
+ const k = n / 1e3;
2913
+ return k >= 100 ? `${k.toFixed(0)}k` : `${k.toFixed(1)}k`;
2776
2914
  }
2777
2915
 
2778
2916
  // src/cli/ui/slash.ts
@@ -2803,6 +2941,7 @@ function handleSlash(cmd, args, loop, ctx = {}) {
2803
2941
  " /branch <N|off> run N parallel samples (N>=2), pick most confident",
2804
2942
  " /mcp list MCP servers + tools attached to this session",
2805
2943
  " /setup (exit + reconfigure) \u2192 run `reasonix setup`",
2944
+ " /compact [cap] shrink large tool results in history (default 4k/result)",
2806
2945
  " /sessions list saved sessions (current is marked with \u25B8)",
2807
2946
  " /forget delete the current session from disk",
2808
2947
  " /clear clear displayed history (log + session kept)",
@@ -2844,6 +2983,19 @@ function handleSlash(cmd, args, loop, ctx = {}) {
2844
2983
  return {
2845
2984
  info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
2846
2985
  };
2986
+ case "compact": {
2987
+ const tight = Number.parseInt(args[0] ?? "", 10);
2988
+ const cap = Number.isFinite(tight) && tight >= 500 ? tight : 4e3;
2989
+ const { healedCount, charsSaved } = loop.compact(cap);
2990
+ if (healedCount === 0) {
2991
+ return {
2992
+ info: `\u25B8 nothing to compact \u2014 no tool result in history exceeds ${cap.toLocaleString()} chars.`
2993
+ };
2994
+ }
2995
+ return {
2996
+ info: `\u25B8 compacted ${healedCount} tool result(s), saved ${charsSaved.toLocaleString()} chars (~${Math.round(charsSaved / 4).toLocaleString()} tokens). Session file rewritten.`
2997
+ };
2998
+ }
2847
2999
  case "sessions": {
2848
3000
  const items = listSessions();
2849
3001
  if (items.length === 0) {
@@ -2948,12 +3100,14 @@ function App({
2948
3100
  const [streaming, setStreaming] = useState2(null);
2949
3101
  const [input, setInput] = useState2("");
2950
3102
  const [busy, setBusy] = useState2(false);
3103
+ const abortedThisTurn = useRef(false);
2951
3104
  const [summary, setSummary] = useState2({
2952
3105
  turns: 0,
2953
3106
  totalCostUsd: 0,
2954
3107
  claudeEquivalentUsd: 0,
2955
3108
  savingsVsClaudePct: 0,
2956
- cacheHitRatio: 0
3109
+ cacheHitRatio: 0,
3110
+ lastPromptTokens: 0
2957
3111
  });
2958
3112
  const transcriptRef = useRef(null);
2959
3113
  if (transcript && !transcriptRef.current) {
@@ -3014,6 +3168,13 @@ function App({
3014
3168
  ]);
3015
3169
  }
3016
3170
  }, [session, loop]);
3171
+ useInput((_input, key) => {
3172
+ if (!key.escape) return;
3173
+ if (!busy) return;
3174
+ if (abortedThisTurn.current) return;
3175
+ abortedThisTurn.current = true;
3176
+ loop.abort();
3177
+ });
3017
3178
  const prefixHash = loop.prefix.fingerprint;
3018
3179
  const writeTranscript = useCallback(
3019
3180
  (ev) => {
@@ -3059,6 +3220,7 @@ function App({
3059
3220
  const reasoningBuf = { current: "" };
3060
3221
  setStreaming({ id: assistantId, role: "assistant", text: "", streaming: true });
3061
3222
  setBusy(true);
3223
+ abortedThisTurn.current = false;
3062
3224
  const flush = () => {
3063
3225
  if (!contentBuf.current && !reasoningBuf.current) return;
3064
3226
  streamRef.text += contentBuf.current;
@@ -3131,6 +3293,11 @@ function App({
3131
3293
  ...prev,
3132
3294
  { id: `e-${Date.now()}`, role: "error", text: ev.error ?? ev.content }
3133
3295
  ]);
3296
+ } else if (ev.role === "warning") {
3297
+ setHistorical((prev) => [
3298
+ ...prev,
3299
+ { id: `w-${Date.now()}-${Math.random()}`, role: "warning", text: ev.content }
3300
+ ]);
3134
3301
  }
3135
3302
  }
3136
3303
  flush();
@@ -3155,7 +3322,7 @@ function App({
3155
3322
  ), /* @__PURE__ */ React6.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React6.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React6.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React6.createElement(CommandStrip, null));
3156
3323
  }
3157
3324
  function CommandStrip() {
3158
- return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"));
3325
+ return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /compact \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Esc (while thinking) \u2014 abort & summarize what was found so far"));
3159
3326
  }
3160
3327
  function describeRepair(repair) {
3161
3328
  const parts = [];
@@ -3286,13 +3453,13 @@ async function chatCommand(opts) {
3286
3453
  }
3287
3454
 
3288
3455
  // src/cli/commands/diff.ts
3289
- import { writeFileSync as writeFileSync2 } from "fs";
3456
+ import { writeFileSync as writeFileSync3 } from "fs";
3290
3457
  import { basename } from "path";
3291
3458
  import { render as render2 } from "ink";
3292
3459
  import React11 from "react";
3293
3460
 
3294
3461
  // src/cli/ui/DiffApp.tsx
3295
- import { Box as Box9, Static as Static2, Text as Text9, useApp as useApp3, useInput } from "ink";
3462
+ import { Box as Box9, Static as Static2, Text as Text9, useApp as useApp3, useInput as useInput2 } from "ink";
3296
3463
  import React10, { useState as useState5 } from "react";
3297
3464
 
3298
3465
  // src/cli/ui/RecordView.tsx
@@ -3337,7 +3504,7 @@ function DiffApp({ report }) {
3337
3504
  const maxIdx = Math.max(0, report.pairs.length - 1);
3338
3505
  const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
3339
3506
  const [idx, setIdx] = useState5(Math.max(0, initialIdx));
3340
- useInput((input, key) => {
3507
+ useInput2((input, key) => {
3341
3508
  if (input === "q" || key.ctrl && input === "c") {
3342
3509
  exit();
3343
3510
  return;
@@ -3432,7 +3599,7 @@ async function diffCommand(opts) {
3432
3599
  if (wantMarkdown) {
3433
3600
  console.log(renderSummaryTable(report));
3434
3601
  const md = renderMarkdown(report);
3435
- writeFileSync2(opts.mdPath, md, "utf8");
3602
+ writeFileSync3(opts.mdPath, md, "utf8");
3436
3603
  console.log(`
3437
3604
  markdown report written to ${opts.mdPath}`);
3438
3605
  return;
@@ -3517,13 +3684,13 @@ import { render as render3 } from "ink";
3517
3684
  import React13 from "react";
3518
3685
 
3519
3686
  // src/cli/ui/ReplayApp.tsx
3520
- import { Box as Box10, Static as Static3, Text as Text10, useApp as useApp4, useInput as useInput2 } from "ink";
3687
+ import { Box as Box10, Static as Static3, Text as Text10, useApp as useApp4, useInput as useInput3 } from "ink";
3521
3688
  import React12, { useMemo as useMemo2, useState as useState6 } from "react";
3522
3689
  function ReplayApp({ meta, pages }) {
3523
3690
  const { exit } = useApp4();
3524
3691
  const maxIdx = Math.max(0, pages.length - 1);
3525
3692
  const [idx, setIdx] = useState6(maxIdx);
3526
- useInput2((input, key) => {
3693
+ useInput3((input, key) => {
3527
3694
  if (input === "q" || key.ctrl && input === "c") {
3528
3695
  exit();
3529
3696
  return;
@@ -3548,7 +3715,9 @@ function ReplayApp({ meta, pages }) {
3548
3715
  totalCostUsd: cumStats.totalCostUsd,
3549
3716
  claudeEquivalentUsd: cumStats.claudeEquivalentUsd,
3550
3717
  savingsVsClaudePct: cumStats.savingsVsClaudePct,
3551
- cacheHitRatio: cumStats.cacheHitRatio
3718
+ cacheHitRatio: cumStats.cacheHitRatio,
3719
+ // Replay is read-only — no live last-turn prompt tokens to show.
3720
+ lastPromptTokens: 0
3552
3721
  };
3553
3722
  const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
3554
3723
  const currentPage = pages[idx];
@@ -3876,12 +4045,12 @@ import { render as render4 } from "ink";
3876
4045
  import React16 from "react";
3877
4046
 
3878
4047
  // src/cli/ui/Wizard.tsx
3879
- import { Box as Box12, Text as Text12, useApp as useApp5, useInput as useInput4 } from "ink";
4048
+ import { Box as Box12, Text as Text12, useApp as useApp5, useInput as useInput5 } from "ink";
3880
4049
  import TextInput3 from "ink-text-input";
3881
4050
  import React15, { useState as useState8 } from "react";
3882
4051
 
3883
4052
  // src/cli/ui/Select.tsx
3884
- import { Box as Box11, Text as Text11, useInput as useInput3 } from "ink";
4053
+ import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
3885
4054
  import React14, { useState as useState7 } from "react";
3886
4055
  function SingleSelect({
3887
4056
  items,
@@ -3894,7 +4063,7 @@ function SingleSelect({
3894
4063
  items.findIndex((i) => i.value === initialValue && !i.disabled)
3895
4064
  );
3896
4065
  const [index, setIndex] = useState7(initialIndex === -1 ? 0 : initialIndex);
3897
- useInput3((_input, key) => {
4066
+ useInput4((_input, key) => {
3898
4067
  if (key.upArrow) {
3899
4068
  setIndex((i) => findNextEnabled(items, i, -1));
3900
4069
  } else if (key.downArrow) {
@@ -3928,7 +4097,7 @@ function MultiSelect({
3928
4097
  return first === -1 ? 0 : first;
3929
4098
  });
3930
4099
  const [selected, setSelected] = useState7(new Set(initialSelected));
3931
- useInput3((input, key) => {
4100
+ useInput4((input, key) => {
3932
4101
  if (key.upArrow) {
3933
4102
  setIndex((i) => findNextEnabled(items, i, -1));
3934
4103
  } else if (key.downArrow) {
@@ -4014,7 +4183,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
4014
4183
  catalogArgs: {}
4015
4184
  });
4016
4185
  const [error, setError] = useState8(null);
4017
- useInput4((_input, key) => {
4186
+ useInput5((_input, key) => {
4018
4187
  if (key.escape && step !== "saved" && onCancel) onCancel();
4019
4188
  });
4020
4189
  if (step === "apiKey") {
@@ -4176,13 +4345,13 @@ function McpArgsStep({
4176
4345
  )), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : null));
4177
4346
  }
4178
4347
  function ReviewConfirm({ onConfirm }) {
4179
- useInput4((_i, key) => {
4348
+ useInput5((_i, key) => {
4180
4349
  if (key.return) onConfirm();
4181
4350
  });
4182
4351
  return null;
4183
4352
  }
4184
4353
  function ExitOnEnter({ onExit }) {
4185
- useInput4((_i, key) => {
4354
+ useInput5((_i, key) => {
4186
4355
  if (key.return) onExit();
4187
4356
  });
4188
4357
  return null;