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/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((resolve2, reject) => {
99
- const timer = setTimeout(resolve2, ms);
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 ?? 24;
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 forcing summary from what was gathered`
1332
+ content: `aborted at iter ${iter}/${this.maxToolIters} \u2014 stopped without producing a summary (press \u2191 + Enter or /retry to resume)`
1299
1333
  };
1300
- yield* this.forceSummaryAfterIterLimit({ reason: "aborted" });
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((resolve2) => {
1359
- waiter = resolve2;
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 summary = resp.content?.trim() || "(model returned no text; try a narrower question or raise --max-tool-iters)";
1509
- const reasonPrefix = opts.reason === "aborted" ? "[aborted by user (Esc) \u2014 summarizing what I found so far]" : `[tool-call budget (${this.maxToolIters}) reached \u2014 forcing summary from what I found]`;
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 === "aborted" ? "aborted by user" : `tool-call budget (${this.maxToolIters}) reached`;
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((resolve2, reject) => {
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: resolve2,
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((resolve2, reject) => {
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 resolve2();
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((resolve2) => {
2310
- this.waiters.push(resolve2);
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((resolve2, reject) => {
2377
- this.resolveEndpoint = resolve2;
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((resolve2) => {
2405
- this.waiters.push(resolve2);
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.2";
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 (const rawLine of lines) {
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
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, `tool<${event.toolName ?? "?"}> \u2192`), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ", truncate2(event.text, 400)));
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 = 220;
3081
+ const max = 260;
2827
3082
  const flat = reasoning.replace(/\s+/g, " ").trim();
2828
- const preview = flat.length <= max ? flat : `${flat.slice(0, max)}\u2026 (+${flat.length - max} chars)`;
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
- 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(Text3, { dimColor: true }, "(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) : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, " (waiting for first token\u2026)"));
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
- return {
3032
- info: `model=${loop.model} harvest=${loop.harvestEnabled ? "on" : "off"} branch=${branchBudget > 1 ? branchBudget : "off"} stream=${loop.stream ? "on" : "off"}`
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 (!key.escape) return;
3173
- if (!busy) return;
3174
- if (abortedThisTurn.current) return;
3175
- abortedThisTurn.current = true;
3176
- loop.abort();
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
- const text = raw.trim();
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, { mcpSpecs });
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
- return;
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: ev.content || streamRef.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, null));
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 CommandStrip() {
3325
- return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /compact \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Esc (while thinking) \u2014 abort & summarize what was found so far"));
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 writeFileSync3 } from "fs";
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 ?? basename(opts.a), parsed: aParsed },
3594
- { label: opts.labelB ?? basename(opts.b), parsed: bParsed }
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
- writeFileSync3(opts.mdPath, md, "utf8");
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 ? ` ${quoteIfNeeded(userArg)}` : "";
5007
+ const tail = userArg ? ` ${quoteIfNeeded2(userArg)}` : "";
4412
5008
  return `${entry.name}=npx -y ${entry.package}${tail}`;
4413
5009
  }
4414
- function quoteIfNeeded(s) {
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 existsSync2, readFileSync as readFileSync5 } from "fs";
5038
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
4443
5039
  function statsCommand(opts) {
4444
- if (!existsSync2(opts.transcript)) {
5040
+ if (!existsSync3(opts.transcript)) {
4445
5041
  console.error(`no such transcript: ${opts.transcript}`);
4446
5042
  process.exit(1);
4447
5043
  }
4448
- const lines = readFileSync5(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
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."