reasonix 0.3.1 → 0.4.1

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