reasonix 0.5.23 → 0.5.24

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
@@ -7,7 +7,7 @@ import {
7
7
  memoryEnabled,
8
8
  readProjectMemory,
9
9
  sanitizeMemoryName
10
- } from "./chunk-ANMDY236.js";
10
+ } from "./chunk-C266QOQU.js";
11
11
 
12
12
  // src/cli/index.ts
13
13
  import { Command } from "commander";
@@ -67,6 +67,33 @@ function addProjectShellAllowed(rootDir, prefix, path = defaultConfigPath()) {
67
67
  cfg.projects[rootDir].shellAllowed = [...existing, trimmed];
68
68
  writeConfig(cfg, path);
69
69
  }
70
+ function loadEditMode(path = defaultConfigPath()) {
71
+ const v = readConfig(path).editMode;
72
+ return v === "auto" ? "auto" : "review";
73
+ }
74
+ function saveEditMode(mode, path = defaultConfigPath()) {
75
+ const cfg = readConfig(path);
76
+ cfg.editMode = mode;
77
+ writeConfig(cfg, path);
78
+ }
79
+ function editModeHintShown(path = defaultConfigPath()) {
80
+ return readConfig(path).editModeHintShown === true;
81
+ }
82
+ function loadReasoningEffort(path = defaultConfigPath()) {
83
+ const v = readConfig(path).reasoningEffort;
84
+ return v === "high" ? "high" : "max";
85
+ }
86
+ function saveReasoningEffort(effort, path = defaultConfigPath()) {
87
+ const cfg = readConfig(path);
88
+ cfg.reasoningEffort = effort;
89
+ writeConfig(cfg, path);
90
+ }
91
+ function markEditModeHintShown(path = defaultConfigPath()) {
92
+ const cfg = readConfig(path);
93
+ if (cfg.editModeHintShown === true) return;
94
+ cfg.editModeHintShown = true;
95
+ writeConfig(cfg, path);
96
+ }
70
97
  function isPlausibleKey(key) {
71
98
  const trimmed = key.trim();
72
99
  return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
@@ -126,8 +153,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
126
153
  }
127
154
  function sleep(ms, signal) {
128
155
  if (ms <= 0) return Promise.resolve();
129
- return new Promise((resolve7, reject) => {
130
- const timer = setTimeout(resolve7, ms);
156
+ return new Promise((resolve8, reject) => {
157
+ const timer = setTimeout(resolve8, ms);
131
158
  if (signal) {
132
159
  const onAbort = () => {
133
160
  clearTimeout(timer);
@@ -612,7 +639,7 @@ function matchesTool(hook, toolName) {
612
639
  }
613
640
  }
614
641
  function defaultSpawner(input) {
615
- return new Promise((resolve7) => {
642
+ return new Promise((resolve8) => {
616
643
  const child = spawn(input.command, {
617
644
  cwd: input.cwd,
618
645
  shell: true,
@@ -639,7 +666,7 @@ function defaultSpawner(input) {
639
666
  });
640
667
  child.once("error", (err) => {
641
668
  clearTimeout(timer);
642
- resolve7({
669
+ resolve8({
643
670
  exitCode: null,
644
671
  stdout: stdout2,
645
672
  stderr,
@@ -649,7 +676,7 @@ function defaultSpawner(input) {
649
676
  });
650
677
  child.once("close", (code) => {
651
678
  clearTimeout(timer);
652
- resolve7({
679
+ resolve8({
653
680
  exitCode: code,
654
681
  stdout: stdout2.trim(),
655
682
  stderr: stderr.trim(),
@@ -979,6 +1006,12 @@ var ToolRegistry = class {
979
1006
  * bounced until the user approves a submitted plan.
980
1007
  */
981
1008
  _planMode = false;
1009
+ /**
1010
+ * Optional hook run after arg parsing but before tool.fn. Lets the TUI
1011
+ * reroute specific tool calls (e.g. edit_file in review mode) without
1012
+ * modifying the tool definitions themselves.
1013
+ */
1014
+ _interceptor = null;
982
1015
  constructor(opts = {}) {
983
1016
  this._autoFlatten = opts.autoFlatten !== false;
984
1017
  }
@@ -990,6 +1023,14 @@ var ToolRegistry = class {
990
1023
  get planMode() {
991
1024
  return this._planMode;
992
1025
  }
1026
+ /**
1027
+ * Install or clear the dispatch interceptor. At most one interceptor
1028
+ * is active at a time — calling twice replaces the previous. Pass
1029
+ * `null` to remove.
1030
+ */
1031
+ setToolInterceptor(fn) {
1032
+ this._interceptor = fn;
1033
+ }
993
1034
  register(def) {
994
1035
  if (!def.name) throw new Error("tool requires a name");
995
1036
  const internal = { ...def };
@@ -1046,6 +1087,16 @@ var ToolRegistry = class {
1046
1087
  error: `${name}: unavailable in plan mode \u2014 this is a read-only exploration phase. Use read_file / list_directory / search_files / directory_tree / web_search / allowlisted shell commands to investigate. Call submit_plan with your proposed plan when you're ready for the user's review.`
1047
1088
  });
1048
1089
  }
1090
+ if (this._interceptor) {
1091
+ try {
1092
+ const short = await this._interceptor(name, args);
1093
+ if (typeof short === "string") return short;
1094
+ } catch (err) {
1095
+ return JSON.stringify({
1096
+ error: `${name}: interceptor failed \u2014 ${err.message}`
1097
+ });
1098
+ }
1099
+ }
1049
1100
  try {
1050
1101
  const result = await tool.fn(args, { signal: opts.signal });
1051
1102
  const str = typeof result === "string" ? result : JSON.stringify(result);
@@ -1779,6 +1830,7 @@ function round(n, digits) {
1779
1830
  }
1780
1831
 
1781
1832
  // src/loop.ts
1833
+ var ARGS_COMPACT_THRESHOLD_TOKENS = 800;
1782
1834
  var CacheFirstLoop = class {
1783
1835
  client;
1784
1836
  prefix;
@@ -1882,12 +1934,62 @@ var CacheFirstLoop = class {
1882
1934
  * authored intent we can't mechanically shrink without losing
1883
1935
  * meaning.
1884
1936
  */
1885
- compact(maxTokens = 4e3) {
1937
+ /**
1938
+ * Conservative args-only shrink fired after every tool response —
1939
+ * strictly about ONE thing: stop oversized `edit_file` / `write_file`
1940
+ * arguments from riding every future turn's prompt.
1941
+ *
1942
+ * Why this is worth doing AUTOMATICALLY (not just on /compact):
1943
+ * Each tool-call arguments string sticks in the log verbatim. On a
1944
+ * coding session with ~10 edits, that's 20-40K tokens of stale
1945
+ * SEARCH/REPLACE text riding along on every turn. Even at a 98.9%
1946
+ * cache hit rate the input cost still adds up linearly (cache-hit
1947
+ * price × tokens × turns). Compacting IMMEDIATELY after the tool
1948
+ * responds means the next turn's prompt is already smaller — the
1949
+ * shrink is a one-time write that saves every future prompt.
1950
+ *
1951
+ * Threshold rationale: 800 tokens ≈ 3 KB. A typical 20-line edit's
1952
+ * args land well under that; massive rewrites (whole-file content,
1953
+ * 100+ line refactors) land above and get the compaction. Small
1954
+ * edits stay byte-verbatim so nothing common-case changes.
1955
+ *
1956
+ * Safety: we ONLY shrink args whose tool has ALREADY responded.
1957
+ * Structurally that's every call in `log.toMessages()` at this
1958
+ * point — the current turn's assistant/tool pairing is by
1959
+ * construction closed by the time we get here (append happens
1960
+ * AFTER dispatch). The in-flight assistant message being built
1961
+ * lives in scratch, not the log, so this pass can't touch it.
1962
+ *
1963
+ * Model impact: the model may occasionally want to reference the
1964
+ * exact SEARCH text of a prior edit — it then reads the file
1965
+ * directly (which shows current state) or looks at the preceding
1966
+ * assistant text (which has its plan). Losing the stale args is a
1967
+ * net win: one extra read_file vs. dragging N KB of stale text
1968
+ * through every subsequent turn.
1969
+ */
1970
+ compactToolCallArgsAfterResponse() {
1886
1971
  const before = this.log.toMessages();
1887
- const { messages, healedCount, tokensSaved, charsSaved } = shrinkOversizedToolResultsByTokens(
1972
+ const { messages, healedCount } = shrinkOversizedToolCallArgsByTokens(
1888
1973
  before,
1889
- maxTokens
1974
+ ARGS_COMPACT_THRESHOLD_TOKENS
1890
1975
  );
1976
+ if (healedCount === 0) return;
1977
+ this.log.compactInPlace(messages);
1978
+ if (this.sessionName) {
1979
+ try {
1980
+ rewriteSession(this.sessionName, messages);
1981
+ } catch {
1982
+ }
1983
+ }
1984
+ }
1985
+ compact(maxTokens = 4e3) {
1986
+ const before = this.log.toMessages();
1987
+ const resultsPass = shrinkOversizedToolResultsByTokens(before, maxTokens);
1988
+ const argsPass = shrinkOversizedToolCallArgsByTokens(resultsPass.messages, maxTokens);
1989
+ const messages = argsPass.messages;
1990
+ const healedCount = resultsPass.healedCount + argsPass.healedCount;
1991
+ const tokensSaved = resultsPass.tokensSaved + argsPass.tokensSaved;
1992
+ const charsSaved = resultsPass.charsSaved + argsPass.charsSaved;
1891
1993
  if (healedCount > 0) {
1892
1994
  this.log.compactInPlace(messages);
1893
1995
  if (this.sessionName) {
@@ -2130,8 +2232,8 @@ var CacheFirstLoop = class {
2130
2232
  }
2131
2233
  );
2132
2234
  for (let k = 0; k < budget; k++) {
2133
- const sample = queue.shift() ?? await new Promise((resolve7) => {
2134
- waiter = resolve7;
2235
+ const sample = queue.shift() ?? await new Promise((resolve8) => {
2236
+ waiter = resolve8;
2135
2237
  });
2136
2238
  yield {
2137
2239
  turn: this._turn,
@@ -2397,6 +2499,7 @@ ${reason}`;
2397
2499
  name,
2398
2500
  content: result
2399
2501
  });
2502
+ this.compactToolCallArgsAfterResponse();
2400
2503
  yield {
2401
2504
  turn: this._turn,
2402
2505
  role: "tool",
@@ -2582,6 +2685,56 @@ function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
2582
2685
  });
2583
2686
  return { messages: out, healedCount, tokensSaved, charsSaved };
2584
2687
  }
2688
+ function shrinkOversizedToolCallArgsByTokens(messages, maxTokens) {
2689
+ let healedCount = 0;
2690
+ let tokensSaved = 0;
2691
+ let charsSaved = 0;
2692
+ const out = messages.map((msg) => {
2693
+ if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls)) return msg;
2694
+ let changed = false;
2695
+ const newCalls = msg.tool_calls.map((call) => {
2696
+ const args = call.function?.arguments;
2697
+ if (typeof args !== "string" || args.length <= maxTokens) return call;
2698
+ const beforeTokens = countTokens(args);
2699
+ if (beforeTokens <= maxTokens) return call;
2700
+ const shrunk = shrinkJsonLongStrings(args);
2701
+ const afterTokens = countTokens(shrunk);
2702
+ if (afterTokens >= beforeTokens) return call;
2703
+ changed = true;
2704
+ healedCount += 1;
2705
+ tokensSaved += beforeTokens - afterTokens;
2706
+ charsSaved += args.length - shrunk.length;
2707
+ return { ...call, function: { ...call.function, arguments: shrunk } };
2708
+ });
2709
+ if (!changed) return msg;
2710
+ return { ...msg, tool_calls: newCalls };
2711
+ });
2712
+ return { messages: out, healedCount, tokensSaved, charsSaved };
2713
+ }
2714
+ function shrinkJsonLongStrings(jsonStr) {
2715
+ let parsed;
2716
+ try {
2717
+ parsed = JSON.parse(jsonStr);
2718
+ } catch {
2719
+ const head = jsonStr.slice(0, 200);
2720
+ return `${head}\u2026[shrunk: ${jsonStr.length} chars, unparsed]`;
2721
+ }
2722
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2723
+ return jsonStr;
2724
+ }
2725
+ const LONG_THRESHOLD = 300;
2726
+ const input = parsed;
2727
+ const output = {};
2728
+ for (const [k, v] of Object.entries(input)) {
2729
+ if (typeof v === "string" && v.length > LONG_THRESHOLD) {
2730
+ const newlines = v.match(/\n/g)?.length ?? 0;
2731
+ output[k] = `[\u2026shrunk: ${v.length} chars, ${newlines} lines \u2014 tool already responded, see result]`;
2732
+ } else {
2733
+ output[k] = v;
2734
+ }
2735
+ }
2736
+ return JSON.stringify(output);
2737
+ }
2585
2738
  function fixToolCallPairing(messages) {
2586
2739
  const out = [];
2587
2740
  let droppedAssistantCalls = 0;
@@ -2876,6 +3029,9 @@ import { promises as fs } from "fs";
2876
3029
  import * as pathMod from "path";
2877
3030
  var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
2878
3031
  var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
3032
+ var DEFAULT_AUTO_PREVIEW_LINES = 200;
3033
+ var AUTO_PREVIEW_HEAD_LINES = 80;
3034
+ var AUTO_PREVIEW_TAIL_LINES = 40;
2879
3035
  var SKIP_DIR_NAMES = /* @__PURE__ */ new Set([
2880
3036
  "node_modules",
2881
3037
  ".git",
@@ -2968,14 +3124,22 @@ function registerFilesystemTools(registry, opts) {
2968
3124
  };
2969
3125
  registry.register({
2970
3126
  name: "read_file",
2971
- description: "Read a file under the sandbox root. Returns the full contents (truncated with a notice if larger than the per-call cap). Paths may be relative to the root or absolute-under-root.",
3127
+ description: `Read a file under the sandbox root. To save context, PREFER to scope the read instead of pulling the whole file:
3128
+ - head: N \u2192 first N lines (imports, public API, small configs)
3129
+ - tail: N \u2192 last N lines (recently-added code, log tails)
3130
+ - range: "A-B" \u2192 inclusive line range A..B, 1-indexed (e.g. "120-180" around an edit site)
3131
+ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_LINES} lines, the tool auto-returns a head+tail preview with an "N lines omitted" marker rather than dumping everything. If you need the middle, re-call with a range. Prefer search_content to locate a symbol first, then read_file with a range around the hit \u2014 one scoped read beats three full-file reads.`,
2972
3132
  readOnly: true,
2973
3133
  parameters: {
2974
3134
  type: "object",
2975
3135
  properties: {
2976
3136
  path: { type: "string", description: "Path to read (relative to rootDir or absolute)." },
2977
3137
  head: { type: "integer", description: "If set, return only the first N lines." },
2978
- tail: { type: "integer", description: "If set, return only the last N lines." }
3138
+ tail: { type: "integer", description: "If set, return only the last N lines." },
3139
+ range: {
3140
+ type: "string",
3141
+ description: 'Inclusive line range like "50-100" or "50-50". 1-indexed. Takes precedence over head/tail when all three are set. Out-of-range requests clamp to file bounds.'
3142
+ }
2979
3143
  },
2980
3144
  required: ["path"]
2981
3145
  },
@@ -2987,21 +3151,52 @@ function registerFilesystemTools(registry, opts) {
2987
3151
  }
2988
3152
  const raw = await fs.readFile(abs);
2989
3153
  if (raw.length > maxReadBytes) {
2990
- const head = raw.slice(0, maxReadBytes).toString("utf8");
2991
- return `${head}
3154
+ const headBytes = raw.slice(0, maxReadBytes).toString("utf8");
3155
+ return `${headBytes}
2992
3156
 
2993
- [\u2026truncated ${raw.length - maxReadBytes} bytes \u2014 file is ${raw.length} B, cap ${maxReadBytes} B. Retry with head/tail for targeted view.]`;
3157
+ [\u2026truncated ${raw.length - maxReadBytes} bytes \u2014 file is ${raw.length} B, cap ${maxReadBytes} B. Retry with head/tail/range for targeted view.]`;
2994
3158
  }
2995
3159
  const text = raw.toString("utf8");
3160
+ let lines = text.split(/\r?\n/);
3161
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
3162
+ const totalLines = lines.length;
3163
+ if (typeof args.range === "string" && /^\d+\s*-\s*\d+$/.test(args.range)) {
3164
+ const [rawStart, rawEnd] = args.range.split("-").map((s) => Number.parseInt(s, 10));
3165
+ const start = Math.max(1, rawStart ?? 1);
3166
+ const end = Math.min(totalLines, Math.max(start, rawEnd ?? totalLines));
3167
+ const slice = lines.slice(start - 1, end);
3168
+ const label = `[range ${start}-${end} of ${totalLines} lines]`;
3169
+ return `${label}
3170
+ ${slice.join("\n")}`;
3171
+ }
2996
3172
  if (typeof args.head === "number" && args.head > 0) {
2997
- return text.split(/\r?\n/).slice(0, args.head).join("\n");
3173
+ const count = Math.min(args.head, totalLines);
3174
+ const slice = lines.slice(0, count);
3175
+ const marker = count < totalLines ? `
3176
+
3177
+ [\u2026head ${count} of ${totalLines} lines \u2014 call again with range / tail for more]` : "";
3178
+ return slice.join("\n") + marker;
2998
3179
  }
2999
3180
  if (typeof args.tail === "number" && args.tail > 0) {
3000
- let lines = text.split(/\r?\n/);
3001
- if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
3002
- return lines.slice(Math.max(0, lines.length - args.tail)).join("\n");
3003
- }
3004
- return text;
3181
+ const count = Math.min(args.tail, totalLines);
3182
+ const slice = lines.slice(totalLines - count);
3183
+ const marker = count < totalLines ? `[\u2026tail ${count} of ${totalLines} lines \u2014 call again with range / head for more]
3184
+
3185
+ ` : "";
3186
+ return marker + slice.join("\n");
3187
+ }
3188
+ if (totalLines <= DEFAULT_AUTO_PREVIEW_LINES) return lines.join("\n");
3189
+ const head = lines.slice(0, AUTO_PREVIEW_HEAD_LINES).join("\n");
3190
+ const tail = lines.slice(totalLines - AUTO_PREVIEW_TAIL_LINES).join("\n");
3191
+ const omitted = totalLines - AUTO_PREVIEW_HEAD_LINES - AUTO_PREVIEW_TAIL_LINES;
3192
+ return [
3193
+ `[auto-preview: head ${AUTO_PREVIEW_HEAD_LINES} + tail ${AUTO_PREVIEW_TAIL_LINES} of ${totalLines} lines]`,
3194
+ head,
3195
+ `
3196
+ [\u2026 ${omitted} lines omitted \u2014 call read_file again with range:"A-B" (1-indexed) or head / tail to get the middle]
3197
+ `,
3198
+ tail
3199
+ ].join("\n");
3005
3200
  }
3006
3201
  });
3007
3202
  registry.register({
@@ -3026,21 +3221,34 @@ function registerFilesystemTools(registry, opts) {
3026
3221
  });
3027
3222
  registry.register({
3028
3223
  name: "directory_tree",
3029
- description: "Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Caps output so a huge tree doesn't drown the context.",
3224
+ description: `Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Budget-aware by default:
3225
+ - maxDepth defaults to 2 (root + one level). A depth-4 tree on a real repo blew ~5K tokens in one call. If you truly need deeper, pass maxDepth:N explicitly.
3226
+ - Skips ${[...SKIP_DIR_NAMES].sort().join(", ")} unless include_deps:true. Traversing into node_modules / .git / dist is almost always token-waste.
3227
+ - Large subtrees (>50 children) auto-collapse to "[N files, M dirs hidden \u2014 list_directory <path> to inspect]" so one huge folder can't dominate the output.
3228
+ Prefer \`list_directory\` for a single-level view, \`search_files\` to find specific paths, and \`search_content\` to find code.`,
3030
3229
  readOnly: true,
3031
3230
  parameters: {
3032
3231
  type: "object",
3033
3232
  properties: {
3034
3233
  path: { type: "string", description: "Root of the tree (default: sandbox root)." },
3035
- maxDepth: { type: "integer", description: "Max recursion depth (default 4)." }
3234
+ maxDepth: {
3235
+ type: "integer",
3236
+ description: "Max recursion depth (default 2). Depth 0 shows only the top-level entries; depth 2 is usually enough to see module structure."
3237
+ },
3238
+ include_deps: {
3239
+ type: "boolean",
3240
+ description: "When true, also traverse node_modules / .git / dist / build / etc. Off by default \u2014 most exploration questions are about the user's own code."
3241
+ }
3036
3242
  }
3037
3243
  },
3038
3244
  fn: async (args) => {
3039
3245
  const startAbs = safePath(args.path ?? ".");
3040
- const maxDepth = typeof args.maxDepth === "number" ? args.maxDepth : 4;
3246
+ const maxDepth = typeof args.maxDepth === "number" ? args.maxDepth : 2;
3247
+ const includeDeps = args.include_deps === true;
3041
3248
  const lines = [];
3042
3249
  let totalBytes = 0;
3043
3250
  let truncated = false;
3251
+ const PER_DIR_CHILD_CAP = 50;
3044
3252
  const walk2 = async (dir, depth) => {
3045
3253
  if (truncated) return;
3046
3254
  if (depth > maxDepth) return;
@@ -3051,10 +3259,27 @@ function registerFilesystemTools(registry, opts) {
3051
3259
  return;
3052
3260
  }
3053
3261
  entries.sort((a, b) => a.name.localeCompare(b.name));
3262
+ let emitted = 0;
3054
3263
  for (const e of entries) {
3055
3264
  if (truncated) return;
3265
+ const skip = e.isDirectory() && !includeDeps && SKIP_DIR_NAMES.has(e.name);
3266
+ if (emitted >= PER_DIR_CHILD_CAP) {
3267
+ const remaining = entries.length - emitted;
3268
+ let restFiles = 0;
3269
+ let restDirs = 0;
3270
+ for (const r of entries.slice(emitted)) {
3271
+ if (r.isDirectory()) restDirs++;
3272
+ else restFiles++;
3273
+ }
3274
+ const indent2 = " ".repeat(depth);
3275
+ lines.push(
3276
+ `${indent2}[\u2026 ${remaining} entries hidden (${restDirs} dirs, ${restFiles} files) \u2014 list_directory on this path to see all]`
3277
+ );
3278
+ return;
3279
+ }
3056
3280
  const indent = " ".repeat(depth);
3057
- const line = e.isDirectory() ? `${indent}${e.name}/` : `${indent}${e.name}`;
3281
+ const suffix = skip ? " (skipped \u2014 pass include_deps:true to traverse)" : "";
3282
+ const line = e.isDirectory() ? `${indent}${e.name}/${suffix}` : `${indent}${e.name}`;
3058
3283
  totalBytes += line.length + 1;
3059
3284
  if (totalBytes > maxListBytes) {
3060
3285
  lines.push(` [\u2026 tree truncated at ${maxListBytes} bytes \u2026]`);
@@ -3062,7 +3287,8 @@ function registerFilesystemTools(registry, opts) {
3062
3287
  return;
3063
3288
  }
3064
3289
  lines.push(line);
3065
- if (e.isDirectory()) {
3290
+ emitted++;
3291
+ if (e.isDirectory() && !skip) {
3066
3292
  await walk2(pathMod.join(dir, e.name), depth + 1);
3067
3293
  }
3068
3294
  }
@@ -3704,9 +3930,311 @@ function forkRegistryExcluding(parent, exclude) {
3704
3930
  }
3705
3931
 
3706
3932
  // src/tools/shell.ts
3707
- import { spawn as spawn2 } from "child_process";
3933
+ import { spawn as spawn3 } from "child_process";
3708
3934
  import { existsSync as existsSync5, statSync as statSync3 } from "fs";
3935
+ import * as pathMod3 from "path";
3936
+
3937
+ // src/tools/jobs.ts
3938
+ import { spawn as spawn2 } from "child_process";
3709
3939
  import * as pathMod2 from "path";
3940
+ function killProcessTree(pid, signal) {
3941
+ if (process.platform === "win32") {
3942
+ const args = ["/pid", String(pid), "/T"];
3943
+ if (signal === "SIGKILL") args.push("/F");
3944
+ try {
3945
+ const killer = spawn2("taskkill", args, {
3946
+ stdio: "ignore",
3947
+ windowsHide: true
3948
+ });
3949
+ killer.on("error", () => {
3950
+ });
3951
+ } catch {
3952
+ }
3953
+ return;
3954
+ }
3955
+ try {
3956
+ process.kill(-pid, signal);
3957
+ return;
3958
+ } catch {
3959
+ }
3960
+ try {
3961
+ process.kill(pid, signal);
3962
+ } catch {
3963
+ }
3964
+ }
3965
+ var DEFAULT_OUTPUT_CAP_BYTES = 64 * 1024;
3966
+ var READY_SIGNALS = [
3967
+ // HTTP server banners
3968
+ /\blistening on\b/i,
3969
+ /\blocal:\s+https?:\/\//i,
3970
+ /\bhttps?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(?::\d+)?\b/i,
3971
+ /\b(?:ready|server started|started server|app listening)\b/i,
3972
+ // Bundlers / compilers
3973
+ /\bcompiled successfully\b/i,
3974
+ /\bbuild complete(?:d)?\b/i,
3975
+ /\bwatching for (?:file )?changes\b/i,
3976
+ /\bready in \d+/i,
3977
+ // Generic
3978
+ /\bstartup (?:complete|finished)\b/i
3979
+ ];
3980
+ var JobRegistry = class {
3981
+ jobs = /* @__PURE__ */ new Map();
3982
+ nextId = 1;
3983
+ /**
3984
+ * Spawn a background child. Resolves after `waitSec` OR on ready
3985
+ * signal OR on early exit, whichever comes first. The child continues
3986
+ * to run (and buffer output) regardless of which path fires.
3987
+ */
3988
+ async start(command, opts) {
3989
+ const trimmed = command.trim();
3990
+ if (!trimmed) throw new Error("run_background: empty command");
3991
+ const op = detectShellOperator(trimmed);
3992
+ if (op !== null) {
3993
+ throw new Error(
3994
+ `run_background: shell operator "${op}" is not supported \u2014 spawn one process per background job. Compose via your orchestration, not the shell.`
3995
+ );
3996
+ }
3997
+ const argv = tokenizeCommand(trimmed);
3998
+ if (argv.length === 0) throw new Error("run_background: empty command");
3999
+ const waitMs = Math.max(0, Math.min(30, opts.waitSec ?? 3)) * 1e3;
4000
+ const maxBytes = opts.maxBufferBytes ?? DEFAULT_OUTPUT_CAP_BYTES;
4001
+ const { bin, args, spawnOverrides } = prepareSpawn(argv);
4002
+ const spawnOpts = {
4003
+ cwd: pathMod2.resolve(opts.cwd),
4004
+ shell: false,
4005
+ windowsHide: true,
4006
+ env: process.env,
4007
+ // POSIX: detach so the child becomes its own process-group leader.
4008
+ // Required for `process.kill(-pid, …)` later — without it a group
4009
+ // kill fails and we end up only signaling the wrapper, leaving
4010
+ // grandchildren (node → vite → esbuild …) orphaned.
4011
+ // Windows: detached would spawn a new console window; leave the
4012
+ // default and use taskkill /T for tree termination.
4013
+ detached: process.platform !== "win32",
4014
+ ...spawnOverrides
4015
+ };
4016
+ let child;
4017
+ try {
4018
+ child = spawn2(bin, args, spawnOpts);
4019
+ } catch (err) {
4020
+ const id2 = this.nextId++;
4021
+ const job2 = {
4022
+ id: id2,
4023
+ command: trimmed,
4024
+ pid: null,
4025
+ startedAt: Date.now(),
4026
+ exitCode: null,
4027
+ output: `[spawn failed] ${err.message}`,
4028
+ totalBytesWritten: 0,
4029
+ running: false,
4030
+ spawnError: err.message,
4031
+ child: null,
4032
+ readyPromise: Promise.resolve(),
4033
+ signalReady: () => {
4034
+ }
4035
+ };
4036
+ this.jobs.set(id2, job2);
4037
+ return {
4038
+ jobId: id2,
4039
+ pid: null,
4040
+ stillRunning: false,
4041
+ readyMatched: false,
4042
+ preview: job2.output,
4043
+ exitCode: null
4044
+ };
4045
+ }
4046
+ const id = this.nextId++;
4047
+ let readyResolve = () => {
4048
+ };
4049
+ const readyPromise = new Promise((res) => {
4050
+ readyResolve = res;
4051
+ });
4052
+ const job = {
4053
+ id,
4054
+ command: trimmed,
4055
+ pid: child.pid ?? null,
4056
+ startedAt: Date.now(),
4057
+ exitCode: null,
4058
+ output: "",
4059
+ totalBytesWritten: 0,
4060
+ running: true,
4061
+ child,
4062
+ readyPromise,
4063
+ signalReady: readyResolve
4064
+ };
4065
+ this.jobs.set(id, job);
4066
+ let readyMatched = false;
4067
+ const onData = (chunk) => {
4068
+ const s = chunk.toString();
4069
+ job.totalBytesWritten += s.length;
4070
+ job.output += s;
4071
+ if (job.output.length > maxBytes) {
4072
+ const overflow = job.output.length - maxBytes;
4073
+ const cut = job.output.indexOf("\n", overflow);
4074
+ const start = cut >= 0 ? cut + 1 : overflow;
4075
+ job.output = `[\u2026 older output dropped \u2026]
4076
+ ${job.output.slice(start)}`;
4077
+ }
4078
+ if (!readyMatched) {
4079
+ for (const re of READY_SIGNALS) {
4080
+ if (re.test(s) || re.test(job.output)) {
4081
+ readyMatched = true;
4082
+ job.signalReady();
4083
+ break;
4084
+ }
4085
+ }
4086
+ }
4087
+ };
4088
+ child.stdout?.on("data", onData);
4089
+ child.stderr?.on("data", onData);
4090
+ child.on("error", (err) => {
4091
+ job.running = false;
4092
+ job.spawnError = err.message;
4093
+ job.signalReady();
4094
+ });
4095
+ child.on("close", (code) => {
4096
+ job.running = false;
4097
+ job.exitCode = code;
4098
+ job.signalReady();
4099
+ });
4100
+ const onAbort = () => this.stop(id, { graceMs: 100 });
4101
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
4102
+ let timer = null;
4103
+ await Promise.race([
4104
+ readyPromise,
4105
+ new Promise((res) => {
4106
+ timer = setTimeout(res, waitMs);
4107
+ })
4108
+ ]);
4109
+ if (timer) clearTimeout(timer);
4110
+ return {
4111
+ jobId: id,
4112
+ pid: job.pid,
4113
+ stillRunning: job.running,
4114
+ readyMatched,
4115
+ preview: job.output,
4116
+ exitCode: job.exitCode
4117
+ };
4118
+ }
4119
+ /**
4120
+ * Read a job's accumulated output. `since` lets a caller poll
4121
+ * incrementally: pass the byte count returned from the last call to
4122
+ * get only newly-written content. Returns both full output and a
4123
+ * running snapshot so the caller can use whichever.
4124
+ */
4125
+ read(id, opts = {}) {
4126
+ const job = this.jobs.get(id);
4127
+ if (!job) return null;
4128
+ const full = job.output;
4129
+ let slice = full;
4130
+ if (typeof opts.since === "number" && opts.since >= 0 && opts.since < full.length) {
4131
+ slice = full.slice(opts.since);
4132
+ }
4133
+ if (typeof opts.tailLines === "number" && opts.tailLines > 0) {
4134
+ const lines = slice.split("\n");
4135
+ const keep = lines.slice(Math.max(0, lines.length - opts.tailLines));
4136
+ slice = keep.join("\n");
4137
+ }
4138
+ return {
4139
+ output: slice,
4140
+ byteLength: full.length,
4141
+ running: job.running,
4142
+ exitCode: job.exitCode,
4143
+ command: job.command,
4144
+ pid: job.pid,
4145
+ spawnError: job.spawnError
4146
+ };
4147
+ }
4148
+ /**
4149
+ * Send SIGTERM, wait `graceMs`, then SIGKILL if still alive. Returns
4150
+ * the final job record (or null when the job id is unknown). Safe to
4151
+ * call on an already-exited job — returns the record unchanged.
4152
+ */
4153
+ async stop(id, opts = {}) {
4154
+ const job = this.jobs.get(id);
4155
+ if (!job) return null;
4156
+ if (!job.running || !job.child) return snapshot(job);
4157
+ const graceMs = Math.max(0, opts.graceMs ?? 2e3);
4158
+ if (job.pid !== null) {
4159
+ killProcessTree(job.pid, "SIGTERM");
4160
+ } else {
4161
+ try {
4162
+ job.child.kill("SIGTERM");
4163
+ } catch {
4164
+ }
4165
+ }
4166
+ await Promise.race([job.readyPromise, new Promise((res) => setTimeout(res, graceMs))]);
4167
+ if (job.running) {
4168
+ if (job.pid !== null) {
4169
+ killProcessTree(job.pid, "SIGKILL");
4170
+ } else {
4171
+ try {
4172
+ job.child.kill("SIGKILL");
4173
+ } catch {
4174
+ }
4175
+ }
4176
+ await new Promise((res) => setTimeout(res, 800));
4177
+ }
4178
+ return snapshot(job);
4179
+ }
4180
+ list() {
4181
+ return [...this.jobs.values()].map(snapshot);
4182
+ }
4183
+ /**
4184
+ * Best-effort kill of every still-running job. Called on TUI shutdown
4185
+ * so dev servers don't outlive the Reasonix process. Resolves after
4186
+ * every child has closed or a hard deadline passes (3s total).
4187
+ */
4188
+ async shutdown(deadlineMs = 5e3) {
4189
+ const start = Date.now();
4190
+ const runningJobs = [...this.jobs.values()].filter((j) => j.running && j.child);
4191
+ if (runningJobs.length === 0) return;
4192
+ for (const job of runningJobs) {
4193
+ if (job.pid !== null) killProcessTree(job.pid, "SIGTERM");
4194
+ else
4195
+ try {
4196
+ job.child?.kill("SIGTERM");
4197
+ } catch {
4198
+ }
4199
+ }
4200
+ const allClose = Promise.all(runningJobs.map((j) => j.readyPromise));
4201
+ const elapsed = () => Date.now() - start;
4202
+ const graceMs = Math.min(1500, Math.max(0, deadlineMs / 2));
4203
+ await Promise.race([allClose, new Promise((res) => setTimeout(res, graceMs))]);
4204
+ for (const job of runningJobs) {
4205
+ if (!job.running) continue;
4206
+ if (job.pid !== null) killProcessTree(job.pid, "SIGKILL");
4207
+ else
4208
+ try {
4209
+ job.child?.kill("SIGKILL");
4210
+ } catch {
4211
+ }
4212
+ }
4213
+ const remaining = Math.max(800, deadlineMs - elapsed());
4214
+ await Promise.race([allClose, new Promise((res) => setTimeout(res, remaining))]);
4215
+ }
4216
+ /** Count of still-running jobs — drives the TUI status-bar indicator. */
4217
+ runningCount() {
4218
+ let n = 0;
4219
+ for (const job of this.jobs.values()) if (job.running) n++;
4220
+ return n;
4221
+ }
4222
+ };
4223
+ function snapshot(job) {
4224
+ return {
4225
+ id: job.id,
4226
+ command: job.command,
4227
+ pid: job.pid,
4228
+ startedAt: job.startedAt,
4229
+ exitCode: job.exitCode,
4230
+ output: job.output,
4231
+ totalBytesWritten: job.totalBytesWritten,
4232
+ running: job.running,
4233
+ spawnError: job.spawnError
4234
+ };
4235
+ }
4236
+
4237
+ // src/tools/shell.ts
3710
4238
  var DEFAULT_TIMEOUT_SEC = 60;
3711
4239
  var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
3712
4240
  var BUILTIN_ALLOWLIST = [
@@ -3875,10 +4403,10 @@ async function runCommand(cmd, opts) {
3875
4403
  };
3876
4404
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
3877
4405
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
3878
- return await new Promise((resolve7, reject) => {
4406
+ return await new Promise((resolve8, reject) => {
3879
4407
  let child;
3880
4408
  try {
3881
- child = spawn2(bin, args, effectiveSpawnOpts);
4409
+ child = spawn3(bin, args, effectiveSpawnOpts);
3882
4410
  } catch (err) {
3883
4411
  reject(err);
3884
4412
  return;
@@ -3908,7 +4436,7 @@ async function runCommand(cmd, opts) {
3908
4436
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
3909
4437
 
3910
4438
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
3911
- resolve7({ exitCode: code, output, timedOut });
4439
+ resolve8({ exitCode: code, output, timedOut });
3912
4440
  });
3913
4441
  });
3914
4442
  }
@@ -3916,16 +4444,16 @@ function resolveExecutable(cmd, opts = {}) {
3916
4444
  const platform = opts.platform ?? process.platform;
3917
4445
  if (platform !== "win32") return cmd;
3918
4446
  if (!cmd) return cmd;
3919
- if (cmd.includes("/") || cmd.includes("\\") || pathMod2.isAbsolute(cmd)) return cmd;
3920
- if (pathMod2.extname(cmd)) return cmd;
4447
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod3.isAbsolute(cmd)) return cmd;
4448
+ if (pathMod3.extname(cmd)) return cmd;
3921
4449
  const env = opts.env ?? process.env;
3922
4450
  const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
3923
- const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod2.delimiter);
4451
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod3.delimiter);
3924
4452
  const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
3925
4453
  const isFile = opts.isFile ?? defaultIsFile;
3926
4454
  for (const dir of pathDirs) {
3927
4455
  for (const ext of pathExt) {
3928
- const full = pathMod2.win32.join(dir, cmd + ext);
4456
+ const full = pathMod3.win32.join(dir, cmd + ext);
3929
4457
  if (isFile(full)) return full;
3930
4458
  }
3931
4459
  }
@@ -3995,8 +4523,8 @@ function withUtf8Codepage(cmdline) {
3995
4523
  function isBareWindowsName(s) {
3996
4524
  if (!s) return false;
3997
4525
  if (s.includes("/") || s.includes("\\")) return false;
3998
- if (pathMod2.isAbsolute(s)) return false;
3999
- if (pathMod2.extname(s)) return false;
4526
+ if (pathMod3.isAbsolute(s)) return false;
4527
+ if (pathMod3.extname(s)) return false;
4000
4528
  return true;
4001
4529
  }
4002
4530
  function quoteForCmdExe(arg) {
@@ -4015,12 +4543,13 @@ var NeedsConfirmationError = class extends Error {
4015
4543
  }
4016
4544
  };
4017
4545
  function registerShellTools(registry, opts) {
4018
- const rootDir = pathMod2.resolve(opts.rootDir);
4546
+ const rootDir = pathMod3.resolve(opts.rootDir);
4019
4547
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
4020
4548
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
4549
+ const jobs = opts.jobs ?? new JobRegistry();
4021
4550
  const getExtraAllowed = typeof opts.extraAllowed === "function" ? opts.extraAllowed : (() => {
4022
- const snapshot = opts.extraAllowed ?? [];
4023
- return () => snapshot;
4551
+ const snapshot2 = opts.extraAllowed ?? [];
4552
+ return () => snapshot2;
4024
4553
  })();
4025
4554
  const allowAll = opts.allowAll ?? false;
4026
4555
  registry.register({
@@ -4066,8 +4595,126 @@ function registerShellTools(registry, opts) {
4066
4595
  return formatCommandResult(cmd, result);
4067
4596
  }
4068
4597
  });
4598
+ registry.register({
4599
+ name: "run_background",
4600
+ description: "Spawn a long-running process (dev server, watcher, any command that doesn't naturally exit) and detach. Waits up to `waitSec` seconds for startup (or until the output matches a readiness signal like 'Local:', 'listening on', 'compiled successfully'), then returns the job id + startup preview. The process keeps running; call `job_output` to tail its logs, `stop_job` to kill it, `list_jobs` to see all running jobs. USE THIS \u2014 not `run_command` \u2014 for: npm/yarn/pnpm run dev, uvicorn / flask run, go run, cargo watch, tsc --watch, webpack serve, anything with 'dev' / 'serve' / 'watch' in the name.",
4601
+ parameters: {
4602
+ type: "object",
4603
+ properties: {
4604
+ command: {
4605
+ type: "string",
4606
+ description: "Full command line. Same quoting rules as run_command (no pipes / redirects / chaining)."
4607
+ },
4608
+ waitSec: {
4609
+ type: "integer",
4610
+ description: "Max seconds to wait for startup before returning. 0..30, default 3. A ready-signal match short-circuits this."
4611
+ }
4612
+ },
4613
+ required: ["command"]
4614
+ },
4615
+ fn: async (args, ctx) => {
4616
+ const cmd = args.command.trim();
4617
+ if (!cmd) throw new Error("run_background: empty command");
4618
+ if (!allowAll && !isAllowed(cmd, getExtraAllowed())) {
4619
+ throw new NeedsConfirmationError(cmd);
4620
+ }
4621
+ const result = await jobs.start(cmd, {
4622
+ cwd: rootDir,
4623
+ waitSec: args.waitSec,
4624
+ signal: ctx?.signal
4625
+ });
4626
+ return formatJobStart(result);
4627
+ }
4628
+ });
4629
+ registry.register({
4630
+ name: "job_output",
4631
+ description: "Read the latest output of a background job started with `run_background`. By default returns the tail of the buffer (last 80 lines). Pass `since` (the `byteLength` from a previous call) to stream only new content incrementally. Tells you whether the job is still running, so you can stop polling when it's done.",
4632
+ readOnly: true,
4633
+ parameters: {
4634
+ type: "object",
4635
+ properties: {
4636
+ jobId: { type: "integer", description: "Job id returned by run_background." },
4637
+ since: {
4638
+ type: "integer",
4639
+ description: "Return only output written past this byte offset (for incremental polling)."
4640
+ },
4641
+ tailLines: {
4642
+ type: "integer",
4643
+ description: "Cap the returned slice to the last N lines. Default 80, 0 = unlimited."
4644
+ }
4645
+ },
4646
+ required: ["jobId"]
4647
+ },
4648
+ fn: async (args) => {
4649
+ const out = jobs.read(args.jobId, {
4650
+ since: args.since,
4651
+ tailLines: args.tailLines ?? 80
4652
+ });
4653
+ if (!out) return `job ${args.jobId}: not found (use list_jobs)`;
4654
+ return formatJobRead(args.jobId, out);
4655
+ }
4656
+ });
4657
+ registry.register({
4658
+ name: "stop_job",
4659
+ description: "Stop a background job started with `run_background`. SIGTERM first; SIGKILL after a short grace period if it doesn't exit cleanly. Returns the final output + exit code. Safe to call on an already-exited job.",
4660
+ parameters: {
4661
+ type: "object",
4662
+ properties: {
4663
+ jobId: { type: "integer" }
4664
+ },
4665
+ required: ["jobId"]
4666
+ },
4667
+ fn: async (args) => {
4668
+ const rec = await jobs.stop(args.jobId);
4669
+ if (!rec) return `job ${args.jobId}: not found`;
4670
+ return formatJobStop(rec);
4671
+ }
4672
+ });
4673
+ registry.register({
4674
+ name: "list_jobs",
4675
+ description: "List every background job started this session \u2014 running and exited \u2014 with id, command, pid, status. Use when you've lost track of which job_id corresponds to which process, or to see what's still alive.",
4676
+ readOnly: true,
4677
+ parameters: { type: "object", properties: {} },
4678
+ fn: async () => {
4679
+ const all = jobs.list();
4680
+ if (all.length === 0) return "(no background jobs started this session)";
4681
+ return all.map(formatJobRow).join("\n");
4682
+ }
4683
+ });
4069
4684
  return registry;
4070
4685
  }
4686
+ function formatJobStart(r) {
4687
+ const header2 = r.stillRunning ? `[job ${r.jobId} started \xB7 pid ${r.pid ?? "?"} \xB7 ${r.readyMatched ? "READY signal matched" : "running (no ready signal yet)"}]` : r.exitCode !== null ? `[job ${r.jobId} exited during startup \xB7 exit ${r.exitCode}]` : `[job ${r.jobId} failed to start]`;
4688
+ return r.preview ? `${header2}
4689
+ ${r.preview}` : header2;
4690
+ }
4691
+ function formatJobRead(jobId, r) {
4692
+ const status = r.running ? `running \xB7 pid ${r.pid ?? "?"}` : r.exitCode !== null ? `exited ${r.exitCode}` : r.spawnError ? `failed (${r.spawnError})` : "stopped";
4693
+ const header2 = `[job ${jobId} \xB7 ${status} \xB7 byteLength=${r.byteLength}]
4694
+ $ ${r.command}`;
4695
+ return r.output ? `${header2}
4696
+ ${r.output}` : header2;
4697
+ }
4698
+ function formatJobStop(r) {
4699
+ const running = r.running ? "still running (SIGKILL may be pending)" : `exit ${r.exitCode ?? "?"}`;
4700
+ const tail = tailLines(r.output, 40);
4701
+ const header2 = `[job ${r.id} stopped \xB7 ${running}]
4702
+ $ ${r.command}`;
4703
+ return tail ? `${header2}
4704
+ ${tail}` : header2;
4705
+ }
4706
+ function formatJobRow(r) {
4707
+ const age = ((Date.now() - r.startedAt) / 1e3).toFixed(1);
4708
+ const state = r.running ? `running \xB7 pid ${r.pid ?? "?"}` : r.exitCode !== null ? `exit ${r.exitCode}` : r.spawnError ? "failed" : "stopped";
4709
+ return ` ${String(r.id).padStart(3)} ${state.padEnd(24)} ${age}s ago $ ${r.command}`;
4710
+ }
4711
+ function tailLines(s, n) {
4712
+ if (!s) return "";
4713
+ const lines = s.split("\n");
4714
+ if (lines.length <= n) return s;
4715
+ const dropped = lines.length - n;
4716
+ return [`[\u2026 ${dropped} earlier lines \u2026]`, ...lines.slice(-n)].join("\n");
4717
+ }
4071
4718
  function formatCommandResult(cmd, r) {
4072
4719
  const header2 = r.timedOut ? `$ ${cmd}
4073
4720
  [killed after timeout]` : `$ ${cmd}
@@ -4261,11 +4908,11 @@ ${i + 1}. ${r.title}`);
4261
4908
 
4262
4909
  // src/env.ts
4263
4910
  import { readFileSync as readFileSync6 } from "fs";
4264
- import { resolve as resolve4 } from "path";
4911
+ import { resolve as resolve5 } from "path";
4265
4912
  function loadDotenv(path = ".env") {
4266
4913
  let raw;
4267
4914
  try {
4268
- raw = readFileSync6(resolve4(process.cwd(), path), "utf8");
4915
+ raw = readFileSync6(resolve5(process.cwd(), path), "utf8");
4269
4916
  } catch {
4270
4917
  return;
4271
4918
  }
@@ -4978,7 +5625,7 @@ var McpClient = class {
4978
5625
  const id = this.nextId++;
4979
5626
  const frame = { jsonrpc: "2.0", id, method, params };
4980
5627
  let abortHandler = null;
4981
- const promise = new Promise((resolve7, reject) => {
5628
+ const promise = new Promise((resolve8, reject) => {
4982
5629
  const timeout = setTimeout(() => {
4983
5630
  this.pending.delete(id);
4984
5631
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -4987,7 +5634,7 @@ var McpClient = class {
4987
5634
  );
4988
5635
  }, this.requestTimeoutMs);
4989
5636
  this.pending.set(id, {
4990
- resolve: resolve7,
5637
+ resolve: resolve8,
4991
5638
  reject,
4992
5639
  timeout
4993
5640
  });
@@ -5069,7 +5716,7 @@ var McpClient = class {
5069
5716
  };
5070
5717
 
5071
5718
  // src/mcp/stdio.ts
5072
- import { spawn as spawn3 } from "child_process";
5719
+ import { spawn as spawn4 } from "child_process";
5073
5720
  var StdioTransport = class {
5074
5721
  child;
5075
5722
  queue = [];
@@ -5084,14 +5731,14 @@ var StdioTransport = class {
5084
5731
  opts.command,
5085
5732
  ...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
5086
5733
  ].join(" ");
5087
- this.child = spawn3(line, [], {
5734
+ this.child = spawn4(line, [], {
5088
5735
  env,
5089
5736
  cwd: opts.cwd,
5090
5737
  stdio: ["pipe", "pipe", "inherit"],
5091
5738
  shell: true
5092
5739
  });
5093
5740
  } else {
5094
- this.child = spawn3(opts.command, opts.args ?? [], {
5741
+ this.child = spawn4(opts.command, opts.args ?? [], {
5095
5742
  env,
5096
5743
  cwd: opts.cwd,
5097
5744
  stdio: ["pipe", "pipe", "inherit"]
@@ -5110,12 +5757,12 @@ var StdioTransport = class {
5110
5757
  }
5111
5758
  async send(message) {
5112
5759
  if (this.closed) throw new Error("MCP transport is closed");
5113
- return new Promise((resolve7, reject) => {
5760
+ return new Promise((resolve8, reject) => {
5114
5761
  const line = `${JSON.stringify(message)}
5115
5762
  `;
5116
5763
  this.child.stdin.write(line, "utf8", (err) => {
5117
5764
  if (err) reject(err);
5118
- else resolve7();
5765
+ else resolve8();
5119
5766
  });
5120
5767
  });
5121
5768
  }
@@ -5126,8 +5773,8 @@ var StdioTransport = class {
5126
5773
  continue;
5127
5774
  }
5128
5775
  if (this.closed) return;
5129
- const next = await new Promise((resolve7) => {
5130
- this.waiters.push(resolve7);
5776
+ const next = await new Promise((resolve8) => {
5777
+ this.waiters.push(resolve8);
5131
5778
  });
5132
5779
  if (next === null) return;
5133
5780
  yield next;
@@ -5193,8 +5840,8 @@ var SseTransport = class {
5193
5840
  constructor(opts) {
5194
5841
  this.url = opts.url;
5195
5842
  this.headers = opts.headers ?? {};
5196
- this.endpointReady = new Promise((resolve7, reject) => {
5197
- this.resolveEndpoint = resolve7;
5843
+ this.endpointReady = new Promise((resolve8, reject) => {
5844
+ this.resolveEndpoint = resolve8;
5198
5845
  this.rejectEndpoint = reject;
5199
5846
  });
5200
5847
  this.endpointReady.catch(() => void 0);
@@ -5221,8 +5868,8 @@ var SseTransport = class {
5221
5868
  continue;
5222
5869
  }
5223
5870
  if (this.closed) return;
5224
- const next = await new Promise((resolve7) => {
5225
- this.waiters.push(resolve7);
5871
+ const next = await new Promise((resolve8) => {
5872
+ this.waiters.push(resolve8);
5226
5873
  });
5227
5874
  if (next === null) return;
5228
5875
  yield next;
@@ -5422,7 +6069,7 @@ async function trySection(load) {
5422
6069
 
5423
6070
  // src/code/edit-blocks.ts
5424
6071
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
5425
- import { dirname as dirname5, resolve as resolve5 } from "path";
6072
+ import { dirname as dirname5, resolve as resolve6 } from "path";
5426
6073
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
5427
6074
  function parseEditBlocks(text) {
5428
6075
  const out = [];
@@ -5440,8 +6087,8 @@ function parseEditBlocks(text) {
5440
6087
  return out;
5441
6088
  }
5442
6089
  function applyEditBlock(block, rootDir) {
5443
- const absRoot = resolve5(rootDir);
5444
- const absTarget = resolve5(absRoot, block.path);
6090
+ const absRoot = resolve6(rootDir);
6091
+ const absTarget = resolve6(absRoot, block.path);
5445
6092
  if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
5446
6093
  return {
5447
6094
  path: block.path,
@@ -5490,14 +6137,26 @@ function applyEditBlock(block, rootDir) {
5490
6137
  function applyEditBlocks(blocks, rootDir) {
5491
6138
  return blocks.map((b) => applyEditBlock(b, rootDir));
5492
6139
  }
6140
+ function toWholeFileEditBlock(path, content, rootDir) {
6141
+ const abs = resolve6(rootDir, path);
6142
+ let search = "";
6143
+ if (existsSync6(abs)) {
6144
+ try {
6145
+ search = readFileSync8(abs, "utf8");
6146
+ } catch {
6147
+ search = "";
6148
+ }
6149
+ }
6150
+ return { path, search, replace: content, offset: 0 };
6151
+ }
5493
6152
  function snapshotBeforeEdits(blocks, rootDir) {
5494
- const absRoot = resolve5(rootDir);
6153
+ const absRoot = resolve6(rootDir);
5495
6154
  const seen = /* @__PURE__ */ new Set();
5496
6155
  const snapshots = [];
5497
6156
  for (const b of blocks) {
5498
6157
  if (seen.has(b.path)) continue;
5499
6158
  seen.add(b.path);
5500
- const abs = resolve5(absRoot, b.path);
6159
+ const abs = resolve6(absRoot, b.path);
5501
6160
  if (!existsSync6(abs)) {
5502
6161
  snapshots.push({ path: b.path, prevContent: null });
5503
6162
  continue;
@@ -5511,9 +6170,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
5511
6170
  return snapshots;
5512
6171
  }
5513
6172
  function restoreSnapshots(snapshots, rootDir) {
5514
- const absRoot = resolve5(rootDir);
6173
+ const absRoot = resolve6(rootDir);
5515
6174
  return snapshots.map((snap) => {
5516
- const abs = resolve5(absRoot, snap.path);
6175
+ const abs = resolve6(absRoot, snap.path);
5517
6176
  if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
5518
6177
  return {
5519
6178
  path: snap.path,
@@ -5800,11 +6459,11 @@ function formatLogSize(path = defaultUsageLogPath()) {
5800
6459
  // src/cli/commands/chat.tsx
5801
6460
  import { existsSync as existsSync11, statSync as statSync6 } from "fs";
5802
6461
  import { render } from "ink";
5803
- import React17, { useState as useState7 } from "react";
6462
+ import React18, { useState as useState8 } from "react";
5804
6463
 
5805
6464
  // src/cli/ui/App.tsx
5806
- import { Box as Box13, Static, Text as Text13, useApp, useInput as useInput4 } from "ink";
5807
- import React14, { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState5 } from "react";
6465
+ import { Box as Box14, Static, Text as Text14, useApp, useInput as useInput5 } from "ink";
6466
+ import React15, { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState6 } from "react";
5808
6467
 
5809
6468
  // src/code/diff-preview.ts
5810
6469
  function formatEditBlockDiff(block, opts = {}) {
@@ -6027,13 +6686,108 @@ function FileRow({ path, isSelected }) {
6027
6686
  return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, marker, " ", base, dir ? ` ${dir}` : ""));
6028
6687
  }
6029
6688
 
6689
+ // src/cli/ui/EditConfirm.tsx
6690
+ import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
6691
+ import React2, { useMemo, useState } from "react";
6692
+ var MODAL_OVERHEAD_ROWS = 18;
6693
+ var MIN_DIFF_ROWS = 8;
6694
+ function EditConfirm({ block, onChoose }) {
6695
+ const { stdout: stdout2 } = useStdout();
6696
+ const rows = stdout2?.rows ?? 40;
6697
+ const budget = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
6698
+ const allLines = useMemo(
6699
+ () => formatEditBlockDiff(block, { contextLines: 2, maxLines: 1e5, indent: " " }),
6700
+ [block]
6701
+ );
6702
+ const [scroll, setScroll] = useState(0);
6703
+ const maxScroll = Math.max(0, allLines.length - budget);
6704
+ const effectiveScroll = Math.min(scroll, maxScroll);
6705
+ useInput((input, key) => {
6706
+ if (key.return || input === "y") {
6707
+ onChoose("apply");
6708
+ return;
6709
+ }
6710
+ if (input === "n") {
6711
+ onChoose("reject");
6712
+ return;
6713
+ }
6714
+ if (input === "a") {
6715
+ onChoose("apply-rest-of-turn");
6716
+ return;
6717
+ }
6718
+ if (input === "A") {
6719
+ onChoose("flip-to-auto");
6720
+ return;
6721
+ }
6722
+ if (key.downArrow || input === "j") {
6723
+ setScroll((s) => Math.min(maxScroll, s + 1));
6724
+ return;
6725
+ }
6726
+ if (key.upArrow || input === "k") {
6727
+ setScroll((s) => Math.max(0, s - 1));
6728
+ return;
6729
+ }
6730
+ if (key.pageDown || input === " " || input === "f") {
6731
+ setScroll((s) => Math.min(maxScroll, s + Math.max(1, budget - 2)));
6732
+ return;
6733
+ }
6734
+ if (key.pageUp || input === "b") {
6735
+ setScroll((s) => Math.max(0, s - Math.max(1, budget - 2)));
6736
+ return;
6737
+ }
6738
+ if (input === "g") {
6739
+ setScroll(0);
6740
+ return;
6741
+ }
6742
+ if (input === "G") {
6743
+ setScroll(maxScroll);
6744
+ return;
6745
+ }
6746
+ });
6747
+ const isNew = block.search === "";
6748
+ const removed = isNew ? 0 : (block.search.match(/\n/g)?.length ?? 0) + 1;
6749
+ const added = block.replace === "" ? 0 : (block.replace.match(/\n/g)?.length ?? 0) + 1;
6750
+ const tag = isNew ? "NEW" : "EDIT";
6751
+ const visibleLines = allLines.slice(effectiveScroll, effectiveScroll + budget);
6752
+ const hiddenAbove = effectiveScroll;
6753
+ const hiddenBelow = Math.max(0, allLines.length - effectiveScroll - budget);
6754
+ const totalLines = allLines.length;
6755
+ const showScrollHud = hiddenAbove + hiddenBelow > 0;
6756
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u25B8 model wants to edit a file")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, null, /* @__PURE__ */ React2.createElement(Text2, { color: isNew ? "green" : "yellow", bold: true }, `[${tag}] `), /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, block.path), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, ` (-${removed} +${added} lines)`), showScrollHud ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, ` \xB7 viewing ${effectiveScroll + 1}-${effectiveScroll + visibleLines.length}/${totalLines}`) : null)), hiddenAbove > 0 ? /* @__PURE__ */ React2.createElement(
6757
+ Text2,
6758
+ {
6759
+ dimColor: true
6760
+ },
6761
+ ` \u2191 ${hiddenAbove} line${hiddenAbove === 1 ? "" : "s"} above (\u2191/k or PgUp)`
6762
+ ) : null, /* @__PURE__ */ React2.createElement(Box2, { marginTop: hiddenAbove > 0 ? 0 : 1, flexDirection: "column" }, visibleLines.map((line, i) => {
6763
+ const trimmed = line.trimStart();
6764
+ const color = trimmed.startsWith("+") ? "green" : trimmed.startsWith("-") ? "red" : void 0;
6765
+ const dim = !color;
6766
+ return /* @__PURE__ */ React2.createElement(
6767
+ Text2,
6768
+ {
6769
+ key: `diff-${effectiveScroll}-${i}`,
6770
+ color,
6771
+ dimColor: dim
6772
+ },
6773
+ line
6774
+ );
6775
+ })), hiddenBelow > 0 ? /* @__PURE__ */ React2.createElement(
6776
+ Text2,
6777
+ {
6778
+ dimColor: true
6779
+ },
6780
+ ` \u2193 ${hiddenBelow} line${hiddenBelow === 1 ? "" : "s"} below (\u2193/j or Space/PgDn)`
6781
+ ) : null, /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "[", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", bold: true }, "y"), "/Enter] apply \xB7 [", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", bold: true }, "n"), "] reject \xB7 [", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", bold: true }, "a"), "] apply rest \xB7 [", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", bold: true }, "A"), "] flip AUTO \xB7 [", /* @__PURE__ */ React2.createElement(Text2, { color: "cyan", bold: true }, "\u2191\u2193/Space"), "] scroll \xB7 [Esc] abort")));
6782
+ }
6783
+
6030
6784
  // src/cli/ui/EventLog.tsx
6031
- import { Box as Box4, Text as Text4, useStdout } from "ink";
6032
- import React5 from "react";
6785
+ import { Box as Box5, Text as Text5, useStdout as useStdout2 } from "ink";
6786
+ import React6 from "react";
6033
6787
 
6034
6788
  // src/cli/ui/PlanStateBlock.tsx
6035
- import { Box as Box2, Text as Text2 } from "ink";
6036
- import React2 from "react";
6789
+ import { Box as Box3, Text as Text3 } from "ink";
6790
+ import React3 from "react";
6037
6791
  function PlanStateBlock({ planState }) {
6038
6792
  const fields = [];
6039
6793
  if (planState.subgoals.length) fields.push(["subgoals", planState.subgoals, "cyan", false]);
@@ -6044,14 +6798,14 @@ function PlanStateBlock({ planState }) {
6044
6798
  if (planState.rejectedPaths.length)
6045
6799
  fields.push(["rejected", planState.rejectedPaths, "red", true]);
6046
6800
  if (fields.length === 0) return null;
6047
- return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginBottom: 1 }, fields.map(([label, items, color, dim]) => /* @__PURE__ */ React2.createElement(Text2, { key: label }, /* @__PURE__ */ React2.createElement(Text2, { color, bold: true, dimColor: dim }, "\u2039 ", label), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, ` (${items.length})`), /* @__PURE__ */ React2.createElement(Text2, null, `: ${items.join(" \xB7 ")}`))));
6801
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginBottom: 1 }, fields.map(([label, items, color, dim]) => /* @__PURE__ */ React3.createElement(Text3, { key: label }, /* @__PURE__ */ React3.createElement(Text3, { color, bold: true, dimColor: dim }, "\u2039 ", label), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, ` (${items.length})`), /* @__PURE__ */ React3.createElement(Text3, null, `: ${items.join(" \xB7 ")}`))));
6048
6802
  }
6049
6803
 
6050
6804
  // src/cli/ui/markdown.tsx
6051
6805
  import { readFileSync as readFileSync12, statSync as statSync5 } from "fs";
6052
6806
  import { isAbsolute as isAbsolute4, join as join10 } from "path";
6053
- import { Box as Box3, Text as Text3 } from "ink";
6054
- import React3 from "react";
6807
+ import { Box as Box4, Text as Text4 } from "ink";
6808
+ import React4 from "react";
6055
6809
  var SUPERSCRIPT = {
6056
6810
  "0": "\u2070",
6057
6811
  "1": "\xB9",
@@ -6176,57 +6930,57 @@ function InlineMd({
6176
6930
  for (const m of text.matchAll(INLINE_RE)) {
6177
6931
  const start = m.index ?? 0;
6178
6932
  if (start > last) {
6179
- parts.push(/* @__PURE__ */ React3.createElement(Text3, { key: `t${idx++}` }, text.slice(last, start)));
6933
+ parts.push(/* @__PURE__ */ React4.createElement(Text4, { key: `t${idx++}` }, text.slice(last, start)));
6180
6934
  }
6181
6935
  if (m[2] !== void 0 && m[3] !== void 0) {
6182
6936
  const linkText = m[2];
6183
6937
  const url = m[3];
6184
6938
  if (isExternalUrl(url)) {
6185
6939
  parts.push(
6186
- /* @__PURE__ */ React3.createElement(Text3, { key: `l${idx++}`, color: "blue", underline: true }, linkText)
6940
+ /* @__PURE__ */ React4.createElement(Text4, { key: `l${idx++}`, color: "blue", underline: true }, linkText)
6187
6941
  );
6188
6942
  } else {
6189
6943
  const status = citations?.get(url);
6190
6944
  if (status && !status.ok) {
6191
6945
  parts.push(
6192
- /* @__PURE__ */ React3.createElement(Text3, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u2717`)
6946
+ /* @__PURE__ */ React4.createElement(Text4, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u2717`)
6193
6947
  );
6194
6948
  } else {
6195
6949
  parts.push(
6196
- /* @__PURE__ */ React3.createElement(Text3, { key: `l${idx++}`, color: "cyan", underline: true }, linkText)
6950
+ /* @__PURE__ */ React4.createElement(Text4, { key: `l${idx++}`, color: "cyan", underline: true }, linkText)
6197
6951
  );
6198
6952
  }
6199
6953
  }
6200
6954
  } else if (m[4] !== void 0) {
6201
6955
  parts.push(
6202
- /* @__PURE__ */ React3.createElement(Text3, { key: `b${idx++}`, bold: true }, m[4])
6956
+ /* @__PURE__ */ React4.createElement(Text4, { key: `b${idx++}`, bold: true }, m[4])
6203
6957
  );
6204
6958
  } else if (m[5] !== void 0) {
6205
6959
  const stripped = m[5].replace(/^(\w+)\s+/, "");
6206
6960
  parts.push(
6207
- /* @__PURE__ */ React3.createElement(Text3, { key: `c${idx++}`, color: "yellow" }, stripped)
6961
+ /* @__PURE__ */ React4.createElement(Text4, { key: `c${idx++}`, color: "yellow" }, stripped)
6208
6962
  );
6209
6963
  } else if (m[6] !== void 0) {
6210
6964
  parts.push(
6211
- /* @__PURE__ */ React3.createElement(Text3, { key: `c${idx++}`, color: "yellow" }, m[6])
6965
+ /* @__PURE__ */ React4.createElement(Text4, { key: `c${idx++}`, color: "yellow" }, m[6])
6212
6966
  );
6213
6967
  } else if (m[7] !== void 0) {
6214
6968
  parts.push(
6215
- /* @__PURE__ */ React3.createElement(Text3, { key: `i${idx++}`, italic: true }, m[7])
6969
+ /* @__PURE__ */ React4.createElement(Text4, { key: `i${idx++}`, italic: true }, m[7])
6216
6970
  );
6217
6971
  }
6218
6972
  last = start + m[0].length;
6219
6973
  }
6220
6974
  if (last < text.length) {
6221
- parts.push(/* @__PURE__ */ React3.createElement(Text3, { key: `t${idx++}` }, text.slice(last)));
6975
+ parts.push(/* @__PURE__ */ React4.createElement(Text4, { key: `t${idx++}` }, text.slice(last)));
6222
6976
  }
6223
6977
  if (padTo !== void 0) {
6224
6978
  const seen = visibleWidth(text);
6225
6979
  if (seen < padTo) {
6226
- parts.push(/* @__PURE__ */ React3.createElement(Text3, { key: `pad${idx++}` }, " ".repeat(padTo - seen)));
6980
+ parts.push(/* @__PURE__ */ React4.createElement(Text4, { key: `pad${idx++}` }, " ".repeat(padTo - seen)));
6227
6981
  }
6228
6982
  }
6229
- return /* @__PURE__ */ React3.createElement(Text3, null, parts);
6983
+ return /* @__PURE__ */ React4.createElement(Text4, null, parts);
6230
6984
  }
6231
6985
  function stripInlineMarkup(s) {
6232
6986
  return s.replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, "$1").replace(/\*\*([^*\n]+?)\*\*/g, "$1").replace(/```([^\n]+?)```/g, (_m, c) => c.replace(/^(\w+)\s+/, "")).replace(/`([^`\n]+?)`/g, "$1").replace(/(?<![*\w])\*([^*\n]+?)\*(?!\w)/g, "$1");
@@ -6422,19 +7176,19 @@ function parseBlocks(raw) {
6422
7176
  function BlockView({ block, citations }) {
6423
7177
  switch (block.kind) {
6424
7178
  case "heading":
6425
- return /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "cyan" }, /* @__PURE__ */ React3.createElement(InlineMd, { text: block.text, citations }));
7179
+ return /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "cyan" }, /* @__PURE__ */ React4.createElement(InlineMd, { text: block.text, citations }));
6426
7180
  case "paragraph":
6427
- return /* @__PURE__ */ React3.createElement(InlineMd, { text: block.text, citations });
7181
+ return /* @__PURE__ */ React4.createElement(InlineMd, { text: block.text, citations });
6428
7182
  case "bullet":
6429
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React3.createElement(Box3, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React3.createElement(InlineMd, { text: item, citations }))));
7183
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React4.createElement(Box4, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React4.createElement(InlineMd, { text: item, citations }))));
6430
7184
  case "code":
6431
- return /* @__PURE__ */ React3.createElement(Box3, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, block.text));
7185
+ return /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, block.text));
6432
7186
  case "edit-block":
6433
- return /* @__PURE__ */ React3.createElement(EditBlockRow, { block });
7187
+ return /* @__PURE__ */ React4.createElement(EditBlockRow, { block });
6434
7188
  case "table":
6435
- return /* @__PURE__ */ React3.createElement(TableBlockRow, { block, citations });
7189
+ return /* @__PURE__ */ React4.createElement(TableBlockRow, { block, citations });
6436
7190
  case "hr":
6437
- return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
7191
+ return /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
6438
7192
  }
6439
7193
  }
6440
7194
  function splitTableRow(line) {
@@ -6452,14 +7206,14 @@ function TableBlockRow({ block, citations }) {
6452
7206
  widths.push(Math.min(40, Math.max(3, ...cellLengths)));
6453
7207
  }
6454
7208
  const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u253C\u2500");
6455
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(Box3, null, block.header.map((cell, ci) => (
7209
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Box4, null, block.header.map((cell, ci) => (
6456
7210
  // biome-ignore lint/suspicious/noArrayIndexKey: table columns never reorder — derived from a static header array
6457
- /* @__PURE__ */ React3.createElement(Text3, { key: `h-${ci}`, bold: true, color: "cyan" }, /* @__PURE__ */ React3.createElement(InlineMd, { text: cell, padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
6458
- ))), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, separator), block.rows.map((row2, ri) => (
7211
+ /* @__PURE__ */ React4.createElement(Text4, { key: `h-${ci}`, bold: true, color: "cyan" }, /* @__PURE__ */ React4.createElement(InlineMd, { text: cell, padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
7212
+ ))), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, separator), block.rows.map((row2, ri) => (
6459
7213
  // biome-ignore lint/suspicious/noArrayIndexKey: table rows render in source order and don't reorder
6460
- /* @__PURE__ */ React3.createElement(Box3, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
7214
+ /* @__PURE__ */ React4.createElement(Box4, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
6461
7215
  // biome-ignore lint/suspicious/noArrayIndexKey: same — column axis is fixed by the table shape
6462
- /* @__PURE__ */ React3.createElement(Text3, { key: `c-${ri}-${ci}` }, /* @__PURE__ */ React3.createElement(InlineMd, { text: row2[ci] ?? "", padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
7216
+ /* @__PURE__ */ React4.createElement(Text4, { key: `c-${ri}-${ci}` }, /* @__PURE__ */ React4.createElement(InlineMd, { text: row2[ci] ?? "", padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
6463
7217
  )))
6464
7218
  )));
6465
7219
  }
@@ -6479,44 +7233,44 @@ function EditBlockRow({ block }) {
6479
7233
  const isNewFile = block.search.length === 0;
6480
7234
  const searchLines = block.search.split("\n");
6481
7235
  const replaceLines = block.replace.split("\n");
6482
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "cyan" }, block.filename), isNewFile ? /* @__PURE__ */ React3.createElement(Text3, { color: "green", bold: true }, " (new file)") : null), isNewFile ? null : /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, searchLines.map((line, i) => /* @__PURE__ */ React3.createElement(Text3, { key: `s-${i}-${line.length}`, color: "red" }, `- ${line}`))), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: isNewFile ? 1 : 0 }, replaceLines.map((line, i) => /* @__PURE__ */ React3.createElement(Text3, { key: `r-${i}-${line.length}`, color: "green" }, `+ ${line}`))));
7236
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "cyan" }, block.filename), isNewFile ? /* @__PURE__ */ React4.createElement(Text4, { color: "green", bold: true }, " (new file)") : null), isNewFile ? null : /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginTop: 1 }, searchLines.map((line, i) => /* @__PURE__ */ React4.createElement(Text4, { key: `s-${i}-${line.length}`, color: "red" }, `- ${line}`))), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginTop: isNewFile ? 1 : 0 }, replaceLines.map((line, i) => /* @__PURE__ */ React4.createElement(Text4, { key: `r-${i}-${line.length}`, color: "green" }, `+ ${line}`))));
6483
7237
  }
6484
7238
  function Markdown({ text, projectRoot }) {
6485
7239
  const cleaned = stripMath(text);
6486
7240
  const root = projectRoot ?? process.cwd();
6487
- const citations = React3.useMemo(() => collectCitations(cleaned, root), [cleaned, root]);
6488
- const blocks = React3.useMemo(() => parseBlocks(cleaned), [cleaned]);
7241
+ const citations = React4.useMemo(() => collectCitations(cleaned, root), [cleaned, root]);
7242
+ const blocks = React4.useMemo(() => parseBlocks(cleaned), [cleaned]);
6489
7243
  const broken = [];
6490
7244
  for (const [url, status] of citations) {
6491
7245
  if (!status.ok) broken.push({ url, reason: status.reason });
6492
7246
  }
6493
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React3.createElement(BlockView, { key: `${i}-${b.kind}`, block: b, citations })), broken.length > 0 ? /* @__PURE__ */ React3.createElement(BrokenCitationsBlock, { items: broken }) : null);
7247
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React4.createElement(BlockView, { key: `${i}-${b.kind}`, block: b, citations })), broken.length > 0 ? /* @__PURE__ */ React4.createElement(BrokenCitationsBlock, { items: broken }) : null);
6494
7248
  }
6495
7249
  function BrokenCitationsBlock({ items }) {
6496
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, `\u26A0 ${items.length} broken citation${items.length > 1 ? "s" : ""} \u2014 the model referenced paths or lines that don't exist`), items.map((b, i) => (
7250
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "red", bold: true }, `\u26A0 ${items.length} broken citation${items.length > 1 ? "s" : ""} \u2014 the model referenced paths or lines that don't exist`), items.map((b, i) => (
6497
7251
  // biome-ignore lint/suspicious/noArrayIndexKey: list is derived from a Map iteration order, stable per render
6498
- /* @__PURE__ */ React3.createElement(Text3, { key: `bc-${i}`, color: "red" }, ` \u2717 ${b.url} \u2192 ${b.reason}`)
7252
+ /* @__PURE__ */ React4.createElement(Text4, { key: `bc-${i}`, color: "red" }, ` \u2717 ${b.url} \u2192 ${b.reason}`)
6499
7253
  )));
6500
7254
  }
6501
7255
 
6502
7256
  // src/cli/ui/ticker.tsx
6503
- import React4, { createContext, useContext, useEffect, useState } from "react";
7257
+ import React5, { createContext, useContext, useEffect, useState as useState2 } from "react";
6504
7258
  var TICK_MS = 120;
6505
7259
  var TickContext = createContext(0);
6506
7260
  function TickerProvider({ children, disabled }) {
6507
- const [tick, setTick] = useState(0);
7261
+ const [tick, setTick] = useState2(0);
6508
7262
  useEffect(() => {
6509
7263
  if (disabled) return;
6510
7264
  const id = setInterval(() => setTick((t) => t + 1), TICK_MS);
6511
7265
  return () => clearInterval(id);
6512
7266
  }, [disabled]);
6513
- return /* @__PURE__ */ React4.createElement(TickContext.Provider, { value: tick }, children);
7267
+ return /* @__PURE__ */ React5.createElement(TickContext.Provider, { value: tick }, children);
6514
7268
  }
6515
7269
  function useTick() {
6516
7270
  return useContext(TickContext);
6517
7271
  }
6518
7272
  function useElapsedSeconds() {
6519
- const [start] = useState(() => Date.now());
7273
+ const [start] = useState2(() => Date.now());
6520
7274
  useTick();
6521
7275
  return Math.floor((Date.now() - start) / 1e3);
6522
7276
  }
@@ -6536,18 +7290,18 @@ function RoleGlyph({
6536
7290
  glyph,
6537
7291
  color
6538
7292
  }) {
6539
- return /* @__PURE__ */ React5.createElement(Text4, { color, bold: true }, glyph);
7293
+ return /* @__PURE__ */ React6.createElement(Text5, { color, bold: true }, glyph);
6540
7294
  }
6541
- var EventRow = React5.memo(function EventRow2({
7295
+ var EventRow = React6.memo(function EventRow2({
6542
7296
  event,
6543
7297
  projectRoot
6544
7298
  }) {
6545
7299
  if (event.role === "user") {
6546
- return /* @__PURE__ */ React5.createElement(Box4, { marginTop: event.leadSeparator ? 1 : 0 }, /* @__PURE__ */ React5.createElement(RoleGlyph, { glyph: ROLE_GLYPH.user, color: "cyan" }), /* @__PURE__ */ React5.createElement(Text4, null, " ", event.text));
7300
+ return /* @__PURE__ */ React6.createElement(Box5, { marginTop: event.leadSeparator ? 1 : 0 }, /* @__PURE__ */ React6.createElement(RoleGlyph, { glyph: ROLE_GLYPH.user, color: "cyan" }), /* @__PURE__ */ React6.createElement(Text5, null, " ", event.text));
6547
7301
  }
6548
7302
  if (event.role === "assistant") {
6549
- if (event.streaming) return /* @__PURE__ */ React5.createElement(StreamingAssistant, { event });
6550
- return /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(RoleGlyph, { glyph: ROLE_GLYPH.assistant, color: "green" }), event.stats ? /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, ` ${event.stats.model}`) : null), /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, event.branch ? /* @__PURE__ */ React5.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React5.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React5.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React5.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React5.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React5.createElement(Text4, { color: "magenta" }, event.repair) : null));
7303
+ if (event.streaming) return /* @__PURE__ */ React6.createElement(StreamingAssistant, { event });
7304
+ return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(RoleGlyph, { glyph: ROLE_GLYPH.assistant, color: "green" }), event.stats ? /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, ` ${event.stats.model}`) : null), /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, event.branch ? /* @__PURE__ */ React6.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React6.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React6.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React6.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React6.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React6.createElement(Text5, { color: "magenta" }, event.repair) : null));
6551
7305
  }
6552
7306
  if (event.role === "tool") {
6553
7307
  const isError = event.text.startsWith("ERROR:");
@@ -6555,31 +7309,31 @@ var EventRow = React5.memo(function EventRow2({
6555
7309
  const glyph = isError ? ROLE_GLYPH.toolErr : ROLE_GLYPH.toolOk;
6556
7310
  const marker = isError ? "\u2717" : "\u2192";
6557
7311
  const isEditFile = (event.toolName === "edit_file" || event.toolName?.endsWith("_edit_file")) && !isError;
6558
- return /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(RoleGlyph, { glyph, color }), /* @__PURE__ */ React5.createElement(Text4, { color, bold: true }, ` ${event.toolName ?? "?"}`), /* @__PURE__ */ React5.createElement(Text4, { color, dimColor: true }, ` ${marker}`)), /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, isEditFile ? /* @__PURE__ */ React5.createElement(EditFileDiff, { text: event.text }) : /* @__PURE__ */ React5.createElement(Text4, { color: isError ? "red" : void 0, dimColor: !isError }, truncate2(event.text, 400))));
7312
+ return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(RoleGlyph, { glyph, color }), /* @__PURE__ */ React6.createElement(Text5, { color, bold: true }, ` ${event.toolName ?? "?"}`), /* @__PURE__ */ React6.createElement(Text5, { color, dimColor: true }, ` ${marker}`)), /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, isEditFile ? /* @__PURE__ */ React6.createElement(EditFileDiff, { text: event.text }) : /* @__PURE__ */ React6.createElement(Text5, { color: isError ? "red" : void 0, dimColor: !isError }, truncate2(event.text, 400))));
6559
7313
  }
6560
7314
  if (event.role === "error") {
6561
- return /* @__PURE__ */ React5.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React5.createElement(RoleGlyph, { glyph: ROLE_GLYPH.error, color: "red" }), /* @__PURE__ */ React5.createElement(Text4, { color: "red" }, " ", event.text));
7315
+ return /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(RoleGlyph, { glyph: ROLE_GLYPH.error, color: "red" }), /* @__PURE__ */ React6.createElement(Text5, { color: "red" }, " ", event.text));
6562
7316
  }
6563
7317
  if (event.role === "info") {
6564
- return /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, event.text));
7318
+ return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, event.text));
6565
7319
  }
6566
7320
  if (event.role === "warning") {
6567
- return /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(RoleGlyph, { glyph: ROLE_GLYPH.warning, color: "yellow" }), /* @__PURE__ */ React5.createElement(Text4, { color: "yellow" }, " ", event.text));
7321
+ return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(RoleGlyph, { glyph: ROLE_GLYPH.warning, color: "yellow" }), /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, " ", event.text));
6568
7322
  }
6569
- return /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(Text4, null, event.text));
7323
+ return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, null, event.text));
6570
7324
  });
6571
7325
  function EditFileDiff({ text }) {
6572
7326
  const lines = text.split(/\r?\n/);
6573
7327
  const [statusHeader, hunkHeader, ...body] = lines;
6574
- return /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, ` ${statusHeader ?? ""}`), hunkHeader !== void 0 ? /* @__PURE__ */ React5.createElement(Text4, { color: "cyan", bold: true }, hunkHeader) : null, body.map((line, i) => {
7328
+ return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, ` ${statusHeader ?? ""}`), hunkHeader !== void 0 ? /* @__PURE__ */ React6.createElement(Text5, { color: "cyan", bold: true }, hunkHeader) : null, body.map((line, i) => {
6575
7329
  const key = `${i}-${line.slice(0, 32)}`;
6576
7330
  if (line.startsWith("- ")) {
6577
- return /* @__PURE__ */ React5.createElement(Text4, { key, color: "red" }, line);
7331
+ return /* @__PURE__ */ React6.createElement(Text5, { key, color: "red" }, line);
6578
7332
  }
6579
7333
  if (line.startsWith("+ ")) {
6580
- return /* @__PURE__ */ React5.createElement(Text4, { key, color: "green" }, line);
7334
+ return /* @__PURE__ */ React6.createElement(Text5, { key, color: "green" }, line);
6581
7335
  }
6582
- return /* @__PURE__ */ React5.createElement(Text4, { key, dimColor: true }, line);
7336
+ return /* @__PURE__ */ React6.createElement(Text5, { key, dimColor: true }, line);
6583
7337
  }));
6584
7338
  }
6585
7339
  function BranchBlock({ branch }) {
@@ -6588,33 +7342,33 @@ function BranchBlock({ branch }) {
6588
7342
  const t = (branch.temperatures[i] ?? 0).toFixed(1);
6589
7343
  return `${marker} #${i} T=${t} u=${u}`;
6590
7344
  }).join(" ");
6591
- return /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(Text4, { color: "blue" }, "\u2387 branched ", /* @__PURE__ */ React5.createElement(Text4, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, per)));
7345
+ return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { color: "blue" }, "\u2387 branched ", /* @__PURE__ */ React6.createElement(Text5, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, per)));
6592
7346
  }
6593
7347
  function ReasoningBlock({ reasoning }) {
6594
7348
  const max = 260;
6595
7349
  const flat = reasoning.replace(/\s+/g, " ").trim();
6596
7350
  const preview = flat.length <= max ? flat : `\u2026 (+${flat.length - max} earlier chars) ${flat.slice(-max)}`;
6597
- return /* @__PURE__ */ React5.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, "\u258F "), /* @__PURE__ */ React5.createElement(Text4, { dimColor: true, italic: true }, "thinking ", preview));
7351
+ return /* @__PURE__ */ React6.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "\u258F "), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true, italic: true }, "thinking ", preview));
6598
7352
  }
6599
7353
  function Elapsed() {
6600
7354
  const s = useElapsedSeconds();
6601
7355
  const mm = String(Math.floor(s / 60)).padStart(2, "0");
6602
7356
  const ss = String(s % 60).padStart(2, "0");
6603
- return /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, `${mm}:${ss}`);
7357
+ return /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, `${mm}:${ss}`);
6604
7358
  }
6605
7359
  function PulsingAssistantGlyph() {
6606
7360
  const tick = useTick();
6607
7361
  const on = Math.floor(tick / 4) % 2 === 0;
6608
- return /* @__PURE__ */ React5.createElement(Text4, { color: "green", bold: true }, on ? ROLE_GLYPH.assistant : ROLE_GLYPH.assistantPulse);
7362
+ return /* @__PURE__ */ React6.createElement(Text5, { color: "green", bold: true }, on ? ROLE_GLYPH.assistant : ROLE_GLYPH.assistantPulse);
6609
7363
  }
6610
7364
  function StreamingAssistant({ event }) {
6611
7365
  if (event.branchProgress) {
6612
7366
  const p = event.branchProgress;
6613
7367
  if (p.completed === 0) {
6614
- return /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React5.createElement(Text4, { color: "blue" }, " \u2387 launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026 "), /* @__PURE__ */ React5.createElement(Elapsed, null)), /* @__PURE__ */ React5.createElement(Text4, { color: "yellow" }, " ", "spread across T=0.0/0.5/1.0 \xB7 reasoner typically takes 30-90s \u2014 this is normal"));
7368
+ return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React6.createElement(Text5, { color: "blue" }, " \u2387 launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026 "), /* @__PURE__ */ React6.createElement(Elapsed, null)), /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, " ", "spread across T=0.0/0.5/1.0 \xB7 reasoner typically takes 30-90s \u2014 this is normal"));
6615
7369
  }
6616
7370
  const pct2 = Math.round(p.completed / p.total * 100);
6617
- return /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React5.createElement(Text4, { color: "blue" }, " \u2387 branching ", p.completed, "/", p.total, " (", pct2, "%) "), /* @__PURE__ */ React5.createElement(Elapsed, null)), /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, " latest #", p.latestIndex, " T=", p.latestTemperature.toFixed(1), " u=", p.latestUncertainties, p.completed < p.total ? " \xB7 waiting for other samples\u2026" : " \xB7 selecting winner\u2026"));
7371
+ return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React6.createElement(Text5, { color: "blue" }, " \u2387 branching ", p.completed, "/", p.total, " (", pct2, "%) "), /* @__PURE__ */ React6.createElement(Elapsed, null)), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " latest #", p.latestIndex, " T=", p.latestTemperature.toFixed(1), " u=", p.latestUncertainties, p.completed < p.total ? " \xB7 waiting for other samples\u2026" : " \xB7 selecting winner\u2026"));
6618
7372
  }
6619
7373
  const tail = lastLine(event.text, 140);
6620
7374
  const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
@@ -6644,16 +7398,16 @@ function StreamingAssistant({ event }) {
6644
7398
  label = parts.join(" \xB7 ");
6645
7399
  labelColor = "green";
6646
7400
  }
6647
- return /* @__PURE__ */ React5.createElement(Box4, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React5.createElement(Text4, null, " "), /* @__PURE__ */ React5.createElement(Pulse, null), /* @__PURE__ */ React5.createElement(Text4, { color: labelColor }, ` ${label} `), /* @__PURE__ */ React5.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React5.createElement(Text4, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, "\u25B8 ", tail) : preFirstByte ? (
7401
+ return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React6.createElement(Text5, null, " "), /* @__PURE__ */ React6.createElement(Pulse, null), /* @__PURE__ */ React6.createElement(Text5, { color: labelColor }, ` ${label} `), /* @__PURE__ */ React6.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React6.createElement(Text5, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "\u25B8 ", tail) : preFirstByte ? (
6648
7402
  // Non-dim yellow: first-time users misread the dim version as
6649
7403
  // "app frozen". The reassurance has to be VISIBLE to do its job.
6650
- /* @__PURE__ */ React5.createElement(Text4, { color: "yellow", italic: true }, " waiting for first byte \u2014 this is normal, typically 5-60s depending on model + load")
6651
- ) : reasoningOnly ? /* @__PURE__ */ React5.createElement(Text4, { color: "yellow", italic: true }, " R1 is thinking before it speaks \u2014 body text arrives when reasoning finishes (typically 20-90s, this is normal)") : toolCallOnly ? /* @__PURE__ */ React5.createElement(Text4, { color: "magenta", italic: true }, " tool-call arguments streaming \u2014 the model is about to dispatch a tool") : event.reasoning ? /* @__PURE__ */ React5.createElement(Text4, { color: "yellow", italic: true }, " R1 still reasoning \u2014 body text or tool call arrives when thinking finishes") : null);
7404
+ /* @__PURE__ */ React6.createElement(Text5, { color: "yellow", italic: true }, " waiting for first byte \u2014 this is normal, typically 5-60s depending on model + load")
7405
+ ) : reasoningOnly ? /* @__PURE__ */ React6.createElement(Text5, { color: "yellow", italic: true }, " R1 is thinking before it speaks \u2014 body text arrives when reasoning finishes (typically 20-90s, this is normal)") : toolCallOnly ? /* @__PURE__ */ React6.createElement(Text5, { color: "magenta", italic: true }, " tool-call arguments streaming \u2014 the model is about to dispatch a tool") : event.reasoning ? /* @__PURE__ */ React6.createElement(Text5, { color: "yellow", italic: true }, " R1 still reasoning \u2014 body text or tool call arrives when thinking finishes") : null);
6652
7406
  }
6653
7407
  function Pulse() {
6654
7408
  const tick = useTick();
6655
7409
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
6656
- return /* @__PURE__ */ React5.createElement(Text4, { color: "cyan" }, frames[Math.floor(tick / 4) % frames.length]);
7410
+ return /* @__PURE__ */ React6.createElement(Text5, { color: "cyan" }, frames[Math.floor(tick / 4) % frames.length]);
6657
7411
  }
6658
7412
  function formatToolCallIndex(tb) {
6659
7413
  if (!tb || tb.index === void 0) return "";
@@ -6672,7 +7426,7 @@ function lastLine(s, maxChars) {
6672
7426
  }
6673
7427
  function StatsLine({ stats }) {
6674
7428
  const hit = (stats.cacheHitRatio * 100).toFixed(1);
6675
- return /* @__PURE__ */ React5.createElement(Box4, null, /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, "\u258F "), /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, "cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, " \u2192 ", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6)));
7429
+ return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "\u258F "), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, " \u2192 ", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6)));
6676
7430
  }
6677
7431
  function truncate2(s, max) {
6678
7432
  if (s.length <= max) return s;
@@ -6695,12 +7449,12 @@ ${s.slice(-max)}`;
6695
7449
  }
6696
7450
 
6697
7451
  // src/cli/ui/PlanConfirm.tsx
6698
- import { Box as Box6, Text as Text6 } from "ink";
6699
- import React7 from "react";
7452
+ import { Box as Box7, Text as Text7 } from "ink";
7453
+ import React8 from "react";
6700
7454
 
6701
7455
  // src/cli/ui/Select.tsx
6702
- import { Box as Box5, Text as Text5, useInput } from "ink";
6703
- import React6, { useState as useState2 } from "react";
7456
+ import { Box as Box6, Text as Text6, useInput as useInput2 } from "ink";
7457
+ import React7, { useState as useState3 } from "react";
6704
7458
  function SingleSelect({
6705
7459
  items,
6706
7460
  initialValue,
@@ -6712,8 +7466,8 @@ function SingleSelect({
6712
7466
  0,
6713
7467
  items.findIndex((i) => i.value === initialValue && !i.disabled)
6714
7468
  );
6715
- const [index, setIndex] = useState2(initialIndex === -1 ? 0 : initialIndex);
6716
- useInput((_input, key) => {
7469
+ const [index, setIndex] = useState3(initialIndex === -1 ? 0 : initialIndex);
7470
+ useInput2((_input, key) => {
6717
7471
  if (key.upArrow) {
6718
7472
  setIndex((i) => findNextEnabled(items, i, -1));
6719
7473
  } else if (key.downArrow) {
@@ -6725,7 +7479,7 @@ function SingleSelect({
6725
7479
  onCancel();
6726
7480
  }
6727
7481
  });
6728
- return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React6.createElement(
7482
+ return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React7.createElement(
6729
7483
  SelectRow,
6730
7484
  {
6731
7485
  key: item.value,
@@ -6733,7 +7487,7 @@ function SingleSelect({
6733
7487
  active: i === index,
6734
7488
  marker: i === index ? "\u25B8" : " "
6735
7489
  }
6736
- )), footer ? /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, footer)) : null);
7490
+ )), footer ? /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, footer)) : null);
6737
7491
  }
6738
7492
  function MultiSelect({
6739
7493
  items,
@@ -6742,12 +7496,12 @@ function MultiSelect({
6742
7496
  onCancel,
6743
7497
  footer
6744
7498
  }) {
6745
- const [index, setIndex] = useState2(() => {
7499
+ const [index, setIndex] = useState3(() => {
6746
7500
  const first = items.findIndex((i) => !i.disabled);
6747
7501
  return first === -1 ? 0 : first;
6748
7502
  });
6749
- const [selected, setSelected] = useState2(new Set(initialSelected));
6750
- useInput((input, key) => {
7503
+ const [selected, setSelected] = useState3(new Set(initialSelected));
7504
+ useInput2((input, key) => {
6751
7505
  if (key.upArrow) {
6752
7506
  setIndex((i) => findNextEnabled(items, i, -1));
6753
7507
  } else if (key.downArrow) {
@@ -6768,10 +7522,10 @@ function MultiSelect({
6768
7522
  onCancel();
6769
7523
  }
6770
7524
  });
6771
- return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column" }, items.map((item, i) => {
7525
+ return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column" }, items.map((item, i) => {
6772
7526
  const checked = selected.has(item.value);
6773
7527
  const marker = checked ? "[x]" : "[ ]";
6774
- return /* @__PURE__ */ React6.createElement(
7528
+ return /* @__PURE__ */ React7.createElement(
6775
7529
  SelectRow,
6776
7530
  {
6777
7531
  key: item.value,
@@ -6780,7 +7534,7 @@ function MultiSelect({
6780
7534
  marker: `${i === index ? "\u25B8" : " "} ${marker}`
6781
7535
  }
6782
7536
  );
6783
- }), footer ? /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, footer)) : null);
7537
+ }), footer ? /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, footer)) : null);
6784
7538
  }
6785
7539
  function SelectRow({
6786
7540
  item,
@@ -6788,7 +7542,7 @@ function SelectRow({
6788
7542
  marker
6789
7543
  }) {
6790
7544
  const color = item.disabled ? "gray" : active ? "cyan" : void 0;
6791
- return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React6.createElement(Box5, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, item.hint)) : null);
7545
+ return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Box6, null, /* @__PURE__ */ React7.createElement(Text6, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React7.createElement(Box6, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, item.hint)) : null);
6792
7546
  }
6793
7547
  function findNextEnabled(items, from, step) {
6794
7548
  if (items.length === 0) return 0;
@@ -6831,7 +7585,7 @@ function PlanConfirmInner({
6831
7585
  const sourceLineBudget = Math.max(MIN_BODY_ROWS, Math.floor(renderedBudget / MARKDOWN_EXPANSION));
6832
7586
  const visible = clampBodyByLines(charCapped, sourceLineBudget);
6833
7587
  const hasOpenQuestions = /^#{1,6}\s*(open[-\s]?questions?|risks?|unknowns?|assumptions?|unclear)/im.test(plan) || /^#{1,6}\s*(待确认|开放问题|风险|未知|假设|不确定)/im.test(plan);
6834
- return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React7.createElement(Box6, null, /* @__PURE__ */ React7.createElement(Text6, { bold: true, color: "cyan" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React7.createElement(Box6, null, /* @__PURE__ */ React7.createElement(Text6, { color: "cyan", dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Markdown, { text: visible, projectRoot })), hasOpenQuestions ? /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React7.createElement(Text6, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(
7588
+ return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "cyan" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { color: "cyan", dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React8.createElement(Markdown, { text: visible, projectRoot })), hasOpenQuestions ? /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React8.createElement(Text7, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(
6835
7589
  SingleSelect,
6836
7590
  {
6837
7591
  initialValue: hasOpenQuestions ? "refine" : "approve",
@@ -6858,14 +7612,14 @@ function PlanConfirmInner({
6858
7612
  }
6859
7613
  )));
6860
7614
  }
6861
- var PlanConfirm = React7.memo(PlanConfirmInner);
7615
+ var PlanConfirm = React8.memo(PlanConfirmInner);
6862
7616
 
6863
7617
  // src/cli/ui/PlanRefineInput.tsx
6864
- import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
6865
- import React8, { useState as useState3 } from "react";
7618
+ import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
7619
+ import React9, { useState as useState4 } from "react";
6866
7620
  function PlanRefineInput({ mode, onSubmit, onCancel }) {
6867
- const [value, setValue] = useState3("");
6868
- useInput2((input, key) => {
7621
+ const [value, setValue] = useState4("");
7622
+ useInput3((input, key) => {
6869
7623
  if (key.escape) {
6870
7624
  onCancel();
6871
7625
  return;
@@ -6885,12 +7639,12 @@ function PlanRefineInput({ mode, onSubmit, onCancel }) {
6885
7639
  const title = mode === "approve" ? "\u25B8 approving \u2014 any last instructions or answers to open questions?" : "\u25B8 refining \u2014 what should the model change?";
6886
7640
  const hint = mode === "approve" ? "Answer questions the plan raised, add constraints, or just press Enter to approve as-is." : "Describe what's wrong or missing, or answer questions the plan raised.";
6887
7641
  const blankHint = mode === "approve" ? " (Enter with blank = approve without extra instructions.)" : " (Enter with blank = ask the model to list concrete questions.)";
6888
- return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "yellow" }, title)), /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, hint, " Enter to send \xB7 Esc to return to the picker.", value === "" ? blankHint : "")), /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, null, /* @__PURE__ */ React8.createElement(Text7, { color: "yellow" }, "\u203A "), /* @__PURE__ */ React8.createElement(Text7, null, value || " "), /* @__PURE__ */ React8.createElement(Text7, { color: "yellow" }, "\u258D"))));
7642
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "yellow" }, title)), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, hint, " Enter to send \xB7 Esc to return to the picker.", value === "" ? blankHint : "")), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u203A "), /* @__PURE__ */ React9.createElement(Text8, null, value || " "), /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u258D"))));
6889
7643
  }
6890
7644
 
6891
7645
  // src/cli/ui/PromptInput.tsx
6892
- import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
6893
- import React9, { useRef, useState as useState4 } from "react";
7646
+ import { Box as Box9, Text as Text9, useInput as useInput4 } from "ink";
7647
+ import React10, { useRef, useState as useState5 } from "react";
6894
7648
 
6895
7649
  // src/cli/ui/multiline-keys.ts
6896
7650
  var BACKSLASH_SUFFIX = /\\$/;
@@ -7005,7 +7759,7 @@ function PromptInput({
7005
7759
  disabled,
7006
7760
  placeholder
7007
7761
  }) {
7008
- const [cursor, setCursor] = useState4(value.length);
7762
+ const [cursor, setCursor] = useState5(value.length);
7009
7763
  const lastLocalValueRef = useRef(value);
7010
7764
  if (value !== lastLocalValueRef.current) {
7011
7765
  lastLocalValueRef.current = value;
@@ -7015,7 +7769,7 @@ function PromptInput({
7015
7769
  }
7016
7770
  const tick = useTick();
7017
7771
  const showCursor = disabled ? false : Math.floor(tick / 4) % 2 === 0;
7018
- useInput3(
7772
+ useInput4(
7019
7773
  (input, key) => {
7020
7774
  const ke = {
7021
7775
  input,
@@ -7050,13 +7804,13 @@ function PromptInput({
7050
7804
  const lines = value.length > 0 ? value.split("\n") : [""];
7051
7805
  const borderColor = disabled ? "gray" : "cyan";
7052
7806
  const { line: cursorLine, col: cursorCol } = lineAndColumn(value, cursor);
7053
- return /* @__PURE__ */ React9.createElement(Box8, { borderStyle: "round", borderColor, paddingX: 1, flexDirection: "column" }, lines.map((line, i) => {
7807
+ return /* @__PURE__ */ React10.createElement(Box9, { borderStyle: "round", borderColor, paddingX: 1, flexDirection: "column" }, lines.map((line, i) => {
7054
7808
  const isFirst = i === 0;
7055
7809
  const showPlaceholder = isFirst && value.length === 0;
7056
7810
  const isCursorLine = i === cursorLine;
7057
7811
  return (
7058
7812
  // biome-ignore lint/suspicious/noArrayIndexKey: stable by construction — lines are derived from `value.split("\n")` and never reordered
7059
- /* @__PURE__ */ React9.createElement(Box8, { key: i }, isFirst ? /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: borderColor }, "you \u203A", " ") : /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " "), showPlaceholder ? /* @__PURE__ */ React9.createElement(React9.Fragment, null, isCursorLine && !disabled ? /* @__PURE__ */ React9.createElement(Text8, { color: borderColor }, showCursor ? "\u258C" : " ") : null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, effectivePlaceholder)) : isCursorLine && !disabled ? /* @__PURE__ */ React9.createElement(
7813
+ /* @__PURE__ */ React10.createElement(Box9, { key: i }, isFirst ? /* @__PURE__ */ React10.createElement(Text9, { bold: true, color: borderColor }, "you \u203A", " ") : /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " "), showPlaceholder ? /* @__PURE__ */ React10.createElement(React10.Fragment, null, isCursorLine && !disabled ? /* @__PURE__ */ React10.createElement(Text9, { color: borderColor }, showCursor ? "\u258C" : " ") : null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, effectivePlaceholder)) : isCursorLine && !disabled ? /* @__PURE__ */ React10.createElement(
7060
7814
  LineWithCursor,
7061
7815
  {
7062
7816
  line,
@@ -7064,7 +7818,7 @@ function PromptInput({
7064
7818
  showCursor,
7065
7819
  borderColor
7066
7820
  }
7067
- ) : /* @__PURE__ */ React9.createElement(Text8, null, line))
7821
+ ) : /* @__PURE__ */ React10.createElement(Text9, null, line))
7068
7822
  );
7069
7823
  }));
7070
7824
  }
@@ -7078,16 +7832,17 @@ function LineWithCursor({
7078
7832
  const atCursor = line.slice(col, col + 1);
7079
7833
  const after = line.slice(col + 1);
7080
7834
  if (atCursor.length === 0) {
7081
- return /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(Text8, null, before), /* @__PURE__ */ React9.createElement(Text8, { color: borderColor }, showCursor ? "\u258C" : " "));
7835
+ return /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement(Text9, null, before), /* @__PURE__ */ React10.createElement(Text9, { color: borderColor }, showCursor ? "\u258C" : " "));
7082
7836
  }
7083
- return /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(Text8, null, before), /* @__PURE__ */ React9.createElement(Text8, { inverse: showCursor }, atCursor), /* @__PURE__ */ React9.createElement(Text8, null, after));
7837
+ return /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement(Text9, null, before), /* @__PURE__ */ React10.createElement(Text9, { inverse: showCursor }, atCursor), /* @__PURE__ */ React10.createElement(Text9, null, after));
7084
7838
  }
7085
7839
 
7086
7840
  // src/cli/ui/ShellConfirm.tsx
7087
- import { Box as Box9, Text as Text9 } from "ink";
7088
- import React10 from "react";
7089
- function ShellConfirm({ command, allowPrefix, onChoose }) {
7090
- return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React10.createElement(Box9, null, /* @__PURE__ */ React10.createElement(Text9, { bold: true, color: "yellow" }, "\u25B8 model wants to run a shell command")), /* @__PURE__ */ React10.createElement(Box9, null, /* @__PURE__ */ React10.createElement(Text9, { color: "yellow", dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(Text9, null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, "$ "), /* @__PURE__ */ React10.createElement(Text9, { color: "cyan" }, command))), /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(
7841
+ import { Box as Box10, Text as Text10 } from "ink";
7842
+ import React11 from "react";
7843
+ function ShellConfirm({ command, allowPrefix, kind, onChoose }) {
7844
+ const isBackground = kind === "run_background";
7845
+ return /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Text10, { bold: true, color: "yellow" }, isBackground ? "\u25B8 model wants to start a BACKGROUND process" : "\u25B8 model wants to run a shell command")), isBackground ? /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (long-running: dev server / watcher; keeps running after approval, /kill to stop)")) : null, /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Text10, { color: "yellow", dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "$ "), /* @__PURE__ */ React11.createElement(Text10, { color: "cyan" }, command))), /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React11.createElement(
7091
7846
  SingleSelect,
7092
7847
  {
7093
7848
  initialValue: "run_once",
@@ -7144,8 +7899,8 @@ function derivePrefix(command) {
7144
7899
  }
7145
7900
 
7146
7901
  // src/cli/ui/SlashArgPicker.tsx
7147
- import { Box as Box10, Text as Text10 } from "ink";
7148
- import React11 from "react";
7902
+ import { Box as Box11, Text as Text11 } from "ink";
7903
+ import React12 from "react";
7149
7904
  function SlashArgPicker({
7150
7905
  matches,
7151
7906
  selectedIndex,
@@ -7154,11 +7909,11 @@ function SlashArgPicker({
7154
7909
  partial
7155
7910
  }) {
7156
7911
  if (kind === "hint") {
7157
- return /* @__PURE__ */ React11.createElement(Box10, { paddingX: 1 }, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " ", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary));
7912
+ return /* @__PURE__ */ React12.createElement(Box11, { paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " ", /* @__PURE__ */ React12.createElement(Text11, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary));
7158
7913
  }
7159
7914
  if (matches === null) return null;
7160
7915
  if (matches.length === 0) {
7161
- return /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " ", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary), /* @__PURE__ */ React11.createElement(Text10, { color: "yellow" }, ' no match for "', partial, '" \u2014 keep typing, or Backspace to edit'));
7916
+ return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " ", /* @__PURE__ */ React12.createElement(Text11, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary), /* @__PURE__ */ React12.createElement(Text11, { color: "yellow" }, ' no match for "', partial, '" \u2014 keep typing, or Backspace to edit'));
7162
7917
  }
7163
7918
  const MAX = 8;
7164
7919
  const total = matches.length;
@@ -7166,26 +7921,26 @@ function SlashArgPicker({
7166
7921
  const shown = matches.slice(windowStart, windowStart + MAX);
7167
7922
  const hiddenAbove = windowStart;
7168
7923
  const hiddenBelow = total - windowStart - shown.length;
7169
- return /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " ", /* @__PURE__ */ React11.createElement(Text10, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary), hiddenAbove > 0 ? /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((value, i) => /* @__PURE__ */ React11.createElement(ArgRow, { key: value, value, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick"));
7924
+ return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " ", /* @__PURE__ */ React12.createElement(Text11, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary), hiddenAbove > 0 ? /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((value, i) => /* @__PURE__ */ React12.createElement(ArgRow, { key: value, value, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick"));
7170
7925
  }
7171
7926
  function ArgRow({ value, isSelected }) {
7172
7927
  const marker = isSelected ? "\u25B8" : " ";
7173
7928
  if (isSelected) {
7174
- return /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Text10, { bold: true, color: "cyan" }, marker, " ", value));
7929
+ return /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Text11, { bold: true, color: "cyan" }, marker, " ", value));
7175
7930
  }
7176
- return /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, marker, " ", value));
7931
+ return /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, marker, " ", value));
7177
7932
  }
7178
7933
 
7179
7934
  // src/cli/ui/SlashSuggestions.tsx
7180
- import { Box as Box11, Text as Text11 } from "ink";
7181
- import React12 from "react";
7935
+ import { Box as Box12, Text as Text12 } from "ink";
7936
+ import React13 from "react";
7182
7937
  function SlashSuggestions({
7183
7938
  matches,
7184
7939
  selectedIndex
7185
7940
  }) {
7186
7941
  if (matches === null) return null;
7187
7942
  if (matches.length === 0) {
7188
- return /* @__PURE__ */ React12.createElement(Box11, { paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text11, { color: "yellow" }, "no slash command matches that prefix"), /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " \u2014 Backspace to edit, or /help for the full list"));
7943
+ return /* @__PURE__ */ React13.createElement(Box12, { paddingX: 1 }, /* @__PURE__ */ React13.createElement(Text12, { color: "yellow" }, "no slash command matches that prefix"), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " \u2014 Backspace to edit, or /help for the full list"));
7189
7944
  }
7190
7945
  const MAX = 8;
7191
7946
  const total = matches.length;
@@ -7193,21 +7948,21 @@ function SlashSuggestions({
7193
7948
  const shown = matches.slice(windowStart, windowStart + MAX);
7194
7949
  const hiddenAbove = windowStart;
7195
7950
  const hiddenBelow = total - windowStart - shown.length;
7196
- return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React12.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick"));
7951
+ return /* @__PURE__ */ React13.createElement(Box12, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React13.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick"));
7197
7952
  }
7198
7953
  function SuggestionRow({ spec, isSelected }) {
7199
7954
  const marker = isSelected ? "\u25B8" : " ";
7200
7955
  const name = `/${spec.cmd}`;
7201
7956
  const argsSuffix = spec.argsHint ? ` ${spec.argsHint}` : "";
7202
7957
  if (isSelected) {
7203
- return /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Text11, { bold: true, color: "cyan" }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16)), /* @__PURE__ */ React12.createElement(Text11, { color: "cyan" }, " ", spec.summary));
7958
+ return /* @__PURE__ */ React13.createElement(Box12, null, /* @__PURE__ */ React13.createElement(Text12, { bold: true, color: "cyan" }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16)), /* @__PURE__ */ React13.createElement(Text12, { color: "cyan" }, " ", spec.summary));
7204
7959
  }
7205
- return /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16), " ", spec.summary));
7960
+ return /* @__PURE__ */ React13.createElement(Box12, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16), " ", spec.summary));
7206
7961
  }
7207
7962
 
7208
7963
  // src/cli/ui/StatsPanel.tsx
7209
- import { Box as Box12, Text as Text12, useStdout as useStdout2 } from "ink";
7210
- import React13 from "react";
7964
+ import { Box as Box13, Text as Text13, useStdout as useStdout3 } from "ink";
7965
+ import React14 from "react";
7211
7966
  var WORDMARK_STYLES = [
7212
7967
  { ch: "\u25C8", color: "#5eead4", isLogo: true },
7213
7968
  // teal — brand mark
@@ -7233,7 +7988,7 @@ function Wordmark({ busy }) {
7233
7988
  const tick = useTick();
7234
7989
  const period = busy ? 5 : 12;
7235
7990
  const bright = Math.floor(tick / period) % 2 === 0;
7236
- return /* @__PURE__ */ React13.createElement(Text12, null, WORDMARK_STYLES.map((c) => /* @__PURE__ */ React13.createElement(Text12, { key: `${c.ch}-${c.color}`, color: c.color, bold: c.isLogo ? bright : true }, c.ch)));
7991
+ return /* @__PURE__ */ React14.createElement(Text13, null, WORDMARK_STYLES.map((c) => /* @__PURE__ */ React14.createElement(Text13, { key: `${c.ch}-${c.color}`, color: c.color, bold: c.isLogo ? bright : true }, c.ch)));
7237
7992
  }
7238
7993
  var NARROW_BREAKPOINT = 120;
7239
7994
  var COLD_START_TURNS = 3;
@@ -7245,6 +8000,7 @@ function StatsPanel({
7245
8000
  branchBudget,
7246
8001
  reasoningEffort,
7247
8002
  planMode,
8003
+ editMode,
7248
8004
  balance,
7249
8005
  updateAvailable,
7250
8006
  busy
@@ -7252,11 +8008,11 @@ function StatsPanel({
7252
8008
  const branchOn = (branchBudget ?? 1) > 1;
7253
8009
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
7254
8010
  const ctxRatio = summary.lastPromptTokens / ctxMax;
7255
- const { stdout: stdout2 } = useStdout2();
8011
+ const { stdout: stdout2 } = useStdout3();
7256
8012
  const columns = stdout2?.columns ?? 80;
7257
8013
  const narrow = columns < NARROW_BREAKPOINT;
7258
8014
  const coldStart = summary.turns <= COLD_START_TURNS;
7259
- return /* @__PURE__ */ React13.createElement(Box12, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React13.createElement(
8015
+ return /* @__PURE__ */ React14.createElement(Box13, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React14.createElement(
7260
8016
  Header,
7261
8017
  {
7262
8018
  model,
@@ -7266,12 +8022,13 @@ function StatsPanel({
7266
8022
  branchBudget: branchBudget ?? 1,
7267
8023
  reasoningEffort,
7268
8024
  planMode,
8025
+ editMode,
7269
8026
  turns: summary.turns,
7270
8027
  updateAvailable,
7271
8028
  narrow,
7272
8029
  busy: busy ?? false
7273
8030
  }
7274
- ), narrow ? /* @__PURE__ */ React13.createElement(
8031
+ ), narrow ? /* @__PURE__ */ React14.createElement(
7275
8032
  StackedMetrics,
7276
8033
  {
7277
8034
  summary,
@@ -7280,7 +8037,7 @@ function StatsPanel({
7280
8037
  balance,
7281
8038
  coldStart
7282
8039
  }
7283
- ) : /* @__PURE__ */ React13.createElement(
8040
+ ) : /* @__PURE__ */ React14.createElement(
7284
8041
  InlineMetrics,
7285
8042
  {
7286
8043
  summary,
@@ -7299,12 +8056,13 @@ function Header({
7299
8056
  branchBudget,
7300
8057
  reasoningEffort,
7301
8058
  planMode,
8059
+ editMode,
7302
8060
  turns,
7303
8061
  updateAvailable,
7304
8062
  narrow,
7305
8063
  busy
7306
8064
  }) {
7307
- return /* @__PURE__ */ React13.createElement(Box12, { justifyContent: "space-between" }, /* @__PURE__ */ React13.createElement(Box12, null, /* @__PURE__ */ React13.createElement(Wordmark, { busy }), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, ` v${VERSION}`), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React13.createElement(Text12, { color: "yellow" }, model), narrow ? null : /* @__PURE__ */ React13.createElement(React13.Fragment, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, prefixHash)), harvestOn ? /* @__PURE__ */ React13.createElement(Text12, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React13.createElement(Text12, { color: "blue" }, " \xB7 branch", branchBudget) : null, reasoningEffort === "max" ? /* @__PURE__ */ React13.createElement(Text12, { color: "green" }, " \xB7 max") : null, reasoningEffort === "high" ? /* @__PURE__ */ React13.createElement(Text12, { color: "yellow" }, " \xB7 high") : null, planMode ? /* @__PURE__ */ React13.createElement(Text12, { color: "red", bold: true }, " \xB7 PLAN") : null), /* @__PURE__ */ React13.createElement(Text12, null, updateAvailable ? /* @__PURE__ */ React13.createElement(Text12, { color: "yellow", bold: true }, `update: ${updateAvailable} \xB7 `) : null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, narrow ? `turn ${turns}` : `turn ${turns} \xB7 /help`)));
8065
+ return /* @__PURE__ */ React14.createElement(Box13, { justifyContent: "space-between" }, /* @__PURE__ */ React14.createElement(Box13, null, /* @__PURE__ */ React14.createElement(Wordmark, { busy }), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, ` v${VERSION}`), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React14.createElement(Text13, { color: "yellow" }, model), narrow ? null : /* @__PURE__ */ React14.createElement(React14.Fragment, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, prefixHash)), harvestOn ? /* @__PURE__ */ React14.createElement(Text13, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React14.createElement(Text13, { color: "blue" }, " \xB7 branch", branchBudget) : null, reasoningEffort === "max" ? /* @__PURE__ */ React14.createElement(Text13, { color: "green" }, " \xB7 max") : null, reasoningEffort === "high" ? /* @__PURE__ */ React14.createElement(Text13, { color: "yellow" }, " \xB7 high") : null, planMode ? /* @__PURE__ */ React14.createElement(Text13, { color: "red", bold: true }, " \xB7 PLAN") : null, editMode ? /* @__PURE__ */ React14.createElement(Text13, { color: editMode === "auto" ? "magenta" : "cyan", bold: true }, editMode === "auto" ? " \xB7 AUTO" : " \xB7 review") : null), /* @__PURE__ */ React14.createElement(Text13, null, updateAvailable ? /* @__PURE__ */ React14.createElement(Text13, { color: "yellow", bold: true }, `update: ${updateAvailable} \xB7 `) : null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, narrow ? `turn ${turns}` : `turn ${turns} \xB7 /help`)));
7308
8066
  }
7309
8067
  function InlineMetrics({
7310
8068
  summary,
@@ -7313,7 +8071,7 @@ function InlineMetrics({
7313
8071
  balance,
7314
8072
  coldStart
7315
8073
  }) {
7316
- return /* @__PURE__ */ React13.createElement(Box12, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React13.createElement(ContextCell, { ratio: ctxRatio, promptTokens: summary.lastPromptTokens, ctxMax }), /* @__PURE__ */ React13.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React13.createElement(CostCell, { summary, coldStart }), balance ? /* @__PURE__ */ React13.createElement(BalanceCell, { balance }) : null);
8074
+ return /* @__PURE__ */ React14.createElement(Box13, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React14.createElement(ContextCell, { ratio: ctxRatio, promptTokens: summary.lastPromptTokens, ctxMax }), /* @__PURE__ */ React14.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React14.createElement(CostCell, { summary, coldStart }), balance ? /* @__PURE__ */ React14.createElement(BalanceCell, { balance }) : null);
7317
8075
  }
7318
8076
  function StackedMetrics({
7319
8077
  summary,
@@ -7322,7 +8080,7 @@ function StackedMetrics({
7322
8080
  balance,
7323
8081
  coldStart
7324
8082
  }) {
7325
- return /* @__PURE__ */ React13.createElement(Box12, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React13.createElement(
8083
+ return /* @__PURE__ */ React14.createElement(Box13, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React14.createElement(
7326
8084
  ContextCell,
7327
8085
  {
7328
8086
  ratio: ctxRatio,
@@ -7330,7 +8088,7 @@ function StackedMetrics({
7330
8088
  ctxMax,
7331
8089
  showBar: true
7332
8090
  }
7333
- ), balance ? /* @__PURE__ */ React13.createElement(BalanceCell, { balance }) : null, /* @__PURE__ */ React13.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React13.createElement(CostCell, { summary, coldStart }));
8091
+ ), balance ? /* @__PURE__ */ React14.createElement(BalanceCell, { balance }) : null, /* @__PURE__ */ React14.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React14.createElement(CostCell, { summary, coldStart }));
7334
8092
  }
7335
8093
  function ContextCell({
7336
8094
  ratio,
@@ -7339,11 +8097,11 @@ function ContextCell({
7339
8097
  showBar
7340
8098
  }) {
7341
8099
  if (promptTokens === 0) {
7342
- return /* @__PURE__ */ React13.createElement(Text12, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "ctx "), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "\u2014 (no turns yet)"));
8100
+ return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "ctx "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "\u2014 (no turns yet)"));
7343
8101
  }
7344
8102
  const color = ratio >= 0.8 ? "red" : ratio >= 0.6 ? "yellow" : "green";
7345
8103
  const pct2 = Math.round(ratio * 100);
7346
- return /* @__PURE__ */ React13.createElement(Text12, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "ctx "), showBar ? /* @__PURE__ */ React13.createElement(Bar, { ratio, color }) : null, showBar ? /* @__PURE__ */ React13.createElement(Text12, null, " ") : null, /* @__PURE__ */ React13.createElement(Text12, { color, bold: true }, formatTokens(promptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " (", pct2, "%)"), ratio >= 0.8 ? /* @__PURE__ */ React13.createElement(Text12, { color: "red", bold: true }, " \xB7 /compact") : null);
8104
+ return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "ctx "), showBar ? /* @__PURE__ */ React14.createElement(Bar, { ratio, color }) : null, showBar ? /* @__PURE__ */ React14.createElement(Text13, null, " ") : null, /* @__PURE__ */ React14.createElement(Text13, { color, bold: true }, formatTokens(promptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " (", pct2, "%)"), ratio >= 0.8 ? /* @__PURE__ */ React14.createElement(Text13, { color: "red", bold: true }, " \xB7 /compact") : null);
7347
8105
  }
7348
8106
  function CacheCell({
7349
8107
  hitRatio,
@@ -7352,33 +8110,33 @@ function CacheCell({
7352
8110
  }) {
7353
8111
  const pct2 = (hitRatio * 100).toFixed(1);
7354
8112
  if (turns === 0) {
7355
- return /* @__PURE__ */ React13.createElement(Text12, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "cache "), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "\u2014"));
8113
+ return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "cache "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "\u2014"));
7356
8114
  }
7357
8115
  if (coldStart) {
7358
- return /* @__PURE__ */ React13.createElement(Text12, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "cache "), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, pct2, "% "), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true, italic: true }, "(cold start)"));
8116
+ return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "cache "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, pct2, "% "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true, italic: true }, "(cold start)"));
7359
8117
  }
7360
8118
  const color = hitRatio >= 0.7 ? "green" : hitRatio >= 0.4 ? "yellow" : "red";
7361
- return /* @__PURE__ */ React13.createElement(Text12, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "cache "), /* @__PURE__ */ React13.createElement(Text12, { color, bold: true }, pct2, "%"));
8119
+ return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "cache "), /* @__PURE__ */ React14.createElement(Text13, { color, bold: true }, pct2, "%"));
7362
8120
  }
7363
8121
  function CostCell({
7364
8122
  summary,
7365
8123
  coldStart
7366
8124
  }) {
7367
8125
  if (summary.turns === 0) {
7368
- return /* @__PURE__ */ React13.createElement(Text12, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "cost "), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "\u2014"));
8126
+ return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "cost "), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "\u2014"));
7369
8127
  }
7370
8128
  const primaryColor = coldStart ? void 0 : "green";
7371
- return /* @__PURE__ */ React13.createElement(Text12, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "cost "), /* @__PURE__ */ React13.createElement(Text12, { color: primaryColor, bold: !coldStart, dimColor: coldStart }, "$", summary.totalCostUsd.toFixed(6)), /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " (in ", "$", summary.totalInputCostUsd.toFixed(6), " \xB7 out ", "$", summary.totalOutputCostUsd.toFixed(6), ")"));
8129
+ return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "cost "), /* @__PURE__ */ React14.createElement(Text13, { color: primaryColor, bold: !coldStart, dimColor: coldStart }, "$", summary.totalCostUsd.toFixed(6)), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " (in ", "$", summary.totalInputCostUsd.toFixed(6), " \xB7 out ", "$", summary.totalOutputCostUsd.toFixed(6), ")"));
7372
8130
  }
7373
8131
  function BalanceCell({ balance }) {
7374
8132
  const color = balance.total < 1 ? "red" : balance.total < 5 ? "yellow" : "green";
7375
- return /* @__PURE__ */ React13.createElement(Text12, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, "balance "), /* @__PURE__ */ React13.createElement(Text12, { color, bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : ""));
8133
+ return /* @__PURE__ */ React14.createElement(Text13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, "balance "), /* @__PURE__ */ React14.createElement(Text13, { color, bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : ""));
7376
8134
  }
7377
8135
  function Bar({ ratio, color }) {
7378
8136
  const cells = 10;
7379
8137
  const filled = Math.max(0, Math.min(cells, Math.round(ratio * cells)));
7380
8138
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(cells - filled);
7381
- return /* @__PURE__ */ React13.createElement(Text12, { color }, bar);
8139
+ return /* @__PURE__ */ React14.createElement(Text13, { color }, bar);
7382
8140
  }
7383
8141
  function formatTokens(n) {
7384
8142
  if (n < 1024) return String(n);
@@ -7814,7 +8572,7 @@ var SLASH_COMMANDS = [
7814
8572
  {
7815
8573
  cmd: "compact",
7816
8574
  argsHint: "[tokens]",
7817
- summary: "shrink oversized tool results in the log (cap in tokens, default 4000)"
8575
+ summary: "shrink oversized tool results AND tool-call args (edit_file search/replace) in the log; cap in tokens, default 4000"
7818
8576
  },
7819
8577
  { cmd: "keys", summary: "show all keyboard shortcuts and prompt prefixes" },
7820
8578
  { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
@@ -7827,6 +8585,17 @@ var SLASH_COMMANDS = [
7827
8585
  { cmd: "apply", summary: "commit pending edit blocks to disk", contextual: "code" },
7828
8586
  { cmd: "discard", summary: "drop pending edit blocks without writing", contextual: "code" },
7829
8587
  { cmd: "undo", summary: "roll back the last applied edit batch", contextual: "code" },
8588
+ {
8589
+ cmd: "history",
8590
+ summary: "list every edit batch this session (ids for /show, undone markers)",
8591
+ contextual: "code"
8592
+ },
8593
+ {
8594
+ cmd: "show",
8595
+ argsHint: "[id]",
8596
+ summary: "dump a stored edit diff (omit id for newest non-undone)",
8597
+ contextual: "code"
8598
+ },
7830
8599
  {
7831
8600
  cmd: "commit",
7832
8601
  argsHint: '"msg"',
@@ -7844,6 +8613,26 @@ var SLASH_COMMANDS = [
7844
8613
  cmd: "apply-plan",
7845
8614
  summary: "force-approve a pending / in-text plan (fallback if picker was missed)",
7846
8615
  contextual: "code"
8616
+ },
8617
+ {
8618
+ cmd: "mode",
8619
+ argsHint: "[review|auto]",
8620
+ summary: "edit-gate: review (queue for /apply) or auto (apply+undo banner). Shift+Tab cycles.",
8621
+ contextual: "code",
8622
+ argCompleter: ["review", "auto"]
8623
+ },
8624
+ { cmd: "jobs", summary: "list background jobs started by run_background", contextual: "code" },
8625
+ {
8626
+ cmd: "kill",
8627
+ argsHint: "<id>",
8628
+ summary: "stop a background job by id (SIGTERM \u2192 SIGKILL after grace)",
8629
+ contextual: "code"
8630
+ },
8631
+ {
8632
+ cmd: "logs",
8633
+ argsHint: "<id> [lines]",
8634
+ summary: "tail a background job's output (default last 80 lines)",
8635
+ contextual: "code"
7847
8636
  }
7848
8637
  ];
7849
8638
  function suggestSlashCommands(prefix, codeMode = false) {
@@ -7912,6 +8701,8 @@ function handleSlash(cmd, args, loop, ctx = {}) {
7912
8701
  " Backspace delete left; Delete delete under cursor",
7913
8702
  " Esc abort the in-flight turn",
7914
8703
  " y / n accept / reject pending edits (code mode)",
8704
+ " Shift+Tab cycle edit gate: review \u2194 AUTO (code mode, persists to config)",
8705
+ " u undo the latest non-undone edit batch (session-wide, not just banner)",
7915
8706
  "",
7916
8707
  "Prompt prefixes:",
7917
8708
  " /<name> slash command; Tab/Enter picks from the suggestion list",
@@ -7960,10 +8751,16 @@ function handleSlash(cmd, args, loop, ctx = {}) {
7960
8751
  " /retry truncate & resend your last message (fresh sample from the model)",
7961
8752
  " /apply (code mode) commit the pending edit blocks to disk",
7962
8753
  " /discard (code mode) drop pending edits without writing",
7963
- " /undo (code mode) roll back the last applied edit batch",
8754
+ " /undo (code mode) roll back the latest non-undone edit batch",
8755
+ " /history (code mode) list every edit batch this session",
8756
+ " /show [id] (code mode) dump a stored edit diff (newest when id omitted)",
7964
8757
  ' /commit "msg" (code mode) git add -A && git commit -m "msg"',
7965
8758
  " /plan [on|off] (code mode) toggle read-only plan mode; writes gated behind submit_plan + your approval",
7966
8759
  " /apply-plan (code mode) force-approve pending/in-text plan (fallback)",
8760
+ " /mode [review|auto] (code mode) edit-gate: queue edits for /apply or apply instantly (Shift+Tab cycles, u undoes within 5s)",
8761
+ " /jobs (code mode) list background processes (run_background) \u2014 running and exited",
8762
+ " /kill <id> (code mode) stop a background job by id (SIGTERM \u2192 SIGKILL)",
8763
+ " /logs <id> [lines] (code mode) tail a background job's output (default 80 lines)",
7967
8764
  " /sessions list saved sessions (current is marked with \u25B8)",
7968
8765
  " /forget delete the current session from disk",
7969
8766
  " /new start fresh: drop all context + clear scrollback",
@@ -8128,7 +8925,19 @@ ${entry.text}`
8128
8925
  info: "/undo is only available inside `reasonix code` \u2014 chat mode doesn't apply edits."
8129
8926
  };
8130
8927
  }
8131
- return { info: ctx.codeUndo() };
8928
+ return { info: ctx.codeUndo(args) };
8929
+ }
8930
+ case "history": {
8931
+ if (!ctx.codeHistory) {
8932
+ return { info: "/history is only available inside `reasonix code`." };
8933
+ }
8934
+ return { info: ctx.codeHistory() };
8935
+ }
8936
+ case "show": {
8937
+ if (!ctx.codeShowEdit) {
8938
+ return { info: "/show is only available inside `reasonix code`." };
8939
+ }
8940
+ return { info: ctx.codeShowEdit(args) };
8132
8941
  }
8133
8942
  case "apply": {
8134
8943
  if (!ctx.codeApply) {
@@ -8168,6 +8977,81 @@ ${entry.text}`
8168
8977
  info: "\u25B8 plan mode OFF \u2014 write tools are live again. Model can still propose plans autonomously for large tasks."
8169
8978
  };
8170
8979
  }
8980
+ case "jobs": {
8981
+ if (!ctx.jobs) {
8982
+ return { info: "/jobs is only available inside `reasonix code`." };
8983
+ }
8984
+ const rows = ctx.jobs.list();
8985
+ if (rows.length === 0) {
8986
+ return { info: "no background jobs yet \u2014 use run_background to start one" };
8987
+ }
8988
+ const lines = ["Background jobs:"];
8989
+ for (const r of rows) {
8990
+ const age = ((Date.now() - r.startedAt) / 1e3).toFixed(1);
8991
+ const state = r.running ? `running \xB7 pid ${r.pid ?? "?"}` : r.exitCode !== null ? `exit ${r.exitCode}` : r.spawnError ? "failed" : "stopped";
8992
+ lines.push(
8993
+ ` ${String(r.id).padStart(3)} ${state.padEnd(20)} ${age.padStart(6)}s ago $ ${r.command}`
8994
+ );
8995
+ }
8996
+ lines.push("");
8997
+ lines.push("/kill <id> to stop one \xB7 /logs <id> [lines] to tail output");
8998
+ return { info: lines.join("\n") };
8999
+ }
9000
+ case "kill": {
9001
+ if (!ctx.jobs) return { info: "/kill is only available inside `reasonix code`." };
9002
+ const id = Number.parseInt(args[0] ?? "", 10);
9003
+ if (!Number.isFinite(id)) return { info: "usage: /kill <id> (see /jobs for ids)" };
9004
+ const rec = ctx.jobs.list().find((r) => r.id === id);
9005
+ if (!rec) return { info: `job ${id}: not found` };
9006
+ if (!rec.running) return { info: `job ${id} already exited (${rec.exitCode ?? "?"})` };
9007
+ const jobsRef = ctx.jobs;
9008
+ void (async () => {
9009
+ const final = await jobsRef.stop(id);
9010
+ if (!final) return;
9011
+ const status = final.running ? "still alive after SIGKILL (!) \u2014 report this as a bug" : final.exitCode !== null ? `exit ${final.exitCode}` : "stopped";
9012
+ ctx.postInfo?.(`\u25B8 job ${id} ${status}`);
9013
+ })();
9014
+ return {
9015
+ info: `\u25B8 stopping job ${id} (tree kill: SIGTERM \u2192 SIGKILL after 2s grace; Windows: taskkill /T /F)`
9016
+ };
9017
+ }
9018
+ case "logs": {
9019
+ if (!ctx.jobs) return { info: "/logs is only available inside `reasonix code`." };
9020
+ const id = Number.parseInt(args[0] ?? "", 10);
9021
+ if (!Number.isFinite(id)) {
9022
+ return { info: "usage: /logs <id> [lines] (default last 80 lines)" };
9023
+ }
9024
+ const requested = Number.parseInt(args[1] ?? "", 10);
9025
+ const tail = Number.isFinite(requested) && requested > 0 ? requested : 80;
9026
+ const out = ctx.jobs.read(id, { tailLines: tail });
9027
+ if (!out) return { info: `job ${id}: not found` };
9028
+ const status = out.running ? `running \xB7 pid ${out.pid ?? "?"}` : out.exitCode !== null ? `exited ${out.exitCode}` : out.spawnError ? `failed (${out.spawnError})` : "stopped";
9029
+ const header2 = `[job ${id} \xB7 ${status}]
9030
+ $ ${out.command}`;
9031
+ return { info: out.output ? `${header2}
9032
+ ${out.output}` : header2 };
9033
+ }
9034
+ case "mode": {
9035
+ if (!ctx.setEditMode) {
9036
+ return {
9037
+ info: "/mode is only available inside `reasonix code`."
9038
+ };
9039
+ }
9040
+ const raw = (args[0] ?? "").toLowerCase();
9041
+ const current = ctx.editMode ?? "review";
9042
+ let target;
9043
+ if (raw === "review") target = "review";
9044
+ else if (raw === "auto") target = "auto";
9045
+ else if (raw === "") {
9046
+ target = current === "auto" ? "review" : "auto";
9047
+ } else {
9048
+ return { info: "usage: /mode <review|auto> (Shift+Tab also cycles)" };
9049
+ }
9050
+ ctx.setEditMode(target);
9051
+ return {
9052
+ 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)"
9053
+ };
9054
+ }
8171
9055
  case "apply-plan":
8172
9056
  case "applyplan": {
8173
9057
  if (!ctx.setPlanMode) {
@@ -8203,11 +9087,11 @@ ${entry.text}`
8203
9087
  const { healedCount, tokensSaved, charsSaved } = loop.compact(cap);
8204
9088
  if (healedCount === 0) {
8205
9089
  return {
8206
- info: `\u25B8 nothing to compact \u2014 no tool result in history exceeds ${cap.toLocaleString()} tokens.`
9090
+ info: `\u25B8 nothing to compact \u2014 no tool result or tool-call args in history exceed ${cap.toLocaleString()} tokens.`
8207
9091
  };
8208
9092
  }
8209
9093
  return {
8210
- info: `\u25B8 compacted ${healedCount} tool result(s) to ${cap.toLocaleString()} tokens each, saved ${tokensSaved.toLocaleString()} tokens (${charsSaved.toLocaleString()} chars). Session file rewritten.`
9094
+ info: `\u25B8 compacted ${healedCount} payload(s) to ${cap.toLocaleString()} tokens each (tool results + tool-call args), saved ${tokensSaved.toLocaleString()} tokens (${charsSaved.toLocaleString()} chars). Session file rewritten.`
8211
9095
  };
8212
9096
  }
8213
9097
  case "sessions": {
@@ -8313,6 +9197,7 @@ ${entry.text}`
8313
9197
  const mcpLine = ` mcp ${mcpCount} server(s), ${toolCount} tool(s) in registry`;
8314
9198
  const pendingLine = pending > 0 ? ` edits ${pending} pending (/apply to commit, /discard to drop)` : "";
8315
9199
  const planLine = ctx.planMode ? " plan ON \u2014 writes gated (submit_plan + approval)" : "";
9200
+ 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)" : "";
8316
9201
  const lines = [
8317
9202
  ` model ${loop.model}`,
8318
9203
  ` flags harvest=${loop.harvestEnabled ? "on" : "off"} \xB7 branch=${branchBudget > 1 ? branchBudget : "off"} \xB7 stream=${loop.stream ? "on" : "off"} \xB7 effort=${loop.reasoningEffort}`,
@@ -8322,6 +9207,7 @@ ${entry.text}`
8322
9207
  ];
8323
9208
  if (pendingLine) lines.push(pendingLine);
8324
9209
  if (planLine) lines.push(planLine);
9210
+ if (modeLine) lines.push(modeLine);
8325
9211
  return { info: lines.join("\n") };
8326
9212
  }
8327
9213
  case "model": {
@@ -8408,14 +9294,18 @@ ${entry.text}`
8408
9294
  const raw = (args[0] ?? "").toLowerCase();
8409
9295
  if (raw === "") {
8410
9296
  return {
8411
- info: `reasoning_effort \u2192 ${loop.reasoningEffort} (use /effort high for cheaper/faster, /effort max for the agent-class default)`
9297
+ info: `reasoning_effort \u2192 ${loop.reasoningEffort} (use /effort high for cheaper/faster, /effort max for the agent-class default \xB7 persisted across relaunches)`
8412
9298
  };
8413
9299
  }
8414
9300
  if (raw !== "high" && raw !== "max") {
8415
9301
  return { info: "usage: /effort <high|max>" };
8416
9302
  }
8417
9303
  loop.configure({ reasoningEffort: raw });
8418
- return { info: `reasoning_effort \u2192 ${raw}` };
9304
+ try {
9305
+ saveReasoningEffort(raw);
9306
+ } catch {
9307
+ }
9308
+ return { info: `reasoning_effort \u2192 ${raw} (persisted)` };
8419
9309
  }
8420
9310
  default:
8421
9311
  return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
@@ -8827,6 +9717,14 @@ function gitTail(res) {
8827
9717
 
8828
9718
  // src/cli/ui/App.tsx
8829
9719
  var FLUSH_INTERVAL_MS = 100;
9720
+ function isEntryFullyUndone(e) {
9721
+ return e.snapshots.length > 0 && e.snapshots.every((s) => e.undoneFiles.has(s.path));
9722
+ }
9723
+ function entryStatus(e) {
9724
+ if (e.undoneFiles.size === 0) return "applied";
9725
+ if (isEntryFullyUndone(e)) return "UNDONE";
9726
+ return "PARTIAL";
9727
+ }
8830
9728
  var PLAIN_UI = process.env.REASONIX_UI === "plain";
8831
9729
  function App({
8832
9730
  model,
@@ -8842,36 +9740,66 @@ function App({
8842
9740
  codeMode
8843
9741
  }) {
8844
9742
  const { exit } = useApp();
8845
- const [historical, setHistorical] = useState5([]);
8846
- const [streaming, setStreaming] = useState5(null);
8847
- const [input, setInput] = useState5("");
8848
- const [busy, setBusy] = useState5(false);
9743
+ const [historical, setHistorical] = useState6([]);
9744
+ const [streaming, setStreaming] = useState6(null);
9745
+ const [input, setInput] = useState6("");
9746
+ const [busy, setBusy] = useState6(false);
8849
9747
  const abortedThisTurn = useRef2(false);
8850
- const [ongoingTool, setOngoingTool] = useState5(null);
8851
- const [toolProgress, setToolProgress] = useState5(null);
8852
- const [subagentActivity, setSubagentActivity] = useState5(null);
8853
- const [statusLine, setStatusLine] = useState5(null);
8854
- const [balance, setBalance] = useState5(null);
8855
- const [models, setModels] = useState5(null);
8856
- const [latestVersion, setLatestVersion] = useState5(null);
9748
+ const [ongoingTool, setOngoingTool] = useState6(null);
9749
+ const [toolProgress, setToolProgress] = useState6(null);
9750
+ const [subagentActivity, setSubagentActivity] = useState6(null);
9751
+ const [statusLine, setStatusLine] = useState6(null);
9752
+ const [balance, setBalance] = useState6(null);
9753
+ const [models, setModels] = useState6(null);
9754
+ const [latestVersion, setLatestVersion] = useState6(null);
8857
9755
  const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
8858
- const [hookList, setHookList] = useState5(
9756
+ const [hookList, setHookList] = useState6(
8859
9757
  () => loadHooks({ projectRoot: codeMode?.rootDir })
8860
9758
  );
8861
9759
  const hookCwd = codeMode?.rootDir ?? process.cwd();
8862
- const lastEditSnapshots = useRef2(null);
9760
+ const editHistory = useRef2([]);
9761
+ const nextHistoryId = useRef2(1);
9762
+ const currentTurnEntry = useRef2(null);
8863
9763
  const pendingEdits = useRef2([]);
8864
- const [pendingShell, setPendingShell] = useState5(null);
8865
- const [pendingPlan, setPendingPlan] = useState5(null);
8866
- const [stagedInput, setStagedInput] = useState5(null);
8867
- const [planMode, setPlanMode] = useState5(false);
8868
- const [queuedSubmit, setQueuedSubmit] = useState5(null);
9764
+ const [pendingCount, setPendingCount] = useState6(0);
9765
+ const syncPendingCount = useCallback(() => {
9766
+ setPendingCount(pendingEdits.current.length);
9767
+ }, []);
9768
+ const [editMode, setEditMode] = useState6(() => codeMode ? loadEditMode() : "review");
9769
+ const editModeRef = useRef2(editMode);
9770
+ useEffect2(() => {
9771
+ editModeRef.current = editMode;
9772
+ if (codeMode) saveEditMode(editMode);
9773
+ }, [editMode, codeMode]);
9774
+ const [undoBanner, setUndoBanner] = useState6(null);
9775
+ const undoTimeoutRef = useRef2(null);
9776
+ const [pendingEditReview, setPendingEditReview] = useState6(null);
9777
+ const editReviewResolveRef = useRef2(null);
9778
+ const turnEditPolicyRef = useRef2("ask");
9779
+ const [modeFlash, setModeFlash] = useState6(false);
9780
+ const modeFlashTimeoutRef = useRef2(null);
9781
+ const prevEditModeRef = useRef2(editMode);
9782
+ useEffect2(() => {
9783
+ if (prevEditModeRef.current === editMode) return;
9784
+ prevEditModeRef.current = editMode;
9785
+ setModeFlash(true);
9786
+ if (modeFlashTimeoutRef.current) clearTimeout(modeFlashTimeoutRef.current);
9787
+ modeFlashTimeoutRef.current = setTimeout(() => {
9788
+ setModeFlash(false);
9789
+ modeFlashTimeoutRef.current = null;
9790
+ }, 1200);
9791
+ }, [editMode]);
9792
+ const [pendingShell, setPendingShell] = useState6(null);
9793
+ const [pendingPlan, setPendingPlan] = useState6(null);
9794
+ const [stagedInput, setStagedInput] = useState6(null);
9795
+ const [planMode, setPlanMode] = useState6(false);
9796
+ const [queuedSubmit, setQueuedSubmit] = useState6(null);
8869
9797
  const promptHistory = useRef2([]);
8870
9798
  const historyCursor = useRef2(-1);
8871
9799
  const assistantIterCounter = useRef2(0);
8872
9800
  const toolHistoryRef = useRef2([]);
8873
- const [slashSelected, setSlashSelected] = useState5(0);
8874
- const [summary, setSummary] = useState5({
9801
+ const [slashSelected, setSlashSelected] = useState6(0);
9802
+ const [summary, setSummary] = useState6({
8875
9803
  turns: 0,
8876
9804
  totalCostUsd: 0,
8877
9805
  totalInputCostUsd: 0,
@@ -8895,7 +9823,7 @@ function App({
8895
9823
  transcriptRef.current?.end();
8896
9824
  };
8897
9825
  }, []);
8898
- const slashMatches = useMemo(() => {
9826
+ const slashMatches = useMemo2(() => {
8899
9827
  if (!input.startsWith("/") || input.includes(" ")) return null;
8900
9828
  return suggestSlashCommands(input.slice(1), !!codeMode);
8901
9829
  }, [input, codeMode]);
@@ -8906,8 +9834,8 @@ function App({
8906
9834
  return prev;
8907
9835
  });
8908
9836
  }, [slashMatches]);
8909
- const [atSelected, setAtSelected] = useState5(0);
8910
- const atFiles = useMemo(() => {
9837
+ const [atSelected, setAtSelected] = useState6(0);
9838
+ const atFiles = useMemo2(() => {
8911
9839
  if (!codeMode?.rootDir) return [];
8912
9840
  try {
8913
9841
  return listFilesWithStatsSync(codeMode.rootDir, { maxResults: 500 });
@@ -8923,12 +9851,12 @@ function App({
8923
9851
  list.unshift(p);
8924
9852
  if (list.length > 20) list.length = 20;
8925
9853
  }, []);
8926
- const atPicker = useMemo(() => {
9854
+ const atPicker = useMemo2(() => {
8927
9855
  if (!codeMode?.rootDir) return null;
8928
9856
  if (slashMatches !== null) return null;
8929
9857
  return detectAtPicker(input);
8930
9858
  }, [codeMode?.rootDir, input, slashMatches]);
8931
- const atMatches = useMemo(() => {
9859
+ const atMatches = useMemo2(() => {
8932
9860
  if (!atPicker) return null;
8933
9861
  return rankPickerCandidates(atFiles, atPicker.query, {
8934
9862
  limit: 40,
@@ -8950,13 +9878,13 @@ function App({
8950
9878
  },
8951
9879
  [atPicker, input]
8952
9880
  );
8953
- const [slashArgSelected, setSlashArgSelected] = useState5(0);
8954
- const slashArgContext = useMemo(() => {
9881
+ const [slashArgSelected, setSlashArgSelected] = useState6(0);
9882
+ const slashArgContext = useMemo2(() => {
8955
9883
  if (!input.startsWith("/")) return null;
8956
9884
  if (slashMatches !== null) return null;
8957
9885
  return detectSlashArgContext(input, !!codeMode);
8958
9886
  }, [input, slashMatches, codeMode]);
8959
- const slashArgMatches = useMemo(() => {
9887
+ const slashArgMatches = useMemo2(() => {
8960
9888
  if (!slashArgContext || slashArgContext.kind !== "picker") return null;
8961
9889
  const completer = slashArgContext.spec.argCompleter;
8962
9890
  const partial = slashArgContext.partial;
@@ -9013,7 +9941,7 @@ function App({
9013
9941
  );
9014
9942
  const loopRef = useRef2(null);
9015
9943
  const subagentSinkRef = useRef2({ current: null });
9016
- const loop = useMemo(() => {
9944
+ const loop = useMemo2(() => {
9017
9945
  if (loopRef.current) return loopRef.current;
9018
9946
  const client = new DeepSeekClient();
9019
9947
  if (tools && !tools.has("run_skill")) {
@@ -9052,7 +9980,11 @@ function App({
9052
9980
  branch,
9053
9981
  session,
9054
9982
  hooks: hookList,
9055
- hookCwd
9983
+ hookCwd,
9984
+ // Restore the user's last-chosen effort cap. Without this a
9985
+ // `/effort high` silently reverted to `max` on relaunch — the
9986
+ // loop's constructor default wins over persisted state.
9987
+ reasoningEffort: loadReasoningEffort()
9056
9988
  });
9057
9989
  loopRef.current = l;
9058
9990
  return l;
@@ -9192,6 +10124,7 @@ function App({
9192
10124
  const restored = loadPendingEdits(session);
9193
10125
  if (restored && restored.length > 0) {
9194
10126
  pendingEdits.current = restored;
10127
+ syncPendingCount();
9195
10128
  setHistorical((prev) => [
9196
10129
  ...prev,
9197
10130
  {
@@ -9202,14 +10135,54 @@ function App({
9202
10135
  ]);
9203
10136
  }
9204
10137
  }
9205
- }, [session, loop, codeMode]);
9206
- useInput4((_input, key) => {
10138
+ if (codeMode && !editModeHintShown()) {
10139
+ setHistorical((prev) => [
10140
+ ...prev,
10141
+ {
10142
+ id: `sys-edittip-${Date.now()}`,
10143
+ role: "info",
10144
+ text: "\u25B8 TIP: edit-gate keybindings\n y / n accept or drop pending edits\n Shift+Tab switch review \u2194 AUTO (persisted; AUTO applies instantly)\n u undo the last auto-applied batch (within the 5s banner)\n Current mode is shown in the bottom status bar. Run /keys anytime for the full list.\n (This tip shows once \u2014 suppressed after.)"
10145
+ }
10146
+ ]);
10147
+ markEditModeHintShown();
10148
+ }
10149
+ }, [session, loop, codeMode, syncPendingCount]);
10150
+ useInput5((chKey, key) => {
9207
10151
  if (key.escape && busy) {
9208
10152
  if (abortedThisTurn.current) return;
9209
10153
  abortedThisTurn.current = true;
10154
+ const resolve8 = editReviewResolveRef.current;
10155
+ if (resolve8) {
10156
+ editReviewResolveRef.current = null;
10157
+ setPendingEditReview(null);
10158
+ resolve8("reject");
10159
+ }
9210
10160
  loop.abort();
9211
10161
  return;
9212
10162
  }
10163
+ if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview) {
10164
+ setEditMode((m) => {
10165
+ const next = m === "auto" ? "review" : "auto";
10166
+ setHistorical((prev) => [
10167
+ ...prev,
10168
+ {
10169
+ id: `mode-${Date.now()}`,
10170
+ role: "info",
10171
+ 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)"
10172
+ }
10173
+ ]);
10174
+ return next;
10175
+ });
10176
+ return;
10177
+ }
10178
+ if (codeMode && input.length === 0 && (chKey === "u" || chKey === "U") && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && // Fire when EITHER the banner is up OR there's any non-undone
10179
+ // history entry — the keybind is useful long after the 5-second
10180
+ // banner expires, which users rightly want.
10181
+ (undoBanner || editHistory.current.some((e) => !isEntryFullyUndone(e)))) {
10182
+ const out = codeUndo([]);
10183
+ setHistorical((prev) => [...prev, { id: `undo-${Date.now()}`, role: "info", text: out }]);
10184
+ return;
10185
+ }
9213
10186
  if (busy) return;
9214
10187
  if (pendingShell) return;
9215
10188
  if (atMatches && atMatches.length > 0) {
@@ -9275,16 +10248,269 @@ function App({
9275
10248
  }
9276
10249
  }
9277
10250
  });
9278
- const codeUndo = useCallback(() => {
10251
+ const recordEdit = useCallback(
10252
+ (source, blocks, results, snaps) => {
10253
+ if (snaps.length === 0) return;
10254
+ let entry = currentTurnEntry.current;
10255
+ if (!entry) {
10256
+ entry = {
10257
+ id: nextHistoryId.current++,
10258
+ at: Date.now(),
10259
+ source,
10260
+ blocks: [],
10261
+ results: [],
10262
+ snapshots: [],
10263
+ undoneFiles: /* @__PURE__ */ new Set()
10264
+ };
10265
+ currentTurnEntry.current = entry;
10266
+ editHistory.current.push(entry);
10267
+ }
10268
+ entry.blocks.push(...blocks);
10269
+ entry.results.push(...results);
10270
+ const seen = new Set(entry.snapshots.map((s) => s.path));
10271
+ for (const s of snaps) {
10272
+ if (!seen.has(s.path)) entry.snapshots.push(s);
10273
+ }
10274
+ },
10275
+ []
10276
+ );
10277
+ const armUndoBanner = useCallback((results) => {
10278
+ setUndoBanner({ results, expiresAt: Date.now() + 5e3 });
10279
+ if (undoTimeoutRef.current) clearTimeout(undoTimeoutRef.current);
10280
+ undoTimeoutRef.current = setTimeout(() => {
10281
+ setUndoBanner(null);
10282
+ undoTimeoutRef.current = null;
10283
+ }, 5e3);
10284
+ }, []);
10285
+ const codeUndo = useCallback(
10286
+ (args = []) => {
10287
+ if (!codeMode) return "not in code mode";
10288
+ const root = codeMode.rootDir;
10289
+ const revert = (entry2, paths) => {
10290
+ const subset = entry2.snapshots.filter((s) => paths.includes(s.path));
10291
+ if (subset.length === 0) {
10292
+ return `batch #${entry2.id}: nothing to undo (already restored or path not in batch)`;
10293
+ }
10294
+ const results = restoreSnapshots(subset, root);
10295
+ for (const s of subset) entry2.undoneFiles.add(s.path);
10296
+ if (currentTurnEntry.current === entry2 && isEntryFullyUndone(entry2)) {
10297
+ currentTurnEntry.current = null;
10298
+ }
10299
+ if (undoTimeoutRef.current) {
10300
+ clearTimeout(undoTimeoutRef.current);
10301
+ undoTimeoutRef.current = null;
10302
+ }
10303
+ setUndoBanner(null);
10304
+ const when = new Date(entry2.at).toISOString().replace("T", " ").slice(11, 19);
10305
+ const scope = subset.length === 1 ? subset[0].path : `${subset.length} file(s)`;
10306
+ const header2 = `\u25B8 undo: reverted ${scope} from batch #${entry2.id} (${when})`;
10307
+ return [header2, ...formatUndoRows(results)].join("\n");
10308
+ };
10309
+ const idArg = args[0];
10310
+ const pathArg = args[1];
10311
+ if (!idArg) {
10312
+ for (let i = editHistory.current.length - 1; i >= 0; i--) {
10313
+ const e = editHistory.current[i];
10314
+ if (isEntryFullyUndone(e)) continue;
10315
+ const remaining = e.snapshots.map((s) => s.path).filter((p) => !e.undoneFiles.has(p));
10316
+ return revert(e, remaining);
10317
+ }
10318
+ return "nothing to undo \u2014 every batch in the session history is already undone";
10319
+ }
10320
+ const id = Number.parseInt(idArg, 10);
10321
+ if (!Number.isFinite(id)) {
10322
+ return "usage: /undo [id] [path] (omit id for newest; id from /history; path from /show <id>)";
10323
+ }
10324
+ const entry = editHistory.current.find((e) => e.id === id);
10325
+ if (!entry) return `no edit #${id} \u2014 run /history to see valid ids`;
10326
+ if (!pathArg) {
10327
+ const remaining = entry.snapshots.map((s) => s.path).filter((p) => !entry.undoneFiles.has(p));
10328
+ if (remaining.length === 0) return `batch #${id} is already fully undone`;
10329
+ return revert(entry, remaining);
10330
+ }
10331
+ const snap = entry.snapshots.find((s) => s.path === pathArg);
10332
+ if (!snap) {
10333
+ const files = [...new Set(entry.blocks.map((b) => b.path))];
10334
+ return `batch #${id} doesn't include "${pathArg}" \u2014 files in this batch: ${files.join(", ")}`;
10335
+ }
10336
+ if (entry.undoneFiles.has(pathArg)) {
10337
+ return `${pathArg} in batch #${id} is already undone`;
10338
+ }
10339
+ return revert(entry, [pathArg]);
10340
+ },
10341
+ [codeMode]
10342
+ );
10343
+ const codeHistory = useCallback(() => {
9279
10344
  if (!codeMode) return "not in code mode";
9280
- const snaps = lastEditSnapshots.current;
9281
- if (!snaps || snaps.length === 0) {
9282
- return "nothing to undo \u2014 no recent edit batch to restore";
10345
+ const entries = editHistory.current;
10346
+ if (entries.length === 0) return "no edits recorded this session yet";
10347
+ const lines = ["Edit history (oldest first):"];
10348
+ for (const e of entries) {
10349
+ const when = new Date(e.at).toISOString().replace("T", " ").slice(11, 19);
10350
+ const files = new Set(e.blocks.map((b) => b.path));
10351
+ const fileList = [...files].join(", ");
10352
+ const fileSummary = fileList.length > 60 ? `${fileList.slice(0, 60)}\u2026` : fileList;
10353
+ const status = entryStatus(e);
10354
+ const statusText = status === "applied" ? "applied" : status === "PARTIAL" ? "PARTIAL" : "UNDONE ";
10355
+ lines.push(
10356
+ ` #${String(e.id).padStart(3)} ${when} ${statusText} ${e.source.padEnd(12)} ${files.size} file \xB7 ${e.blocks.length} block ${fileSummary}`
10357
+ );
9283
10358
  }
9284
- const results = restoreSnapshots(snaps, codeMode.rootDir);
9285
- lastEditSnapshots.current = null;
9286
- return formatUndoResults(results);
10359
+ lines.push("");
10360
+ lines.push(
10361
+ "/show <id> \u2192 per-file summary \xB7 /show <id> <path> \u2192 full diff of one file"
10362
+ );
10363
+ lines.push(
10364
+ "/undo \u2192 newest non-undone \xB7 /undo <id> [path] \u2192 target a specific batch or file"
10365
+ );
10366
+ return lines.join("\n");
9287
10367
  }, [codeMode]);
10368
+ const codeShowEdit = useCallback(
10369
+ (args = []) => {
10370
+ if (!codeMode) return "not in code mode";
10371
+ const entries = editHistory.current;
10372
+ if (entries.length === 0) return "no edits recorded this session \u2014 /history is empty";
10373
+ const idArg = args[0];
10374
+ const pathArg = args[1];
10375
+ let entry;
10376
+ if (!idArg) {
10377
+ entry = [...entries].reverse().find((e) => !isEntryFullyUndone(e)) ?? entries[entries.length - 1];
10378
+ } else {
10379
+ const id = Number.parseInt(idArg, 10);
10380
+ if (!Number.isFinite(id)) {
10381
+ return "usage: /show [id] [path] (omit id for newest; path from the per-file summary)";
10382
+ }
10383
+ entry = entries.find((e) => e.id === id);
10384
+ if (!entry) return `no edit #${id} \u2014 run /history to see valid ids`;
10385
+ }
10386
+ if (!entry) return "unexpected: history lookup failed";
10387
+ if (pathArg) {
10388
+ const fileBlocks = entry.blocks.filter((b) => b.path === pathArg);
10389
+ if (fileBlocks.length === 0) {
10390
+ const files2 = [...new Set(entry.blocks.map((b) => b.path))];
10391
+ return `batch #${entry.id} doesn't include "${pathArg}" \u2014 files in this batch: ${files2.join(", ")}`;
10392
+ }
10393
+ const when2 = new Date(entry.at).toISOString().replace("T", " ").slice(11, 19);
10394
+ const state = entry.undoneFiles.has(pathArg) ? "UNDONE" : "applied";
10395
+ const header3 = `\u25B8 edit #${entry.id} \xB7 ${when2} \xB7 ${pathArg} \xB7 ${state} \xB7 ${fileBlocks.length} block(s)`;
10396
+ const diff = formatAllBlockDiffs(fileBlocks, { maxLines: 60, contextLines: 2 });
10397
+ const footer = entry.undoneFiles.has(pathArg) ? "(already reverted \u2014 /history shows the batch-level status)" : `/undo ${entry.id} ${pathArg} \u2192 revert just this file`;
10398
+ return [header3, ...diff, "", footer].join("\n");
10399
+ }
10400
+ const when = new Date(entry.at).toISOString().replace("T", " ").slice(11, 19);
10401
+ const files = [...new Set(entry.blocks.map((b) => b.path))];
10402
+ const status = entryStatus(entry);
10403
+ const header2 = `\u25B8 edit #${entry.id} \xB7 ${when} \xB7 ${entry.source} \xB7 ${status} \xB7 ${files.length} file(s)`;
10404
+ const countLines3 = (s) => s.length === 0 ? 0 : (s.match(/\n/g)?.length ?? 0) + 1;
10405
+ const fileLines = files.map((path) => {
10406
+ const fileBlocks = entry.blocks.filter((b) => b.path === path);
10407
+ let removed = 0;
10408
+ let added = 0;
10409
+ for (const b of fileBlocks) {
10410
+ removed += countLines3(b.search);
10411
+ added += countLines3(b.replace);
10412
+ }
10413
+ const state = entry.undoneFiles.has(path) ? "UNDONE" : "applied";
10414
+ return ` ${state.padEnd(7)} -${String(removed).padStart(3)}/+${String(added).padStart(3)} ${path} (${fileBlocks.length} block${fileBlocks.length === 1 ? "" : "s"})`;
10415
+ });
10416
+ return [
10417
+ header2,
10418
+ ...fileLines,
10419
+ "",
10420
+ `/show ${entry.id} <path> \u2192 full diff of one file`,
10421
+ `/undo ${entry.id} <path> \u2192 revert just that file \xB7 /undo ${entry.id} \u2192 revert whole batch`
10422
+ ].join("\n");
10423
+ },
10424
+ [codeMode]
10425
+ );
10426
+ useEffect2(() => {
10427
+ if (!tools || !codeMode) return;
10428
+ tools.setToolInterceptor(async (name, args) => {
10429
+ if (name !== "edit_file" && name !== "write_file") return null;
10430
+ const rawPath = typeof args.path === "string" ? args.path : "";
10431
+ if (!rawPath) return null;
10432
+ let relPath = rawPath;
10433
+ while (relPath.startsWith("/") || relPath.startsWith("\\")) {
10434
+ relPath = relPath.slice(1);
10435
+ }
10436
+ if (!relPath) return null;
10437
+ let block;
10438
+ if (name === "edit_file") {
10439
+ const search = typeof args.search === "string" ? args.search : "";
10440
+ const replace = typeof args.replace === "string" ? args.replace : "";
10441
+ if (!search) return null;
10442
+ block = { path: relPath, search, replace, offset: 0 };
10443
+ } else {
10444
+ const content = typeof args.content === "string" ? args.content : "";
10445
+ block = toWholeFileEditBlock(relPath, content, codeMode.rootDir);
10446
+ }
10447
+ const applyNow = () => {
10448
+ const snaps = snapshotBeforeEdits([block], codeMode.rootDir);
10449
+ const results = applyEditBlocks([block], codeMode.rootDir);
10450
+ const good = results.some((r) => r.status === "applied" || r.status === "created");
10451
+ if (good) {
10452
+ recordEdit("auto", [block], results, snaps);
10453
+ armUndoBanner(results);
10454
+ }
10455
+ setHistorical((prev) => [
10456
+ ...prev,
10457
+ {
10458
+ id: `ae-${Date.now()}-${Math.random()}`,
10459
+ role: "info",
10460
+ text: formatEditResults(results)
10461
+ }
10462
+ ]);
10463
+ return formatEditResults(results);
10464
+ };
10465
+ if (editModeRef.current === "auto") return applyNow();
10466
+ if (turnEditPolicyRef.current === "apply-all") return applyNow();
10467
+ const choice = await new Promise((resolveChoice) => {
10468
+ editReviewResolveRef.current = resolveChoice;
10469
+ setPendingEditReview(block);
10470
+ });
10471
+ editReviewResolveRef.current = null;
10472
+ setPendingEditReview(null);
10473
+ if (choice === "reject") {
10474
+ setHistorical((prev) => [
10475
+ ...prev,
10476
+ {
10477
+ id: `er-${Date.now()}-${Math.random()}`,
10478
+ role: "info",
10479
+ text: `\u25B8 rejected edit to ${block.path}`
10480
+ }
10481
+ ]);
10482
+ return `User rejected this edit to ${block.path}. Don't retry the same SEARCH/REPLACE \u2014 either try a different approach or ask the user what they want instead.`;
10483
+ }
10484
+ if (choice === "apply-rest-of-turn") {
10485
+ turnEditPolicyRef.current = "apply-all";
10486
+ setHistorical((prev) => [
10487
+ ...prev,
10488
+ {
10489
+ id: `er-${Date.now()}-${Math.random()}`,
10490
+ role: "info",
10491
+ text: "\u25B8 auto-approving remaining edits for this turn"
10492
+ }
10493
+ ]);
10494
+ return applyNow();
10495
+ }
10496
+ if (choice === "flip-to-auto") {
10497
+ setEditMode("auto");
10498
+ setHistorical((prev) => [
10499
+ ...prev,
10500
+ {
10501
+ id: `er-${Date.now()}-${Math.random()}`,
10502
+ role: "info",
10503
+ text: "\u25B8 flipped to AUTO mode for the rest of the session (persisted)"
10504
+ }
10505
+ ]);
10506
+ return applyNow();
10507
+ }
10508
+ return applyNow();
10509
+ });
10510
+ return () => {
10511
+ tools.setToolInterceptor(null);
10512
+ };
10513
+ }, [tools, codeMode, session, recordEdit, armUndoBanner, syncPendingCount, setEditMode]);
9288
10514
  const codeApply = useCallback(() => {
9289
10515
  if (!codeMode) return "not in code mode";
9290
10516
  const blocks = pendingEdits.current;
@@ -9294,18 +10520,20 @@ function App({
9294
10520
  const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
9295
10521
  const results = applyEditBlocks(blocks, codeMode.rootDir);
9296
10522
  const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
9297
- if (anyApplied) lastEditSnapshots.current = snaps;
10523
+ if (anyApplied) recordEdit("review-apply", blocks, results, snaps);
9298
10524
  pendingEdits.current = [];
9299
10525
  clearPendingEdits(session ?? null);
10526
+ syncPendingCount();
9300
10527
  return formatEditResults(results);
9301
- }, [codeMode, session]);
10528
+ }, [codeMode, session, syncPendingCount, recordEdit]);
9302
10529
  const codeDiscard = useCallback(() => {
9303
10530
  const count = pendingEdits.current.length;
9304
10531
  if (count === 0) return "nothing pending to discard.";
9305
10532
  pendingEdits.current = [];
9306
10533
  clearPendingEdits(session ?? null);
10534
+ syncPendingCount();
9307
10535
  return `\u25B8 discarded ${count} pending edit block(s). Nothing was written to disk.`;
9308
- }, [session]);
10536
+ }, [session, syncPendingCount]);
9309
10537
  const prefixHash = loop.prefix.fingerprint;
9310
10538
  const writeTranscript = useCallback(
9311
10539
  (ev) => {
@@ -9423,6 +10651,8 @@ function App({
9423
10651
  codeUndo: codeMode ? codeUndo : void 0,
9424
10652
  codeApply: codeMode ? codeApply : void 0,
9425
10653
  codeDiscard: codeMode ? codeDiscard : void 0,
10654
+ codeHistory: codeMode ? codeHistory : void 0,
10655
+ codeShowEdit: codeMode ? codeShowEdit : void 0,
9426
10656
  codeRoot: codeMode?.rootDir,
9427
10657
  pendingEditCount: codeMode ? pendingEdits.current.length : void 0,
9428
10658
  toolHistory: () => toolHistoryRef.current,
@@ -9430,6 +10660,13 @@ function App({
9430
10660
  planMode,
9431
10661
  setPlanMode: codeMode ? togglePlanMode : void 0,
9432
10662
  clearPendingPlan: codeMode ? clearPendingPlan : void 0,
10663
+ editMode: codeMode ? editMode : void 0,
10664
+ setEditMode: codeMode ? setEditMode : void 0,
10665
+ jobs: codeMode?.jobs,
10666
+ postInfo: (text2) => setHistorical((prev) => [
10667
+ ...prev,
10668
+ { id: `sys-late-${Date.now()}-${Math.random()}`, role: "info", text: text2 }
10669
+ ]),
9433
10670
  reloadHooks: () => {
9434
10671
  const fresh = loadHooks({ projectRoot: codeMode?.rootDir });
9435
10672
  setHookList(fresh);
@@ -9466,6 +10703,7 @@ function App({
9466
10703
  if (codeMode) {
9467
10704
  pendingEdits.current = [];
9468
10705
  clearPendingEdits(session ?? null);
10706
+ syncPendingCount();
9469
10707
  }
9470
10708
  return;
9471
10709
  }
@@ -9474,6 +10712,7 @@ function App({
9474
10712
  if (codeMode) {
9475
10713
  pendingEdits.current = [];
9476
10714
  clearPendingEdits(session ?? null);
10715
+ syncPendingCount();
9477
10716
  }
9478
10717
  return;
9479
10718
  }
@@ -9536,6 +10775,10 @@ function App({
9536
10775
  setStreaming({ id: assistantId, role: "assistant", text: "", streaming: true });
9537
10776
  setBusy(true);
9538
10777
  abortedThisTurn.current = false;
10778
+ if (codeMode) {
10779
+ currentTurnEntry.current = null;
10780
+ }
10781
+ turnEditPolicyRef.current = "ask";
9539
10782
  const flush = () => {
9540
10783
  if (!contentBuf.current && !reasoningBuf.current && !toolCallBuildBuf.current) return;
9541
10784
  streamRef.text += contentBuf.current;
@@ -9649,16 +10892,37 @@ function App({
9649
10892
  if (codeMode && finalText && !ev.forcedSummary) {
9650
10893
  const blocks = parseEditBlocks(finalText);
9651
10894
  if (blocks.length > 0) {
9652
- pendingEdits.current = blocks;
9653
- savePendingEdits(session ?? null, blocks);
9654
- setHistorical((prev) => [
9655
- ...prev,
9656
- {
9657
- id: `pending-${Date.now()}`,
9658
- role: "info",
9659
- text: formatPendingPreview(blocks)
10895
+ if (editModeRef.current === "auto") {
10896
+ const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
10897
+ const results = applyEditBlocks(blocks, codeMode.rootDir);
10898
+ const good = results.some(
10899
+ (r) => r.status === "applied" || r.status === "created"
10900
+ );
10901
+ if (good) {
10902
+ recordEdit("auto-text", blocks, results, snaps);
10903
+ armUndoBanner(results);
9660
10904
  }
9661
- ]);
10905
+ setHistorical((prev) => [
10906
+ ...prev,
10907
+ {
10908
+ id: `applied-${Date.now()}`,
10909
+ role: "info",
10910
+ text: formatEditResults(results)
10911
+ }
10912
+ ]);
10913
+ } else {
10914
+ pendingEdits.current = [...pendingEdits.current, ...blocks];
10915
+ savePendingEdits(session ?? null, pendingEdits.current);
10916
+ syncPendingCount();
10917
+ setHistorical((prev) => [
10918
+ ...prev,
10919
+ {
10920
+ id: `pending-${Date.now()}`,
10921
+ role: "info",
10922
+ text: formatPendingPreview(pendingEdits.current)
10923
+ }
10924
+ ]);
10925
+ }
9662
10926
  }
9663
10927
  }
9664
10928
  } else if (ev.role === "tool_start") {
@@ -9694,11 +10958,14 @@ function App({
9694
10958
  toolName: ev.toolName
9695
10959
  }
9696
10960
  ]);
9697
- if (codeMode && ev.toolName === "run_command" && ev.content.includes('"NeedsConfirmationError:') && ev.toolArgs) {
10961
+ if (codeMode && (ev.toolName === "run_command" || ev.toolName === "run_background") && ev.content.includes('"NeedsConfirmationError:') && ev.toolArgs) {
9698
10962
  try {
9699
10963
  const parsed = JSON.parse(ev.toolArgs);
9700
10964
  if (typeof parsed.command === "string" && parsed.command.trim()) {
9701
- setPendingShell(parsed.command.trim());
10965
+ setPendingShell({
10966
+ command: parsed.command.trim(),
10967
+ kind: ev.toolName
10968
+ });
9702
10969
  }
9703
10970
  } catch {
9704
10971
  }
@@ -9769,7 +11036,9 @@ function App({
9769
11036
  clearPendingPlan,
9770
11037
  codeApply,
9771
11038
  codeDiscard,
11039
+ codeHistory,
9772
11040
  codeMode,
11041
+ codeShowEdit,
9773
11042
  codeUndo,
9774
11043
  exit,
9775
11044
  hookCwd,
@@ -9792,13 +11061,18 @@ function App({
9792
11061
  slashArgSelected,
9793
11062
  pickSlashArg,
9794
11063
  togglePlanMode,
9795
- writeTranscript
11064
+ writeTranscript,
11065
+ recordEdit,
11066
+ armUndoBanner,
11067
+ editMode,
11068
+ syncPendingCount
9796
11069
  ]
9797
11070
  );
9798
11071
  const handleShellConfirm = useCallback(
9799
11072
  async (choice) => {
9800
- const cmd = pendingShell;
9801
- if (!cmd || !codeMode) return;
11073
+ const pending = pendingShell;
11074
+ if (!pending || !codeMode) return;
11075
+ const { command: cmd, kind } = pending;
9802
11076
  setPendingShell(null);
9803
11077
  let synthetic;
9804
11078
  if (choice === "deny") {
@@ -9822,23 +11096,65 @@ function App({
9822
11096
  }
9823
11097
  setHistorical((prev) => [
9824
11098
  ...prev,
9825
- { id: `sh-run-${Date.now()}`, role: "info", text: `\u25B8 running: ${cmd}` }
11099
+ {
11100
+ id: `sh-run-${Date.now()}`,
11101
+ role: "info",
11102
+ text: kind === "run_background" ? `\u25B8 starting (background): ${cmd}` : `\u25B8 running: ${cmd}`
11103
+ }
9826
11104
  ]);
9827
- let body;
9828
- try {
9829
- const res = await runCommand(cmd, { cwd: codeMode.rootDir });
9830
- body = formatCommandResult(cmd, res);
9831
- } catch (err) {
9832
- body = `$ ${cmd}
11105
+ if (kind === "run_background" && codeMode.jobs) {
11106
+ let startedOk = false;
11107
+ let jobId = null;
11108
+ let preview = "";
11109
+ try {
11110
+ const res = await codeMode.jobs.start(cmd, { cwd: codeMode.rootDir });
11111
+ startedOk = true;
11112
+ jobId = res.jobId;
11113
+ preview = res.preview;
11114
+ const header2 = res.stillRunning ? `[job ${res.jobId} started \xB7 pid ${res.pid ?? "?"} \xB7 ${res.readyMatched ? "READY signal matched" : "running"}]` : res.exitCode !== null ? `[job ${res.jobId} exited during startup \xB7 exit ${res.exitCode}]` : `[job ${res.jobId} failed to start]`;
11115
+ const body = preview ? `${header2}
11116
+ ${preview}` : header2;
11117
+ setHistorical((prev) => [
11118
+ ...prev,
11119
+ { id: `sh-out-${Date.now()}`, role: "info", text: body }
11120
+ ]);
11121
+ synthetic = `I approved the background spawn. ${header2}
11122
+
11123
+ Startup preview:
11124
+
11125
+ ${preview || "(no output yet)"}
11126
+
11127
+ The process is still running \u2014 use job_output to read newer logs, stop_job to halt it.`;
11128
+ } catch (err) {
11129
+ const msg = `$ ${cmd}
11130
+ [failed to start] ${err.message}`;
11131
+ setHistorical((prev) => [
11132
+ ...prev,
11133
+ { id: `sh-out-${Date.now()}`, role: "info", text: msg }
11134
+ ]);
11135
+ synthetic = `I approved the background spawn but it failed to start:
11136
+
11137
+ ${msg}`;
11138
+ }
11139
+ void startedOk;
11140
+ void jobId;
11141
+ } else {
11142
+ let body;
11143
+ try {
11144
+ const res = await runCommand(cmd, { cwd: codeMode.rootDir });
11145
+ body = formatCommandResult(cmd, res);
11146
+ } catch (err) {
11147
+ body = `$ ${cmd}
9833
11148
  [failed to spawn] ${err.message}`;
9834
- }
9835
- setHistorical((prev) => [
9836
- ...prev,
9837
- { id: `sh-out-${Date.now()}`, role: "info", text: body }
9838
- ]);
9839
- synthetic = `I ran the command you requested. Output:
11149
+ }
11150
+ setHistorical((prev) => [
11151
+ ...prev,
11152
+ { id: `sh-out-${Date.now()}`, role: "info", text: body }
11153
+ ]);
11154
+ synthetic = `I ran the command you requested. Output:
9840
11155
 
9841
11156
  ${body}`;
11157
+ }
9842
11158
  }
9843
11159
  if (busy) {
9844
11160
  loop.abort();
@@ -9949,7 +11265,7 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
9949
11265
  if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
9950
11266
  setStagedInput(null);
9951
11267
  }, [stagedInput]);
9952
- return /* @__PURE__ */ React14.createElement(TickerProvider, { disabled: PLAIN_UI || !!pendingPlan || !!pendingShell }, /* @__PURE__ */ React14.createElement(Box13, { flexDirection: "column" }, /* @__PURE__ */ React14.createElement(
11268
+ return /* @__PURE__ */ React15.createElement(TickerProvider, { disabled: PLAIN_UI || !!pendingPlan || !!pendingShell || !!pendingEditReview }, /* @__PURE__ */ React15.createElement(Box14, { flexDirection: "column" }, /* @__PURE__ */ React15.createElement(
9953
11269
  StatsPanel,
9954
11270
  {
9955
11271
  summary,
@@ -9959,32 +11275,56 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
9959
11275
  branchBudget: loop.branchOptions.budget,
9960
11276
  reasoningEffort: loop.reasoningEffort,
9961
11277
  planMode,
11278
+ editMode: codeMode ? editMode : void 0,
9962
11279
  balance,
9963
11280
  busy,
9964
11281
  updateAvailable
9965
11282
  }
9966
- ), /* @__PURE__ */ React14.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React14.createElement(EventRow, { key: item.id, event: item, projectRoot: hookCwd })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && streaming ? /* @__PURE__ */ React14.createElement(Box13, { marginY: 1 }, /* @__PURE__ */ React14.createElement(EventRow, { event: streaming, projectRoot: hookCwd })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && ongoingTool ? /* @__PURE__ */ React14.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && subagentActivity ? /* @__PURE__ */ React14.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !ongoingTool && statusLine ? /* @__PURE__ */ React14.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React14.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React14.createElement(
11283
+ ), /* @__PURE__ */ React15.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React15.createElement(EventRow, { key: item.id, event: item, projectRoot: hookCwd })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && streaming ? /* @__PURE__ */ React15.createElement(Box14, { marginY: 1 }, /* @__PURE__ */ React15.createElement(EventRow, { event: streaming, projectRoot: hookCwd })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && ongoingTool ? /* @__PURE__ */ React15.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && subagentActivity ? /* @__PURE__ */ React15.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !ongoingTool && statusLine ? /* @__PURE__ */ React15.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && undoBanner && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview ? /* @__PURE__ */ React15.createElement(UndoBanner, { banner: undoBanner }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React15.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React15.createElement(
9967
11284
  PlanRefineInput,
9968
11285
  {
9969
11286
  mode: stagedInput.mode,
9970
11287
  onSubmit: handleStagedInputSubmit,
9971
11288
  onCancel: handleStagedInputCancel
9972
11289
  }
9973
- ) : pendingPlan ? /* @__PURE__ */ React14.createElement(
11290
+ ) : pendingPlan ? /* @__PURE__ */ React15.createElement(
9974
11291
  PlanConfirm,
9975
11292
  {
9976
11293
  plan: pendingPlan,
9977
11294
  onChoose: stableHandlePlanConfirm,
9978
11295
  projectRoot: hookCwd
9979
11296
  }
9980
- ) : pendingShell ? /* @__PURE__ */ React14.createElement(
11297
+ ) : pendingShell ? /* @__PURE__ */ React15.createElement(
9981
11298
  ShellConfirm,
9982
11299
  {
9983
- command: pendingShell,
9984
- allowPrefix: derivePrefix(pendingShell),
11300
+ command: pendingShell.command,
11301
+ allowPrefix: derivePrefix(pendingShell.command),
11302
+ kind: pendingShell.kind,
9985
11303
  onChoose: handleShellConfirm
9986
11304
  }
9987
- ) : /* @__PURE__ */ React14.createElement(React14.Fragment, null, /* @__PURE__ */ React14.createElement(
11305
+ ) : pendingEditReview ? /* @__PURE__ */ React15.createElement(
11306
+ EditConfirm,
11307
+ {
11308
+ block: pendingEditReview,
11309
+ onChoose: (choice) => {
11310
+ const resolve8 = editReviewResolveRef.current;
11311
+ if (resolve8) {
11312
+ editReviewResolveRef.current = null;
11313
+ resolve8(choice);
11314
+ }
11315
+ }
11316
+ }
11317
+ ) : /* @__PURE__ */ React15.createElement(React15.Fragment, null, codeMode ? /* @__PURE__ */ React15.createElement(
11318
+ ModeStatusBar,
11319
+ {
11320
+ editMode,
11321
+ pendingCount,
11322
+ flash: modeFlash,
11323
+ planMode,
11324
+ undoArmed: !!undoBanner || editHistory.current.some((e) => !isEntryFullyUndone(e)),
11325
+ jobs: codeMode.jobs
11326
+ }
11327
+ ) : null, /* @__PURE__ */ React15.createElement(
9988
11328
  PromptInput,
9989
11329
  {
9990
11330
  value: input,
@@ -9992,14 +11332,14 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
9992
11332
  onSubmit: handleSubmit,
9993
11333
  disabled: busy
9994
11334
  }
9995
- ), /* @__PURE__ */ React14.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }), /* @__PURE__ */ React14.createElement(
11335
+ ), /* @__PURE__ */ React15.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }), /* @__PURE__ */ React15.createElement(
9996
11336
  AtMentionSuggestions,
9997
11337
  {
9998
11338
  matches: atMatches,
9999
11339
  selectedIndex: atSelected,
10000
11340
  query: atPicker?.query ?? ""
10001
11341
  }
10002
- ), slashArgContext ? /* @__PURE__ */ React14.createElement(
11342
+ ), slashArgContext ? /* @__PURE__ */ React15.createElement(
10003
11343
  SlashArgPicker,
10004
11344
  {
10005
11345
  matches: slashArgMatches,
@@ -10014,14 +11354,44 @@ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834"
10014
11354
  function StatusRow({ text }) {
10015
11355
  const tick = useTick();
10016
11356
  const elapsed = useElapsedSeconds();
10017
- return /* @__PURE__ */ React14.createElement(Box13, { marginY: 1 }, /* @__PURE__ */ React14.createElement(Text13, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React14.createElement(Text13, { color: "magenta" }, ` ${text}`), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, ` ${elapsed}s`));
11357
+ return /* @__PURE__ */ React15.createElement(Box14, { marginY: 1 }, /* @__PURE__ */ React15.createElement(Text14, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React15.createElement(Text14, { color: "magenta" }, ` ${text}`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ` ${elapsed}s`));
11358
+ }
11359
+ function ModeStatusBar({
11360
+ editMode,
11361
+ pendingCount,
11362
+ flash,
11363
+ planMode,
11364
+ undoArmed,
11365
+ jobs
11366
+ }) {
11367
+ useTick();
11368
+ const running = jobs?.runningCount() ?? 0;
11369
+ const jobsTag = running > 0 ? /* @__PURE__ */ React15.createElement(Text14, { color: "yellow", bold: true }, ` \xB7 \u23F5 ${running} job${running === 1 ? "" : "s"}`) : null;
11370
+ if (planMode) {
11371
+ return /* @__PURE__ */ React15.createElement(Box14, { paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text14, { color: "red", bold: true, inverse: flash }, "\u25B8 PLAN"), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, " writes gated \u2014 submit_plan + approval required \xB7 /plan off to leave"), jobsTag);
11372
+ }
11373
+ const label = editMode === "auto" ? "AUTO" : "review";
11374
+ const color = editMode === "auto" ? "magenta" : "cyan";
11375
+ const mid = editMode === "auto" ? undoArmed ? "edits apply immediately \xB7 u = undo last batch" : "edits apply immediately \xB7 5s undo window after each batch" : pendingCount > 0 ? `${pendingCount} queued \xB7 y = /apply \xB7 n = /discard` : "edits queue for review \xB7 y = /apply \xB7 n = /discard";
11376
+ const flip = editMode === "auto" ? "Shift+Tab \u2192 review" : "Shift+Tab \u2192 AUTO";
11377
+ return /* @__PURE__ */ React15.createElement(Box14, { paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text14, { color, bold: true, inverse: flash }, `\u25B8 ${label}`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ` ${mid} \xB7 ${flip}`), jobsTag);
11378
+ }
11379
+ function UndoBanner({
11380
+ banner
11381
+ }) {
11382
+ useTick();
11383
+ const remainingMs = Math.max(0, banner.expiresAt - Date.now());
11384
+ const remainingSec = Math.ceil(remainingMs / 1e3);
11385
+ const ok = banner.results.filter((r) => r.status === "applied" || r.status === "created").length;
11386
+ const total = banner.results.length;
11387
+ return /* @__PURE__ */ React15.createElement(Box14, { marginY: 1, borderStyle: "round", borderColor: "magenta", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text14, { color: "magenta", bold: true }, "\u2713 auto-applied "), /* @__PURE__ */ React15.createElement(Text14, { color: "magenta" }, `${ok}/${total} edit${total === 1 ? "" : "s"}`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, " \xB7 press "), /* @__PURE__ */ React15.createElement(Text14, { color: "magenta", bold: true }, "u"), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, " to undo ("), /* @__PURE__ */ React15.createElement(Text14, { color: remainingSec <= 1 ? "red" : "magenta" }, `${remainingSec}s`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ")"));
10018
11388
  }
10019
11389
  function SubagentRow({
10020
11390
  activity
10021
11391
  }) {
10022
11392
  const tick = useTick();
10023
11393
  const seconds = (activity.elapsedMs / 1e3).toFixed(1);
10024
- return /* @__PURE__ */ React14.createElement(Box13, { paddingLeft: 2 }, /* @__PURE__ */ React14.createElement(Text13, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React14.createElement(Text13, { color: "magenta" }, ` \u232C subagent: ${activity.task}`), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, ` \xB7 iter ${activity.iter} \xB7 ${seconds}s`));
11394
+ return /* @__PURE__ */ React15.createElement(Box14, { paddingLeft: 2 }, /* @__PURE__ */ React15.createElement(Text14, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React15.createElement(Text14, { color: "magenta" }, ` \u232C subagent: ${activity.task}`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ` \xB7 iter ${activity.iter} \xB7 ${seconds}s`));
10025
11395
  }
10026
11396
  function OngoingToolRow({
10027
11397
  tool,
@@ -10030,7 +11400,7 @@ function OngoingToolRow({
10030
11400
  const tick = useTick();
10031
11401
  const elapsed = useElapsedSeconds();
10032
11402
  const summary = summarizeToolArgs(tool.name, tool.args);
10033
- return /* @__PURE__ */ React14.createElement(Box13, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React14.createElement(Box13, null, /* @__PURE__ */ React14.createElement(Text13, { color: "cyan" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React14.createElement(Text13, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, ` ${elapsed}s`)), progress ? /* @__PURE__ */ React14.createElement(Box13, { paddingLeft: 2 }, /* @__PURE__ */ React14.createElement(Text13, { color: "cyan" }, renderProgressLine(progress))) : null, summary ? /* @__PURE__ */ React14.createElement(Box13, { paddingLeft: 2 }, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, summary)) : null);
11403
+ return /* @__PURE__ */ React15.createElement(Box14, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React15.createElement(Box14, null, /* @__PURE__ */ React15.createElement(Text14, { color: "cyan" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React15.createElement(Text14, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ` ${elapsed}s`)), progress ? /* @__PURE__ */ React15.createElement(Box14, { paddingLeft: 2 }, /* @__PURE__ */ React15.createElement(Text14, { color: "cyan" }, renderProgressLine(progress))) : null, summary ? /* @__PURE__ */ React15.createElement(Box14, { paddingLeft: 2 }, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, summary)) : null);
10034
11404
  }
10035
11405
  function renderProgressLine(p) {
10036
11406
  const msg = p.message ? ` ${p.message}` : "";
@@ -10100,13 +11470,12 @@ function formatPendingPreview(blocks) {
10100
11470
  const diffLines = formatAllBlockDiffs(blocks);
10101
11471
  return [header2, ...diffLines].join("\n");
10102
11472
  }
10103
- function formatUndoResults(results) {
10104
- const lines = results.map((r) => {
11473
+ function formatUndoRows(results) {
11474
+ return results.map((r) => {
10105
11475
  const mark = r.status === "applied" ? "\u2713" : "\u2717";
10106
11476
  const detail = r.message ? ` (${r.message})` : "";
10107
11477
  return ` ${mark} ${r.path}${detail}`;
10108
11478
  });
10109
- return [`\u25B8 undo: restored ${results.length} file(s) to pre-edit state`, ...lines].join("\n");
10110
11479
  }
10111
11480
  function describeRepair(repair) {
10112
11481
  const parts = [];
@@ -10117,15 +11486,15 @@ function describeRepair(repair) {
10117
11486
  }
10118
11487
 
10119
11488
  // src/cli/ui/SessionPicker.tsx
10120
- import { Box as Box14, Text as Text14 } from "ink";
10121
- import React15 from "react";
11489
+ import { Box as Box15, Text as Text15 } from "ink";
11490
+ import React16 from "react";
10122
11491
  function SessionPicker({
10123
11492
  sessionName,
10124
11493
  messageCount,
10125
11494
  lastActive,
10126
11495
  onChoose
10127
11496
  }) {
10128
- return /* @__PURE__ */ React15.createElement(Box14, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React15.createElement(Box14, { marginBottom: 1 }, /* @__PURE__ */ React15.createElement(Text14, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ` \xB7 last active ${relativeTime(lastActive)}`)), /* @__PURE__ */ React15.createElement(
11497
+ return /* @__PURE__ */ React16.createElement(Box15, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React16.createElement(Box15, { marginBottom: 1 }, /* @__PURE__ */ React16.createElement(Text15, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React16.createElement(Text15, { dimColor: true }, ` \xB7 last active ${relativeTime(lastActive)}`)), /* @__PURE__ */ React16.createElement(
10129
11498
  SingleSelect,
10130
11499
  {
10131
11500
  initialValue: "new",
@@ -10148,7 +11517,7 @@ function SessionPicker({
10148
11517
  ],
10149
11518
  onSubmit: (v) => onChoose(v)
10150
11519
  }
10151
- ), /* @__PURE__ */ React15.createElement(Box14, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "\u2191\u2193 to move \xB7 Enter to pick")));
11520
+ ), /* @__PURE__ */ React16.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text15, { dimColor: true }, "\u2191\u2193 to move \xB7 Enter to pick")));
10152
11521
  }
10153
11522
  function relativeTime(date) {
10154
11523
  const ms = Date.now() - date.getTime();
@@ -10164,12 +11533,12 @@ function relativeTime(date) {
10164
11533
  }
10165
11534
 
10166
11535
  // src/cli/ui/Setup.tsx
10167
- import { Box as Box15, Text as Text15, useApp as useApp2 } from "ink";
11536
+ import { Box as Box16, Text as Text16, useApp as useApp2 } from "ink";
10168
11537
  import TextInput from "ink-text-input";
10169
- import React16, { useState as useState6 } from "react";
11538
+ import React17, { useState as useState7 } from "react";
10170
11539
  function Setup({ onReady }) {
10171
- const [value, setValue] = useState6("");
10172
- const [error, setError] = useState6(null);
11540
+ const [value, setValue] = useState7("");
11541
+ const [error, setError] = useState7(null);
10173
11542
  const { exit } = useApp2();
10174
11543
  const handleSubmit = (raw) => {
10175
11544
  const trimmed = raw.trim();
@@ -10190,7 +11559,7 @@ function Setup({ onReady }) {
10190
11559
  }
10191
11560
  onReady(trimmed);
10192
11561
  };
10193
- return /* @__PURE__ */ React16.createElement(Box15, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React16.createElement(Text15, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React16.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text15, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React16.createElement(Text15, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React16.createElement(Text15, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React16.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text15, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React16.createElement(
11562
+ return /* @__PURE__ */ React17.createElement(Box16, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React17.createElement(Text16, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React17.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text16, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React17.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text16, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React17.createElement(
10194
11563
  TextInput,
10195
11564
  {
10196
11565
  value,
@@ -10199,7 +11568,7 @@ function Setup({ onReady }) {
10199
11568
  mask: "\u2022",
10200
11569
  placeholder: "sk-..."
10201
11570
  }
10202
- )), error ? /* @__PURE__ */ React16.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text15, { color: "red" }, error)) : value ? /* @__PURE__ */ React16.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text15, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React16.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text15, { dimColor: true }, "(Type /exit to abort.)")));
11571
+ )), error ? /* @__PURE__ */ React17.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text16, { color: "red" }, error)) : value ? /* @__PURE__ */ React17.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React17.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, "(Type /exit to abort.)")));
10203
11572
  }
10204
11573
 
10205
11574
  // src/cli/commands/chat.tsx
@@ -10212,10 +11581,10 @@ function Root({
10212
11581
  sessionPreview,
10213
11582
  ...appProps
10214
11583
  }) {
10215
- const [key, setKey] = useState7(initialKey);
10216
- const [pending, setPending] = useState7(sessionPreview);
11584
+ const [key, setKey] = useState8(initialKey);
11585
+ const [pending, setPending] = useState8(sessionPreview);
10217
11586
  if (!key) {
10218
- return /* @__PURE__ */ React17.createElement(
11587
+ return /* @__PURE__ */ React18.createElement(
10219
11588
  Setup,
10220
11589
  {
10221
11590
  onReady: (k) => {
@@ -10227,7 +11596,7 @@ function Root({
10227
11596
  }
10228
11597
  process.env.DEEPSEEK_API_KEY = key;
10229
11598
  if (pending && appProps.session) {
10230
- return /* @__PURE__ */ React17.createElement(
11599
+ return /* @__PURE__ */ React18.createElement(
10231
11600
  SessionPicker,
10232
11601
  {
10233
11602
  sessionName: appProps.session,
@@ -10242,7 +11611,7 @@ function Root({
10242
11611
  }
10243
11612
  );
10244
11613
  }
10245
- return /* @__PURE__ */ React17.createElement(
11614
+ return /* @__PURE__ */ React18.createElement(
10246
11615
  App,
10247
11616
  {
10248
11617
  model: appProps.model,
@@ -10346,7 +11715,7 @@ async function chatCommand(opts) {
10346
11715
  rewriteSession(opts.session, []);
10347
11716
  }
10348
11717
  const { waitUntilExit } = render(
10349
- /* @__PURE__ */ React17.createElement(
11718
+ /* @__PURE__ */ React18.createElement(
10350
11719
  Root,
10351
11720
  {
10352
11721
  initialKey,
@@ -10370,13 +11739,14 @@ async function chatCommand(opts) {
10370
11739
  }
10371
11740
 
10372
11741
  // src/cli/commands/code.tsx
10373
- import { basename, resolve as resolve6 } from "path";
11742
+ import { basename, resolve as resolve7 } from "path";
10374
11743
  async function codeCommand(opts = {}) {
10375
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-75XLIUTO.js");
10376
- const rootDir = resolve6(opts.dir ?? process.cwd());
11744
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-OVVMCH5F.js");
11745
+ const rootDir = resolve7(opts.dir ?? process.cwd());
10377
11746
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
10378
11747
  const tools = new ToolRegistry();
10379
11748
  registerFilesystemTools(tools, { rootDir });
11749
+ const jobs = new JobRegistry();
10380
11750
  registerShellTools(tools, {
10381
11751
  rootDir,
10382
11752
  // Per-project "always allow" list persisted from prior ShellConfirm
@@ -10384,7 +11754,8 @@ async function codeCommand(opts = {}) {
10384
11754
  // GETTER form — re-read every dispatch so a prefix the user adds
10385
11755
  // via ShellConfirm mid-session takes effect on the next shell call
10386
11756
  // instead of waiting for `/new` or a relaunch.
10387
- extraAllowed: () => loadProjectShellAllowed(rootDir)
11757
+ extraAllowed: () => loadProjectShellAllowed(rootDir),
11758
+ jobs
10388
11759
  });
10389
11760
  registerPlanTool(tools);
10390
11761
  registerMemoryTools(tools, { projectRoot: rootDir });
@@ -10392,6 +11763,12 @@ async function codeCommand(opts = {}) {
10392
11763
  `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
10393
11764
  `
10394
11765
  );
11766
+ const sigShutdown = () => {
11767
+ void jobs.shutdown();
11768
+ };
11769
+ process.once("SIGINT", sigShutdown);
11770
+ process.once("SIGTERM", sigShutdown);
11771
+ process.once("exit", sigShutdown);
10395
11772
  await chatCommand({
10396
11773
  model: opts.model ?? "deepseek-v4-pro",
10397
11774
  harvest: opts.harvest ?? false,
@@ -10399,7 +11776,7 @@ async function codeCommand(opts = {}) {
10399
11776
  transcript: opts.transcript,
10400
11777
  session,
10401
11778
  seedTools: tools,
10402
- codeMode: { rootDir },
11779
+ codeMode: { rootDir, jobs },
10403
11780
  forceResume: opts.forceResume,
10404
11781
  forceNew: opts.forceNew
10405
11782
  });
@@ -10409,34 +11786,34 @@ async function codeCommand(opts = {}) {
10409
11786
  import { writeFileSync as writeFileSync6 } from "fs";
10410
11787
  import { basename as basename2 } from "path";
10411
11788
  import { render as render2 } from "ink";
10412
- import React20 from "react";
11789
+ import React21 from "react";
10413
11790
 
10414
11791
  // src/cli/ui/DiffApp.tsx
10415
- import { Box as Box17, Static as Static2, Text as Text17, useApp as useApp3, useInput as useInput5 } from "ink";
10416
- import React19, { useState as useState8 } from "react";
11792
+ import { Box as Box18, Static as Static2, Text as Text18, useApp as useApp3, useInput as useInput6 } from "ink";
11793
+ import React20, { useState as useState9 } from "react";
10417
11794
 
10418
11795
  // src/cli/ui/RecordView.tsx
10419
- import { Box as Box16, Text as Text16 } from "ink";
10420
- import React18 from "react";
11796
+ import { Box as Box17, Text as Text17 } from "ink";
11797
+ import React19 from "react";
10421
11798
  function RecordView({ rec, compact = false }) {
10422
11799
  const toolArgsMax = compact ? 120 : 200;
10423
11800
  const toolContentMax = compact ? 200 : 400;
10424
11801
  if (rec.role === "user") {
10425
- return /* @__PURE__ */ React18.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React18.createElement(Text16, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React18.createElement(Text16, null, rec.content));
11802
+ return /* @__PURE__ */ React19.createElement(Box17, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text17, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React19.createElement(Text17, null, rec.content));
10426
11803
  }
10427
11804
  if (rec.role === "assistant_final") {
10428
- return /* @__PURE__ */ React18.createElement(Box16, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React18.createElement(Box16, null, /* @__PURE__ */ React18.createElement(Text16, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React18.createElement(Text16, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React18.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React18.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React18.createElement(Text16, null, rec.content) : /* @__PURE__ */ React18.createElement(Text16, { dimColor: true, italic: true }, "(tool-call response only)"));
11805
+ return /* @__PURE__ */ React19.createElement(Box17, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React19.createElement(Box17, null, /* @__PURE__ */ React19.createElement(Text17, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React19.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React19.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React19.createElement(Text17, null, rec.content) : /* @__PURE__ */ React19.createElement(Text17, { dimColor: true, italic: true }, "(tool-call response only)"));
10429
11806
  }
10430
11807
  if (rec.role === "tool") {
10431
- return /* @__PURE__ */ React18.createElement(Box16, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React18.createElement(Text16, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React18.createElement(Text16, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React18.createElement(Text16, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
11808
+ return /* @__PURE__ */ React19.createElement(Box17, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text17, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
10432
11809
  }
10433
11810
  if (rec.role === "error") {
10434
- return /* @__PURE__ */ React18.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React18.createElement(Text16, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React18.createElement(Text16, { color: "red" }, rec.error ?? rec.content));
11811
+ return /* @__PURE__ */ React19.createElement(Box17, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text17, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React19.createElement(Text17, { color: "red" }, rec.error ?? rec.content));
10435
11812
  }
10436
11813
  if (rec.role === "done" || rec.role === "assistant_delta") {
10437
11814
  return null;
10438
11815
  }
10439
- return /* @__PURE__ */ React18.createElement(Box16, null, /* @__PURE__ */ React18.createElement(Text16, { dimColor: true }, "[", rec.role, "] ", rec.content));
11816
+ return /* @__PURE__ */ React19.createElement(Box17, null, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, "[", rec.role, "] ", rec.content));
10440
11817
  }
10441
11818
  function CacheBadge({ usage }) {
10442
11819
  const hit = usage.prompt_cache_hit_tokens ?? 0;
@@ -10445,7 +11822,7 @@ function CacheBadge({ usage }) {
10445
11822
  if (total === 0) return null;
10446
11823
  const pct2 = hit / total * 100;
10447
11824
  const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
10448
- return /* @__PURE__ */ React18.createElement(Text16, null, /* @__PURE__ */ React18.createElement(Text16, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React18.createElement(Text16, { color }, pct2.toFixed(1), "%"));
11825
+ return /* @__PURE__ */ React19.createElement(Text17, null, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React19.createElement(Text17, { color }, pct2.toFixed(1), "%"));
10449
11826
  }
10450
11827
  function truncate3(s, max) {
10451
11828
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
@@ -10456,8 +11833,8 @@ function DiffApp({ report }) {
10456
11833
  const { exit } = useApp3();
10457
11834
  const maxIdx = Math.max(0, report.pairs.length - 1);
10458
11835
  const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
10459
- const [idx, setIdx] = useState8(Math.max(0, initialIdx));
10460
- useInput5((input, key) => {
11836
+ const [idx, setIdx] = useState9(Math.max(0, initialIdx));
11837
+ useInput6((input, key) => {
10461
11838
  if (input === "q" || key.ctrl && input === "c") {
10462
11839
  exit();
10463
11840
  return;
@@ -10479,7 +11856,7 @@ function DiffApp({ report }) {
10479
11856
  }
10480
11857
  });
10481
11858
  const pair = report.pairs[idx];
10482
- return /* @__PURE__ */ React19.createElement(Box17, { flexDirection: "column" }, /* @__PURE__ */ React19.createElement(DiffHeader, { report }), /* @__PURE__ */ React19.createElement(Box17, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React19.createElement(Text17, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React19.createElement(Text17, null, pair ? /* @__PURE__ */ React19.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React19.createElement(Box17, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React19.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React19.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React19.createElement(Box17, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React19.createElement(Text17, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React19.createElement(Text17, null, pair.divergenceNote)) : null, /* @__PURE__ */ React19.createElement(Box17, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, /* @__PURE__ */ React19.createElement(Text17, { bold: true }, "j"), "/", /* @__PURE__ */ React19.createElement(Text17, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React19.createElement(Text17, { bold: true }, "k"), "/", /* @__PURE__ */ React19.createElement(Text17, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React19.createElement(Text17, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React19.createElement(Text17, { bold: true }, "N"), "/", /* @__PURE__ */ React19.createElement(Text17, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React19.createElement(Text17, { bold: true }, "g"), "/", /* @__PURE__ */ React19.createElement(Text17, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React19.createElement(Text17, { bold: true }, "q"), " ", "quit")));
11859
+ return /* @__PURE__ */ React20.createElement(Box18, { flexDirection: "column" }, /* @__PURE__ */ React20.createElement(DiffHeader, { report }), /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React20.createElement(Text18, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React20.createElement(Text18, null, pair ? /* @__PURE__ */ React20.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React20.createElement(Box18, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React20.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React20.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React20.createElement(Text18, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React20.createElement(Text18, null, pair.divergenceNote)) : null, /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "j"), "/", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "k"), "/", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "N"), "/", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "g"), "/", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React20.createElement(Text18, { bold: true }, "q"), " ", "quit")));
10483
11860
  }
10484
11861
  function DiffHeader({ report }) {
10485
11862
  const a = report.a;
@@ -10497,15 +11874,15 @@ function DiffHeader({ report }) {
10497
11874
  } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
10498
11875
  prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
10499
11876
  }
10500
- return /* @__PURE__ */ React19.createElement(Box17, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React19.createElement(Box17, { justifyContent: "space-between" }, /* @__PURE__ */ React19.createElement(Text17, null, /* @__PURE__ */ React19.createElement(Text17, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React19.createElement(Text17, { color: "blue" }, a.label), /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " vs B="), /* @__PURE__ */ React19.createElement(Text17, { color: "magenta" }, b.label)), /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React19.createElement(Box17, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React19.createElement(Text17, null, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, "cache "), /* @__PURE__ */ React19.createElement(Text17, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React19.createElement(Text17, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React19.createElement(Text17, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React19.createElement(Text17, null, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, "cost "), /* @__PURE__ */ React19.createElement(Text17, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React19.createElement(Text17, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React19.createElement(Text17, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React19.createElement(Text17, null, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true }, "model calls "), /* @__PURE__ */ React19.createElement(Text17, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React19.createElement(Box17, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true, italic: true }, prefixLine)) : null);
11877
+ return /* @__PURE__ */ React20.createElement(Box18, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React20.createElement(Box18, { justifyContent: "space-between" }, /* @__PURE__ */ React20.createElement(Text18, null, /* @__PURE__ */ React20.createElement(Text18, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React20.createElement(Text18, { color: "blue" }, a.label), /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, " vs B="), /* @__PURE__ */ React20.createElement(Text18, { color: "magenta" }, b.label)), /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React20.createElement(Text18, null, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, "cache "), /* @__PURE__ */ React20.createElement(Text18, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React20.createElement(Text18, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React20.createElement(Text18, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React20.createElement(Text18, null, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, "cost "), /* @__PURE__ */ React20.createElement(Text18, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React20.createElement(Text18, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React20.createElement(Text18, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React20.createElement(Text18, null, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true }, "model calls "), /* @__PURE__ */ React20.createElement(Text18, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1 }, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true, italic: true }, prefixLine)) : null);
10501
11878
  }
10502
11879
  function Pane({
10503
11880
  label,
10504
11881
  headerColor,
10505
11882
  records
10506
11883
  }) {
10507
- return /* @__PURE__ */ React19.createElement(
10508
- Box17,
11884
+ return /* @__PURE__ */ React20.createElement(
11885
+ Box18,
10509
11886
  {
10510
11887
  flexDirection: "column",
10511
11888
  flexGrow: 1,
@@ -10513,21 +11890,21 @@ function Pane({
10513
11890
  borderStyle: "single",
10514
11891
  borderColor: headerColor
10515
11892
  },
10516
- /* @__PURE__ */ React19.createElement(Text17, { color: headerColor, bold: true }, label),
10517
- records.length === 0 ? /* @__PURE__ */ React19.createElement(Box17, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text17, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React19.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React19.createElement(RecordView, { key, rec, compact: true }))
11893
+ /* @__PURE__ */ React20.createElement(Text18, { color: headerColor, bold: true }, label),
11894
+ records.length === 0 ? /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1 }, /* @__PURE__ */ React20.createElement(Text18, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React20.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React20.createElement(RecordView, { key, rec, compact: true }))
10518
11895
  );
10519
11896
  }
10520
11897
  function KindBadge({ kind }) {
10521
11898
  if (kind === "match") {
10522
- return /* @__PURE__ */ React19.createElement(Text17, { color: "green" }, "\u2713 match");
11899
+ return /* @__PURE__ */ React20.createElement(Text18, { color: "green" }, "\u2713 match");
10523
11900
  }
10524
11901
  if (kind === "diverge") {
10525
- return /* @__PURE__ */ React19.createElement(Text17, { color: "yellow" }, "\u2605 diverge");
11902
+ return /* @__PURE__ */ React20.createElement(Text18, { color: "yellow" }, "\u2605 diverge");
10526
11903
  }
10527
11904
  if (kind === "only_in_a") {
10528
- return /* @__PURE__ */ React19.createElement(Text17, { color: "blue" }, "\u2190 only in A");
11905
+ return /* @__PURE__ */ React20.createElement(Text18, { color: "blue" }, "\u2190 only in A");
10529
11906
  }
10530
- return /* @__PURE__ */ React19.createElement(Text17, { color: "magenta" }, "\u2192 only in B");
11907
+ return /* @__PURE__ */ React20.createElement(Text18, { color: "magenta" }, "\u2192 only in B");
10531
11908
  }
10532
11909
  function paneRecords(pair, side) {
10533
11910
  if (!pair) return [];
@@ -10558,7 +11935,7 @@ markdown report written to ${opts.mdPath}`);
10558
11935
  return;
10559
11936
  }
10560
11937
  if (wantTui) {
10561
- const { waitUntilExit } = render2(React20.createElement(DiffApp, { report }), {
11938
+ const { waitUntilExit } = render2(React21.createElement(DiffApp, { report }), {
10562
11939
  exitOnCtrlC: true,
10563
11940
  patchConsole: false
10564
11941
  });
@@ -10699,16 +12076,16 @@ function pad2(s, width) {
10699
12076
 
10700
12077
  // src/cli/commands/replay.ts
10701
12078
  import { render as render3 } from "ink";
10702
- import React22 from "react";
12079
+ import React23 from "react";
10703
12080
 
10704
12081
  // src/cli/ui/ReplayApp.tsx
10705
- import { Box as Box18, Static as Static3, Text as Text18, useApp as useApp4, useInput as useInput6 } from "ink";
10706
- import React21, { useMemo as useMemo2, useState as useState9 } from "react";
12082
+ import { Box as Box19, Static as Static3, Text as Text19, useApp as useApp4, useInput as useInput7 } from "ink";
12083
+ import React22, { useMemo as useMemo3, useState as useState10 } from "react";
10707
12084
  function ReplayApp({ meta, pages }) {
10708
12085
  const { exit } = useApp4();
10709
12086
  const maxIdx = Math.max(0, pages.length - 1);
10710
- const [idx, setIdx] = useState9(maxIdx);
10711
- useInput6((input, key) => {
12087
+ const [idx, setIdx] = useState10(maxIdx);
12088
+ useInput7((input, key) => {
10712
12089
  if (input === "q" || key.ctrl && input === "c") {
10713
12090
  exit();
10714
12091
  return;
@@ -10727,7 +12104,7 @@ function ReplayApp({ meta, pages }) {
10727
12104
  setIdx(maxIdx);
10728
12105
  }
10729
12106
  });
10730
- const cumStats = useMemo2(() => computeCumulativeStats(pages, idx), [pages, idx]);
12107
+ const cumStats = useMemo3(() => computeCumulativeStats(pages, idx), [pages, idx]);
10731
12108
  const summary = {
10732
12109
  turns: cumStats.turns,
10733
12110
  totalCostUsd: cumStats.totalCostUsd,
@@ -10742,14 +12119,14 @@ function ReplayApp({ meta, pages }) {
10742
12119
  const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
10743
12120
  const currentPage = pages[idx];
10744
12121
  const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
10745
- return /* @__PURE__ */ React21.createElement(Box18, { flexDirection: "column" }, /* @__PURE__ */ React21.createElement(
12122
+ return /* @__PURE__ */ React22.createElement(Box19, { flexDirection: "column" }, /* @__PURE__ */ React22.createElement(
10746
12123
  StatsPanel,
10747
12124
  {
10748
12125
  summary,
10749
12126
  model: cumStats.models[0] ?? meta?.model ?? "?",
10750
12127
  prefixHash
10751
12128
  }
10752
- ), /* @__PURE__ */ React21.createElement(Box18, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React21.createElement(Box18, { justifyContent: "space-between" }, /* @__PURE__ */ React21.createElement(Text18, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React21.createElement(Text18, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React21.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React21.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React21.createElement(Text18, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React21.createElement(Box18, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React21.createElement(Text18, { dimColor: true }, /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "j"), "/", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "k"), "/", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "q"), " quit")));
12129
+ ), /* @__PURE__ */ React22.createElement(Box19, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React22.createElement(Box19, { justifyContent: "space-between" }, /* @__PURE__ */ React22.createElement(Text19, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React22.createElement(Text19, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React22.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React22.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React22.createElement(Text19, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React22.createElement(Box19, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React22.createElement(Text19, { dimColor: true }, /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "j"), "/", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "k"), "/", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React22.createElement(Text19, { bold: true }, "q"), " quit")));
10753
12130
  }
10754
12131
 
10755
12132
  // src/cli/commands/replay.ts
@@ -10761,7 +12138,7 @@ async function replayCommand(opts) {
10761
12138
  }
10762
12139
  const { parsed } = replayFromFile(opts.path);
10763
12140
  const pages = groupRecordsByTurn(parsed.records);
10764
- const { waitUntilExit } = render3(React22.createElement(ReplayApp, { meta: parsed.meta, pages }), {
12141
+ const { waitUntilExit } = render3(React23.createElement(ReplayApp, { meta: parsed.meta, pages }), {
10765
12142
  exitOnCtrlC: true,
10766
12143
  patchConsole: false
10767
12144
  });
@@ -11066,12 +12443,12 @@ function truncate4(s, max) {
11066
12443
 
11067
12444
  // src/cli/commands/setup.tsx
11068
12445
  import { render as render4 } from "ink";
11069
- import React24 from "react";
12446
+ import React25 from "react";
11070
12447
 
11071
12448
  // src/cli/ui/Wizard.tsx
11072
- import { Box as Box19, Text as Text19, useApp as useApp5, useInput as useInput7 } from "ink";
12449
+ import { Box as Box20, Text as Text20, useApp as useApp5, useInput as useInput8 } from "ink";
11073
12450
  import TextInput2 from "ink-text-input";
11074
- import React23, { useState as useState10 } from "react";
12451
+ import React24, { useState as useState11 } from "react";
11075
12452
 
11076
12453
  // src/cli/ui/presets.ts
11077
12454
  var PRESETS = {
@@ -11103,19 +12480,19 @@ var PRESET_DESCRIPTIONS = {
11103
12480
  var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
11104
12481
  function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11105
12482
  const { exit } = useApp5();
11106
- const [step, setStep] = useState10(existingApiKey ? "preset" : "apiKey");
11107
- const [data, setData] = useState10({
12483
+ const [step, setStep] = useState11(existingApiKey ? "preset" : "apiKey");
12484
+ const [data, setData] = useState11({
11108
12485
  apiKey: existingApiKey ?? "",
11109
12486
  preset: initial?.preset ?? "fast",
11110
12487
  selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
11111
12488
  catalogArgs: {}
11112
12489
  });
11113
- const [error, setError] = useState10(null);
11114
- useInput7((_input, key) => {
12490
+ const [error, setError] = useState11(null);
12491
+ useInput8((_input, key) => {
11115
12492
  if (key.escape && step !== "saved" && onCancel) onCancel();
11116
12493
  });
11117
12494
  if (step === "apiKey") {
11118
- return /* @__PURE__ */ React23.createElement(
12495
+ return /* @__PURE__ */ React24.createElement(
11119
12496
  ApiKeyStep,
11120
12497
  {
11121
12498
  onSubmit: (key) => {
@@ -11129,7 +12506,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11129
12506
  );
11130
12507
  }
11131
12508
  if (step === "preset") {
11132
- return /* @__PURE__ */ React23.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React23.createElement(
12509
+ return /* @__PURE__ */ React24.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React24.createElement(
11133
12510
  SingleSelect,
11134
12511
  {
11135
12512
  items: presetItems(),
@@ -11139,10 +12516,10 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11139
12516
  setStep("mcp");
11140
12517
  }
11141
12518
  }
11142
- ), /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
12519
+ ), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
11143
12520
  }
11144
12521
  if (step === "mcp") {
11145
- return /* @__PURE__ */ React23.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React23.createElement(
12522
+ return /* @__PURE__ */ React24.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React24.createElement(
11146
12523
  MultiSelect,
11147
12524
  {
11148
12525
  items: mcpItems(),
@@ -11167,7 +12544,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11167
12544
  }
11168
12545
  const currentName = pending[0];
11169
12546
  const entry = CATALOG_BY_NAME.get(currentName);
11170
- return /* @__PURE__ */ React23.createElement(
12547
+ return /* @__PURE__ */ React24.createElement(
11171
12548
  McpArgsStep,
11172
12549
  {
11173
12550
  entry,
@@ -11185,7 +12562,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11185
12562
  }
11186
12563
  if (step === "review") {
11187
12564
  const specs = data.selectedCatalog.map((name) => buildSpec(name, data.catalogArgs));
11188
- return /* @__PURE__ */ React23.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React23.createElement(Box19, { flexDirection: "column" }, /* @__PURE__ */ React23.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React23.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React23.createElement(
12565
+ return /* @__PURE__ */ React24.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React24.createElement(Box20, { flexDirection: "column" }, /* @__PURE__ */ React24.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React24.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React24.createElement(
11189
12566
  SummaryLine,
11190
12567
  {
11191
12568
  label: "MCP",
@@ -11193,8 +12570,8 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11193
12570
  }
11194
12571
  ), specs.map((spec, i) => (
11195
12572
  // biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
11196
- /* @__PURE__ */ React23.createElement(Box19, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, "\xB7 ", spec))
11197
- )), /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, { color: "red" }, error)) : null, /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, "enter save \xB7 esc cancel"))), /* @__PURE__ */ React23.createElement(
12573
+ /* @__PURE__ */ React24.createElement(Box20, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "\xB7 ", spec))
12574
+ )), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { color: "red" }, error)) : null, /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "enter save \xB7 esc cancel"))), /* @__PURE__ */ React24.createElement(
11198
12575
  ReviewConfirm,
11199
12576
  {
11200
12577
  onConfirm: () => {
@@ -11220,15 +12597,15 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11220
12597
  }
11221
12598
  ));
11222
12599
  }
11223
- return /* @__PURE__ */ React23.createElement(Box19, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React23.createElement(Text19, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, "Press enter to exit.")), /* @__PURE__ */ React23.createElement(ExitOnEnter, { onExit: exit }));
12600
+ return /* @__PURE__ */ React24.createElement(Box20, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "Press enter to exit.")), /* @__PURE__ */ React24.createElement(ExitOnEnter, { onExit: exit }));
11224
12601
  }
11225
12602
  function ApiKeyStep({
11226
12603
  onSubmit,
11227
12604
  error,
11228
12605
  onError
11229
12606
  }) {
11230
- const [value, setValue] = useState10("");
11231
- return /* @__PURE__ */ React23.createElement(Box19, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React23.createElement(Text19, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React23.createElement(
12607
+ const [value, setValue] = useState11("");
12608
+ return /* @__PURE__ */ React24.createElement(Box20, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React24.createElement(
11232
12609
  TextInput2,
11233
12610
  {
11234
12611
  value,
@@ -11245,7 +12622,7 @@ function ApiKeyStep({
11245
12622
  mask: "\u2022",
11246
12623
  placeholder: "sk-..."
11247
12624
  }
11248
- )), error ? /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, { color: "red" }, error)) : value ? /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, "preview: ", redactKey(value))) : null);
12625
+ )), error ? /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { color: "red" }, error)) : value ? /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "preview: ", redactKey(value))) : null);
11249
12626
  }
11250
12627
  function McpArgsStep({
11251
12628
  entry,
@@ -11253,8 +12630,8 @@ function McpArgsStep({
11253
12630
  onSubmit,
11254
12631
  onError
11255
12632
  }) {
11256
- const [value, setValue] = useState10("");
11257
- return /* @__PURE__ */ React23.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React23.createElement(Box19, { flexDirection: "column" }, /* @__PURE__ */ React23.createElement(Text19, null, entry.summary), entry.note ? /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, null, "Required parameter: "), /* @__PURE__ */ React23.createElement(Text19, { bold: true }, entry.userArgs)), /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React23.createElement(
12633
+ const [value, setValue] = useState11("");
12634
+ return /* @__PURE__ */ React24.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React24.createElement(Box20, { flexDirection: "column" }, /* @__PURE__ */ React24.createElement(Text20, null, entry.summary), entry.note ? /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, null, "Required parameter: "), /* @__PURE__ */ React24.createElement(Text20, { bold: true }, entry.userArgs)), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React24.createElement(
11258
12635
  TextInput2,
11259
12636
  {
11260
12637
  value,
@@ -11270,16 +12647,16 @@ function McpArgsStep({
11270
12647
  },
11271
12648
  placeholder: placeholderFor(entry)
11272
12649
  }
11273
- )), error ? /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React23.createElement(Text19, { color: "red" }, error)) : null));
12650
+ )), error ? /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { color: "red" }, error)) : null));
11274
12651
  }
11275
12652
  function ReviewConfirm({ onConfirm }) {
11276
- useInput7((_i, key) => {
12653
+ useInput8((_i, key) => {
11277
12654
  if (key.return) onConfirm();
11278
12655
  });
11279
12656
  return null;
11280
12657
  }
11281
12658
  function ExitOnEnter({ onExit }) {
11282
- useInput7((_i, key) => {
12659
+ useInput8((_i, key) => {
11283
12660
  if (key.return) onExit();
11284
12661
  });
11285
12662
  return null;
@@ -11290,10 +12667,10 @@ function StepFrame({
11290
12667
  total,
11291
12668
  children
11292
12669
  }) {
11293
- return /* @__PURE__ */ React23.createElement(Box19, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React23.createElement(Box19, null, /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React23.createElement(Text19, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React23.createElement(Box19, { marginTop: 1, flexDirection: "column" }, children));
12670
+ return /* @__PURE__ */ React24.createElement(Box20, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React24.createElement(Box20, null, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React24.createElement(Box20, { marginTop: 1, flexDirection: "column" }, children));
11294
12671
  }
11295
12672
  function SummaryLine({ label, value }) {
11296
- return /* @__PURE__ */ React23.createElement(Box19, null, /* @__PURE__ */ React23.createElement(Text19, null, label.padEnd(12)), /* @__PURE__ */ React23.createElement(Text19, { bold: true }, value));
12673
+ return /* @__PURE__ */ React24.createElement(Box20, null, /* @__PURE__ */ React24.createElement(Text20, null, label.padEnd(12)), /* @__PURE__ */ React24.createElement(Text20, { bold: true }, value));
11297
12674
  }
11298
12675
  function presetItems() {
11299
12676
  return ["fast", "smart", "max"].map((name) => ({
@@ -11349,7 +12726,7 @@ async function setupCommand(_opts = {}) {
11349
12726
  const existingKey = loadApiKey();
11350
12727
  const existing = readConfig();
11351
12728
  const { waitUntilExit, unmount } = render4(
11352
- /* @__PURE__ */ React24.createElement(
12729
+ /* @__PURE__ */ React25.createElement(
11353
12730
  Wizard,
11354
12731
  {
11355
12732
  existingApiKey: existingKey,
@@ -11367,7 +12744,7 @@ async function setupCommand(_opts = {}) {
11367
12744
  }
11368
12745
 
11369
12746
  // src/cli/commands/update.ts
11370
- import { spawn as spawn4 } from "child_process";
12747
+ import { spawn as spawn5 } from "child_process";
11371
12748
  function planUpdate(input) {
11372
12749
  const diff = compareVersions(input.current, input.latest);
11373
12750
  if (diff > 0) {
@@ -11397,13 +12774,13 @@ function planUpdate(input) {
11397
12774
  };
11398
12775
  }
11399
12776
  function defaultSpawn(argv) {
11400
- return new Promise((resolve7, reject) => {
11401
- const child = spawn4(argv[0], argv.slice(1), {
12777
+ return new Promise((resolve8, reject) => {
12778
+ const child = spawn5(argv[0], argv.slice(1), {
11402
12779
  stdio: "inherit",
11403
12780
  shell: process.platform === "win32"
11404
12781
  });
11405
12782
  child.once("error", reject);
11406
- child.once("exit", (code) => resolve7(code ?? 1));
12783
+ child.once("exit", (code) => resolve8(code ?? 1));
11407
12784
  });
11408
12785
  }
11409
12786
  async function updateCommand(opts = {}) {