reasonix 0.4.9 → 0.4.13

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 {
@@ -1534,6 +1610,15 @@ var CacheFirstLoop = class {
1534
1610
  if (d.argumentsDelta)
1535
1611
  cur.function.arguments = (cur.function.arguments ?? "") + d.argumentsDelta;
1536
1612
  callBuf.set(d.index, cur);
1613
+ if (cur.function.name) {
1614
+ yield {
1615
+ turn: this._turn,
1616
+ role: "tool_call_delta",
1617
+ content: "",
1618
+ toolName: cur.function.name,
1619
+ toolCallArgsChars: (cur.function.arguments ?? "").length
1620
+ };
1621
+ }
1537
1622
  }
1538
1623
  if (chunk.usage) usage = chunk.usage;
1539
1624
  }
@@ -1565,7 +1650,14 @@ var CacheFirstLoop = class {
1565
1650
  pendingUser = null;
1566
1651
  }
1567
1652
  this.scratch.reasoning = reasoningContent || null;
1568
- const planState = preHarvestedPlanState ? preHarvestedPlanState : this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions) : emptyPlanState();
1653
+ if (!preHarvestedPlanState && this.harvestEnabled && (reasoningContent?.trim().length ?? 0) >= 40) {
1654
+ yield {
1655
+ turn: this._turn,
1656
+ role: "status",
1657
+ content: "extracting plan state from reasoning\u2026"
1658
+ };
1659
+ }
1660
+ const planState = preHarvestedPlanState ? preHarvestedPlanState : this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions, signal) : emptyPlanState();
1569
1661
  const { calls: repairedCalls, report } = this.repair.process(
1570
1662
  toolCalls,
1571
1663
  reasoningContent || null,
@@ -1587,15 +1679,38 @@ var CacheFirstLoop = class {
1587
1679
  }
1588
1680
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
1589
1681
  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;
1682
+ const before = usage.promptTokens;
1683
+ const compactResult = this.compact(4e3);
1684
+ if (compactResult.healedCount > 0) {
1685
+ const approxSaved = Math.round(compactResult.charsSaved / 4);
1686
+ const after = before - approxSaved;
1687
+ yield {
1688
+ turn: this._turn,
1689
+ role: "warning",
1690
+ content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} \u2014 auto-compacted ${compactResult.healedCount} oversized tool result(s), saved ~${approxSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Continuing.`
1691
+ };
1692
+ } else {
1693
+ yield {
1694
+ turn: this._turn,
1695
+ role: "warning",
1696
+ content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
1697
+ before / ctxMax * 100
1698
+ )}%) \u2014 nothing to auto-compact. Forcing summary from what was gathered.`
1699
+ };
1700
+ const tail = this.log.entries[this.log.entries.length - 1];
1701
+ if (tail && tail.role === "assistant" && Array.isArray(tail.tool_calls) && tail.tool_calls.length > 0) {
1702
+ const kept = this.log.entries.slice(0, -1);
1703
+ this.log.compactInPlace([...kept]);
1704
+ if (this.sessionName) {
1705
+ try {
1706
+ rewriteSession(this.sessionName, kept);
1707
+ } catch {
1708
+ }
1709
+ }
1710
+ }
1711
+ yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
1712
+ return;
1713
+ }
1599
1714
  }
1600
1715
  for (const call of repairedCalls) {
1601
1716
  const name = call.function?.name ?? "";
@@ -1627,6 +1742,11 @@ var CacheFirstLoop = class {
1627
1742
  }
1628
1743
  async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
1629
1744
  try {
1745
+ yield {
1746
+ turn: this._turn,
1747
+ role: "status",
1748
+ content: "summarizing what was gathered\u2026"
1749
+ };
1630
1750
  const messages = this.buildMessages(null);
1631
1751
  messages.push({
1632
1752
  role: "user",
@@ -1709,7 +1829,7 @@ function summarizeBranch(chosen, samples) {
1709
1829
  temperatures: samples.map((s) => s.temperature)
1710
1830
  };
1711
1831
  }
1712
- function healLoadedMessages(messages, maxChars) {
1832
+ function shrinkOversizedToolResults(messages, maxChars) {
1713
1833
  let healedCount = 0;
1714
1834
  let healedFrom = 0;
1715
1835
  const out = messages.map((msg) => {
@@ -1722,6 +1842,51 @@ function healLoadedMessages(messages, maxChars) {
1722
1842
  });
1723
1843
  return { messages: out, healedCount, healedFrom };
1724
1844
  }
1845
+ function healLoadedMessages(messages, maxChars) {
1846
+ const shrunk = shrinkOversizedToolResults(messages, maxChars);
1847
+ let healedCount = shrunk.healedCount;
1848
+ const out = [];
1849
+ const openCallIds = /* @__PURE__ */ new Set();
1850
+ let droppedAssistantCalls = 0;
1851
+ let droppedStrayTools = 0;
1852
+ for (let i = 0; i < shrunk.messages.length; i++) {
1853
+ const msg = shrunk.messages[i];
1854
+ if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
1855
+ const needed = /* @__PURE__ */ new Set();
1856
+ for (const call of msg.tool_calls) {
1857
+ if (call?.id) needed.add(call.id);
1858
+ }
1859
+ const candidates = [];
1860
+ let j = i + 1;
1861
+ while (j < shrunk.messages.length && needed.size > 0) {
1862
+ const nxt = shrunk.messages[j];
1863
+ if (nxt.role !== "tool") break;
1864
+ const id = nxt.tool_call_id ?? "";
1865
+ if (!needed.has(id)) break;
1866
+ needed.delete(id);
1867
+ candidates.push(nxt);
1868
+ j++;
1869
+ }
1870
+ if (needed.size === 0) {
1871
+ out.push(msg);
1872
+ for (const r of candidates) out.push(r);
1873
+ i = j - 1;
1874
+ } else {
1875
+ droppedAssistantCalls += 1;
1876
+ droppedStrayTools += candidates.length;
1877
+ i = j - 1;
1878
+ }
1879
+ continue;
1880
+ }
1881
+ if (msg.role === "tool") {
1882
+ droppedStrayTools += 1;
1883
+ continue;
1884
+ }
1885
+ out.push(msg);
1886
+ }
1887
+ healedCount += droppedAssistantCalls + droppedStrayTools;
1888
+ return { messages: out, healedCount, healedFrom: shrunk.healedFrom };
1889
+ }
1725
1890
  function formatLoopError(err) {
1726
1891
  const msg = err.message ?? "";
1727
1892
  if (msg.includes("maximum context length")) {
@@ -1978,7 +2143,12 @@ function registerFilesystemTools(registry, opts) {
1978
2143
  }
1979
2144
  const after = before.slice(0, firstIdx) + args.replace + before.slice(firstIdx + args.search.length);
1980
2145
  await fs.writeFile(abs, after, "utf8");
1981
- return `edited ${pathMod.relative(rootDir, abs)} (${args.search.length}\u2192${args.replace.length} chars)`;
2146
+ const rel = pathMod.relative(rootDir, abs);
2147
+ const header = `edited ${rel} (${args.search.length}\u2192${args.replace.length} chars)`;
2148
+ const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
2149
+ const diff = renderEditDiff(args.search, args.replace, startLine);
2150
+ return `${header}
2151
+ ${diff}`;
1982
2152
  }
1983
2153
  });
1984
2154
  registry.register({
@@ -2016,6 +2186,51 @@ function registerFilesystemTools(registry, opts) {
2016
2186
  });
2017
2187
  return registry;
2018
2188
  }
2189
+ function renderEditDiff(search, replace, startLine) {
2190
+ const a = search.split(/\r?\n/);
2191
+ const b = replace.split(/\r?\n/);
2192
+ const diff = lineDiff(a, b);
2193
+ const hunk = `@@ -${startLine},${a.length} +${startLine},${b.length} @@`;
2194
+ const body = diff.map((d) => `${d.op === " " ? " " : d.op} ${d.line}`).join("\n");
2195
+ return `${hunk}
2196
+ ${body}`;
2197
+ }
2198
+ function lineDiff(a, b) {
2199
+ const n = a.length;
2200
+ const m = b.length;
2201
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
2202
+ for (let i2 = 1; i2 <= n; i2++) {
2203
+ for (let j2 = 1; j2 <= m; j2++) {
2204
+ if (a[i2 - 1] === b[j2 - 1]) dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
2205
+ else dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
2206
+ }
2207
+ }
2208
+ const out = [];
2209
+ let i = n;
2210
+ let j = m;
2211
+ while (i > 0 && j > 0) {
2212
+ if (a[i - 1] === b[j - 1]) {
2213
+ out.unshift({ op: " ", line: a[i - 1] });
2214
+ i--;
2215
+ j--;
2216
+ } else if ((dp[i - 1][j] ?? 0) > (dp[i][j - 1] ?? 0)) {
2217
+ out.unshift({ op: "-", line: a[i - 1] });
2218
+ i--;
2219
+ } else {
2220
+ out.unshift({ op: "+", line: b[j - 1] });
2221
+ j--;
2222
+ }
2223
+ }
2224
+ while (i > 0) {
2225
+ out.unshift({ op: "-", line: a[i - 1] });
2226
+ i--;
2227
+ }
2228
+ while (j > 0) {
2229
+ out.unshift({ op: "+", line: b[j - 1] });
2230
+ j--;
2231
+ }
2232
+ return out;
2233
+ }
2019
2234
 
2020
2235
  // src/env.ts
2021
2236
  import { readFileSync as readFileSync3 } from "fs";
@@ -2201,6 +2416,8 @@ function computeReplayStats(records) {
2201
2416
  }
2202
2417
  function summarizeTurns(turns) {
2203
2418
  const totalCost = turns.reduce((s, t) => s + t.cost, 0);
2419
+ const totalInput = turns.reduce((s, t) => s + inputCostUsd(t.model, t.usage), 0);
2420
+ const totalOutput = turns.reduce((s, t) => s + outputCostUsd(t.model, t.usage), 0);
2204
2421
  const totalClaude = turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0);
2205
2422
  let hit = 0;
2206
2423
  let miss = 0;
@@ -2214,6 +2431,8 @@ function summarizeTurns(turns) {
2214
2431
  return {
2215
2432
  turns: turns.length,
2216
2433
  totalCostUsd: round2(totalCost, 6),
2434
+ totalInputCostUsd: round2(totalInput, 6),
2435
+ totalOutputCostUsd: round2(totalOutput, 6),
2217
2436
  claudeEquivalentUsd: round2(totalClaude, 6),
2218
2437
  savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
2219
2438
  cacheHitRatio: round2(cacheHitRatio, 4),
@@ -3501,6 +3720,25 @@ function parseBlocks(raw) {
3501
3720
  out.push({ kind: "heading", level: hm[1].length, text: hm[2].trim() });
3502
3721
  continue;
3503
3722
  }
3723
+ if (line.includes("|")) {
3724
+ const next = (lines[i + 1] ?? "").trim();
3725
+ if (/^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(next)) {
3726
+ flushPara();
3727
+ flushList();
3728
+ const header = splitTableRow(line);
3729
+ const rows = [];
3730
+ let j = i + 2;
3731
+ while (j < lines.length) {
3732
+ const r = lines[j].replace(/\s+$/g, "");
3733
+ if (r.trim() === "" || !r.includes("|")) break;
3734
+ rows.push(splitTableRow(r));
3735
+ j++;
3736
+ }
3737
+ out.push({ kind: "table", header, rows });
3738
+ i = j - 1;
3739
+ continue;
3740
+ }
3741
+ }
3504
3742
  const bm = line.match(/^\s*[-*+]\s+(.+)$/);
3505
3743
  if (bm) {
3506
3744
  flushPara();
@@ -3543,10 +3781,55 @@ function BlockView({ block }) {
3543
3781
  return /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, block.text));
3544
3782
  case "edit-block":
3545
3783
  return /* @__PURE__ */ React2.createElement(EditBlockRow, { block });
3784
+ case "table":
3785
+ return /* @__PURE__ */ React2.createElement(TableBlockRow, { block });
3546
3786
  case "hr":
3547
3787
  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
3788
  }
3549
3789
  }
3790
+ function splitTableRow(line) {
3791
+ const SENTINEL = "\0";
3792
+ const masked = line.replace(/\\\|/g, SENTINEL);
3793
+ const trimmed = masked.trim().replace(/^\||\|$/g, "");
3794
+ return trimmed.split("|").map((c) => c.trim().replace(new RegExp(SENTINEL, "g"), "|"));
3795
+ }
3796
+ function TableBlockRow({ block }) {
3797
+ const colCount = Math.max(block.header.length, ...block.rows.map((r) => r.length));
3798
+ const widths = [];
3799
+ for (let c = 0; c < colCount; c++) {
3800
+ const cellLengths = [displayWidth(block.header[c] ?? "")];
3801
+ for (const r of block.rows) cellLengths.push(displayWidth(r[c] ?? ""));
3802
+ widths.push(Math.min(40, Math.max(3, ...cellLengths)));
3803
+ }
3804
+ const pad2 = (s, w) => {
3805
+ const dw = displayWidth(s);
3806
+ if (dw >= w) return s;
3807
+ return s + " ".repeat(w - dw);
3808
+ };
3809
+ const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u253C\u2500");
3810
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Box2, null, block.header.map((cell, ci) => (
3811
+ // biome-ignore lint/suspicious/noArrayIndexKey: table columns never reorder — derived from a static header array
3812
+ /* @__PURE__ */ React2.createElement(Text2, { key: `h-${ci}`, bold: true, color: "cyan" }, pad2(cell, widths[ci] ?? 3), ci < colCount - 1 ? " \u2502 " : "")
3813
+ ))), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, separator), block.rows.map((row2, ri) => (
3814
+ // biome-ignore lint/suspicious/noArrayIndexKey: table rows render in source order and don't reorder
3815
+ /* @__PURE__ */ React2.createElement(Box2, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
3816
+ // biome-ignore lint/suspicious/noArrayIndexKey: same — column axis is fixed by the table shape
3817
+ /* @__PURE__ */ React2.createElement(Text2, { key: `c-${ri}-${ci}` }, pad2(row2[ci] ?? "", widths[ci] ?? 3), ci < colCount - 1 ? " \u2502 " : "")
3818
+ )))
3819
+ )));
3820
+ }
3821
+ function displayWidth(s) {
3822
+ let w = 0;
3823
+ for (const ch of s) {
3824
+ const code = ch.codePointAt(0) ?? 0;
3825
+ 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) {
3826
+ w += 2;
3827
+ } else {
3828
+ w += 1;
3829
+ }
3830
+ }
3831
+ return w;
3832
+ }
3550
3833
  function EditBlockRow({ block }) {
3551
3834
  const isNewFile = block.search.length === 0;
3552
3835
  const searchLines = block.search.split("\n");
@@ -3572,7 +3855,8 @@ var EventRow = React3.memo(function EventRow2({ event }) {
3572
3855
  const isError = event.text.startsWith("ERROR:");
3573
3856
  const color = isError ? "red" : "yellow";
3574
3857
  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)));
3858
+ const isEditFile = (event.toolName === "edit_file" || event.toolName?.endsWith("_edit_file")) && !isError;
3859
+ 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
3860
  }
3577
3861
  if (event.role === "error") {
3578
3862
  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 +3869,20 @@ var EventRow = React3.memo(function EventRow2({ event }) {
3585
3869
  }
3586
3870
  return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, null, event.text));
3587
3871
  });
3872
+ function EditFileDiff({ text }) {
3873
+ const lines = text.split(/\r?\n/);
3874
+ const [statusHeader, hunkHeader, ...body] = lines;
3875
+ 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) => {
3876
+ const key = `${i}-${line.slice(0, 32)}`;
3877
+ if (line.startsWith("- ")) {
3878
+ return /* @__PURE__ */ React3.createElement(Text3, { key, color: "red" }, line);
3879
+ }
3880
+ if (line.startsWith("+ ")) {
3881
+ return /* @__PURE__ */ React3.createElement(Text3, { key, color: "green" }, line);
3882
+ }
3883
+ return /* @__PURE__ */ React3.createElement(Text3, { key, dimColor: true }, line);
3884
+ }));
3885
+ }
3588
3886
  function BranchBlock({ branch }) {
3589
3887
  const per = branch.uncertainties.map((u, i) => {
3590
3888
  const marker = i === branch.chosenIndex ? "\u25B8" : " ";
@@ -3621,8 +3919,31 @@ function StreamingAssistant({ event }) {
3621
3919
  }
3622
3920
  const tail = lastLine(event.text, 140);
3623
3921
  const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
3624
- 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)"));
3922
+ const toolCallBuild = event.toolCallBuild;
3923
+ const preFirstByte = !event.text && !event.reasoning && !toolCallBuild;
3924
+ const reasoningOnly = !event.text && !!event.reasoning && !toolCallBuild;
3925
+ const toolCallOnly = !event.text && !event.reasoning && !!toolCallBuild;
3926
+ let label;
3927
+ let labelColor;
3928
+ if (preFirstByte) {
3929
+ label = "request sent \xB7 waiting for server";
3930
+ labelColor = "yellow";
3931
+ } else if (reasoningOnly) {
3932
+ label = `R1 reasoning \xB7 ${event.reasoning?.length ?? 0} chars of thought`;
3933
+ labelColor = "cyan";
3934
+ } else if (toolCallOnly) {
3935
+ label = `assembling tool call <${toolCallBuild.name}> \xB7 ${toolCallBuild.chars} chars of arguments`;
3936
+ labelColor = "magenta";
3937
+ } else {
3938
+ const parts = [`writing response \xB7 ${event.text.length} chars`];
3939
+ if (event.reasoning) parts.push(`after ${event.reasoning.length} chars of reasoning`);
3940
+ if (toolCallBuild) {
3941
+ parts.push(`building tool call <${toolCallBuild.name}> \xB7 ${toolCallBuild.chars} chars`);
3942
+ }
3943
+ label = parts.join(" \xB7 ");
3944
+ labelColor = "green";
3945
+ }
3946
+ 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
3947
  }
3627
3948
  function Pulse() {
3628
3949
  const [tick, setTick] = useState(0);
@@ -3778,7 +4099,8 @@ function StatsPanel({
3778
4099
  model,
3779
4100
  prefixHash,
3780
4101
  harvestOn,
3781
- branchBudget
4102
+ branchBudget,
4103
+ balance
3782
4104
  }) {
3783
4105
  const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
3784
4106
  const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
@@ -3786,7 +4108,7 @@ function StatsPanel({
3786
4108
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
3787
4109
  const ctxRatio = summary.lastPromptTokens / ctxMax;
3788
4110
  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));
4111
+ 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
4112
  }
3791
4113
  function formatTokens(n) {
3792
4114
  if (n < 1e3) return String(n);
@@ -3815,7 +4137,8 @@ var SLASH_COMMANDS = [
3815
4137
  { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
3816
4138
  { cmd: "forget", summary: "delete the current session from disk" },
3817
4139
  { cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
3818
- { cmd: "clear", summary: "clear the visible scrollback (log + session kept)" },
4140
+ { cmd: "clear", summary: "clear visible scrollback only (log/context kept)" },
4141
+ { cmd: "new", summary: "start a fresh conversation (clear context + scrollback)" },
3819
4142
  { cmd: "exit", summary: "quit the TUI" },
3820
4143
  // Code-mode only
3821
4144
  { cmd: "apply", summary: "commit pending edit blocks to disk", contextual: "code" },
@@ -3848,7 +4171,18 @@ function handleSlash(cmd, args, loop, ctx = {}) {
3848
4171
  case "quit":
3849
4172
  return { exit: true };
3850
4173
  case "clear":
3851
- return { clear: true };
4174
+ return {
4175
+ clear: true,
4176
+ 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."
4177
+ };
4178
+ case "new":
4179
+ case "reset": {
4180
+ const { dropped } = loop.clearLog();
4181
+ return {
4182
+ clear: true,
4183
+ info: `\u25B8 new conversation \u2014 dropped ${dropped} message(s) from context. Same session, fresh slate.`
4184
+ };
4185
+ }
3852
4186
  case "help":
3853
4187
  case "?":
3854
4188
  return {
@@ -3872,7 +4206,8 @@ function handleSlash(cmd, args, loop, ctx = {}) {
3872
4206
  ' /commit "msg" (code mode) git add -A && git commit -m "msg"',
3873
4207
  " /sessions list saved sessions (current is marked with \u25B8)",
3874
4208
  " /forget delete the current session from disk",
3875
- " /clear clear displayed history (log + session kept)",
4209
+ " /new start fresh: drop all context + clear scrollback",
4210
+ " /clear clear displayed scrollback only (context kept \u2014 model still sees it)",
3876
4211
  " /exit quit",
3877
4212
  "",
3878
4213
  "Presets:",
@@ -4243,15 +4578,20 @@ function App({
4243
4578
  const abortedThisTurn = useRef(false);
4244
4579
  const [ongoingTool, setOngoingTool] = useState3(null);
4245
4580
  const [toolProgress, setToolProgress] = useState3(null);
4581
+ const [statusLine, setStatusLine] = useState3(null);
4582
+ const [balance, setBalance] = useState3(null);
4246
4583
  const lastEditSnapshots = useRef(null);
4247
4584
  const pendingEdits = useRef([]);
4248
4585
  const promptHistory = useRef([]);
4249
4586
  const historyCursor = useRef(-1);
4587
+ const assistantIterCounter = useRef(0);
4250
4588
  const toolHistoryRef = useRef([]);
4251
4589
  const [slashSelected, setSlashSelected] = useState3(0);
4252
4590
  const [summary, setSummary] = useState3({
4253
4591
  turns: 0,
4254
4592
  totalCostUsd: 0,
4593
+ totalInputCostUsd: 0,
4594
+ totalOutputCostUsd: 0,
4255
4595
  claudeEquivalentUsd: 0,
4256
4596
  savingsVsClaudePct: 0,
4257
4597
  cacheHitRatio: 0,
@@ -4294,6 +4634,18 @@ function App({
4294
4634
  loopRef.current = l;
4295
4635
  return l;
4296
4636
  }, [model, system, harvest2, branch, session, tools]);
4637
+ useEffect3(() => {
4638
+ let cancelled = false;
4639
+ void (async () => {
4640
+ const bal = await loop.client.getBalance().catch(() => null);
4641
+ if (cancelled || !bal || !bal.balance_infos.length) return;
4642
+ const primary = bal.balance_infos[0];
4643
+ setBalance({ currency: primary.currency, total: Number(primary.total_balance) });
4644
+ })();
4645
+ return () => {
4646
+ cancelled = true;
4647
+ };
4648
+ }, [loop]);
4297
4649
  useEffect3(() => {
4298
4650
  if (!progressSink) return;
4299
4651
  progressSink.current = (info) => {
@@ -4455,6 +4807,16 @@ function App({
4455
4807
  exit();
4456
4808
  return;
4457
4809
  }
4810
+ if (result.clear && result.info) {
4811
+ setHistorical([
4812
+ {
4813
+ id: `sys-${Date.now()}`,
4814
+ role: "info",
4815
+ text: result.info
4816
+ }
4817
+ ]);
4818
+ return;
4819
+ }
4458
4820
  if (result.clear) {
4459
4821
  setHistorical([]);
4460
4822
  return;
@@ -4482,20 +4844,28 @@ function App({
4482
4844
  const streamRef = { id: assistantId, text: "", reasoning: "" };
4483
4845
  const contentBuf = { current: "" };
4484
4846
  const reasoningBuf = { current: "" };
4847
+ const toolCallBuildBuf = {
4848
+ current: null
4849
+ };
4485
4850
  setStreaming({ id: assistantId, role: "assistant", text: "", streaming: true });
4486
4851
  setBusy(true);
4487
4852
  abortedThisTurn.current = false;
4488
4853
  const flush = () => {
4489
- if (!contentBuf.current && !reasoningBuf.current) return;
4854
+ if (!contentBuf.current && !reasoningBuf.current && !toolCallBuildBuf.current) return;
4490
4855
  streamRef.text += contentBuf.current;
4491
4856
  streamRef.reasoning += reasoningBuf.current;
4857
+ if (toolCallBuildBuf.current) {
4858
+ streamRef.toolCallBuild = toolCallBuildBuf.current;
4859
+ }
4492
4860
  contentBuf.current = "";
4493
4861
  reasoningBuf.current = "";
4862
+ toolCallBuildBuf.current = null;
4494
4863
  setStreaming({
4495
4864
  id: assistantId,
4496
4865
  role: "assistant",
4497
4866
  text: streamRef.text,
4498
4867
  reasoning: streamRef.reasoning || void 0,
4868
+ toolCallBuild: streamRef.toolCallBuild,
4499
4869
  streaming: true
4500
4870
  });
4501
4871
  };
@@ -4503,9 +4873,21 @@ function App({
4503
4873
  try {
4504
4874
  for await (const ev of loop.step(text)) {
4505
4875
  writeTranscript(ev);
4506
- if (ev.role === "assistant_delta") {
4876
+ if (ev.role !== "status") {
4877
+ setStatusLine((cur) => cur ? null : cur);
4878
+ }
4879
+ if (ev.role === "status") {
4880
+ setStatusLine(ev.content);
4881
+ } else if (ev.role === "assistant_delta") {
4507
4882
  if (ev.content) contentBuf.current += ev.content;
4508
4883
  if (ev.reasoningDelta) reasoningBuf.current += ev.reasoningDelta;
4884
+ } else if (ev.role === "tool_call_delta") {
4885
+ if (ev.toolName) {
4886
+ toolCallBuildBuf.current = {
4887
+ name: ev.toolName,
4888
+ chars: ev.toolCallArgsChars ?? 0
4889
+ };
4890
+ }
4509
4891
  } else if (ev.role === "branch_start") {
4510
4892
  setStreaming({
4511
4893
  id: assistantId,
@@ -4527,14 +4909,17 @@ function App({
4527
4909
  flush();
4528
4910
  const repairNote = ev.repair ? describeRepair(ev.repair) : "";
4529
4911
  setStreaming(null);
4912
+ setSummary(loop.stats.summary());
4530
4913
  const finalText = ev.content || streamRef.text;
4914
+ const iterReasoning = streamRef.reasoning || void 0;
4915
+ const iterId = `${assistantId}-i${assistantIterCounter.current++}`;
4531
4916
  setHistorical((prev) => [
4532
4917
  ...prev,
4533
4918
  {
4534
- id: assistantId,
4919
+ id: iterId,
4535
4920
  role: "assistant",
4536
4921
  text: finalText,
4537
- reasoning: streamRef.reasoning || void 0,
4922
+ reasoning: iterReasoning,
4538
4923
  planState: ev.planState,
4539
4924
  branch: ev.branch,
4540
4925
  stats: ev.stats,
@@ -4542,6 +4927,12 @@ function App({
4542
4927
  streaming: false
4543
4928
  }
4544
4929
  ]);
4930
+ streamRef.text = "";
4931
+ streamRef.reasoning = "";
4932
+ streamRef.toolCallBuild = void 0;
4933
+ contentBuf.current = "";
4934
+ reasoningBuf.current = "";
4935
+ toolCallBuildBuf.current = null;
4545
4936
  if (codeMode && finalText && !ev.forcedSummary) {
4546
4937
  const blocks = parseEditBlocks(finalText);
4547
4938
  if (blocks.length > 0) {
@@ -4594,8 +4985,16 @@ function App({
4594
4985
  setStreaming(null);
4595
4986
  setOngoingTool(null);
4596
4987
  setToolProgress(null);
4988
+ setStatusLine(null);
4597
4989
  setSummary(loop.stats.summary());
4598
4990
  setBusy(false);
4991
+ void (async () => {
4992
+ const bal = await loop.client.getBalance().catch(() => null);
4993
+ if (bal?.balance_infos.length) {
4994
+ const p = bal.balance_infos[0];
4995
+ setBalance({ currency: p.currency, total: Number(p.total_balance) });
4996
+ }
4997
+ })();
4599
4998
  }
4600
4999
  },
4601
5000
  [
@@ -4619,9 +5018,25 @@ function App({
4619
5018
  model: loop.model,
4620
5019
  prefixHash,
4621
5020
  harvestOn: loop.harvestEnabled,
4622
- branchBudget: loop.branchOptions.budget
5021
+ branchBudget: loop.branchOptions.budget,
5022
+ balance
4623
5023
  }
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 }));
5024
+ ), /* @__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 }));
5025
+ }
5026
+ function StatusRow({ text }) {
5027
+ const [tick, setTick] = useState3(0);
5028
+ const [elapsed, setElapsed] = useState3(0);
5029
+ useEffect3(() => {
5030
+ const start = Date.now();
5031
+ const frameId = setInterval(() => setTick((t) => t + 1), 120);
5032
+ const secId = setInterval(() => setElapsed(Math.floor((Date.now() - start) / 1e3)), 1e3);
5033
+ return () => {
5034
+ clearInterval(frameId);
5035
+ clearInterval(secId);
5036
+ };
5037
+ }, []);
5038
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
5039
+ 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
5040
  }
4626
5041
  function OngoingToolRow({
4627
5042
  tool,
@@ -5242,6 +5657,8 @@ function ReplayApp({ meta, pages }) {
5242
5657
  const summary = {
5243
5658
  turns: cumStats.turns,
5244
5659
  totalCostUsd: cumStats.totalCostUsd,
5660
+ totalInputCostUsd: cumStats.totalInputCostUsd,
5661
+ totalOutputCostUsd: cumStats.totalOutputCostUsd,
5245
5662
  claudeEquivalentUsd: cumStats.claudeEquivalentUsd,
5246
5663
  savingsVsClaudePct: cumStats.savingsVsClaudePct,
5247
5664
  cacheHitRatio: cumStats.cacheHitRatio,