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 +401 -27
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +68 -9
- package/dist/index.js +229 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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}
|
|
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 } =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
|
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 {
|
|
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
|
-
" /
|
|
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
|
|
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,
|