reasonix 0.8.0 → 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-DVBNMXA6.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, {
@@ -7116,7 +7218,7 @@ 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
@@ -9467,10 +9569,9 @@ function ModeStatusBar({
9467
9569
  if (planMode) {
9468
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);
9469
9571
  }
9470
- const isAuto = editMode === "auto";
9471
- const label = isAuto ? "AUTO" : "REVIEW";
9472
- const bg = isAuto ? "magenta" : "cyan";
9473
- 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";
9474
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);
9475
9576
  }
9476
9577
  function ModeBarFrame({ children }) {
@@ -10927,13 +11028,21 @@ function describeRepair(repair) {
10927
11028
  }
10928
11029
 
10929
11030
  // src/cli/ui/hash-memory.ts
10930
- import { appendFileSync as appendFileSync3, existsSync as existsSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
10931
- import { join as join12 } from "path";
10932
- var NEW_FILE_HEADER = `# Reasonix project memory
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
10933
11035
 
10934
11036
  Notes the user pinned via the \`#\` prompt prefix. The whole file is
10935
11037
  loaded into the immutable system prefix every session \u2014 keep it terse.
10936
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
+
10937
11046
  `;
10938
11047
  function detectHashMemory(text) {
10939
11048
  if (text.startsWith("\\#")) {
@@ -10941,18 +11050,36 @@ function detectHashMemory(text) {
10941
11050
  }
10942
11051
  if (!text.startsWith("#")) return null;
10943
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
+ }
10944
11060
  const body = text.slice(1).trim();
10945
11061
  if (!body) return null;
10946
11062
  return { kind: "memory", note: body };
10947
11063
  }
10948
11064
  function appendProjectMemory(rootDir, note) {
10949
- const path = join12(rootDir, PROJECT_MEMORY_FILE);
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) {
10950
11076
  const trimmed = note.trim();
10951
11077
  if (!trimmed) throw new Error("note body cannot be empty");
10952
11078
  const bullet = `- ${trimmed}
10953
11079
  `;
10954
11080
  if (!existsSync11(path)) {
10955
- writeFileSync7(path, `${NEW_FILE_HEADER}${bullet}`, "utf8");
11081
+ mkdirSync8(dirname10(path), { recursive: true });
11082
+ writeFileSync7(path, `${newFileHeader}${bullet}`, "utf8");
10956
11083
  return { path, created: true };
10957
11084
  }
10958
11085
  let prefix = "";
@@ -10965,6 +11092,65 @@ function appendProjectMemory(rootDir, note) {
10965
11092
  return { path, created: false };
10966
11093
  }
10967
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
+
10968
11154
  // src/cli/ui/mcp-browse.ts
10969
11155
  function formatResourceList(servers) {
10970
11156
  const lines = [];
@@ -11270,6 +11456,11 @@ var SLASH_COMMANDS = [
11270
11456
  { cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
11271
11457
  { cmd: "clear", summary: "clear visible scrollback only (log/context kept)" },
11272
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
+ },
11273
11464
  { cmd: "exit", summary: "quit the TUI" },
11274
11465
  // Code-mode only
11275
11466
  {
@@ -11284,6 +11475,11 @@ var SLASH_COMMANDS = [
11284
11475
  summary: "drop pending edit blocks without writing (no arg \u2192 all; indices \u2192 that subset)",
11285
11476
  contextual: "code"
11286
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
+ },
11287
11483
  { cmd: "undo", summary: "roll back the last applied edit batch", contextual: "code" },
11288
11484
  {
11289
11485
  cmd: "history",
@@ -11316,10 +11512,10 @@ var SLASH_COMMANDS = [
11316
11512
  },
11317
11513
  {
11318
11514
  cmd: "mode",
11319
- argsHint: "[review|auto]",
11320
- 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.",
11321
11517
  contextual: "code",
11322
- argCompleter: ["review", "auto"]
11518
+ argCompleter: ["review", "auto", "yolo"]
11323
11519
  },
11324
11520
  { cmd: "jobs", summary: "list background jobs started by run_background", contextual: "code" },
11325
11521
  {
@@ -11500,7 +11696,7 @@ function pad(s, width, align = "left") {
11500
11696
  }
11501
11697
 
11502
11698
  // src/cli/ui/slash/handlers/admin.ts
11503
- var hooks = (args, loop, ctx) => {
11699
+ var hooks = (args, loop2, ctx) => {
11504
11700
  const sub = (args[0] ?? "").toLowerCase();
11505
11701
  if (sub === "reload") {
11506
11702
  if (!ctx.reloadHooks) {
@@ -11516,7 +11712,7 @@ var hooks = (args, loop, ctx) => {
11516
11712
  info: "usage: /hooks list active hooks\n /hooks reload re-read settings.json files"
11517
11713
  };
11518
11714
  }
11519
- const all = loop.hooks;
11715
+ const all = loop2.hooks;
11520
11716
  const projPath = ctx.codeRoot ? projectSettingsPath(ctx.codeRoot) : void 0;
11521
11717
  const globPath = globalSettingsPath();
11522
11718
  if (all.length === 0) {
@@ -11618,8 +11814,8 @@ var clear = () => ({
11618
11814
  clear: true,
11619
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."
11620
11816
  });
11621
- var resetLog = (_args, loop) => {
11622
- const { dropped } = loop.clearLog();
11817
+ var resetLog = (_args, loop2) => {
11818
+ const { dropped } = loop2.clearLog();
11623
11819
  return {
11624
11820
  clear: true,
11625
11821
  info: `\u25B8 new conversation \u2014 dropped ${dropped} message(s) from context. Same session, fresh slate.`
@@ -11647,9 +11843,13 @@ var keys = () => ({
11647
11843
  " /<name> slash command; Tab/Enter picks from the suggestion list",
11648
11844
  " @<path> inline a file under [Referenced files] (code mode).",
11649
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.",
11650
11848
  " !<cmd> run <cmd> as shell in the sandbox root; output goes into context",
11651
11849
  " so the model sees it next turn. No allowlist gate.",
11652
- " #<note> append <note> to REASONIX.md so it pins into every future session.",
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.",
11653
11853
  " Use `\\#literal` if you actually want a `#` heading sent to the model.",
11654
11854
  "",
11655
11855
  "Pickers (slash + @-mention):",
@@ -11691,13 +11891,14 @@ var help = () => ({
11691
11891
  " /retry truncate & resend your last message (fresh sample from the model)",
11692
11892
  " /apply [N|1,3|1-4] (code mode) commit pending edit blocks (no arg \u2192 all; index \u2192 subset)",
11693
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)",
11694
11895
  " /undo (code mode) roll back the latest non-undone edit batch",
11695
11896
  " /history (code mode) list every edit batch this session",
11696
11897
  " /show [id] (code mode) dump a stored edit diff (newest when id omitted)",
11697
11898
  ' /commit "msg" (code mode) git add -A && git commit -m "msg"',
11698
11899
  " /plan [on|off] (code mode) toggle read-only plan mode; writes gated behind submit_plan + your approval",
11699
11900
  " /apply-plan (code mode) force-approve pending/in-text plan (fallback)",
11700
- " /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.",
11701
11902
  " /jobs (code mode) list background processes (run_background) \u2014 running and exited",
11702
11903
  " /kill <id> (code mode) stop a background job by id (SIGTERM \u2192 SIGKILL)",
11703
11904
  " /logs <id> [lines] (code mode) tail a background job's output (default 80 lines)",
@@ -11705,6 +11906,7 @@ var help = () => ({
11705
11906
  " /forget delete the current session from disk",
11706
11907
  " /new start fresh: drop all context + clear scrollback",
11707
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.",
11708
11910
  " /exit quit",
11709
11911
  "",
11710
11912
  "Shell shortcut:",
@@ -11714,15 +11916,22 @@ var help = () => ({
11714
11916
  " Example: !git status !ls src/ !npm test",
11715
11917
  "",
11716
11918
  "Quick memory:",
11717
- " #<note> append <note> to REASONIX.md (pinned into every",
11718
- " future session's prefix). Faster than /memory for",
11719
- " one-liners. Example: #always use pnpm not npm",
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.",
11720
11924
  " Use `\\#text` to send a literal `#text` to the model.",
11721
11925
  "",
11722
11926
  "File references (code mode):",
11723
11927
  " @path/to/file inline file content under [Referenced files] on send.",
11724
11928
  " Type `@` to open the picker (\u2191\u2193 navigate, Tab/Enter pick).",
11725
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
+ "",
11726
11935
  "Presets (branch + harvest are NEVER auto-enabled \u2014 opt-in only):",
11727
11936
  " fast v4-flash \xB7 effort=high cheapest \xB7 quick Q&A, one-line edits",
11728
11937
  " smart v4-flash \xB7 effort=max \u2190 default \xB7 day-to-day coding",
@@ -11742,8 +11951,8 @@ var help = () => ({
11742
11951
  var setup = () => ({
11743
11952
  info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
11744
11953
  });
11745
- var retry = (_args, loop) => {
11746
- const prev = loop.retryLastUser();
11954
+ var retry = (_args, loop2) => {
11955
+ const prev = loop2.retryLastUser();
11747
11956
  if (!prev) {
11748
11957
  return {
11749
11958
  info: "nothing to retry \u2014 no prior user message in this session's log."
@@ -11755,6 +11964,37 @@ var retry = (_args, loop) => {
11755
11964
  resubmit: prev
11756
11965
  };
11757
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
+ };
11758
11998
  var handlers2 = {
11759
11999
  exit,
11760
12000
  quit: exit,
@@ -11765,7 +12005,8 @@ var handlers2 = {
11765
12005
  help,
11766
12006
  "?": help,
11767
12007
  setup,
11768
- retry
12008
+ retry,
12009
+ loop
11769
12010
  };
11770
12011
 
11771
12012
  // src/cli/ui/slash/helpers.ts
@@ -11961,15 +12202,17 @@ var mode = (args, _loop, ctx) => {
11961
12202
  let target;
11962
12203
  if (raw === "review") target = "review";
11963
12204
  else if (raw === "auto") target = "auto";
12205
+ else if (raw === "yolo") target = "yolo";
11964
12206
  else if (raw === "") {
11965
- target = current === "auto" ? "review" : "auto";
12207
+ target = current === "review" ? "auto" : current === "auto" ? "yolo" : "review";
11966
12208
  } else {
11967
- return { info: "usage: /mode <review|auto> (Shift+Tab also cycles)" };
12209
+ return {
12210
+ info: "usage: /mode <review|auto|yolo> (Shift+Tab also cycles)"
12211
+ };
11968
12212
  }
11969
12213
  ctx.setEditMode(target);
11970
- return {
11971
- 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)"
11972
- };
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 };
11973
12216
  };
11974
12217
  var commit = (args, _loop, ctx) => {
11975
12218
  if (!ctx.codeRoot) {
@@ -11986,6 +12229,14 @@ var commit = (args, _loop, ctx) => {
11986
12229
  }
11987
12230
  return runGitCommit(ctx.codeRoot, message);
11988
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
+ };
11989
12240
  var handlers3 = {
11990
12241
  undo,
11991
12242
  history,
@@ -11996,7 +12247,8 @@ var handlers3 = {
11996
12247
  "apply-plan": applyPlan,
11997
12248
  applyplan: applyPlan,
11998
12249
  mode,
11999
- commit
12250
+ commit,
12251
+ walk: walk2
12000
12252
  };
12001
12253
 
12002
12254
  // src/cli/ui/slash/handlers/jobs.ts
@@ -12061,10 +12313,10 @@ var handlers4 = {
12061
12313
  };
12062
12314
 
12063
12315
  // src/cli/ui/slash/handlers/mcp.ts
12064
- var mcp = (_args, loop, ctx) => {
12316
+ var mcp = (_args, loop2, ctx) => {
12065
12317
  const servers = ctx.mcpServers ?? [];
12066
12318
  const specs = ctx.mcpSpecs ?? [];
12067
- const toolSpecs = loop.prefix.toolSpecs ?? [];
12319
+ const toolSpecs = loop2.prefix.toolSpecs ?? [];
12068
12320
  if (servers.length === 0 && specs.length === 0 && toolSpecs.length === 0) {
12069
12321
  return {
12070
12322
  info: 'no MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp "<spec>". `reasonix mcp list` shows the catalog.'
@@ -12253,14 +12505,14 @@ var memory = (args, _loop, ctx) => {
12253
12505
  var handlers6 = { memory };
12254
12506
 
12255
12507
  // src/cli/ui/slash/handlers/model.ts
12256
- var model = (args, loop, ctx) => {
12508
+ var model = (args, loop2, ctx) => {
12257
12509
  const id = args[0];
12258
12510
  const known = ctx.models ?? null;
12259
12511
  if (!id) {
12260
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";
12261
12513
  return { info: `usage: /model <id> (${hint})` };
12262
12514
  }
12263
- loop.configure({ model: id });
12515
+ loop2.configure({ model: id });
12264
12516
  if (known && known.length > 0 && !known.includes(id)) {
12265
12517
  return {
12266
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.)`
@@ -12268,7 +12520,7 @@ var model = (args, loop, ctx) => {
12268
12520
  }
12269
12521
  return { info: `model \u2192 ${id}` };
12270
12522
  };
12271
- var models = (_args, loop, ctx) => {
12523
+ var models = (_args, loop2, ctx) => {
12272
12524
  const list = ctx.models ?? null;
12273
12525
  if (list === null) {
12274
12526
  ctx.refreshModels?.();
@@ -12281,7 +12533,7 @@ var models = (_args, loop, ctx) => {
12281
12533
  info: "DeepSeek /models returned an empty list. Try /models again, or check your account status at api-docs.deepseek.com."
12282
12534
  };
12283
12535
  }
12284
- const current = loop.model;
12536
+ const current = loop2.model;
12285
12537
  const lines = list.map((id) => id === current ? `\u25B8 ${id} (current)` : ` ${id}`);
12286
12538
  return {
12287
12539
  info: [
@@ -12293,18 +12545,18 @@ var models = (_args, loop, ctx) => {
12293
12545
  ].join("\n")
12294
12546
  };
12295
12547
  };
12296
- var harvest2 = (args, loop) => {
12548
+ var harvest2 = (args, loop2) => {
12297
12549
  const arg = (args[0] ?? "").toLowerCase();
12298
- const on = arg === "" ? !loop.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
12299
- loop.configure({ harvest: on });
12300
- if (loop.harvestEnabled) {
12550
+ const on = arg === "" ? !loop2.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
12551
+ loop2.configure({ harvest: on });
12552
+ if (loop2.harvestEnabled) {
12301
12553
  return {
12302
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)"
12303
12555
  };
12304
12556
  }
12305
12557
  return { info: "harvest \u2192 off" };
12306
12558
  };
12307
- var preset = (args, loop) => {
12559
+ var preset = (args, loop2) => {
12308
12560
  const name = (args[0] ?? "").toLowerCase();
12309
12561
  const applyAndPersist = (effort2) => {
12310
12562
  try {
@@ -12313,7 +12565,7 @@ var preset = (args, loop) => {
12313
12565
  }
12314
12566
  };
12315
12567
  if (name === "fast" || name === "default") {
12316
- loop.configure({
12568
+ loop2.configure({
12317
12569
  model: "deepseek-v4-flash",
12318
12570
  reasoningEffort: "high",
12319
12571
  harvest: false,
@@ -12323,7 +12575,7 @@ var preset = (args, loop) => {
12323
12575
  return { info: "preset \u2192 fast (v4-flash \xB7 effort=high \xB7 cheapest)" };
12324
12576
  }
12325
12577
  if (name === "smart") {
12326
- loop.configure({
12578
+ loop2.configure({
12327
12579
  model: "deepseek-v4-flash",
12328
12580
  reasoningEffort: "max",
12329
12581
  harvest: false,
@@ -12333,7 +12585,7 @@ var preset = (args, loop) => {
12333
12585
  return { info: "preset \u2192 smart (v4-flash \xB7 effort=max \xB7 default \xB7 ~1.5\xD7 fast)" };
12334
12586
  }
12335
12587
  if (name === "max" || name === "best") {
12336
- loop.configure({
12588
+ loop2.configure({
12337
12589
  model: "deepseek-v4-pro",
12338
12590
  reasoningEffort: "max",
12339
12591
  harvest: false,
@@ -12346,10 +12598,10 @@ var preset = (args, loop) => {
12346
12598
  }
12347
12599
  return { info: "usage: /preset <fast|smart|max>" };
12348
12600
  };
12349
- var branch = (args, loop) => {
12601
+ var branch = (args, loop2) => {
12350
12602
  const raw = (args[0] ?? "").toLowerCase();
12351
12603
  if (raw === "" || raw === "off" || raw === "0" || raw === "1") {
12352
- loop.configure({ branch: 1 });
12604
+ loop2.configure({ branch: 1 });
12353
12605
  return { info: "branch \u2192 off" };
12354
12606
  }
12355
12607
  const n = Number.parseInt(raw, 10);
@@ -12359,36 +12611,36 @@ var branch = (args, loop) => {
12359
12611
  if (n > 8) {
12360
12612
  return { info: "branch budget capped at 8 to prevent runaway cost" };
12361
12613
  }
12362
- loop.configure({ branch: n });
12614
+ loop2.configure({ branch: n });
12363
12615
  return {
12364
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)`
12365
12617
  };
12366
12618
  };
12367
- var effort = (args, loop) => {
12619
+ var effort = (args, loop2) => {
12368
12620
  const raw = (args[0] ?? "").toLowerCase();
12369
12621
  if (raw === "") {
12370
12622
  return {
12371
- 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)`
12372
12624
  };
12373
12625
  }
12374
12626
  if (raw !== "high" && raw !== "max") {
12375
12627
  return { info: "usage: /effort <high|max>" };
12376
12628
  }
12377
- loop.configure({ reasoningEffort: raw });
12629
+ loop2.configure({ reasoningEffort: raw });
12378
12630
  try {
12379
12631
  saveReasoningEffort(raw);
12380
12632
  } catch {
12381
12633
  }
12382
12634
  return { info: `reasoning_effort \u2192 ${raw} (persisted)` };
12383
12635
  };
12384
- var pro = (args, loop, ctx) => {
12636
+ var pro = (args, loop2, ctx) => {
12385
12637
  const arg = (args[0] ?? "").toLowerCase();
12386
12638
  if (arg === "off" || arg === "cancel" || arg === "disarm") {
12387
- if (!loop.proArmed) {
12639
+ if (!loop2.proArmed) {
12388
12640
  return { info: "nothing armed \u2014 /pro with no args will arm pro for your next turn" };
12389
12641
  }
12390
12642
  if (ctx.disarmPro) ctx.disarmPro();
12391
- else loop.disarmPro();
12643
+ else loop2.disarmPro();
12392
12644
  return { info: "\u25B8 /pro disarmed \u2014 next turn falls back to the current preset" };
12393
12645
  }
12394
12646
  if (arg && arg !== "on" && arg !== "arm") {
@@ -12397,7 +12649,7 @@ var pro = (args, loop, ctx) => {
12397
12649
  };
12398
12650
  }
12399
12651
  if (ctx.armPro) ctx.armPro();
12400
- else loop.armProForNextTurn();
12652
+ else loop2.armProForNextTurn();
12401
12653
  return {
12402
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.`
12403
12655
  };
@@ -12414,8 +12666,8 @@ var handlers7 = {
12414
12666
  };
12415
12667
 
12416
12668
  // src/cli/ui/slash/handlers/observability.ts
12417
- var think = (_args, loop) => {
12418
- const raw = loop.scratch.reasoning;
12669
+ var think = (_args, loop2) => {
12670
+ const raw = loop2.scratch.reasoning;
12419
12671
  if (!raw || !raw.trim()) {
12420
12672
  return {
12421
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."
@@ -12457,10 +12709,10 @@ var tool = (args, _loop, ctx) => {
12457
12709
  ${entry.text}`
12458
12710
  };
12459
12711
  };
12460
- var context = (_args, loop) => {
12461
- const systemTokens = countTokens(loop.prefix.system);
12462
- const toolsTokens = countTokens(JSON.stringify(loop.prefix.toolSpecs));
12463
- 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();
12464
12716
  let userTokens = 0;
12465
12717
  let assistantTokens = 0;
12466
12718
  let toolResultTokens = 0;
@@ -12485,7 +12737,7 @@ var context = (_args, loop) => {
12485
12737
  }
12486
12738
  const logTokens = userTokens + assistantTokens + toolResultTokens + toolCallTokens;
12487
12739
  const total = systemTokens + toolsTokens + logTokens;
12488
- const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
12740
+ const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop2.model] ?? DEFAULT_CONTEXT_TOKENS;
12489
12741
  const pct2 = (n) => total > 0 ? `${Math.round(n / total * 100)}%`.padStart(4) : " 0%";
12490
12742
  const row2 = (label, n, note = "") => ` ${label.padEnd(20)}${compactNum(n).padStart(8)} tokens ${pct2(n)}${note ? ` ${note}` : ""}`;
12491
12743
  const lines = [
@@ -12494,7 +12746,7 @@ var context = (_args, loop) => {
12494
12746
  )}% of window)`,
12495
12747
  "",
12496
12748
  row2("system prompt", systemTokens),
12497
- row2("tool specs", toolsTokens, `(${loop.prefix.toolSpecs.length} tools)`),
12749
+ row2("tool specs", toolsTokens, `(${loop2.prefix.toolSpecs.length} tools)`),
12498
12750
  row2("log (all turns)", logTokens, `(${entries.length} messages)`),
12499
12751
  ` user ${compactNum(userTokens).padStart(8)} tokens`,
12500
12752
  ` assistant ${compactNum(assistantTokens).padStart(8)} tokens`,
@@ -12517,23 +12769,23 @@ var context = (_args, loop) => {
12517
12769
  );
12518
12770
  return { info: lines.join("\n") };
12519
12771
  };
12520
- var status = (_args, loop, ctx) => {
12521
- const branchBudget = loop.branchOptions.budget ?? 1;
12522
- const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
12523
- 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;
12524
12776
  const ctxPct = ctxMax > 0 ? Math.round(lastPromptTokens / ctxMax * 100) : 0;
12525
12777
  const ctxLine = lastPromptTokens > 0 ? ` ctx ${compactNum(lastPromptTokens)}/${compactNum(ctxMax)} (${ctxPct}%)` : " ctx no turns yet";
12526
12778
  const pending = ctx.pendingEditCount ?? 0;
12527
- 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)";
12528
12780
  const mcpCount = ctx.mcpSpecs?.length ?? 0;
12529
- const toolCount = loop.prefix.toolSpecs.length;
12781
+ const toolCount = loop2.prefix.toolSpecs.length;
12530
12782
  const mcpLine = ` mcp ${mcpCount} server(s), ${toolCount} tool(s) in registry`;
12531
12783
  const pendingLine = pending > 0 ? ` edits ${pending} pending (/apply to commit, /discard to drop)` : "";
12532
12784
  const planLine = ctx.planMode ? " plan ON \u2014 writes gated (submit_plan + approval)" : "";
12533
- 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)" : "";
12534
12786
  const lines = [
12535
- ` model ${loop.model}`,
12536
- ` 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}`,
12537
12789
  ctxLine,
12538
12790
  mcpLine,
12539
12791
  sessionLine
@@ -12543,10 +12795,10 @@ var status = (_args, loop, ctx) => {
12543
12795
  if (modeLine) lines.push(modeLine);
12544
12796
  return { info: lines.join("\n") };
12545
12797
  };
12546
- var compact = (args, loop) => {
12798
+ var compact = (args, loop2) => {
12547
12799
  const tight = Number.parseInt(args[0] ?? "", 10);
12548
12800
  const cap = Number.isFinite(tight) && tight >= 100 ? tight : 4e3;
12549
- const { healedCount, tokensSaved, charsSaved } = loop.compact(cap);
12801
+ const { healedCount, tokensSaved, charsSaved } = loop2.compact(cap);
12550
12802
  if (healedCount === 0) {
12551
12803
  return {
12552
12804
  info: `\u25B8 nothing to compact \u2014 no tool result or tool-call args in history exceed ${cap.toLocaleString()} tokens.`
@@ -12567,8 +12819,8 @@ var handlers8 = {
12567
12819
 
12568
12820
  // src/cli/ui/slash/handlers/plans.ts
12569
12821
  import { basename } from "path";
12570
- var plans = (_args, loop) => {
12571
- const sessionName = loop.sessionName;
12822
+ var plans = (_args, loop2) => {
12823
+ const sessionName = loop2.sessionName;
12572
12824
  if (!sessionName) {
12573
12825
  return {
12574
12826
  info: "no session attached \u2014 `/plans` is per-session. Run `reasonix code` in a project to get a session."
@@ -12609,8 +12861,8 @@ var plans = (_args, loop) => {
12609
12861
  }
12610
12862
  return { info: lines.join("\n") };
12611
12863
  };
12612
- var replay = (args, loop) => {
12613
- const sessionName = loop.sessionName;
12864
+ var replay = (args, loop2) => {
12865
+ const sessionName = loop2.sessionName;
12614
12866
  if (!sessionName) {
12615
12867
  return {
12616
12868
  info: "no session attached \u2014 `/replay` is per-session. Run `reasonix code` in a project to get a session."
@@ -12650,7 +12902,7 @@ var handlers9 = {
12650
12902
  };
12651
12903
 
12652
12904
  // src/cli/ui/slash/handlers/sessions.ts
12653
- var sessions = (_args, loop) => {
12905
+ var sessions = (_args, loop2) => {
12654
12906
  const items = listSessions();
12655
12907
  if (items.length === 0) {
12656
12908
  return {
@@ -12661,7 +12913,7 @@ var sessions = (_args, loop) => {
12661
12913
  for (const s of items) {
12662
12914
  const sizeKb = (s.size / 1024).toFixed(1);
12663
12915
  const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
12664
- const marker = s.name === loop.sessionName ? "\u25B8" : " ";
12916
+ const marker = s.name === loop2.sessionName ? "\u25B8" : " ";
12665
12917
  lines.push(
12666
12918
  ` ${marker} ${s.name.padEnd(22)} ${String(s.messageCount).padStart(5)} msgs ${sizeKb.padStart(7)} KB ${when}`
12667
12919
  );
@@ -12670,11 +12922,11 @@ var sessions = (_args, loop) => {
12670
12922
  lines.push("Resume with: reasonix chat --session <name>");
12671
12923
  return { info: lines.join("\n") };
12672
12924
  };
12673
- var forget = (_args, loop) => {
12674
- if (!loop.sessionName) {
12925
+ var forget = (_args, loop2) => {
12926
+ if (!loop2.sessionName) {
12675
12927
  return { info: "not in a session \u2014 nothing to forget" };
12676
12928
  }
12677
- const name = loop.sessionName;
12929
+ const name = loop2.sessionName;
12678
12930
  const ok = deleteSession(name);
12679
12931
  return {
12680
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?)`
@@ -12775,9 +13027,9 @@ var HANDLERS = {
12775
13027
  ...handlers10,
12776
13028
  ...handlers11
12777
13029
  };
12778
- function handleSlash(cmd, args, loop, ctx = {}) {
13030
+ function handleSlash(cmd, args, loop2, ctx = {}) {
12779
13031
  const h = HANDLERS[cmd];
12780
- if (h) return h(args, loop, ctx);
13032
+ if (h) return h(args, loop2, ctx);
12781
13033
  return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
12782
13034
  }
12783
13035
 
@@ -13129,14 +13381,14 @@ function useEditHistory(codeMode) {
13129
13381
 
13130
13382
  // src/cli/ui/useSessionInfo.ts
13131
13383
  import { useCallback as useCallback3, useEffect as useEffect4, useState as useState8 } from "react";
13132
- function useSessionInfo(loop) {
13384
+ function useSessionInfo(loop2) {
13133
13385
  const [balance, setBalance] = useState8(null);
13134
13386
  const [models2, setModels] = useState8(null);
13135
13387
  const [latestVersion, setLatestVersion] = useState8(null);
13136
13388
  useEffect4(() => {
13137
13389
  let cancelled = false;
13138
13390
  void (async () => {
13139
- const bal = await loop.client.getBalance().catch(() => null);
13391
+ const bal = await loop2.client.getBalance().catch(() => null);
13140
13392
  if (cancelled || !bal || !bal.balance_infos.length) return;
13141
13393
  const primary = bal.balance_infos[0];
13142
13394
  setBalance({ currency: primary.currency, total: Number(primary.total_balance) });
@@ -13144,18 +13396,18 @@ function useSessionInfo(loop) {
13144
13396
  return () => {
13145
13397
  cancelled = true;
13146
13398
  };
13147
- }, [loop]);
13399
+ }, [loop2]);
13148
13400
  useEffect4(() => {
13149
13401
  let cancelled = false;
13150
13402
  void (async () => {
13151
- const list = await loop.client.listModels().catch(() => null);
13403
+ const list = await loop2.client.listModels().catch(() => null);
13152
13404
  if (cancelled || !list) return;
13153
13405
  setModels(list.data.map((m) => m.id));
13154
13406
  })();
13155
13407
  return () => {
13156
13408
  cancelled = true;
13157
13409
  };
13158
- }, [loop]);
13410
+ }, [loop2]);
13159
13411
  useEffect4(() => {
13160
13412
  let cancelled = false;
13161
13413
  void (async () => {
@@ -13170,19 +13422,19 @@ function useSessionInfo(loop) {
13170
13422
  const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
13171
13423
  const refreshBalance = useCallback3(() => {
13172
13424
  void (async () => {
13173
- const bal = await loop.client.getBalance().catch(() => null);
13425
+ const bal = await loop2.client.getBalance().catch(() => null);
13174
13426
  if (bal?.balance_infos.length) {
13175
13427
  const p = bal.balance_infos[0];
13176
13428
  setBalance({ currency: p.currency, total: Number(p.total_balance) });
13177
13429
  }
13178
13430
  })();
13179
- }, [loop]);
13431
+ }, [loop2]);
13180
13432
  const refreshModels = useCallback3(() => {
13181
13433
  void (async () => {
13182
- const list = await loop.client.listModels().catch(() => null);
13434
+ const list = await loop2.client.listModels().catch(() => null);
13183
13435
  if (list) setModels(list.data.map((m) => m.id));
13184
13436
  })();
13185
- }, [loop]);
13437
+ }, [loop2]);
13186
13438
  const refreshLatestVersion = useCallback3(() => {
13187
13439
  void (async () => {
13188
13440
  const fresh = await getLatestVersion({ force: true });
@@ -13260,6 +13512,17 @@ function useSubagent({ session, setHistorical }) {
13260
13512
  // src/cli/ui/App.tsx
13261
13513
  var FLUSH_INTERVAL_MS = 100;
13262
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
+ }
13263
13526
  function App({
13264
13527
  model: model2,
13265
13528
  system,
@@ -13279,6 +13542,9 @@ function App({
13279
13542
  const [input, setInput] = useState10("");
13280
13543
  const [busy, setBusy] = useState10(false);
13281
13544
  const abortedThisTurn = useRef6(false);
13545
+ useEffect6(() => {
13546
+ busyRef.current = busy;
13547
+ }, [busy]);
13282
13548
  const [ongoingTool, setOngoingTool] = useState10(null);
13283
13549
  const [toolProgress, setToolProgress] = useState10(null);
13284
13550
  const { stdout: stdout2 } = useStdout8();
@@ -13332,6 +13598,7 @@ function App({
13332
13598
  const [pendingCount, setPendingCount] = useState10(0);
13333
13599
  const syncPendingCount = useCallback4(() => {
13334
13600
  setPendingCount(pendingEdits.current.length);
13601
+ setPendingTick((t) => t + 1);
13335
13602
  }, []);
13336
13603
  const [editMode, setEditMode] = useState10(() => codeMode ? loadEditMode() : "review");
13337
13604
  const editModeRef = useRef6(editMode);
@@ -13340,6 +13607,8 @@ function App({
13340
13607
  if (codeMode) saveEditMode(editMode);
13341
13608
  }, [editMode, codeMode]);
13342
13609
  const [pendingEditReview, setPendingEditReview] = useState10(null);
13610
+ const [walkthroughActive, setWalkthroughActive] = useState10(false);
13611
+ const [pendingTick, setPendingTick] = useState10(0);
13343
13612
  const editReviewResolveRef = useRef6(null);
13344
13613
  const turnEditPolicyRef = useRef6("ask");
13345
13614
  const [modeFlash, setModeFlash] = useState10(false);
@@ -13370,6 +13639,16 @@ function App({
13370
13639
  const promptHistory = useRef6([]);
13371
13640
  const historyCursor = useRef6(-1);
13372
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]);
13373
13652
  const toolHistoryRef = useRef6([]);
13374
13653
  const planStepsRef = useRef6(null);
13375
13654
  const completedStepIdsRef = useRef6(/* @__PURE__ */ new Set());
@@ -13414,7 +13693,7 @@ function App({
13414
13693
  };
13415
13694
  }, []);
13416
13695
  const loopRef = useRef6(null);
13417
- const loop = useMemo3(() => {
13696
+ const loop2 = useMemo3(() => {
13418
13697
  if (loopRef.current) return loopRef.current;
13419
13698
  const client = new DeepSeekClient();
13420
13699
  if (tools && !tools.has("run_skill")) {
@@ -13463,8 +13742,8 @@ function App({
13463
13742
  return l;
13464
13743
  }, [model2, system, harvest3, branch2, session, tools, codeMode]);
13465
13744
  useEffect6(() => {
13466
- loop.hooks = hookList;
13467
- }, [loop, hookList]);
13745
+ loop2.hooks = hookList;
13746
+ }, [loop2, hookList]);
13468
13747
  const {
13469
13748
  balance,
13470
13749
  models: models2,
@@ -13473,7 +13752,7 @@ function App({
13473
13752
  refreshBalance,
13474
13753
  refreshModels,
13475
13754
  refreshLatestVersion
13476
- } = useSessionInfo(loop);
13755
+ } = useSessionInfo(loop2);
13477
13756
  const {
13478
13757
  slashMatches,
13479
13758
  slashSelected,
@@ -13516,13 +13795,13 @@ function App({
13516
13795
  text: "\u25B8 ephemeral chat (no session persistence) \u2014 drop --no-session to enable"
13517
13796
  }
13518
13797
  ]);
13519
- } else if (loop.resumedMessageCount > 0) {
13798
+ } else if (loop2.resumedMessageCount > 0) {
13520
13799
  setHistorical((prev) => [
13521
13800
  ...prev,
13522
13801
  {
13523
13802
  id: `sys-resume-${Date.now()}`,
13524
13803
  role: "info",
13525
- 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`
13526
13805
  }
13527
13806
  ]);
13528
13807
  } else {
@@ -13585,7 +13864,7 @@ function App({
13585
13864
  ]);
13586
13865
  markEditModeHintShown();
13587
13866
  }
13588
- }, [session, loop, codeMode, syncPendingCount]);
13867
+ }, [session, loop2, codeMode, syncPendingCount]);
13589
13868
  const quitProcess = useCallback4(() => {
13590
13869
  transcriptRef.current?.end();
13591
13870
  process.exit(0);
@@ -13615,25 +13894,40 @@ function App({
13615
13894
  setPendingEditReview(null);
13616
13895
  resolve8("reject");
13617
13896
  }
13618
- loop.abort();
13897
+ if (activeLoopRef.current) stopLoop();
13898
+ loop2.abort();
13899
+ return;
13900
+ }
13901
+ if (key.escape && !busy && activeLoopRef.current) {
13902
+ stopLoop();
13619
13903
  return;
13620
13904
  }
13621
- if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision) {
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) {
13622
13919
  setEditMode((m) => {
13623
- 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)";
13624
13922
  setHistorical((prev) => [
13625
13923
  ...prev,
13626
- {
13627
- id: `mode-${Date.now()}`,
13628
- role: "info",
13629
- 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)"
13630
- }
13924
+ { id: `mode-${Date.now()}`, role: "info", text: message }
13631
13925
  ]);
13632
13926
  return next;
13633
13927
  });
13634
13928
  return;
13635
13929
  }
13636
- 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
13637
13931
  // history entry — the keybind is useful long after the 5-second
13638
13932
  // banner expires, which users rightly want.
13639
13933
  (undoBanner || hasUndoable())) {
@@ -13734,7 +14028,7 @@ function App({
13734
14028
  }
13735
14029
  return formatEditResults(results);
13736
14030
  };
13737
- if (editModeRef.current === "auto") return applyNow();
14031
+ if (editModeRef.current === "auto" || editModeRef.current === "yolo") return applyNow();
13738
14032
  if (turnEditPolicyRef.current === "apply-all") return applyNow();
13739
14033
  const choice = await new Promise((resolveChoice) => {
13740
14034
  editReviewResolveRef.current = resolveChoice;
@@ -13827,7 +14121,7 @@ function App({
13827
14121
  },
13828
14122
  [session, syncPendingCount]
13829
14123
  );
13830
- const prefixHash = loop.prefix.fingerprint;
14124
+ const prefixHash = loop2.prefix.fingerprint;
13831
14125
  const writeTranscript = useCallback4(
13832
14126
  (ev) => {
13833
14127
  const stream = transcriptRef.current;
@@ -13846,10 +14140,98 @@ function App({
13846
14140
  const clearPendingPlan = useCallback4(() => {
13847
14141
  setPendingPlan(null);
13848
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
+ }, []);
13849
14226
  const handleSubmit = useCallback4(
13850
14227
  async (raw) => {
13851
14228
  let text = raw.trim();
13852
- 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;
13853
14235
  if (atMatches && atMatches.length > 0 && atPicker) {
13854
14236
  const sel = atMatches[atSelected] ?? atMatches[0];
13855
14237
  if (sel) {
@@ -13882,18 +14264,20 @@ function App({
13882
14264
  return;
13883
14265
  }
13884
14266
  const hashParse = detectHashMemory(text);
13885
- if (hashParse?.kind === "memory") {
14267
+ if (hashParse?.kind === "memory" || hashParse?.kind === "memory-global") {
14268
+ const isGlobal = hashParse.kind === "memory-global";
13886
14269
  const memRoot = codeMode?.rootDir ?? process.cwd();
13887
14270
  promptHistory.current.push(text);
13888
14271
  try {
13889
- const result = appendProjectMemory(memRoot, hashParse.note);
14272
+ const result = isGlobal ? appendGlobalMemory(hashParse.note) : appendProjectMemory(memRoot, hashParse.note);
13890
14273
  const verb = result.created ? "created" : "appended to";
14274
+ const scopeTag = isGlobal ? "global" : "project";
13891
14275
  setHistorical((prev) => [
13892
14276
  ...prev,
13893
14277
  {
13894
14278
  id: `hash-${Date.now()}`,
13895
14279
  role: "info",
13896
- text: `\u25B8 noted \u2014 ${verb} ${result.path}`
14280
+ text: `\u25B8 noted (${scopeTag}) \u2014 ${verb} ${result.path}`
13897
14281
  }
13898
14282
  ]);
13899
14283
  } catch (err) {
@@ -13936,7 +14320,7 @@ function App({
13936
14320
  ...prev,
13937
14321
  { id: `bang-o-${Date.now()}`, role: "info", text: formatted }
13938
14322
  ]);
13939
- loop.appendAndPersist({
14323
+ loop2.appendAndPersist({
13940
14324
  role: "user",
13941
14325
  content: formatBangUserMessage(bangCmd, formatted)
13942
14326
  });
@@ -13968,7 +14352,7 @@ function App({
13968
14352
  }
13969
14353
  const slash = parseSlash(text);
13970
14354
  if (slash) {
13971
- const result = handleSlash(slash.cmd, slash.args, loop, {
14355
+ const result = handleSlash(slash.cmd, slash.args, loop2, {
13972
14356
  mcpSpecs,
13973
14357
  mcpServers,
13974
14358
  codeUndo: codeMode ? codeUndo : void 0,
@@ -13986,13 +14370,17 @@ function App({
13986
14370
  editMode: codeMode ? editMode : void 0,
13987
14371
  setEditMode: codeMode ? setEditMode : void 0,
13988
14372
  armPro: () => {
13989
- loop.armProForNextTurn();
14373
+ loop2.armProForNextTurn();
13990
14374
  setProArmed(true);
13991
14375
  },
13992
14376
  disarmPro: () => {
13993
- loop.disarmPro();
14377
+ loop2.disarmPro();
13994
14378
  setProArmed(false);
13995
14379
  },
14380
+ startLoop,
14381
+ stopLoop,
14382
+ getLoopStatus,
14383
+ startWalkthrough: codeMode ? startWalkthrough : void 0,
13996
14384
  jobs: codeMode?.jobs,
13997
14385
  postInfo: (text2) => setHistorical((prev) => [
13998
14386
  ...prev,
@@ -14009,6 +14397,7 @@ function App({
14009
14397
  refreshModels
14010
14398
  });
14011
14399
  if (result.exit) {
14400
+ if (activeLoopRef.current) stopLoop();
14012
14401
  transcriptRef.current?.end();
14013
14402
  exit2();
14014
14403
  return;
@@ -14027,6 +14416,7 @@ function App({
14027
14416
  clearPendingEdits(session ?? null);
14028
14417
  syncPendingCount();
14029
14418
  }
14419
+ if (activeLoopRef.current) stopLoop();
14030
14420
  return;
14031
14421
  }
14032
14422
  if (result.clear) {
@@ -14037,6 +14427,7 @@ function App({
14037
14427
  clearPendingEdits(session ?? null);
14038
14428
  syncPendingCount();
14039
14429
  }
14430
+ if (activeLoopRef.current) stopLoop();
14040
14431
  return;
14041
14432
  }
14042
14433
  if (result.info) {
@@ -14165,8 +14556,47 @@ function App({
14165
14556
  }
14166
14557
  }
14167
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
+ }
14168
14598
  try {
14169
- for await (const ev of loop.step(modelInput)) {
14599
+ for await (const ev of loop2.step(modelInput)) {
14170
14600
  writeTranscript(ev);
14171
14601
  if (ev.role !== "status") {
14172
14602
  setStatusLine((cur) => cur ? null : cur);
@@ -14206,7 +14636,7 @@ function App({
14206
14636
  flush();
14207
14637
  const repairNote = ev.repair ? describeRepair(ev.repair) : "";
14208
14638
  setStreaming(null);
14209
- setSummary(loop.stats.summary());
14639
+ setSummary(loop2.stats.summary());
14210
14640
  if (ev.stats?.usage) {
14211
14641
  appendUsage({
14212
14642
  session: session ?? null,
@@ -14240,7 +14670,7 @@ function App({
14240
14670
  if (codeMode && finalText && !ev.forcedSummary) {
14241
14671
  const blocks = parseEditBlocks(finalText);
14242
14672
  if (blocks.length > 0) {
14243
- if (editModeRef.current === "auto") {
14673
+ if (editModeRef.current === "auto" || editModeRef.current === "yolo") {
14244
14674
  const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
14245
14675
  const results = applyEditBlocks(blocks, codeMode.rootDir);
14246
14676
  const good = results.some(
@@ -14452,7 +14882,7 @@ function App({
14452
14882
  event: "Stop",
14453
14883
  cwd: hookCwd,
14454
14884
  lastAssistantText: streamRef.text,
14455
- turn: loop.stats.summary().turns
14885
+ turn: loop2.stats.summary().turns
14456
14886
  }
14457
14887
  });
14458
14888
  for (const o of stopReport.outcomes) {
@@ -14473,7 +14903,7 @@ function App({
14473
14903
  setOngoingTool(null);
14474
14904
  setToolProgress(null);
14475
14905
  setStatusLine(null);
14476
- setSummary(loop.stats.summary());
14906
+ setSummary(loop2.stats.summary());
14477
14907
  setBusy(false);
14478
14908
  setTurnOnPro(false);
14479
14909
  refreshBalance();
@@ -14491,7 +14921,7 @@ function App({
14491
14921
  exit2,
14492
14922
  hookCwd,
14493
14923
  hookList,
14494
- loop,
14924
+ loop2,
14495
14925
  latestVersion,
14496
14926
  mcpSpecs,
14497
14927
  mcpServers,
@@ -14520,9 +14950,51 @@ function App({
14520
14950
  refreshModels,
14521
14951
  proArmed,
14522
14952
  persistPlanState,
14523
- stdout2
14953
+ stdout2,
14954
+ stopLoop,
14955
+ startLoop,
14956
+ getLoopStatus,
14957
+ startWalkthrough
14524
14958
  ]
14525
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]);
14526
14998
  const handleShellConfirm = useCallback4(
14527
14999
  async (choice) => {
14528
15000
  const pending = pendingShell;
@@ -14612,13 +15084,13 @@ ${body}`;
14612
15084
  }
14613
15085
  }
14614
15086
  if (busy) {
14615
- loop.abort();
15087
+ loop2.abort();
14616
15088
  setQueuedSubmit(synthetic);
14617
15089
  } else {
14618
15090
  await handleSubmit(synthetic);
14619
15091
  }
14620
15092
  },
14621
- [pendingShell, codeMode, handleSubmit, busy, loop]
15093
+ [pendingShell, codeMode, handleSubmit, busy, loop2]
14622
15094
  );
14623
15095
  useEffect6(() => {
14624
15096
  if (!busy && queuedSubmit !== null) {
@@ -14656,13 +15128,13 @@ ${body}`;
14656
15128
  { id: `plan-${choice}-${Date.now()}`, role: "info", text: marker }
14657
15129
  ]);
14658
15130
  if (busy) {
14659
- loop.abort();
15131
+ loop2.abort();
14660
15132
  setQueuedSubmit(synthetic);
14661
15133
  } else {
14662
15134
  await handleSubmit(synthetic);
14663
15135
  }
14664
15136
  },
14665
- [pendingPlan, togglePlanMode, busy, loop, handleSubmit, persistPlanState]
15137
+ [pendingPlan, togglePlanMode, busy, loop2, handleSubmit, persistPlanState]
14666
15138
  );
14667
15139
  const handlePlanConfirmRef = useRef6(handlePlanConfirm);
14668
15140
  useEffect6(() => {
@@ -14713,13 +15185,13 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
14713
15185
  { id: `plan-${staged.mode}-${Date.now()}`, role: "info", text: marker }
14714
15186
  ]);
14715
15187
  if (busy) {
14716
- loop.abort();
15188
+ loop2.abort();
14717
15189
  setQueuedSubmit(synthetic);
14718
15190
  } else {
14719
15191
  await handleSubmit(synthetic);
14720
15192
  }
14721
15193
  },
14722
- [stagedInput, togglePlanMode, busy, loop, handleSubmit]
15194
+ [stagedInput, togglePlanMode, busy, loop2, handleSubmit]
14723
15195
  );
14724
15196
  const handleStagedInputCancel = useCallback4(() => {
14725
15197
  if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
@@ -14748,13 +15220,13 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
14748
15220
  { id: `cp-${choice}-${Date.now()}`, role: "info", text: marker }
14749
15221
  ]);
14750
15222
  if (busy) {
14751
- loop.abort();
15223
+ loop2.abort();
14752
15224
  setQueuedSubmit(synthetic);
14753
15225
  } else {
14754
15226
  await handleSubmit(synthetic);
14755
15227
  }
14756
15228
  },
14757
- [pendingCheckpoint, busy, loop, handleSubmit]
15229
+ [pendingCheckpoint, busy, loop2, handleSubmit]
14758
15230
  );
14759
15231
  const handleCheckpointConfirmRef = useRef6(handleCheckpointConfirm);
14760
15232
  useEffect6(() => {
@@ -14782,13 +15254,13 @@ If the feedback only tweaks how you execute (extra constraints, style preference
14782
15254
  { id: `cp-revise-${Date.now()}`, role: "info", text: marker }
14783
15255
  ]);
14784
15256
  if (busy) {
14785
- loop.abort();
15257
+ loop2.abort();
14786
15258
  setQueuedSubmit(synthetic);
14787
15259
  } else {
14788
15260
  await handleSubmit(synthetic);
14789
15261
  }
14790
15262
  },
14791
- [stagedCheckpointRevise, busy, loop, handleSubmit]
15263
+ [stagedCheckpointRevise, busy, loop2, handleSubmit]
14792
15264
  );
14793
15265
  const handleCheckpointReviseCancel = useCallback4(() => {
14794
15266
  const snap = stagedCheckpointRevise;
@@ -14811,7 +15283,7 @@ If the feedback only tweaks how you execute (extra constraints, style preference
14811
15283
  { id: `choice-cancel-${Date.now()}`, role: "info", text: "\u25B8 choice cancelled" }
14812
15284
  ]);
14813
15285
  if (busy) {
14814
- loop.abort();
15286
+ loop2.abort();
14815
15287
  setQueuedSubmit(synthetic2);
14816
15288
  } else {
14817
15289
  await handleSubmit(synthetic2);
@@ -14826,13 +15298,13 @@ If the feedback only tweaks how you execute (extra constraints, style preference
14826
15298
  { id: `choice-pick-${Date.now()}`, role: "info", text: `\u25B8 chose ${label}` }
14827
15299
  ]);
14828
15300
  if (busy) {
14829
- loop.abort();
15301
+ loop2.abort();
14830
15302
  setQueuedSubmit(synthetic);
14831
15303
  } else {
14832
15304
  await handleSubmit(synthetic);
14833
15305
  }
14834
15306
  },
14835
- [pendingChoice, busy, loop, handleSubmit]
15307
+ [pendingChoice, busy, loop2, handleSubmit]
14836
15308
  );
14837
15309
  const handleChoiceConfirmRef = useRef6(handleChoiceConfirm);
14838
15310
  useEffect6(() => {
@@ -14857,13 +15329,13 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
14857
15329
  { id: `choice-custom-${Date.now()}`, role: "info", text: marker }
14858
15330
  ]);
14859
15331
  if (busy) {
14860
- loop.abort();
15332
+ loop2.abort();
14861
15333
  setQueuedSubmit(synthetic);
14862
15334
  } else {
14863
15335
  await handleSubmit(synthetic);
14864
15336
  }
14865
15337
  },
14866
- [busy, loop, handleSubmit]
15338
+ [busy, loop2, handleSubmit]
14867
15339
  );
14868
15340
  const handleChoiceCustomCancel = useCallback4(() => {
14869
15341
  const snap = stagedChoiceCustom;
@@ -14882,7 +15354,7 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
14882
15354
  { id: `revise-reject-${Date.now()}`, role: "info", text: "\u25B8 revision rejected" }
14883
15355
  ]);
14884
15356
  if (busy) {
14885
- loop.abort();
15357
+ loop2.abort();
14886
15358
  setQueuedSubmit(synthetic2);
14887
15359
  } else {
14888
15360
  await handleSubmit(synthetic2);
@@ -14917,13 +15389,13 @@ ${snap.remainingSteps.map((s, i) => ` ${i + 1}. ${s.id} \xB7 ${s.title} \u2014
14917
15389
 
14918
15390
  Continue executing from the next pending step. Call mark_step_complete after each one as before.`;
14919
15391
  if (busy) {
14920
- loop.abort();
15392
+ loop2.abort();
14921
15393
  setQueuedSubmit(synthetic);
14922
15394
  } else {
14923
15395
  await handleSubmit(synthetic);
14924
15396
  }
14925
15397
  },
14926
- [pendingRevision, busy, loop, handleSubmit, persistPlanState]
15398
+ [pendingRevision, busy, loop2, handleSubmit, persistPlanState]
14927
15399
  );
14928
15400
  const handleReviseConfirmRef = useRef6(handleReviseConfirm);
14929
15401
  useEffect6(() => {
@@ -14936,17 +15408,17 @@ Continue executing from the next pending step. Call mark_step_complete after eac
14936
15408
  return /* @__PURE__ */ React23.createElement(React23.Fragment, null, /* @__PURE__ */ React23.createElement(
14937
15409
  TickerProvider,
14938
15410
  {
14939
- 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
14940
15412
  },
14941
15413
  /* @__PURE__ */ React23.createElement(Box21, { flexDirection: "column" }, /* @__PURE__ */ React23.createElement(
14942
15414
  StatsPanel,
14943
15415
  {
14944
15416
  summary,
14945
- model: loop.model,
15417
+ model: loop2.model,
14946
15418
  prefixHash,
14947
- harvestOn: loop.harvestEnabled,
14948
- branchBudget: loop.branchOptions.budget,
14949
- reasoningEffort: loop.reasoningEffort,
15419
+ harvestOn: loop2.harvestEnabled,
15420
+ branchBudget: loop2.branchOptions.budget,
15421
+ reasoningEffort: loop2.reasoningEffort,
14950
15422
  planMode,
14951
15423
  editMode: codeMode ? editMode : void 0,
14952
15424
  balance,
@@ -15035,6 +15507,13 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15035
15507
  }
15036
15508
  }
15037
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
+ }
15038
15517
  ) : /* @__PURE__ */ React23.createElement(React23.Fragment, null, codeMode ? /* @__PURE__ */ React23.createElement(
15039
15518
  ModeStatusBar,
15040
15519
  {
@@ -15045,7 +15524,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15045
15524
  undoArmed: !!undoBanner || hasUndoable(),
15046
15525
  jobs: codeMode.jobs
15047
15526
  }
15048
- ) : null, /* @__PURE__ */ React23.createElement(
15527
+ ) : null, activeLoop ? /* @__PURE__ */ React23.createElement(LoopStatusRow, { loop: activeLoop }) : null, /* @__PURE__ */ React23.createElement(
15049
15528
  PromptInput,
15050
15529
  {
15051
15530
  value: input,
@@ -15076,7 +15555,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15076
15555
  }
15077
15556
 
15078
15557
  // src/cli/ui/SessionPicker.tsx
15079
- import { Box as Box22, Text as Text19 } from "ink";
15558
+ import { Box as Box22, Text as Text20 } from "ink";
15080
15559
  import React24 from "react";
15081
15560
  function SessionPicker({
15082
15561
  sessionName,
@@ -15084,7 +15563,7 @@ function SessionPicker({
15084
15563
  lastActive,
15085
15564
  onChoose
15086
15565
  }) {
15087
- 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(
15088
15567
  SingleSelect,
15089
15568
  {
15090
15569
  initialValue: "new",
@@ -15107,7 +15586,7 @@ function SessionPicker({
15107
15586
  ],
15108
15587
  onSubmit: (v) => onChoose(v)
15109
15588
  }
15110
- ), /* @__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")));
15111
15590
  }
15112
15591
  function relativeTime2(date) {
15113
15592
  const ms = Date.now() - date.getTime();
@@ -15123,7 +15602,7 @@ function relativeTime2(date) {
15123
15602
  }
15124
15603
 
15125
15604
  // src/cli/ui/Setup.tsx
15126
- 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";
15127
15606
  import TextInput from "ink-text-input";
15128
15607
  import React25, { useState as useState11 } from "react";
15129
15608
  function Setup({ onReady }) {
@@ -15149,7 +15628,7 @@ function Setup({ onReady }) {
15149
15628
  }
15150
15629
  onReady(trimmed);
15151
15630
  };
15152
- 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(
15153
15632
  TextInput,
15154
15633
  {
15155
15634
  value,
@@ -15158,7 +15637,7 @@ function Setup({ onReady }) {
15158
15637
  mask: "\u2022",
15159
15638
  placeholder: "sk-..."
15160
15639
  }
15161
- )), 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.)")));
15162
15641
  }
15163
15642
 
15164
15643
  // src/cli/commands/chat.tsx
@@ -15332,7 +15811,7 @@ async function chatCommand(opts) {
15332
15811
  // src/cli/commands/code.tsx
15333
15812
  import { basename as basename2, resolve as resolve7 } from "path";
15334
15813
  async function codeCommand(opts = {}) {
15335
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-POARCKKR.js");
15814
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-LJ44NWSU.js");
15336
15815
  const rootDir = resolve7(opts.dir ?? process.cwd());
15337
15816
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
15338
15817
  const tools = new ToolRegistry();
@@ -15346,6 +15825,10 @@ async function codeCommand(opts = {}) {
15346
15825
  // via ShellConfirm mid-session takes effect on the next shell call
15347
15826
  // instead of waiting for `/new` or a relaunch.
15348
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",
15349
15832
  jobs: jobs2
15350
15833
  });
15351
15834
  registerPlanTool(tools);
@@ -15378,32 +15861,32 @@ import { render as render2 } from "ink";
15378
15861
  import React29 from "react";
15379
15862
 
15380
15863
  // src/cli/ui/DiffApp.tsx
15381
- 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";
15382
15865
  import React28, { useState as useState13 } from "react";
15383
15866
 
15384
15867
  // src/cli/ui/RecordView.tsx
15385
- import { Box as Box24, Text as Text21 } from "ink";
15868
+ import { Box as Box24, Text as Text22 } from "ink";
15386
15869
  import React27 from "react";
15387
15870
  function RecordView({ rec, compact: compact2 = false }) {
15388
15871
  const toolArgsMax = compact2 ? 120 : 200;
15389
15872
  const toolContentMax = compact2 ? 200 : 400;
15390
15873
  if (rec.role === "user") {
15391
15874
  const content = rec.content.includes("\n") ? rec.content.split("\n").join("\n ") : rec.content;
15392
- 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));
15393
15876
  }
15394
15877
  if (rec.role === "assistant_final") {
15395
- 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)"));
15396
15879
  }
15397
15880
  if (rec.role === "tool") {
15398
- 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)));
15399
15882
  }
15400
15883
  if (rec.role === "error") {
15401
- 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));
15402
15885
  }
15403
15886
  if (rec.role === "done" || rec.role === "assistant_delta") {
15404
15887
  return null;
15405
15888
  }
15406
- 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));
15407
15890
  }
15408
15891
  function CacheBadge({ usage }) {
15409
15892
  const hit = usage.prompt_cache_hit_tokens ?? 0;
@@ -15412,7 +15895,7 @@ function CacheBadge({ usage }) {
15412
15895
  if (total === 0) return null;
15413
15896
  const pct2 = hit / total * 100;
15414
15897
  const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
15415
- 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), "%"));
15416
15899
  }
15417
15900
  function truncate2(s, max) {
15418
15901
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
@@ -15446,7 +15929,7 @@ function DiffApp({ report }) {
15446
15929
  }
15447
15930
  });
15448
15931
  const pair = report.pairs[idx];
15449
- 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")));
15450
15933
  }
15451
15934
  function DiffHeader({ report }) {
15452
15935
  const a = report.a;
@@ -15464,7 +15947,7 @@ function DiffHeader({ report }) {
15464
15947
  } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
15465
15948
  prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
15466
15949
  }
15467
- 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);
15468
15951
  }
15469
15952
  function Pane({
15470
15953
  label,
@@ -15480,21 +15963,21 @@ function Pane({
15480
15963
  borderStyle: "single",
15481
15964
  borderColor: headerColor
15482
15965
  },
15483
- /* @__PURE__ */ React28.createElement(Text22, { color: headerColor, bold: true }, label),
15484
- 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 }))
15485
15968
  );
15486
15969
  }
15487
15970
  function KindBadge({ kind }) {
15488
15971
  if (kind === "match") {
15489
- return /* @__PURE__ */ React28.createElement(Text22, { color: "green" }, "\u2713 match");
15972
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "green" }, "\u2713 match");
15490
15973
  }
15491
15974
  if (kind === "diverge") {
15492
- return /* @__PURE__ */ React28.createElement(Text22, { color: "yellow" }, "\u2605 diverge");
15975
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "yellow" }, "\u2605 diverge");
15493
15976
  }
15494
15977
  if (kind === "only_in_a") {
15495
- return /* @__PURE__ */ React28.createElement(Text22, { color: "blue" }, "\u2190 only in A");
15978
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "blue" }, "\u2190 only in A");
15496
15979
  }
15497
- return /* @__PURE__ */ React28.createElement(Text22, { color: "magenta" }, "\u2192 only in B");
15980
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "magenta" }, "\u2192 only in B");
15498
15981
  }
15499
15982
  function paneRecords(pair, side) {
15500
15983
  if (!pair) return [];
@@ -15669,7 +16152,7 @@ import { render as render3 } from "ink";
15669
16152
  import React31 from "react";
15670
16153
 
15671
16154
  // src/cli/ui/ReplayApp.tsx
15672
- 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";
15673
16156
  import React30, { useMemo as useMemo4, useState as useState14 } from "react";
15674
16157
  function ReplayApp({ meta, pages }) {
15675
16158
  const { exit: exit2 } = useApp4();
@@ -15717,7 +16200,7 @@ function ReplayApp({ meta, pages }) {
15717
16200
  model: cumStats.models[0] ?? meta?.model ?? "?",
15718
16201
  prefixHash
15719
16202
  }
15720
- ), /* @__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")));
15721
16204
  }
15722
16205
 
15723
16206
  // src/cli/commands/replay.ts
@@ -15896,7 +16379,7 @@ async function runCommand2(opts) {
15896
16379
  system: opts.system,
15897
16380
  toolSpecs: tools?.specs()
15898
16381
  });
15899
- const loop = new CacheFirstLoop({
16382
+ const loop2 = new CacheFirstLoop({
15900
16383
  client,
15901
16384
  prefix,
15902
16385
  tools,
@@ -15921,7 +16404,7 @@ async function runCommand2(opts) {
15921
16404
  });
15922
16405
  }
15923
16406
  try {
15924
- for await (const ev of loop.step(opts.task)) {
16407
+ for await (const ev of loop2.step(opts.task)) {
15925
16408
  if (ev.role === "assistant_delta" && ev.content) process.stdout.write(ev.content);
15926
16409
  if (ev.role === "tool") process.stdout.write(`
15927
16410
  [tool ${ev.toolName}] ${ev.content}
@@ -15940,7 +16423,7 @@ async function runCommand2(opts) {
15940
16423
  } finally {
15941
16424
  transcriptStream?.end();
15942
16425
  }
15943
- const s = loop.stats.summary();
16426
+ const s = loop2.stats.summary();
15944
16427
  process.stdout.write(
15945
16428
  `
15946
16429
  \u2014 turns:${s.turns} cache:${(s.cacheHitRatio * 100).toFixed(1)}% cost:$${s.totalCostUsd.toFixed(6)} save-vs-claude:${s.savingsVsClaudePct.toFixed(1)}%
@@ -16037,7 +16520,7 @@ import { render as render4 } from "ink";
16037
16520
  import React33 from "react";
16038
16521
 
16039
16522
  // src/cli/ui/Wizard.tsx
16040
- 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";
16041
16524
  import TextInput2 from "ink-text-input";
16042
16525
  import React32, { useState as useState15 } from "react";
16043
16526
 
@@ -16111,7 +16594,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16111
16594
  setStep("mcp");
16112
16595
  }
16113
16596
  }
16114
- ), /* @__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")));
16115
16598
  }
16116
16599
  if (step === "mcp") {
16117
16600
  return /* @__PURE__ */ React32.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(
@@ -16165,8 +16648,8 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16165
16648
  }
16166
16649
  ), specs.map((spec, i) => (
16167
16650
  // biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
16168
- /* @__PURE__ */ React32.createElement(Box27, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "\xB7 ", spec))
16169
- )), /* @__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(
16170
16653
  ReviewConfirm,
16171
16654
  {
16172
16655
  onConfirm: () => {
@@ -16192,7 +16675,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16192
16675
  }
16193
16676
  ));
16194
16677
  }
16195
- 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 }));
16196
16679
  }
16197
16680
  function ApiKeyStep({
16198
16681
  onSubmit,
@@ -16200,7 +16683,7 @@ function ApiKeyStep({
16200
16683
  onError
16201
16684
  }) {
16202
16685
  const [value, setValue] = useState15("");
16203
- 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(
16204
16687
  TextInput2,
16205
16688
  {
16206
16689
  value,
@@ -16217,7 +16700,7 @@ function ApiKeyStep({
16217
16700
  mask: "\u2022",
16218
16701
  placeholder: "sk-..."
16219
16702
  }
16220
- )), 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);
16221
16704
  }
16222
16705
  function McpArgsStep({
16223
16706
  entry,
@@ -16226,7 +16709,7 @@ function McpArgsStep({
16226
16709
  onError
16227
16710
  }) {
16228
16711
  const [value, setValue] = useState15("");
16229
- 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(
16230
16713
  TextInput2,
16231
16714
  {
16232
16715
  value,
@@ -16242,7 +16725,7 @@ function McpArgsStep({
16242
16725
  },
16243
16726
  placeholder: placeholderFor(entry)
16244
16727
  }
16245
- )), 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));
16246
16729
  }
16247
16730
  function ReviewConfirm({ onConfirm }) {
16248
16731
  useInput3((_i, key) => {
@@ -16262,10 +16745,10 @@ function StepFrame({
16262
16745
  total,
16263
16746
  children
16264
16747
  }) {
16265
- 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));
16266
16749
  }
16267
16750
  function SummaryLine({ label, value }) {
16268
- 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));
16269
16752
  }
16270
16753
  function presetItems() {
16271
16754
  return ["fast", "smart", "max"].map((name) => ({