reasonix 0.3.2 → 0.4.3
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 +32 -0
- package/dist/cli/chunk-2P2MZLCE.js +81 -0
- package/dist/cli/chunk-2P2MZLCE.js.map +1 -0
- package/dist/cli/index.js +661 -55
- 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 +151 -3
- package/dist/index.js +327 -28
- 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);
|
|
@@ -1166,7 +1167,7 @@ var CacheFirstLoop = class {
|
|
|
1166
1167
|
this.prefix = opts.prefix;
|
|
1167
1168
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
1168
1169
|
this.model = opts.model ?? "deepseek-chat";
|
|
1169
|
-
this.maxToolIters = opts.maxToolIters ??
|
|
1170
|
+
this.maxToolIters = opts.maxToolIters ?? 64;
|
|
1170
1171
|
if (typeof opts.branch === "number") {
|
|
1171
1172
|
this.branchOptions = { budget: opts.branch };
|
|
1172
1173
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1282,6 +1283,39 @@ var CacheFirstLoop = class {
|
|
|
1282
1283
|
abort() {
|
|
1283
1284
|
this._aborted = true;
|
|
1284
1285
|
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Drop everything in the log after (and including) the most recent
|
|
1288
|
+
* user message. Used by `/retry` so the caller can re-send that
|
|
1289
|
+
* message with a fresh turn instead of layering another response on
|
|
1290
|
+
* top of the prior exchange. Returns the content of the dropped user
|
|
1291
|
+
* message, or `null` if there isn't one yet.
|
|
1292
|
+
*
|
|
1293
|
+
* Persists by rewriting the session file — otherwise the next
|
|
1294
|
+
* launch would rehydrate the old exchange and `/retry` would seem
|
|
1295
|
+
* to have done nothing.
|
|
1296
|
+
*/
|
|
1297
|
+
retryLastUser() {
|
|
1298
|
+
const entries = this.log.entries;
|
|
1299
|
+
let lastUserIdx = -1;
|
|
1300
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1301
|
+
if (entries[i].role === "user") {
|
|
1302
|
+
lastUserIdx = i;
|
|
1303
|
+
break;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (lastUserIdx < 0) return null;
|
|
1307
|
+
const raw = entries[lastUserIdx].content;
|
|
1308
|
+
const userText = typeof raw === "string" ? raw : "";
|
|
1309
|
+
const preserved = entries.slice(0, lastUserIdx).map((m) => ({ ...m }));
|
|
1310
|
+
this.log.compactInPlace(preserved);
|
|
1311
|
+
if (this.sessionName) {
|
|
1312
|
+
try {
|
|
1313
|
+
rewriteSession(this.sessionName, preserved);
|
|
1314
|
+
} catch {
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return userText;
|
|
1318
|
+
}
|
|
1285
1319
|
async *step(userInput) {
|
|
1286
1320
|
this._turn++;
|
|
1287
1321
|
this.scratch.reset();
|
|
@@ -1295,9 +1329,17 @@ var CacheFirstLoop = class {
|
|
|
1295
1329
|
yield {
|
|
1296
1330
|
turn: this._turn,
|
|
1297
1331
|
role: "warning",
|
|
1298
|
-
content: `aborted at iter ${iter}/${this.maxToolIters} \u2014
|
|
1332
|
+
content: `aborted at iter ${iter}/${this.maxToolIters} \u2014 stopped without producing a summary (press \u2191 + Enter or /retry to resume)`
|
|
1299
1333
|
};
|
|
1300
|
-
|
|
1334
|
+
const stoppedMsg = "[aborted by user (Esc) \u2014 no summary produced. Ask again or /retry when ready; prior tool output is still in the log.]";
|
|
1335
|
+
this.appendAndPersist({ role: "assistant", content: stoppedMsg });
|
|
1336
|
+
yield {
|
|
1337
|
+
turn: this._turn,
|
|
1338
|
+
role: "assistant_final",
|
|
1339
|
+
content: stoppedMsg,
|
|
1340
|
+
forcedSummary: true
|
|
1341
|
+
};
|
|
1342
|
+
yield { turn: this._turn, role: "done", content: stoppedMsg };
|
|
1301
1343
|
return;
|
|
1302
1344
|
}
|
|
1303
1345
|
if (!warnedForIterBudget && iter >= warnAt) {
|
|
@@ -1355,8 +1397,8 @@ var CacheFirstLoop = class {
|
|
|
1355
1397
|
}
|
|
1356
1398
|
);
|
|
1357
1399
|
for (let k = 0; k < budget; k++) {
|
|
1358
|
-
const sample = queue.shift() ?? await new Promise((
|
|
1359
|
-
waiter =
|
|
1400
|
+
const sample = queue.shift() ?? await new Promise((resolve4) => {
|
|
1401
|
+
waiter = resolve4;
|
|
1360
1402
|
});
|
|
1361
1403
|
yield {
|
|
1362
1404
|
turn: this._turn,
|
|
@@ -1476,9 +1518,28 @@ var CacheFirstLoop = class {
|
|
|
1476
1518
|
yield { turn: this._turn, role: "done", content: assistantContent };
|
|
1477
1519
|
return;
|
|
1478
1520
|
}
|
|
1521
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1522
|
+
if (usage && usage.promptTokens / ctxMax > 0.8) {
|
|
1523
|
+
yield {
|
|
1524
|
+
turn: this._turn,
|
|
1525
|
+
role: "warning",
|
|
1526
|
+
content: `context ${usage.promptTokens}/${ctxMax} (${Math.round(
|
|
1527
|
+
usage.promptTokens / ctxMax * 100
|
|
1528
|
+
)}%) \u2014 more tools would overflow. Forcing summary from what was gathered.`
|
|
1529
|
+
};
|
|
1530
|
+
yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1479
1533
|
for (const call of repairedCalls) {
|
|
1480
1534
|
const name = call.function?.name ?? "";
|
|
1481
1535
|
const args = call.function?.arguments ?? "{}";
|
|
1536
|
+
yield {
|
|
1537
|
+
turn: this._turn,
|
|
1538
|
+
role: "tool_start",
|
|
1539
|
+
content: "",
|
|
1540
|
+
toolName: name,
|
|
1541
|
+
toolArgs: args
|
|
1542
|
+
};
|
|
1482
1543
|
const result = await this.tools.dispatch(name, args);
|
|
1483
1544
|
this.appendAndPersist({
|
|
1484
1545
|
role: "tool",
|
|
@@ -1500,13 +1561,19 @@ var CacheFirstLoop = class {
|
|
|
1500
1561
|
async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
|
|
1501
1562
|
try {
|
|
1502
1563
|
const messages = this.buildMessages(null);
|
|
1564
|
+
messages.push({
|
|
1565
|
+
role: "user",
|
|
1566
|
+
content: "I'm out of tool-call budget for this turn. Summarize in plain prose what you learned from the tool results above. Do NOT emit any tool calls, function-call markup, DSML invocations, or SEARCH/REPLACE edit blocks \u2014 they will be silently discarded. Just plain text."
|
|
1567
|
+
});
|
|
1503
1568
|
const resp = await this.client.chat({
|
|
1504
1569
|
model: this.model,
|
|
1505
1570
|
messages
|
|
1506
1571
|
// no tools → model is forced to answer in text
|
|
1507
1572
|
});
|
|
1508
|
-
const
|
|
1509
|
-
const
|
|
1573
|
+
const rawContent = resp.content?.trim() ?? "";
|
|
1574
|
+
const cleaned = stripHallucinatedToolMarkup(rawContent);
|
|
1575
|
+
const summary = cleaned || "(model emitted fake tool-call markup instead of a prose summary \u2014 try /retry with a narrower question, or /think to inspect R1's reasoning)";
|
|
1576
|
+
const reasonPrefix = reasonPrefixFor(opts.reason, this.maxToolIters);
|
|
1510
1577
|
const annotated = `${reasonPrefix}
|
|
1511
1578
|
|
|
1512
1579
|
${summary}`;
|
|
@@ -1516,11 +1583,12 @@ ${summary}`;
|
|
|
1516
1583
|
turn: this._turn,
|
|
1517
1584
|
role: "assistant_final",
|
|
1518
1585
|
content: annotated,
|
|
1519
|
-
stats: summaryStats
|
|
1586
|
+
stats: summaryStats,
|
|
1587
|
+
forcedSummary: true
|
|
1520
1588
|
};
|
|
1521
1589
|
yield { turn: this._turn, role: "done", content: summary };
|
|
1522
1590
|
} catch (err) {
|
|
1523
|
-
const label = opts.reason
|
|
1591
|
+
const label = errorLabelFor(opts.reason, this.maxToolIters);
|
|
1524
1592
|
yield {
|
|
1525
1593
|
turn: this._turn,
|
|
1526
1594
|
role: "error",
|
|
@@ -1545,6 +1613,26 @@ ${summary}`;
|
|
|
1545
1613
|
return msg;
|
|
1546
1614
|
}
|
|
1547
1615
|
};
|
|
1616
|
+
function stripHallucinatedToolMarkup(s) {
|
|
1617
|
+
let out = s;
|
|
1618
|
+
out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
|
|
1619
|
+
out = out.replace(/<\|DSML\|function_calls>[\s\S]*?<\/?\|DSML\|function_calls>/g, "");
|
|
1620
|
+
out = out.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "");
|
|
1621
|
+
out = out.replace(/<|DSML|[\s\S]*$/g, "");
|
|
1622
|
+
return out.trim();
|
|
1623
|
+
}
|
|
1624
|
+
function reasonPrefixFor(reason, iterCap) {
|
|
1625
|
+
if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
|
|
1626
|
+
if (reason === "context-guard") {
|
|
1627
|
+
return "[context budget running low \u2014 summarizing before the next call would overflow]";
|
|
1628
|
+
}
|
|
1629
|
+
return `[tool-call budget (${iterCap}) reached \u2014 forcing summary from what I found]`;
|
|
1630
|
+
}
|
|
1631
|
+
function errorLabelFor(reason, iterCap) {
|
|
1632
|
+
if (reason === "aborted") return "aborted by user";
|
|
1633
|
+
if (reason === "context-guard") return "context-guard triggered (prompt > 80% of window)";
|
|
1634
|
+
return `tool-call budget (${iterCap}) reached`;
|
|
1635
|
+
}
|
|
1548
1636
|
function summarizeBranch(chosen, samples) {
|
|
1549
1637
|
return {
|
|
1550
1638
|
budget: samples.length,
|
|
@@ -2198,7 +2286,7 @@ var McpClient = class {
|
|
|
2198
2286
|
async request(method, params) {
|
|
2199
2287
|
const id = this.nextId++;
|
|
2200
2288
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
2201
|
-
const promise = new Promise((
|
|
2289
|
+
const promise = new Promise((resolve4, reject) => {
|
|
2202
2290
|
const timeout = setTimeout(() => {
|
|
2203
2291
|
this.pending.delete(id);
|
|
2204
2292
|
reject(
|
|
@@ -2206,7 +2294,7 @@ var McpClient = class {
|
|
|
2206
2294
|
);
|
|
2207
2295
|
}, this.requestTimeoutMs);
|
|
2208
2296
|
this.pending.set(id, {
|
|
2209
|
-
resolve:
|
|
2297
|
+
resolve: resolve4,
|
|
2210
2298
|
reject,
|
|
2211
2299
|
timeout
|
|
2212
2300
|
});
|
|
@@ -2290,12 +2378,12 @@ var StdioTransport = class {
|
|
|
2290
2378
|
}
|
|
2291
2379
|
async send(message) {
|
|
2292
2380
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
2293
|
-
return new Promise((
|
|
2381
|
+
return new Promise((resolve4, reject) => {
|
|
2294
2382
|
const line = `${JSON.stringify(message)}
|
|
2295
2383
|
`;
|
|
2296
2384
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
2297
2385
|
if (err) reject(err);
|
|
2298
|
-
else
|
|
2386
|
+
else resolve4();
|
|
2299
2387
|
});
|
|
2300
2388
|
});
|
|
2301
2389
|
}
|
|
@@ -2306,8 +2394,8 @@ var StdioTransport = class {
|
|
|
2306
2394
|
continue;
|
|
2307
2395
|
}
|
|
2308
2396
|
if (this.closed) return;
|
|
2309
|
-
const next = await new Promise((
|
|
2310
|
-
this.waiters.push(
|
|
2397
|
+
const next = await new Promise((resolve4) => {
|
|
2398
|
+
this.waiters.push(resolve4);
|
|
2311
2399
|
});
|
|
2312
2400
|
if (next === null) return;
|
|
2313
2401
|
yield next;
|
|
@@ -2373,8 +2461,8 @@ var SseTransport = class {
|
|
|
2373
2461
|
constructor(opts) {
|
|
2374
2462
|
this.url = opts.url;
|
|
2375
2463
|
this.headers = opts.headers ?? {};
|
|
2376
|
-
this.endpointReady = new Promise((
|
|
2377
|
-
this.resolveEndpoint =
|
|
2464
|
+
this.endpointReady = new Promise((resolve4, reject) => {
|
|
2465
|
+
this.resolveEndpoint = resolve4;
|
|
2378
2466
|
this.rejectEndpoint = reject;
|
|
2379
2467
|
});
|
|
2380
2468
|
this.endpointReady.catch(() => void 0);
|
|
@@ -2401,8 +2489,8 @@ var SseTransport = class {
|
|
|
2401
2489
|
continue;
|
|
2402
2490
|
}
|
|
2403
2491
|
if (this.closed) return;
|
|
2404
|
-
const next = await new Promise((
|
|
2405
|
-
this.waiters.push(
|
|
2492
|
+
const next = await new Promise((resolve4) => {
|
|
2493
|
+
this.waiters.push(resolve4);
|
|
2406
2494
|
});
|
|
2407
2495
|
if (next === null) return;
|
|
2408
2496
|
yield next;
|
|
@@ -2570,8 +2658,133 @@ function parseMcpSpec(input) {
|
|
|
2570
2658
|
return { transport: "stdio", name, command, args };
|
|
2571
2659
|
}
|
|
2572
2660
|
|
|
2661
|
+
// src/code/edit-blocks.ts
|
|
2662
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2663
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
2664
|
+
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
2665
|
+
function parseEditBlocks(text) {
|
|
2666
|
+
const out = [];
|
|
2667
|
+
BLOCK_RE.lastIndex = 0;
|
|
2668
|
+
let m = BLOCK_RE.exec(text);
|
|
2669
|
+
while (m !== null) {
|
|
2670
|
+
out.push({
|
|
2671
|
+
path: m[1].trim(),
|
|
2672
|
+
search: m[2],
|
|
2673
|
+
replace: m[3],
|
|
2674
|
+
offset: m.index
|
|
2675
|
+
});
|
|
2676
|
+
m = BLOCK_RE.exec(text);
|
|
2677
|
+
}
|
|
2678
|
+
return out;
|
|
2679
|
+
}
|
|
2680
|
+
function applyEditBlock(block, rootDir) {
|
|
2681
|
+
const absRoot = resolve2(rootDir);
|
|
2682
|
+
const absTarget = resolve2(absRoot, block.path);
|
|
2683
|
+
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
2684
|
+
return {
|
|
2685
|
+
path: block.path,
|
|
2686
|
+
status: "path-escape",
|
|
2687
|
+
message: `resolved path ${absTarget} is outside rootDir ${absRoot}`
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
const searchEmpty = block.search.length === 0;
|
|
2691
|
+
const exists = existsSync2(absTarget);
|
|
2692
|
+
try {
|
|
2693
|
+
if (!exists) {
|
|
2694
|
+
if (!searchEmpty) {
|
|
2695
|
+
return {
|
|
2696
|
+
path: block.path,
|
|
2697
|
+
status: "file-missing",
|
|
2698
|
+
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
mkdirSync3(dirname3(absTarget), { recursive: true });
|
|
2702
|
+
writeFileSync3(absTarget, block.replace, "utf8");
|
|
2703
|
+
return { path: block.path, status: "created" };
|
|
2704
|
+
}
|
|
2705
|
+
const content = readFileSync5(absTarget, "utf8");
|
|
2706
|
+
if (searchEmpty) {
|
|
2707
|
+
return {
|
|
2708
|
+
path: block.path,
|
|
2709
|
+
status: "not-found",
|
|
2710
|
+
message: "empty SEARCH only creates new files \u2014 this file already exists"
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
const idx = content.indexOf(block.search);
|
|
2714
|
+
if (idx === -1) {
|
|
2715
|
+
return {
|
|
2716
|
+
path: block.path,
|
|
2717
|
+
status: "not-found",
|
|
2718
|
+
message: "SEARCH text does not match the current file content exactly"
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
|
|
2722
|
+
writeFileSync3(absTarget, replaced, "utf8");
|
|
2723
|
+
return { path: block.path, status: "applied" };
|
|
2724
|
+
} catch (err) {
|
|
2725
|
+
return { path: block.path, status: "error", message: err.message };
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
function applyEditBlocks(blocks, rootDir) {
|
|
2729
|
+
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
2730
|
+
}
|
|
2731
|
+
function snapshotBeforeEdits(blocks, rootDir) {
|
|
2732
|
+
const absRoot = resolve2(rootDir);
|
|
2733
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2734
|
+
const snapshots = [];
|
|
2735
|
+
for (const b of blocks) {
|
|
2736
|
+
if (seen.has(b.path)) continue;
|
|
2737
|
+
seen.add(b.path);
|
|
2738
|
+
const abs = resolve2(absRoot, b.path);
|
|
2739
|
+
if (!existsSync2(abs)) {
|
|
2740
|
+
snapshots.push({ path: b.path, prevContent: null });
|
|
2741
|
+
continue;
|
|
2742
|
+
}
|
|
2743
|
+
try {
|
|
2744
|
+
snapshots.push({ path: b.path, prevContent: readFileSync5(abs, "utf8") });
|
|
2745
|
+
} catch {
|
|
2746
|
+
snapshots.push({ path: b.path, prevContent: null });
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
return snapshots;
|
|
2750
|
+
}
|
|
2751
|
+
function restoreSnapshots(snapshots, rootDir) {
|
|
2752
|
+
const absRoot = resolve2(rootDir);
|
|
2753
|
+
return snapshots.map((snap) => {
|
|
2754
|
+
const abs = resolve2(absRoot, snap.path);
|
|
2755
|
+
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
2756
|
+
return {
|
|
2757
|
+
path: snap.path,
|
|
2758
|
+
status: "path-escape",
|
|
2759
|
+
message: "snapshot path escapes rootDir \u2014 refusing to restore"
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
try {
|
|
2763
|
+
if (snap.prevContent === null) {
|
|
2764
|
+
if (existsSync2(abs)) unlinkSync2(abs);
|
|
2765
|
+
return {
|
|
2766
|
+
path: snap.path,
|
|
2767
|
+
status: "applied",
|
|
2768
|
+
message: "removed (the edit had created it)"
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
writeFileSync3(abs, snap.prevContent, "utf8");
|
|
2772
|
+
return {
|
|
2773
|
+
path: snap.path,
|
|
2774
|
+
status: "applied",
|
|
2775
|
+
message: "restored to pre-edit content"
|
|
2776
|
+
};
|
|
2777
|
+
} catch (err) {
|
|
2778
|
+
return { path: snap.path, status: "error", message: err.message };
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
function sep() {
|
|
2783
|
+
return process.platform === "win32" ? "\\" : "/";
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2573
2786
|
// src/index.ts
|
|
2574
|
-
var VERSION = "0.3
|
|
2787
|
+
var VERSION = "0.4.3";
|
|
2575
2788
|
|
|
2576
2789
|
// src/cli/commands/chat.tsx
|
|
2577
2790
|
import { render } from "ink";
|
|
@@ -2702,8 +2915,39 @@ function parseBlocks(raw) {
|
|
|
2702
2915
|
listBuf = null;
|
|
2703
2916
|
}
|
|
2704
2917
|
};
|
|
2705
|
-
for (
|
|
2918
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2919
|
+
const rawLine = lines[i];
|
|
2706
2920
|
const line = rawLine.replace(/\s+$/g, "");
|
|
2921
|
+
if (!inCode && /^<{7} SEARCH\s*$/.test(line)) {
|
|
2922
|
+
const filename = para.pop()?.trim();
|
|
2923
|
+
if (filename) {
|
|
2924
|
+
flushPara();
|
|
2925
|
+
flushList();
|
|
2926
|
+
let j = i + 1;
|
|
2927
|
+
const searchLines = [];
|
|
2928
|
+
while (j < lines.length && !/^={7}\s*$/.test(lines[j])) {
|
|
2929
|
+
searchLines.push(lines[j]);
|
|
2930
|
+
j++;
|
|
2931
|
+
}
|
|
2932
|
+
const replaceLines = [];
|
|
2933
|
+
let k = j + 1;
|
|
2934
|
+
while (k < lines.length && !/^>{7} REPLACE\s*$/.test(lines[k])) {
|
|
2935
|
+
replaceLines.push(lines[k]);
|
|
2936
|
+
k++;
|
|
2937
|
+
}
|
|
2938
|
+
if (j < lines.length && k < lines.length) {
|
|
2939
|
+
out.push({
|
|
2940
|
+
kind: "edit-block",
|
|
2941
|
+
filename,
|
|
2942
|
+
search: searchLines.join("\n"),
|
|
2943
|
+
replace: replaceLines.join("\n")
|
|
2944
|
+
});
|
|
2945
|
+
i = k;
|
|
2946
|
+
continue;
|
|
2947
|
+
}
|
|
2948
|
+
para.push(filename);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2707
2951
|
const fence = line.match(/^```(\w*)/);
|
|
2708
2952
|
if (fence) {
|
|
2709
2953
|
if (inCode) {
|
|
@@ -2781,10 +3025,18 @@ function BlockView({ block }) {
|
|
|
2781
3025
|
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 }))));
|
|
2782
3026
|
case "code":
|
|
2783
3027
|
return /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, block.text));
|
|
3028
|
+
case "edit-block":
|
|
3029
|
+
return /* @__PURE__ */ React2.createElement(EditBlockRow, { block });
|
|
2784
3030
|
case "hr":
|
|
2785
3031
|
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");
|
|
2786
3032
|
}
|
|
2787
3033
|
}
|
|
3034
|
+
function EditBlockRow({ block }) {
|
|
3035
|
+
const isNewFile = block.search.length === 0;
|
|
3036
|
+
const searchLines = block.search.split("\n");
|
|
3037
|
+
const replaceLines = block.replace.split("\n");
|
|
3038
|
+
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}`))));
|
|
3039
|
+
}
|
|
2788
3040
|
function Markdown({ text }) {
|
|
2789
3041
|
const cleaned = stripMath(text);
|
|
2790
3042
|
const blocks = React2.useMemo(() => parseBlocks(cleaned), [cleaned]);
|
|
@@ -2801,7 +3053,10 @@ var EventRow = React3.memo(function EventRow2({ event }) {
|
|
|
2801
3053
|
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React3.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React3.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React3.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React3.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React3.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React3.createElement(Text3, { color: "magenta" }, event.repair) : null);
|
|
2802
3054
|
}
|
|
2803
3055
|
if (event.role === "tool") {
|
|
2804
|
-
|
|
3056
|
+
const isError = event.text.startsWith("ERROR:");
|
|
3057
|
+
const color = isError ? "red" : "yellow";
|
|
3058
|
+
const marker = isError ? "\u2717" : "\u2192";
|
|
3059
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color }, `tool<${event.toolName ?? "?"}> ${marker}`), /* @__PURE__ */ React3.createElement(Text3, { color: isError ? "red" : void 0, dimColor: !isError }, " ", truncate2(event.text, 400)));
|
|
2805
3060
|
}
|
|
2806
3061
|
if (event.role === "error") {
|
|
2807
3062
|
return /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, event.text));
|
|
@@ -2823,9 +3078,9 @@ function BranchBlock({ branch }) {
|
|
|
2823
3078
|
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "blue" }, "\u{1F500} branched ", /* @__PURE__ */ React3.createElement(Text3, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, per)));
|
|
2824
3079
|
}
|
|
2825
3080
|
function ReasoningBlock({ reasoning }) {
|
|
2826
|
-
const max =
|
|
3081
|
+
const max = 260;
|
|
2827
3082
|
const flat = reasoning.replace(/\s+/g, " ").trim();
|
|
2828
|
-
const preview = flat.length <= max ? flat :
|
|
3083
|
+
const preview = flat.length <= max ? flat : `\u2026 (+${flat.length - max} earlier chars) ${flat.slice(-max)}`;
|
|
2829
3084
|
return /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
|
|
2830
3085
|
}
|
|
2831
3086
|
function Elapsed() {
|
|
@@ -2850,7 +3105,17 @@ function StreamingAssistant({ event }) {
|
|
|
2850
3105
|
}
|
|
2851
3106
|
const tail = lastLine(event.text, 140);
|
|
2852
3107
|
const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
|
|
2853
|
-
|
|
3108
|
+
const reasoningOnly = !event.text && !!event.reasoning;
|
|
3109
|
+
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)"));
|
|
3110
|
+
}
|
|
3111
|
+
function Pulse() {
|
|
3112
|
+
const [tick, setTick] = useState(0);
|
|
3113
|
+
useEffect(() => {
|
|
3114
|
+
const id = setInterval(() => setTick((t) => t + 1), 500);
|
|
3115
|
+
return () => clearInterval(id);
|
|
3116
|
+
}, []);
|
|
3117
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3118
|
+
return /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, frames[tick % frames.length]);
|
|
2854
3119
|
}
|
|
2855
3120
|
function lastLine(s, maxChars) {
|
|
2856
3121
|
const flat = s.replace(/\s+/g, " ").trim();
|
|
@@ -2914,6 +3179,7 @@ function formatTokens(n) {
|
|
|
2914
3179
|
}
|
|
2915
3180
|
|
|
2916
3181
|
// src/cli/ui/slash.ts
|
|
3182
|
+
import { spawnSync } from "child_process";
|
|
2917
3183
|
function parseSlash(text) {
|
|
2918
3184
|
if (!text.startsWith("/")) return null;
|
|
2919
3185
|
const parts = text.slice(1).trim().split(/\s+/);
|
|
@@ -2942,6 +3208,12 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
2942
3208
|
" /mcp list MCP servers + tools attached to this session",
|
|
2943
3209
|
" /setup (exit + reconfigure) \u2192 run `reasonix setup`",
|
|
2944
3210
|
" /compact [cap] shrink large tool results in history (default 4k/result)",
|
|
3211
|
+
" /think dump the most recent turn's full R1 reasoning (reasoner only)",
|
|
3212
|
+
" /retry truncate & resend your last message (fresh sample from the model)",
|
|
3213
|
+
" /apply (code mode) commit the pending edit blocks to disk",
|
|
3214
|
+
" /discard (code mode) drop pending edits without writing",
|
|
3215
|
+
" /undo (code mode) roll back the last applied edit batch",
|
|
3216
|
+
' /commit "msg" (code mode) git add -A && git commit -m "msg"',
|
|
2945
3217
|
" /sessions list saved sessions (current is marked with \u25B8)",
|
|
2946
3218
|
" /forget delete the current session from disk",
|
|
2947
3219
|
" /clear clear displayed history (log + session kept)",
|
|
@@ -2983,6 +3255,70 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
2983
3255
|
return {
|
|
2984
3256
|
info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
|
|
2985
3257
|
};
|
|
3258
|
+
case "retry": {
|
|
3259
|
+
const prev = loop.retryLastUser();
|
|
3260
|
+
if (!prev) {
|
|
3261
|
+
return {
|
|
3262
|
+
info: "nothing to retry \u2014 no prior user message in this session's log."
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
const preview = prev.length > 80 ? `${prev.slice(0, 80)}\u2026` : prev;
|
|
3266
|
+
return {
|
|
3267
|
+
info: `\u25B8 retrying: "${preview}"`,
|
|
3268
|
+
resubmit: prev
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
case "think":
|
|
3272
|
+
case "reasoning": {
|
|
3273
|
+
const raw = loop.scratch.reasoning;
|
|
3274
|
+
if (!raw || !raw.trim()) {
|
|
3275
|
+
return {
|
|
3276
|
+
info: "no reasoning cached. `/think` shows the full R1 thought for the most recent turn \u2014 only `deepseek-reasoner` produces it, and only once the turn completes."
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
return { info: `\u21B3 full thinking (${raw.length} chars):
|
|
3280
|
+
|
|
3281
|
+
${raw.trim()}` };
|
|
3282
|
+
}
|
|
3283
|
+
case "undo": {
|
|
3284
|
+
if (!ctx.codeUndo) {
|
|
3285
|
+
return {
|
|
3286
|
+
info: "/undo is only available inside `reasonix code` \u2014 chat mode doesn't apply edits."
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
return { info: ctx.codeUndo() };
|
|
3290
|
+
}
|
|
3291
|
+
case "apply": {
|
|
3292
|
+
if (!ctx.codeApply) {
|
|
3293
|
+
return {
|
|
3294
|
+
info: "/apply is only available inside `reasonix code` (nothing to apply here)."
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
return { info: ctx.codeApply() };
|
|
3298
|
+
}
|
|
3299
|
+
case "discard": {
|
|
3300
|
+
if (!ctx.codeDiscard) {
|
|
3301
|
+
return {
|
|
3302
|
+
info: "/discard is only available inside `reasonix code`."
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
return { info: ctx.codeDiscard() };
|
|
3306
|
+
}
|
|
3307
|
+
case "commit": {
|
|
3308
|
+
if (!ctx.codeRoot) {
|
|
3309
|
+
return {
|
|
3310
|
+
info: "/commit is only available inside `reasonix code` (needs a rooted git repo)."
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
const raw = args.join(" ").trim();
|
|
3314
|
+
const message = stripOuterQuotes(raw);
|
|
3315
|
+
if (!message) {
|
|
3316
|
+
return {
|
|
3317
|
+
info: `usage: /commit "your commit message" \u2014 runs \`git add -A && git commit -m "\u2026"\` in ${ctx.codeRoot}`
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
return runGitCommit(ctx.codeRoot, message);
|
|
3321
|
+
}
|
|
2986
3322
|
case "compact": {
|
|
2987
3323
|
const tight = Number.parseInt(args[0] ?? "", 10);
|
|
2988
3324
|
const cap = Number.isFinite(tight) && tight >= 500 ? tight : 4e3;
|
|
@@ -3028,9 +3364,25 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
3028
3364
|
}
|
|
3029
3365
|
case "status": {
|
|
3030
3366
|
const branchBudget = loop.branchOptions.budget ?? 1;
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3367
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
3368
|
+
const lastPromptTokens = loop.stats.summary().lastPromptTokens;
|
|
3369
|
+
const ctxPct = ctxMax > 0 ? Math.round(lastPromptTokens / ctxMax * 100) : 0;
|
|
3370
|
+
const ctxLine = lastPromptTokens > 0 ? ` ctx ${compactNum(lastPromptTokens)}/${compactNum(ctxMax)} (${ctxPct}%)` : " ctx no turns yet";
|
|
3371
|
+
const pending = ctx.pendingEditCount ?? 0;
|
|
3372
|
+
const sessionLine = loop.sessionName ? ` session "${loop.sessionName}" \xB7 ${loop.log.length} messages in log (resumed ${loop.resumedMessageCount})` : " session (ephemeral \u2014 no persistence)";
|
|
3373
|
+
const mcpCount = ctx.mcpSpecs?.length ?? 0;
|
|
3374
|
+
const toolCount = loop.prefix.toolSpecs.length;
|
|
3375
|
+
const mcpLine = ` mcp ${mcpCount} server(s), ${toolCount} tool(s) in registry`;
|
|
3376
|
+
const pendingLine = pending > 0 ? ` edits ${pending} pending (/apply to commit, /discard to drop)` : "";
|
|
3377
|
+
const lines = [
|
|
3378
|
+
` model ${loop.model}`,
|
|
3379
|
+
` flags harvest=${loop.harvestEnabled ? "on" : "off"} \xB7 branch=${branchBudget > 1 ? branchBudget : "off"} \xB7 stream=${loop.stream ? "on" : "off"}`,
|
|
3380
|
+
ctxLine,
|
|
3381
|
+
mcpLine,
|
|
3382
|
+
sessionLine
|
|
3383
|
+
];
|
|
3384
|
+
if (pendingLine) lines.push(pendingLine);
|
|
3385
|
+
return { info: lines.join("\n") };
|
|
3034
3386
|
}
|
|
3035
3387
|
case "model": {
|
|
3036
3388
|
const id = args[0];
|
|
@@ -3082,6 +3434,43 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
3082
3434
|
return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
|
|
3083
3435
|
}
|
|
3084
3436
|
}
|
|
3437
|
+
function compactNum(n) {
|
|
3438
|
+
if (n < 1e3) return String(n);
|
|
3439
|
+
const k = n / 1e3;
|
|
3440
|
+
return k >= 100 ? `${Math.round(k)}k` : `${k.toFixed(1)}k`;
|
|
3441
|
+
}
|
|
3442
|
+
function stripOuterQuotes(s) {
|
|
3443
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
3444
|
+
return s.slice(1, -1);
|
|
3445
|
+
}
|
|
3446
|
+
return s;
|
|
3447
|
+
}
|
|
3448
|
+
function runGitCommit(rootDir, message) {
|
|
3449
|
+
const add = spawnSync("git", ["add", "-A"], { cwd: rootDir, encoding: "utf8" });
|
|
3450
|
+
if (add.error || add.status !== 0) {
|
|
3451
|
+
return { info: `git add failed (${add.status ?? "?"}):
|
|
3452
|
+
${gitTail(add)}` };
|
|
3453
|
+
}
|
|
3454
|
+
const commit = spawnSync("git", ["commit", "-m", message], {
|
|
3455
|
+
cwd: rootDir,
|
|
3456
|
+
encoding: "utf8"
|
|
3457
|
+
});
|
|
3458
|
+
if (commit.error || commit.status !== 0) {
|
|
3459
|
+
return { info: `git commit failed (${commit.status ?? "?"}):
|
|
3460
|
+
${gitTail(commit)}` };
|
|
3461
|
+
}
|
|
3462
|
+
const firstLine = (commit.stdout || "").split(/\r?\n/)[0] ?? "";
|
|
3463
|
+
return { info: `\u25B8 committed: ${message}${firstLine ? `
|
|
3464
|
+
${firstLine}` : ""}` };
|
|
3465
|
+
}
|
|
3466
|
+
function gitTail(res) {
|
|
3467
|
+
const stderr = res.stderr ?? "";
|
|
3468
|
+
const stdout2 = res.stdout ?? "";
|
|
3469
|
+
const body = stderr.trim() || stdout2.trim();
|
|
3470
|
+
if (body) return body;
|
|
3471
|
+
if (res.error) return res.error.message;
|
|
3472
|
+
return "(no output from git)";
|
|
3473
|
+
}
|
|
3085
3474
|
|
|
3086
3475
|
// src/cli/ui/App.tsx
|
|
3087
3476
|
var FLUSH_INTERVAL_MS = 60;
|
|
@@ -3093,7 +3482,8 @@ function App({
|
|
|
3093
3482
|
branch,
|
|
3094
3483
|
session,
|
|
3095
3484
|
tools,
|
|
3096
|
-
mcpSpecs
|
|
3485
|
+
mcpSpecs,
|
|
3486
|
+
codeMode
|
|
3097
3487
|
}) {
|
|
3098
3488
|
const { exit } = useApp();
|
|
3099
3489
|
const [historical, setHistorical] = useState2([]);
|
|
@@ -3101,6 +3491,11 @@ function App({
|
|
|
3101
3491
|
const [input, setInput] = useState2("");
|
|
3102
3492
|
const [busy, setBusy] = useState2(false);
|
|
3103
3493
|
const abortedThisTurn = useRef(false);
|
|
3494
|
+
const [ongoingTool, setOngoingTool] = useState2(null);
|
|
3495
|
+
const lastEditSnapshots = useRef(null);
|
|
3496
|
+
const pendingEdits = useRef([]);
|
|
3497
|
+
const promptHistory = useRef([]);
|
|
3498
|
+
const historyCursor = useRef(-1);
|
|
3104
3499
|
const [summary, setSummary] = useState2({
|
|
3105
3500
|
turns: 0,
|
|
3106
3501
|
totalCostUsd: 0,
|
|
@@ -3169,12 +3564,58 @@ function App({
|
|
|
3169
3564
|
}
|
|
3170
3565
|
}, [session, loop]);
|
|
3171
3566
|
useInput((_input, key) => {
|
|
3172
|
-
if (
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3567
|
+
if (key.escape && busy) {
|
|
3568
|
+
if (abortedThisTurn.current) return;
|
|
3569
|
+
abortedThisTurn.current = true;
|
|
3570
|
+
loop.abort();
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
if (busy) return;
|
|
3574
|
+
const hist = promptHistory.current;
|
|
3575
|
+
if (key.upArrow) {
|
|
3576
|
+
if (hist.length === 0) return;
|
|
3577
|
+
const nextCursor = Math.min(historyCursor.current + 1, hist.length - 1);
|
|
3578
|
+
historyCursor.current = nextCursor;
|
|
3579
|
+
setInput(hist[hist.length - 1 - nextCursor] ?? "");
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
if (key.downArrow) {
|
|
3583
|
+
if (historyCursor.current < 0) return;
|
|
3584
|
+
const nextCursor = historyCursor.current - 1;
|
|
3585
|
+
historyCursor.current = nextCursor;
|
|
3586
|
+
setInput(nextCursor < 0 ? "" : hist[hist.length - 1 - nextCursor] ?? "");
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3177
3589
|
});
|
|
3590
|
+
const codeUndo = useCallback(() => {
|
|
3591
|
+
if (!codeMode) return "not in code mode";
|
|
3592
|
+
const snaps = lastEditSnapshots.current;
|
|
3593
|
+
if (!snaps || snaps.length === 0) {
|
|
3594
|
+
return "nothing to undo \u2014 no recent edit batch to restore";
|
|
3595
|
+
}
|
|
3596
|
+
const results = restoreSnapshots(snaps, codeMode.rootDir);
|
|
3597
|
+
lastEditSnapshots.current = null;
|
|
3598
|
+
return formatUndoResults(results);
|
|
3599
|
+
}, [codeMode]);
|
|
3600
|
+
const codeApply = useCallback(() => {
|
|
3601
|
+
if (!codeMode) return "not in code mode";
|
|
3602
|
+
const blocks = pendingEdits.current;
|
|
3603
|
+
if (blocks.length === 0) {
|
|
3604
|
+
return "nothing pending \u2014 the assistant hasn't proposed edits since the last /apply or /discard.";
|
|
3605
|
+
}
|
|
3606
|
+
const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
|
|
3607
|
+
const results = applyEditBlocks(blocks, codeMode.rootDir);
|
|
3608
|
+
const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
|
|
3609
|
+
if (anyApplied) lastEditSnapshots.current = snaps;
|
|
3610
|
+
pendingEdits.current = [];
|
|
3611
|
+
return formatEditResults(results);
|
|
3612
|
+
}, [codeMode]);
|
|
3613
|
+
const codeDiscard = useCallback(() => {
|
|
3614
|
+
const count = pendingEdits.current.length;
|
|
3615
|
+
if (count === 0) return "nothing pending to discard.";
|
|
3616
|
+
pendingEdits.current = [];
|
|
3617
|
+
return `\u25B8 discarded ${count} pending edit block(s). Nothing was written to disk.`;
|
|
3618
|
+
}, []);
|
|
3178
3619
|
const prefixHash = loop.prefix.fingerprint;
|
|
3179
3620
|
const writeTranscript = useCallback(
|
|
3180
3621
|
(ev) => {
|
|
@@ -3186,12 +3627,26 @@ function App({
|
|
|
3186
3627
|
);
|
|
3187
3628
|
const handleSubmit = useCallback(
|
|
3188
3629
|
async (raw) => {
|
|
3189
|
-
|
|
3630
|
+
let text = raw.trim();
|
|
3190
3631
|
if (!text || busy) return;
|
|
3191
3632
|
setInput("");
|
|
3633
|
+
historyCursor.current = -1;
|
|
3634
|
+
if (codeMode && pendingEdits.current.length > 0 && (text === "y" || text === "n")) {
|
|
3635
|
+
const out = text === "y" ? codeApply() : codeDiscard();
|
|
3636
|
+
setHistorical((prev) => [...prev, { id: `sys-${Date.now()}`, role: "info", text: out }]);
|
|
3637
|
+
promptHistory.current.push(text);
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3192
3640
|
const slash = parseSlash(text);
|
|
3193
3641
|
if (slash) {
|
|
3194
|
-
const result = handleSlash(slash.cmd, slash.args, loop, {
|
|
3642
|
+
const result = handleSlash(slash.cmd, slash.args, loop, {
|
|
3643
|
+
mcpSpecs,
|
|
3644
|
+
codeUndo: codeMode ? codeUndo : void 0,
|
|
3645
|
+
codeApply: codeMode ? codeApply : void 0,
|
|
3646
|
+
codeDiscard: codeMode ? codeDiscard : void 0,
|
|
3647
|
+
codeRoot: codeMode?.rootDir,
|
|
3648
|
+
pendingEditCount: codeMode ? pendingEdits.current.length : void 0
|
|
3649
|
+
});
|
|
3195
3650
|
if (result.exit) {
|
|
3196
3651
|
transcriptRef.current?.end();
|
|
3197
3652
|
exit();
|
|
@@ -3211,8 +3666,14 @@ function App({
|
|
|
3211
3666
|
}
|
|
3212
3667
|
]);
|
|
3213
3668
|
}
|
|
3214
|
-
|
|
3669
|
+
if (result.resubmit) {
|
|
3670
|
+
text = result.resubmit;
|
|
3671
|
+
} else {
|
|
3672
|
+
promptHistory.current.push(text);
|
|
3673
|
+
return;
|
|
3674
|
+
}
|
|
3215
3675
|
}
|
|
3676
|
+
promptHistory.current.push(text);
|
|
3216
3677
|
setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
|
|
3217
3678
|
const assistantId = `a-${Date.now()}`;
|
|
3218
3679
|
const streamRef = { id: assistantId, text: "", reasoning: "" };
|
|
@@ -3263,12 +3724,13 @@ function App({
|
|
|
3263
3724
|
flush();
|
|
3264
3725
|
const repairNote = ev.repair ? describeRepair(ev.repair) : "";
|
|
3265
3726
|
setStreaming(null);
|
|
3727
|
+
const finalText = ev.content || streamRef.text;
|
|
3266
3728
|
setHistorical((prev) => [
|
|
3267
3729
|
...prev,
|
|
3268
3730
|
{
|
|
3269
3731
|
id: assistantId,
|
|
3270
3732
|
role: "assistant",
|
|
3271
|
-
text:
|
|
3733
|
+
text: finalText,
|
|
3272
3734
|
reasoning: streamRef.reasoning || void 0,
|
|
3273
3735
|
planState: ev.planState,
|
|
3274
3736
|
branch: ev.branch,
|
|
@@ -3277,8 +3739,25 @@ function App({
|
|
|
3277
3739
|
streaming: false
|
|
3278
3740
|
}
|
|
3279
3741
|
]);
|
|
3742
|
+
if (codeMode && finalText && !ev.forcedSummary) {
|
|
3743
|
+
const blocks = parseEditBlocks(finalText);
|
|
3744
|
+
if (blocks.length > 0) {
|
|
3745
|
+
pendingEdits.current = blocks;
|
|
3746
|
+
setHistorical((prev) => [
|
|
3747
|
+
...prev,
|
|
3748
|
+
{
|
|
3749
|
+
id: `pending-${Date.now()}`,
|
|
3750
|
+
role: "info",
|
|
3751
|
+
text: formatPendingPreview(blocks)
|
|
3752
|
+
}
|
|
3753
|
+
]);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
} else if (ev.role === "tool_start") {
|
|
3757
|
+
setOngoingTool({ name: ev.toolName ?? "?", args: ev.toolArgs });
|
|
3280
3758
|
} else if (ev.role === "tool") {
|
|
3281
3759
|
flush();
|
|
3760
|
+
setOngoingTool(null);
|
|
3282
3761
|
setHistorical((prev) => [
|
|
3283
3762
|
...prev,
|
|
3284
3763
|
{
|
|
@@ -3304,11 +3783,12 @@ function App({
|
|
|
3304
3783
|
} finally {
|
|
3305
3784
|
clearInterval(timer);
|
|
3306
3785
|
setStreaming(null);
|
|
3786
|
+
setOngoingTool(null);
|
|
3307
3787
|
setSummary(loop.stats.summary());
|
|
3308
3788
|
setBusy(false);
|
|
3309
3789
|
}
|
|
3310
3790
|
},
|
|
3311
|
-
[busy, exit, loop, mcpSpecs, writeTranscript]
|
|
3791
|
+
[busy, codeApply, codeDiscard, codeMode, codeUndo, exit, loop, mcpSpecs, writeTranscript]
|
|
3312
3792
|
);
|
|
3313
3793
|
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(
|
|
3314
3794
|
StatsPanel,
|
|
@@ -3319,10 +3799,99 @@ function App({
|
|
|
3319
3799
|
harvestOn: loop.harvestEnabled,
|
|
3320
3800
|
branchBudget: loop.branchOptions.budget
|
|
3321
3801
|
}
|
|
3322
|
-
), /* @__PURE__ */ React6.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React6.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React6.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React6.createElement(CommandStrip,
|
|
3802
|
+
), /* @__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 }));
|
|
3323
3803
|
}
|
|
3324
|
-
function
|
|
3325
|
-
|
|
3804
|
+
function OngoingToolRow({ tool }) {
|
|
3805
|
+
const [tick, setTick] = useState2(0);
|
|
3806
|
+
const [elapsed, setElapsed] = useState2(0);
|
|
3807
|
+
useEffect2(() => {
|
|
3808
|
+
const start = Date.now();
|
|
3809
|
+
const frameId = setInterval(() => setTick((t) => t + 1), 120);
|
|
3810
|
+
const secId = setInterval(() => setElapsed(Math.floor((Date.now() - start) / 1e3)), 1e3);
|
|
3811
|
+
return () => {
|
|
3812
|
+
clearInterval(frameId);
|
|
3813
|
+
clearInterval(secId);
|
|
3814
|
+
};
|
|
3815
|
+
}, []);
|
|
3816
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3817
|
+
const summary = summarizeToolArgs(tool.name, tool.args);
|
|
3818
|
+
return /* @__PURE__ */ React6.createElement(Box6, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Box6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, frames[tick % frames.length]), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, ` ${elapsed}s`)), summary ? /* @__PURE__ */ React6.createElement(Box6, { paddingLeft: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, summary)) : null);
|
|
3819
|
+
}
|
|
3820
|
+
function summarizeToolArgs(name, args) {
|
|
3821
|
+
if (!args || args === "{}") return "";
|
|
3822
|
+
let parsed;
|
|
3823
|
+
try {
|
|
3824
|
+
parsed = JSON.parse(args);
|
|
3825
|
+
} catch {
|
|
3826
|
+
return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
|
|
3827
|
+
}
|
|
3828
|
+
const hasSuffix = (s) => name === s || name.endsWith(`_${s}`);
|
|
3829
|
+
const path = typeof parsed.path === "string" ? parsed.path : void 0;
|
|
3830
|
+
if (hasSuffix("read_file")) {
|
|
3831
|
+
const head = typeof parsed.head === "number" ? `, head=${parsed.head}` : "";
|
|
3832
|
+
const tail = typeof parsed.tail === "number" ? `, tail=${parsed.tail}` : "";
|
|
3833
|
+
return `path: ${path ?? "?"}${head}${tail}`;
|
|
3834
|
+
}
|
|
3835
|
+
if (hasSuffix("write_file")) {
|
|
3836
|
+
const content = typeof parsed.content === "string" ? parsed.content : "";
|
|
3837
|
+
return `path: ${path ?? "?"} (${content.length} chars)`;
|
|
3838
|
+
}
|
|
3839
|
+
if (hasSuffix("edit_file")) {
|
|
3840
|
+
const edits = Array.isArray(parsed.edits) ? parsed.edits.length : 0;
|
|
3841
|
+
return `path: ${path ?? "?"} (${edits} edit${edits === 1 ? "" : "s"})`;
|
|
3842
|
+
}
|
|
3843
|
+
if (hasSuffix("list_directory") || hasSuffix("directory_tree")) {
|
|
3844
|
+
return `path: ${path ?? "?"}`;
|
|
3845
|
+
}
|
|
3846
|
+
if (hasSuffix("search_files")) {
|
|
3847
|
+
const pattern = typeof parsed.pattern === "string" ? parsed.pattern : "?";
|
|
3848
|
+
return `path: ${path ?? "?"} \xB7 pattern: ${pattern}`;
|
|
3849
|
+
}
|
|
3850
|
+
if (hasSuffix("move_file")) {
|
|
3851
|
+
const src = typeof parsed.source === "string" ? parsed.source : "?";
|
|
3852
|
+
const dst = typeof parsed.destination === "string" ? parsed.destination : "?";
|
|
3853
|
+
return `${src} \u2192 ${dst}`;
|
|
3854
|
+
}
|
|
3855
|
+
if (hasSuffix("get_file_info")) {
|
|
3856
|
+
return `path: ${path ?? "?"}`;
|
|
3857
|
+
}
|
|
3858
|
+
return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
|
|
3859
|
+
}
|
|
3860
|
+
function CommandStrip({ codeMode }) {
|
|
3861
|
+
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 (y) \xB7 /discard (n) \xB7 /undo \xB7 /commit "msg" \u2014 edits NEVER write without /apply') : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "\u2191/\u2193 recall prompts \xB7 /retry re-send last \xB7 /think see R1 full reasoning"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Esc (while thinking) \u2014 abort & summarize what was found so far"));
|
|
3862
|
+
}
|
|
3863
|
+
function formatEditResults(results) {
|
|
3864
|
+
const lines = results.map((r) => {
|
|
3865
|
+
const mark = r.status === "applied" || r.status === "created" ? "\u2713" : "\u2717";
|
|
3866
|
+
const detail = r.message ? ` (${r.message})` : "";
|
|
3867
|
+
return ` ${mark} ${r.status.padEnd(11)} ${r.path}${detail}`;
|
|
3868
|
+
});
|
|
3869
|
+
const ok = results.filter((r) => r.status === "applied" || r.status === "created").length;
|
|
3870
|
+
const total = results.length;
|
|
3871
|
+
const header = `\u25B8 edit blocks: ${ok}/${total} applied \u2014 /undo to roll back, or \`git diff\` to review`;
|
|
3872
|
+
return [header, ...lines].join("\n");
|
|
3873
|
+
}
|
|
3874
|
+
function formatPendingPreview(blocks) {
|
|
3875
|
+
const lines = blocks.map((b) => {
|
|
3876
|
+
const removed = b.search === "" ? 0 : countLines2(b.search);
|
|
3877
|
+
const added = countLines2(b.replace);
|
|
3878
|
+
const tag = b.search === "" ? "NEW " : " ";
|
|
3879
|
+
return ` ${tag}${b.path} (-${removed} +${added} lines)`;
|
|
3880
|
+
});
|
|
3881
|
+
const header = `\u25B8 ${blocks.length} pending edit block(s) \u2014 /apply (or y) to commit \xB7 /discard (or n) to drop`;
|
|
3882
|
+
return [header, ...lines].join("\n");
|
|
3883
|
+
}
|
|
3884
|
+
function countLines2(s) {
|
|
3885
|
+
if (s.length === 0) return 0;
|
|
3886
|
+
return (s.match(/\n/g)?.length ?? 0) + 1;
|
|
3887
|
+
}
|
|
3888
|
+
function formatUndoResults(results) {
|
|
3889
|
+
const lines = results.map((r) => {
|
|
3890
|
+
const mark = r.status === "applied" ? "\u2713" : "\u2717";
|
|
3891
|
+
const detail = r.message ? ` (${r.message})` : "";
|
|
3892
|
+
return ` ${mark} ${r.path}${detail}`;
|
|
3893
|
+
});
|
|
3894
|
+
return [`\u25B8 undo: restored ${results.length} file(s) to pre-edit state`, ...lines].join("\n");
|
|
3326
3895
|
}
|
|
3327
3896
|
function describeRepair(repair) {
|
|
3328
3897
|
const parts = [];
|
|
@@ -3396,7 +3965,8 @@ function Root({ initialKey, tools, mcpSpecs, ...appProps }) {
|
|
|
3396
3965
|
branch: appProps.branch,
|
|
3397
3966
|
session: appProps.session,
|
|
3398
3967
|
tools,
|
|
3399
|
-
mcpSpecs
|
|
3968
|
+
mcpSpecs,
|
|
3969
|
+
codeMode: appProps.codeMode
|
|
3400
3970
|
}
|
|
3401
3971
|
);
|
|
3402
3972
|
}
|
|
@@ -3452,9 +4022,35 @@ async function chatCommand(opts) {
|
|
|
3452
4022
|
}
|
|
3453
4023
|
}
|
|
3454
4024
|
|
|
4025
|
+
// src/cli/commands/code.tsx
|
|
4026
|
+
import { basename, resolve as resolve3 } from "path";
|
|
4027
|
+
async function codeCommand(opts = {}) {
|
|
4028
|
+
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-MMANQ36Z.js");
|
|
4029
|
+
const rootDir = resolve3(opts.dir ?? process.cwd());
|
|
4030
|
+
const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
|
|
4031
|
+
const fsSpec = `filesystem=npx -y @modelcontextprotocol/server-filesystem ${quoteIfNeeded(rootDir)}`;
|
|
4032
|
+
process.stderr.write(
|
|
4033
|
+
`\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}"
|
|
4034
|
+
`
|
|
4035
|
+
);
|
|
4036
|
+
await chatCommand({
|
|
4037
|
+
model: opts.model ?? "deepseek-reasoner",
|
|
4038
|
+
harvest: true,
|
|
4039
|
+
// smart preset's harvest setting, always on for code
|
|
4040
|
+
system: codeSystemPrompt2(rootDir),
|
|
4041
|
+
transcript: opts.transcript,
|
|
4042
|
+
session,
|
|
4043
|
+
mcp: [fsSpec],
|
|
4044
|
+
codeMode: { rootDir }
|
|
4045
|
+
});
|
|
4046
|
+
}
|
|
4047
|
+
function quoteIfNeeded(s) {
|
|
4048
|
+
return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
4049
|
+
}
|
|
4050
|
+
|
|
3455
4051
|
// src/cli/commands/diff.ts
|
|
3456
|
-
import { writeFileSync as
|
|
3457
|
-
import { basename } from "path";
|
|
4052
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
4053
|
+
import { basename as basename2 } from "path";
|
|
3458
4054
|
import { render as render2 } from "ink";
|
|
3459
4055
|
import React11 from "react";
|
|
3460
4056
|
|
|
@@ -3590,8 +4186,8 @@ async function diffCommand(opts) {
|
|
|
3590
4186
|
const aParsed = readTranscript(opts.a);
|
|
3591
4187
|
const bParsed = readTranscript(opts.b);
|
|
3592
4188
|
const report = diffTranscripts(
|
|
3593
|
-
{ label: opts.labelA ??
|
|
3594
|
-
{ label: opts.labelB ??
|
|
4189
|
+
{ label: opts.labelA ?? basename2(opts.a), parsed: aParsed },
|
|
4190
|
+
{ label: opts.labelB ?? basename2(opts.b), parsed: bParsed }
|
|
3595
4191
|
);
|
|
3596
4192
|
const wantMarkdown = !!opts.mdPath;
|
|
3597
4193
|
const wantPrint = opts.print || !process.stdout.isTTY;
|
|
@@ -3599,7 +4195,7 @@ async function diffCommand(opts) {
|
|
|
3599
4195
|
if (wantMarkdown) {
|
|
3600
4196
|
console.log(renderSummaryTable(report));
|
|
3601
4197
|
const md = renderMarkdown(report);
|
|
3602
|
-
|
|
4198
|
+
writeFileSync4(opts.mdPath, md, "utf8");
|
|
3603
4199
|
console.log(`
|
|
3604
4200
|
markdown report written to ${opts.mdPath}`);
|
|
3605
4201
|
return;
|
|
@@ -4408,10 +5004,10 @@ function buildSpec(name, argsByName) {
|
|
|
4408
5004
|
const entry = CATALOG_BY_NAME.get(name);
|
|
4409
5005
|
if (!entry) return name;
|
|
4410
5006
|
const userArg = entry.userArgs ? argsByName[name] : void 0;
|
|
4411
|
-
const tail = userArg ? ` ${
|
|
5007
|
+
const tail = userArg ? ` ${quoteIfNeeded2(userArg)}` : "";
|
|
4412
5008
|
return `${entry.name}=npx -y ${entry.package}${tail}`;
|
|
4413
5009
|
}
|
|
4414
|
-
function
|
|
5010
|
+
function quoteIfNeeded2(s) {
|
|
4415
5011
|
return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
4416
5012
|
}
|
|
4417
5013
|
|
|
@@ -4439,13 +5035,13 @@ async function setupCommand(_opts = {}) {
|
|
|
4439
5035
|
}
|
|
4440
5036
|
|
|
4441
5037
|
// src/cli/commands/stats.ts
|
|
4442
|
-
import { existsSync as
|
|
5038
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
|
|
4443
5039
|
function statsCommand(opts) {
|
|
4444
|
-
if (!
|
|
5040
|
+
if (!existsSync3(opts.transcript)) {
|
|
4445
5041
|
console.error(`no such transcript: ${opts.transcript}`);
|
|
4446
5042
|
process.exit(1);
|
|
4447
5043
|
}
|
|
4448
|
-
const lines =
|
|
5044
|
+
const lines = readFileSync6(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
|
|
4449
5045
|
let assistantTurns = 0;
|
|
4450
5046
|
let toolCalls = 0;
|
|
4451
5047
|
let lastTurn = 0;
|
|
@@ -4526,6 +5122,16 @@ program.action(async () => {
|
|
|
4526
5122
|
program.command("setup").description("Interactive wizard \u2014 API key, preset, MCP servers. Re-run any time to reconfigure.").action(async () => {
|
|
4527
5123
|
await setupCommand({});
|
|
4528
5124
|
});
|
|
5125
|
+
program.command("code [dir]").description(
|
|
5126
|
+
"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."
|
|
5127
|
+
).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) => {
|
|
5128
|
+
await codeCommand({
|
|
5129
|
+
dir,
|
|
5130
|
+
model: opts.model,
|
|
5131
|
+
noSession: opts.session === false,
|
|
5132
|
+
transcript: opts.transcript
|
|
5133
|
+
});
|
|
5134
|
+
});
|
|
4529
5135
|
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(
|
|
4530
5136
|
"--preset <name>",
|
|
4531
5137
|
"Bundle of model + harvest + branch. One of: fast, smart, max. Overrides config.preset."
|