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 +448 -31
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +77 -9
- package/dist/index.js +238 -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 {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3625
|
-
|
|
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))
|
|
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
|
|
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 {
|
|
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
|
-
" /
|
|
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
|
|
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:
|
|
4919
|
+
id: iterId,
|
|
4535
4920
|
role: "assistant",
|
|
4536
4921
|
text: finalText,
|
|
4537
|
-
reasoning:
|
|
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,
|