reasonix 0.4.6 → 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 +1021 -162
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +194 -22
- package/dist/index.js +665 -76
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -96,8 +96,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
|
|
|
96
96
|
}
|
|
97
97
|
function sleep(ms, signal) {
|
|
98
98
|
if (ms <= 0) return Promise.resolve();
|
|
99
|
-
return new Promise((
|
|
100
|
-
const timer = setTimeout(
|
|
99
|
+
return new Promise((resolve5, reject) => {
|
|
100
|
+
const timer = setTimeout(resolve5, ms);
|
|
101
101
|
if (signal) {
|
|
102
102
|
const onAbort = () => {
|
|
103
103
|
clearTimeout(timer);
|
|
@@ -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 {
|
|
@@ -563,7 +586,7 @@ var ToolRegistry = class {
|
|
|
563
586
|
}
|
|
564
587
|
}));
|
|
565
588
|
}
|
|
566
|
-
async dispatch(name, argumentsRaw) {
|
|
589
|
+
async dispatch(name, argumentsRaw, opts = {}) {
|
|
567
590
|
const tool = this._tools.get(name);
|
|
568
591
|
if (!tool) {
|
|
569
592
|
return JSON.stringify({ error: `unknown tool: ${name}` });
|
|
@@ -580,7 +603,7 @@ var ToolRegistry = class {
|
|
|
580
603
|
args = nestArguments(args);
|
|
581
604
|
}
|
|
582
605
|
try {
|
|
583
|
-
const result = await tool.fn(args);
|
|
606
|
+
const result = await tool.fn(args, { signal: opts.signal });
|
|
584
607
|
return typeof result === "string" ? result : JSON.stringify(result);
|
|
585
608
|
} catch (err) {
|
|
586
609
|
return JSON.stringify({
|
|
@@ -614,8 +637,20 @@ async function bridgeMcpTools(client, opts = {}) {
|
|
|
614
637
|
name: registeredName,
|
|
615
638
|
description: mcpTool.description ?? "",
|
|
616
639
|
parameters: mcpTool.inputSchema,
|
|
617
|
-
fn: async (args) => {
|
|
618
|
-
const toolResult = await client.callTool(mcpTool.name, args
|
|
640
|
+
fn: async (args, ctx) => {
|
|
641
|
+
const toolResult = await client.callTool(mcpTool.name, args, {
|
|
642
|
+
// Forward server-side progress frames to the bridge caller,
|
|
643
|
+
// tagged with the registered name so multi-server UIs can
|
|
644
|
+
// disambiguate. No-op when `onProgress` isn't configured —
|
|
645
|
+
// the client then also omits the _meta.progressToken and
|
|
646
|
+
// the server won't emit progress.
|
|
647
|
+
onProgress: opts.onProgress ? (info) => opts.onProgress({ toolName: registeredName, ...info }) : void 0,
|
|
648
|
+
// Thread the tool-dispatch AbortSignal all the way down to
|
|
649
|
+
// the MCP request so Esc truly cancels in flight — the
|
|
650
|
+
// client will emit notifications/cancelled AND reject the
|
|
651
|
+
// pending promise immediately, no "wait for subprocess".
|
|
652
|
+
signal: ctx?.signal
|
|
653
|
+
});
|
|
619
654
|
return flattenMcpResult(toolResult, { maxChars: maxResultChars });
|
|
620
655
|
}
|
|
621
656
|
});
|
|
@@ -1126,6 +1161,16 @@ function costUsd(model, usage) {
|
|
|
1126
1161
|
if (!p) return 0;
|
|
1127
1162
|
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss + usage.completionTokens * p.output) / 1e6;
|
|
1128
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
|
+
}
|
|
1129
1174
|
function claudeEquivalentCost(usage) {
|
|
1130
1175
|
return (usage.promptTokens * CLAUDE_SONNET_PRICING.input + usage.completionTokens * CLAUDE_SONNET_PRICING.output) / 1e6;
|
|
1131
1176
|
}
|
|
@@ -1153,6 +1198,12 @@ var SessionStats = class {
|
|
|
1153
1198
|
const c = this.totalClaudeEquivalent;
|
|
1154
1199
|
return c > 0 ? 1 - this.totalCost / c : 0;
|
|
1155
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
|
+
}
|
|
1156
1207
|
get aggregateCacheHitRatio() {
|
|
1157
1208
|
let hit = 0;
|
|
1158
1209
|
let miss = 0;
|
|
@@ -1168,6 +1219,8 @@ var SessionStats = class {
|
|
|
1168
1219
|
return {
|
|
1169
1220
|
turns: this.turns.length,
|
|
1170
1221
|
totalCostUsd: round(this.totalCost, 6),
|
|
1222
|
+
totalInputCostUsd: round(this.totalInputCost, 6),
|
|
1223
|
+
totalOutputCostUsd: round(this.totalOutputCost, 6),
|
|
1171
1224
|
claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
|
|
1172
1225
|
savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
|
|
1173
1226
|
cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
|
|
@@ -1204,11 +1257,13 @@ var CacheFirstLoop = class {
|
|
|
1204
1257
|
_turn = 0;
|
|
1205
1258
|
_streamPreference;
|
|
1206
1259
|
/**
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
1260
|
+
* AbortController per active turn. Threaded through the DeepSeek
|
|
1261
|
+
* HTTP calls AND every tool dispatch so Esc actually cancels the
|
|
1262
|
+
* in-flight network/subprocess work — not "we'll get to it after
|
|
1263
|
+
* the current call finishes." Re-created at the start of each
|
|
1264
|
+
* `step()` (the prior turn's signal has already fired).
|
|
1210
1265
|
*/
|
|
1211
|
-
|
|
1266
|
+
_turnAbort = new AbortController();
|
|
1212
1267
|
constructor(opts) {
|
|
1213
1268
|
this.client = opts.client;
|
|
1214
1269
|
this.prefix = opts.prefix;
|
|
@@ -1240,8 +1295,12 @@ var CacheFirstLoop = class {
|
|
|
1240
1295
|
for (const msg of messages) this.log.append(msg);
|
|
1241
1296
|
this.resumedMessageCount = messages.length;
|
|
1242
1297
|
if (healedCount > 0) {
|
|
1298
|
+
try {
|
|
1299
|
+
rewriteSession(this.sessionName, messages);
|
|
1300
|
+
} catch {
|
|
1301
|
+
}
|
|
1243
1302
|
process.stderr.write(
|
|
1244
|
-
`\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.
|
|
1245
1304
|
`
|
|
1246
1305
|
);
|
|
1247
1306
|
}
|
|
@@ -1262,7 +1321,7 @@ var CacheFirstLoop = class {
|
|
|
1262
1321
|
*/
|
|
1263
1322
|
compact(tightCapChars = 4e3) {
|
|
1264
1323
|
const before = this.log.toMessages();
|
|
1265
|
-
const { messages, healedCount, healedFrom } =
|
|
1324
|
+
const { messages, healedCount, healedFrom } = shrinkOversizedToolResults(before, tightCapChars);
|
|
1266
1325
|
const afterBytes = messages.filter((m) => m.role === "tool").reduce((s, m) => s + (typeof m.content === "string" ? m.content.length : 0), 0);
|
|
1267
1326
|
const charsSaved = healedFrom - afterBytes;
|
|
1268
1327
|
if (healedCount > 0) {
|
|
@@ -1285,6 +1344,29 @@ var CacheFirstLoop = class {
|
|
|
1285
1344
|
}
|
|
1286
1345
|
}
|
|
1287
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
|
+
}
|
|
1288
1370
|
/**
|
|
1289
1371
|
* Reconfigure model/harvest/branch/stream mid-session. The loop's log,
|
|
1290
1372
|
* scratch, and stats are preserved — only the per-turn behavior changes.
|
|
@@ -1316,19 +1398,21 @@ var CacheFirstLoop = class {
|
|
|
1316
1398
|
this.stream = this.branchEnabled ? false : this._streamPreference;
|
|
1317
1399
|
}
|
|
1318
1400
|
buildMessages(pendingUser) {
|
|
1319
|
-
const
|
|
1401
|
+
const healed = healLoadedMessages(this.log.toMessages(), DEFAULT_MAX_RESULT_CHARS);
|
|
1402
|
+
const msgs = [...this.prefix.toMessages(), ...healed.messages];
|
|
1320
1403
|
if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
|
|
1321
1404
|
return msgs;
|
|
1322
1405
|
}
|
|
1323
1406
|
/**
|
|
1324
|
-
* Signal the currently-running {@link step}
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1407
|
+
* Signal the currently-running {@link step} to stop **now**. Cancels
|
|
1408
|
+
* the in-flight network request (DeepSeek HTTP/SSE) AND any tool call
|
|
1409
|
+
* currently dispatching (MCP `notifications/cancelled` + promise
|
|
1410
|
+
* reject). The loop itself also sees `signal.aborted` at each
|
|
1411
|
+
* iteration boundary and exits quickly instead of looping again.
|
|
1412
|
+
* Called by the TUI on Esc.
|
|
1329
1413
|
*/
|
|
1330
1414
|
abort() {
|
|
1331
|
-
this.
|
|
1415
|
+
this._turnAbort.abort();
|
|
1332
1416
|
}
|
|
1333
1417
|
/**
|
|
1334
1418
|
* Drop everything in the log after (and including) the most recent
|
|
@@ -1366,13 +1450,14 @@ var CacheFirstLoop = class {
|
|
|
1366
1450
|
async *step(userInput) {
|
|
1367
1451
|
this._turn++;
|
|
1368
1452
|
this.scratch.reset();
|
|
1369
|
-
this.
|
|
1453
|
+
this._turnAbort = new AbortController();
|
|
1454
|
+
const signal = this._turnAbort.signal;
|
|
1370
1455
|
let pendingUser = userInput;
|
|
1371
1456
|
const toolSpecs = this.prefix.tools();
|
|
1372
1457
|
const warnAt = Math.max(1, Math.floor(this.maxToolIters * 0.7));
|
|
1373
1458
|
let warnedForIterBudget = false;
|
|
1374
1459
|
for (let iter = 0; iter < this.maxToolIters; iter++) {
|
|
1375
|
-
if (
|
|
1460
|
+
if (signal.aborted) {
|
|
1376
1461
|
yield {
|
|
1377
1462
|
turn: this._turn,
|
|
1378
1463
|
role: "warning",
|
|
@@ -1389,6 +1474,13 @@ var CacheFirstLoop = class {
|
|
|
1389
1474
|
yield { turn: this._turn, role: "done", content: stoppedMsg };
|
|
1390
1475
|
return;
|
|
1391
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
|
+
}
|
|
1392
1484
|
if (!warnedForIterBudget && iter >= warnAt) {
|
|
1393
1485
|
warnedForIterBudget = true;
|
|
1394
1486
|
yield {
|
|
@@ -1435,7 +1527,8 @@ var CacheFirstLoop = class {
|
|
|
1435
1527
|
{
|
|
1436
1528
|
model: this.model,
|
|
1437
1529
|
messages,
|
|
1438
|
-
tools: toolSpecs.length ? toolSpecs : void 0
|
|
1530
|
+
tools: toolSpecs.length ? toolSpecs : void 0,
|
|
1531
|
+
signal
|
|
1439
1532
|
},
|
|
1440
1533
|
{
|
|
1441
1534
|
...this.branchOptions,
|
|
@@ -1444,8 +1537,8 @@ var CacheFirstLoop = class {
|
|
|
1444
1537
|
}
|
|
1445
1538
|
);
|
|
1446
1539
|
for (let k = 0; k < budget; k++) {
|
|
1447
|
-
const sample = queue.shift() ?? await new Promise((
|
|
1448
|
-
waiter =
|
|
1540
|
+
const sample = queue.shift() ?? await new Promise((resolve5) => {
|
|
1541
|
+
waiter = resolve5;
|
|
1449
1542
|
});
|
|
1450
1543
|
yield {
|
|
1451
1544
|
turn: this._turn,
|
|
@@ -1485,7 +1578,8 @@ var CacheFirstLoop = class {
|
|
|
1485
1578
|
for await (const chunk of this.client.stream({
|
|
1486
1579
|
model: this.model,
|
|
1487
1580
|
messages,
|
|
1488
|
-
tools: toolSpecs.length ? toolSpecs : void 0
|
|
1581
|
+
tools: toolSpecs.length ? toolSpecs : void 0,
|
|
1582
|
+
signal
|
|
1489
1583
|
})) {
|
|
1490
1584
|
if (chunk.contentDelta) {
|
|
1491
1585
|
assistantContent += chunk.contentDelta;
|
|
@@ -1524,7 +1618,8 @@ var CacheFirstLoop = class {
|
|
|
1524
1618
|
const resp = await this.client.chat({
|
|
1525
1619
|
model: this.model,
|
|
1526
1620
|
messages,
|
|
1527
|
-
tools: toolSpecs.length ? toolSpecs : void 0
|
|
1621
|
+
tools: toolSpecs.length ? toolSpecs : void 0,
|
|
1622
|
+
signal
|
|
1528
1623
|
});
|
|
1529
1624
|
assistantContent = resp.content;
|
|
1530
1625
|
reasoningContent = resp.reasoningContent ?? "";
|
|
@@ -1546,7 +1641,14 @@ var CacheFirstLoop = class {
|
|
|
1546
1641
|
pendingUser = null;
|
|
1547
1642
|
}
|
|
1548
1643
|
this.scratch.reasoning = reasoningContent || null;
|
|
1549
|
-
|
|
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();
|
|
1550
1652
|
const { calls: repairedCalls, report } = this.repair.process(
|
|
1551
1653
|
toolCalls,
|
|
1552
1654
|
reasoningContent || null,
|
|
@@ -1568,15 +1670,38 @@ var CacheFirstLoop = class {
|
|
|
1568
1670
|
}
|
|
1569
1671
|
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1570
1672
|
if (usage && usage.promptTokens / ctxMax > 0.8) {
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
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
|
+
}
|
|
1580
1705
|
}
|
|
1581
1706
|
for (const call of repairedCalls) {
|
|
1582
1707
|
const name = call.function?.name ?? "";
|
|
@@ -1588,7 +1713,7 @@ var CacheFirstLoop = class {
|
|
|
1588
1713
|
toolName: name,
|
|
1589
1714
|
toolArgs: args
|
|
1590
1715
|
};
|
|
1591
|
-
const result = await this.tools.dispatch(name, args);
|
|
1716
|
+
const result = await this.tools.dispatch(name, args, { signal });
|
|
1592
1717
|
this.appendAndPersist({
|
|
1593
1718
|
role: "tool",
|
|
1594
1719
|
tool_call_id: call.id ?? "",
|
|
@@ -1608,6 +1733,11 @@ var CacheFirstLoop = class {
|
|
|
1608
1733
|
}
|
|
1609
1734
|
async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
|
|
1610
1735
|
try {
|
|
1736
|
+
yield {
|
|
1737
|
+
turn: this._turn,
|
|
1738
|
+
role: "status",
|
|
1739
|
+
content: "summarizing what was gathered\u2026"
|
|
1740
|
+
};
|
|
1611
1741
|
const messages = this.buildMessages(null);
|
|
1612
1742
|
messages.push({
|
|
1613
1743
|
role: "user",
|
|
@@ -1615,8 +1745,9 @@ var CacheFirstLoop = class {
|
|
|
1615
1745
|
});
|
|
1616
1746
|
const resp = await this.client.chat({
|
|
1617
1747
|
model: this.model,
|
|
1618
|
-
messages
|
|
1748
|
+
messages,
|
|
1619
1749
|
// no tools → model is forced to answer in text
|
|
1750
|
+
signal: this._turnAbort.signal
|
|
1620
1751
|
});
|
|
1621
1752
|
const rawContent = resp.content?.trim() ?? "";
|
|
1622
1753
|
const cleaned = stripHallucinatedToolMarkup(rawContent);
|
|
@@ -1689,7 +1820,7 @@ function summarizeBranch(chosen, samples) {
|
|
|
1689
1820
|
temperatures: samples.map((s) => s.temperature)
|
|
1690
1821
|
};
|
|
1691
1822
|
}
|
|
1692
|
-
function
|
|
1823
|
+
function shrinkOversizedToolResults(messages, maxChars) {
|
|
1693
1824
|
let healedCount = 0;
|
|
1694
1825
|
let healedFrom = 0;
|
|
1695
1826
|
const out = messages.map((msg) => {
|
|
@@ -1702,6 +1833,51 @@ function healLoadedMessages(messages, maxChars) {
|
|
|
1702
1833
|
});
|
|
1703
1834
|
return { messages: out, healedCount, healedFrom };
|
|
1704
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
|
+
}
|
|
1705
1881
|
function formatLoopError(err) {
|
|
1706
1882
|
const msg = err.message ?? "";
|
|
1707
1883
|
if (msg.includes("maximum context length")) {
|
|
@@ -1712,13 +1888,348 @@ function formatLoopError(err) {
|
|
|
1712
1888
|
return msg;
|
|
1713
1889
|
}
|
|
1714
1890
|
|
|
1891
|
+
// src/tools/filesystem.ts
|
|
1892
|
+
import { promises as fs } from "fs";
|
|
1893
|
+
import * as pathMod from "path";
|
|
1894
|
+
var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
|
|
1895
|
+
var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
|
|
1896
|
+
function registerFilesystemTools(registry, opts) {
|
|
1897
|
+
const rootDir = pathMod.resolve(opts.rootDir);
|
|
1898
|
+
const allowWriting = opts.allowWriting !== false;
|
|
1899
|
+
const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
1900
|
+
const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
|
|
1901
|
+
const safePath = (raw) => {
|
|
1902
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
1903
|
+
throw new Error("path must be a non-empty string");
|
|
1904
|
+
}
|
|
1905
|
+
const resolved = pathMod.resolve(rootDir, raw);
|
|
1906
|
+
const normRoot = pathMod.resolve(rootDir);
|
|
1907
|
+
const rel = pathMod.relative(normRoot, resolved);
|
|
1908
|
+
if (rel.startsWith("..") || pathMod.isAbsolute(rel)) {
|
|
1909
|
+
throw new Error(`path escapes sandbox root (${normRoot}): ${raw}`);
|
|
1910
|
+
}
|
|
1911
|
+
return resolved;
|
|
1912
|
+
};
|
|
1913
|
+
registry.register({
|
|
1914
|
+
name: "read_file",
|
|
1915
|
+
description: "Read a file under the sandbox root. Returns the full contents (truncated with a notice if larger than the per-call cap). Paths may be relative to the root or absolute-under-root.",
|
|
1916
|
+
parameters: {
|
|
1917
|
+
type: "object",
|
|
1918
|
+
properties: {
|
|
1919
|
+
path: { type: "string", description: "Path to read (relative to rootDir or absolute)." },
|
|
1920
|
+
head: { type: "integer", description: "If set, return only the first N lines." },
|
|
1921
|
+
tail: { type: "integer", description: "If set, return only the last N lines." }
|
|
1922
|
+
},
|
|
1923
|
+
required: ["path"]
|
|
1924
|
+
},
|
|
1925
|
+
fn: async (args) => {
|
|
1926
|
+
const abs = safePath(args.path);
|
|
1927
|
+
const stat = await fs.stat(abs);
|
|
1928
|
+
if (stat.isDirectory()) {
|
|
1929
|
+
throw new Error(`not a file: ${args.path} (it's a directory)`);
|
|
1930
|
+
}
|
|
1931
|
+
const raw = await fs.readFile(abs);
|
|
1932
|
+
if (raw.length > maxReadBytes) {
|
|
1933
|
+
const head = raw.slice(0, maxReadBytes).toString("utf8");
|
|
1934
|
+
return `${head}
|
|
1935
|
+
|
|
1936
|
+
[\u2026truncated ${raw.length - maxReadBytes} bytes \u2014 file is ${raw.length} B, cap ${maxReadBytes} B. Retry with head/tail for targeted view.]`;
|
|
1937
|
+
}
|
|
1938
|
+
const text = raw.toString("utf8");
|
|
1939
|
+
if (typeof args.head === "number" && args.head > 0) {
|
|
1940
|
+
return text.split(/\r?\n/).slice(0, args.head).join("\n");
|
|
1941
|
+
}
|
|
1942
|
+
if (typeof args.tail === "number" && args.tail > 0) {
|
|
1943
|
+
let lines = text.split(/\r?\n/);
|
|
1944
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
|
|
1945
|
+
return lines.slice(Math.max(0, lines.length - args.tail)).join("\n");
|
|
1946
|
+
}
|
|
1947
|
+
return text;
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
registry.register({
|
|
1951
|
+
name: "list_directory",
|
|
1952
|
+
description: "List entries in a directory under the sandbox root. Returns one line per entry, marking directories with a trailing slash. Not recursive \u2014 use directory_tree for that.",
|
|
1953
|
+
parameters: {
|
|
1954
|
+
type: "object",
|
|
1955
|
+
properties: {
|
|
1956
|
+
path: { type: "string", description: "Directory to list (default: root)." }
|
|
1957
|
+
}
|
|
1958
|
+
},
|
|
1959
|
+
fn: async (args) => {
|
|
1960
|
+
const abs = safePath(args.path ?? ".");
|
|
1961
|
+
const entries = await fs.readdir(abs, { withFileTypes: true });
|
|
1962
|
+
const lines = [];
|
|
1963
|
+
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1964
|
+
lines.push(e.isDirectory() ? `${e.name}/` : e.name);
|
|
1965
|
+
}
|
|
1966
|
+
return lines.join("\n") || "(empty directory)";
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
registry.register({
|
|
1970
|
+
name: "directory_tree",
|
|
1971
|
+
description: "Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Caps output so a huge tree doesn't drown the context.",
|
|
1972
|
+
parameters: {
|
|
1973
|
+
type: "object",
|
|
1974
|
+
properties: {
|
|
1975
|
+
path: { type: "string", description: "Root of the tree (default: sandbox root)." },
|
|
1976
|
+
maxDepth: { type: "integer", description: "Max recursion depth (default 4)." }
|
|
1977
|
+
}
|
|
1978
|
+
},
|
|
1979
|
+
fn: async (args) => {
|
|
1980
|
+
const startAbs = safePath(args.path ?? ".");
|
|
1981
|
+
const maxDepth = typeof args.maxDepth === "number" ? args.maxDepth : 4;
|
|
1982
|
+
const lines = [];
|
|
1983
|
+
let totalBytes = 0;
|
|
1984
|
+
let truncated = false;
|
|
1985
|
+
const walk2 = async (dir, depth) => {
|
|
1986
|
+
if (truncated) return;
|
|
1987
|
+
if (depth > maxDepth) return;
|
|
1988
|
+
let entries;
|
|
1989
|
+
try {
|
|
1990
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1991
|
+
} catch {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1995
|
+
for (const e of entries) {
|
|
1996
|
+
if (truncated) return;
|
|
1997
|
+
const indent = " ".repeat(depth);
|
|
1998
|
+
const line = e.isDirectory() ? `${indent}${e.name}/` : `${indent}${e.name}`;
|
|
1999
|
+
totalBytes += line.length + 1;
|
|
2000
|
+
if (totalBytes > maxListBytes) {
|
|
2001
|
+
lines.push(` [\u2026 tree truncated at ${maxListBytes} bytes \u2026]`);
|
|
2002
|
+
truncated = true;
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
lines.push(line);
|
|
2006
|
+
if (e.isDirectory()) {
|
|
2007
|
+
await walk2(pathMod.join(dir, e.name), depth + 1);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
await walk2(startAbs, 0);
|
|
2012
|
+
return lines.join("\n") || "(empty tree)";
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
2015
|
+
registry.register({
|
|
2016
|
+
name: "search_files",
|
|
2017
|
+
description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line.",
|
|
2018
|
+
parameters: {
|
|
2019
|
+
type: "object",
|
|
2020
|
+
properties: {
|
|
2021
|
+
path: { type: "string", description: "Directory to start the search at (default: root)." },
|
|
2022
|
+
pattern: {
|
|
2023
|
+
type: "string",
|
|
2024
|
+
description: "Substring (or regex) to match against filenames."
|
|
2025
|
+
}
|
|
2026
|
+
},
|
|
2027
|
+
required: ["pattern"]
|
|
2028
|
+
},
|
|
2029
|
+
fn: async (args) => {
|
|
2030
|
+
const startAbs = safePath(args.path ?? ".");
|
|
2031
|
+
const needle = args.pattern.toLowerCase();
|
|
2032
|
+
let re = null;
|
|
2033
|
+
try {
|
|
2034
|
+
re = new RegExp(args.pattern, "i");
|
|
2035
|
+
} catch {
|
|
2036
|
+
re = null;
|
|
2037
|
+
}
|
|
2038
|
+
const matches = [];
|
|
2039
|
+
let totalBytes = 0;
|
|
2040
|
+
const walk2 = async (dir) => {
|
|
2041
|
+
let entries;
|
|
2042
|
+
try {
|
|
2043
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
2044
|
+
} catch {
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
for (const e of entries) {
|
|
2048
|
+
const full = pathMod.join(dir, e.name);
|
|
2049
|
+
const lower = e.name.toLowerCase();
|
|
2050
|
+
const hit = re ? re.test(e.name) : lower.includes(needle);
|
|
2051
|
+
if (hit) {
|
|
2052
|
+
const rel = pathMod.relative(rootDir, full);
|
|
2053
|
+
if (totalBytes + rel.length + 1 > maxListBytes) {
|
|
2054
|
+
matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
matches.push(rel);
|
|
2058
|
+
totalBytes += rel.length + 1;
|
|
2059
|
+
}
|
|
2060
|
+
if (e.isDirectory()) await walk2(full);
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
await walk2(startAbs);
|
|
2064
|
+
return matches.length === 0 ? "(no matches)" : matches.join("\n");
|
|
2065
|
+
}
|
|
2066
|
+
});
|
|
2067
|
+
registry.register({
|
|
2068
|
+
name: "get_file_info",
|
|
2069
|
+
description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
|
|
2070
|
+
parameters: {
|
|
2071
|
+
type: "object",
|
|
2072
|
+
properties: {
|
|
2073
|
+
path: { type: "string" }
|
|
2074
|
+
},
|
|
2075
|
+
required: ["path"]
|
|
2076
|
+
},
|
|
2077
|
+
fn: async (args) => {
|
|
2078
|
+
const abs = safePath(args.path);
|
|
2079
|
+
const st = await fs.lstat(abs);
|
|
2080
|
+
const type = st.isDirectory() ? "directory" : st.isSymbolicLink() ? "symlink" : "file";
|
|
2081
|
+
return JSON.stringify({
|
|
2082
|
+
type,
|
|
2083
|
+
size: st.size,
|
|
2084
|
+
mtime: st.mtime.toISOString()
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
if (!allowWriting) return registry;
|
|
2089
|
+
registry.register({
|
|
2090
|
+
name: "write_file",
|
|
2091
|
+
description: "Create or overwrite a file under the sandbox root with the given content. Parent directories are created as needed.",
|
|
2092
|
+
parameters: {
|
|
2093
|
+
type: "object",
|
|
2094
|
+
properties: {
|
|
2095
|
+
path: { type: "string" },
|
|
2096
|
+
content: { type: "string" }
|
|
2097
|
+
},
|
|
2098
|
+
required: ["path", "content"]
|
|
2099
|
+
},
|
|
2100
|
+
fn: async (args) => {
|
|
2101
|
+
const abs = safePath(args.path);
|
|
2102
|
+
await fs.mkdir(pathMod.dirname(abs), { recursive: true });
|
|
2103
|
+
await fs.writeFile(abs, args.content, "utf8");
|
|
2104
|
+
return `wrote ${args.content.length} chars to ${pathMod.relative(rootDir, abs)}`;
|
|
2105
|
+
}
|
|
2106
|
+
});
|
|
2107
|
+
registry.register({
|
|
2108
|
+
name: "edit_file",
|
|
2109
|
+
description: "Apply a SEARCH/REPLACE edit to an existing file. `search` must match exactly (whitespace sensitive) \u2014 no regex. The match must be unique in the file; otherwise the edit is refused to avoid surprise rewrites. This flat-string shape replaces the `{oldText, newText}[]` JSON array form that previously triggered R1 DSML hallucinations.",
|
|
2110
|
+
parameters: {
|
|
2111
|
+
type: "object",
|
|
2112
|
+
properties: {
|
|
2113
|
+
path: { type: "string" },
|
|
2114
|
+
search: { type: "string", description: "Exact text to find (must be unique)." },
|
|
2115
|
+
replace: { type: "string", description: "Text to substitute in place of `search`." }
|
|
2116
|
+
},
|
|
2117
|
+
required: ["path", "search", "replace"]
|
|
2118
|
+
},
|
|
2119
|
+
fn: async (args) => {
|
|
2120
|
+
const abs = safePath(args.path);
|
|
2121
|
+
const before = await fs.readFile(abs, "utf8");
|
|
2122
|
+
if (args.search.length === 0) {
|
|
2123
|
+
throw new Error("edit_file: search cannot be empty");
|
|
2124
|
+
}
|
|
2125
|
+
const firstIdx = before.indexOf(args.search);
|
|
2126
|
+
if (firstIdx < 0) {
|
|
2127
|
+
throw new Error(`edit_file: search text not found in ${pathMod.relative(rootDir, abs)}`);
|
|
2128
|
+
}
|
|
2129
|
+
const nextIdx = before.indexOf(args.search, firstIdx + 1);
|
|
2130
|
+
if (nextIdx >= 0) {
|
|
2131
|
+
throw new Error(
|
|
2132
|
+
`edit_file: search text appears multiple times in ${pathMod.relative(rootDir, abs)} \u2014 include more context to disambiguate`
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
const after = before.slice(0, firstIdx) + args.replace + before.slice(firstIdx + args.search.length);
|
|
2136
|
+
await fs.writeFile(abs, after, "utf8");
|
|
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}`;
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
registry.register({
|
|
2146
|
+
name: "create_directory",
|
|
2147
|
+
description: "Create a directory (and any missing parents) under the sandbox root.",
|
|
2148
|
+
parameters: {
|
|
2149
|
+
type: "object",
|
|
2150
|
+
properties: { path: { type: "string" } },
|
|
2151
|
+
required: ["path"]
|
|
2152
|
+
},
|
|
2153
|
+
fn: async (args) => {
|
|
2154
|
+
const abs = safePath(args.path);
|
|
2155
|
+
await fs.mkdir(abs, { recursive: true });
|
|
2156
|
+
return `created ${pathMod.relative(rootDir, abs)}/`;
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
registry.register({
|
|
2160
|
+
name: "move_file",
|
|
2161
|
+
description: "Rename/move a file or directory under the sandbox root.",
|
|
2162
|
+
parameters: {
|
|
2163
|
+
type: "object",
|
|
2164
|
+
properties: {
|
|
2165
|
+
source: { type: "string" },
|
|
2166
|
+
destination: { type: "string" }
|
|
2167
|
+
},
|
|
2168
|
+
required: ["source", "destination"]
|
|
2169
|
+
},
|
|
2170
|
+
fn: async (args) => {
|
|
2171
|
+
const src = safePath(args.source);
|
|
2172
|
+
const dst = safePath(args.destination);
|
|
2173
|
+
await fs.mkdir(pathMod.dirname(dst), { recursive: true });
|
|
2174
|
+
await fs.rename(src, dst);
|
|
2175
|
+
return `moved ${pathMod.relative(rootDir, src)} \u2192 ${pathMod.relative(rootDir, dst)}`;
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
return registry;
|
|
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
|
+
}
|
|
2225
|
+
|
|
1715
2226
|
// src/env.ts
|
|
1716
2227
|
import { readFileSync as readFileSync3 } from "fs";
|
|
1717
|
-
import { resolve } from "path";
|
|
2228
|
+
import { resolve as resolve2 } from "path";
|
|
1718
2229
|
function loadDotenv(path = ".env") {
|
|
1719
2230
|
let raw;
|
|
1720
2231
|
try {
|
|
1721
|
-
raw = readFileSync3(
|
|
2232
|
+
raw = readFileSync3(resolve2(process.cwd(), path), "utf8");
|
|
1722
2233
|
} catch {
|
|
1723
2234
|
return;
|
|
1724
2235
|
}
|
|
@@ -1896,6 +2407,8 @@ function computeReplayStats(records) {
|
|
|
1896
2407
|
}
|
|
1897
2408
|
function summarizeTurns(turns) {
|
|
1898
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);
|
|
1899
2412
|
const totalClaude = turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0);
|
|
1900
2413
|
let hit = 0;
|
|
1901
2414
|
let miss = 0;
|
|
@@ -1909,6 +2422,8 @@ function summarizeTurns(turns) {
|
|
|
1909
2422
|
return {
|
|
1910
2423
|
turns: turns.length,
|
|
1911
2424
|
totalCostUsd: round2(totalCost, 6),
|
|
2425
|
+
totalInputCostUsd: round2(totalInput, 6),
|
|
2426
|
+
totalOutputCostUsd: round2(totalOutput, 6),
|
|
1912
2427
|
claudeEquivalentUsd: round2(totalClaude, 6),
|
|
1913
2428
|
savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
|
|
1914
2429
|
cacheHitRatio: round2(cacheHitRatio, 4),
|
|
@@ -2279,6 +2794,13 @@ var McpClient = class {
|
|
|
2279
2794
|
_serverInfo = { name: "", version: "" };
|
|
2280
2795
|
_protocolVersion = "";
|
|
2281
2796
|
_instructions;
|
|
2797
|
+
// Progress-token → handler for notifications/progress routing. Tokens
|
|
2798
|
+
// are minted per call when the caller supplies an onProgress
|
|
2799
|
+
// callback; cleared when the final response lands (or the pending
|
|
2800
|
+
// request rejects). No leaks — the `try/finally` in callTool
|
|
2801
|
+
// guarantees cleanup even on timeout.
|
|
2802
|
+
progressHandlers = /* @__PURE__ */ new Map();
|
|
2803
|
+
nextProgressToken = 1;
|
|
2282
2804
|
constructor(opts) {
|
|
2283
2805
|
this.transport = opts.transport;
|
|
2284
2806
|
this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: "0.3.0-dev" };
|
|
@@ -2333,13 +2855,36 @@ var McpClient = class {
|
|
|
2333
2855
|
this.assertInitialized();
|
|
2334
2856
|
return this.request("tools/list", {});
|
|
2335
2857
|
}
|
|
2336
|
-
/**
|
|
2337
|
-
|
|
2858
|
+
/**
|
|
2859
|
+
* Invoke a tool by name. When `onProgress` is supplied, attaches a
|
|
2860
|
+
* fresh progress token so the server can send incremental updates
|
|
2861
|
+
* via `notifications/progress`; they're routed to the callback until
|
|
2862
|
+
* the final response arrives (or the request times out, in which
|
|
2863
|
+
* case the handler is simply dropped — no extra notification).
|
|
2864
|
+
*
|
|
2865
|
+
* When `signal` is supplied, aborting it:
|
|
2866
|
+
* 1) fires `notifications/cancelled` to the server (MCP 2024-11-05
|
|
2867
|
+
* way of saying "forget this request, I no longer care"), and
|
|
2868
|
+
* 2) rejects the pending promise immediately with an AbortError,
|
|
2869
|
+
* so the caller doesn't have to wait for the subprocess to
|
|
2870
|
+
* finish its in-flight file write or network request.
|
|
2871
|
+
* The server MAY still emit a late response; we drop it in dispatch
|
|
2872
|
+
* since the request id is gone from `pending`.
|
|
2873
|
+
*/
|
|
2874
|
+
async callTool(name, args, opts = {}) {
|
|
2338
2875
|
this.assertInitialized();
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2876
|
+
const params = { name, arguments: args ?? {} };
|
|
2877
|
+
let token;
|
|
2878
|
+
if (opts.onProgress) {
|
|
2879
|
+
token = this.nextProgressToken++;
|
|
2880
|
+
this.progressHandlers.set(token, opts.onProgress);
|
|
2881
|
+
params._meta = { progressToken: token };
|
|
2882
|
+
}
|
|
2883
|
+
try {
|
|
2884
|
+
return await this.request("tools/call", params, opts.signal);
|
|
2885
|
+
} finally {
|
|
2886
|
+
if (token !== void 0) this.progressHandlers.delete(token);
|
|
2887
|
+
}
|
|
2343
2888
|
}
|
|
2344
2889
|
/**
|
|
2345
2890
|
* List resources the server exposes. Supports a pagination cursor;
|
|
@@ -2393,24 +2938,56 @@ var McpClient = class {
|
|
|
2393
2938
|
assertInitialized() {
|
|
2394
2939
|
if (!this.initialized) throw new Error("MCP client not initialized \u2014 call initialize() first");
|
|
2395
2940
|
}
|
|
2396
|
-
async request(method, params) {
|
|
2941
|
+
async request(method, params, signal) {
|
|
2397
2942
|
const id = this.nextId++;
|
|
2398
2943
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
2399
|
-
|
|
2944
|
+
let abortHandler = null;
|
|
2945
|
+
const promise = new Promise((resolve5, reject) => {
|
|
2400
2946
|
const timeout = setTimeout(() => {
|
|
2401
2947
|
this.pending.delete(id);
|
|
2948
|
+
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
2402
2949
|
reject(
|
|
2403
2950
|
new Error(`MCP request ${method} (id=${id}) timed out after ${this.requestTimeoutMs}ms`)
|
|
2404
2951
|
);
|
|
2405
2952
|
}, this.requestTimeoutMs);
|
|
2406
2953
|
this.pending.set(id, {
|
|
2407
|
-
resolve:
|
|
2954
|
+
resolve: resolve5,
|
|
2408
2955
|
reject,
|
|
2409
2956
|
timeout
|
|
2410
2957
|
});
|
|
2958
|
+
if (signal) {
|
|
2959
|
+
if (signal.aborted) {
|
|
2960
|
+
this.pending.delete(id);
|
|
2961
|
+
clearTimeout(timeout);
|
|
2962
|
+
reject(new Error(`MCP request ${method} (id=${id}) aborted before send`));
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
abortHandler = () => {
|
|
2966
|
+
this.pending.delete(id);
|
|
2967
|
+
clearTimeout(timeout);
|
|
2968
|
+
void this.transport.send({
|
|
2969
|
+
jsonrpc: "2.0",
|
|
2970
|
+
method: "notifications/cancelled",
|
|
2971
|
+
params: { requestId: id, reason: "aborted by user" }
|
|
2972
|
+
}).catch(() => {
|
|
2973
|
+
});
|
|
2974
|
+
reject(new Error(`MCP request ${method} (id=${id}) aborted by user`));
|
|
2975
|
+
};
|
|
2976
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
2977
|
+
}
|
|
2411
2978
|
});
|
|
2412
|
-
|
|
2413
|
-
|
|
2979
|
+
try {
|
|
2980
|
+
await this.transport.send(frame);
|
|
2981
|
+
} catch (err) {
|
|
2982
|
+
this.pending.delete(id);
|
|
2983
|
+
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
2984
|
+
throw err;
|
|
2985
|
+
}
|
|
2986
|
+
try {
|
|
2987
|
+
return await promise;
|
|
2988
|
+
} finally {
|
|
2989
|
+
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
2990
|
+
}
|
|
2414
2991
|
}
|
|
2415
2992
|
startReaderIfNeeded() {
|
|
2416
2993
|
if (this.readerStarted) return;
|
|
@@ -2431,7 +3008,16 @@ var McpClient = class {
|
|
|
2431
3008
|
}
|
|
2432
3009
|
}
|
|
2433
3010
|
dispatch(msg) {
|
|
2434
|
-
if (!("id" in msg) || msg.id === null || msg.id === void 0)
|
|
3011
|
+
if (!("id" in msg) || msg.id === null || msg.id === void 0) {
|
|
3012
|
+
if ("method" in msg && msg.method === "notifications/progress") {
|
|
3013
|
+
const p = msg.params;
|
|
3014
|
+
if (!p || p.progressToken === void 0) return;
|
|
3015
|
+
const handler = this.progressHandlers.get(p.progressToken);
|
|
3016
|
+
if (!handler) return;
|
|
3017
|
+
handler({ progress: p.progress, total: p.total, message: p.message });
|
|
3018
|
+
}
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
2435
3021
|
if (!("result" in msg) && !("error" in msg)) return;
|
|
2436
3022
|
const pending = this.pending.get(msg.id);
|
|
2437
3023
|
if (!pending) return;
|
|
@@ -2488,12 +3074,12 @@ var StdioTransport = class {
|
|
|
2488
3074
|
}
|
|
2489
3075
|
async send(message) {
|
|
2490
3076
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
2491
|
-
return new Promise((
|
|
3077
|
+
return new Promise((resolve5, reject) => {
|
|
2492
3078
|
const line = `${JSON.stringify(message)}
|
|
2493
3079
|
`;
|
|
2494
3080
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
2495
3081
|
if (err) reject(err);
|
|
2496
|
-
else
|
|
3082
|
+
else resolve5();
|
|
2497
3083
|
});
|
|
2498
3084
|
});
|
|
2499
3085
|
}
|
|
@@ -2504,8 +3090,8 @@ var StdioTransport = class {
|
|
|
2504
3090
|
continue;
|
|
2505
3091
|
}
|
|
2506
3092
|
if (this.closed) return;
|
|
2507
|
-
const next = await new Promise((
|
|
2508
|
-
this.waiters.push(
|
|
3093
|
+
const next = await new Promise((resolve5) => {
|
|
3094
|
+
this.waiters.push(resolve5);
|
|
2509
3095
|
});
|
|
2510
3096
|
if (next === null) return;
|
|
2511
3097
|
yield next;
|
|
@@ -2571,8 +3157,8 @@ var SseTransport = class {
|
|
|
2571
3157
|
constructor(opts) {
|
|
2572
3158
|
this.url = opts.url;
|
|
2573
3159
|
this.headers = opts.headers ?? {};
|
|
2574
|
-
this.endpointReady = new Promise((
|
|
2575
|
-
this.resolveEndpoint =
|
|
3160
|
+
this.endpointReady = new Promise((resolve5, reject) => {
|
|
3161
|
+
this.resolveEndpoint = resolve5;
|
|
2576
3162
|
this.rejectEndpoint = reject;
|
|
2577
3163
|
});
|
|
2578
3164
|
this.endpointReady.catch(() => void 0);
|
|
@@ -2599,8 +3185,8 @@ var SseTransport = class {
|
|
|
2599
3185
|
continue;
|
|
2600
3186
|
}
|
|
2601
3187
|
if (this.closed) return;
|
|
2602
|
-
const next = await new Promise((
|
|
2603
|
-
this.waiters.push(
|
|
3188
|
+
const next = await new Promise((resolve5) => {
|
|
3189
|
+
this.waiters.push(resolve5);
|
|
2604
3190
|
});
|
|
2605
3191
|
if (next === null) return;
|
|
2606
3192
|
yield next;
|
|
@@ -2800,7 +3386,7 @@ async function trySection(load) {
|
|
|
2800
3386
|
|
|
2801
3387
|
// src/code/edit-blocks.ts
|
|
2802
3388
|
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2803
|
-
import { dirname as
|
|
3389
|
+
import { dirname as dirname4, resolve as resolve3 } from "path";
|
|
2804
3390
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
2805
3391
|
function parseEditBlocks(text) {
|
|
2806
3392
|
const out = [];
|
|
@@ -2818,8 +3404,8 @@ function parseEditBlocks(text) {
|
|
|
2818
3404
|
return out;
|
|
2819
3405
|
}
|
|
2820
3406
|
function applyEditBlock(block, rootDir) {
|
|
2821
|
-
const absRoot =
|
|
2822
|
-
const absTarget =
|
|
3407
|
+
const absRoot = resolve3(rootDir);
|
|
3408
|
+
const absTarget = resolve3(absRoot, block.path);
|
|
2823
3409
|
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
2824
3410
|
return {
|
|
2825
3411
|
path: block.path,
|
|
@@ -2838,7 +3424,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
2838
3424
|
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
2839
3425
|
};
|
|
2840
3426
|
}
|
|
2841
|
-
mkdirSync3(
|
|
3427
|
+
mkdirSync3(dirname4(absTarget), { recursive: true });
|
|
2842
3428
|
writeFileSync3(absTarget, block.replace, "utf8");
|
|
2843
3429
|
return { path: block.path, status: "created" };
|
|
2844
3430
|
}
|
|
@@ -2869,13 +3455,13 @@ function applyEditBlocks(blocks, rootDir) {
|
|
|
2869
3455
|
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
2870
3456
|
}
|
|
2871
3457
|
function snapshotBeforeEdits(blocks, rootDir) {
|
|
2872
|
-
const absRoot =
|
|
3458
|
+
const absRoot = resolve3(rootDir);
|
|
2873
3459
|
const seen = /* @__PURE__ */ new Set();
|
|
2874
3460
|
const snapshots = [];
|
|
2875
3461
|
for (const b of blocks) {
|
|
2876
3462
|
if (seen.has(b.path)) continue;
|
|
2877
3463
|
seen.add(b.path);
|
|
2878
|
-
const abs =
|
|
3464
|
+
const abs = resolve3(absRoot, b.path);
|
|
2879
3465
|
if (!existsSync2(abs)) {
|
|
2880
3466
|
snapshots.push({ path: b.path, prevContent: null });
|
|
2881
3467
|
continue;
|
|
@@ -2889,9 +3475,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
2889
3475
|
return snapshots;
|
|
2890
3476
|
}
|
|
2891
3477
|
function restoreSnapshots(snapshots, rootDir) {
|
|
2892
|
-
const absRoot =
|
|
3478
|
+
const absRoot = resolve3(rootDir);
|
|
2893
3479
|
return snapshots.map((snap) => {
|
|
2894
|
-
const abs =
|
|
3480
|
+
const abs = resolve3(absRoot, snap.path);
|
|
2895
3481
|
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
2896
3482
|
return {
|
|
2897
3483
|
path: snap.path,
|
|
@@ -2928,11 +3514,11 @@ var VERSION = "0.4.3";
|
|
|
2928
3514
|
|
|
2929
3515
|
// src/cli/commands/chat.tsx
|
|
2930
3516
|
import { render } from "ink";
|
|
2931
|
-
import React9, { useState as
|
|
3517
|
+
import React9, { useState as useState5 } from "react";
|
|
2932
3518
|
|
|
2933
3519
|
// src/cli/ui/App.tsx
|
|
2934
|
-
import { Box as Box7, Static, Text as Text7, useApp, useInput } from "ink";
|
|
2935
|
-
import React7, { useCallback, useEffect as
|
|
3520
|
+
import { Box as Box7, Static, Text as Text7, useApp, useInput as useInput2 } from "ink";
|
|
3521
|
+
import React7, { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
|
|
2936
3522
|
|
|
2937
3523
|
// src/cli/ui/EventLog.tsx
|
|
2938
3524
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
@@ -3125,6 +3711,25 @@ function parseBlocks(raw) {
|
|
|
3125
3711
|
out.push({ kind: "heading", level: hm[1].length, text: hm[2].trim() });
|
|
3126
3712
|
continue;
|
|
3127
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
|
+
}
|
|
3128
3733
|
const bm = line.match(/^\s*[-*+]\s+(.+)$/);
|
|
3129
3734
|
if (bm) {
|
|
3130
3735
|
flushPara();
|
|
@@ -3167,10 +3772,55 @@ function BlockView({ block }) {
|
|
|
3167
3772
|
return /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, block.text));
|
|
3168
3773
|
case "edit-block":
|
|
3169
3774
|
return /* @__PURE__ */ React2.createElement(EditBlockRow, { block });
|
|
3775
|
+
case "table":
|
|
3776
|
+
return /* @__PURE__ */ React2.createElement(TableBlockRow, { block });
|
|
3170
3777
|
case "hr":
|
|
3171
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");
|
|
3172
3779
|
}
|
|
3173
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
|
+
}
|
|
3174
3824
|
function EditBlockRow({ block }) {
|
|
3175
3825
|
const isNewFile = block.search.length === 0;
|
|
3176
3826
|
const searchLines = block.search.split("\n");
|
|
@@ -3196,7 +3846,8 @@ var EventRow = React3.memo(function EventRow2({ event }) {
|
|
|
3196
3846
|
const isError = event.text.startsWith("ERROR:");
|
|
3197
3847
|
const color = isError ? "red" : "yellow";
|
|
3198
3848
|
const marker = isError ? "\u2717" : "\u2192";
|
|
3199
|
-
|
|
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)));
|
|
3200
3851
|
}
|
|
3201
3852
|
if (event.role === "error") {
|
|
3202
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));
|
|
@@ -3209,6 +3860,20 @@ var EventRow = React3.memo(function EventRow2({ event }) {
|
|
|
3209
3860
|
}
|
|
3210
3861
|
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, null, event.text));
|
|
3211
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
|
+
}
|
|
3212
3877
|
function BranchBlock({ branch }) {
|
|
3213
3878
|
const per = branch.uncertainties.map((u, i) => {
|
|
3214
3879
|
const marker = i === branch.chosenIndex ? "\u25B8" : " ";
|
|
@@ -3245,8 +3910,21 @@ function StreamingAssistant({ event }) {
|
|
|
3245
3910
|
}
|
|
3246
3911
|
const tail = lastLine(event.text, 140);
|
|
3247
3912
|
const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
|
|
3913
|
+
const preFirstByte = !event.text && !event.reasoning;
|
|
3248
3914
|
const reasoningOnly = !event.text && !!event.reasoning;
|
|
3249
|
-
|
|
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"));
|
|
3250
3928
|
}
|
|
3251
3929
|
function Pulse() {
|
|
3252
3930
|
const [tick, setTick] = useState(0);
|
|
@@ -3271,9 +3949,44 @@ function truncate2(s, max) {
|
|
|
3271
3949
|
}
|
|
3272
3950
|
|
|
3273
3951
|
// src/cli/ui/PromptInput.tsx
|
|
3274
|
-
import { Box as Box4, Text as Text4 } from "ink";
|
|
3275
|
-
import
|
|
3276
|
-
|
|
3952
|
+
import { Box as Box4, Text as Text4, useInput } from "ink";
|
|
3953
|
+
import React4, { useEffect as useEffect2, useState as useState2 } from "react";
|
|
3954
|
+
|
|
3955
|
+
// src/cli/ui/multiline-keys.ts
|
|
3956
|
+
var BACKSLASH_SUFFIX = /\\$/;
|
|
3957
|
+
function processMultilineKey(value, key) {
|
|
3958
|
+
if (key.tab || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.escape || key.pageUp || key.pageDown) {
|
|
3959
|
+
return { next: null, submit: false };
|
|
3960
|
+
}
|
|
3961
|
+
if (key.input === "\n" || key.ctrl && key.input === "j") {
|
|
3962
|
+
return { next: `${value}
|
|
3963
|
+
`, submit: false };
|
|
3964
|
+
}
|
|
3965
|
+
if (key.return) {
|
|
3966
|
+
if (key.shift) {
|
|
3967
|
+
return { next: `${value}
|
|
3968
|
+
`, submit: false };
|
|
3969
|
+
}
|
|
3970
|
+
if (BACKSLASH_SUFFIX.test(value)) {
|
|
3971
|
+
return { next: `${value.slice(0, -1)}
|
|
3972
|
+
`, submit: false };
|
|
3973
|
+
}
|
|
3974
|
+
return { next: null, submit: true, submitValue: value };
|
|
3975
|
+
}
|
|
3976
|
+
if (key.backspace || key.delete) {
|
|
3977
|
+
if (value.length === 0) return { next: null, submit: false };
|
|
3978
|
+
return { next: value.slice(0, -1), submit: false };
|
|
3979
|
+
}
|
|
3980
|
+
if ((key.ctrl || key.meta) && key.input.length === 0) {
|
|
3981
|
+
return { next: null, submit: false };
|
|
3982
|
+
}
|
|
3983
|
+
if (key.input.length > 0 && !key.ctrl && !key.meta) {
|
|
3984
|
+
return { next: value + key.input, submit: false };
|
|
3985
|
+
}
|
|
3986
|
+
return { next: null, submit: false };
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
// src/cli/ui/PromptInput.tsx
|
|
3277
3990
|
function PromptInput({
|
|
3278
3991
|
value,
|
|
3279
3992
|
onChange,
|
|
@@ -3281,17 +3994,53 @@ function PromptInput({
|
|
|
3281
3994
|
disabled,
|
|
3282
3995
|
placeholder
|
|
3283
3996
|
}) {
|
|
3284
|
-
const
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
onChange,
|
|
3290
|
-
onSubmit,
|
|
3291
|
-
focus: !disabled,
|
|
3292
|
-
placeholder: effectivePlaceholder
|
|
3997
|
+
const [showCursor, setShowCursor] = useState2(true);
|
|
3998
|
+
useEffect2(() => {
|
|
3999
|
+
if (disabled) {
|
|
4000
|
+
setShowCursor(false);
|
|
4001
|
+
return;
|
|
3293
4002
|
}
|
|
3294
|
-
|
|
4003
|
+
setShowCursor(true);
|
|
4004
|
+
const id = setInterval(() => setShowCursor((s) => !s), 500);
|
|
4005
|
+
return () => clearInterval(id);
|
|
4006
|
+
}, [disabled]);
|
|
4007
|
+
useInput(
|
|
4008
|
+
(input, key) => {
|
|
4009
|
+
const keyEvent = {
|
|
4010
|
+
input,
|
|
4011
|
+
return: key.return,
|
|
4012
|
+
shift: key.shift,
|
|
4013
|
+
ctrl: key.ctrl,
|
|
4014
|
+
meta: key.meta,
|
|
4015
|
+
backspace: key.backspace,
|
|
4016
|
+
delete: key.delete,
|
|
4017
|
+
tab: key.tab,
|
|
4018
|
+
upArrow: key.upArrow,
|
|
4019
|
+
downArrow: key.downArrow,
|
|
4020
|
+
leftArrow: key.leftArrow,
|
|
4021
|
+
rightArrow: key.rightArrow,
|
|
4022
|
+
escape: key.escape,
|
|
4023
|
+
pageUp: key.pageUp,
|
|
4024
|
+
pageDown: key.pageDown
|
|
4025
|
+
};
|
|
4026
|
+
const action = processMultilineKey(value, keyEvent);
|
|
4027
|
+
if (action.next !== null) onChange(action.next);
|
|
4028
|
+
if (action.submit) onSubmit(action.submitValue ?? value);
|
|
4029
|
+
},
|
|
4030
|
+
{ isActive: !disabled }
|
|
4031
|
+
);
|
|
4032
|
+
const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command \xB7 Ctrl+J for newline";
|
|
4033
|
+
const lines = value.length > 0 ? value.split("\n") : [""];
|
|
4034
|
+
const borderColor = disabled ? "gray" : "cyan";
|
|
4035
|
+
return /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor, paddingX: 1, flexDirection: "column" }, lines.map((line, i) => {
|
|
4036
|
+
const isLast = i === lines.length - 1;
|
|
4037
|
+
const isFirst = i === 0;
|
|
4038
|
+
const showPlaceholder = isFirst && value.length === 0;
|
|
4039
|
+
return (
|
|
4040
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: stable by construction — lines are derived from `value.split("\n")` and never reordered
|
|
4041
|
+
/* @__PURE__ */ React4.createElement(Box4, { key: i }, isFirst ? /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: borderColor }, "you \u203A", " ") : /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " "), showPlaceholder && isLast && !disabled ? /* @__PURE__ */ React4.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " ") : null, showPlaceholder ? /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, effectivePlaceholder) : /* @__PURE__ */ React4.createElement(Text4, null, line), !showPlaceholder && isLast && !disabled ? /* @__PURE__ */ React4.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " ") : null)
|
|
4042
|
+
);
|
|
4043
|
+
}));
|
|
3295
4044
|
}
|
|
3296
4045
|
|
|
3297
4046
|
// src/cli/ui/SlashSuggestions.tsx
|
|
@@ -3331,7 +4080,8 @@ function StatsPanel({
|
|
|
3331
4080
|
model,
|
|
3332
4081
|
prefixHash,
|
|
3333
4082
|
harvestOn,
|
|
3334
|
-
branchBudget
|
|
4083
|
+
branchBudget,
|
|
4084
|
+
balance
|
|
3335
4085
|
}) {
|
|
3336
4086
|
const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
|
|
3337
4087
|
const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
|
|
@@ -3339,7 +4089,7 @@ function StatsPanel({
|
|
|
3339
4089
|
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
3340
4090
|
const ctxRatio = summary.lastPromptTokens / ctxMax;
|
|
3341
4091
|
const ctxColor = ctxRatio >= 0.8 ? "red" : ctxRatio >= 0.5 ? "yellow" : void 0;
|
|
3342
|
-
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));
|
|
3343
4093
|
}
|
|
3344
4094
|
function formatTokens(n) {
|
|
3345
4095
|
if (n < 1e3) return String(n);
|
|
@@ -3368,7 +4118,8 @@ var SLASH_COMMANDS = [
|
|
|
3368
4118
|
{ cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
|
|
3369
4119
|
{ cmd: "forget", summary: "delete the current session from disk" },
|
|
3370
4120
|
{ cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
|
|
3371
|
-
{ 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)" },
|
|
3372
4123
|
{ cmd: "exit", summary: "quit the TUI" },
|
|
3373
4124
|
// Code-mode only
|
|
3374
4125
|
{ cmd: "apply", summary: "commit pending edit blocks to disk", contextual: "code" },
|
|
@@ -3401,7 +4152,18 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
3401
4152
|
case "quit":
|
|
3402
4153
|
return { exit: true };
|
|
3403
4154
|
case "clear":
|
|
3404
|
-
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
|
+
}
|
|
3405
4167
|
case "help":
|
|
3406
4168
|
case "?":
|
|
3407
4169
|
return {
|
|
@@ -3425,7 +4187,8 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
3425
4187
|
' /commit "msg" (code mode) git add -A && git commit -m "msg"',
|
|
3426
4188
|
" /sessions list saved sessions (current is marked with \u25B8)",
|
|
3427
4189
|
" /forget delete the current session from disk",
|
|
3428
|
-
" /
|
|
4190
|
+
" /new start fresh: drop all context + clear scrollback",
|
|
4191
|
+
" /clear clear displayed scrollback only (context kept \u2014 model still sees it)",
|
|
3429
4192
|
" /exit quit",
|
|
3430
4193
|
"",
|
|
3431
4194
|
"Presets:",
|
|
@@ -3785,24 +4548,30 @@ function App({
|
|
|
3785
4548
|
tools,
|
|
3786
4549
|
mcpSpecs,
|
|
3787
4550
|
mcpServers,
|
|
4551
|
+
progressSink,
|
|
3788
4552
|
codeMode
|
|
3789
4553
|
}) {
|
|
3790
4554
|
const { exit } = useApp();
|
|
3791
|
-
const [historical, setHistorical] =
|
|
3792
|
-
const [streaming, setStreaming] =
|
|
3793
|
-
const [input, setInput] =
|
|
3794
|
-
const [busy, setBusy] =
|
|
4555
|
+
const [historical, setHistorical] = useState3([]);
|
|
4556
|
+
const [streaming, setStreaming] = useState3(null);
|
|
4557
|
+
const [input, setInput] = useState3("");
|
|
4558
|
+
const [busy, setBusy] = useState3(false);
|
|
3795
4559
|
const abortedThisTurn = useRef(false);
|
|
3796
|
-
const [ongoingTool, setOngoingTool] =
|
|
4560
|
+
const [ongoingTool, setOngoingTool] = useState3(null);
|
|
4561
|
+
const [toolProgress, setToolProgress] = useState3(null);
|
|
4562
|
+
const [statusLine, setStatusLine] = useState3(null);
|
|
4563
|
+
const [balance, setBalance] = useState3(null);
|
|
3797
4564
|
const lastEditSnapshots = useRef(null);
|
|
3798
4565
|
const pendingEdits = useRef([]);
|
|
3799
4566
|
const promptHistory = useRef([]);
|
|
3800
4567
|
const historyCursor = useRef(-1);
|
|
3801
4568
|
const toolHistoryRef = useRef([]);
|
|
3802
|
-
const [slashSelected, setSlashSelected] =
|
|
3803
|
-
const [summary, setSummary] =
|
|
4569
|
+
const [slashSelected, setSlashSelected] = useState3(0);
|
|
4570
|
+
const [summary, setSummary] = useState3({
|
|
3804
4571
|
turns: 0,
|
|
3805
4572
|
totalCostUsd: 0,
|
|
4573
|
+
totalInputCostUsd: 0,
|
|
4574
|
+
totalOutputCostUsd: 0,
|
|
3806
4575
|
claudeEquivalentUsd: 0,
|
|
3807
4576
|
savingsVsClaudePct: 0,
|
|
3808
4577
|
cacheHitRatio: 0,
|
|
@@ -3817,7 +4586,7 @@ function App({
|
|
|
3817
4586
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3818
4587
|
});
|
|
3819
4588
|
}
|
|
3820
|
-
|
|
4589
|
+
useEffect3(() => {
|
|
3821
4590
|
return () => {
|
|
3822
4591
|
transcriptRef.current?.end();
|
|
3823
4592
|
};
|
|
@@ -3826,7 +4595,7 @@ function App({
|
|
|
3826
4595
|
if (!input.startsWith("/") || input.includes(" ")) return null;
|
|
3827
4596
|
return suggestSlashCommands(input.slice(1), !!codeMode);
|
|
3828
4597
|
}, [input, codeMode]);
|
|
3829
|
-
|
|
4598
|
+
useEffect3(() => {
|
|
3830
4599
|
setSlashSelected((prev) => {
|
|
3831
4600
|
if (!slashMatches || slashMatches.length === 0) return 0;
|
|
3832
4601
|
if (prev >= slashMatches.length) return slashMatches.length - 1;
|
|
@@ -3845,8 +4614,33 @@ function App({
|
|
|
3845
4614
|
loopRef.current = l;
|
|
3846
4615
|
return l;
|
|
3847
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]);
|
|
4629
|
+
useEffect3(() => {
|
|
4630
|
+
if (!progressSink) return;
|
|
4631
|
+
progressSink.current = (info) => {
|
|
4632
|
+
setToolProgress({
|
|
4633
|
+
progress: info.progress,
|
|
4634
|
+
total: info.total,
|
|
4635
|
+
message: info.message
|
|
4636
|
+
});
|
|
4637
|
+
};
|
|
4638
|
+
return () => {
|
|
4639
|
+
if (progressSink.current) progressSink.current = null;
|
|
4640
|
+
};
|
|
4641
|
+
}, [progressSink]);
|
|
3848
4642
|
const sessionBannerShown = useRef(false);
|
|
3849
|
-
|
|
4643
|
+
useEffect3(() => {
|
|
3850
4644
|
if (sessionBannerShown.current) return;
|
|
3851
4645
|
sessionBannerShown.current = true;
|
|
3852
4646
|
if (!session) {
|
|
@@ -3878,7 +4672,7 @@ function App({
|
|
|
3878
4672
|
]);
|
|
3879
4673
|
}
|
|
3880
4674
|
}, [session, loop]);
|
|
3881
|
-
|
|
4675
|
+
useInput2((_input, key) => {
|
|
3882
4676
|
if (key.escape && busy) {
|
|
3883
4677
|
if (abortedThisTurn.current) return;
|
|
3884
4678
|
abortedThisTurn.current = true;
|
|
@@ -3993,6 +4787,16 @@ function App({
|
|
|
3993
4787
|
exit();
|
|
3994
4788
|
return;
|
|
3995
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
|
+
}
|
|
3996
4800
|
if (result.clear) {
|
|
3997
4801
|
setHistorical([]);
|
|
3998
4802
|
return;
|
|
@@ -4041,7 +4845,12 @@ function App({
|
|
|
4041
4845
|
try {
|
|
4042
4846
|
for await (const ev of loop.step(text)) {
|
|
4043
4847
|
writeTranscript(ev);
|
|
4044
|
-
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") {
|
|
4045
4854
|
if (ev.content) contentBuf.current += ev.content;
|
|
4046
4855
|
if (ev.reasoningDelta) reasoningBuf.current += ev.reasoningDelta;
|
|
4047
4856
|
} else if (ev.role === "branch_start") {
|
|
@@ -4065,6 +4874,7 @@ function App({
|
|
|
4065
4874
|
flush();
|
|
4066
4875
|
const repairNote = ev.repair ? describeRepair(ev.repair) : "";
|
|
4067
4876
|
setStreaming(null);
|
|
4877
|
+
setSummary(loop.stats.summary());
|
|
4068
4878
|
const finalText = ev.content || streamRef.text;
|
|
4069
4879
|
setHistorical((prev) => [
|
|
4070
4880
|
...prev,
|
|
@@ -4096,9 +4906,11 @@ function App({
|
|
|
4096
4906
|
}
|
|
4097
4907
|
} else if (ev.role === "tool_start") {
|
|
4098
4908
|
setOngoingTool({ name: ev.toolName ?? "?", args: ev.toolArgs });
|
|
4909
|
+
setToolProgress(null);
|
|
4099
4910
|
} else if (ev.role === "tool") {
|
|
4100
4911
|
flush();
|
|
4101
4912
|
setOngoingTool(null);
|
|
4913
|
+
setToolProgress(null);
|
|
4102
4914
|
toolHistoryRef.current.push({
|
|
4103
4915
|
toolName: ev.toolName ?? "?",
|
|
4104
4916
|
text: ev.content
|
|
@@ -4129,8 +4941,17 @@ function App({
|
|
|
4129
4941
|
clearInterval(timer);
|
|
4130
4942
|
setStreaming(null);
|
|
4131
4943
|
setOngoingTool(null);
|
|
4944
|
+
setToolProgress(null);
|
|
4945
|
+
setStatusLine(null);
|
|
4132
4946
|
setSummary(loop.stats.summary());
|
|
4133
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
|
+
})();
|
|
4134
4955
|
}
|
|
4135
4956
|
},
|
|
4136
4957
|
[
|
|
@@ -4154,14 +4975,33 @@ function App({
|
|
|
4154
4975
|
model: loop.model,
|
|
4155
4976
|
prefixHash,
|
|
4156
4977
|
harvestOn: loop.harvestEnabled,
|
|
4157
|
-
branchBudget: loop.branchOptions.budget
|
|
4978
|
+
branchBudget: loop.branchOptions.budget,
|
|
4979
|
+
balance
|
|
4158
4980
|
}
|
|
4159
|
-
), /* @__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 }) : 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 }));
|
|
4160
4982
|
}
|
|
4161
|
-
function
|
|
4162
|
-
const [tick, setTick] =
|
|
4163
|
-
const [elapsed, setElapsed] =
|
|
4164
|
-
|
|
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`));
|
|
4997
|
+
}
|
|
4998
|
+
function OngoingToolRow({
|
|
4999
|
+
tool,
|
|
5000
|
+
progress
|
|
5001
|
+
}) {
|
|
5002
|
+
const [tick, setTick] = useState3(0);
|
|
5003
|
+
const [elapsed, setElapsed] = useState3(0);
|
|
5004
|
+
useEffect3(() => {
|
|
4165
5005
|
const start = Date.now();
|
|
4166
5006
|
const frameId = setInterval(() => setTick((t) => t + 1), 120);
|
|
4167
5007
|
const secId = setInterval(() => setElapsed(Math.floor((Date.now() - start) / 1e3)), 1e3);
|
|
@@ -4172,7 +5012,19 @@ function OngoingToolRow({ tool }) {
|
|
|
4172
5012
|
}, []);
|
|
4173
5013
|
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4174
5014
|
const summary = summarizeToolArgs(tool.name, tool.args);
|
|
4175
|
-
return /* @__PURE__ */ React7.createElement(Box7, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Box7, null, /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, frames[tick % frames.length]), /* @__PURE__ */ React7.createElement(Text7, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, ` ${elapsed}s`)), summary ? /* @__PURE__ */ React7.createElement(Box7, { paddingLeft: 2 }, /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, summary)) : null);
|
|
5015
|
+
return /* @__PURE__ */ React7.createElement(Box7, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Box7, null, /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, frames[tick % frames.length]), /* @__PURE__ */ React7.createElement(Text7, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, ` ${elapsed}s`)), progress ? /* @__PURE__ */ React7.createElement(Box7, { paddingLeft: 2 }, /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, renderProgressLine(progress))) : null, summary ? /* @__PURE__ */ React7.createElement(Box7, { paddingLeft: 2 }, /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, summary)) : null);
|
|
5016
|
+
}
|
|
5017
|
+
function renderProgressLine(p) {
|
|
5018
|
+
const msg = p.message ? ` ${p.message}` : "";
|
|
5019
|
+
if (p.total && p.total > 0) {
|
|
5020
|
+
const ratio = Math.max(0, Math.min(1, p.progress / p.total));
|
|
5021
|
+
const width = 20;
|
|
5022
|
+
const filled = Math.round(ratio * width);
|
|
5023
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
5024
|
+
const pct2 = (ratio * 100).toFixed(0);
|
|
5025
|
+
return `[${bar}] ${p.progress}/${p.total} ${pct2}%${msg}`;
|
|
5026
|
+
}
|
|
5027
|
+
return `progress: ${p.progress}${msg}`;
|
|
4176
5028
|
}
|
|
4177
5029
|
function summarizeToolArgs(name, args) {
|
|
4178
5030
|
if (!args || args === "{}") return "";
|
|
@@ -4257,11 +5109,11 @@ function describeRepair(repair) {
|
|
|
4257
5109
|
|
|
4258
5110
|
// src/cli/ui/Setup.tsx
|
|
4259
5111
|
import { Box as Box8, Text as Text8, useApp as useApp2 } from "ink";
|
|
4260
|
-
import
|
|
4261
|
-
import React8, { useState as
|
|
5112
|
+
import TextInput from "ink-text-input";
|
|
5113
|
+
import React8, { useState as useState4 } from "react";
|
|
4262
5114
|
function Setup({ onReady }) {
|
|
4263
|
-
const [value, setValue] =
|
|
4264
|
-
const [error, setError] =
|
|
5115
|
+
const [value, setValue] = useState4("");
|
|
5116
|
+
const [error, setError] = useState4(null);
|
|
4265
5117
|
const { exit } = useApp2();
|
|
4266
5118
|
const handleSubmit = (raw) => {
|
|
4267
5119
|
const trimmed = raw.trim();
|
|
@@ -4283,7 +5135,7 @@ function Setup({ onReady }) {
|
|
|
4283
5135
|
onReady(trimmed);
|
|
4284
5136
|
};
|
|
4285
5137
|
return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React8.createElement(
|
|
4286
|
-
|
|
5138
|
+
TextInput,
|
|
4287
5139
|
{
|
|
4288
5140
|
value,
|
|
4289
5141
|
onChange: setValue,
|
|
@@ -4295,8 +5147,8 @@ function Setup({ onReady }) {
|
|
|
4295
5147
|
}
|
|
4296
5148
|
|
|
4297
5149
|
// src/cli/commands/chat.tsx
|
|
4298
|
-
function Root({ initialKey, tools, mcpSpecs, mcpServers, ...appProps }) {
|
|
4299
|
-
const [key, setKey] =
|
|
5150
|
+
function Root({ initialKey, tools, mcpSpecs, mcpServers, progressSink, ...appProps }) {
|
|
5151
|
+
const [key, setKey] = useState5(initialKey);
|
|
4300
5152
|
if (!key) {
|
|
4301
5153
|
return /* @__PURE__ */ React9.createElement(
|
|
4302
5154
|
Setup,
|
|
@@ -4321,6 +5173,7 @@ function Root({ initialKey, tools, mcpSpecs, mcpServers, ...appProps }) {
|
|
|
4321
5173
|
tools,
|
|
4322
5174
|
mcpSpecs,
|
|
4323
5175
|
mcpServers,
|
|
5176
|
+
progressSink,
|
|
4324
5177
|
codeMode: appProps.codeMode
|
|
4325
5178
|
}
|
|
4326
5179
|
);
|
|
@@ -4333,9 +5186,10 @@ async function chatCommand(opts) {
|
|
|
4333
5186
|
const successfulSpecs = [];
|
|
4334
5187
|
const failedSpecs = [];
|
|
4335
5188
|
const mcpServers = [];
|
|
4336
|
-
|
|
5189
|
+
const progressSink = { current: null };
|
|
5190
|
+
let tools = opts.seedTools;
|
|
4337
5191
|
if (requestedSpecs.length > 0) {
|
|
4338
|
-
tools = new ToolRegistry();
|
|
5192
|
+
if (!tools) tools = new ToolRegistry();
|
|
4339
5193
|
for (const raw of requestedSpecs) {
|
|
4340
5194
|
try {
|
|
4341
5195
|
const spec = parseMcpSpec(raw);
|
|
@@ -4343,7 +5197,11 @@ async function chatCommand(opts) {
|
|
|
4343
5197
|
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
4344
5198
|
const mcp2 = new McpClient({ transport });
|
|
4345
5199
|
await mcp2.initialize();
|
|
4346
|
-
const bridge = await bridgeMcpTools(mcp2, {
|
|
5200
|
+
const bridge = await bridgeMcpTools(mcp2, {
|
|
5201
|
+
registry: tools,
|
|
5202
|
+
namePrefix: prefix,
|
|
5203
|
+
onProgress: (info) => progressSink.current?.(info)
|
|
5204
|
+
});
|
|
4347
5205
|
let report;
|
|
4348
5206
|
try {
|
|
4349
5207
|
report = await inspectMcpServer(mcp2);
|
|
@@ -4381,7 +5239,7 @@ async function chatCommand(opts) {
|
|
|
4381
5239
|
);
|
|
4382
5240
|
}
|
|
4383
5241
|
}
|
|
4384
|
-
if (successfulSpecs.length === 0) {
|
|
5242
|
+
if (successfulSpecs.length === 0 && !opts.seedTools) {
|
|
4385
5243
|
tools = void 0;
|
|
4386
5244
|
}
|
|
4387
5245
|
}
|
|
@@ -4394,6 +5252,7 @@ async function chatCommand(opts) {
|
|
|
4394
5252
|
tools,
|
|
4395
5253
|
mcpSpecs,
|
|
4396
5254
|
mcpServers,
|
|
5255
|
+
progressSink,
|
|
4397
5256
|
...opts
|
|
4398
5257
|
}
|
|
4399
5258
|
),
|
|
@@ -4407,14 +5266,15 @@ async function chatCommand(opts) {
|
|
|
4407
5266
|
}
|
|
4408
5267
|
|
|
4409
5268
|
// src/cli/commands/code.tsx
|
|
4410
|
-
import { basename, resolve as
|
|
5269
|
+
import { basename, resolve as resolve4 } from "path";
|
|
4411
5270
|
async function codeCommand(opts = {}) {
|
|
4412
5271
|
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-MMANQ36Z.js");
|
|
4413
|
-
const rootDir =
|
|
5272
|
+
const rootDir = resolve4(opts.dir ?? process.cwd());
|
|
4414
5273
|
const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
|
|
4415
|
-
const
|
|
5274
|
+
const tools = new ToolRegistry();
|
|
5275
|
+
registerFilesystemTools(tools, { rootDir });
|
|
4416
5276
|
process.stderr.write(
|
|
4417
|
-
`\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}"
|
|
5277
|
+
`\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native fs tool(s)
|
|
4418
5278
|
`
|
|
4419
5279
|
);
|
|
4420
5280
|
await chatCommand({
|
|
@@ -4424,13 +5284,10 @@ async function codeCommand(opts = {}) {
|
|
|
4424
5284
|
system: codeSystemPrompt2(rootDir),
|
|
4425
5285
|
transcript: opts.transcript,
|
|
4426
5286
|
session,
|
|
4427
|
-
|
|
5287
|
+
seedTools: tools,
|
|
4428
5288
|
codeMode: { rootDir }
|
|
4429
5289
|
});
|
|
4430
5290
|
}
|
|
4431
|
-
function quoteIfNeeded(s) {
|
|
4432
|
-
return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
4433
|
-
}
|
|
4434
5291
|
|
|
4435
5292
|
// src/cli/commands/diff.ts
|
|
4436
5293
|
import { writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -4439,8 +5296,8 @@ import { render as render2 } from "ink";
|
|
|
4439
5296
|
import React12 from "react";
|
|
4440
5297
|
|
|
4441
5298
|
// src/cli/ui/DiffApp.tsx
|
|
4442
|
-
import { Box as Box10, Static as Static2, Text as Text10, useApp as useApp3, useInput as
|
|
4443
|
-
import React11, { useState as
|
|
5299
|
+
import { Box as Box10, Static as Static2, Text as Text10, useApp as useApp3, useInput as useInput3 } from "ink";
|
|
5300
|
+
import React11, { useState as useState6 } from "react";
|
|
4444
5301
|
|
|
4445
5302
|
// src/cli/ui/RecordView.tsx
|
|
4446
5303
|
import { Box as Box9, Text as Text9 } from "ink";
|
|
@@ -4483,8 +5340,8 @@ function DiffApp({ report }) {
|
|
|
4483
5340
|
const { exit } = useApp3();
|
|
4484
5341
|
const maxIdx = Math.max(0, report.pairs.length - 1);
|
|
4485
5342
|
const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
|
|
4486
|
-
const [idx, setIdx] =
|
|
4487
|
-
|
|
5343
|
+
const [idx, setIdx] = useState6(Math.max(0, initialIdx));
|
|
5344
|
+
useInput3((input, key) => {
|
|
4488
5345
|
if (input === "q" || key.ctrl && input === "c") {
|
|
4489
5346
|
exit();
|
|
4490
5347
|
return;
|
|
@@ -4728,13 +5585,13 @@ import { render as render3 } from "ink";
|
|
|
4728
5585
|
import React14 from "react";
|
|
4729
5586
|
|
|
4730
5587
|
// src/cli/ui/ReplayApp.tsx
|
|
4731
|
-
import { Box as Box11, Static as Static3, Text as Text11, useApp as useApp4, useInput as
|
|
4732
|
-
import React13, { useMemo as useMemo2, useState as
|
|
5588
|
+
import { Box as Box11, Static as Static3, Text as Text11, useApp as useApp4, useInput as useInput4 } from "ink";
|
|
5589
|
+
import React13, { useMemo as useMemo2, useState as useState7 } from "react";
|
|
4733
5590
|
function ReplayApp({ meta, pages }) {
|
|
4734
5591
|
const { exit } = useApp4();
|
|
4735
5592
|
const maxIdx = Math.max(0, pages.length - 1);
|
|
4736
|
-
const [idx, setIdx] =
|
|
4737
|
-
|
|
5593
|
+
const [idx, setIdx] = useState7(maxIdx);
|
|
5594
|
+
useInput4((input, key) => {
|
|
4738
5595
|
if (input === "q" || key.ctrl && input === "c") {
|
|
4739
5596
|
exit();
|
|
4740
5597
|
return;
|
|
@@ -4757,6 +5614,8 @@ function ReplayApp({ meta, pages }) {
|
|
|
4757
5614
|
const summary = {
|
|
4758
5615
|
turns: cumStats.turns,
|
|
4759
5616
|
totalCostUsd: cumStats.totalCostUsd,
|
|
5617
|
+
totalInputCostUsd: cumStats.totalInputCostUsd,
|
|
5618
|
+
totalOutputCostUsd: cumStats.totalOutputCostUsd,
|
|
4760
5619
|
claudeEquivalentUsd: cumStats.claudeEquivalentUsd,
|
|
4761
5620
|
savingsVsClaudePct: cumStats.savingsVsClaudePct,
|
|
4762
5621
|
cacheHitRatio: cumStats.cacheHitRatio,
|
|
@@ -5089,13 +5948,13 @@ import { render as render4 } from "ink";
|
|
|
5089
5948
|
import React17 from "react";
|
|
5090
5949
|
|
|
5091
5950
|
// src/cli/ui/Wizard.tsx
|
|
5092
|
-
import { Box as Box13, Text as Text13, useApp as useApp5, useInput as
|
|
5093
|
-
import
|
|
5094
|
-
import React16, { useState as
|
|
5951
|
+
import { Box as Box13, Text as Text13, useApp as useApp5, useInput as useInput6 } from "ink";
|
|
5952
|
+
import TextInput2 from "ink-text-input";
|
|
5953
|
+
import React16, { useState as useState9 } from "react";
|
|
5095
5954
|
|
|
5096
5955
|
// src/cli/ui/Select.tsx
|
|
5097
|
-
import { Box as Box12, Text as Text12, useInput as
|
|
5098
|
-
import React15, { useState as
|
|
5956
|
+
import { Box as Box12, Text as Text12, useInput as useInput5 } from "ink";
|
|
5957
|
+
import React15, { useState as useState8 } from "react";
|
|
5099
5958
|
function SingleSelect({
|
|
5100
5959
|
items,
|
|
5101
5960
|
initialValue,
|
|
@@ -5106,8 +5965,8 @@ function SingleSelect({
|
|
|
5106
5965
|
0,
|
|
5107
5966
|
items.findIndex((i) => i.value === initialValue && !i.disabled)
|
|
5108
5967
|
);
|
|
5109
|
-
const [index, setIndex] =
|
|
5110
|
-
|
|
5968
|
+
const [index, setIndex] = useState8(initialIndex === -1 ? 0 : initialIndex);
|
|
5969
|
+
useInput5((_input, key) => {
|
|
5111
5970
|
if (key.upArrow) {
|
|
5112
5971
|
setIndex((i) => findNextEnabled(items, i, -1));
|
|
5113
5972
|
} else if (key.downArrow) {
|
|
@@ -5136,12 +5995,12 @@ function MultiSelect({
|
|
|
5136
5995
|
onCancel,
|
|
5137
5996
|
footer
|
|
5138
5997
|
}) {
|
|
5139
|
-
const [index, setIndex] =
|
|
5998
|
+
const [index, setIndex] = useState8(() => {
|
|
5140
5999
|
const first = items.findIndex((i) => !i.disabled);
|
|
5141
6000
|
return first === -1 ? 0 : first;
|
|
5142
6001
|
});
|
|
5143
|
-
const [selected, setSelected] =
|
|
5144
|
-
|
|
6002
|
+
const [selected, setSelected] = useState8(new Set(initialSelected));
|
|
6003
|
+
useInput5((input, key) => {
|
|
5145
6004
|
if (key.upArrow) {
|
|
5146
6005
|
setIndex((i) => findNextEnabled(items, i, -1));
|
|
5147
6006
|
} else if (key.downArrow) {
|
|
@@ -5219,15 +6078,15 @@ var PRESET_DESCRIPTIONS = {
|
|
|
5219
6078
|
var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
|
|
5220
6079
|
function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
5221
6080
|
const { exit } = useApp5();
|
|
5222
|
-
const [step, setStep] =
|
|
5223
|
-
const [data, setData] =
|
|
6081
|
+
const [step, setStep] = useState9(existingApiKey ? "preset" : "apiKey");
|
|
6082
|
+
const [data, setData] = useState9({
|
|
5224
6083
|
apiKey: existingApiKey ?? "",
|
|
5225
6084
|
preset: initial?.preset ?? "fast",
|
|
5226
6085
|
selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
|
|
5227
6086
|
catalogArgs: {}
|
|
5228
6087
|
});
|
|
5229
|
-
const [error, setError] =
|
|
5230
|
-
|
|
6088
|
+
const [error, setError] = useState9(null);
|
|
6089
|
+
useInput6((_input, key) => {
|
|
5231
6090
|
if (key.escape && step !== "saved" && onCancel) onCancel();
|
|
5232
6091
|
});
|
|
5233
6092
|
if (step === "apiKey") {
|
|
@@ -5343,9 +6202,9 @@ function ApiKeyStep({
|
|
|
5343
6202
|
error,
|
|
5344
6203
|
onError
|
|
5345
6204
|
}) {
|
|
5346
|
-
const [value, setValue] =
|
|
6205
|
+
const [value, setValue] = useState9("");
|
|
5347
6206
|
return /* @__PURE__ */ React16.createElement(Box13, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React16.createElement(Text13, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React16.createElement(
|
|
5348
|
-
|
|
6207
|
+
TextInput2,
|
|
5349
6208
|
{
|
|
5350
6209
|
value,
|
|
5351
6210
|
onChange: setValue,
|
|
@@ -5369,9 +6228,9 @@ function McpArgsStep({
|
|
|
5369
6228
|
onSubmit,
|
|
5370
6229
|
onError
|
|
5371
6230
|
}) {
|
|
5372
|
-
const [value, setValue] =
|
|
6231
|
+
const [value, setValue] = useState9("");
|
|
5373
6232
|
return /* @__PURE__ */ React16.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React16.createElement(Box13, { flexDirection: "column" }, /* @__PURE__ */ React16.createElement(Text13, null, entry.summary), entry.note ? /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, null, "Required parameter: "), /* @__PURE__ */ React16.createElement(Text13, { bold: true }, entry.userArgs)), /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React16.createElement(
|
|
5374
|
-
|
|
6233
|
+
TextInput2,
|
|
5375
6234
|
{
|
|
5376
6235
|
value,
|
|
5377
6236
|
onChange: setValue,
|
|
@@ -5389,13 +6248,13 @@ function McpArgsStep({
|
|
|
5389
6248
|
)), error ? /* @__PURE__ */ React16.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text13, { color: "red" }, error)) : null));
|
|
5390
6249
|
}
|
|
5391
6250
|
function ReviewConfirm({ onConfirm }) {
|
|
5392
|
-
|
|
6251
|
+
useInput6((_i, key) => {
|
|
5393
6252
|
if (key.return) onConfirm();
|
|
5394
6253
|
});
|
|
5395
6254
|
return null;
|
|
5396
6255
|
}
|
|
5397
6256
|
function ExitOnEnter({ onExit }) {
|
|
5398
|
-
|
|
6257
|
+
useInput6((_i, key) => {
|
|
5399
6258
|
if (key.return) onExit();
|
|
5400
6259
|
});
|
|
5401
6260
|
return null;
|
|
@@ -5452,10 +6311,10 @@ function buildSpec(name, argsByName) {
|
|
|
5452
6311
|
const entry = CATALOG_BY_NAME.get(name);
|
|
5453
6312
|
if (!entry) return name;
|
|
5454
6313
|
const userArg = entry.userArgs ? argsByName[name] : void 0;
|
|
5455
|
-
const tail = userArg ? ` ${
|
|
6314
|
+
const tail = userArg ? ` ${quoteIfNeeded(userArg)}` : "";
|
|
5456
6315
|
return `${entry.name}=npx -y ${entry.package}${tail}`;
|
|
5457
6316
|
}
|
|
5458
|
-
function
|
|
6317
|
+
function quoteIfNeeded(s) {
|
|
5459
6318
|
return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
5460
6319
|
}
|
|
5461
6320
|
|