reasonix 0.4.9 → 0.4.12

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
@@ -182,6 +182,27 @@ var DeepSeekClient = class {
182
182
  if (opts.responseFormat) payload.response_format = opts.responseFormat;
183
183
  return payload;
184
184
  }
185
+ /**
186
+ * Fetch the current DeepSeek account balance. Separate endpoint
187
+ * from chat completions, no billing impact. Returns null on any
188
+ * network/auth failure so callers can gate the balance display
189
+ * without a hard error — the rest of the session works regardless.
190
+ */
191
+ async getBalance(opts = {}) {
192
+ try {
193
+ const resp = await this._fetch(`${this.baseUrl}/user/balance`, {
194
+ method: "GET",
195
+ headers: { Authorization: `Bearer ${this.apiKey}` },
196
+ signal: opts.signal
197
+ });
198
+ if (!resp.ok) return null;
199
+ const data = await resp.json();
200
+ if (!data || !Array.isArray(data.balance_infos)) return null;
201
+ return data;
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
185
206
  async chat(opts) {
186
207
  const ctrl = new AbortController();
187
208
  const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
@@ -328,8 +349,9 @@ Constraints:
328
349
  - Each item is plain text, at most {maxItemLen} characters, no markdown.
329
350
  - Write in the same language as the trace (Chinese in \u2192 Chinese out, etc.).
330
351
  - Do not quote back the trace; write short, specific phrases.`;
331
- async function harvest(reasoningContent, client, options = {}) {
352
+ async function harvest(reasoningContent, client, options = {}, signal) {
332
353
  if (!client || !reasoningContent) return emptyPlanState();
354
+ if (signal?.aborted) return emptyPlanState();
333
355
  const minLen = options.minReasoningLen ?? 40;
334
356
  const trimmed = reasoningContent.trim();
335
357
  if (trimmed.length < minLen) return emptyPlanState();
@@ -349,7 +371,8 @@ async function harvest(reasoningContent, client, options = {}) {
349
371
  ],
350
372
  responseFormat: { type: "json_object" },
351
373
  temperature: 0,
352
- maxTokens: 600
374
+ maxTokens: 600,
375
+ signal
353
376
  });
354
377
  return parsePlanState(resp.content, maxItems, maxItemLen);
355
378
  } catch {
@@ -1138,6 +1161,16 @@ function costUsd(model, usage) {
1138
1161
  if (!p) return 0;
1139
1162
  return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss + usage.completionTokens * p.output) / 1e6;
1140
1163
  }
1164
+ function inputCostUsd(model, usage) {
1165
+ const p = DEEPSEEK_PRICING[model];
1166
+ if (!p) return 0;
1167
+ return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss) / 1e6;
1168
+ }
1169
+ function outputCostUsd(model, usage) {
1170
+ const p = DEEPSEEK_PRICING[model];
1171
+ if (!p) return 0;
1172
+ return usage.completionTokens * p.output / 1e6;
1173
+ }
1141
1174
  function claudeEquivalentCost(usage) {
1142
1175
  return (usage.promptTokens * CLAUDE_SONNET_PRICING.input + usage.completionTokens * CLAUDE_SONNET_PRICING.output) / 1e6;
1143
1176
  }
@@ -1165,6 +1198,12 @@ var SessionStats = class {
1165
1198
  const c = this.totalClaudeEquivalent;
1166
1199
  return c > 0 ? 1 - this.totalCost / c : 0;
1167
1200
  }
1201
+ get totalInputCost() {
1202
+ return this.turns.reduce((sum, t) => sum + inputCostUsd(t.model, t.usage), 0);
1203
+ }
1204
+ get totalOutputCost() {
1205
+ return this.turns.reduce((sum, t) => sum + outputCostUsd(t.model, t.usage), 0);
1206
+ }
1168
1207
  get aggregateCacheHitRatio() {
1169
1208
  let hit = 0;
1170
1209
  let miss = 0;
@@ -1180,6 +1219,8 @@ var SessionStats = class {
1180
1219
  return {
1181
1220
  turns: this.turns.length,
1182
1221
  totalCostUsd: round(this.totalCost, 6),
1222
+ totalInputCostUsd: round(this.totalInputCost, 6),
1223
+ totalOutputCostUsd: round(this.totalOutputCost, 6),
1183
1224
  claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
1184
1225
  savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
1185
1226
  cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
@@ -1254,8 +1295,12 @@ var CacheFirstLoop = class {
1254
1295
  for (const msg of messages) this.log.append(msg);
1255
1296
  this.resumedMessageCount = messages.length;
1256
1297
  if (healedCount > 0) {
1298
+ try {
1299
+ rewriteSession(this.sessionName, messages);
1300
+ } catch {
1301
+ }
1257
1302
  process.stderr.write(
1258
- `\u25B8 session "${this.sessionName}": healed ${healedCount} oversized tool result(s) (was ${healedFrom.toLocaleString()} chars total). Old payloads were truncated to fit DeepSeek's context window; the conversation is preserved.
1303
+ `\u25B8 session "${this.sessionName}": healed ${healedCount} entr${healedCount === 1 ? "y" : "ies"}${healedFrom > 0 ? ` (was ${healedFrom.toLocaleString()} chars oversized)` : " (dropped dangling tool_calls tail)"}. Rewrote session file.
1259
1304
  `
1260
1305
  );
1261
1306
  }
@@ -1276,7 +1321,7 @@ var CacheFirstLoop = class {
1276
1321
  */
1277
1322
  compact(tightCapChars = 4e3) {
1278
1323
  const before = this.log.toMessages();
1279
- const { messages, healedCount, healedFrom } = healLoadedMessages(before, tightCapChars);
1324
+ const { messages, healedCount, healedFrom } = shrinkOversizedToolResults(before, tightCapChars);
1280
1325
  const afterBytes = messages.filter((m) => m.role === "tool").reduce((s, m) => s + (typeof m.content === "string" ? m.content.length : 0), 0);
1281
1326
  const charsSaved = healedFrom - afterBytes;
1282
1327
  if (healedCount > 0) {
@@ -1299,6 +1344,29 @@ var CacheFirstLoop = class {
1299
1344
  }
1300
1345
  }
1301
1346
  }
1347
+ /**
1348
+ * Start a fresh conversation WITHOUT exiting. Drops every message
1349
+ * in the in-memory log AND rewrites the session file to empty so
1350
+ * a resume won't re-hydrate the old turns. Unlike `/forget`, which
1351
+ * deletes the session entirely, this keeps the session name and
1352
+ * config intact — it's the "new chat" button.
1353
+ *
1354
+ * The immutable prefix (system prompt + tool specs) is preserved
1355
+ * — that's the cache-first invariant, not part of the conversation.
1356
+ * Returns the number of messages dropped so the UI can show it.
1357
+ */
1358
+ clearLog() {
1359
+ const dropped = this.log.length;
1360
+ this.log.compactInPlace([]);
1361
+ if (this.sessionName) {
1362
+ try {
1363
+ rewriteSession(this.sessionName, []);
1364
+ } catch {
1365
+ }
1366
+ }
1367
+ this.scratch.reset();
1368
+ return { dropped };
1369
+ }
1302
1370
  /**
1303
1371
  * Reconfigure model/harvest/branch/stream mid-session. The loop's log,
1304
1372
  * scratch, and stats are preserved — only the per-turn behavior changes.
@@ -1330,7 +1398,8 @@ var CacheFirstLoop = class {
1330
1398
  this.stream = this.branchEnabled ? false : this._streamPreference;
1331
1399
  }
1332
1400
  buildMessages(pendingUser) {
1333
- const msgs = [...this.prefix.toMessages(), ...this.log.toMessages()];
1401
+ const healed = healLoadedMessages(this.log.toMessages(), DEFAULT_MAX_RESULT_CHARS);
1402
+ const msgs = [...this.prefix.toMessages(), ...healed.messages];
1334
1403
  if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
1335
1404
  return msgs;
1336
1405
  }
@@ -1405,6 +1474,13 @@ var CacheFirstLoop = class {
1405
1474
  yield { turn: this._turn, role: "done", content: stoppedMsg };
1406
1475
  return;
1407
1476
  }
1477
+ if (iter > 0) {
1478
+ yield {
1479
+ turn: this._turn,
1480
+ role: "status",
1481
+ content: "tool result uploaded \xB7 model thinking before next response\u2026"
1482
+ };
1483
+ }
1408
1484
  if (!warnedForIterBudget && iter >= warnAt) {
1409
1485
  warnedForIterBudget = true;
1410
1486
  yield {
@@ -1565,7 +1641,14 @@ var CacheFirstLoop = class {
1565
1641
  pendingUser = null;
1566
1642
  }
1567
1643
  this.scratch.reasoning = reasoningContent || null;
1568
- const planState = preHarvestedPlanState ? preHarvestedPlanState : this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions) : emptyPlanState();
1644
+ if (!preHarvestedPlanState && this.harvestEnabled && (reasoningContent?.trim().length ?? 0) >= 40) {
1645
+ yield {
1646
+ turn: this._turn,
1647
+ role: "status",
1648
+ content: "extracting plan state from reasoning\u2026"
1649
+ };
1650
+ }
1651
+ const planState = preHarvestedPlanState ? preHarvestedPlanState : this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions, signal) : emptyPlanState();
1569
1652
  const { calls: repairedCalls, report } = this.repair.process(
1570
1653
  toolCalls,
1571
1654
  reasoningContent || null,
@@ -1587,15 +1670,38 @@ var CacheFirstLoop = class {
1587
1670
  }
1588
1671
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
1589
1672
  if (usage && usage.promptTokens / ctxMax > 0.8) {
1590
- yield {
1591
- turn: this._turn,
1592
- role: "warning",
1593
- content: `context ${usage.promptTokens}/${ctxMax} (${Math.round(
1594
- usage.promptTokens / ctxMax * 100
1595
- )}%) \u2014 more tools would overflow. Forcing summary from what was gathered.`
1596
- };
1597
- yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
1598
- return;
1673
+ const before = usage.promptTokens;
1674
+ const compactResult = this.compact(4e3);
1675
+ if (compactResult.healedCount > 0) {
1676
+ const approxSaved = Math.round(compactResult.charsSaved / 4);
1677
+ const after = before - approxSaved;
1678
+ yield {
1679
+ turn: this._turn,
1680
+ role: "warning",
1681
+ content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} \u2014 auto-compacted ${compactResult.healedCount} oversized tool result(s), saved ~${approxSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Continuing.`
1682
+ };
1683
+ } else {
1684
+ yield {
1685
+ turn: this._turn,
1686
+ role: "warning",
1687
+ content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
1688
+ before / ctxMax * 100
1689
+ )}%) \u2014 nothing to auto-compact. Forcing summary from what was gathered.`
1690
+ };
1691
+ const tail = this.log.entries[this.log.entries.length - 1];
1692
+ if (tail && tail.role === "assistant" && Array.isArray(tail.tool_calls) && tail.tool_calls.length > 0) {
1693
+ const kept = this.log.entries.slice(0, -1);
1694
+ this.log.compactInPlace([...kept]);
1695
+ if (this.sessionName) {
1696
+ try {
1697
+ rewriteSession(this.sessionName, kept);
1698
+ } catch {
1699
+ }
1700
+ }
1701
+ }
1702
+ yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
1703
+ return;
1704
+ }
1599
1705
  }
1600
1706
  for (const call of repairedCalls) {
1601
1707
  const name = call.function?.name ?? "";
@@ -1627,6 +1733,11 @@ var CacheFirstLoop = class {
1627
1733
  }
1628
1734
  async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
1629
1735
  try {
1736
+ yield {
1737
+ turn: this._turn,
1738
+ role: "status",
1739
+ content: "summarizing what was gathered\u2026"
1740
+ };
1630
1741
  const messages = this.buildMessages(null);
1631
1742
  messages.push({
1632
1743
  role: "user",
@@ -1709,7 +1820,7 @@ function summarizeBranch(chosen, samples) {
1709
1820
  temperatures: samples.map((s) => s.temperature)
1710
1821
  };
1711
1822
  }
1712
- function healLoadedMessages(messages, maxChars) {
1823
+ function shrinkOversizedToolResults(messages, maxChars) {
1713
1824
  let healedCount = 0;
1714
1825
  let healedFrom = 0;
1715
1826
  const out = messages.map((msg) => {
@@ -1722,6 +1833,51 @@ function healLoadedMessages(messages, maxChars) {
1722
1833
  });
1723
1834
  return { messages: out, healedCount, healedFrom };
1724
1835
  }
1836
+ function healLoadedMessages(messages, maxChars) {
1837
+ const shrunk = shrinkOversizedToolResults(messages, maxChars);
1838
+ let healedCount = shrunk.healedCount;
1839
+ const out = [];
1840
+ const openCallIds = /* @__PURE__ */ new Set();
1841
+ let droppedAssistantCalls = 0;
1842
+ let droppedStrayTools = 0;
1843
+ for (let i = 0; i < shrunk.messages.length; i++) {
1844
+ const msg = shrunk.messages[i];
1845
+ if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
1846
+ const needed = /* @__PURE__ */ new Set();
1847
+ for (const call of msg.tool_calls) {
1848
+ if (call?.id) needed.add(call.id);
1849
+ }
1850
+ const candidates = [];
1851
+ let j = i + 1;
1852
+ while (j < shrunk.messages.length && needed.size > 0) {
1853
+ const nxt = shrunk.messages[j];
1854
+ if (nxt.role !== "tool") break;
1855
+ const id = nxt.tool_call_id ?? "";
1856
+ if (!needed.has(id)) break;
1857
+ needed.delete(id);
1858
+ candidates.push(nxt);
1859
+ j++;
1860
+ }
1861
+ if (needed.size === 0) {
1862
+ out.push(msg);
1863
+ for (const r of candidates) out.push(r);
1864
+ i = j - 1;
1865
+ } else {
1866
+ droppedAssistantCalls += 1;
1867
+ droppedStrayTools += candidates.length;
1868
+ i = j - 1;
1869
+ }
1870
+ continue;
1871
+ }
1872
+ if (msg.role === "tool") {
1873
+ droppedStrayTools += 1;
1874
+ continue;
1875
+ }
1876
+ out.push(msg);
1877
+ }
1878
+ healedCount += droppedAssistantCalls + droppedStrayTools;
1879
+ return { messages: out, healedCount, healedFrom: shrunk.healedFrom };
1880
+ }
1725
1881
  function formatLoopError(err) {
1726
1882
  const msg = err.message ?? "";
1727
1883
  if (msg.includes("maximum context length")) {
@@ -1978,7 +2134,12 @@ function registerFilesystemTools(registry, opts) {
1978
2134
  }
1979
2135
  const after = before.slice(0, firstIdx) + args.replace + before.slice(firstIdx + args.search.length);
1980
2136
  await fs.writeFile(abs, after, "utf8");
1981
- return `edited ${pathMod.relative(rootDir, abs)} (${args.search.length}\u2192${args.replace.length} chars)`;
2137
+ const rel = pathMod.relative(rootDir, abs);
2138
+ const header = `edited ${rel} (${args.search.length}\u2192${args.replace.length} chars)`;
2139
+ const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
2140
+ const diff = renderEditDiff(args.search, args.replace, startLine);
2141
+ return `${header}
2142
+ ${diff}`;
1982
2143
  }
1983
2144
  });
1984
2145
  registry.register({
@@ -2016,6 +2177,51 @@ function registerFilesystemTools(registry, opts) {
2016
2177
  });
2017
2178
  return registry;
2018
2179
  }
2180
+ function renderEditDiff(search, replace, startLine) {
2181
+ const a = search.split(/\r?\n/);
2182
+ const b = replace.split(/\r?\n/);
2183
+ const diff = lineDiff(a, b);
2184
+ const hunk = `@@ -${startLine},${a.length} +${startLine},${b.length} @@`;
2185
+ const body = diff.map((d) => `${d.op === " " ? " " : d.op} ${d.line}`).join("\n");
2186
+ return `${hunk}
2187
+ ${body}`;
2188
+ }
2189
+ function lineDiff(a, b) {
2190
+ const n = a.length;
2191
+ const m = b.length;
2192
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
2193
+ for (let i2 = 1; i2 <= n; i2++) {
2194
+ for (let j2 = 1; j2 <= m; j2++) {
2195
+ if (a[i2 - 1] === b[j2 - 1]) dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
2196
+ else dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
2197
+ }
2198
+ }
2199
+ const out = [];
2200
+ let i = n;
2201
+ let j = m;
2202
+ while (i > 0 && j > 0) {
2203
+ if (a[i - 1] === b[j - 1]) {
2204
+ out.unshift({ op: " ", line: a[i - 1] });
2205
+ i--;
2206
+ j--;
2207
+ } else if ((dp[i - 1][j] ?? 0) > (dp[i][j - 1] ?? 0)) {
2208
+ out.unshift({ op: "-", line: a[i - 1] });
2209
+ i--;
2210
+ } else {
2211
+ out.unshift({ op: "+", line: b[j - 1] });
2212
+ j--;
2213
+ }
2214
+ }
2215
+ while (i > 0) {
2216
+ out.unshift({ op: "-", line: a[i - 1] });
2217
+ i--;
2218
+ }
2219
+ while (j > 0) {
2220
+ out.unshift({ op: "+", line: b[j - 1] });
2221
+ j--;
2222
+ }
2223
+ return out;
2224
+ }
2019
2225
 
2020
2226
  // src/env.ts
2021
2227
  import { readFileSync as readFileSync3 } from "fs";
@@ -2201,6 +2407,8 @@ function computeReplayStats(records) {
2201
2407
  }
2202
2408
  function summarizeTurns(turns) {
2203
2409
  const totalCost = turns.reduce((s, t) => s + t.cost, 0);
2410
+ const totalInput = turns.reduce((s, t) => s + inputCostUsd(t.model, t.usage), 0);
2411
+ const totalOutput = turns.reduce((s, t) => s + outputCostUsd(t.model, t.usage), 0);
2204
2412
  const totalClaude = turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0);
2205
2413
  let hit = 0;
2206
2414
  let miss = 0;
@@ -2214,6 +2422,8 @@ function summarizeTurns(turns) {
2214
2422
  return {
2215
2423
  turns: turns.length,
2216
2424
  totalCostUsd: round2(totalCost, 6),
2425
+ totalInputCostUsd: round2(totalInput, 6),
2426
+ totalOutputCostUsd: round2(totalOutput, 6),
2217
2427
  claudeEquivalentUsd: round2(totalClaude, 6),
2218
2428
  savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
2219
2429
  cacheHitRatio: round2(cacheHitRatio, 4),
@@ -3501,6 +3711,25 @@ function parseBlocks(raw) {
3501
3711
  out.push({ kind: "heading", level: hm[1].length, text: hm[2].trim() });
3502
3712
  continue;
3503
3713
  }
3714
+ if (line.includes("|")) {
3715
+ const next = (lines[i + 1] ?? "").trim();
3716
+ if (/^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(next)) {
3717
+ flushPara();
3718
+ flushList();
3719
+ const header = splitTableRow(line);
3720
+ const rows = [];
3721
+ let j = i + 2;
3722
+ while (j < lines.length) {
3723
+ const r = lines[j].replace(/\s+$/g, "");
3724
+ if (r.trim() === "" || !r.includes("|")) break;
3725
+ rows.push(splitTableRow(r));
3726
+ j++;
3727
+ }
3728
+ out.push({ kind: "table", header, rows });
3729
+ i = j - 1;
3730
+ continue;
3731
+ }
3732
+ }
3504
3733
  const bm = line.match(/^\s*[-*+]\s+(.+)$/);
3505
3734
  if (bm) {
3506
3735
  flushPara();
@@ -3543,10 +3772,55 @@ function BlockView({ block }) {
3543
3772
  return /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, block.text));
3544
3773
  case "edit-block":
3545
3774
  return /* @__PURE__ */ React2.createElement(EditBlockRow, { block });
3775
+ case "table":
3776
+ return /* @__PURE__ */ React2.createElement(TableBlockRow, { block });
3546
3777
  case "hr":
3547
3778
  return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3548
3779
  }
3549
3780
  }
3781
+ function splitTableRow(line) {
3782
+ const SENTINEL = "\0";
3783
+ const masked = line.replace(/\\\|/g, SENTINEL);
3784
+ const trimmed = masked.trim().replace(/^\||\|$/g, "");
3785
+ return trimmed.split("|").map((c) => c.trim().replace(new RegExp(SENTINEL, "g"), "|"));
3786
+ }
3787
+ function TableBlockRow({ block }) {
3788
+ const colCount = Math.max(block.header.length, ...block.rows.map((r) => r.length));
3789
+ const widths = [];
3790
+ for (let c = 0; c < colCount; c++) {
3791
+ const cellLengths = [displayWidth(block.header[c] ?? "")];
3792
+ for (const r of block.rows) cellLengths.push(displayWidth(r[c] ?? ""));
3793
+ widths.push(Math.min(40, Math.max(3, ...cellLengths)));
3794
+ }
3795
+ const pad2 = (s, w) => {
3796
+ const dw = displayWidth(s);
3797
+ if (dw >= w) return s;
3798
+ return s + " ".repeat(w - dw);
3799
+ };
3800
+ const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u253C\u2500");
3801
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Box2, null, block.header.map((cell, ci) => (
3802
+ // biome-ignore lint/suspicious/noArrayIndexKey: table columns never reorder — derived from a static header array
3803
+ /* @__PURE__ */ React2.createElement(Text2, { key: `h-${ci}`, bold: true, color: "cyan" }, pad2(cell, widths[ci] ?? 3), ci < colCount - 1 ? " \u2502 " : "")
3804
+ ))), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, separator), block.rows.map((row2, ri) => (
3805
+ // biome-ignore lint/suspicious/noArrayIndexKey: table rows render in source order and don't reorder
3806
+ /* @__PURE__ */ React2.createElement(Box2, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
3807
+ // biome-ignore lint/suspicious/noArrayIndexKey: same — column axis is fixed by the table shape
3808
+ /* @__PURE__ */ React2.createElement(Text2, { key: `c-${ri}-${ci}` }, pad2(row2[ci] ?? "", widths[ci] ?? 3), ci < colCount - 1 ? " \u2502 " : "")
3809
+ )))
3810
+ )));
3811
+ }
3812
+ function displayWidth(s) {
3813
+ let w = 0;
3814
+ for (const ch of s) {
3815
+ const code = ch.codePointAt(0) ?? 0;
3816
+ if (code >= 4352 && code <= 4447 || code >= 11904 && code <= 12350 || code >= 12353 && code <= 13311 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 40960 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65103 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510) {
3817
+ w += 2;
3818
+ } else {
3819
+ w += 1;
3820
+ }
3821
+ }
3822
+ return w;
3823
+ }
3550
3824
  function EditBlockRow({ block }) {
3551
3825
  const isNewFile = block.search.length === 0;
3552
3826
  const searchLines = block.search.split("\n");
@@ -3572,7 +3846,8 @@ var EventRow = React3.memo(function EventRow2({ event }) {
3572
3846
  const isError = event.text.startsWith("ERROR:");
3573
3847
  const color = isError ? "red" : "yellow";
3574
3848
  const marker = isError ? "\u2717" : "\u2192";
3575
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color }, `tool<${event.toolName ?? "?"}> ${marker}`), /* @__PURE__ */ React3.createElement(Text3, { color: isError ? "red" : void 0, dimColor: !isError }, " ", truncate2(event.text, 400)));
3849
+ const isEditFile = (event.toolName === "edit_file" || event.toolName?.endsWith("_edit_file")) && !isError;
3850
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color }, `tool<${event.toolName ?? "?"}> ${marker}`), isEditFile ? /* @__PURE__ */ React3.createElement(EditFileDiff, { text: event.text }) : /* @__PURE__ */ React3.createElement(Text3, { color: isError ? "red" : void 0, dimColor: !isError }, " ", truncate2(event.text, 400)));
3576
3851
  }
3577
3852
  if (event.role === "error") {
3578
3853
  return /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, event.text));
@@ -3585,6 +3860,20 @@ var EventRow = React3.memo(function EventRow2({ event }) {
3585
3860
  }
3586
3861
  return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, null, event.text));
3587
3862
  });
3863
+ function EditFileDiff({ text }) {
3864
+ const lines = text.split(/\r?\n/);
3865
+ const [statusHeader, hunkHeader, ...body] = lines;
3866
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, ` ${statusHeader ?? ""}`), hunkHeader !== void 0 ? /* @__PURE__ */ React3.createElement(Text3, { color: "cyan", bold: true }, hunkHeader) : null, body.map((line, i) => {
3867
+ const key = `${i}-${line.slice(0, 32)}`;
3868
+ if (line.startsWith("- ")) {
3869
+ return /* @__PURE__ */ React3.createElement(Text3, { key, color: "red" }, line);
3870
+ }
3871
+ if (line.startsWith("+ ")) {
3872
+ return /* @__PURE__ */ React3.createElement(Text3, { key, color: "green" }, line);
3873
+ }
3874
+ return /* @__PURE__ */ React3.createElement(Text3, { key, dimColor: true }, line);
3875
+ }));
3876
+ }
3588
3877
  function BranchBlock({ branch }) {
3589
3878
  const per = branch.uncertainties.map((u, i) => {
3590
3879
  const marker = i === branch.chosenIndex ? "\u25B8" : " ";
@@ -3621,8 +3910,21 @@ function StreamingAssistant({ event }) {
3621
3910
  }
3622
3911
  const tail = lastLine(event.text, 140);
3623
3912
  const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
3913
+ const preFirstByte = !event.text && !event.reasoning;
3624
3914
  const reasoningOnly = !event.text && !!event.reasoning;
3625
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React3.createElement(Pulse, null), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ", "(", reasoningOnly ? "reasoning" : "streaming", " \xB7 ", event.text.length, event.reasoning ? ` + think ${event.reasoning.length}` : "", " chars)", " "), /* @__PURE__ */ React3.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "\u25B8 ", tail) : reasoningOnly ? /* @__PURE__ */ React3.createElement(Text3, { color: "yellow", dimColor: true }, " R1 is thinking before it speaks \u2014 body text starts when reasoning completes (typically 20-90s).") : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, " (waiting for first byte \u2014 connection is open)"));
3915
+ let label;
3916
+ let labelColor;
3917
+ if (preFirstByte) {
3918
+ label = "request sent \xB7 waiting for server";
3919
+ labelColor = "yellow";
3920
+ } else if (reasoningOnly) {
3921
+ label = `R1 reasoning \xB7 ${event.reasoning?.length ?? 0} chars of thought`;
3922
+ labelColor = "cyan";
3923
+ } else {
3924
+ label = event.reasoning ? `writing response \xB7 ${event.text.length} chars \xB7 after ${event.reasoning.length} chars of reasoning` : `writing response \xB7 ${event.text.length} chars`;
3925
+ labelColor = "green";
3926
+ }
3927
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React3.createElement(Pulse, null), /* @__PURE__ */ React3.createElement(Text3, { color: labelColor }, ` ${label} `), /* @__PURE__ */ React3.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "\u25B8 ", tail) : reasoningOnly ? /* @__PURE__ */ React3.createElement(Text3, { color: "yellow", dimColor: true }, " R1 is thinking before it speaks \u2014 body text starts when reasoning completes (typically 20-90s).") : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, " connection open, first byte typically in 5-60s depending on model + load"));
3626
3928
  }
3627
3929
  function Pulse() {
3628
3930
  const [tick, setTick] = useState(0);
@@ -3778,7 +4080,8 @@ function StatsPanel({
3778
4080
  model,
3779
4081
  prefixHash,
3780
4082
  harvestOn,
3781
- branchBudget
4083
+ branchBudget,
4084
+ balance
3782
4085
  }) {
3783
4086
  const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
3784
4087
  const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
@@ -3786,7 +4089,7 @@ function StatsPanel({
3786
4089
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
3787
4090
  const ctxRatio = summary.lastPromptTokens / ctxMax;
3788
4091
  const ctxColor = ctxRatio >= 0.8 ? "red" : ctxRatio >= 0.5 ? "yellow" : void 0;
3789
- return /* @__PURE__ */ React6.createElement(Box6, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React6.createElement(Box6, { justifyContent: "space-between" }, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, model), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React6.createElement(Text6, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React6.createElement(Text6, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "cache hit "), /* @__PURE__ */ React6.createElement(Text6, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "cost "), /* @__PURE__ */ React6.createElement(Text6, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React6.createElement(Text6, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "saving "), /* @__PURE__ */ React6.createElement(Text6, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "ctx "), /* @__PURE__ */ React6.createElement(Text6, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React6.createElement(Text6, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null));
4092
+ return /* @__PURE__ */ React6.createElement(Box6, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React6.createElement(Box6, { justifyContent: "space-between" }, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, model), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React6.createElement(Text6, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React6.createElement(Text6, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "cache hit "), /* @__PURE__ */ React6.createElement(Text6, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "cost "), /* @__PURE__ */ React6.createElement(Text6, { color: "green", bold: true }, "$", summary.totalCostUsd.toFixed(6)), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (in ", "$", summary.totalInputCostUsd.toFixed(6), " \xB7 out ", "$", summary.totalOutputCostUsd.toFixed(6), ")")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "ctx "), /* @__PURE__ */ React6.createElement(Text6, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React6.createElement(Text6, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null, balance ? /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "balance "), /* @__PURE__ */ React6.createElement(Text6, { color: balance.total < 1 ? "red" : balance.total < 5 ? "yellow" : "green", bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : "")) : null));
3790
4093
  }
3791
4094
  function formatTokens(n) {
3792
4095
  if (n < 1e3) return String(n);
@@ -3815,7 +4118,8 @@ var SLASH_COMMANDS = [
3815
4118
  { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
3816
4119
  { cmd: "forget", summary: "delete the current session from disk" },
3817
4120
  { cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
3818
- { cmd: "clear", summary: "clear the visible scrollback (log + session kept)" },
4121
+ { cmd: "clear", summary: "clear visible scrollback only (log/context kept)" },
4122
+ { cmd: "new", summary: "start a fresh conversation (clear context + scrollback)" },
3819
4123
  { cmd: "exit", summary: "quit the TUI" },
3820
4124
  // Code-mode only
3821
4125
  { cmd: "apply", summary: "commit pending edit blocks to disk", contextual: "code" },
@@ -3848,7 +4152,18 @@ function handleSlash(cmd, args, loop, ctx = {}) {
3848
4152
  case "quit":
3849
4153
  return { exit: true };
3850
4154
  case "clear":
3851
- return { clear: true };
4155
+ return {
4156
+ clear: true,
4157
+ info: "\u25B8 cleared visible scrollback only. Context (message log) is intact \u2014 next turn still sees everything. Use /new to start fresh, or /forget to delete the session entirely."
4158
+ };
4159
+ case "new":
4160
+ case "reset": {
4161
+ const { dropped } = loop.clearLog();
4162
+ return {
4163
+ clear: true,
4164
+ info: `\u25B8 new conversation \u2014 dropped ${dropped} message(s) from context. Same session, fresh slate.`
4165
+ };
4166
+ }
3852
4167
  case "help":
3853
4168
  case "?":
3854
4169
  return {
@@ -3872,7 +4187,8 @@ function handleSlash(cmd, args, loop, ctx = {}) {
3872
4187
  ' /commit "msg" (code mode) git add -A && git commit -m "msg"',
3873
4188
  " /sessions list saved sessions (current is marked with \u25B8)",
3874
4189
  " /forget delete the current session from disk",
3875
- " /clear clear displayed history (log + session kept)",
4190
+ " /new start fresh: drop all context + clear scrollback",
4191
+ " /clear clear displayed scrollback only (context kept \u2014 model still sees it)",
3876
4192
  " /exit quit",
3877
4193
  "",
3878
4194
  "Presets:",
@@ -4243,6 +4559,8 @@ function App({
4243
4559
  const abortedThisTurn = useRef(false);
4244
4560
  const [ongoingTool, setOngoingTool] = useState3(null);
4245
4561
  const [toolProgress, setToolProgress] = useState3(null);
4562
+ const [statusLine, setStatusLine] = useState3(null);
4563
+ const [balance, setBalance] = useState3(null);
4246
4564
  const lastEditSnapshots = useRef(null);
4247
4565
  const pendingEdits = useRef([]);
4248
4566
  const promptHistory = useRef([]);
@@ -4252,6 +4570,8 @@ function App({
4252
4570
  const [summary, setSummary] = useState3({
4253
4571
  turns: 0,
4254
4572
  totalCostUsd: 0,
4573
+ totalInputCostUsd: 0,
4574
+ totalOutputCostUsd: 0,
4255
4575
  claudeEquivalentUsd: 0,
4256
4576
  savingsVsClaudePct: 0,
4257
4577
  cacheHitRatio: 0,
@@ -4294,6 +4614,18 @@ function App({
4294
4614
  loopRef.current = l;
4295
4615
  return l;
4296
4616
  }, [model, system, harvest2, branch, session, tools]);
4617
+ useEffect3(() => {
4618
+ let cancelled = false;
4619
+ void (async () => {
4620
+ const bal = await loop.client.getBalance().catch(() => null);
4621
+ if (cancelled || !bal || !bal.balance_infos.length) return;
4622
+ const primary = bal.balance_infos[0];
4623
+ setBalance({ currency: primary.currency, total: Number(primary.total_balance) });
4624
+ })();
4625
+ return () => {
4626
+ cancelled = true;
4627
+ };
4628
+ }, [loop]);
4297
4629
  useEffect3(() => {
4298
4630
  if (!progressSink) return;
4299
4631
  progressSink.current = (info) => {
@@ -4455,6 +4787,16 @@ function App({
4455
4787
  exit();
4456
4788
  return;
4457
4789
  }
4790
+ if (result.clear && result.info) {
4791
+ setHistorical([
4792
+ {
4793
+ id: `sys-${Date.now()}`,
4794
+ role: "info",
4795
+ text: result.info
4796
+ }
4797
+ ]);
4798
+ return;
4799
+ }
4458
4800
  if (result.clear) {
4459
4801
  setHistorical([]);
4460
4802
  return;
@@ -4503,7 +4845,12 @@ function App({
4503
4845
  try {
4504
4846
  for await (const ev of loop.step(text)) {
4505
4847
  writeTranscript(ev);
4506
- if (ev.role === "assistant_delta") {
4848
+ if (ev.role !== "status") {
4849
+ setStatusLine((cur) => cur ? null : cur);
4850
+ }
4851
+ if (ev.role === "status") {
4852
+ setStatusLine(ev.content);
4853
+ } else if (ev.role === "assistant_delta") {
4507
4854
  if (ev.content) contentBuf.current += ev.content;
4508
4855
  if (ev.reasoningDelta) reasoningBuf.current += ev.reasoningDelta;
4509
4856
  } else if (ev.role === "branch_start") {
@@ -4527,6 +4874,7 @@ function App({
4527
4874
  flush();
4528
4875
  const repairNote = ev.repair ? describeRepair(ev.repair) : "";
4529
4876
  setStreaming(null);
4877
+ setSummary(loop.stats.summary());
4530
4878
  const finalText = ev.content || streamRef.text;
4531
4879
  setHistorical((prev) => [
4532
4880
  ...prev,
@@ -4594,8 +4942,16 @@ function App({
4594
4942
  setStreaming(null);
4595
4943
  setOngoingTool(null);
4596
4944
  setToolProgress(null);
4945
+ setStatusLine(null);
4597
4946
  setSummary(loop.stats.summary());
4598
4947
  setBusy(false);
4948
+ void (async () => {
4949
+ const bal = await loop.client.getBalance().catch(() => null);
4950
+ if (bal?.balance_infos.length) {
4951
+ const p = bal.balance_infos[0];
4952
+ setBalance({ currency: p.currency, total: Number(p.total_balance) });
4953
+ }
4954
+ })();
4599
4955
  }
4600
4956
  },
4601
4957
  [
@@ -4619,9 +4975,25 @@ function App({
4619
4975
  model: loop.model,
4620
4976
  prefixHash,
4621
4977
  harvestOn: loop.harvestEnabled,
4622
- branchBudget: loop.branchOptions.budget
4978
+ branchBudget: loop.branchOptions.budget,
4979
+ balance
4623
4980
  }
4624
- ), /* @__PURE__ */ React7.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React7.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React7.createElement(Box7, { marginY: 1 }, /* @__PURE__ */ React7.createElement(EventRow, { event: streaming })) : null, ongoingTool ? /* @__PURE__ */ React7.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, /* @__PURE__ */ React7.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React7.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }));
4981
+ ), /* @__PURE__ */ React7.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React7.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React7.createElement(Box7, { marginY: 1 }, /* @__PURE__ */ React7.createElement(EventRow, { event: streaming })) : null, ongoingTool ? /* @__PURE__ */ React7.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !ongoingTool && statusLine ? /* @__PURE__ */ React7.createElement(StatusRow, { text: statusLine }) : null, busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React7.createElement(StatusRow, { text: "processing\u2026" }) : null, /* @__PURE__ */ React7.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React7.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }));
4982
+ }
4983
+ function StatusRow({ text }) {
4984
+ const [tick, setTick] = useState3(0);
4985
+ const [elapsed, setElapsed] = useState3(0);
4986
+ useEffect3(() => {
4987
+ const start = Date.now();
4988
+ const frameId = setInterval(() => setTick((t) => t + 1), 120);
4989
+ const secId = setInterval(() => setElapsed(Math.floor((Date.now() - start) / 1e3)), 1e3);
4990
+ return () => {
4991
+ clearInterval(frameId);
4992
+ clearInterval(secId);
4993
+ };
4994
+ }, []);
4995
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4996
+ return /* @__PURE__ */ React7.createElement(Box7, { marginY: 1 }, /* @__PURE__ */ React7.createElement(Text7, { color: "magenta" }, frames[tick % frames.length]), /* @__PURE__ */ React7.createElement(Text7, { color: "magenta" }, ` ${text}`), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, ` ${elapsed}s`));
4625
4997
  }
4626
4998
  function OngoingToolRow({
4627
4999
  tool,
@@ -5242,6 +5614,8 @@ function ReplayApp({ meta, pages }) {
5242
5614
  const summary = {
5243
5615
  turns: cumStats.turns,
5244
5616
  totalCostUsd: cumStats.totalCostUsd,
5617
+ totalInputCostUsd: cumStats.totalInputCostUsd,
5618
+ totalOutputCostUsd: cumStats.totalOutputCostUsd,
5245
5619
  claudeEquivalentUsd: cumStats.claudeEquivalentUsd,
5246
5620
  savingsVsClaudePct: cumStats.savingsVsClaudePct,
5247
5621
  cacheHitRatio: cumStats.cacheHitRatio,