reasonix 0.7.12 → 0.10.0

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
@@ -10,7 +10,7 @@ import {
10
10
  memoryEnabled,
11
11
  readProjectMemory,
12
12
  sanitizeMemoryName
13
- } from "./chunk-5DZMZCCW.js";
13
+ } from "./chunk-WRG56OKI.js";
14
14
 
15
15
  // src/cli/index.ts
16
16
  import { Command } from "commander";
@@ -3174,7 +3174,7 @@ function listFilesWithStatsSync(root, opts = {}) {
3174
3174
  const ignore = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
3175
3175
  const rootAbs = resolve(root);
3176
3176
  const out = [];
3177
- const walk2 = (dirAbs, dirRel) => {
3177
+ const walk3 = (dirAbs, dirRel) => {
3178
3178
  if (out.length >= maxResults) return;
3179
3179
  let entries;
3180
3180
  try {
@@ -3188,7 +3188,7 @@ function listFilesWithStatsSync(root, opts = {}) {
3188
3188
  const relPath = dirRel ? `${dirRel}/${ent.name}` : ent.name;
3189
3189
  if (ent.isDirectory()) {
3190
3190
  if (ent.name.startsWith(".") || ignore.has(ent.name)) continue;
3191
- walk2(join5(dirAbs, ent.name), relPath);
3191
+ walk3(join5(dirAbs, ent.name), relPath);
3192
3192
  } else if (ent.isFile()) {
3193
3193
  let mtimeMs = 0;
3194
3194
  try {
@@ -3199,7 +3199,7 @@ function listFilesWithStatsSync(root, opts = {}) {
3199
3199
  }
3200
3200
  }
3201
3201
  };
3202
- walk2(rootAbs, "");
3202
+ walk3(rootAbs, "");
3203
3203
  return out;
3204
3204
  }
3205
3205
  var AT_PICKER_PREFIX = /(?:^|\s)@([a-zA-Z0-9_./\\-]*)$/;
@@ -3338,6 +3338,108 @@ var defaultFs = {
3338
3338
  },
3339
3339
  read: (p) => readFileSync5(p, "utf8")
3340
3340
  };
3341
+ var AT_URL_PATTERN = /(?<=^|\s)@(https?:\/\/\S+)/g;
3342
+ var DEFAULT_AT_URL_MAX_CHARS = 32e3;
3343
+ async function expandAtUrls(text, opts = {}) {
3344
+ const maxChars = opts.maxChars ?? DEFAULT_AT_URL_MAX_CHARS;
3345
+ const fetcher = opts.fetcher;
3346
+ if (!fetcher) {
3347
+ throw new Error("expandAtUrls: fetcher option is required (wire src/tools/web.ts:webFetch)");
3348
+ }
3349
+ const seen = /* @__PURE__ */ new Map();
3350
+ const bodies = /* @__PURE__ */ new Map();
3351
+ const order = [];
3352
+ for (const match of text.matchAll(AT_URL_PATTERN)) {
3353
+ const rawUrl = match[1] ?? "";
3354
+ const url = stripUrlTail(rawUrl);
3355
+ if (!url) continue;
3356
+ if (seen.has(url)) continue;
3357
+ const cached2 = opts.cache?.get(url);
3358
+ if (cached2) {
3359
+ seen.set(url, cached2);
3360
+ if (cached2.body) bodies.set(url, cached2.body);
3361
+ order.push(url);
3362
+ continue;
3363
+ }
3364
+ let expansion;
3365
+ let body = "";
3366
+ try {
3367
+ const page = await fetcher(url, {
3368
+ maxChars,
3369
+ timeoutMs: opts.timeoutMs,
3370
+ signal: opts.signal
3371
+ });
3372
+ body = page.text;
3373
+ expansion = {
3374
+ token: `@${url}`,
3375
+ url,
3376
+ ok: true,
3377
+ title: page.title,
3378
+ chars: body.length,
3379
+ truncated: page.truncated
3380
+ };
3381
+ } catch (err) {
3382
+ const message = err.message ?? String(err);
3383
+ let skip = "fetch-error";
3384
+ if (/aborted|timeout/i.test(message)) skip = "timeout";
3385
+ else if (/40\d|forbidden|access denied|captcha/i.test(message)) skip = "blocked";
3386
+ expansion = {
3387
+ token: `@${url}`,
3388
+ url,
3389
+ ok: false,
3390
+ skip,
3391
+ error: message
3392
+ };
3393
+ }
3394
+ seen.set(url, expansion);
3395
+ if (body) bodies.set(url, body);
3396
+ if (opts.cache) opts.cache.set(url, { ...expansion, body });
3397
+ order.push(url);
3398
+ }
3399
+ if (seen.size === 0) return { text, expansions: [] };
3400
+ const expansions = order.map((u) => seen.get(u)).filter(Boolean);
3401
+ const blocks = [];
3402
+ for (const ex of expansions) {
3403
+ if (ex.ok) {
3404
+ const titleAttr = ex.title ? ` title="${escapeAttr(ex.title)}"` : "";
3405
+ const truncTag = ex.truncated ? ' truncated="true"' : "";
3406
+ const body = bodies.get(ex.url) ?? "";
3407
+ blocks.push(`<url href="${ex.url}"${titleAttr}${truncTag}>
3408
+ ${body}
3409
+ </url>`);
3410
+ } else {
3411
+ const reasonAttr = ex.skip ?? "fetch-error";
3412
+ blocks.push(`<url href="${ex.url}" skipped="${reasonAttr}" />`);
3413
+ }
3414
+ }
3415
+ const augmented = `${text}
3416
+
3417
+ [Referenced URLs]
3418
+ ${blocks.join("\n\n")}`;
3419
+ return { text: augmented, expansions };
3420
+ }
3421
+ function stripUrlTail(raw) {
3422
+ let s = raw;
3423
+ while (s.length > 0) {
3424
+ const last = s[s.length - 1];
3425
+ if (".,;:!?".includes(last)) {
3426
+ s = s.slice(0, -1);
3427
+ continue;
3428
+ }
3429
+ if (")]}>".includes(last)) {
3430
+ const open = { ")": "(", "]": "[", "}": "{", ">": "<" }[last];
3431
+ if (!s.includes(open)) {
3432
+ s = s.slice(0, -1);
3433
+ continue;
3434
+ }
3435
+ }
3436
+ break;
3437
+ }
3438
+ return s;
3439
+ }
3440
+ function escapeAttr(s) {
3441
+ return s.replace(/"/g, "&quot;").replace(/[\r\n]+/g, " ").trim();
3442
+ }
3341
3443
 
3342
3444
  // src/tools/filesystem.ts
3343
3445
  import { promises as fs } from "fs";
@@ -3564,7 +3666,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3564
3666
  let totalBytes = 0;
3565
3667
  let truncated = false;
3566
3668
  const PER_DIR_CHILD_CAP = 50;
3567
- const walk2 = async (dir, depth) => {
3669
+ const walk3 = async (dir, depth) => {
3568
3670
  if (truncated) return;
3569
3671
  if (depth > maxDepth) return;
3570
3672
  let entries;
@@ -3604,11 +3706,11 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3604
3706
  lines.push(line);
3605
3707
  emitted++;
3606
3708
  if (e.isDirectory() && !skip) {
3607
- await walk2(pathMod.join(dir, e.name), depth + 1);
3709
+ await walk3(pathMod.join(dir, e.name), depth + 1);
3608
3710
  }
3609
3711
  }
3610
3712
  };
3611
- await walk2(startAbs, 0);
3713
+ await walk3(startAbs, 0);
3612
3714
  return lines.join("\n") || "(empty tree)";
3613
3715
  }
3614
3716
  });
@@ -3638,7 +3740,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3638
3740
  }
3639
3741
  const matches = [];
3640
3742
  let totalBytes = 0;
3641
- const walk2 = async (dir) => {
3743
+ const walk3 = async (dir) => {
3642
3744
  let entries;
3643
3745
  try {
3644
3746
  entries = await fs.readdir(dir, { withFileTypes: true });
@@ -3658,10 +3760,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3658
3760
  matches.push(rel);
3659
3761
  totalBytes += rel.length + 1;
3660
3762
  }
3661
- if (e.isDirectory()) await walk2(full);
3763
+ if (e.isDirectory()) await walk3(full);
3662
3764
  }
3663
3765
  };
3664
- await walk2(startAbs);
3766
+ await walk3(startAbs);
3665
3767
  return matches.length === 0 ? "(no matches)" : matches.join("\n");
3666
3768
  }
3667
3769
  });
@@ -3711,7 +3813,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3711
3813
  let totalBytes = 0;
3712
3814
  let scanned = 0;
3713
3815
  let truncated = false;
3714
- const walk2 = async (dir) => {
3816
+ const walk3 = async (dir) => {
3715
3817
  if (truncated) return;
3716
3818
  let entries;
3717
3819
  try {
@@ -3723,7 +3825,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3723
3825
  if (truncated) return;
3724
3826
  if (e.isDirectory()) {
3725
3827
  if (!includeDeps && SKIP_DIR_NAMES.has(e.name)) continue;
3726
- await walk2(pathMod.join(dir, e.name));
3828
+ await walk3(pathMod.join(dir, e.name));
3727
3829
  continue;
3728
3830
  }
3729
3831
  if (!e.isFile()) continue;
@@ -3766,7 +3868,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3766
3868
  scanned++;
3767
3869
  }
3768
3870
  };
3769
- await walk2(startAbs);
3871
+ await walk3(startAbs);
3770
3872
  if (matches.length === 0) {
3771
3873
  return scanned === 0 ? "(no files scanned \u2014 path empty or all files filtered out)" : `(no matches across ${scanned} file${scanned === 1 ? "" : "s"})`;
3772
3874
  }
@@ -4531,9 +4633,9 @@ async function spawnSubagent(opts) {
4531
4633
  usage
4532
4634
  };
4533
4635
  }
4534
- function aggregateChildUsage(loop) {
4636
+ function aggregateChildUsage(loop2) {
4535
4637
  const agg = new Usage();
4536
- for (const t of loop.stats.turns) {
4638
+ for (const t of loop2.stats.turns) {
4537
4639
  agg.promptTokens += t.usage.promptTokens;
4538
4640
  agg.completionTokens += t.usage.completionTokens;
4539
4641
  agg.totalTokens += t.usage.totalTokens;
@@ -5204,7 +5306,7 @@ function registerShellTools(registry, opts) {
5204
5306
  const snapshot2 = opts.extraAllowed ?? [];
5205
5307
  return () => snapshot2;
5206
5308
  })();
5207
- const allowAll = opts.allowAll ?? false;
5309
+ const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
5208
5310
  registry.register({
5209
5311
  name: "run_command",
5210
5312
  description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 ONE process per call, NO shell expansion. `&&`, `||`, `|`, `;`, `>`, `<`, `2>&1` are all rejected up-front \u2014 split into separate calls and combine results in reasoning. Example: instead of `grep foo *.ts | wc -l`, use `grep -c foo *.ts`; instead of `cd sub && npm test`, use `npm test --prefix sub` (or whatever --cwd flag the binary accepts).\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project. If a tool needs a subdirectory, pass it via the tool's own flag (`npm --prefix`, `cargo -C`, `git -C`, `pytest tests/\u2026`), NOT via a preceding `cd`.\n\u2022 Avoid commands with unbounded output (`netstat -ano`, `find /`, etc.) \u2014 they waste tokens. Filter at source: `netstat -ano -p TCP`, `find src -name '*.ts'`, `grep -c`, `wc -l`.\n\nCommon read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
@@ -5213,7 +5315,7 @@ function registerShellTools(registry, opts) {
5213
5315
  // during planning. Anything that would otherwise trigger a
5214
5316
  // confirmation prompt is treated as "not read-only" and bounced.
5215
5317
  readOnlyCheck: (args) => {
5216
- if (allowAll) return true;
5318
+ if (isAllowAll()) return true;
5217
5319
  const cmd = typeof args?.command === "string" ? args.command.trim() : "";
5218
5320
  if (!cmd) return false;
5219
5321
  return isAllowed(cmd, getExtraAllowed());
@@ -5235,7 +5337,7 @@ function registerShellTools(registry, opts) {
5235
5337
  fn: async (args, ctx) => {
5236
5338
  const cmd = args.command.trim();
5237
5339
  if (!cmd) throw new Error("run_command: empty command");
5238
- if (!allowAll && !isAllowed(cmd, getExtraAllowed())) {
5340
+ if (!isAllowAll() && !isAllowed(cmd, getExtraAllowed())) {
5239
5341
  throw new NeedsConfirmationError(cmd);
5240
5342
  }
5241
5343
  const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
@@ -5268,7 +5370,7 @@ function registerShellTools(registry, opts) {
5268
5370
  fn: async (args, ctx) => {
5269
5371
  const cmd = args.command.trim();
5270
5372
  if (!cmd) throw new Error("run_background: empty command");
5271
- if (!allowAll && !isAllowed(cmd, getExtraAllowed())) {
5373
+ if (!isAllowAll() && !isAllowed(cmd, getExtraAllowed())) {
5272
5374
  throw new NeedsConfirmationError(cmd);
5273
5375
  }
5274
5376
  const result = await jobs2.start(cmd, {
@@ -7111,12 +7213,12 @@ function formatLogSize(path = defaultUsageLogPath()) {
7111
7213
  }
7112
7214
 
7113
7215
  // src/cli/commands/chat.tsx
7114
- import { existsSync as existsSync12, statSync as statSync7 } from "fs";
7216
+ import { existsSync as existsSync13, statSync as statSync7 } from "fs";
7115
7217
  import { render } from "ink";
7116
7218
  import React26, { useState as useState12 } from "react";
7117
7219
 
7118
7220
  // src/cli/ui/App.tsx
7119
- import { Box as Box21, Static, useApp, useStdout as useStdout8 } from "ink";
7221
+ import { Box as Box21, Static, Text as Text19, useApp, useStdout as useStdout8 } from "ink";
7120
7222
  import React23, { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState10 } from "react";
7121
7223
 
7122
7224
  // src/code/pending-edits.ts
@@ -7991,7 +8093,8 @@ function formatAllBlockDiffs(blocks, opts = {}) {
7991
8093
  const added = countLines2(b.replace);
7992
8094
  const tag = b.search === "" ? "NEW " : " ";
7993
8095
  if (i > 0) out.push("");
7994
- out.push(` ${tag}${b.path} (-${removed} +${added} lines)`);
8096
+ const label = opts.numbered ? `[${i + 1}] ` : "";
8097
+ out.push(` ${label}${tag}${b.path} (-${removed} +${added} lines)`);
7995
8098
  out.push(...formatEditBlockDiff(b, opts));
7996
8099
  }
7997
8100
  return out;
@@ -9466,10 +9569,9 @@ function ModeStatusBar({
9466
9569
  if (planMode) {
9467
9570
  return /* @__PURE__ */ React12.createElement(ModeBarFrame, null, /* @__PURE__ */ React12.createElement(ModePill, { label: "PLAN MODE", bg: "red", flash }), /* @__PURE__ */ React12.createElement(Text9, { dimColor: true }, " writes gated \xB7 /plan off to leave"), jobsTag);
9468
9571
  }
9469
- const isAuto = editMode === "auto";
9470
- const label = isAuto ? "AUTO" : "REVIEW";
9471
- const bg = isAuto ? "magenta" : "cyan";
9472
- const mid = isAuto ? "edits land now \xB7 u to undo" : pendingCount > 0 ? `${pendingCount} queued \xB7 y apply \xB7 n discard` : "edits queued \xB7 y apply \xB7 n discard";
9572
+ const label = editMode === "yolo" ? "YOLO" : editMode === "auto" ? "AUTO" : "REVIEW";
9573
+ const bg = editMode === "yolo" ? "red" : editMode === "auto" ? "magenta" : "cyan";
9574
+ const mid = editMode === "yolo" ? "edits + shell auto \xB7 /undo to roll back" : editMode === "auto" ? "edits land now \xB7 u to undo" : pendingCount > 0 ? `${pendingCount} queued \xB7 y apply \xB7 n discard` : "edits queued \xB7 y apply \xB7 n discard";
9473
9575
  return /* @__PURE__ */ React12.createElement(ModeBarFrame, null, /* @__PURE__ */ React12.createElement(ModePill, { label, bg, flash }), /* @__PURE__ */ React12.createElement(Text9, { dimColor: true }, ` ${mid} \xB7 Shift+Tab to flip`), jobsTag);
9474
9576
  }
9475
9577
  function ModeBarFrame({ children }) {
@@ -10866,10 +10968,50 @@ function formatEditResults(results) {
10866
10968
  return [header2, ...lines].join("\n");
10867
10969
  }
10868
10970
  function formatPendingPreview(blocks) {
10869
- const header2 = `\u25B8 ${blocks.length} pending edit block(s) \u2014 /apply (or y) to commit \xB7 /discard (or n) to drop`;
10870
- const diffLines = formatAllBlockDiffs(blocks);
10971
+ const partial = blocks.length > 1 ? " \xB7 /apply N or 1,3-4 for partial" : "";
10972
+ const header2 = `\u25B8 ${blocks.length} pending edit block(s) \u2014 /apply (or y) to commit \xB7 /discard (or n) to drop${partial}`;
10973
+ const diffLines = formatAllBlockDiffs(blocks, { numbered: blocks.length > 1 });
10871
10974
  return [header2, ...diffLines].join("\n");
10872
10975
  }
10976
+ function parseEditIndices(raw, max) {
10977
+ const trimmed = raw.trim();
10978
+ if (!trimmed) return { ok: [] };
10979
+ if (max <= 0) return { error: "no pending edits to address" };
10980
+ const seen = /* @__PURE__ */ new Set();
10981
+ const tokens = trimmed.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
10982
+ if (tokens.length === 0) return { ok: [] };
10983
+ for (const tok of tokens) {
10984
+ const range = tok.match(/^(\d+)-(\d+)$/);
10985
+ if (range) {
10986
+ const a = Number.parseInt(range[1] ?? "", 10);
10987
+ const b = Number.parseInt(range[2] ?? "", 10);
10988
+ if (!Number.isFinite(a) || !Number.isFinite(b) || a < 1 || b < 1) {
10989
+ return { error: `invalid range: "${tok}"` };
10990
+ }
10991
+ const lo = Math.min(a, b);
10992
+ const hi = Math.max(a, b);
10993
+ if (hi > max) return { error: `index ${hi} out of range (max ${max})` };
10994
+ for (let i = lo; i <= hi; i++) seen.add(i);
10995
+ continue;
10996
+ }
10997
+ if (!/^\d+$/.test(tok)) return { error: `invalid index: "${tok}"` };
10998
+ const n = Number.parseInt(tok, 10);
10999
+ if (!Number.isFinite(n) || n < 1) return { error: `invalid index: "${tok}"` };
11000
+ if (n > max) return { error: `index ${n} out of range (max ${max})` };
11001
+ seen.add(n);
11002
+ }
11003
+ return { ok: [...seen].sort((a, b) => a - b) };
11004
+ }
11005
+ function partitionEdits(edits, indices1Based) {
11006
+ const picked = new Set(indices1Based);
11007
+ const selected = [];
11008
+ const remaining = [];
11009
+ for (let i = 0; i < edits.length; i++) {
11010
+ if (picked.has(i + 1)) selected.push(edits[i]);
11011
+ else remaining.push(edits[i]);
11012
+ }
11013
+ return { selected, remaining };
11014
+ }
10873
11015
  function formatUndoRows(results) {
10874
11016
  return results.map((r) => {
10875
11017
  const mark = r.status === "applied" ? "\u2713" : "\u2717";
@@ -10885,6 +11027,130 @@ function describeRepair(repair) {
10885
11027
  return parts.length ? `[repair] ${parts.join(", ")}` : "";
10886
11028
  }
10887
11029
 
11030
+ // src/cli/ui/hash-memory.ts
11031
+ import { appendFileSync as appendFileSync3, existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
11032
+ import { homedir as homedir6 } from "os";
11033
+ import { dirname as dirname10, join as join12 } from "path";
11034
+ var PROJECT_HEADER = `# Reasonix project memory
11035
+
11036
+ Notes the user pinned via the \`#\` prompt prefix. The whole file is
11037
+ loaded into the immutable system prefix every session \u2014 keep it terse.
11038
+
11039
+ `;
11040
+ var GLOBAL_HEADER = `# Reasonix global memory
11041
+
11042
+ Cross-project notes the user pinned via the \`#g\` prompt prefix. Loaded
11043
+ into every Reasonix session's prefix regardless of working directory.
11044
+ Private to this machine \u2014 not committed anywhere.
11045
+
11046
+ `;
11047
+ function detectHashMemory(text) {
11048
+ if (text.startsWith("\\#")) {
11049
+ return { kind: "escape", text: text.slice(1) };
11050
+ }
11051
+ if (!text.startsWith("#")) return null;
11052
+ if (text.startsWith("##")) return null;
11053
+ if (/^#g\s*$/.test(text)) return null;
11054
+ const globalMatch = /^#g\s+(.+)$/s.exec(text);
11055
+ if (globalMatch) {
11056
+ const body2 = globalMatch[1].trim();
11057
+ if (!body2) return null;
11058
+ return { kind: "memory-global", note: body2 };
11059
+ }
11060
+ const body = text.slice(1).trim();
11061
+ if (!body) return null;
11062
+ return { kind: "memory", note: body };
11063
+ }
11064
+ function appendProjectMemory(rootDir, note) {
11065
+ return appendBulletToFile(join12(rootDir, PROJECT_MEMORY_FILE), note, PROJECT_HEADER);
11066
+ }
11067
+ var GLOBAL_MEMORY_DIR = ".reasonix";
11068
+ var GLOBAL_MEMORY_FILE = "REASONIX.md";
11069
+ function globalMemoryPath(homeDir = homedir6()) {
11070
+ return join12(homeDir, GLOBAL_MEMORY_DIR, GLOBAL_MEMORY_FILE);
11071
+ }
11072
+ function appendGlobalMemory(note, homeDir) {
11073
+ return appendBulletToFile(globalMemoryPath(homeDir), note, GLOBAL_HEADER);
11074
+ }
11075
+ function appendBulletToFile(path, note, newFileHeader) {
11076
+ const trimmed = note.trim();
11077
+ if (!trimmed) throw new Error("note body cannot be empty");
11078
+ const bullet = `- ${trimmed}
11079
+ `;
11080
+ if (!existsSync11(path)) {
11081
+ mkdirSync8(dirname10(path), { recursive: true });
11082
+ writeFileSync7(path, `${newFileHeader}${bullet}`, "utf8");
11083
+ return { path, created: true };
11084
+ }
11085
+ let prefix = "";
11086
+ try {
11087
+ const existing = readFileSync14(path, "utf8");
11088
+ if (existing.length > 0 && !existing.endsWith("\n")) prefix = "\n";
11089
+ } catch {
11090
+ }
11091
+ appendFileSync3(path, `${prefix}${bullet}`, "utf8");
11092
+ return { path, created: false };
11093
+ }
11094
+
11095
+ // src/cli/ui/loop.ts
11096
+ var MIN_LOOP_INTERVAL_MS = 5e3;
11097
+ var MAX_LOOP_INTERVAL_MS = 6 * 60 * 6e4;
11098
+ function parseLoopInterval(raw) {
11099
+ const s = raw.trim().toLowerCase();
11100
+ if (!s) return null;
11101
+ const m = /^([0-9]+(?:\.[0-9]+)?)(s|sec|secs|m|min|mins|h|hr|hrs)?$/.exec(s);
11102
+ if (!m) return null;
11103
+ const n = Number.parseFloat(m[1] ?? "");
11104
+ if (!Number.isFinite(n) || n <= 0) return null;
11105
+ const unit = m[2] ?? "s";
11106
+ let ms;
11107
+ if (unit === "s" || unit === "sec" || unit === "secs") ms = Math.round(n * 1e3);
11108
+ else if (unit === "m" || unit === "min" || unit === "mins") ms = Math.round(n * 6e4);
11109
+ else if (unit === "h" || unit === "hr" || unit === "hrs") ms = Math.round(n * 60 * 6e4);
11110
+ else return null;
11111
+ if (ms < MIN_LOOP_INTERVAL_MS) return null;
11112
+ if (ms > MAX_LOOP_INTERVAL_MS) return null;
11113
+ return { ms };
11114
+ }
11115
+ function parseLoopCommand(args) {
11116
+ if (args.length === 0) return { kind: "status" };
11117
+ const first = (args[0] ?? "").toLowerCase();
11118
+ if (args.length === 1 && (first === "stop" || first === "off" || first === "cancel")) {
11119
+ return { kind: "stop" };
11120
+ }
11121
+ const interval = parseLoopInterval(args[0] ?? "");
11122
+ if (!interval) {
11123
+ return {
11124
+ kind: "error",
11125
+ message: "usage: /loop <interval> <prompt> (interval = 5s..6h, e.g. 30s, 5m, 1h)\n /loop stop (cancel an active loop)\n /loop (show active-loop status)"
11126
+ };
11127
+ }
11128
+ const prompt = args.slice(1).join(" ").trim();
11129
+ if (!prompt) {
11130
+ return {
11131
+ kind: "error",
11132
+ message: `usage: /loop ${args[0]} <prompt> \u2014 interval is fine but the prompt is missing.`
11133
+ };
11134
+ }
11135
+ return { kind: "start", intervalMs: interval.ms, prompt };
11136
+ }
11137
+ function formatLoopStatus(prompt, nextFireMs, iter) {
11138
+ const preview = prompt.length > 36 ? `${prompt.slice(0, 33)}\u2026` : prompt;
11139
+ const when = nextFireMs <= 0 ? "firing now" : `next in ${formatDuration2(nextFireMs)}`;
11140
+ return `loop: \`${preview}\` \xB7 ${when} \xB7 iter ${iter}`;
11141
+ }
11142
+ function formatDuration2(ms) {
11143
+ if (ms < 1e3) return `${ms}ms`;
11144
+ const totalSec = Math.round(ms / 1e3);
11145
+ if (totalSec < 60) return `${totalSec}s`;
11146
+ const m = Math.floor(totalSec / 60);
11147
+ const s = totalSec % 60;
11148
+ if (m < 60) return s === 0 ? `${m}m` : `${m}m${s}s`;
11149
+ const h = Math.floor(m / 60);
11150
+ const mm = m % 60;
11151
+ return mm === 0 ? `${h}h` : `${h}h${mm}m`;
11152
+ }
11153
+
10888
11154
  // src/cli/ui/mcp-browse.ts
10889
11155
  function formatResourceList(servers) {
10890
11156
  const lines = [];
@@ -11190,10 +11456,30 @@ var SLASH_COMMANDS = [
11190
11456
  { cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
11191
11457
  { cmd: "clear", summary: "clear visible scrollback only (log/context kept)" },
11192
11458
  { cmd: "new", summary: "start a fresh conversation (clear context + scrollback)" },
11459
+ {
11460
+ cmd: "loop",
11461
+ argsHint: "<5s..6h> <prompt> \xB7 stop \xB7 (no args = status)",
11462
+ summary: "auto-resubmit <prompt> every <interval> until you type something / Esc / /loop stop"
11463
+ },
11193
11464
  { cmd: "exit", summary: "quit the TUI" },
11194
11465
  // Code-mode only
11195
- { cmd: "apply", summary: "commit pending edit blocks to disk", contextual: "code" },
11196
- { cmd: "discard", summary: "drop pending edit blocks without writing", contextual: "code" },
11466
+ {
11467
+ cmd: "apply",
11468
+ argsHint: "[N|N,M|N-M]",
11469
+ summary: "commit pending edit blocks to disk (no arg \u2192 all; `1`, `1,3`, or `1-4` \u2192 that subset, rest stay pending)",
11470
+ contextual: "code"
11471
+ },
11472
+ {
11473
+ cmd: "discard",
11474
+ argsHint: "[N|N,M|N-M]",
11475
+ summary: "drop pending edit blocks without writing (no arg \u2192 all; indices \u2192 that subset)",
11476
+ contextual: "code"
11477
+ },
11478
+ {
11479
+ cmd: "walk",
11480
+ summary: "step through pending edits one block at a time (git-add-p style: y/n per block, a apply rest, A flip AUTO)",
11481
+ contextual: "code"
11482
+ },
11197
11483
  { cmd: "undo", summary: "roll back the last applied edit batch", contextual: "code" },
11198
11484
  {
11199
11485
  cmd: "history",
@@ -11226,10 +11512,10 @@ var SLASH_COMMANDS = [
11226
11512
  },
11227
11513
  {
11228
11514
  cmd: "mode",
11229
- argsHint: "[review|auto]",
11230
- summary: "edit-gate: review (queue for /apply) or auto (apply+undo banner). Shift+Tab cycles.",
11515
+ argsHint: "[review|auto|yolo]",
11516
+ summary: "edit-gate: review (queue) \xB7 auto (apply+undo) \xB7 yolo (apply+auto-shell). Shift+Tab cycles.",
11231
11517
  contextual: "code",
11232
- argCompleter: ["review", "auto"]
11518
+ argCompleter: ["review", "auto", "yolo"]
11233
11519
  },
11234
11520
  { cmd: "jobs", summary: "list background jobs started by run_background", contextual: "code" },
11235
11521
  {
@@ -11282,7 +11568,7 @@ function parseSlash(text) {
11282
11568
  }
11283
11569
 
11284
11570
  // src/cli/commands/stats.ts
11285
- import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
11571
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
11286
11572
  function statsCommand(opts) {
11287
11573
  if (opts.transcript) {
11288
11574
  transcriptSummary(opts.transcript);
@@ -11291,11 +11577,11 @@ function statsCommand(opts) {
11291
11577
  dashboard(opts);
11292
11578
  }
11293
11579
  function transcriptSummary(path) {
11294
- if (!existsSync11(path)) {
11580
+ if (!existsSync12(path)) {
11295
11581
  console.error(`no such transcript: ${path}`);
11296
11582
  process.exit(1);
11297
11583
  }
11298
- const lines = readFileSync14(path, "utf8").split(/\r?\n/).filter(Boolean);
11584
+ const lines = readFileSync15(path, "utf8").split(/\r?\n/).filter(Boolean);
11299
11585
  let assistantTurns = 0;
11300
11586
  let toolCalls = 0;
11301
11587
  let lastTurn = 0;
@@ -11410,7 +11696,7 @@ function pad(s, width, align = "left") {
11410
11696
  }
11411
11697
 
11412
11698
  // src/cli/ui/slash/handlers/admin.ts
11413
- var hooks = (args, loop, ctx) => {
11699
+ var hooks = (args, loop2, ctx) => {
11414
11700
  const sub = (args[0] ?? "").toLowerCase();
11415
11701
  if (sub === "reload") {
11416
11702
  if (!ctx.reloadHooks) {
@@ -11426,7 +11712,7 @@ var hooks = (args, loop, ctx) => {
11426
11712
  info: "usage: /hooks list active hooks\n /hooks reload re-read settings.json files"
11427
11713
  };
11428
11714
  }
11429
- const all = loop.hooks;
11715
+ const all = loop2.hooks;
11430
11716
  const projPath = ctx.codeRoot ? projectSettingsPath(ctx.codeRoot) : void 0;
11431
11717
  const globPath = globalSettingsPath();
11432
11718
  if (all.length === 0) {
@@ -11528,8 +11814,8 @@ var clear = () => ({
11528
11814
  clear: true,
11529
11815
  info: "\u25B8 terminal cleared (viewport + scrollback). Context (message log) is intact \u2014 next turn still sees everything. Use /new to start fresh, or /forget to delete the session entirely."
11530
11816
  });
11531
- var resetLog = (_args, loop) => {
11532
- const { dropped } = loop.clearLog();
11817
+ var resetLog = (_args, loop2) => {
11818
+ const { dropped } = loop2.clearLog();
11533
11819
  return {
11534
11820
  clear: true,
11535
11821
  info: `\u25B8 new conversation \u2014 dropped ${dropped} message(s) from context. Same session, fresh slate.`
@@ -11557,8 +11843,14 @@ var keys = () => ({
11557
11843
  " /<name> slash command; Tab/Enter picks from the suggestion list",
11558
11844
  " @<path> inline a file under [Referenced files] (code mode).",
11559
11845
  " Trailing `@\u2026` opens a file picker; \u2191/\u2193 navigate, Tab/Enter pick.",
11846
+ " @https://... fetch the URL, strip HTML, inline under [Referenced URLs].",
11847
+ " Cached per session \u2014 same URL twice fetches once.",
11560
11848
  " !<cmd> run <cmd> as shell in the sandbox root; output goes into context",
11561
11849
  " so the model sees it next turn. No allowlist gate.",
11850
+ " #<note> append <note> to <project>/REASONIX.md (committable, team-shared).",
11851
+ " #g <note> append <note> to ~/.reasonix/REASONIX.md (global, never committed).",
11852
+ " Both pin into the immutable prefix every future session.",
11853
+ " Use `\\#literal` if you actually want a `#` heading sent to the model.",
11562
11854
  "",
11563
11855
  "Pickers (slash + @-mention):",
11564
11856
  " \u2191 / \u2193 navigate the suggestion list",
@@ -11597,15 +11889,16 @@ var help = () => ({
11597
11889
  " /skill [sub] list / run user skills (project/.reasonix/skills + ~/.reasonix/skills).",
11598
11890
  " subs: list | show <name> | <name> [args] (injects skill body as user turn)",
11599
11891
  " /retry truncate & resend your last message (fresh sample from the model)",
11600
- " /apply (code mode) commit the pending edit blocks to disk",
11601
- " /discard (code mode) drop pending edits without writing",
11892
+ " /apply [N|1,3|1-4] (code mode) commit pending edit blocks (no arg \u2192 all; index \u2192 subset)",
11893
+ " /discard [N|1,3|1-4] (code mode) drop pending edits (no arg \u2192 all; index \u2192 subset)",
11894
+ " /walk (code mode) step through pending edits one block at a time (y/n per block, a apply rest, A flip AUTO)",
11602
11895
  " /undo (code mode) roll back the latest non-undone edit batch",
11603
11896
  " /history (code mode) list every edit batch this session",
11604
11897
  " /show [id] (code mode) dump a stored edit diff (newest when id omitted)",
11605
11898
  ' /commit "msg" (code mode) git add -A && git commit -m "msg"',
11606
11899
  " /plan [on|off] (code mode) toggle read-only plan mode; writes gated behind submit_plan + your approval",
11607
11900
  " /apply-plan (code mode) force-approve pending/in-text plan (fallback)",
11608
- " /mode [review|auto] (code mode) edit-gate: queue edits for /apply or apply instantly (Shift+Tab cycles, u undoes within 5s)",
11901
+ " /mode [review|auto|yolo] (code mode) review = queue \xB7 auto = apply+undo banner \xB7 yolo = apply+auto-shell. Shift+Tab cycles all three.",
11609
11902
  " /jobs (code mode) list background processes (run_background) \u2014 running and exited",
11610
11903
  " /kill <id> (code mode) stop a background job by id (SIGTERM \u2192 SIGKILL)",
11611
11904
  " /logs <id> [lines] (code mode) tail a background job's output (default 80 lines)",
@@ -11613,6 +11906,7 @@ var help = () => ({
11613
11906
  " /forget delete the current session from disk",
11614
11907
  " /new start fresh: drop all context + clear scrollback",
11615
11908
  " /clear clear displayed scrollback only (context kept \u2014 model still sees it)",
11909
+ " /loop <interval> <prompt> auto-resubmit <prompt> every <interval> (5s..6h). /loop stop \xB7 type anything to cancel.",
11616
11910
  " /exit quit",
11617
11911
  "",
11618
11912
  "Shell shortcut:",
@@ -11621,10 +11915,23 @@ var help = () => ({
11621
11915
  " No allowlist gate \u2014 user-typed = explicit consent.",
11622
11916
  " Example: !git status !ls src/ !npm test",
11623
11917
  "",
11918
+ "Quick memory:",
11919
+ " #<note> append <note> to <project>/REASONIX.md (committable).",
11920
+ " Example: #findByEmail must be case-insensitive",
11921
+ " #g <note> append <note> to ~/.reasonix/REASONIX.md (global, never committed).",
11922
+ " Example: #g always run pnpm not npm",
11923
+ " Both pin into every future session's prefix. Faster than /memory.",
11924
+ " Use `\\#text` to send a literal `#text` to the model.",
11925
+ "",
11624
11926
  "File references (code mode):",
11625
11927
  " @path/to/file inline file content under [Referenced files] on send.",
11626
11928
  " Type `@` to open the picker (\u2191\u2193 navigate, Tab/Enter pick).",
11627
11929
  "",
11930
+ "URL references:",
11931
+ " @https://example.com fetch the URL, strip HTML, inline under [Referenced URLs].",
11932
+ " Same URL twice in one session fetches once (in-mem cache).",
11933
+ " Trailing sentence punctuation (./,/)) is stripped automatically.",
11934
+ "",
11628
11935
  "Presets (branch + harvest are NEVER auto-enabled \u2014 opt-in only):",
11629
11936
  " fast v4-flash \xB7 effort=high cheapest \xB7 quick Q&A, one-line edits",
11630
11937
  " smart v4-flash \xB7 effort=max \u2190 default \xB7 day-to-day coding",
@@ -11644,8 +11951,8 @@ var help = () => ({
11644
11951
  var setup = () => ({
11645
11952
  info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
11646
11953
  });
11647
- var retry = (_args, loop) => {
11648
- const prev = loop.retryLastUser();
11954
+ var retry = (_args, loop2) => {
11955
+ const prev = loop2.retryLastUser();
11649
11956
  if (!prev) {
11650
11957
  return {
11651
11958
  info: "nothing to retry \u2014 no prior user message in this session's log."
@@ -11657,6 +11964,37 @@ var retry = (_args, loop) => {
11657
11964
  resubmit: prev
11658
11965
  };
11659
11966
  };
11967
+ var loop = (args, _loop, ctx) => {
11968
+ if (!ctx.startLoop || !ctx.stopLoop || !ctx.getLoopStatus) {
11969
+ return {
11970
+ info: "/loop is only available in the interactive TUI (not in run/replay)."
11971
+ };
11972
+ }
11973
+ const cmd = parseLoopCommand(args);
11974
+ if (cmd.kind === "error") return { info: cmd.message };
11975
+ if (cmd.kind === "stop") {
11976
+ const wasActive = ctx.getLoopStatus() !== null;
11977
+ ctx.stopLoop();
11978
+ return {
11979
+ info: wasActive ? "\u25B8 loop stopped." : "no active loop to stop."
11980
+ };
11981
+ }
11982
+ if (cmd.kind === "status") {
11983
+ const status2 = ctx.getLoopStatus();
11984
+ if (!status2) {
11985
+ return {
11986
+ info: "no active loop. Start one with `/loop <interval> <prompt>` (e.g. /loop 30s npm test).\nCancels on: /loop stop \xB7 Esc \xB7 /clear \xB7 /new \xB7 any user-typed prompt."
11987
+ };
11988
+ }
11989
+ return { info: `\u25B8 ${formatLoopStatus(status2.prompt, status2.nextFireMs, status2.iter)}` };
11990
+ }
11991
+ ctx.startLoop(cmd.intervalMs, cmd.prompt);
11992
+ return {
11993
+ info: `\u25B8 loop started \u2014 re-submitting "${cmd.prompt}" every ${formatDuration2(
11994
+ cmd.intervalMs
11995
+ )}. Type anything (or /loop stop) to cancel.`
11996
+ };
11997
+ };
11660
11998
  var handlers2 = {
11661
11999
  exit,
11662
12000
  quit: exit,
@@ -11667,7 +12005,8 @@ var handlers2 = {
11667
12005
  help,
11668
12006
  "?": help,
11669
12007
  setup,
11670
- retry
12008
+ retry,
12009
+ loop
11671
12010
  };
11672
12011
 
11673
12012
  // src/cli/ui/slash/helpers.ts
@@ -11790,22 +12129,33 @@ var show = (args, _loop, ctx) => {
11790
12129
  }
11791
12130
  return { info: ctx.codeShowEdit(args) };
11792
12131
  };
11793
- var apply = (_args, _loop, ctx) => {
12132
+ var apply = (args, _loop, ctx) => {
11794
12133
  if (!ctx.codeApply) {
11795
12134
  return {
11796
12135
  info: "/apply is only available inside `reasonix code` (nothing to apply here)."
11797
12136
  };
11798
12137
  }
11799
- return { info: ctx.codeApply() };
12138
+ const parsed = parseIndicesArg(args, ctx.pendingEditCount ?? 0);
12139
+ if ("error" in parsed) return { info: `/apply: ${parsed.error}` };
12140
+ return { info: ctx.codeApply(parsed.indices) };
11800
12141
  };
11801
- var discard = (_args, _loop, ctx) => {
12142
+ var discard = (args, _loop, ctx) => {
11802
12143
  if (!ctx.codeDiscard) {
11803
12144
  return {
11804
12145
  info: "/discard is only available inside `reasonix code`."
11805
12146
  };
11806
12147
  }
11807
- return { info: ctx.codeDiscard() };
12148
+ const parsed = parseIndicesArg(args, ctx.pendingEditCount ?? 0);
12149
+ if ("error" in parsed) return { info: `/discard: ${parsed.error}` };
12150
+ return { info: ctx.codeDiscard(parsed.indices) };
11808
12151
  };
12152
+ function parseIndicesArg(args, max) {
12153
+ const raw = args.join(",").replace(/,+/g, ",").replace(/^,|,$/g, "");
12154
+ if (!raw) return { indices: [] };
12155
+ const parsed = parseEditIndices(raw, max);
12156
+ if ("error" in parsed) return { error: parsed.error };
12157
+ return { indices: parsed.ok };
12158
+ }
11809
12159
  var plan = (args, _loop, ctx) => {
11810
12160
  if (!ctx.setPlanMode) {
11811
12161
  return {
@@ -11852,15 +12202,17 @@ var mode = (args, _loop, ctx) => {
11852
12202
  let target;
11853
12203
  if (raw === "review") target = "review";
11854
12204
  else if (raw === "auto") target = "auto";
12205
+ else if (raw === "yolo") target = "yolo";
11855
12206
  else if (raw === "") {
11856
- target = current === "auto" ? "review" : "auto";
12207
+ target = current === "review" ? "auto" : current === "auto" ? "yolo" : "review";
11857
12208
  } else {
11858
- return { info: "usage: /mode <review|auto> (Shift+Tab also cycles)" };
12209
+ return {
12210
+ info: "usage: /mode <review|auto|yolo> (Shift+Tab also cycles)"
12211
+ };
11859
12212
  }
11860
12213
  ctx.setEditMode(target);
11861
- return {
11862
- info: target === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo, or /undo later" : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)"
11863
- };
12214
+ const banner = target === "yolo" ? "\u25B8 edit mode: YOLO \u2014 edits AND shell commands auto-run with no prompt. /undo still rolls back edits. Use carefully." : target === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo, or /undo later. Shell commands still ask." : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)";
12215
+ return { info: banner };
11864
12216
  };
11865
12217
  var commit = (args, _loop, ctx) => {
11866
12218
  if (!ctx.codeRoot) {
@@ -11877,6 +12229,14 @@ var commit = (args, _loop, ctx) => {
11877
12229
  }
11878
12230
  return runGitCommit(ctx.codeRoot, message);
11879
12231
  };
12232
+ var walk2 = (_args, _loop, ctx) => {
12233
+ if (!ctx.startWalkthrough) {
12234
+ return {
12235
+ info: "/walk is only available inside `reasonix code`."
12236
+ };
12237
+ }
12238
+ return { info: ctx.startWalkthrough() };
12239
+ };
11880
12240
  var handlers3 = {
11881
12241
  undo,
11882
12242
  history,
@@ -11887,7 +12247,8 @@ var handlers3 = {
11887
12247
  "apply-plan": applyPlan,
11888
12248
  applyplan: applyPlan,
11889
12249
  mode,
11890
- commit
12250
+ commit,
12251
+ walk: walk2
11891
12252
  };
11892
12253
 
11893
12254
  // src/cli/ui/slash/handlers/jobs.ts
@@ -11952,10 +12313,10 @@ var handlers4 = {
11952
12313
  };
11953
12314
 
11954
12315
  // src/cli/ui/slash/handlers/mcp.ts
11955
- var mcp = (_args, loop, ctx) => {
12316
+ var mcp = (_args, loop2, ctx) => {
11956
12317
  const servers = ctx.mcpServers ?? [];
11957
12318
  const specs = ctx.mcpSpecs ?? [];
11958
- const toolSpecs = loop.prefix.toolSpecs ?? [];
12319
+ const toolSpecs = loop2.prefix.toolSpecs ?? [];
11959
12320
  if (servers.length === 0 && specs.length === 0 && toolSpecs.length === 0) {
11960
12321
  return {
11961
12322
  info: 'no MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp "<spec>". `reasonix mcp list` shows the catalog.'
@@ -12144,14 +12505,14 @@ var memory = (args, _loop, ctx) => {
12144
12505
  var handlers6 = { memory };
12145
12506
 
12146
12507
  // src/cli/ui/slash/handlers/model.ts
12147
- var model = (args, loop, ctx) => {
12508
+ var model = (args, loop2, ctx) => {
12148
12509
  const id = args[0];
12149
12510
  const known = ctx.models ?? null;
12150
12511
  if (!id) {
12151
12512
  const hint = known && known.length > 0 ? known.join(" | ") : "try deepseek-v4-flash or deepseek-v4-pro \u2014 run /models to fetch the live list";
12152
12513
  return { info: `usage: /model <id> (${hint})` };
12153
12514
  }
12154
- loop.configure({ model: id });
12515
+ loop2.configure({ model: id });
12155
12516
  if (known && known.length > 0 && !known.includes(id)) {
12156
12517
  return {
12157
12518
  info: `model \u2192 ${id} (\u26A0 not in the fetched catalog: ${known.join(", ")}. If this is wrong the next call will 400 \u2014 run /models to refresh.)`
@@ -12159,7 +12520,7 @@ var model = (args, loop, ctx) => {
12159
12520
  }
12160
12521
  return { info: `model \u2192 ${id}` };
12161
12522
  };
12162
- var models = (_args, loop, ctx) => {
12523
+ var models = (_args, loop2, ctx) => {
12163
12524
  const list = ctx.models ?? null;
12164
12525
  if (list === null) {
12165
12526
  ctx.refreshModels?.();
@@ -12172,7 +12533,7 @@ var models = (_args, loop, ctx) => {
12172
12533
  info: "DeepSeek /models returned an empty list. Try /models again, or check your account status at api-docs.deepseek.com."
12173
12534
  };
12174
12535
  }
12175
- const current = loop.model;
12536
+ const current = loop2.model;
12176
12537
  const lines = list.map((id) => id === current ? `\u25B8 ${id} (current)` : ` ${id}`);
12177
12538
  return {
12178
12539
  info: [
@@ -12184,18 +12545,18 @@ var models = (_args, loop, ctx) => {
12184
12545
  ].join("\n")
12185
12546
  };
12186
12547
  };
12187
- var harvest2 = (args, loop) => {
12548
+ var harvest2 = (args, loop2) => {
12188
12549
  const arg = (args[0] ?? "").toLowerCase();
12189
- const on = arg === "" ? !loop.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
12190
- loop.configure({ harvest: on });
12191
- if (loop.harvestEnabled) {
12550
+ const on = arg === "" ? !loop2.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
12551
+ loop2.configure({ harvest: on });
12552
+ if (loop2.harvestEnabled) {
12192
12553
  return {
12193
12554
  info: "harvest \u2192 on (Pillar-2 plan-state extraction \xB7 +1 cheap flash call per turn \xB7 opt-in only; no preset turns it on)"
12194
12555
  };
12195
12556
  }
12196
12557
  return { info: "harvest \u2192 off" };
12197
12558
  };
12198
- var preset = (args, loop) => {
12559
+ var preset = (args, loop2) => {
12199
12560
  const name = (args[0] ?? "").toLowerCase();
12200
12561
  const applyAndPersist = (effort2) => {
12201
12562
  try {
@@ -12204,7 +12565,7 @@ var preset = (args, loop) => {
12204
12565
  }
12205
12566
  };
12206
12567
  if (name === "fast" || name === "default") {
12207
- loop.configure({
12568
+ loop2.configure({
12208
12569
  model: "deepseek-v4-flash",
12209
12570
  reasoningEffort: "high",
12210
12571
  harvest: false,
@@ -12214,7 +12575,7 @@ var preset = (args, loop) => {
12214
12575
  return { info: "preset \u2192 fast (v4-flash \xB7 effort=high \xB7 cheapest)" };
12215
12576
  }
12216
12577
  if (name === "smart") {
12217
- loop.configure({
12578
+ loop2.configure({
12218
12579
  model: "deepseek-v4-flash",
12219
12580
  reasoningEffort: "max",
12220
12581
  harvest: false,
@@ -12224,7 +12585,7 @@ var preset = (args, loop) => {
12224
12585
  return { info: "preset \u2192 smart (v4-flash \xB7 effort=max \xB7 default \xB7 ~1.5\xD7 fast)" };
12225
12586
  }
12226
12587
  if (name === "max" || name === "best") {
12227
- loop.configure({
12588
+ loop2.configure({
12228
12589
  model: "deepseek-v4-pro",
12229
12590
  reasoningEffort: "max",
12230
12591
  harvest: false,
@@ -12237,10 +12598,10 @@ var preset = (args, loop) => {
12237
12598
  }
12238
12599
  return { info: "usage: /preset <fast|smart|max>" };
12239
12600
  };
12240
- var branch = (args, loop) => {
12601
+ var branch = (args, loop2) => {
12241
12602
  const raw = (args[0] ?? "").toLowerCase();
12242
12603
  if (raw === "" || raw === "off" || raw === "0" || raw === "1") {
12243
- loop.configure({ branch: 1 });
12604
+ loop2.configure({ branch: 1 });
12244
12605
  return { info: "branch \u2192 off" };
12245
12606
  }
12246
12607
  const n = Number.parseInt(raw, 10);
@@ -12250,36 +12611,36 @@ var branch = (args, loop) => {
12250
12611
  if (n > 8) {
12251
12612
  return { info: "branch budget capped at 8 to prevent runaway cost" };
12252
12613
  }
12253
- loop.configure({ branch: n });
12614
+ loop2.configure({ branch: n });
12254
12615
  return {
12255
12616
  info: `branch \u2192 ${n} (runs ${n} parallel samples per turn \xB7 ${n}\xD7 per-turn cost \xB7 streaming disabled \xB7 manual only, no preset enables branching)`
12256
12617
  };
12257
12618
  };
12258
- var effort = (args, loop) => {
12619
+ var effort = (args, loop2) => {
12259
12620
  const raw = (args[0] ?? "").toLowerCase();
12260
12621
  if (raw === "") {
12261
12622
  return {
12262
- info: `reasoning_effort \u2192 ${loop.reasoningEffort} (use /effort high for cheaper/faster, /effort max for the agent-class default \xB7 persisted across relaunches)`
12623
+ info: `reasoning_effort \u2192 ${loop2.reasoningEffort} (use /effort high for cheaper/faster, /effort max for the agent-class default \xB7 persisted across relaunches)`
12263
12624
  };
12264
12625
  }
12265
12626
  if (raw !== "high" && raw !== "max") {
12266
12627
  return { info: "usage: /effort <high|max>" };
12267
12628
  }
12268
- loop.configure({ reasoningEffort: raw });
12629
+ loop2.configure({ reasoningEffort: raw });
12269
12630
  try {
12270
12631
  saveReasoningEffort(raw);
12271
12632
  } catch {
12272
12633
  }
12273
12634
  return { info: `reasoning_effort \u2192 ${raw} (persisted)` };
12274
12635
  };
12275
- var pro = (args, loop, ctx) => {
12636
+ var pro = (args, loop2, ctx) => {
12276
12637
  const arg = (args[0] ?? "").toLowerCase();
12277
12638
  if (arg === "off" || arg === "cancel" || arg === "disarm") {
12278
- if (!loop.proArmed) {
12639
+ if (!loop2.proArmed) {
12279
12640
  return { info: "nothing armed \u2014 /pro with no args will arm pro for your next turn" };
12280
12641
  }
12281
12642
  if (ctx.disarmPro) ctx.disarmPro();
12282
- else loop.disarmPro();
12643
+ else loop2.disarmPro();
12283
12644
  return { info: "\u25B8 /pro disarmed \u2014 next turn falls back to the current preset" };
12284
12645
  }
12285
12646
  if (arg && arg !== "on" && arg !== "arm") {
@@ -12288,7 +12649,7 @@ var pro = (args, loop, ctx) => {
12288
12649
  };
12289
12650
  }
12290
12651
  if (ctx.armPro) ctx.armPro();
12291
- else loop.armProForNextTurn();
12652
+ else loop2.armProForNextTurn();
12292
12653
  return {
12293
12654
  info: `\u25B8 /pro armed \u2014 your NEXT message runs on ${ESCALATION_MODEL_ID} regardless of preset. Auto-disarms after one turn. Use /preset max for a persistent switch.`
12294
12655
  };
@@ -12305,8 +12666,8 @@ var handlers7 = {
12305
12666
  };
12306
12667
 
12307
12668
  // src/cli/ui/slash/handlers/observability.ts
12308
- var think = (_args, loop) => {
12309
- const raw = loop.scratch.reasoning;
12669
+ var think = (_args, loop2) => {
12670
+ const raw = loop2.scratch.reasoning;
12310
12671
  if (!raw || !raw.trim()) {
12311
12672
  return {
12312
12673
  info: "no reasoning cached. `/think` shows the full thinking-mode thought for the most recent turn \u2014 only thinking-mode models (deepseek-v4-flash / -v4-pro / -reasoner) produce it, and only once the turn completes."
@@ -12348,10 +12709,10 @@ var tool = (args, _loop, ctx) => {
12348
12709
  ${entry.text}`
12349
12710
  };
12350
12711
  };
12351
- var context = (_args, loop) => {
12352
- const systemTokens = countTokens(loop.prefix.system);
12353
- const toolsTokens = countTokens(JSON.stringify(loop.prefix.toolSpecs));
12354
- const entries = loop.log.toMessages();
12712
+ var context = (_args, loop2) => {
12713
+ const systemTokens = countTokens(loop2.prefix.system);
12714
+ const toolsTokens = countTokens(JSON.stringify(loop2.prefix.toolSpecs));
12715
+ const entries = loop2.log.toMessages();
12355
12716
  let userTokens = 0;
12356
12717
  let assistantTokens = 0;
12357
12718
  let toolResultTokens = 0;
@@ -12376,7 +12737,7 @@ var context = (_args, loop) => {
12376
12737
  }
12377
12738
  const logTokens = userTokens + assistantTokens + toolResultTokens + toolCallTokens;
12378
12739
  const total = systemTokens + toolsTokens + logTokens;
12379
- const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
12740
+ const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop2.model] ?? DEFAULT_CONTEXT_TOKENS;
12380
12741
  const pct2 = (n) => total > 0 ? `${Math.round(n / total * 100)}%`.padStart(4) : " 0%";
12381
12742
  const row2 = (label, n, note = "") => ` ${label.padEnd(20)}${compactNum(n).padStart(8)} tokens ${pct2(n)}${note ? ` ${note}` : ""}`;
12382
12743
  const lines = [
@@ -12385,7 +12746,7 @@ var context = (_args, loop) => {
12385
12746
  )}% of window)`,
12386
12747
  "",
12387
12748
  row2("system prompt", systemTokens),
12388
- row2("tool specs", toolsTokens, `(${loop.prefix.toolSpecs.length} tools)`),
12749
+ row2("tool specs", toolsTokens, `(${loop2.prefix.toolSpecs.length} tools)`),
12389
12750
  row2("log (all turns)", logTokens, `(${entries.length} messages)`),
12390
12751
  ` user ${compactNum(userTokens).padStart(8)} tokens`,
12391
12752
  ` assistant ${compactNum(assistantTokens).padStart(8)} tokens`,
@@ -12408,23 +12769,23 @@ var context = (_args, loop) => {
12408
12769
  );
12409
12770
  return { info: lines.join("\n") };
12410
12771
  };
12411
- var status = (_args, loop, ctx) => {
12412
- const branchBudget = loop.branchOptions.budget ?? 1;
12413
- const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
12414
- const lastPromptTokens = loop.stats.summary().lastPromptTokens;
12772
+ var status = (_args, loop2, ctx) => {
12773
+ const branchBudget = loop2.branchOptions.budget ?? 1;
12774
+ const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop2.model] ?? DEFAULT_CONTEXT_TOKENS;
12775
+ const lastPromptTokens = loop2.stats.summary().lastPromptTokens;
12415
12776
  const ctxPct = ctxMax > 0 ? Math.round(lastPromptTokens / ctxMax * 100) : 0;
12416
12777
  const ctxLine = lastPromptTokens > 0 ? ` ctx ${compactNum(lastPromptTokens)}/${compactNum(ctxMax)} (${ctxPct}%)` : " ctx no turns yet";
12417
12778
  const pending = ctx.pendingEditCount ?? 0;
12418
- const sessionLine = loop.sessionName ? ` session "${loop.sessionName}" \xB7 ${loop.log.length} messages in log (resumed ${loop.resumedMessageCount})` : " session (ephemeral \u2014 no persistence)";
12779
+ const sessionLine = loop2.sessionName ? ` session "${loop2.sessionName}" \xB7 ${loop2.log.length} messages in log (resumed ${loop2.resumedMessageCount})` : " session (ephemeral \u2014 no persistence)";
12419
12780
  const mcpCount = ctx.mcpSpecs?.length ?? 0;
12420
- const toolCount = loop.prefix.toolSpecs.length;
12781
+ const toolCount = loop2.prefix.toolSpecs.length;
12421
12782
  const mcpLine = ` mcp ${mcpCount} server(s), ${toolCount} tool(s) in registry`;
12422
12783
  const pendingLine = pending > 0 ? ` edits ${pending} pending (/apply to commit, /discard to drop)` : "";
12423
12784
  const planLine = ctx.planMode ? " plan ON \u2014 writes gated (submit_plan + approval)" : "";
12424
- const modeLine = ctx.editMode === "auto" ? " mode AUTO \u2014 edits apply immediately (u to undo within 5s \xB7 Shift+Tab to flip)" : ctx.editMode === "review" ? " mode review \u2014 edits queue for /apply or y (Shift+Tab to flip)" : "";
12785
+ const modeLine = ctx.editMode === "yolo" ? " mode YOLO \u2014 edits + shell auto-run with no prompt (/undo still rolls back \xB7 Shift+Tab to flip)" : ctx.editMode === "auto" ? " mode AUTO \u2014 edits apply immediately (u to undo within 5s \xB7 Shift+Tab to flip)" : ctx.editMode === "review" ? " mode review \u2014 edits queue for /apply or y (Shift+Tab to flip)" : "";
12425
12786
  const lines = [
12426
- ` model ${loop.model}`,
12427
- ` flags harvest=${loop.harvestEnabled ? "on" : "off"} \xB7 branch=${branchBudget > 1 ? branchBudget : "off"} \xB7 stream=${loop.stream ? "on" : "off"} \xB7 effort=${loop.reasoningEffort}`,
12787
+ ` model ${loop2.model}`,
12788
+ ` flags harvest=${loop2.harvestEnabled ? "on" : "off"} \xB7 branch=${branchBudget > 1 ? branchBudget : "off"} \xB7 stream=${loop2.stream ? "on" : "off"} \xB7 effort=${loop2.reasoningEffort}`,
12428
12789
  ctxLine,
12429
12790
  mcpLine,
12430
12791
  sessionLine
@@ -12434,10 +12795,10 @@ var status = (_args, loop, ctx) => {
12434
12795
  if (modeLine) lines.push(modeLine);
12435
12796
  return { info: lines.join("\n") };
12436
12797
  };
12437
- var compact = (args, loop) => {
12798
+ var compact = (args, loop2) => {
12438
12799
  const tight = Number.parseInt(args[0] ?? "", 10);
12439
12800
  const cap = Number.isFinite(tight) && tight >= 100 ? tight : 4e3;
12440
- const { healedCount, tokensSaved, charsSaved } = loop.compact(cap);
12801
+ const { healedCount, tokensSaved, charsSaved } = loop2.compact(cap);
12441
12802
  if (healedCount === 0) {
12442
12803
  return {
12443
12804
  info: `\u25B8 nothing to compact \u2014 no tool result or tool-call args in history exceed ${cap.toLocaleString()} tokens.`
@@ -12458,8 +12819,8 @@ var handlers8 = {
12458
12819
 
12459
12820
  // src/cli/ui/slash/handlers/plans.ts
12460
12821
  import { basename } from "path";
12461
- var plans = (_args, loop) => {
12462
- const sessionName = loop.sessionName;
12822
+ var plans = (_args, loop2) => {
12823
+ const sessionName = loop2.sessionName;
12463
12824
  if (!sessionName) {
12464
12825
  return {
12465
12826
  info: "no session attached \u2014 `/plans` is per-session. Run `reasonix code` in a project to get a session."
@@ -12500,8 +12861,8 @@ var plans = (_args, loop) => {
12500
12861
  }
12501
12862
  return { info: lines.join("\n") };
12502
12863
  };
12503
- var replay = (args, loop) => {
12504
- const sessionName = loop.sessionName;
12864
+ var replay = (args, loop2) => {
12865
+ const sessionName = loop2.sessionName;
12505
12866
  if (!sessionName) {
12506
12867
  return {
12507
12868
  info: "no session attached \u2014 `/replay` is per-session. Run `reasonix code` in a project to get a session."
@@ -12541,7 +12902,7 @@ var handlers9 = {
12541
12902
  };
12542
12903
 
12543
12904
  // src/cli/ui/slash/handlers/sessions.ts
12544
- var sessions = (_args, loop) => {
12905
+ var sessions = (_args, loop2) => {
12545
12906
  const items = listSessions();
12546
12907
  if (items.length === 0) {
12547
12908
  return {
@@ -12552,7 +12913,7 @@ var sessions = (_args, loop) => {
12552
12913
  for (const s of items) {
12553
12914
  const sizeKb = (s.size / 1024).toFixed(1);
12554
12915
  const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
12555
- const marker = s.name === loop.sessionName ? "\u25B8" : " ";
12916
+ const marker = s.name === loop2.sessionName ? "\u25B8" : " ";
12556
12917
  lines.push(
12557
12918
  ` ${marker} ${s.name.padEnd(22)} ${String(s.messageCount).padStart(5)} msgs ${sizeKb.padStart(7)} KB ${when}`
12558
12919
  );
@@ -12561,11 +12922,11 @@ var sessions = (_args, loop) => {
12561
12922
  lines.push("Resume with: reasonix chat --session <name>");
12562
12923
  return { info: lines.join("\n") };
12563
12924
  };
12564
- var forget = (_args, loop) => {
12565
- if (!loop.sessionName) {
12925
+ var forget = (_args, loop2) => {
12926
+ if (!loop2.sessionName) {
12566
12927
  return { info: "not in a session \u2014 nothing to forget" };
12567
12928
  }
12568
- const name = loop.sessionName;
12929
+ const name = loop2.sessionName;
12569
12930
  const ok = deleteSession(name);
12570
12931
  return {
12571
12932
  info: ok ? `\u25B8 deleted session "${name}" \u2014 current screen still shows the conversation, but next launch starts fresh` : `could not delete session "${name}" (already gone?)`
@@ -12666,9 +13027,9 @@ var HANDLERS = {
12666
13027
  ...handlers10,
12667
13028
  ...handlers11
12668
13029
  };
12669
- function handleSlash(cmd, args, loop, ctx = {}) {
13030
+ function handleSlash(cmd, args, loop2, ctx = {}) {
12670
13031
  const h = HANDLERS[cmd];
12671
- if (h) return h(args, loop, ctx);
13032
+ if (h) return h(args, loop2, ctx);
12672
13033
  return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
12673
13034
  }
12674
13035
 
@@ -13020,14 +13381,14 @@ function useEditHistory(codeMode) {
13020
13381
 
13021
13382
  // src/cli/ui/useSessionInfo.ts
13022
13383
  import { useCallback as useCallback3, useEffect as useEffect4, useState as useState8 } from "react";
13023
- function useSessionInfo(loop) {
13384
+ function useSessionInfo(loop2) {
13024
13385
  const [balance, setBalance] = useState8(null);
13025
13386
  const [models2, setModels] = useState8(null);
13026
13387
  const [latestVersion, setLatestVersion] = useState8(null);
13027
13388
  useEffect4(() => {
13028
13389
  let cancelled = false;
13029
13390
  void (async () => {
13030
- const bal = await loop.client.getBalance().catch(() => null);
13391
+ const bal = await loop2.client.getBalance().catch(() => null);
13031
13392
  if (cancelled || !bal || !bal.balance_infos.length) return;
13032
13393
  const primary = bal.balance_infos[0];
13033
13394
  setBalance({ currency: primary.currency, total: Number(primary.total_balance) });
@@ -13035,18 +13396,18 @@ function useSessionInfo(loop) {
13035
13396
  return () => {
13036
13397
  cancelled = true;
13037
13398
  };
13038
- }, [loop]);
13399
+ }, [loop2]);
13039
13400
  useEffect4(() => {
13040
13401
  let cancelled = false;
13041
13402
  void (async () => {
13042
- const list = await loop.client.listModels().catch(() => null);
13403
+ const list = await loop2.client.listModels().catch(() => null);
13043
13404
  if (cancelled || !list) return;
13044
13405
  setModels(list.data.map((m) => m.id));
13045
13406
  })();
13046
13407
  return () => {
13047
13408
  cancelled = true;
13048
13409
  };
13049
- }, [loop]);
13410
+ }, [loop2]);
13050
13411
  useEffect4(() => {
13051
13412
  let cancelled = false;
13052
13413
  void (async () => {
@@ -13061,19 +13422,19 @@ function useSessionInfo(loop) {
13061
13422
  const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
13062
13423
  const refreshBalance = useCallback3(() => {
13063
13424
  void (async () => {
13064
- const bal = await loop.client.getBalance().catch(() => null);
13425
+ const bal = await loop2.client.getBalance().catch(() => null);
13065
13426
  if (bal?.balance_infos.length) {
13066
13427
  const p = bal.balance_infos[0];
13067
13428
  setBalance({ currency: p.currency, total: Number(p.total_balance) });
13068
13429
  }
13069
13430
  })();
13070
- }, [loop]);
13431
+ }, [loop2]);
13071
13432
  const refreshModels = useCallback3(() => {
13072
13433
  void (async () => {
13073
- const list = await loop.client.listModels().catch(() => null);
13434
+ const list = await loop2.client.listModels().catch(() => null);
13074
13435
  if (list) setModels(list.data.map((m) => m.id));
13075
13436
  })();
13076
- }, [loop]);
13437
+ }, [loop2]);
13077
13438
  const refreshLatestVersion = useCallback3(() => {
13078
13439
  void (async () => {
13079
13440
  const fresh = await getLatestVersion({ force: true });
@@ -13151,6 +13512,17 @@ function useSubagent({ session, setHistorical }) {
13151
13512
  // src/cli/ui/App.tsx
13152
13513
  var FLUSH_INTERVAL_MS = 100;
13153
13514
  var PLAIN_UI = process.env.REASONIX_UI === "plain";
13515
+ function LoopStatusRow({
13516
+ loop: loop2
13517
+ }) {
13518
+ const [, setTick] = React23.useState(0);
13519
+ React23.useEffect(() => {
13520
+ const id = setInterval(() => setTick((t) => t + 1), 1e3);
13521
+ return () => clearInterval(id);
13522
+ }, []);
13523
+ const nextFireMs = Math.max(0, loop2.nextFireAt - Date.now());
13524
+ return /* @__PURE__ */ React23.createElement(Box21, null, /* @__PURE__ */ React23.createElement(Text19, { color: "cyan" }, `\u25B8 ${formatLoopStatus(loop2.prompt, nextFireMs, loop2.iter)} \xB7 /loop stop or type to cancel`));
13525
+ }
13154
13526
  function App({
13155
13527
  model: model2,
13156
13528
  system,
@@ -13170,6 +13542,9 @@ function App({
13170
13542
  const [input, setInput] = useState10("");
13171
13543
  const [busy, setBusy] = useState10(false);
13172
13544
  const abortedThisTurn = useRef6(false);
13545
+ useEffect6(() => {
13546
+ busyRef.current = busy;
13547
+ }, [busy]);
13173
13548
  const [ongoingTool, setOngoingTool] = useState10(null);
13174
13549
  const [toolProgress, setToolProgress] = useState10(null);
13175
13550
  const { stdout: stdout2 } = useStdout8();
@@ -13223,6 +13598,7 @@ function App({
13223
13598
  const [pendingCount, setPendingCount] = useState10(0);
13224
13599
  const syncPendingCount = useCallback4(() => {
13225
13600
  setPendingCount(pendingEdits.current.length);
13601
+ setPendingTick((t) => t + 1);
13226
13602
  }, []);
13227
13603
  const [editMode, setEditMode] = useState10(() => codeMode ? loadEditMode() : "review");
13228
13604
  const editModeRef = useRef6(editMode);
@@ -13231,6 +13607,8 @@ function App({
13231
13607
  if (codeMode) saveEditMode(editMode);
13232
13608
  }, [editMode, codeMode]);
13233
13609
  const [pendingEditReview, setPendingEditReview] = useState10(null);
13610
+ const [walkthroughActive, setWalkthroughActive] = useState10(false);
13611
+ const [pendingTick, setPendingTick] = useState10(0);
13234
13612
  const editReviewResolveRef = useRef6(null);
13235
13613
  const turnEditPolicyRef = useRef6("ask");
13236
13614
  const [modeFlash, setModeFlash] = useState10(false);
@@ -13261,6 +13639,16 @@ function App({
13261
13639
  const promptHistory = useRef6([]);
13262
13640
  const historyCursor = useRef6(-1);
13263
13641
  const assistantIterCounter = useRef6(0);
13642
+ const atUrlCache = useRef6(/* @__PURE__ */ new Map());
13643
+ const [activeLoop, setActiveLoop] = useState10(null);
13644
+ const loopTimerRef = useRef6(null);
13645
+ const handleSubmitRef = useRef6(null);
13646
+ const busyRef = useRef6(false);
13647
+ const activeLoopRef = useRef6(activeLoop);
13648
+ const loopFiringRef = useRef6(false);
13649
+ useEffect6(() => {
13650
+ activeLoopRef.current = activeLoop;
13651
+ }, [activeLoop]);
13264
13652
  const toolHistoryRef = useRef6([]);
13265
13653
  const planStepsRef = useRef6(null);
13266
13654
  const completedStepIdsRef = useRef6(/* @__PURE__ */ new Set());
@@ -13305,7 +13693,7 @@ function App({
13305
13693
  };
13306
13694
  }, []);
13307
13695
  const loopRef = useRef6(null);
13308
- const loop = useMemo3(() => {
13696
+ const loop2 = useMemo3(() => {
13309
13697
  if (loopRef.current) return loopRef.current;
13310
13698
  const client = new DeepSeekClient();
13311
13699
  if (tools && !tools.has("run_skill")) {
@@ -13354,8 +13742,8 @@ function App({
13354
13742
  return l;
13355
13743
  }, [model2, system, harvest3, branch2, session, tools, codeMode]);
13356
13744
  useEffect6(() => {
13357
- loop.hooks = hookList;
13358
- }, [loop, hookList]);
13745
+ loop2.hooks = hookList;
13746
+ }, [loop2, hookList]);
13359
13747
  const {
13360
13748
  balance,
13361
13749
  models: models2,
@@ -13364,7 +13752,7 @@ function App({
13364
13752
  refreshBalance,
13365
13753
  refreshModels,
13366
13754
  refreshLatestVersion
13367
- } = useSessionInfo(loop);
13755
+ } = useSessionInfo(loop2);
13368
13756
  const {
13369
13757
  slashMatches,
13370
13758
  slashSelected,
@@ -13407,13 +13795,13 @@ function App({
13407
13795
  text: "\u25B8 ephemeral chat (no session persistence) \u2014 drop --no-session to enable"
13408
13796
  }
13409
13797
  ]);
13410
- } else if (loop.resumedMessageCount > 0) {
13798
+ } else if (loop2.resumedMessageCount > 0) {
13411
13799
  setHistorical((prev) => [
13412
13800
  ...prev,
13413
13801
  {
13414
13802
  id: `sys-resume-${Date.now()}`,
13415
13803
  role: "info",
13416
- text: `\u25B8 resumed session "${session}" with ${loop.resumedMessageCount} prior messages \xB7 /forget to start over \xB7 /sessions to list`
13804
+ text: `\u25B8 resumed session "${session}" with ${loop2.resumedMessageCount} prior messages \xB7 /forget to start over \xB7 /sessions to list`
13417
13805
  }
13418
13806
  ]);
13419
13807
  } else {
@@ -13476,7 +13864,7 @@ function App({
13476
13864
  ]);
13477
13865
  markEditModeHintShown();
13478
13866
  }
13479
- }, [session, loop, codeMode, syncPendingCount]);
13867
+ }, [session, loop2, codeMode, syncPendingCount]);
13480
13868
  const quitProcess = useCallback4(() => {
13481
13869
  transcriptRef.current?.end();
13482
13870
  process.exit(0);
@@ -13506,25 +13894,40 @@ function App({
13506
13894
  setPendingEditReview(null);
13507
13895
  resolve8("reject");
13508
13896
  }
13509
- loop.abort();
13897
+ if (activeLoopRef.current) stopLoop();
13898
+ loop2.abort();
13510
13899
  return;
13511
13900
  }
13512
- if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision) {
13901
+ if (key.escape && !busy && activeLoopRef.current) {
13902
+ stopLoop();
13903
+ return;
13904
+ }
13905
+ if (key.escape && walkthroughActive) {
13906
+ setWalkthroughActive(false);
13907
+ const remaining = pendingEdits.current.length;
13908
+ setHistorical((prev) => [
13909
+ ...prev,
13910
+ {
13911
+ id: `walk-esc-${Date.now()}`,
13912
+ role: "info",
13913
+ text: remaining > 0 ? `\u25B8 walk cancelled \u2014 ${remaining} block(s) still pending.` : "\u25B8 walk cancelled."
13914
+ }
13915
+ ]);
13916
+ return;
13917
+ }
13918
+ if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !walkthroughActive && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision) {
13513
13919
  setEditMode((m) => {
13514
- const next = m === "auto" ? "review" : "auto";
13920
+ const next = m === "review" ? "auto" : m === "auto" ? "yolo" : "review";
13921
+ const message = next === "yolo" ? "\u25B8 edit mode: YOLO \u2014 edits AND shell commands auto-run. /undo still rolls back edits. Use carefully." : next === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo. Shell commands still ask." : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)";
13515
13922
  setHistorical((prev) => [
13516
13923
  ...prev,
13517
- {
13518
- id: `mode-${Date.now()}`,
13519
- role: "info",
13520
- text: next === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo" : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)"
13521
- }
13924
+ { id: `mode-${Date.now()}`, role: "info", text: message }
13522
13925
  ]);
13523
13926
  return next;
13524
13927
  });
13525
13928
  return;
13526
13929
  }
13527
- if (codeMode && input.length === 0 && (chKey === "u" || chKey === "U") && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision && // Fire when EITHER the banner is up OR there's any non-undone
13930
+ if (codeMode && input.length === 0 && (chKey === "u" || chKey === "U") && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !walkthroughActive && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision && // Fire when EITHER the banner is up OR there's any non-undone
13528
13931
  // history entry — the keybind is useful long after the 5-second
13529
13932
  // banner expires, which users rightly want.
13530
13933
  (undoBanner || hasUndoable())) {
@@ -13625,7 +14028,7 @@ function App({
13625
14028
  }
13626
14029
  return formatEditResults(results);
13627
14030
  };
13628
- if (editModeRef.current === "auto") return applyNow();
14031
+ if (editModeRef.current === "auto" || editModeRef.current === "yolo") return applyNow();
13629
14032
  if (turnEditPolicyRef.current === "apply-all") return applyNow();
13630
14033
  const choice = await new Promise((resolveChoice) => {
13631
14034
  editReviewResolveRef.current = resolveChoice;
@@ -13674,30 +14077,51 @@ function App({
13674
14077
  tools.setToolInterceptor(null);
13675
14078
  };
13676
14079
  }, [tools, codeMode, session, recordEdit, armUndoBanner, syncPendingCount, setEditMode]);
13677
- const codeApply = useCallback4(() => {
13678
- if (!codeMode) return "not in code mode";
13679
- const blocks = pendingEdits.current;
13680
- if (blocks.length === 0) {
13681
- return "nothing pending \u2014 the model hasn't proposed edits since the last /apply or /discard.";
13682
- }
13683
- const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
13684
- const results = applyEditBlocks(blocks, codeMode.rootDir);
13685
- const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
13686
- if (anyApplied) recordEdit("review-apply", blocks, results, snaps);
13687
- pendingEdits.current = [];
13688
- clearPendingEdits(session ?? null);
13689
- syncPendingCount();
13690
- return formatEditResults(results);
13691
- }, [codeMode, session, syncPendingCount, recordEdit]);
13692
- const codeDiscard = useCallback4(() => {
13693
- const count = pendingEdits.current.length;
13694
- if (count === 0) return "nothing pending to discard.";
13695
- pendingEdits.current = [];
13696
- clearPendingEdits(session ?? null);
13697
- syncPendingCount();
13698
- return `\u25B8 discarded ${count} pending edit block(s). Nothing was written to disk.`;
13699
- }, [session, syncPendingCount]);
13700
- const prefixHash = loop.prefix.fingerprint;
14080
+ const codeApply = useCallback4(
14081
+ (indices) => {
14082
+ if (!codeMode) return "not in code mode";
14083
+ const blocks = pendingEdits.current;
14084
+ if (blocks.length === 0) {
14085
+ return "nothing pending \u2014 the model hasn't proposed edits since the last /apply or /discard.";
14086
+ }
14087
+ const useSubset = indices !== void 0 && indices.length > 0;
14088
+ const { selected, remaining } = useSubset ? partitionEdits(blocks, indices) : { selected: blocks, remaining: [] };
14089
+ if (selected.length === 0) {
14090
+ return "\u25B8 no edits matched those indices \u2014 nothing applied. Use /apply with no args to commit them all.";
14091
+ }
14092
+ const snaps = snapshotBeforeEdits(selected, codeMode.rootDir);
14093
+ const results = applyEditBlocks(selected, codeMode.rootDir);
14094
+ const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
14095
+ if (anyApplied) recordEdit("review-apply", selected, results, snaps);
14096
+ pendingEdits.current = remaining;
14097
+ if (remaining.length === 0) clearPendingEdits(session ?? null);
14098
+ else savePendingEdits(session ?? null, remaining);
14099
+ syncPendingCount();
14100
+ const tail = remaining.length > 0 ? `
14101
+ \u25B8 ${remaining.length} edit block(s) still pending \u2014 /apply or /discard to clear them.` : "";
14102
+ return formatEditResults(results) + tail;
14103
+ },
14104
+ [codeMode, session, syncPendingCount, recordEdit]
14105
+ );
14106
+ const codeDiscard = useCallback4(
14107
+ (indices) => {
14108
+ const blocks = pendingEdits.current;
14109
+ if (blocks.length === 0) return "nothing pending to discard.";
14110
+ const useSubset = indices !== void 0 && indices.length > 0;
14111
+ const { selected, remaining } = useSubset ? partitionEdits(blocks, indices) : { selected: blocks, remaining: [] };
14112
+ if (selected.length === 0) {
14113
+ return "\u25B8 no edits matched those indices \u2014 nothing discarded.";
14114
+ }
14115
+ pendingEdits.current = remaining;
14116
+ if (remaining.length === 0) clearPendingEdits(session ?? null);
14117
+ else savePendingEdits(session ?? null, remaining);
14118
+ syncPendingCount();
14119
+ const tail = remaining.length > 0 ? ` (${remaining.length} block(s) still pending)` : ". Nothing was written to disk.";
14120
+ return `\u25B8 discarded ${selected.length} pending edit block(s)${tail}`;
14121
+ },
14122
+ [session, syncPendingCount]
14123
+ );
14124
+ const prefixHash = loop2.prefix.fingerprint;
13701
14125
  const writeTranscript = useCallback4(
13702
14126
  (ev) => {
13703
14127
  const stream = transcriptRef.current;
@@ -13716,10 +14140,98 @@ function App({
13716
14140
  const clearPendingPlan = useCallback4(() => {
13717
14141
  setPendingPlan(null);
13718
14142
  }, []);
14143
+ const stopLoop = useCallback4(() => {
14144
+ if (loopTimerRef.current) {
14145
+ clearTimeout(loopTimerRef.current);
14146
+ loopTimerRef.current = null;
14147
+ }
14148
+ setActiveLoop((cur) => {
14149
+ if (!cur) return cur;
14150
+ setHistorical((prev) => [
14151
+ ...prev,
14152
+ {
14153
+ id: `loop-stop-${Date.now()}`,
14154
+ role: "info",
14155
+ text: `\u25B8 loop stopped (after ${cur.iter} iter${cur.iter === 1 ? "" : "s"}).`
14156
+ }
14157
+ ]);
14158
+ return null;
14159
+ });
14160
+ }, []);
14161
+ const startLoop = useCallback4((intervalMs, prompt) => {
14162
+ if (loopTimerRef.current) {
14163
+ clearTimeout(loopTimerRef.current);
14164
+ loopTimerRef.current = null;
14165
+ }
14166
+ setActiveLoop({
14167
+ prompt,
14168
+ intervalMs,
14169
+ nextFireAt: Date.now() + intervalMs,
14170
+ iter: 0
14171
+ });
14172
+ }, []);
14173
+ const startWalkthrough = useCallback4(() => {
14174
+ if (!codeMode) {
14175
+ return "/walk is only available inside `reasonix code`.";
14176
+ }
14177
+ if (pendingEdits.current.length === 0) {
14178
+ return "nothing pending \u2014 nothing to walk through.";
14179
+ }
14180
+ setWalkthroughActive(true);
14181
+ return `\u25B8 walking ${pendingEdits.current.length} edit block(s) \u2014 y apply \xB7 n reject \xB7 a apply rest \xB7 A flip to AUTO \xB7 Esc cancels (keeps remaining queued).`;
14182
+ }, [codeMode]);
14183
+ const handleWalkChoice = useCallback4(
14184
+ (choice) => {
14185
+ if (choice === "apply") {
14186
+ const out = codeApply([1]);
14187
+ setHistorical((prev) => [...prev, { id: `walk-${Date.now()}`, role: "info", text: out }]);
14188
+ } else if (choice === "reject") {
14189
+ const out = codeDiscard([1]);
14190
+ setHistorical((prev) => [...prev, { id: `walk-${Date.now()}`, role: "info", text: out }]);
14191
+ } else if (choice === "apply-rest-of-turn") {
14192
+ const out = codeApply();
14193
+ setHistorical((prev) => [...prev, { id: `walk-${Date.now()}`, role: "info", text: out }]);
14194
+ setWalkthroughActive(false);
14195
+ return;
14196
+ } else if (choice === "flip-to-auto") {
14197
+ setEditMode("auto");
14198
+ saveEditMode("auto");
14199
+ const out = codeApply([1]);
14200
+ setHistorical((prev) => [
14201
+ ...prev,
14202
+ { id: `walk-${Date.now()}`, role: "info", text: out },
14203
+ {
14204
+ id: `walk-flip-${Date.now()}`,
14205
+ role: "info",
14206
+ text: "\u25B8 flipped to AUTO mode \u2014 future edits will apply immediately. Walk exited."
14207
+ }
14208
+ ]);
14209
+ setWalkthroughActive(false);
14210
+ return;
14211
+ }
14212
+ if (pendingEdits.current.length === 0) setWalkthroughActive(false);
14213
+ },
14214
+ [codeApply, codeDiscard]
14215
+ );
14216
+ const getLoopStatus = useCallback4(() => {
14217
+ const cur = activeLoopRef.current;
14218
+ if (!cur) return null;
14219
+ return {
14220
+ prompt: cur.prompt,
14221
+ intervalMs: cur.intervalMs,
14222
+ iter: cur.iter,
14223
+ nextFireMs: Math.max(0, cur.nextFireAt - Date.now())
14224
+ };
14225
+ }, []);
13719
14226
  const handleSubmit = useCallback4(
13720
14227
  async (raw) => {
13721
14228
  let text = raw.trim();
13722
- if (!text || busy) return;
14229
+ if (!text) return;
14230
+ if (activeLoopRef.current && !loopFiringRef.current) {
14231
+ stopLoop();
14232
+ }
14233
+ loopFiringRef.current = false;
14234
+ if (busy) return;
13723
14235
  if (atMatches && atMatches.length > 0 && atPicker) {
13724
14236
  const sel = atMatches[atSelected] ?? atMatches[0];
13725
14237
  if (sel) {
@@ -13751,6 +14263,38 @@ function App({
13751
14263
  promptHistory.current.push(text);
13752
14264
  return;
13753
14265
  }
14266
+ const hashParse = detectHashMemory(text);
14267
+ if (hashParse?.kind === "memory" || hashParse?.kind === "memory-global") {
14268
+ const isGlobal = hashParse.kind === "memory-global";
14269
+ const memRoot = codeMode?.rootDir ?? process.cwd();
14270
+ promptHistory.current.push(text);
14271
+ try {
14272
+ const result = isGlobal ? appendGlobalMemory(hashParse.note) : appendProjectMemory(memRoot, hashParse.note);
14273
+ const verb = result.created ? "created" : "appended to";
14274
+ const scopeTag = isGlobal ? "global" : "project";
14275
+ setHistorical((prev) => [
14276
+ ...prev,
14277
+ {
14278
+ id: `hash-${Date.now()}`,
14279
+ role: "info",
14280
+ text: `\u25B8 noted (${scopeTag}) \u2014 ${verb} ${result.path}`
14281
+ }
14282
+ ]);
14283
+ } catch (err) {
14284
+ setHistorical((prev) => [
14285
+ ...prev,
14286
+ {
14287
+ id: `hash-e-${Date.now()}`,
14288
+ role: "warning",
14289
+ text: `# memory write failed: ${err.message}`
14290
+ }
14291
+ ]);
14292
+ }
14293
+ return;
14294
+ }
14295
+ if (hashParse?.kind === "escape") {
14296
+ text = hashParse.text;
14297
+ }
13754
14298
  const bangCmd = detectBangCommand(text);
13755
14299
  if (bangCmd !== null) {
13756
14300
  const bangRoot = codeMode?.rootDir ?? process.cwd();
@@ -13776,7 +14320,7 @@ function App({
13776
14320
  ...prev,
13777
14321
  { id: `bang-o-${Date.now()}`, role: "info", text: formatted }
13778
14322
  ]);
13779
- loop.appendAndPersist({
14323
+ loop2.appendAndPersist({
13780
14324
  role: "user",
13781
14325
  content: formatBangUserMessage(bangCmd, formatted)
13782
14326
  });
@@ -13808,7 +14352,7 @@ function App({
13808
14352
  }
13809
14353
  const slash = parseSlash(text);
13810
14354
  if (slash) {
13811
- const result = handleSlash(slash.cmd, slash.args, loop, {
14355
+ const result = handleSlash(slash.cmd, slash.args, loop2, {
13812
14356
  mcpSpecs,
13813
14357
  mcpServers,
13814
14358
  codeUndo: codeMode ? codeUndo : void 0,
@@ -13826,13 +14370,17 @@ function App({
13826
14370
  editMode: codeMode ? editMode : void 0,
13827
14371
  setEditMode: codeMode ? setEditMode : void 0,
13828
14372
  armPro: () => {
13829
- loop.armProForNextTurn();
14373
+ loop2.armProForNextTurn();
13830
14374
  setProArmed(true);
13831
14375
  },
13832
14376
  disarmPro: () => {
13833
- loop.disarmPro();
14377
+ loop2.disarmPro();
13834
14378
  setProArmed(false);
13835
14379
  },
14380
+ startLoop,
14381
+ stopLoop,
14382
+ getLoopStatus,
14383
+ startWalkthrough: codeMode ? startWalkthrough : void 0,
13836
14384
  jobs: codeMode?.jobs,
13837
14385
  postInfo: (text2) => setHistorical((prev) => [
13838
14386
  ...prev,
@@ -13849,6 +14397,7 @@ function App({
13849
14397
  refreshModels
13850
14398
  });
13851
14399
  if (result.exit) {
14400
+ if (activeLoopRef.current) stopLoop();
13852
14401
  transcriptRef.current?.end();
13853
14402
  exit2();
13854
14403
  return;
@@ -13867,6 +14416,7 @@ function App({
13867
14416
  clearPendingEdits(session ?? null);
13868
14417
  syncPendingCount();
13869
14418
  }
14419
+ if (activeLoopRef.current) stopLoop();
13870
14420
  return;
13871
14421
  }
13872
14422
  if (result.clear) {
@@ -13877,6 +14427,7 @@ function App({
13877
14427
  clearPendingEdits(session ?? null);
13878
14428
  syncPendingCount();
13879
14429
  }
14430
+ if (activeLoopRef.current) stopLoop();
13880
14431
  return;
13881
14432
  }
13882
14433
  if (result.info) {
@@ -14005,8 +14556,47 @@ function App({
14005
14556
  }
14006
14557
  }
14007
14558
  }
14559
+ if (/(?:^|\s)@https?:\/\//.test(text)) {
14560
+ try {
14561
+ const urlExpanded = await expandAtUrls(modelInput, {
14562
+ fetcher: webFetch,
14563
+ cache: atUrlCache.current
14564
+ });
14565
+ if (urlExpanded.expansions.length > 0) {
14566
+ modelInput = urlExpanded.text;
14567
+ const inlined = urlExpanded.expansions.filter((ex) => ex.ok).map((ex) => {
14568
+ const tag = ex.title ? `${ex.title} (${ex.url})` : ex.url;
14569
+ const trunc = ex.truncated ? " \xB7 truncated" : "";
14570
+ return `${tag} \xB7 ${(ex.chars ?? 0).toLocaleString()} chars${trunc}`;
14571
+ });
14572
+ const skipped = urlExpanded.expansions.filter((ex) => !ex.ok).map((ex) => `${ex.url} (${ex.skip ?? "fetch-error"})`);
14573
+ const parts = [];
14574
+ if (inlined.length > 0) parts.push(`inlined ${inlined.join("; ")}`);
14575
+ if (skipped.length > 0) parts.push(`skipped ${skipped.join("; ")}`);
14576
+ if (parts.length > 0) {
14577
+ setHistorical((prev) => [
14578
+ ...prev,
14579
+ {
14580
+ id: `aturl-${Date.now()}`,
14581
+ role: "info",
14582
+ text: `\u25B8 @url: ${parts.join("; ")}`
14583
+ }
14584
+ ]);
14585
+ }
14586
+ }
14587
+ } catch (err) {
14588
+ setHistorical((prev) => [
14589
+ ...prev,
14590
+ {
14591
+ id: `aturl-e-${Date.now()}`,
14592
+ role: "warning",
14593
+ text: `@url expansion failed: ${err.message}`
14594
+ }
14595
+ ]);
14596
+ }
14597
+ }
14008
14598
  try {
14009
- for await (const ev of loop.step(modelInput)) {
14599
+ for await (const ev of loop2.step(modelInput)) {
14010
14600
  writeTranscript(ev);
14011
14601
  if (ev.role !== "status") {
14012
14602
  setStatusLine((cur) => cur ? null : cur);
@@ -14046,7 +14636,7 @@ function App({
14046
14636
  flush();
14047
14637
  const repairNote = ev.repair ? describeRepair(ev.repair) : "";
14048
14638
  setStreaming(null);
14049
- setSummary(loop.stats.summary());
14639
+ setSummary(loop2.stats.summary());
14050
14640
  if (ev.stats?.usage) {
14051
14641
  appendUsage({
14052
14642
  session: session ?? null,
@@ -14080,7 +14670,7 @@ function App({
14080
14670
  if (codeMode && finalText && !ev.forcedSummary) {
14081
14671
  const blocks = parseEditBlocks(finalText);
14082
14672
  if (blocks.length > 0) {
14083
- if (editModeRef.current === "auto") {
14673
+ if (editModeRef.current === "auto" || editModeRef.current === "yolo") {
14084
14674
  const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
14085
14675
  const results = applyEditBlocks(blocks, codeMode.rootDir);
14086
14676
  const good = results.some(
@@ -14292,7 +14882,7 @@ function App({
14292
14882
  event: "Stop",
14293
14883
  cwd: hookCwd,
14294
14884
  lastAssistantText: streamRef.text,
14295
- turn: loop.stats.summary().turns
14885
+ turn: loop2.stats.summary().turns
14296
14886
  }
14297
14887
  });
14298
14888
  for (const o of stopReport.outcomes) {
@@ -14313,7 +14903,7 @@ function App({
14313
14903
  setOngoingTool(null);
14314
14904
  setToolProgress(null);
14315
14905
  setStatusLine(null);
14316
- setSummary(loop.stats.summary());
14906
+ setSummary(loop2.stats.summary());
14317
14907
  setBusy(false);
14318
14908
  setTurnOnPro(false);
14319
14909
  refreshBalance();
@@ -14331,7 +14921,7 @@ function App({
14331
14921
  exit2,
14332
14922
  hookCwd,
14333
14923
  hookList,
14334
- loop,
14924
+ loop2,
14335
14925
  latestVersion,
14336
14926
  mcpSpecs,
14337
14927
  mcpServers,
@@ -14360,9 +14950,51 @@ function App({
14360
14950
  refreshModels,
14361
14951
  proArmed,
14362
14952
  persistPlanState,
14363
- stdout2
14953
+ stdout2,
14954
+ stopLoop,
14955
+ startLoop,
14956
+ getLoopStatus,
14957
+ startWalkthrough
14364
14958
  ]
14365
14959
  );
14960
+ useEffect6(() => {
14961
+ handleSubmitRef.current = handleSubmit;
14962
+ }, [handleSubmit]);
14963
+ useEffect6(() => {
14964
+ if (!activeLoop) return;
14965
+ const delay = Math.max(0, activeLoop.nextFireAt - Date.now());
14966
+ const timer = setTimeout(async () => {
14967
+ loopTimerRef.current = null;
14968
+ if (busyRef.current) {
14969
+ setActiveLoop((cur2) => cur2 ? { ...cur2, nextFireAt: Date.now() + 1e3 } : cur2);
14970
+ return;
14971
+ }
14972
+ const cur = activeLoopRef.current;
14973
+ if (!cur) return;
14974
+ const nextIter = cur.iter + 1;
14975
+ setActiveLoop(
14976
+ (c) => c ? { ...c, iter: nextIter, nextFireAt: Date.now() + cur.intervalMs } : c
14977
+ );
14978
+ setHistorical((prev) => [
14979
+ ...prev,
14980
+ {
14981
+ id: `loop-fire-${Date.now()}`,
14982
+ role: "info",
14983
+ text: `\u25B8 /loop iter ${nextIter} \u2192 ${cur.prompt}`
14984
+ }
14985
+ ]);
14986
+ loopFiringRef.current = true;
14987
+ try {
14988
+ await handleSubmitRef.current?.(cur.prompt);
14989
+ } catch {
14990
+ stopLoop();
14991
+ } finally {
14992
+ loopFiringRef.current = false;
14993
+ }
14994
+ }, delay);
14995
+ loopTimerRef.current = timer;
14996
+ return () => clearTimeout(timer);
14997
+ }, [activeLoop, stopLoop]);
14366
14998
  const handleShellConfirm = useCallback4(
14367
14999
  async (choice) => {
14368
15000
  const pending = pendingShell;
@@ -14452,13 +15084,13 @@ ${body}`;
14452
15084
  }
14453
15085
  }
14454
15086
  if (busy) {
14455
- loop.abort();
15087
+ loop2.abort();
14456
15088
  setQueuedSubmit(synthetic);
14457
15089
  } else {
14458
15090
  await handleSubmit(synthetic);
14459
15091
  }
14460
15092
  },
14461
- [pendingShell, codeMode, handleSubmit, busy, loop]
15093
+ [pendingShell, codeMode, handleSubmit, busy, loop2]
14462
15094
  );
14463
15095
  useEffect6(() => {
14464
15096
  if (!busy && queuedSubmit !== null) {
@@ -14496,13 +15128,13 @@ ${body}`;
14496
15128
  { id: `plan-${choice}-${Date.now()}`, role: "info", text: marker }
14497
15129
  ]);
14498
15130
  if (busy) {
14499
- loop.abort();
15131
+ loop2.abort();
14500
15132
  setQueuedSubmit(synthetic);
14501
15133
  } else {
14502
15134
  await handleSubmit(synthetic);
14503
15135
  }
14504
15136
  },
14505
- [pendingPlan, togglePlanMode, busy, loop, handleSubmit, persistPlanState]
15137
+ [pendingPlan, togglePlanMode, busy, loop2, handleSubmit, persistPlanState]
14506
15138
  );
14507
15139
  const handlePlanConfirmRef = useRef6(handlePlanConfirm);
14508
15140
  useEffect6(() => {
@@ -14553,13 +15185,13 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
14553
15185
  { id: `plan-${staged.mode}-${Date.now()}`, role: "info", text: marker }
14554
15186
  ]);
14555
15187
  if (busy) {
14556
- loop.abort();
15188
+ loop2.abort();
14557
15189
  setQueuedSubmit(synthetic);
14558
15190
  } else {
14559
15191
  await handleSubmit(synthetic);
14560
15192
  }
14561
15193
  },
14562
- [stagedInput, togglePlanMode, busy, loop, handleSubmit]
15194
+ [stagedInput, togglePlanMode, busy, loop2, handleSubmit]
14563
15195
  );
14564
15196
  const handleStagedInputCancel = useCallback4(() => {
14565
15197
  if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
@@ -14588,13 +15220,13 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
14588
15220
  { id: `cp-${choice}-${Date.now()}`, role: "info", text: marker }
14589
15221
  ]);
14590
15222
  if (busy) {
14591
- loop.abort();
15223
+ loop2.abort();
14592
15224
  setQueuedSubmit(synthetic);
14593
15225
  } else {
14594
15226
  await handleSubmit(synthetic);
14595
15227
  }
14596
15228
  },
14597
- [pendingCheckpoint, busy, loop, handleSubmit]
15229
+ [pendingCheckpoint, busy, loop2, handleSubmit]
14598
15230
  );
14599
15231
  const handleCheckpointConfirmRef = useRef6(handleCheckpointConfirm);
14600
15232
  useEffect6(() => {
@@ -14622,13 +15254,13 @@ If the feedback only tweaks how you execute (extra constraints, style preference
14622
15254
  { id: `cp-revise-${Date.now()}`, role: "info", text: marker }
14623
15255
  ]);
14624
15256
  if (busy) {
14625
- loop.abort();
15257
+ loop2.abort();
14626
15258
  setQueuedSubmit(synthetic);
14627
15259
  } else {
14628
15260
  await handleSubmit(synthetic);
14629
15261
  }
14630
15262
  },
14631
- [stagedCheckpointRevise, busy, loop, handleSubmit]
15263
+ [stagedCheckpointRevise, busy, loop2, handleSubmit]
14632
15264
  );
14633
15265
  const handleCheckpointReviseCancel = useCallback4(() => {
14634
15266
  const snap = stagedCheckpointRevise;
@@ -14651,7 +15283,7 @@ If the feedback only tweaks how you execute (extra constraints, style preference
14651
15283
  { id: `choice-cancel-${Date.now()}`, role: "info", text: "\u25B8 choice cancelled" }
14652
15284
  ]);
14653
15285
  if (busy) {
14654
- loop.abort();
15286
+ loop2.abort();
14655
15287
  setQueuedSubmit(synthetic2);
14656
15288
  } else {
14657
15289
  await handleSubmit(synthetic2);
@@ -14666,13 +15298,13 @@ If the feedback only tweaks how you execute (extra constraints, style preference
14666
15298
  { id: `choice-pick-${Date.now()}`, role: "info", text: `\u25B8 chose ${label}` }
14667
15299
  ]);
14668
15300
  if (busy) {
14669
- loop.abort();
15301
+ loop2.abort();
14670
15302
  setQueuedSubmit(synthetic);
14671
15303
  } else {
14672
15304
  await handleSubmit(synthetic);
14673
15305
  }
14674
15306
  },
14675
- [pendingChoice, busy, loop, handleSubmit]
15307
+ [pendingChoice, busy, loop2, handleSubmit]
14676
15308
  );
14677
15309
  const handleChoiceConfirmRef = useRef6(handleChoiceConfirm);
14678
15310
  useEffect6(() => {
@@ -14697,13 +15329,13 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
14697
15329
  { id: `choice-custom-${Date.now()}`, role: "info", text: marker }
14698
15330
  ]);
14699
15331
  if (busy) {
14700
- loop.abort();
15332
+ loop2.abort();
14701
15333
  setQueuedSubmit(synthetic);
14702
15334
  } else {
14703
15335
  await handleSubmit(synthetic);
14704
15336
  }
14705
15337
  },
14706
- [busy, loop, handleSubmit]
15338
+ [busy, loop2, handleSubmit]
14707
15339
  );
14708
15340
  const handleChoiceCustomCancel = useCallback4(() => {
14709
15341
  const snap = stagedChoiceCustom;
@@ -14722,7 +15354,7 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
14722
15354
  { id: `revise-reject-${Date.now()}`, role: "info", text: "\u25B8 revision rejected" }
14723
15355
  ]);
14724
15356
  if (busy) {
14725
- loop.abort();
15357
+ loop2.abort();
14726
15358
  setQueuedSubmit(synthetic2);
14727
15359
  } else {
14728
15360
  await handleSubmit(synthetic2);
@@ -14757,13 +15389,13 @@ ${snap.remainingSteps.map((s, i) => ` ${i + 1}. ${s.id} \xB7 ${s.title} \u2014
14757
15389
 
14758
15390
  Continue executing from the next pending step. Call mark_step_complete after each one as before.`;
14759
15391
  if (busy) {
14760
- loop.abort();
15392
+ loop2.abort();
14761
15393
  setQueuedSubmit(synthetic);
14762
15394
  } else {
14763
15395
  await handleSubmit(synthetic);
14764
15396
  }
14765
15397
  },
14766
- [pendingRevision, busy, loop, handleSubmit, persistPlanState]
15398
+ [pendingRevision, busy, loop2, handleSubmit, persistPlanState]
14767
15399
  );
14768
15400
  const handleReviseConfirmRef = useRef6(handleReviseConfirm);
14769
15401
  useEffect6(() => {
@@ -14776,17 +15408,17 @@ Continue executing from the next pending step. Call mark_step_complete after eac
14776
15408
  return /* @__PURE__ */ React23.createElement(React23.Fragment, null, /* @__PURE__ */ React23.createElement(
14777
15409
  TickerProvider,
14778
15410
  {
14779
- disabled: PLAIN_UI || isResizing || !!pendingPlan || !!pendingShell || !!pendingEditReview || !!pendingCheckpoint || !!stagedCheckpointRevise || !!pendingChoice || !!stagedChoiceCustom || !!pendingRevision
15411
+ disabled: PLAIN_UI || isResizing || !!pendingPlan || !!pendingShell || !!pendingEditReview || walkthroughActive || !!pendingCheckpoint || !!stagedCheckpointRevise || !!pendingChoice || !!stagedChoiceCustom || !!pendingRevision
14780
15412
  },
14781
15413
  /* @__PURE__ */ React23.createElement(Box21, { flexDirection: "column" }, /* @__PURE__ */ React23.createElement(
14782
15414
  StatsPanel,
14783
15415
  {
14784
15416
  summary,
14785
- model: loop.model,
15417
+ model: loop2.model,
14786
15418
  prefixHash,
14787
- harvestOn: loop.harvestEnabled,
14788
- branchBudget: loop.branchOptions.budget,
14789
- reasoningEffort: loop.reasoningEffort,
15419
+ harvestOn: loop2.harvestEnabled,
15420
+ branchBudget: loop2.branchOptions.budget,
15421
+ reasoningEffort: loop2.reasoningEffort,
14790
15422
  planMode,
14791
15423
  editMode: codeMode ? editMode : void 0,
14792
15424
  balance,
@@ -14875,6 +15507,13 @@ Continue executing from the next pending step. Call mark_step_complete after eac
14875
15507
  }
14876
15508
  }
14877
15509
  }
15510
+ ) : walkthroughActive && pendingEdits.current.length > 0 ? /* @__PURE__ */ React23.createElement(
15511
+ EditConfirm,
15512
+ {
15513
+ key: `walk-${pendingTick}`,
15514
+ block: pendingEdits.current[0],
15515
+ onChoose: handleWalkChoice
15516
+ }
14878
15517
  ) : /* @__PURE__ */ React23.createElement(React23.Fragment, null, codeMode ? /* @__PURE__ */ React23.createElement(
14879
15518
  ModeStatusBar,
14880
15519
  {
@@ -14885,7 +15524,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
14885
15524
  undoArmed: !!undoBanner || hasUndoable(),
14886
15525
  jobs: codeMode.jobs
14887
15526
  }
14888
- ) : null, /* @__PURE__ */ React23.createElement(
15527
+ ) : null, activeLoop ? /* @__PURE__ */ React23.createElement(LoopStatusRow, { loop: activeLoop }) : null, /* @__PURE__ */ React23.createElement(
14889
15528
  PromptInput,
14890
15529
  {
14891
15530
  value: input,
@@ -14916,7 +15555,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
14916
15555
  }
14917
15556
 
14918
15557
  // src/cli/ui/SessionPicker.tsx
14919
- import { Box as Box22, Text as Text19 } from "ink";
15558
+ import { Box as Box22, Text as Text20 } from "ink";
14920
15559
  import React24 from "react";
14921
15560
  function SessionPicker({
14922
15561
  sessionName,
@@ -14924,7 +15563,7 @@ function SessionPicker({
14924
15563
  lastActive,
14925
15564
  onChoose
14926
15565
  }) {
14927
- return /* @__PURE__ */ React24.createElement(Box22, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React24.createElement(Box22, { marginBottom: 1 }, /* @__PURE__ */ React24.createElement(Text19, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React24.createElement(Text19, { dimColor: true }, ` \xB7 last active ${relativeTime2(lastActive)}`)), /* @__PURE__ */ React24.createElement(
15566
+ return /* @__PURE__ */ React24.createElement(Box22, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React24.createElement(Box22, { marginBottom: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, ` \xB7 last active ${relativeTime2(lastActive)}`)), /* @__PURE__ */ React24.createElement(
14928
15567
  SingleSelect,
14929
15568
  {
14930
15569
  initialValue: "new",
@@ -14947,7 +15586,7 @@ function SessionPicker({
14947
15586
  ],
14948
15587
  onSubmit: (v) => onChoose(v)
14949
15588
  }
14950
- ), /* @__PURE__ */ React24.createElement(Box22, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text19, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] select")));
15589
+ ), /* @__PURE__ */ React24.createElement(Box22, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] select")));
14951
15590
  }
14952
15591
  function relativeTime2(date) {
14953
15592
  const ms = Date.now() - date.getTime();
@@ -14963,7 +15602,7 @@ function relativeTime2(date) {
14963
15602
  }
14964
15603
 
14965
15604
  // src/cli/ui/Setup.tsx
14966
- import { Box as Box23, Text as Text20, useApp as useApp2 } from "ink";
15605
+ import { Box as Box23, Text as Text21, useApp as useApp2 } from "ink";
14967
15606
  import TextInput from "ink-text-input";
14968
15607
  import React25, { useState as useState11 } from "react";
14969
15608
  function Setup({ onReady }) {
@@ -14989,7 +15628,7 @@ function Setup({ onReady }) {
14989
15628
  }
14990
15629
  onReady(trimmed);
14991
15630
  };
14992
- return /* @__PURE__ */ React25.createElement(Box23, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React25.createElement(Text20, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React25.createElement(
15631
+ return /* @__PURE__ */ React25.createElement(Box23, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React25.createElement(Text21, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React25.createElement(
14993
15632
  TextInput,
14994
15633
  {
14995
15634
  value,
@@ -14998,7 +15637,7 @@ function Setup({ onReady }) {
14998
15637
  mask: "\u2022",
14999
15638
  placeholder: "sk-..."
15000
15639
  }
15001
- )), error ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { color: "red" }, error)) : value ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "(Type /exit to abort.)")));
15640
+ )), error ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { color: "red" }, error)) : value ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "(Type /exit to abort.)")));
15002
15641
  }
15003
15642
 
15004
15643
  // src/cli/commands/chat.tsx
@@ -15139,7 +15778,7 @@ async function chatCommand(opts) {
15139
15778
  const prior = loadSessionMessages(opts.session);
15140
15779
  if (prior.length > 0) {
15141
15780
  const p = sessionPath(opts.session);
15142
- const mtime = existsSync12(p) ? statSync7(p).mtime : /* @__PURE__ */ new Date();
15781
+ const mtime = existsSync13(p) ? statSync7(p).mtime : /* @__PURE__ */ new Date();
15143
15782
  sessionPreview = { messageCount: prior.length, lastActive: mtime };
15144
15783
  }
15145
15784
  } else if (opts.session && opts.forceNew) {
@@ -15172,7 +15811,7 @@ async function chatCommand(opts) {
15172
15811
  // src/cli/commands/code.tsx
15173
15812
  import { basename as basename2, resolve as resolve7 } from "path";
15174
15813
  async function codeCommand(opts = {}) {
15175
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-2OABSPAW.js");
15814
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-LJ44NWSU.js");
15176
15815
  const rootDir = resolve7(opts.dir ?? process.cwd());
15177
15816
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
15178
15817
  const tools = new ToolRegistry();
@@ -15186,6 +15825,10 @@ async function codeCommand(opts = {}) {
15186
15825
  // via ShellConfirm mid-session takes effect on the next shell call
15187
15826
  // instead of waiting for `/new` or a relaunch.
15188
15827
  extraAllowed: () => loadProjectShellAllowed(rootDir),
15828
+ // `yolo` edit-mode disables shell confirmations entirely. Re-read
15829
+ // from config on each dispatch so /mode yolo (or Shift+Tab cycling
15830
+ // through to it) flips the gate live without forcing a relaunch.
15831
+ allowAll: () => loadEditMode() === "yolo",
15189
15832
  jobs: jobs2
15190
15833
  });
15191
15834
  registerPlanTool(tools);
@@ -15212,38 +15855,38 @@ async function codeCommand(opts = {}) {
15212
15855
  }
15213
15856
 
15214
15857
  // src/cli/commands/diff.ts
15215
- import { writeFileSync as writeFileSync7 } from "fs";
15858
+ import { writeFileSync as writeFileSync8 } from "fs";
15216
15859
  import { basename as basename3 } from "path";
15217
15860
  import { render as render2 } from "ink";
15218
15861
  import React29 from "react";
15219
15862
 
15220
15863
  // src/cli/ui/DiffApp.tsx
15221
- import { Box as Box25, Static as Static2, Text as Text22, useApp as useApp3, useInput } from "ink";
15864
+ import { Box as Box25, Static as Static2, Text as Text23, useApp as useApp3, useInput } from "ink";
15222
15865
  import React28, { useState as useState13 } from "react";
15223
15866
 
15224
15867
  // src/cli/ui/RecordView.tsx
15225
- import { Box as Box24, Text as Text21 } from "ink";
15868
+ import { Box as Box24, Text as Text22 } from "ink";
15226
15869
  import React27 from "react";
15227
15870
  function RecordView({ rec, compact: compact2 = false }) {
15228
15871
  const toolArgsMax = compact2 ? 120 : 200;
15229
15872
  const toolContentMax = compact2 ? 200 : 400;
15230
15873
  if (rec.role === "user") {
15231
15874
  const content = rec.content.includes("\n") ? rec.content.split("\n").join("\n ") : rec.content;
15232
- return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text21, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React27.createElement(Text21, null, content));
15875
+ return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React27.createElement(Text22, null, content));
15233
15876
  }
15234
15877
  if (rec.role === "assistant_final") {
15235
- return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text21, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React27.createElement(Text21, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React27.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React27.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React27.createElement(Text21, null, rec.content) : /* @__PURE__ */ React27.createElement(Text21, { dimColor: true, italic: true }, "(tool-call response only)"));
15878
+ return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text22, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React27.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React27.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React27.createElement(Text22, null, rec.content) : /* @__PURE__ */ React27.createElement(Text22, { dimColor: true, italic: true }, "(tool-call response only)"));
15236
15879
  }
15237
15880
  if (rec.role === "tool") {
15238
- return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text21, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React27.createElement(Text21, { dimColor: true }, " args: ", truncate2(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React27.createElement(Text21, { dimColor: true }, " \u2192 ", truncate2(rec.content, toolContentMax)));
15881
+ return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " args: ", truncate2(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " \u2192 ", truncate2(rec.content, toolContentMax)));
15239
15882
  }
15240
15883
  if (rec.role === "error") {
15241
- return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text21, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React27.createElement(Text21, { color: "red" }, rec.error ?? rec.content));
15884
+ return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React27.createElement(Text22, { color: "red" }, rec.error ?? rec.content));
15242
15885
  }
15243
15886
  if (rec.role === "done" || rec.role === "assistant_delta") {
15244
15887
  return null;
15245
15888
  }
15246
- return /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text21, { dimColor: true }, "[", rec.role, "] ", rec.content));
15889
+ return /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, "[", rec.role, "] ", rec.content));
15247
15890
  }
15248
15891
  function CacheBadge({ usage }) {
15249
15892
  const hit = usage.prompt_cache_hit_tokens ?? 0;
@@ -15252,7 +15895,7 @@ function CacheBadge({ usage }) {
15252
15895
  if (total === 0) return null;
15253
15896
  const pct2 = hit / total * 100;
15254
15897
  const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
15255
- return /* @__PURE__ */ React27.createElement(Text21, null, /* @__PURE__ */ React27.createElement(Text21, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React27.createElement(Text21, { color }, pct2.toFixed(1), "%"));
15898
+ return /* @__PURE__ */ React27.createElement(Text22, null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React27.createElement(Text22, { color }, pct2.toFixed(1), "%"));
15256
15899
  }
15257
15900
  function truncate2(s, max) {
15258
15901
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
@@ -15286,7 +15929,7 @@ function DiffApp({ report }) {
15286
15929
  }
15287
15930
  });
15288
15931
  const pair = report.pairs[idx];
15289
- return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column" }, /* @__PURE__ */ React28.createElement(DiffHeader, { report }), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text22, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React28.createElement(Text22, null, pair ? /* @__PURE__ */ React28.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React28.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React28.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React28.createElement(Text22, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React28.createElement(Text22, null, pair.divergenceNote)) : null, /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "j"), "/", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "k"), "/", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "N"), "/", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "g"), "/", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "q"), " ", "quit")));
15932
+ return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column" }, /* @__PURE__ */ React28.createElement(DiffHeader, { report }), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text23, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React28.createElement(Text23, null, pair ? /* @__PURE__ */ React28.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React28.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React28.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React28.createElement(Text23, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React28.createElement(Text23, null, pair.divergenceNote)) : null, /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "j"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "k"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "N"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "g"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "q"), " ", "quit")));
15290
15933
  }
15291
15934
  function DiffHeader({ report }) {
15292
15935
  const a = report.a;
@@ -15304,7 +15947,7 @@ function DiffHeader({ report }) {
15304
15947
  } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
15305
15948
  prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
15306
15949
  }
15307
- return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React28.createElement(Box25, { justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text22, null, /* @__PURE__ */ React28.createElement(Text22, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React28.createElement(Text22, { color: "blue" }, a.label), /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, " vs B="), /* @__PURE__ */ React28.createElement(Text22, { color: "magenta" }, b.label)), /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React28.createElement(Text22, null, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, "cache "), /* @__PURE__ */ React28.createElement(Text22, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text22, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text22, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React28.createElement(Text22, null, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, "cost "), /* @__PURE__ */ React28.createElement(Text22, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text22, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text22, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React28.createElement(Text22, null, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, "model calls "), /* @__PURE__ */ React28.createElement(Text22, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true, italic: true }, prefixLine)) : null);
15950
+ return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React28.createElement(Box25, { justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React28.createElement(Text23, { color: "blue" }, a.label), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " vs B="), /* @__PURE__ */ React28.createElement(Text23, { color: "magenta" }, b.label)), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "cache "), /* @__PURE__ */ React28.createElement(Text23, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text23, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text23, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "cost "), /* @__PURE__ */ React28.createElement(Text23, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text23, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text23, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "model calls "), /* @__PURE__ */ React28.createElement(Text23, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true, italic: true }, prefixLine)) : null);
15308
15951
  }
15309
15952
  function Pane({
15310
15953
  label,
@@ -15320,21 +15963,21 @@ function Pane({
15320
15963
  borderStyle: "single",
15321
15964
  borderColor: headerColor
15322
15965
  },
15323
- /* @__PURE__ */ React28.createElement(Text22, { color: headerColor, bold: true }, label),
15324
- records.length === 0 ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React28.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React28.createElement(RecordView, { key, rec, compact: true }))
15966
+ /* @__PURE__ */ React28.createElement(Text23, { color: headerColor, bold: true }, label),
15967
+ records.length === 0 ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React28.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React28.createElement(RecordView, { key, rec, compact: true }))
15325
15968
  );
15326
15969
  }
15327
15970
  function KindBadge({ kind }) {
15328
15971
  if (kind === "match") {
15329
- return /* @__PURE__ */ React28.createElement(Text22, { color: "green" }, "\u2713 match");
15972
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "green" }, "\u2713 match");
15330
15973
  }
15331
15974
  if (kind === "diverge") {
15332
- return /* @__PURE__ */ React28.createElement(Text22, { color: "yellow" }, "\u2605 diverge");
15975
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "yellow" }, "\u2605 diverge");
15333
15976
  }
15334
15977
  if (kind === "only_in_a") {
15335
- return /* @__PURE__ */ React28.createElement(Text22, { color: "blue" }, "\u2190 only in A");
15978
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "blue" }, "\u2190 only in A");
15336
15979
  }
15337
- return /* @__PURE__ */ React28.createElement(Text22, { color: "magenta" }, "\u2192 only in B");
15980
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "magenta" }, "\u2192 only in B");
15338
15981
  }
15339
15982
  function paneRecords(pair, side) {
15340
15983
  if (!pair) return [];
@@ -15359,7 +16002,7 @@ async function diffCommand(opts) {
15359
16002
  if (wantMarkdown) {
15360
16003
  console.log(renderSummaryTable(report));
15361
16004
  const md = renderMarkdown(report);
15362
- writeFileSync7(opts.mdPath, md, "utf8");
16005
+ writeFileSync8(opts.mdPath, md, "utf8");
15363
16006
  console.log(`
15364
16007
  markdown report written to ${opts.mdPath}`);
15365
16008
  return;
@@ -15509,7 +16152,7 @@ import { render as render3 } from "ink";
15509
16152
  import React31 from "react";
15510
16153
 
15511
16154
  // src/cli/ui/ReplayApp.tsx
15512
- import { Box as Box26, Static as Static3, Text as Text23, useApp as useApp4, useInput as useInput2 } from "ink";
16155
+ import { Box as Box26, Static as Static3, Text as Text24, useApp as useApp4, useInput as useInput2 } from "ink";
15513
16156
  import React30, { useMemo as useMemo4, useState as useState14 } from "react";
15514
16157
  function ReplayApp({ meta, pages }) {
15515
16158
  const { exit: exit2 } = useApp4();
@@ -15557,7 +16200,7 @@ function ReplayApp({ meta, pages }) {
15557
16200
  model: cumStats.models[0] ?? meta?.model ?? "?",
15558
16201
  prefixHash
15559
16202
  }
15560
- ), /* @__PURE__ */ React30.createElement(Box26, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React30.createElement(Box26, { justifyContent: "space-between" }, /* @__PURE__ */ React30.createElement(Text23, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React30.createElement(Text23, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React30.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React30.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React30.createElement(Text23, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React30.createElement(Box26, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React30.createElement(Text23, { dimColor: true }, /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "j"), "/", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "k"), "/", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "q"), " quit")));
16203
+ ), /* @__PURE__ */ React30.createElement(Box26, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React30.createElement(Box26, { justifyContent: "space-between" }, /* @__PURE__ */ React30.createElement(Text24, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React30.createElement(Text24, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React30.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React30.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React30.createElement(Text24, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React30.createElement(Box26, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React30.createElement(Text24, { dimColor: true }, /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "j"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "k"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "q"), " quit")));
15561
16204
  }
15562
16205
 
15563
16206
  // src/cli/commands/replay.ts
@@ -15736,7 +16379,7 @@ async function runCommand2(opts) {
15736
16379
  system: opts.system,
15737
16380
  toolSpecs: tools?.specs()
15738
16381
  });
15739
- const loop = new CacheFirstLoop({
16382
+ const loop2 = new CacheFirstLoop({
15740
16383
  client,
15741
16384
  prefix,
15742
16385
  tools,
@@ -15761,7 +16404,7 @@ async function runCommand2(opts) {
15761
16404
  });
15762
16405
  }
15763
16406
  try {
15764
- for await (const ev of loop.step(opts.task)) {
16407
+ for await (const ev of loop2.step(opts.task)) {
15765
16408
  if (ev.role === "assistant_delta" && ev.content) process.stdout.write(ev.content);
15766
16409
  if (ev.role === "tool") process.stdout.write(`
15767
16410
  [tool ${ev.toolName}] ${ev.content}
@@ -15780,7 +16423,7 @@ async function runCommand2(opts) {
15780
16423
  } finally {
15781
16424
  transcriptStream?.end();
15782
16425
  }
15783
- const s = loop.stats.summary();
16426
+ const s = loop2.stats.summary();
15784
16427
  process.stdout.write(
15785
16428
  `
15786
16429
  \u2014 turns:${s.turns} cache:${(s.cacheHitRatio * 100).toFixed(1)}% cost:$${s.totalCostUsd.toFixed(6)} save-vs-claude:${s.savingsVsClaudePct.toFixed(1)}%
@@ -15877,7 +16520,7 @@ import { render as render4 } from "ink";
15877
16520
  import React33 from "react";
15878
16521
 
15879
16522
  // src/cli/ui/Wizard.tsx
15880
- import { Box as Box27, Text as Text24, useApp as useApp5, useInput as useInput3 } from "ink";
16523
+ import { Box as Box27, Text as Text25, useApp as useApp5, useInput as useInput3 } from "ink";
15881
16524
  import TextInput2 from "ink-text-input";
15882
16525
  import React32, { useState as useState15 } from "react";
15883
16526
 
@@ -15951,7 +16594,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
15951
16594
  setStep("mcp");
15952
16595
  }
15953
16596
  }
15954
- ), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] confirm \xB7 [Esc] cancel")));
16597
+ ), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] confirm \xB7 [Esc] cancel")));
15955
16598
  }
15956
16599
  if (step === "mcp") {
15957
16600
  return /* @__PURE__ */ React32.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(
@@ -16005,8 +16648,8 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16005
16648
  }
16006
16649
  ), specs.map((spec, i) => (
16007
16650
  // biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
16008
- /* @__PURE__ */ React32.createElement(Box27, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "\xB7 ", spec))
16009
- )), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { color: "red" }, error)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "[Enter] save \xB7 [Esc] cancel"))), /* @__PURE__ */ React32.createElement(
16651
+ /* @__PURE__ */ React32.createElement(Box27, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "\xB7 ", spec))
16652
+ )), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[Enter] save \xB7 [Esc] cancel"))), /* @__PURE__ */ React32.createElement(
16010
16653
  ReviewConfirm,
16011
16654
  {
16012
16655
  onConfirm: () => {
@@ -16032,7 +16675,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16032
16675
  }
16033
16676
  ));
16034
16677
  }
16035
- return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text24, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "[Enter] to exit")), /* @__PURE__ */ React32.createElement(ExitOnEnter, { onExit: exit2 }));
16678
+ return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[Enter] to exit")), /* @__PURE__ */ React32.createElement(ExitOnEnter, { onExit: exit2 }));
16036
16679
  }
16037
16680
  function ApiKeyStep({
16038
16681
  onSubmit,
@@ -16040,7 +16683,7 @@ function ApiKeyStep({
16040
16683
  onError
16041
16684
  }) {
16042
16685
  const [value, setValue] = useState15("");
16043
- return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text24, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React32.createElement(
16686
+ return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React32.createElement(
16044
16687
  TextInput2,
16045
16688
  {
16046
16689
  value,
@@ -16057,7 +16700,7 @@ function ApiKeyStep({
16057
16700
  mask: "\u2022",
16058
16701
  placeholder: "sk-..."
16059
16702
  }
16060
- )), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { color: "red" }, error)) : value ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "preview: ", redactKey(value))) : null);
16703
+ )), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : value ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "preview: ", redactKey(value))) : null);
16061
16704
  }
16062
16705
  function McpArgsStep({
16063
16706
  entry,
@@ -16066,7 +16709,7 @@ function McpArgsStep({
16066
16709
  onError
16067
16710
  }) {
16068
16711
  const [value, setValue] = useState15("");
16069
- return /* @__PURE__ */ React32.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column" }, /* @__PURE__ */ React32.createElement(Text24, null, entry.summary), entry.note ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, null, "Required parameter: "), /* @__PURE__ */ React32.createElement(Text24, { bold: true }, entry.userArgs)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React32.createElement(
16712
+ return /* @__PURE__ */ React32.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column" }, /* @__PURE__ */ React32.createElement(Text25, null, entry.summary), entry.note ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Required parameter: "), /* @__PURE__ */ React32.createElement(Text25, { bold: true }, entry.userArgs)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React32.createElement(
16070
16713
  TextInput2,
16071
16714
  {
16072
16715
  value,
@@ -16082,7 +16725,7 @@ function McpArgsStep({
16082
16725
  },
16083
16726
  placeholder: placeholderFor(entry)
16084
16727
  }
16085
- )), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { color: "red" }, error)) : null));
16728
+ )), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : null));
16086
16729
  }
16087
16730
  function ReviewConfirm({ onConfirm }) {
16088
16731
  useInput3((_i, key) => {
@@ -16102,10 +16745,10 @@ function StepFrame({
16102
16745
  total,
16103
16746
  children
16104
16747
  }) {
16105
- return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React32.createElement(Text24, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1, flexDirection: "column" }, children));
16748
+ return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1, flexDirection: "column" }, children));
16106
16749
  }
16107
16750
  function SummaryLine({ label, value }) {
16108
- return /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text24, null, label.padEnd(12)), /* @__PURE__ */ React32.createElement(Text24, { bold: true }, value));
16751
+ return /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text25, null, label.padEnd(12)), /* @__PURE__ */ React32.createElement(Text25, { bold: true }, value));
16109
16752
  }
16110
16753
  function presetItems() {
16111
16754
  return ["fast", "smart", "max"].map((name) => ({
@@ -16295,6 +16938,16 @@ function resolveSession(flag, configSession) {
16295
16938
  if (typeof configSession === "string" && configSession.length > 0) return configSession;
16296
16939
  return "default";
16297
16940
  }
16941
+ function resolveContinueFlag(flag, fallbackSession, getLatestSession, warn = () => {
16942
+ }) {
16943
+ if (!flag) return { session: fallbackSession, forceResume: false };
16944
+ const latest = getLatestSession();
16945
+ if (!latest) {
16946
+ warn("\u25B8 -c/--continue: no saved sessions yet \u2014 starting a fresh one.");
16947
+ return { session: fallbackSession, forceResume: false };
16948
+ }
16949
+ return { session: latest.name, forceResume: true };
16950
+ }
16298
16951
 
16299
16952
  // src/cli/index.ts
16300
16953
  var DEFAULT_SYSTEM = `You are Reasonix, a helpful DeepSeek-powered assistant. Be concise and accurate. Use tools when available.
@@ -16319,21 +16972,32 @@ The signal isn't a topic list \u2014 it's: "if I'm wrong about this, is it becau
16319
16972
 
16320
16973
  ${ESCALATION_CONTRACT}`;
16321
16974
  var program = new Command();
16322
- program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION);
16323
- program.action(async () => {
16975
+ program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION).option(
16976
+ "-c, --continue",
16977
+ "Resume the most recently used chat session without showing the picker."
16978
+ );
16979
+ program.action(async (opts) => {
16324
16980
  const cfg = readConfig();
16325
16981
  if (!cfg.setupCompleted) {
16326
16982
  await setupCommand({});
16327
16983
  return;
16328
16984
  }
16329
16985
  const defaults = resolveDefaults({});
16986
+ const continueOpts = resolveContinueFlag(
16987
+ opts.continue,
16988
+ defaults.session,
16989
+ () => listSessions()[0],
16990
+ (msg) => process.stderr.write(`${msg}
16991
+ `)
16992
+ );
16330
16993
  await chatCommand({
16331
16994
  model: defaults.model,
16332
16995
  system: applyMemoryStack(DEFAULT_SYSTEM, process.cwd()),
16333
16996
  harvest: defaults.harvest,
16334
16997
  branch: defaults.branch,
16335
- session: defaults.session,
16336
- mcp: defaults.mcp
16998
+ session: continueOpts.session,
16999
+ mcp: defaults.mcp,
17000
+ forceResume: continueOpts.forceResume
16337
17001
  });
16338
17002
  });
16339
17003
  program.command("setup").description("Interactive wizard \u2014 API key, preset, MCP servers. Re-run any time to reconfigure.").action(async () => {
@@ -16365,7 +17029,10 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
16365
17029
  "--branch <n>",
16366
17030
  "Self-consistency: run N parallel samples per turn (N\xD7 cost). Manual only \u2014 never auto-enabled.",
16367
17031
  (v) => Number.parseInt(v, 10)
16368
- ).option("--session <name>", "Use a named session (default: from config, usually 'default').").option("--no-session", "Disable session persistence for this run (ephemeral chat)").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option("-n, --new", "Skip the session picker \u2014 always wipe prior messages and start fresh").option(
17032
+ ).option("--session <name>", "Use a named session (default: from config, usually 'default').").option("--no-session", "Disable session persistence for this run (ephemeral chat)").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option(
17033
+ "-c, --continue",
17034
+ "Resume the most-recently-used session (any name) without showing the picker."
17035
+ ).option("-n, --new", "Skip the session picker \u2014 always wipe prior messages and start fresh").option(
16369
17036
  "--mcp <spec>",
16370
17037
  'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE transport). Overrides config.mcp when provided.',
16371
17038
  (value, previous = []) => [...previous, value],
@@ -16383,16 +17050,23 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
16383
17050
  preset: opts.preset,
16384
17051
  noConfig: opts.config === false
16385
17052
  });
17053
+ const continueOpts = opts.resume ? { session: defaults.session, forceResume: true } : resolveContinueFlag(
17054
+ opts.continue,
17055
+ defaults.session,
17056
+ () => listSessions()[0],
17057
+ (msg) => process.stderr.write(`${msg}
17058
+ `)
17059
+ );
16386
17060
  await chatCommand({
16387
17061
  model: defaults.model,
16388
17062
  system: applyMemoryStack(opts.system, process.cwd()),
16389
17063
  transcript: opts.transcript,
16390
17064
  harvest: defaults.harvest,
16391
17065
  branch: defaults.branch,
16392
- session: defaults.session,
17066
+ session: continueOpts.session,
16393
17067
  mcp: defaults.mcp,
16394
17068
  mcpPrefix: opts.mcpPrefix,
16395
- forceResume: !!opts.resume,
17069
+ forceResume: continueOpts.forceResume,
16396
17070
  forceNew: !!opts.new
16397
17071
  });
16398
17072
  });