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