reasonix 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -77
- package/dist/cli/chunk-2P2MZLCE.js +81 -0
- package/dist/cli/chunk-2P2MZLCE.js.map +1 -0
- package/dist/cli/index.js +529 -53
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/prompt-MMANQ36Z.js +10 -0
- package/dist/cli/prompt-MMANQ36Z.js.map +1 -0
- package/dist/index.d.ts +142 -3
- package/dist/index.js +308 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-2P2MZLCE.js";
|
|
2
3
|
|
|
3
4
|
// src/cli/index.ts
|
|
4
5
|
import { Command } from "commander";
|
|
@@ -95,8 +96,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
|
|
|
95
96
|
}
|
|
96
97
|
function sleep(ms, signal) {
|
|
97
98
|
if (ms <= 0) return Promise.resolve();
|
|
98
|
-
return new Promise((
|
|
99
|
-
const timer = setTimeout(
|
|
99
|
+
return new Promise((resolve4, reject) => {
|
|
100
|
+
const timer = setTimeout(resolve4, ms);
|
|
100
101
|
if (signal) {
|
|
101
102
|
const onAbort = () => {
|
|
102
103
|
clearTimeout(timer);
|
|
@@ -1155,12 +1156,18 @@ var CacheFirstLoop = class {
|
|
|
1155
1156
|
resumedMessageCount;
|
|
1156
1157
|
_turn = 0;
|
|
1157
1158
|
_streamPreference;
|
|
1159
|
+
/**
|
|
1160
|
+
* Set by {@link abort} to short-circuit the tool-call loop after the
|
|
1161
|
+
* current iteration. Reset at the start of each `step()` so an Esc
|
|
1162
|
+
* during one turn doesn't poison the next.
|
|
1163
|
+
*/
|
|
1164
|
+
_aborted = false;
|
|
1158
1165
|
constructor(opts) {
|
|
1159
1166
|
this.client = opts.client;
|
|
1160
1167
|
this.prefix = opts.prefix;
|
|
1161
1168
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
1162
1169
|
this.model = opts.model ?? "deepseek-chat";
|
|
1163
|
-
this.maxToolIters = opts.maxToolIters ??
|
|
1170
|
+
this.maxToolIters = opts.maxToolIters ?? 64;
|
|
1164
1171
|
if (typeof opts.branch === "number") {
|
|
1165
1172
|
this.branchOptions = { budget: opts.branch };
|
|
1166
1173
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1266,12 +1273,42 @@ var CacheFirstLoop = class {
|
|
|
1266
1273
|
if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
|
|
1267
1274
|
return msgs;
|
|
1268
1275
|
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Signal the currently-running {@link step} that the user wants to
|
|
1278
|
+
* stop exploring. Takes effect at the next iteration boundary — if a
|
|
1279
|
+
* tool call is mid-flight it will be allowed to finish, then the
|
|
1280
|
+
* loop diverts to the forced-summary path so the user gets an
|
|
1281
|
+
* answer instead of a cliff. Called by the TUI on Esc.
|
|
1282
|
+
*/
|
|
1283
|
+
abort() {
|
|
1284
|
+
this._aborted = true;
|
|
1285
|
+
}
|
|
1269
1286
|
async *step(userInput) {
|
|
1270
1287
|
this._turn++;
|
|
1271
1288
|
this.scratch.reset();
|
|
1289
|
+
this._aborted = false;
|
|
1272
1290
|
let pendingUser = userInput;
|
|
1273
1291
|
const toolSpecs = this.prefix.tools();
|
|
1292
|
+
const warnAt = Math.max(1, Math.floor(this.maxToolIters * 0.7));
|
|
1293
|
+
let warnedForIterBudget = false;
|
|
1274
1294
|
for (let iter = 0; iter < this.maxToolIters; iter++) {
|
|
1295
|
+
if (this._aborted) {
|
|
1296
|
+
yield {
|
|
1297
|
+
turn: this._turn,
|
|
1298
|
+
role: "warning",
|
|
1299
|
+
content: `aborted at iter ${iter}/${this.maxToolIters} \u2014 forcing summary from what was gathered`
|
|
1300
|
+
};
|
|
1301
|
+
yield* this.forceSummaryAfterIterLimit({ reason: "aborted" });
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (!warnedForIterBudget && iter >= warnAt) {
|
|
1305
|
+
warnedForIterBudget = true;
|
|
1306
|
+
yield {
|
|
1307
|
+
turn: this._turn,
|
|
1308
|
+
role: "warning",
|
|
1309
|
+
content: `${iter}/${this.maxToolIters} tool calls used \u2014 approaching budget. Press Esc to force a summary now.`
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1275
1312
|
const messages = this.buildMessages(pendingUser);
|
|
1276
1313
|
let assistantContent = "";
|
|
1277
1314
|
let reasoningContent = "";
|
|
@@ -1319,8 +1356,8 @@ var CacheFirstLoop = class {
|
|
|
1319
1356
|
}
|
|
1320
1357
|
);
|
|
1321
1358
|
for (let k = 0; k < budget; k++) {
|
|
1322
|
-
const sample = queue.shift() ?? await new Promise((
|
|
1323
|
-
waiter =
|
|
1359
|
+
const sample = queue.shift() ?? await new Promise((resolve4) => {
|
|
1360
|
+
waiter = resolve4;
|
|
1324
1361
|
});
|
|
1325
1362
|
yield {
|
|
1326
1363
|
turn: this._turn,
|
|
@@ -1440,9 +1477,28 @@ var CacheFirstLoop = class {
|
|
|
1440
1477
|
yield { turn: this._turn, role: "done", content: assistantContent };
|
|
1441
1478
|
return;
|
|
1442
1479
|
}
|
|
1480
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1481
|
+
if (usage && usage.promptTokens / ctxMax > 0.8) {
|
|
1482
|
+
yield {
|
|
1483
|
+
turn: this._turn,
|
|
1484
|
+
role: "warning",
|
|
1485
|
+
content: `context ${usage.promptTokens}/${ctxMax} (${Math.round(
|
|
1486
|
+
usage.promptTokens / ctxMax * 100
|
|
1487
|
+
)}%) \u2014 more tools would overflow. Forcing summary from what was gathered.`
|
|
1488
|
+
};
|
|
1489
|
+
yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1443
1492
|
for (const call of repairedCalls) {
|
|
1444
1493
|
const name = call.function?.name ?? "";
|
|
1445
1494
|
const args = call.function?.arguments ?? "{}";
|
|
1495
|
+
yield {
|
|
1496
|
+
turn: this._turn,
|
|
1497
|
+
role: "tool_start",
|
|
1498
|
+
content: "",
|
|
1499
|
+
toolName: name,
|
|
1500
|
+
toolArgs: args
|
|
1501
|
+
};
|
|
1446
1502
|
const result = await this.tools.dispatch(name, args);
|
|
1447
1503
|
this.appendAndPersist({
|
|
1448
1504
|
role: "tool",
|
|
@@ -1459,9 +1515,9 @@ var CacheFirstLoop = class {
|
|
|
1459
1515
|
};
|
|
1460
1516
|
}
|
|
1461
1517
|
}
|
|
1462
|
-
yield* this.forceSummaryAfterIterLimit();
|
|
1518
|
+
yield* this.forceSummaryAfterIterLimit({ reason: "budget" });
|
|
1463
1519
|
}
|
|
1464
|
-
async *forceSummaryAfterIterLimit() {
|
|
1520
|
+
async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
|
|
1465
1521
|
try {
|
|
1466
1522
|
const messages = this.buildMessages(null);
|
|
1467
1523
|
const resp = await this.client.chat({
|
|
@@ -1470,7 +1526,8 @@ var CacheFirstLoop = class {
|
|
|
1470
1526
|
// no tools → model is forced to answer in text
|
|
1471
1527
|
});
|
|
1472
1528
|
const summary = resp.content?.trim() || "(model returned no text; try a narrower question or raise --max-tool-iters)";
|
|
1473
|
-
const
|
|
1529
|
+
const reasonPrefix = reasonPrefixFor(opts.reason, this.maxToolIters);
|
|
1530
|
+
const annotated = `${reasonPrefix}
|
|
1474
1531
|
|
|
1475
1532
|
${summary}`;
|
|
1476
1533
|
const summaryStats = this.stats.record(this._turn, this.model, resp.usage ?? new Usage());
|
|
@@ -1479,15 +1536,17 @@ ${summary}`;
|
|
|
1479
1536
|
turn: this._turn,
|
|
1480
1537
|
role: "assistant_final",
|
|
1481
1538
|
content: annotated,
|
|
1482
|
-
stats: summaryStats
|
|
1539
|
+
stats: summaryStats,
|
|
1540
|
+
forcedSummary: true
|
|
1483
1541
|
};
|
|
1484
1542
|
yield { turn: this._turn, role: "done", content: summary };
|
|
1485
1543
|
} catch (err) {
|
|
1544
|
+
const label = errorLabelFor(opts.reason, this.maxToolIters);
|
|
1486
1545
|
yield {
|
|
1487
1546
|
turn: this._turn,
|
|
1488
1547
|
role: "error",
|
|
1489
1548
|
content: "",
|
|
1490
|
-
error:
|
|
1549
|
+
error: `${label} and the fallback summary call failed: ${err.message}. Run /clear and retry with a narrower question, or raise --max-tool-iters.`
|
|
1491
1550
|
};
|
|
1492
1551
|
yield { turn: this._turn, role: "done", content: "" };
|
|
1493
1552
|
}
|
|
@@ -1507,6 +1566,18 @@ ${summary}`;
|
|
|
1507
1566
|
return msg;
|
|
1508
1567
|
}
|
|
1509
1568
|
};
|
|
1569
|
+
function reasonPrefixFor(reason, iterCap) {
|
|
1570
|
+
if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
|
|
1571
|
+
if (reason === "context-guard") {
|
|
1572
|
+
return "[context budget running low \u2014 summarizing before the next call would overflow]";
|
|
1573
|
+
}
|
|
1574
|
+
return `[tool-call budget (${iterCap}) reached \u2014 forcing summary from what I found]`;
|
|
1575
|
+
}
|
|
1576
|
+
function errorLabelFor(reason, iterCap) {
|
|
1577
|
+
if (reason === "aborted") return "aborted by user";
|
|
1578
|
+
if (reason === "context-guard") return "context-guard triggered (prompt > 80% of window)";
|
|
1579
|
+
return `tool-call budget (${iterCap}) reached`;
|
|
1580
|
+
}
|
|
1510
1581
|
function summarizeBranch(chosen, samples) {
|
|
1511
1582
|
return {
|
|
1512
1583
|
budget: samples.length,
|
|
@@ -2160,7 +2231,7 @@ var McpClient = class {
|
|
|
2160
2231
|
async request(method, params) {
|
|
2161
2232
|
const id = this.nextId++;
|
|
2162
2233
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
2163
|
-
const promise = new Promise((
|
|
2234
|
+
const promise = new Promise((resolve4, reject) => {
|
|
2164
2235
|
const timeout = setTimeout(() => {
|
|
2165
2236
|
this.pending.delete(id);
|
|
2166
2237
|
reject(
|
|
@@ -2168,7 +2239,7 @@ var McpClient = class {
|
|
|
2168
2239
|
);
|
|
2169
2240
|
}, this.requestTimeoutMs);
|
|
2170
2241
|
this.pending.set(id, {
|
|
2171
|
-
resolve:
|
|
2242
|
+
resolve: resolve4,
|
|
2172
2243
|
reject,
|
|
2173
2244
|
timeout
|
|
2174
2245
|
});
|
|
@@ -2252,12 +2323,12 @@ var StdioTransport = class {
|
|
|
2252
2323
|
}
|
|
2253
2324
|
async send(message) {
|
|
2254
2325
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
2255
|
-
return new Promise((
|
|
2326
|
+
return new Promise((resolve4, reject) => {
|
|
2256
2327
|
const line = `${JSON.stringify(message)}
|
|
2257
2328
|
`;
|
|
2258
2329
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
2259
2330
|
if (err) reject(err);
|
|
2260
|
-
else
|
|
2331
|
+
else resolve4();
|
|
2261
2332
|
});
|
|
2262
2333
|
});
|
|
2263
2334
|
}
|
|
@@ -2268,8 +2339,8 @@ var StdioTransport = class {
|
|
|
2268
2339
|
continue;
|
|
2269
2340
|
}
|
|
2270
2341
|
if (this.closed) return;
|
|
2271
|
-
const next = await new Promise((
|
|
2272
|
-
this.waiters.push(
|
|
2342
|
+
const next = await new Promise((resolve4) => {
|
|
2343
|
+
this.waiters.push(resolve4);
|
|
2273
2344
|
});
|
|
2274
2345
|
if (next === null) return;
|
|
2275
2346
|
yield next;
|
|
@@ -2335,8 +2406,8 @@ var SseTransport = class {
|
|
|
2335
2406
|
constructor(opts) {
|
|
2336
2407
|
this.url = opts.url;
|
|
2337
2408
|
this.headers = opts.headers ?? {};
|
|
2338
|
-
this.endpointReady = new Promise((
|
|
2339
|
-
this.resolveEndpoint =
|
|
2409
|
+
this.endpointReady = new Promise((resolve4, reject) => {
|
|
2410
|
+
this.resolveEndpoint = resolve4;
|
|
2340
2411
|
this.rejectEndpoint = reject;
|
|
2341
2412
|
});
|
|
2342
2413
|
this.endpointReady.catch(() => void 0);
|
|
@@ -2363,8 +2434,8 @@ var SseTransport = class {
|
|
|
2363
2434
|
continue;
|
|
2364
2435
|
}
|
|
2365
2436
|
if (this.closed) return;
|
|
2366
|
-
const next = await new Promise((
|
|
2367
|
-
this.waiters.push(
|
|
2437
|
+
const next = await new Promise((resolve4) => {
|
|
2438
|
+
this.waiters.push(resolve4);
|
|
2368
2439
|
});
|
|
2369
2440
|
if (next === null) return;
|
|
2370
2441
|
yield next;
|
|
@@ -2532,15 +2603,140 @@ function parseMcpSpec(input) {
|
|
|
2532
2603
|
return { transport: "stdio", name, command, args };
|
|
2533
2604
|
}
|
|
2534
2605
|
|
|
2606
|
+
// src/code/edit-blocks.ts
|
|
2607
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2608
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
2609
|
+
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
2610
|
+
function parseEditBlocks(text) {
|
|
2611
|
+
const out = [];
|
|
2612
|
+
BLOCK_RE.lastIndex = 0;
|
|
2613
|
+
let m = BLOCK_RE.exec(text);
|
|
2614
|
+
while (m !== null) {
|
|
2615
|
+
out.push({
|
|
2616
|
+
path: m[1].trim(),
|
|
2617
|
+
search: m[2],
|
|
2618
|
+
replace: m[3],
|
|
2619
|
+
offset: m.index
|
|
2620
|
+
});
|
|
2621
|
+
m = BLOCK_RE.exec(text);
|
|
2622
|
+
}
|
|
2623
|
+
return out;
|
|
2624
|
+
}
|
|
2625
|
+
function applyEditBlock(block, rootDir) {
|
|
2626
|
+
const absRoot = resolve2(rootDir);
|
|
2627
|
+
const absTarget = resolve2(absRoot, block.path);
|
|
2628
|
+
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
2629
|
+
return {
|
|
2630
|
+
path: block.path,
|
|
2631
|
+
status: "path-escape",
|
|
2632
|
+
message: `resolved path ${absTarget} is outside rootDir ${absRoot}`
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
const searchEmpty = block.search.length === 0;
|
|
2636
|
+
const exists = existsSync2(absTarget);
|
|
2637
|
+
try {
|
|
2638
|
+
if (!exists) {
|
|
2639
|
+
if (!searchEmpty) {
|
|
2640
|
+
return {
|
|
2641
|
+
path: block.path,
|
|
2642
|
+
status: "file-missing",
|
|
2643
|
+
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
mkdirSync3(dirname3(absTarget), { recursive: true });
|
|
2647
|
+
writeFileSync3(absTarget, block.replace, "utf8");
|
|
2648
|
+
return { path: block.path, status: "created" };
|
|
2649
|
+
}
|
|
2650
|
+
const content = readFileSync5(absTarget, "utf8");
|
|
2651
|
+
if (searchEmpty) {
|
|
2652
|
+
return {
|
|
2653
|
+
path: block.path,
|
|
2654
|
+
status: "not-found",
|
|
2655
|
+
message: "empty SEARCH only creates new files \u2014 this file already exists"
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
const idx = content.indexOf(block.search);
|
|
2659
|
+
if (idx === -1) {
|
|
2660
|
+
return {
|
|
2661
|
+
path: block.path,
|
|
2662
|
+
status: "not-found",
|
|
2663
|
+
message: "SEARCH text does not match the current file content exactly"
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
|
|
2667
|
+
writeFileSync3(absTarget, replaced, "utf8");
|
|
2668
|
+
return { path: block.path, status: "applied" };
|
|
2669
|
+
} catch (err) {
|
|
2670
|
+
return { path: block.path, status: "error", message: err.message };
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
function applyEditBlocks(blocks, rootDir) {
|
|
2674
|
+
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
2675
|
+
}
|
|
2676
|
+
function snapshotBeforeEdits(blocks, rootDir) {
|
|
2677
|
+
const absRoot = resolve2(rootDir);
|
|
2678
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2679
|
+
const snapshots = [];
|
|
2680
|
+
for (const b of blocks) {
|
|
2681
|
+
if (seen.has(b.path)) continue;
|
|
2682
|
+
seen.add(b.path);
|
|
2683
|
+
const abs = resolve2(absRoot, b.path);
|
|
2684
|
+
if (!existsSync2(abs)) {
|
|
2685
|
+
snapshots.push({ path: b.path, prevContent: null });
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
try {
|
|
2689
|
+
snapshots.push({ path: b.path, prevContent: readFileSync5(abs, "utf8") });
|
|
2690
|
+
} catch {
|
|
2691
|
+
snapshots.push({ path: b.path, prevContent: null });
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
return snapshots;
|
|
2695
|
+
}
|
|
2696
|
+
function restoreSnapshots(snapshots, rootDir) {
|
|
2697
|
+
const absRoot = resolve2(rootDir);
|
|
2698
|
+
return snapshots.map((snap) => {
|
|
2699
|
+
const abs = resolve2(absRoot, snap.path);
|
|
2700
|
+
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
2701
|
+
return {
|
|
2702
|
+
path: snap.path,
|
|
2703
|
+
status: "path-escape",
|
|
2704
|
+
message: "snapshot path escapes rootDir \u2014 refusing to restore"
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
try {
|
|
2708
|
+
if (snap.prevContent === null) {
|
|
2709
|
+
if (existsSync2(abs)) unlinkSync2(abs);
|
|
2710
|
+
return {
|
|
2711
|
+
path: snap.path,
|
|
2712
|
+
status: "applied",
|
|
2713
|
+
message: "removed (the edit had created it)"
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
writeFileSync3(abs, snap.prevContent, "utf8");
|
|
2717
|
+
return {
|
|
2718
|
+
path: snap.path,
|
|
2719
|
+
status: "applied",
|
|
2720
|
+
message: "restored to pre-edit content"
|
|
2721
|
+
};
|
|
2722
|
+
} catch (err) {
|
|
2723
|
+
return { path: snap.path, status: "error", message: err.message };
|
|
2724
|
+
}
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
function sep() {
|
|
2728
|
+
return process.platform === "win32" ? "\\" : "/";
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2535
2731
|
// src/index.ts
|
|
2536
|
-
var VERSION = "0.
|
|
2732
|
+
var VERSION = "0.4.1";
|
|
2537
2733
|
|
|
2538
2734
|
// src/cli/commands/chat.tsx
|
|
2539
2735
|
import { render } from "ink";
|
|
2540
2736
|
import React8, { useState as useState4 } from "react";
|
|
2541
2737
|
|
|
2542
2738
|
// src/cli/ui/App.tsx
|
|
2543
|
-
import { Box as Box6, Static, Text as Text6, useApp } from "ink";
|
|
2739
|
+
import { Box as Box6, Static, Text as Text6, useApp, useInput } from "ink";
|
|
2544
2740
|
import React6, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
|
|
2545
2741
|
|
|
2546
2742
|
// src/cli/ui/EventLog.tsx
|
|
@@ -2664,8 +2860,39 @@ function parseBlocks(raw) {
|
|
|
2664
2860
|
listBuf = null;
|
|
2665
2861
|
}
|
|
2666
2862
|
};
|
|
2667
|
-
for (
|
|
2863
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2864
|
+
const rawLine = lines[i];
|
|
2668
2865
|
const line = rawLine.replace(/\s+$/g, "");
|
|
2866
|
+
if (!inCode && /^<{7} SEARCH\s*$/.test(line)) {
|
|
2867
|
+
const filename = para.pop()?.trim();
|
|
2868
|
+
if (filename) {
|
|
2869
|
+
flushPara();
|
|
2870
|
+
flushList();
|
|
2871
|
+
let j = i + 1;
|
|
2872
|
+
const searchLines = [];
|
|
2873
|
+
while (j < lines.length && !/^={7}\s*$/.test(lines[j])) {
|
|
2874
|
+
searchLines.push(lines[j]);
|
|
2875
|
+
j++;
|
|
2876
|
+
}
|
|
2877
|
+
const replaceLines = [];
|
|
2878
|
+
let k = j + 1;
|
|
2879
|
+
while (k < lines.length && !/^>{7} REPLACE\s*$/.test(lines[k])) {
|
|
2880
|
+
replaceLines.push(lines[k]);
|
|
2881
|
+
k++;
|
|
2882
|
+
}
|
|
2883
|
+
if (j < lines.length && k < lines.length) {
|
|
2884
|
+
out.push({
|
|
2885
|
+
kind: "edit-block",
|
|
2886
|
+
filename,
|
|
2887
|
+
search: searchLines.join("\n"),
|
|
2888
|
+
replace: replaceLines.join("\n")
|
|
2889
|
+
});
|
|
2890
|
+
i = k;
|
|
2891
|
+
continue;
|
|
2892
|
+
}
|
|
2893
|
+
para.push(filename);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2669
2896
|
const fence = line.match(/^```(\w*)/);
|
|
2670
2897
|
if (fence) {
|
|
2671
2898
|
if (inCode) {
|
|
@@ -2743,10 +2970,18 @@ function BlockView({ block }) {
|
|
|
2743
2970
|
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React2.createElement(Box2, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React2.createElement(InlineMd, { text: item }))));
|
|
2744
2971
|
case "code":
|
|
2745
2972
|
return /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, block.text));
|
|
2973
|
+
case "edit-block":
|
|
2974
|
+
return /* @__PURE__ */ React2.createElement(EditBlockRow, { block });
|
|
2746
2975
|
case "hr":
|
|
2747
2976
|
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");
|
|
2748
2977
|
}
|
|
2749
2978
|
}
|
|
2979
|
+
function EditBlockRow({ block }) {
|
|
2980
|
+
const isNewFile = block.search.length === 0;
|
|
2981
|
+
const searchLines = block.search.split("\n");
|
|
2982
|
+
const replaceLines = block.replace.split("\n");
|
|
2983
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, block.filename), isNewFile ? /* @__PURE__ */ React2.createElement(Text2, { color: "green", bold: true }, " (new file)") : null), isNewFile ? null : /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, searchLines.map((line, i) => /* @__PURE__ */ React2.createElement(Text2, { key: `s-${i}-${line.length}`, color: "red" }, `- ${line}`))), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: isNewFile ? 1 : 0 }, replaceLines.map((line, i) => /* @__PURE__ */ React2.createElement(Text2, { key: `r-${i}-${line.length}`, color: "green" }, `+ ${line}`))));
|
|
2984
|
+
}
|
|
2750
2985
|
function Markdown({ text }) {
|
|
2751
2986
|
const cleaned = stripMath(text);
|
|
2752
2987
|
const blocks = React2.useMemo(() => parseBlocks(cleaned), [cleaned]);
|
|
@@ -2771,6 +3006,9 @@ var EventRow = React3.memo(function EventRow2({ event }) {
|
|
|
2771
3006
|
if (event.role === "info") {
|
|
2772
3007
|
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, event.text));
|
|
2773
3008
|
}
|
|
3009
|
+
if (event.role === "warning") {
|
|
3010
|
+
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "\u25B8 "), /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, event.text));
|
|
3011
|
+
}
|
|
2774
3012
|
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, null, event.text));
|
|
2775
3013
|
});
|
|
2776
3014
|
function BranchBlock({ branch }) {
|
|
@@ -2809,7 +3047,17 @@ function StreamingAssistant({ event }) {
|
|
|
2809
3047
|
}
|
|
2810
3048
|
const tail = lastLine(event.text, 140);
|
|
2811
3049
|
const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
|
|
2812
|
-
|
|
3050
|
+
const reasoningOnly = !event.text && !!event.reasoning;
|
|
3051
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React3.createElement(Pulse, null), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ", "(", reasoningOnly ? "reasoning" : "streaming", " \xB7 ", event.text.length, event.reasoning ? ` + think ${event.reasoning.length}` : "", " chars)", " "), /* @__PURE__ */ React3.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "\u25B8 ", tail) : reasoningOnly ? /* @__PURE__ */ React3.createElement(Text3, { color: "yellow", dimColor: true }, " R1 is thinking before it speaks \u2014 body text starts when reasoning completes (typically 20-90s).") : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, " (waiting for first byte \u2014 connection is open)"));
|
|
3052
|
+
}
|
|
3053
|
+
function Pulse() {
|
|
3054
|
+
const [tick, setTick] = useState(0);
|
|
3055
|
+
useEffect(() => {
|
|
3056
|
+
const id = setInterval(() => setTick((t) => t + 1), 500);
|
|
3057
|
+
return () => clearInterval(id);
|
|
3058
|
+
}, []);
|
|
3059
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3060
|
+
return /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, frames[tick % frames.length]);
|
|
2813
3061
|
}
|
|
2814
3062
|
function lastLine(s, maxChars) {
|
|
2815
3063
|
const flat = s.replace(/\s+/g, " ").trim();
|
|
@@ -2873,6 +3121,7 @@ function formatTokens(n) {
|
|
|
2873
3121
|
}
|
|
2874
3122
|
|
|
2875
3123
|
// src/cli/ui/slash.ts
|
|
3124
|
+
import { spawnSync } from "child_process";
|
|
2876
3125
|
function parseSlash(text) {
|
|
2877
3126
|
if (!text.startsWith("/")) return null;
|
|
2878
3127
|
const parts = text.slice(1).trim().split(/\s+/);
|
|
@@ -2901,6 +3150,10 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
2901
3150
|
" /mcp list MCP servers + tools attached to this session",
|
|
2902
3151
|
" /setup (exit + reconfigure) \u2192 run `reasonix setup`",
|
|
2903
3152
|
" /compact [cap] shrink large tool results in history (default 4k/result)",
|
|
3153
|
+
" /apply (code mode) commit the pending edit blocks to disk",
|
|
3154
|
+
" /discard (code mode) drop pending edits without writing",
|
|
3155
|
+
" /undo (code mode) roll back the last applied edit batch",
|
|
3156
|
+
' /commit "msg" (code mode) git add -A && git commit -m "msg"',
|
|
2904
3157
|
" /sessions list saved sessions (current is marked with \u25B8)",
|
|
2905
3158
|
" /forget delete the current session from disk",
|
|
2906
3159
|
" /clear clear displayed history (log + session kept)",
|
|
@@ -2942,6 +3195,45 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
2942
3195
|
return {
|
|
2943
3196
|
info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
|
|
2944
3197
|
};
|
|
3198
|
+
case "undo": {
|
|
3199
|
+
if (!ctx.codeUndo) {
|
|
3200
|
+
return {
|
|
3201
|
+
info: "/undo is only available inside `reasonix code` \u2014 chat mode doesn't apply edits."
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
return { info: ctx.codeUndo() };
|
|
3205
|
+
}
|
|
3206
|
+
case "apply": {
|
|
3207
|
+
if (!ctx.codeApply) {
|
|
3208
|
+
return {
|
|
3209
|
+
info: "/apply is only available inside `reasonix code` (nothing to apply here)."
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
return { info: ctx.codeApply() };
|
|
3213
|
+
}
|
|
3214
|
+
case "discard": {
|
|
3215
|
+
if (!ctx.codeDiscard) {
|
|
3216
|
+
return {
|
|
3217
|
+
info: "/discard is only available inside `reasonix code`."
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
return { info: ctx.codeDiscard() };
|
|
3221
|
+
}
|
|
3222
|
+
case "commit": {
|
|
3223
|
+
if (!ctx.codeRoot) {
|
|
3224
|
+
return {
|
|
3225
|
+
info: "/commit is only available inside `reasonix code` (needs a rooted git repo)."
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
const raw = args.join(" ").trim();
|
|
3229
|
+
const message = stripOuterQuotes(raw);
|
|
3230
|
+
if (!message) {
|
|
3231
|
+
return {
|
|
3232
|
+
info: `usage: /commit "your commit message" \u2014 runs \`git add -A && git commit -m "\u2026"\` in ${ctx.codeRoot}`
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
return runGitCommit(ctx.codeRoot, message);
|
|
3236
|
+
}
|
|
2945
3237
|
case "compact": {
|
|
2946
3238
|
const tight = Number.parseInt(args[0] ?? "", 10);
|
|
2947
3239
|
const cap = Number.isFinite(tight) && tight >= 500 ? tight : 4e3;
|
|
@@ -3041,6 +3333,38 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
3041
3333
|
return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
|
|
3042
3334
|
}
|
|
3043
3335
|
}
|
|
3336
|
+
function stripOuterQuotes(s) {
|
|
3337
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
3338
|
+
return s.slice(1, -1);
|
|
3339
|
+
}
|
|
3340
|
+
return s;
|
|
3341
|
+
}
|
|
3342
|
+
function runGitCommit(rootDir, message) {
|
|
3343
|
+
const add = spawnSync("git", ["add", "-A"], { cwd: rootDir, encoding: "utf8" });
|
|
3344
|
+
if (add.error || add.status !== 0) {
|
|
3345
|
+
return { info: `git add failed (${add.status ?? "?"}):
|
|
3346
|
+
${gitTail(add)}` };
|
|
3347
|
+
}
|
|
3348
|
+
const commit = spawnSync("git", ["commit", "-m", message], {
|
|
3349
|
+
cwd: rootDir,
|
|
3350
|
+
encoding: "utf8"
|
|
3351
|
+
});
|
|
3352
|
+
if (commit.error || commit.status !== 0) {
|
|
3353
|
+
return { info: `git commit failed (${commit.status ?? "?"}):
|
|
3354
|
+
${gitTail(commit)}` };
|
|
3355
|
+
}
|
|
3356
|
+
const firstLine = (commit.stdout || "").split(/\r?\n/)[0] ?? "";
|
|
3357
|
+
return { info: `\u25B8 committed: ${message}${firstLine ? `
|
|
3358
|
+
${firstLine}` : ""}` };
|
|
3359
|
+
}
|
|
3360
|
+
function gitTail(res) {
|
|
3361
|
+
const stderr = res.stderr ?? "";
|
|
3362
|
+
const stdout2 = res.stdout ?? "";
|
|
3363
|
+
const body = stderr.trim() || stdout2.trim();
|
|
3364
|
+
if (body) return body;
|
|
3365
|
+
if (res.error) return res.error.message;
|
|
3366
|
+
return "(no output from git)";
|
|
3367
|
+
}
|
|
3044
3368
|
|
|
3045
3369
|
// src/cli/ui/App.tsx
|
|
3046
3370
|
var FLUSH_INTERVAL_MS = 60;
|
|
@@ -3052,13 +3376,18 @@ function App({
|
|
|
3052
3376
|
branch,
|
|
3053
3377
|
session,
|
|
3054
3378
|
tools,
|
|
3055
|
-
mcpSpecs
|
|
3379
|
+
mcpSpecs,
|
|
3380
|
+
codeMode
|
|
3056
3381
|
}) {
|
|
3057
3382
|
const { exit } = useApp();
|
|
3058
3383
|
const [historical, setHistorical] = useState2([]);
|
|
3059
3384
|
const [streaming, setStreaming] = useState2(null);
|
|
3060
3385
|
const [input, setInput] = useState2("");
|
|
3061
3386
|
const [busy, setBusy] = useState2(false);
|
|
3387
|
+
const abortedThisTurn = useRef(false);
|
|
3388
|
+
const [ongoingTool, setOngoingTool] = useState2(null);
|
|
3389
|
+
const lastEditSnapshots = useRef(null);
|
|
3390
|
+
const pendingEdits = useRef([]);
|
|
3062
3391
|
const [summary, setSummary] = useState2({
|
|
3063
3392
|
turns: 0,
|
|
3064
3393
|
totalCostUsd: 0,
|
|
@@ -3126,6 +3455,42 @@ function App({
|
|
|
3126
3455
|
]);
|
|
3127
3456
|
}
|
|
3128
3457
|
}, [session, loop]);
|
|
3458
|
+
useInput((_input, key) => {
|
|
3459
|
+
if (!key.escape) return;
|
|
3460
|
+
if (!busy) return;
|
|
3461
|
+
if (abortedThisTurn.current) return;
|
|
3462
|
+
abortedThisTurn.current = true;
|
|
3463
|
+
loop.abort();
|
|
3464
|
+
});
|
|
3465
|
+
const codeUndo = useCallback(() => {
|
|
3466
|
+
if (!codeMode) return "not in code mode";
|
|
3467
|
+
const snaps = lastEditSnapshots.current;
|
|
3468
|
+
if (!snaps || snaps.length === 0) {
|
|
3469
|
+
return "nothing to undo \u2014 no recent edit batch to restore";
|
|
3470
|
+
}
|
|
3471
|
+
const results = restoreSnapshots(snaps, codeMode.rootDir);
|
|
3472
|
+
lastEditSnapshots.current = null;
|
|
3473
|
+
return formatUndoResults(results);
|
|
3474
|
+
}, [codeMode]);
|
|
3475
|
+
const codeApply = useCallback(() => {
|
|
3476
|
+
if (!codeMode) return "not in code mode";
|
|
3477
|
+
const blocks = pendingEdits.current;
|
|
3478
|
+
if (blocks.length === 0) {
|
|
3479
|
+
return "nothing pending \u2014 the assistant hasn't proposed edits since the last /apply or /discard.";
|
|
3480
|
+
}
|
|
3481
|
+
const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
|
|
3482
|
+
const results = applyEditBlocks(blocks, codeMode.rootDir);
|
|
3483
|
+
const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
|
|
3484
|
+
if (anyApplied) lastEditSnapshots.current = snaps;
|
|
3485
|
+
pendingEdits.current = [];
|
|
3486
|
+
return formatEditResults(results);
|
|
3487
|
+
}, [codeMode]);
|
|
3488
|
+
const codeDiscard = useCallback(() => {
|
|
3489
|
+
const count = pendingEdits.current.length;
|
|
3490
|
+
if (count === 0) return "nothing pending to discard.";
|
|
3491
|
+
pendingEdits.current = [];
|
|
3492
|
+
return `\u25B8 discarded ${count} pending edit block(s). Nothing was written to disk.`;
|
|
3493
|
+
}, []);
|
|
3129
3494
|
const prefixHash = loop.prefix.fingerprint;
|
|
3130
3495
|
const writeTranscript = useCallback(
|
|
3131
3496
|
(ev) => {
|
|
@@ -3142,7 +3507,13 @@ function App({
|
|
|
3142
3507
|
setInput("");
|
|
3143
3508
|
const slash = parseSlash(text);
|
|
3144
3509
|
if (slash) {
|
|
3145
|
-
const result = handleSlash(slash.cmd, slash.args, loop, {
|
|
3510
|
+
const result = handleSlash(slash.cmd, slash.args, loop, {
|
|
3511
|
+
mcpSpecs,
|
|
3512
|
+
codeUndo: codeMode ? codeUndo : void 0,
|
|
3513
|
+
codeApply: codeMode ? codeApply : void 0,
|
|
3514
|
+
codeDiscard: codeMode ? codeDiscard : void 0,
|
|
3515
|
+
codeRoot: codeMode?.rootDir
|
|
3516
|
+
});
|
|
3146
3517
|
if (result.exit) {
|
|
3147
3518
|
transcriptRef.current?.end();
|
|
3148
3519
|
exit();
|
|
@@ -3171,6 +3542,7 @@ function App({
|
|
|
3171
3542
|
const reasoningBuf = { current: "" };
|
|
3172
3543
|
setStreaming({ id: assistantId, role: "assistant", text: "", streaming: true });
|
|
3173
3544
|
setBusy(true);
|
|
3545
|
+
abortedThisTurn.current = false;
|
|
3174
3546
|
const flush = () => {
|
|
3175
3547
|
if (!contentBuf.current && !reasoningBuf.current) return;
|
|
3176
3548
|
streamRef.text += contentBuf.current;
|
|
@@ -3213,12 +3585,13 @@ function App({
|
|
|
3213
3585
|
flush();
|
|
3214
3586
|
const repairNote = ev.repair ? describeRepair(ev.repair) : "";
|
|
3215
3587
|
setStreaming(null);
|
|
3588
|
+
const finalText = ev.content || streamRef.text;
|
|
3216
3589
|
setHistorical((prev) => [
|
|
3217
3590
|
...prev,
|
|
3218
3591
|
{
|
|
3219
3592
|
id: assistantId,
|
|
3220
3593
|
role: "assistant",
|
|
3221
|
-
text:
|
|
3594
|
+
text: finalText,
|
|
3222
3595
|
reasoning: streamRef.reasoning || void 0,
|
|
3223
3596
|
planState: ev.planState,
|
|
3224
3597
|
branch: ev.branch,
|
|
@@ -3227,8 +3600,25 @@ function App({
|
|
|
3227
3600
|
streaming: false
|
|
3228
3601
|
}
|
|
3229
3602
|
]);
|
|
3603
|
+
if (codeMode && finalText && !ev.forcedSummary) {
|
|
3604
|
+
const blocks = parseEditBlocks(finalText);
|
|
3605
|
+
if (blocks.length > 0) {
|
|
3606
|
+
pendingEdits.current = blocks;
|
|
3607
|
+
setHistorical((prev) => [
|
|
3608
|
+
...prev,
|
|
3609
|
+
{
|
|
3610
|
+
id: `pending-${Date.now()}`,
|
|
3611
|
+
role: "info",
|
|
3612
|
+
text: formatPendingPreview(blocks)
|
|
3613
|
+
}
|
|
3614
|
+
]);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
} else if (ev.role === "tool_start") {
|
|
3618
|
+
setOngoingTool({ name: ev.toolName ?? "?", args: ev.toolArgs });
|
|
3230
3619
|
} else if (ev.role === "tool") {
|
|
3231
3620
|
flush();
|
|
3621
|
+
setOngoingTool(null);
|
|
3232
3622
|
setHistorical((prev) => [
|
|
3233
3623
|
...prev,
|
|
3234
3624
|
{
|
|
@@ -3243,17 +3633,23 @@ function App({
|
|
|
3243
3633
|
...prev,
|
|
3244
3634
|
{ id: `e-${Date.now()}`, role: "error", text: ev.error ?? ev.content }
|
|
3245
3635
|
]);
|
|
3636
|
+
} else if (ev.role === "warning") {
|
|
3637
|
+
setHistorical((prev) => [
|
|
3638
|
+
...prev,
|
|
3639
|
+
{ id: `w-${Date.now()}-${Math.random()}`, role: "warning", text: ev.content }
|
|
3640
|
+
]);
|
|
3246
3641
|
}
|
|
3247
3642
|
}
|
|
3248
3643
|
flush();
|
|
3249
3644
|
} finally {
|
|
3250
3645
|
clearInterval(timer);
|
|
3251
3646
|
setStreaming(null);
|
|
3647
|
+
setOngoingTool(null);
|
|
3252
3648
|
setSummary(loop.stats.summary());
|
|
3253
3649
|
setBusy(false);
|
|
3254
3650
|
}
|
|
3255
3651
|
},
|
|
3256
|
-
[busy, exit, loop, mcpSpecs, writeTranscript]
|
|
3652
|
+
[busy, codeApply, codeDiscard, codeMode, codeUndo, exit, loop, mcpSpecs, writeTranscript]
|
|
3257
3653
|
);
|
|
3258
3654
|
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(
|
|
3259
3655
|
StatsPanel,
|
|
@@ -3264,10 +3660,53 @@ function App({
|
|
|
3264
3660
|
harvestOn: loop.harvestEnabled,
|
|
3265
3661
|
branchBudget: loop.branchOptions.budget
|
|
3266
3662
|
}
|
|
3267
|
-
), /* @__PURE__ */ React6.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React6.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React6.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React6.createElement(CommandStrip,
|
|
3663
|
+
), /* @__PURE__ */ React6.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React6.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(EventRow, { event: streaming })) : null, ongoingTool ? /* @__PURE__ */ React6.createElement(OngoingToolRow, { tool: ongoingTool }) : null, /* @__PURE__ */ React6.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React6.createElement(CommandStrip, { codeMode: !!codeMode }));
|
|
3268
3664
|
}
|
|
3269
|
-
function
|
|
3270
|
-
|
|
3665
|
+
function OngoingToolRow({ tool }) {
|
|
3666
|
+
const [tick, setTick] = useState2(0);
|
|
3667
|
+
useEffect2(() => {
|
|
3668
|
+
const id = setInterval(() => setTick((t) => t + 1), 120);
|
|
3669
|
+
return () => clearInterval(id);
|
|
3670
|
+
}, []);
|
|
3671
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3672
|
+
const argsPreview = tool.args && tool.args.length > 0 && tool.args !== "{}" ? ` ${tool.args.length > 60 ? `${tool.args.slice(0, 60)}\u2026` : tool.args}` : "";
|
|
3673
|
+
return /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, frames[tick % frames.length]), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), argsPreview ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, argsPreview) : null);
|
|
3674
|
+
}
|
|
3675
|
+
function CommandStrip({ codeMode }) {
|
|
3676
|
+
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /compact \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"), codeMode ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, 'code mode: /apply \xB7 /discard \xB7 /undo \xB7 /commit "msg" \u2014 edits NEVER write without /apply') : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Esc (while thinking) \u2014 abort & summarize what was found so far"));
|
|
3677
|
+
}
|
|
3678
|
+
function formatEditResults(results) {
|
|
3679
|
+
const lines = results.map((r) => {
|
|
3680
|
+
const mark = r.status === "applied" || r.status === "created" ? "\u2713" : "\u2717";
|
|
3681
|
+
const detail = r.message ? ` (${r.message})` : "";
|
|
3682
|
+
return ` ${mark} ${r.status.padEnd(11)} ${r.path}${detail}`;
|
|
3683
|
+
});
|
|
3684
|
+
const ok = results.filter((r) => r.status === "applied" || r.status === "created").length;
|
|
3685
|
+
const total = results.length;
|
|
3686
|
+
const header = `\u25B8 edit blocks: ${ok}/${total} applied \u2014 /undo to roll back, or \`git diff\` to review`;
|
|
3687
|
+
return [header, ...lines].join("\n");
|
|
3688
|
+
}
|
|
3689
|
+
function formatPendingPreview(blocks) {
|
|
3690
|
+
const lines = blocks.map((b) => {
|
|
3691
|
+
const removed = b.search === "" ? 0 : countLines2(b.search);
|
|
3692
|
+
const added = countLines2(b.replace);
|
|
3693
|
+
const tag = b.search === "" ? "NEW " : " ";
|
|
3694
|
+
return ` ${tag}${b.path} (-${removed} +${added} lines)`;
|
|
3695
|
+
});
|
|
3696
|
+
const header = `\u25B8 ${blocks.length} pending edit block(s) \u2014 /apply to commit to disk, /discard to drop`;
|
|
3697
|
+
return [header, ...lines].join("\n");
|
|
3698
|
+
}
|
|
3699
|
+
function countLines2(s) {
|
|
3700
|
+
if (s.length === 0) return 0;
|
|
3701
|
+
return (s.match(/\n/g)?.length ?? 0) + 1;
|
|
3702
|
+
}
|
|
3703
|
+
function formatUndoResults(results) {
|
|
3704
|
+
const lines = results.map((r) => {
|
|
3705
|
+
const mark = r.status === "applied" ? "\u2713" : "\u2717";
|
|
3706
|
+
const detail = r.message ? ` (${r.message})` : "";
|
|
3707
|
+
return ` ${mark} ${r.path}${detail}`;
|
|
3708
|
+
});
|
|
3709
|
+
return [`\u25B8 undo: restored ${results.length} file(s) to pre-edit state`, ...lines].join("\n");
|
|
3271
3710
|
}
|
|
3272
3711
|
function describeRepair(repair) {
|
|
3273
3712
|
const parts = [];
|
|
@@ -3341,7 +3780,8 @@ function Root({ initialKey, tools, mcpSpecs, ...appProps }) {
|
|
|
3341
3780
|
branch: appProps.branch,
|
|
3342
3781
|
session: appProps.session,
|
|
3343
3782
|
tools,
|
|
3344
|
-
mcpSpecs
|
|
3783
|
+
mcpSpecs,
|
|
3784
|
+
codeMode: appProps.codeMode
|
|
3345
3785
|
}
|
|
3346
3786
|
);
|
|
3347
3787
|
}
|
|
@@ -3397,14 +3837,40 @@ async function chatCommand(opts) {
|
|
|
3397
3837
|
}
|
|
3398
3838
|
}
|
|
3399
3839
|
|
|
3840
|
+
// src/cli/commands/code.tsx
|
|
3841
|
+
import { basename, resolve as resolve3 } from "path";
|
|
3842
|
+
async function codeCommand(opts = {}) {
|
|
3843
|
+
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-MMANQ36Z.js");
|
|
3844
|
+
const rootDir = resolve3(opts.dir ?? process.cwd());
|
|
3845
|
+
const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
|
|
3846
|
+
const fsSpec = `filesystem=npx -y @modelcontextprotocol/server-filesystem ${quoteIfNeeded(rootDir)}`;
|
|
3847
|
+
process.stderr.write(
|
|
3848
|
+
`\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}"
|
|
3849
|
+
`
|
|
3850
|
+
);
|
|
3851
|
+
await chatCommand({
|
|
3852
|
+
model: opts.model ?? "deepseek-reasoner",
|
|
3853
|
+
harvest: true,
|
|
3854
|
+
// smart preset's harvest setting, always on for code
|
|
3855
|
+
system: codeSystemPrompt2(rootDir),
|
|
3856
|
+
transcript: opts.transcript,
|
|
3857
|
+
session,
|
|
3858
|
+
mcp: [fsSpec],
|
|
3859
|
+
codeMode: { rootDir }
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
function quoteIfNeeded(s) {
|
|
3863
|
+
return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3400
3866
|
// src/cli/commands/diff.ts
|
|
3401
|
-
import { writeFileSync as
|
|
3402
|
-
import { basename } from "path";
|
|
3867
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
3868
|
+
import { basename as basename2 } from "path";
|
|
3403
3869
|
import { render as render2 } from "ink";
|
|
3404
3870
|
import React11 from "react";
|
|
3405
3871
|
|
|
3406
3872
|
// src/cli/ui/DiffApp.tsx
|
|
3407
|
-
import { Box as Box9, Static as Static2, Text as Text9, useApp as useApp3, useInput } from "ink";
|
|
3873
|
+
import { Box as Box9, Static as Static2, Text as Text9, useApp as useApp3, useInput as useInput2 } from "ink";
|
|
3408
3874
|
import React10, { useState as useState5 } from "react";
|
|
3409
3875
|
|
|
3410
3876
|
// src/cli/ui/RecordView.tsx
|
|
@@ -3449,7 +3915,7 @@ function DiffApp({ report }) {
|
|
|
3449
3915
|
const maxIdx = Math.max(0, report.pairs.length - 1);
|
|
3450
3916
|
const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
|
|
3451
3917
|
const [idx, setIdx] = useState5(Math.max(0, initialIdx));
|
|
3452
|
-
|
|
3918
|
+
useInput2((input, key) => {
|
|
3453
3919
|
if (input === "q" || key.ctrl && input === "c") {
|
|
3454
3920
|
exit();
|
|
3455
3921
|
return;
|
|
@@ -3535,8 +4001,8 @@ async function diffCommand(opts) {
|
|
|
3535
4001
|
const aParsed = readTranscript(opts.a);
|
|
3536
4002
|
const bParsed = readTranscript(opts.b);
|
|
3537
4003
|
const report = diffTranscripts(
|
|
3538
|
-
{ label: opts.labelA ??
|
|
3539
|
-
{ label: opts.labelB ??
|
|
4004
|
+
{ label: opts.labelA ?? basename2(opts.a), parsed: aParsed },
|
|
4005
|
+
{ label: opts.labelB ?? basename2(opts.b), parsed: bParsed }
|
|
3540
4006
|
);
|
|
3541
4007
|
const wantMarkdown = !!opts.mdPath;
|
|
3542
4008
|
const wantPrint = opts.print || !process.stdout.isTTY;
|
|
@@ -3544,7 +4010,7 @@ async function diffCommand(opts) {
|
|
|
3544
4010
|
if (wantMarkdown) {
|
|
3545
4011
|
console.log(renderSummaryTable(report));
|
|
3546
4012
|
const md = renderMarkdown(report);
|
|
3547
|
-
|
|
4013
|
+
writeFileSync4(opts.mdPath, md, "utf8");
|
|
3548
4014
|
console.log(`
|
|
3549
4015
|
markdown report written to ${opts.mdPath}`);
|
|
3550
4016
|
return;
|
|
@@ -3629,13 +4095,13 @@ import { render as render3 } from "ink";
|
|
|
3629
4095
|
import React13 from "react";
|
|
3630
4096
|
|
|
3631
4097
|
// src/cli/ui/ReplayApp.tsx
|
|
3632
|
-
import { Box as Box10, Static as Static3, Text as Text10, useApp as useApp4, useInput as
|
|
4098
|
+
import { Box as Box10, Static as Static3, Text as Text10, useApp as useApp4, useInput as useInput3 } from "ink";
|
|
3633
4099
|
import React12, { useMemo as useMemo2, useState as useState6 } from "react";
|
|
3634
4100
|
function ReplayApp({ meta, pages }) {
|
|
3635
4101
|
const { exit } = useApp4();
|
|
3636
4102
|
const maxIdx = Math.max(0, pages.length - 1);
|
|
3637
4103
|
const [idx, setIdx] = useState6(maxIdx);
|
|
3638
|
-
|
|
4104
|
+
useInput3((input, key) => {
|
|
3639
4105
|
if (input === "q" || key.ctrl && input === "c") {
|
|
3640
4106
|
exit();
|
|
3641
4107
|
return;
|
|
@@ -3990,12 +4456,12 @@ import { render as render4 } from "ink";
|
|
|
3990
4456
|
import React16 from "react";
|
|
3991
4457
|
|
|
3992
4458
|
// src/cli/ui/Wizard.tsx
|
|
3993
|
-
import { Box as Box12, Text as Text12, useApp as useApp5, useInput as
|
|
4459
|
+
import { Box as Box12, Text as Text12, useApp as useApp5, useInput as useInput5 } from "ink";
|
|
3994
4460
|
import TextInput3 from "ink-text-input";
|
|
3995
4461
|
import React15, { useState as useState8 } from "react";
|
|
3996
4462
|
|
|
3997
4463
|
// src/cli/ui/Select.tsx
|
|
3998
|
-
import { Box as Box11, Text as Text11, useInput as
|
|
4464
|
+
import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
|
|
3999
4465
|
import React14, { useState as useState7 } from "react";
|
|
4000
4466
|
function SingleSelect({
|
|
4001
4467
|
items,
|
|
@@ -4008,7 +4474,7 @@ function SingleSelect({
|
|
|
4008
4474
|
items.findIndex((i) => i.value === initialValue && !i.disabled)
|
|
4009
4475
|
);
|
|
4010
4476
|
const [index, setIndex] = useState7(initialIndex === -1 ? 0 : initialIndex);
|
|
4011
|
-
|
|
4477
|
+
useInput4((_input, key) => {
|
|
4012
4478
|
if (key.upArrow) {
|
|
4013
4479
|
setIndex((i) => findNextEnabled(items, i, -1));
|
|
4014
4480
|
} else if (key.downArrow) {
|
|
@@ -4042,7 +4508,7 @@ function MultiSelect({
|
|
|
4042
4508
|
return first === -1 ? 0 : first;
|
|
4043
4509
|
});
|
|
4044
4510
|
const [selected, setSelected] = useState7(new Set(initialSelected));
|
|
4045
|
-
|
|
4511
|
+
useInput4((input, key) => {
|
|
4046
4512
|
if (key.upArrow) {
|
|
4047
4513
|
setIndex((i) => findNextEnabled(items, i, -1));
|
|
4048
4514
|
} else if (key.downArrow) {
|
|
@@ -4128,7 +4594,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
|
4128
4594
|
catalogArgs: {}
|
|
4129
4595
|
});
|
|
4130
4596
|
const [error, setError] = useState8(null);
|
|
4131
|
-
|
|
4597
|
+
useInput5((_input, key) => {
|
|
4132
4598
|
if (key.escape && step !== "saved" && onCancel) onCancel();
|
|
4133
4599
|
});
|
|
4134
4600
|
if (step === "apiKey") {
|
|
@@ -4290,13 +4756,13 @@ function McpArgsStep({
|
|
|
4290
4756
|
)), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : null));
|
|
4291
4757
|
}
|
|
4292
4758
|
function ReviewConfirm({ onConfirm }) {
|
|
4293
|
-
|
|
4759
|
+
useInput5((_i, key) => {
|
|
4294
4760
|
if (key.return) onConfirm();
|
|
4295
4761
|
});
|
|
4296
4762
|
return null;
|
|
4297
4763
|
}
|
|
4298
4764
|
function ExitOnEnter({ onExit }) {
|
|
4299
|
-
|
|
4765
|
+
useInput5((_i, key) => {
|
|
4300
4766
|
if (key.return) onExit();
|
|
4301
4767
|
});
|
|
4302
4768
|
return null;
|
|
@@ -4353,10 +4819,10 @@ function buildSpec(name, argsByName) {
|
|
|
4353
4819
|
const entry = CATALOG_BY_NAME.get(name);
|
|
4354
4820
|
if (!entry) return name;
|
|
4355
4821
|
const userArg = entry.userArgs ? argsByName[name] : void 0;
|
|
4356
|
-
const tail = userArg ? ` ${
|
|
4822
|
+
const tail = userArg ? ` ${quoteIfNeeded2(userArg)}` : "";
|
|
4357
4823
|
return `${entry.name}=npx -y ${entry.package}${tail}`;
|
|
4358
4824
|
}
|
|
4359
|
-
function
|
|
4825
|
+
function quoteIfNeeded2(s) {
|
|
4360
4826
|
return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
4361
4827
|
}
|
|
4362
4828
|
|
|
@@ -4384,13 +4850,13 @@ async function setupCommand(_opts = {}) {
|
|
|
4384
4850
|
}
|
|
4385
4851
|
|
|
4386
4852
|
// src/cli/commands/stats.ts
|
|
4387
|
-
import { existsSync as
|
|
4853
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
|
|
4388
4854
|
function statsCommand(opts) {
|
|
4389
|
-
if (!
|
|
4855
|
+
if (!existsSync3(opts.transcript)) {
|
|
4390
4856
|
console.error(`no such transcript: ${opts.transcript}`);
|
|
4391
4857
|
process.exit(1);
|
|
4392
4858
|
}
|
|
4393
|
-
const lines =
|
|
4859
|
+
const lines = readFileSync6(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
|
|
4394
4860
|
let assistantTurns = 0;
|
|
4395
4861
|
let toolCalls = 0;
|
|
4396
4862
|
let lastTurn = 0;
|
|
@@ -4471,6 +4937,16 @@ program.action(async () => {
|
|
|
4471
4937
|
program.command("setup").description("Interactive wizard \u2014 API key, preset, MCP servers. Re-run any time to reconfigure.").action(async () => {
|
|
4472
4938
|
await setupCommand({});
|
|
4473
4939
|
});
|
|
4940
|
+
program.command("code [dir]").description(
|
|
4941
|
+
"Code-editing chat \u2014 filesystem MCP auto-bridged at <dir> (default: cwd), coding system prompt, smart preset. Model proposes SEARCH/REPLACE blocks; Reasonix applies them to disk."
|
|
4942
|
+
).option("-m, --model <id>", "Override default reasoner model").option("--no-session", "Disable session persistence for this run").option("--transcript <path>", "Write a JSONL transcript to this path").action(async (dir, opts) => {
|
|
4943
|
+
await codeCommand({
|
|
4944
|
+
dir,
|
|
4945
|
+
model: opts.model,
|
|
4946
|
+
noSession: opts.session === false,
|
|
4947
|
+
transcript: opts.transcript
|
|
4948
|
+
});
|
|
4949
|
+
});
|
|
4474
4950
|
program.command("chat").description("Interactive Ink TUI with live cache/cost panel.").option("-m, --model <id>", "DeepSeek model id (overrides preset)").option("-s, --system <prompt>", "System prompt (pinned in the immutable prefix)", DEFAULT_SYSTEM).option("--transcript <path>", "Write a JSONL transcript to this path").option(
|
|
4475
4951
|
"--preset <name>",
|
|
4476
4952
|
"Bundle of model + harvest + branch. One of: fast, smart, max. Overrides config.preset."
|