reasonix 0.5.22 → 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;
@@ -2645,10 +2798,41 @@ function formatLoopError(err) {
2645
2798
  if (msg.includes("maximum context length")) {
2646
2799
  const reqMatch = msg.match(/requested\s+(\d+)\s+tokens/);
2647
2800
  const requested = reqMatch ? `${Number(reqMatch[1]).toLocaleString()} tokens` : "too many tokens";
2648
- return `Context overflow (DeepSeek 400): session history is ${requested}, past the 131,072-token limit. Usually this means a single tool call returned a huge payload. v0.3.0-alpha.6+ caps new tool results at 32k chars, AND auto-heals oversized history on session load \u2014 restart Reasonix and this session should come back trimmed. If it still overflows, run /forget (delete the session) or /clear (drop the displayed history) to start fresh.`;
2801
+ return `Context overflow (DeepSeek 400): session history is ${requested}, past the model's prompt limit (V4: 1M tokens; legacy chat/reasoner: 131k). Usually a single tool result grew too big. Reasonix caps new tool results at 8k tokens and auto-heals oversized history on session load \u2014 a restart often clears it. If it still overflows, run /forget (delete the session) or /clear (drop the displayed history) to start fresh.`;
2802
+ }
2803
+ const m = /^DeepSeek (\d{3}):\s*([\s\S]*)$/.exec(msg);
2804
+ if (!m) return msg;
2805
+ const status = m[1] ?? "";
2806
+ const body = m[2] ?? "";
2807
+ const inner = extractDeepSeekErrorMessage(body);
2808
+ if (status === "401") {
2809
+ return `Authentication failed (DeepSeek 401): ${inner}. Your API key is rejected. Fix with \`reasonix setup\` or \`export DEEPSEEK_API_KEY=sk-...\`. Get one at https://platform.deepseek.com/api_keys.`;
2810
+ }
2811
+ if (status === "402") {
2812
+ return `Out of balance (DeepSeek 402): ${inner}. Top up at https://platform.deepseek.com/top_up \u2014 the panel header shows your balance once it's non-zero.`;
2813
+ }
2814
+ if (status === "422") {
2815
+ return `Invalid parameter (DeepSeek 422): ${inner}`;
2816
+ }
2817
+ if (status === "400") {
2818
+ return `Bad request (DeepSeek 400): ${inner}`;
2649
2819
  }
2650
2820
  return msg;
2651
2821
  }
2822
+ function extractDeepSeekErrorMessage(body) {
2823
+ const trimmed = body.trim();
2824
+ if (!trimmed) return "(no message)";
2825
+ try {
2826
+ const parsed = JSON.parse(trimmed);
2827
+ if (parsed && typeof parsed === "object") {
2828
+ const obj = parsed;
2829
+ if (obj.error && typeof obj.error.message === "string") return obj.error.message;
2830
+ if (typeof obj.message === "string") return obj.message;
2831
+ }
2832
+ } catch {
2833
+ }
2834
+ return trimmed;
2835
+ }
2652
2836
 
2653
2837
  // src/at-mentions.ts
2654
2838
  import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
@@ -2845,6 +3029,9 @@ import { promises as fs } from "fs";
2845
3029
  import * as pathMod from "path";
2846
3030
  var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
2847
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;
2848
3035
  var SKIP_DIR_NAMES = /* @__PURE__ */ new Set([
2849
3036
  "node_modules",
2850
3037
  ".git",
@@ -2937,14 +3124,22 @@ function registerFilesystemTools(registry, opts) {
2937
3124
  };
2938
3125
  registry.register({
2939
3126
  name: "read_file",
2940
- 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.`,
2941
3132
  readOnly: true,
2942
3133
  parameters: {
2943
3134
  type: "object",
2944
3135
  properties: {
2945
3136
  path: { type: "string", description: "Path to read (relative to rootDir or absolute)." },
2946
3137
  head: { type: "integer", description: "If set, return only the first N lines." },
2947
- 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
+ }
2948
3143
  },
2949
3144
  required: ["path"]
2950
3145
  },
@@ -2956,21 +3151,52 @@ function registerFilesystemTools(registry, opts) {
2956
3151
  }
2957
3152
  const raw = await fs.readFile(abs);
2958
3153
  if (raw.length > maxReadBytes) {
2959
- const head = raw.slice(0, maxReadBytes).toString("utf8");
2960
- return `${head}
3154
+ const headBytes = raw.slice(0, maxReadBytes).toString("utf8");
3155
+ return `${headBytes}
2961
3156
 
2962
- [\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.]`;
2963
3158
  }
2964
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
+ }
2965
3172
  if (typeof args.head === "number" && args.head > 0) {
2966
- 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;
2967
3179
  }
2968
3180
  if (typeof args.tail === "number" && args.tail > 0) {
2969
- let lines = text.split(/\r?\n/);
2970
- if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
2971
- return lines.slice(Math.max(0, lines.length - args.tail)).join("\n");
2972
- }
2973
- 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");
2974
3200
  }
2975
3201
  });
2976
3202
  registry.register({
@@ -2995,21 +3221,34 @@ function registerFilesystemTools(registry, opts) {
2995
3221
  });
2996
3222
  registry.register({
2997
3223
  name: "directory_tree",
2998
- 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.`,
2999
3229
  readOnly: true,
3000
3230
  parameters: {
3001
3231
  type: "object",
3002
3232
  properties: {
3003
3233
  path: { type: "string", description: "Root of the tree (default: sandbox root)." },
3004
- 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
+ }
3005
3242
  }
3006
3243
  },
3007
3244
  fn: async (args) => {
3008
3245
  const startAbs = safePath(args.path ?? ".");
3009
- 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;
3010
3248
  const lines = [];
3011
3249
  let totalBytes = 0;
3012
3250
  let truncated = false;
3251
+ const PER_DIR_CHILD_CAP = 50;
3013
3252
  const walk2 = async (dir, depth) => {
3014
3253
  if (truncated) return;
3015
3254
  if (depth > maxDepth) return;
@@ -3020,10 +3259,27 @@ function registerFilesystemTools(registry, opts) {
3020
3259
  return;
3021
3260
  }
3022
3261
  entries.sort((a, b) => a.name.localeCompare(b.name));
3262
+ let emitted = 0;
3023
3263
  for (const e of entries) {
3024
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
+ }
3025
3280
  const indent = " ".repeat(depth);
3026
- 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}`;
3027
3283
  totalBytes += line.length + 1;
3028
3284
  if (totalBytes > maxListBytes) {
3029
3285
  lines.push(` [\u2026 tree truncated at ${maxListBytes} bytes \u2026]`);
@@ -3031,7 +3287,8 @@ function registerFilesystemTools(registry, opts) {
3031
3287
  return;
3032
3288
  }
3033
3289
  lines.push(line);
3034
- if (e.isDirectory()) {
3290
+ emitted++;
3291
+ if (e.isDirectory() && !skip) {
3035
3292
  await walk2(pathMod.join(dir, e.name), depth + 1);
3036
3293
  }
3037
3294
  }
@@ -3673,9 +3930,311 @@ function forkRegistryExcluding(parent, exclude) {
3673
3930
  }
3674
3931
 
3675
3932
  // src/tools/shell.ts
3676
- import { spawn as spawn2 } from "child_process";
3933
+ import { spawn as spawn3 } from "child_process";
3677
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";
3678
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
3679
4238
  var DEFAULT_TIMEOUT_SEC = 60;
3680
4239
  var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
3681
4240
  var BUILTIN_ALLOWLIST = [
@@ -3844,10 +4403,10 @@ async function runCommand(cmd, opts) {
3844
4403
  };
3845
4404
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
3846
4405
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
3847
- return await new Promise((resolve7, reject) => {
4406
+ return await new Promise((resolve8, reject) => {
3848
4407
  let child;
3849
4408
  try {
3850
- child = spawn2(bin, args, effectiveSpawnOpts);
4409
+ child = spawn3(bin, args, effectiveSpawnOpts);
3851
4410
  } catch (err) {
3852
4411
  reject(err);
3853
4412
  return;
@@ -3877,7 +4436,7 @@ async function runCommand(cmd, opts) {
3877
4436
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
3878
4437
 
3879
4438
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
3880
- resolve7({ exitCode: code, output, timedOut });
4439
+ resolve8({ exitCode: code, output, timedOut });
3881
4440
  });
3882
4441
  });
3883
4442
  }
@@ -3885,16 +4444,16 @@ function resolveExecutable(cmd, opts = {}) {
3885
4444
  const platform = opts.platform ?? process.platform;
3886
4445
  if (platform !== "win32") return cmd;
3887
4446
  if (!cmd) return cmd;
3888
- if (cmd.includes("/") || cmd.includes("\\") || pathMod2.isAbsolute(cmd)) return cmd;
3889
- if (pathMod2.extname(cmd)) return cmd;
4447
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod3.isAbsolute(cmd)) return cmd;
4448
+ if (pathMod3.extname(cmd)) return cmd;
3890
4449
  const env = opts.env ?? process.env;
3891
4450
  const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
3892
- const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod2.delimiter);
4451
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod3.delimiter);
3893
4452
  const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
3894
4453
  const isFile = opts.isFile ?? defaultIsFile;
3895
4454
  for (const dir of pathDirs) {
3896
4455
  for (const ext of pathExt) {
3897
- const full = pathMod2.win32.join(dir, cmd + ext);
4456
+ const full = pathMod3.win32.join(dir, cmd + ext);
3898
4457
  if (isFile(full)) return full;
3899
4458
  }
3900
4459
  }
@@ -3964,8 +4523,8 @@ function withUtf8Codepage(cmdline) {
3964
4523
  function isBareWindowsName(s) {
3965
4524
  if (!s) return false;
3966
4525
  if (s.includes("/") || s.includes("\\")) return false;
3967
- if (pathMod2.isAbsolute(s)) return false;
3968
- if (pathMod2.extname(s)) return false;
4526
+ if (pathMod3.isAbsolute(s)) return false;
4527
+ if (pathMod3.extname(s)) return false;
3969
4528
  return true;
3970
4529
  }
3971
4530
  function quoteForCmdExe(arg) {
@@ -3984,12 +4543,13 @@ var NeedsConfirmationError = class extends Error {
3984
4543
  }
3985
4544
  };
3986
4545
  function registerShellTools(registry, opts) {
3987
- const rootDir = pathMod2.resolve(opts.rootDir);
4546
+ const rootDir = pathMod3.resolve(opts.rootDir);
3988
4547
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
3989
4548
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
4549
+ const jobs = opts.jobs ?? new JobRegistry();
3990
4550
  const getExtraAllowed = typeof opts.extraAllowed === "function" ? opts.extraAllowed : (() => {
3991
- const snapshot = opts.extraAllowed ?? [];
3992
- return () => snapshot;
4551
+ const snapshot2 = opts.extraAllowed ?? [];
4552
+ return () => snapshot2;
3993
4553
  })();
3994
4554
  const allowAll = opts.allowAll ?? false;
3995
4555
  registry.register({
@@ -4035,8 +4595,126 @@ function registerShellTools(registry, opts) {
4035
4595
  return formatCommandResult(cmd, result);
4036
4596
  }
4037
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
+ });
4038
4684
  return registry;
4039
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
+ }
4040
4718
  function formatCommandResult(cmd, r) {
4041
4719
  const header2 = r.timedOut ? `$ ${cmd}
4042
4720
  [killed after timeout]` : `$ ${cmd}
@@ -4230,11 +4908,11 @@ ${i + 1}. ${r.title}`);
4230
4908
 
4231
4909
  // src/env.ts
4232
4910
  import { readFileSync as readFileSync6 } from "fs";
4233
- import { resolve as resolve4 } from "path";
4911
+ import { resolve as resolve5 } from "path";
4234
4912
  function loadDotenv(path = ".env") {
4235
4913
  let raw;
4236
4914
  try {
4237
- raw = readFileSync6(resolve4(process.cwd(), path), "utf8");
4915
+ raw = readFileSync6(resolve5(process.cwd(), path), "utf8");
4238
4916
  } catch {
4239
4917
  return;
4240
4918
  }
@@ -4947,7 +5625,7 @@ var McpClient = class {
4947
5625
  const id = this.nextId++;
4948
5626
  const frame = { jsonrpc: "2.0", id, method, params };
4949
5627
  let abortHandler = null;
4950
- const promise = new Promise((resolve7, reject) => {
5628
+ const promise = new Promise((resolve8, reject) => {
4951
5629
  const timeout = setTimeout(() => {
4952
5630
  this.pending.delete(id);
4953
5631
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -4956,7 +5634,7 @@ var McpClient = class {
4956
5634
  );
4957
5635
  }, this.requestTimeoutMs);
4958
5636
  this.pending.set(id, {
4959
- resolve: resolve7,
5637
+ resolve: resolve8,
4960
5638
  reject,
4961
5639
  timeout
4962
5640
  });
@@ -5038,7 +5716,7 @@ var McpClient = class {
5038
5716
  };
5039
5717
 
5040
5718
  // src/mcp/stdio.ts
5041
- import { spawn as spawn3 } from "child_process";
5719
+ import { spawn as spawn4 } from "child_process";
5042
5720
  var StdioTransport = class {
5043
5721
  child;
5044
5722
  queue = [];
@@ -5053,14 +5731,14 @@ var StdioTransport = class {
5053
5731
  opts.command,
5054
5732
  ...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
5055
5733
  ].join(" ");
5056
- this.child = spawn3(line, [], {
5734
+ this.child = spawn4(line, [], {
5057
5735
  env,
5058
5736
  cwd: opts.cwd,
5059
5737
  stdio: ["pipe", "pipe", "inherit"],
5060
5738
  shell: true
5061
5739
  });
5062
5740
  } else {
5063
- this.child = spawn3(opts.command, opts.args ?? [], {
5741
+ this.child = spawn4(opts.command, opts.args ?? [], {
5064
5742
  env,
5065
5743
  cwd: opts.cwd,
5066
5744
  stdio: ["pipe", "pipe", "inherit"]
@@ -5079,12 +5757,12 @@ var StdioTransport = class {
5079
5757
  }
5080
5758
  async send(message) {
5081
5759
  if (this.closed) throw new Error("MCP transport is closed");
5082
- return new Promise((resolve7, reject) => {
5760
+ return new Promise((resolve8, reject) => {
5083
5761
  const line = `${JSON.stringify(message)}
5084
5762
  `;
5085
5763
  this.child.stdin.write(line, "utf8", (err) => {
5086
5764
  if (err) reject(err);
5087
- else resolve7();
5765
+ else resolve8();
5088
5766
  });
5089
5767
  });
5090
5768
  }
@@ -5095,8 +5773,8 @@ var StdioTransport = class {
5095
5773
  continue;
5096
5774
  }
5097
5775
  if (this.closed) return;
5098
- const next = await new Promise((resolve7) => {
5099
- this.waiters.push(resolve7);
5776
+ const next = await new Promise((resolve8) => {
5777
+ this.waiters.push(resolve8);
5100
5778
  });
5101
5779
  if (next === null) return;
5102
5780
  yield next;
@@ -5162,8 +5840,8 @@ var SseTransport = class {
5162
5840
  constructor(opts) {
5163
5841
  this.url = opts.url;
5164
5842
  this.headers = opts.headers ?? {};
5165
- this.endpointReady = new Promise((resolve7, reject) => {
5166
- this.resolveEndpoint = resolve7;
5843
+ this.endpointReady = new Promise((resolve8, reject) => {
5844
+ this.resolveEndpoint = resolve8;
5167
5845
  this.rejectEndpoint = reject;
5168
5846
  });
5169
5847
  this.endpointReady.catch(() => void 0);
@@ -5190,8 +5868,8 @@ var SseTransport = class {
5190
5868
  continue;
5191
5869
  }
5192
5870
  if (this.closed) return;
5193
- const next = await new Promise((resolve7) => {
5194
- this.waiters.push(resolve7);
5871
+ const next = await new Promise((resolve8) => {
5872
+ this.waiters.push(resolve8);
5195
5873
  });
5196
5874
  if (next === null) return;
5197
5875
  yield next;
@@ -5391,7 +6069,7 @@ async function trySection(load) {
5391
6069
 
5392
6070
  // src/code/edit-blocks.ts
5393
6071
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
5394
- import { dirname as dirname5, resolve as resolve5 } from "path";
6072
+ import { dirname as dirname5, resolve as resolve6 } from "path";
5395
6073
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
5396
6074
  function parseEditBlocks(text) {
5397
6075
  const out = [];
@@ -5409,8 +6087,8 @@ function parseEditBlocks(text) {
5409
6087
  return out;
5410
6088
  }
5411
6089
  function applyEditBlock(block, rootDir) {
5412
- const absRoot = resolve5(rootDir);
5413
- const absTarget = resolve5(absRoot, block.path);
6090
+ const absRoot = resolve6(rootDir);
6091
+ const absTarget = resolve6(absRoot, block.path);
5414
6092
  if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
5415
6093
  return {
5416
6094
  path: block.path,
@@ -5459,14 +6137,26 @@ function applyEditBlock(block, rootDir) {
5459
6137
  function applyEditBlocks(blocks, rootDir) {
5460
6138
  return blocks.map((b) => applyEditBlock(b, rootDir));
5461
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
+ }
5462
6152
  function snapshotBeforeEdits(blocks, rootDir) {
5463
- const absRoot = resolve5(rootDir);
6153
+ const absRoot = resolve6(rootDir);
5464
6154
  const seen = /* @__PURE__ */ new Set();
5465
6155
  const snapshots = [];
5466
6156
  for (const b of blocks) {
5467
6157
  if (seen.has(b.path)) continue;
5468
6158
  seen.add(b.path);
5469
- const abs = resolve5(absRoot, b.path);
6159
+ const abs = resolve6(absRoot, b.path);
5470
6160
  if (!existsSync6(abs)) {
5471
6161
  snapshots.push({ path: b.path, prevContent: null });
5472
6162
  continue;
@@ -5480,9 +6170,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
5480
6170
  return snapshots;
5481
6171
  }
5482
6172
  function restoreSnapshots(snapshots, rootDir) {
5483
- const absRoot = resolve5(rootDir);
6173
+ const absRoot = resolve6(rootDir);
5484
6174
  return snapshots.map((snap) => {
5485
- const abs = resolve5(absRoot, snap.path);
6175
+ const abs = resolve6(absRoot, snap.path);
5486
6176
  if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
5487
6177
  return {
5488
6178
  path: snap.path,
@@ -5769,11 +6459,11 @@ function formatLogSize(path = defaultUsageLogPath()) {
5769
6459
  // src/cli/commands/chat.tsx
5770
6460
  import { existsSync as existsSync11, statSync as statSync6 } from "fs";
5771
6461
  import { render } from "ink";
5772
- import React17, { useState as useState7 } from "react";
6462
+ import React18, { useState as useState8 } from "react";
5773
6463
 
5774
6464
  // src/cli/ui/App.tsx
5775
- import { Box as Box13, Static, Text as Text13, useApp, useInput as useInput4 } from "ink";
5776
- 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";
5777
6467
 
5778
6468
  // src/code/diff-preview.ts
5779
6469
  function formatEditBlockDiff(block, opts = {}) {
@@ -5996,13 +6686,108 @@ function FileRow({ path, isSelected }) {
5996
6686
  return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, marker, " ", base, dir ? ` ${dir}` : ""));
5997
6687
  }
5998
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
+
5999
6784
  // src/cli/ui/EventLog.tsx
6000
- import { Box as Box4, Text as Text4, useStdout } from "ink";
6001
- import React5 from "react";
6785
+ import { Box as Box5, Text as Text5, useStdout as useStdout2 } from "ink";
6786
+ import React6 from "react";
6002
6787
 
6003
6788
  // src/cli/ui/PlanStateBlock.tsx
6004
- import { Box as Box2, Text as Text2 } from "ink";
6005
- import React2 from "react";
6789
+ import { Box as Box3, Text as Text3 } from "ink";
6790
+ import React3 from "react";
6006
6791
  function PlanStateBlock({ planState }) {
6007
6792
  const fields = [];
6008
6793
  if (planState.subgoals.length) fields.push(["subgoals", planState.subgoals, "cyan", false]);
@@ -6013,14 +6798,14 @@ function PlanStateBlock({ planState }) {
6013
6798
  if (planState.rejectedPaths.length)
6014
6799
  fields.push(["rejected", planState.rejectedPaths, "red", true]);
6015
6800
  if (fields.length === 0) return null;
6016
- 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 ")}`))));
6017
6802
  }
6018
6803
 
6019
6804
  // src/cli/ui/markdown.tsx
6020
6805
  import { readFileSync as readFileSync12, statSync as statSync5 } from "fs";
6021
6806
  import { isAbsolute as isAbsolute4, join as join10 } from "path";
6022
- import { Box as Box3, Text as Text3 } from "ink";
6023
- import React3 from "react";
6807
+ import { Box as Box4, Text as Text4 } from "ink";
6808
+ import React4 from "react";
6024
6809
  var SUPERSCRIPT = {
6025
6810
  "0": "\u2070",
6026
6811
  "1": "\xB9",
@@ -6145,57 +6930,57 @@ function InlineMd({
6145
6930
  for (const m of text.matchAll(INLINE_RE)) {
6146
6931
  const start = m.index ?? 0;
6147
6932
  if (start > last) {
6148
- 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)));
6149
6934
  }
6150
6935
  if (m[2] !== void 0 && m[3] !== void 0) {
6151
6936
  const linkText = m[2];
6152
6937
  const url = m[3];
6153
6938
  if (isExternalUrl(url)) {
6154
6939
  parts.push(
6155
- /* @__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)
6156
6941
  );
6157
6942
  } else {
6158
6943
  const status = citations?.get(url);
6159
6944
  if (status && !status.ok) {
6160
6945
  parts.push(
6161
- /* @__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`)
6162
6947
  );
6163
6948
  } else {
6164
6949
  parts.push(
6165
- /* @__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)
6166
6951
  );
6167
6952
  }
6168
6953
  }
6169
6954
  } else if (m[4] !== void 0) {
6170
6955
  parts.push(
6171
- /* @__PURE__ */ React3.createElement(Text3, { key: `b${idx++}`, bold: true }, m[4])
6956
+ /* @__PURE__ */ React4.createElement(Text4, { key: `b${idx++}`, bold: true }, m[4])
6172
6957
  );
6173
6958
  } else if (m[5] !== void 0) {
6174
6959
  const stripped = m[5].replace(/^(\w+)\s+/, "");
6175
6960
  parts.push(
6176
- /* @__PURE__ */ React3.createElement(Text3, { key: `c${idx++}`, color: "yellow" }, stripped)
6961
+ /* @__PURE__ */ React4.createElement(Text4, { key: `c${idx++}`, color: "yellow" }, stripped)
6177
6962
  );
6178
6963
  } else if (m[6] !== void 0) {
6179
6964
  parts.push(
6180
- /* @__PURE__ */ React3.createElement(Text3, { key: `c${idx++}`, color: "yellow" }, m[6])
6965
+ /* @__PURE__ */ React4.createElement(Text4, { key: `c${idx++}`, color: "yellow" }, m[6])
6181
6966
  );
6182
6967
  } else if (m[7] !== void 0) {
6183
6968
  parts.push(
6184
- /* @__PURE__ */ React3.createElement(Text3, { key: `i${idx++}`, italic: true }, m[7])
6969
+ /* @__PURE__ */ React4.createElement(Text4, { key: `i${idx++}`, italic: true }, m[7])
6185
6970
  );
6186
6971
  }
6187
6972
  last = start + m[0].length;
6188
6973
  }
6189
6974
  if (last < text.length) {
6190
- 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)));
6191
6976
  }
6192
6977
  if (padTo !== void 0) {
6193
6978
  const seen = visibleWidth(text);
6194
6979
  if (seen < padTo) {
6195
- 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)));
6196
6981
  }
6197
6982
  }
6198
- return /* @__PURE__ */ React3.createElement(Text3, null, parts);
6983
+ return /* @__PURE__ */ React4.createElement(Text4, null, parts);
6199
6984
  }
6200
6985
  function stripInlineMarkup(s) {
6201
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");
@@ -6391,19 +7176,19 @@ function parseBlocks(raw) {
6391
7176
  function BlockView({ block, citations }) {
6392
7177
  switch (block.kind) {
6393
7178
  case "heading":
6394
- 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 }));
6395
7180
  case "paragraph":
6396
- return /* @__PURE__ */ React3.createElement(InlineMd, { text: block.text, citations });
7181
+ return /* @__PURE__ */ React4.createElement(InlineMd, { text: block.text, citations });
6397
7182
  case "bullet":
6398
- 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 }))));
6399
7184
  case "code":
6400
- 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));
6401
7186
  case "edit-block":
6402
- return /* @__PURE__ */ React3.createElement(EditBlockRow, { block });
7187
+ return /* @__PURE__ */ React4.createElement(EditBlockRow, { block });
6403
7188
  case "table":
6404
- return /* @__PURE__ */ React3.createElement(TableBlockRow, { block, citations });
7189
+ return /* @__PURE__ */ React4.createElement(TableBlockRow, { block, citations });
6405
7190
  case "hr":
6406
- 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");
6407
7192
  }
6408
7193
  }
6409
7194
  function splitTableRow(line) {
@@ -6421,14 +7206,14 @@ function TableBlockRow({ block, citations }) {
6421
7206
  widths.push(Math.min(40, Math.max(3, ...cellLengths)));
6422
7207
  }
6423
7208
  const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u253C\u2500");
6424
- 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) => (
6425
7210
  // biome-ignore lint/suspicious/noArrayIndexKey: table columns never reorder — derived from a static header array
6426
- /* @__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 " : "")
6427
- ))), /* @__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) => (
6428
7213
  // biome-ignore lint/suspicious/noArrayIndexKey: table rows render in source order and don't reorder
6429
- /* @__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) => (
6430
7215
  // biome-ignore lint/suspicious/noArrayIndexKey: same — column axis is fixed by the table shape
6431
- /* @__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 " : "")
6432
7217
  )))
6433
7218
  )));
6434
7219
  }
@@ -6448,44 +7233,44 @@ function EditBlockRow({ block }) {
6448
7233
  const isNewFile = block.search.length === 0;
6449
7234
  const searchLines = block.search.split("\n");
6450
7235
  const replaceLines = block.replace.split("\n");
6451
- 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}`))));
6452
7237
  }
6453
7238
  function Markdown({ text, projectRoot }) {
6454
7239
  const cleaned = stripMath(text);
6455
7240
  const root = projectRoot ?? process.cwd();
6456
- const citations = React3.useMemo(() => collectCitations(cleaned, root), [cleaned, root]);
6457
- 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]);
6458
7243
  const broken = [];
6459
7244
  for (const [url, status] of citations) {
6460
7245
  if (!status.ok) broken.push({ url, reason: status.reason });
6461
7246
  }
6462
- 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);
6463
7248
  }
6464
7249
  function BrokenCitationsBlock({ items }) {
6465
- 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) => (
6466
7251
  // biome-ignore lint/suspicious/noArrayIndexKey: list is derived from a Map iteration order, stable per render
6467
- /* @__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}`)
6468
7253
  )));
6469
7254
  }
6470
7255
 
6471
7256
  // src/cli/ui/ticker.tsx
6472
- import React4, { createContext, useContext, useEffect, useState } from "react";
7257
+ import React5, { createContext, useContext, useEffect, useState as useState2 } from "react";
6473
7258
  var TICK_MS = 120;
6474
7259
  var TickContext = createContext(0);
6475
7260
  function TickerProvider({ children, disabled }) {
6476
- const [tick, setTick] = useState(0);
7261
+ const [tick, setTick] = useState2(0);
6477
7262
  useEffect(() => {
6478
7263
  if (disabled) return;
6479
7264
  const id = setInterval(() => setTick((t) => t + 1), TICK_MS);
6480
7265
  return () => clearInterval(id);
6481
7266
  }, [disabled]);
6482
- return /* @__PURE__ */ React4.createElement(TickContext.Provider, { value: tick }, children);
7267
+ return /* @__PURE__ */ React5.createElement(TickContext.Provider, { value: tick }, children);
6483
7268
  }
6484
7269
  function useTick() {
6485
7270
  return useContext(TickContext);
6486
7271
  }
6487
7272
  function useElapsedSeconds() {
6488
- const [start] = useState(() => Date.now());
7273
+ const [start] = useState2(() => Date.now());
6489
7274
  useTick();
6490
7275
  return Math.floor((Date.now() - start) / 1e3);
6491
7276
  }
@@ -6505,18 +7290,18 @@ function RoleGlyph({
6505
7290
  glyph,
6506
7291
  color
6507
7292
  }) {
6508
- return /* @__PURE__ */ React5.createElement(Text4, { color, bold: true }, glyph);
7293
+ return /* @__PURE__ */ React6.createElement(Text5, { color, bold: true }, glyph);
6509
7294
  }
6510
- var EventRow = React5.memo(function EventRow2({
7295
+ var EventRow = React6.memo(function EventRow2({
6511
7296
  event,
6512
7297
  projectRoot
6513
7298
  }) {
6514
7299
  if (event.role === "user") {
6515
- 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));
6516
7301
  }
6517
7302
  if (event.role === "assistant") {
6518
- if (event.streaming) return /* @__PURE__ */ React5.createElement(StreamingAssistant, { event });
6519
- 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));
6520
7305
  }
6521
7306
  if (event.role === "tool") {
6522
7307
  const isError = event.text.startsWith("ERROR:");
@@ -6524,31 +7309,31 @@ var EventRow = React5.memo(function EventRow2({
6524
7309
  const glyph = isError ? ROLE_GLYPH.toolErr : ROLE_GLYPH.toolOk;
6525
7310
  const marker = isError ? "\u2717" : "\u2192";
6526
7311
  const isEditFile = (event.toolName === "edit_file" || event.toolName?.endsWith("_edit_file")) && !isError;
6527
- 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))));
6528
7313
  }
6529
7314
  if (event.role === "error") {
6530
- 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));
6531
7316
  }
6532
7317
  if (event.role === "info") {
6533
- 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));
6534
7319
  }
6535
7320
  if (event.role === "warning") {
6536
- 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));
6537
7322
  }
6538
- 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));
6539
7324
  });
6540
7325
  function EditFileDiff({ text }) {
6541
7326
  const lines = text.split(/\r?\n/);
6542
7327
  const [statusHeader, hunkHeader, ...body] = lines;
6543
- 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) => {
6544
7329
  const key = `${i}-${line.slice(0, 32)}`;
6545
7330
  if (line.startsWith("- ")) {
6546
- return /* @__PURE__ */ React5.createElement(Text4, { key, color: "red" }, line);
7331
+ return /* @__PURE__ */ React6.createElement(Text5, { key, color: "red" }, line);
6547
7332
  }
6548
7333
  if (line.startsWith("+ ")) {
6549
- return /* @__PURE__ */ React5.createElement(Text4, { key, color: "green" }, line);
7334
+ return /* @__PURE__ */ React6.createElement(Text5, { key, color: "green" }, line);
6550
7335
  }
6551
- return /* @__PURE__ */ React5.createElement(Text4, { key, dimColor: true }, line);
7336
+ return /* @__PURE__ */ React6.createElement(Text5, { key, dimColor: true }, line);
6552
7337
  }));
6553
7338
  }
6554
7339
  function BranchBlock({ branch }) {
@@ -6557,33 +7342,33 @@ function BranchBlock({ branch }) {
6557
7342
  const t = (branch.temperatures[i] ?? 0).toFixed(1);
6558
7343
  return `${marker} #${i} T=${t} u=${u}`;
6559
7344
  }).join(" ");
6560
- 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)));
6561
7346
  }
6562
7347
  function ReasoningBlock({ reasoning }) {
6563
7348
  const max = 260;
6564
7349
  const flat = reasoning.replace(/\s+/g, " ").trim();
6565
7350
  const preview = flat.length <= max ? flat : `\u2026 (+${flat.length - max} earlier chars) ${flat.slice(-max)}`;
6566
- 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));
6567
7352
  }
6568
7353
  function Elapsed() {
6569
7354
  const s = useElapsedSeconds();
6570
7355
  const mm = String(Math.floor(s / 60)).padStart(2, "0");
6571
7356
  const ss = String(s % 60).padStart(2, "0");
6572
- return /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, `${mm}:${ss}`);
7357
+ return /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, `${mm}:${ss}`);
6573
7358
  }
6574
7359
  function PulsingAssistantGlyph() {
6575
7360
  const tick = useTick();
6576
7361
  const on = Math.floor(tick / 4) % 2 === 0;
6577
- 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);
6578
7363
  }
6579
7364
  function StreamingAssistant({ event }) {
6580
7365
  if (event.branchProgress) {
6581
7366
  const p = event.branchProgress;
6582
7367
  if (p.completed === 0) {
6583
- 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"));
6584
7369
  }
6585
7370
  const pct2 = Math.round(p.completed / p.total * 100);
6586
- 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"));
6587
7372
  }
6588
7373
  const tail = lastLine(event.text, 140);
6589
7374
  const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
@@ -6613,16 +7398,16 @@ function StreamingAssistant({ event }) {
6613
7398
  label = parts.join(" \xB7 ");
6614
7399
  labelColor = "green";
6615
7400
  }
6616
- 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 ? (
6617
7402
  // Non-dim yellow: first-time users misread the dim version as
6618
7403
  // "app frozen". The reassurance has to be VISIBLE to do its job.
6619
- /* @__PURE__ */ React5.createElement(Text4, { color: "yellow", italic: true }, " waiting for first byte \u2014 this is normal, typically 5-60s depending on model + load")
6620
- ) : 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);
6621
7406
  }
6622
7407
  function Pulse() {
6623
7408
  const tick = useTick();
6624
7409
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
6625
- 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]);
6626
7411
  }
6627
7412
  function formatToolCallIndex(tb) {
6628
7413
  if (!tb || tb.index === void 0) return "";
@@ -6641,7 +7426,7 @@ function lastLine(s, maxChars) {
6641
7426
  }
6642
7427
  function StatsLine({ stats }) {
6643
7428
  const hit = (stats.cacheHitRatio * 100).toFixed(1);
6644
- 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)));
6645
7430
  }
6646
7431
  function truncate2(s, max) {
6647
7432
  if (s.length <= max) return s;
@@ -6664,12 +7449,12 @@ ${s.slice(-max)}`;
6664
7449
  }
6665
7450
 
6666
7451
  // src/cli/ui/PlanConfirm.tsx
6667
- import { Box as Box6, Text as Text6 } from "ink";
6668
- import React7 from "react";
7452
+ import { Box as Box7, Text as Text7 } from "ink";
7453
+ import React8 from "react";
6669
7454
 
6670
7455
  // src/cli/ui/Select.tsx
6671
- import { Box as Box5, Text as Text5, useInput } from "ink";
6672
- 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";
6673
7458
  function SingleSelect({
6674
7459
  items,
6675
7460
  initialValue,
@@ -6681,8 +7466,8 @@ function SingleSelect({
6681
7466
  0,
6682
7467
  items.findIndex((i) => i.value === initialValue && !i.disabled)
6683
7468
  );
6684
- const [index, setIndex] = useState2(initialIndex === -1 ? 0 : initialIndex);
6685
- useInput((_input, key) => {
7469
+ const [index, setIndex] = useState3(initialIndex === -1 ? 0 : initialIndex);
7470
+ useInput2((_input, key) => {
6686
7471
  if (key.upArrow) {
6687
7472
  setIndex((i) => findNextEnabled(items, i, -1));
6688
7473
  } else if (key.downArrow) {
@@ -6694,7 +7479,7 @@ function SingleSelect({
6694
7479
  onCancel();
6695
7480
  }
6696
7481
  });
6697
- 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(
6698
7483
  SelectRow,
6699
7484
  {
6700
7485
  key: item.value,
@@ -6702,7 +7487,7 @@ function SingleSelect({
6702
7487
  active: i === index,
6703
7488
  marker: i === index ? "\u25B8" : " "
6704
7489
  }
6705
- )), 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);
6706
7491
  }
6707
7492
  function MultiSelect({
6708
7493
  items,
@@ -6711,12 +7496,12 @@ function MultiSelect({
6711
7496
  onCancel,
6712
7497
  footer
6713
7498
  }) {
6714
- const [index, setIndex] = useState2(() => {
7499
+ const [index, setIndex] = useState3(() => {
6715
7500
  const first = items.findIndex((i) => !i.disabled);
6716
7501
  return first === -1 ? 0 : first;
6717
7502
  });
6718
- const [selected, setSelected] = useState2(new Set(initialSelected));
6719
- useInput((input, key) => {
7503
+ const [selected, setSelected] = useState3(new Set(initialSelected));
7504
+ useInput2((input, key) => {
6720
7505
  if (key.upArrow) {
6721
7506
  setIndex((i) => findNextEnabled(items, i, -1));
6722
7507
  } else if (key.downArrow) {
@@ -6737,10 +7522,10 @@ function MultiSelect({
6737
7522
  onCancel();
6738
7523
  }
6739
7524
  });
6740
- return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column" }, items.map((item, i) => {
7525
+ return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column" }, items.map((item, i) => {
6741
7526
  const checked = selected.has(item.value);
6742
7527
  const marker = checked ? "[x]" : "[ ]";
6743
- return /* @__PURE__ */ React6.createElement(
7528
+ return /* @__PURE__ */ React7.createElement(
6744
7529
  SelectRow,
6745
7530
  {
6746
7531
  key: item.value,
@@ -6749,7 +7534,7 @@ function MultiSelect({
6749
7534
  marker: `${i === index ? "\u25B8" : " "} ${marker}`
6750
7535
  }
6751
7536
  );
6752
- }), 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);
6753
7538
  }
6754
7539
  function SelectRow({
6755
7540
  item,
@@ -6757,7 +7542,7 @@ function SelectRow({
6757
7542
  marker
6758
7543
  }) {
6759
7544
  const color = item.disabled ? "gray" : active ? "cyan" : void 0;
6760
- 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);
6761
7546
  }
6762
7547
  function findNextEnabled(items, from, step) {
6763
7548
  if (items.length === 0) return 0;
@@ -6800,7 +7585,7 @@ function PlanConfirmInner({
6800
7585
  const sourceLineBudget = Math.max(MIN_BODY_ROWS, Math.floor(renderedBudget / MARKDOWN_EXPANSION));
6801
7586
  const visible = clampBodyByLines(charCapped, sourceLineBudget);
6802
7587
  const hasOpenQuestions = /^#{1,6}\s*(open[-\s]?questions?|risks?|unknowns?|assumptions?|unclear)/im.test(plan) || /^#{1,6}\s*(待确认|开放问题|风险|未知|假设|不确定)/im.test(plan);
6803
- 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(
6804
7589
  SingleSelect,
6805
7590
  {
6806
7591
  initialValue: hasOpenQuestions ? "refine" : "approve",
@@ -6827,14 +7612,14 @@ function PlanConfirmInner({
6827
7612
  }
6828
7613
  )));
6829
7614
  }
6830
- var PlanConfirm = React7.memo(PlanConfirmInner);
7615
+ var PlanConfirm = React8.memo(PlanConfirmInner);
6831
7616
 
6832
7617
  // src/cli/ui/PlanRefineInput.tsx
6833
- import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
6834
- 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";
6835
7620
  function PlanRefineInput({ mode, onSubmit, onCancel }) {
6836
- const [value, setValue] = useState3("");
6837
- useInput2((input, key) => {
7621
+ const [value, setValue] = useState4("");
7622
+ useInput3((input, key) => {
6838
7623
  if (key.escape) {
6839
7624
  onCancel();
6840
7625
  return;
@@ -6854,12 +7639,12 @@ function PlanRefineInput({ mode, onSubmit, onCancel }) {
6854
7639
  const title = mode === "approve" ? "\u25B8 approving \u2014 any last instructions or answers to open questions?" : "\u25B8 refining \u2014 what should the model change?";
6855
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.";
6856
7641
  const blankHint = mode === "approve" ? " (Enter with blank = approve without extra instructions.)" : " (Enter with blank = ask the model to list concrete questions.)";
6857
- 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"))));
6858
7643
  }
6859
7644
 
6860
7645
  // src/cli/ui/PromptInput.tsx
6861
- import { Box as Box8, Text as Text8, useInput as useInput3 } from "ink";
6862
- 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";
6863
7648
 
6864
7649
  // src/cli/ui/multiline-keys.ts
6865
7650
  var BACKSLASH_SUFFIX = /\\$/;
@@ -6974,7 +7759,7 @@ function PromptInput({
6974
7759
  disabled,
6975
7760
  placeholder
6976
7761
  }) {
6977
- const [cursor, setCursor] = useState4(value.length);
7762
+ const [cursor, setCursor] = useState5(value.length);
6978
7763
  const lastLocalValueRef = useRef(value);
6979
7764
  if (value !== lastLocalValueRef.current) {
6980
7765
  lastLocalValueRef.current = value;
@@ -6984,7 +7769,7 @@ function PromptInput({
6984
7769
  }
6985
7770
  const tick = useTick();
6986
7771
  const showCursor = disabled ? false : Math.floor(tick / 4) % 2 === 0;
6987
- useInput3(
7772
+ useInput4(
6988
7773
  (input, key) => {
6989
7774
  const ke = {
6990
7775
  input,
@@ -7019,13 +7804,13 @@ function PromptInput({
7019
7804
  const lines = value.length > 0 ? value.split("\n") : [""];
7020
7805
  const borderColor = disabled ? "gray" : "cyan";
7021
7806
  const { line: cursorLine, col: cursorCol } = lineAndColumn(value, cursor);
7022
- 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) => {
7023
7808
  const isFirst = i === 0;
7024
7809
  const showPlaceholder = isFirst && value.length === 0;
7025
7810
  const isCursorLine = i === cursorLine;
7026
7811
  return (
7027
7812
  // biome-ignore lint/suspicious/noArrayIndexKey: stable by construction — lines are derived from `value.split("\n")` and never reordered
7028
- /* @__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(
7029
7814
  LineWithCursor,
7030
7815
  {
7031
7816
  line,
@@ -7033,7 +7818,7 @@ function PromptInput({
7033
7818
  showCursor,
7034
7819
  borderColor
7035
7820
  }
7036
- ) : /* @__PURE__ */ React9.createElement(Text8, null, line))
7821
+ ) : /* @__PURE__ */ React10.createElement(Text9, null, line))
7037
7822
  );
7038
7823
  }));
7039
7824
  }
@@ -7047,16 +7832,17 @@ function LineWithCursor({
7047
7832
  const atCursor = line.slice(col, col + 1);
7048
7833
  const after = line.slice(col + 1);
7049
7834
  if (atCursor.length === 0) {
7050
- 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" : " "));
7051
7836
  }
7052
- 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));
7053
7838
  }
7054
7839
 
7055
7840
  // src/cli/ui/ShellConfirm.tsx
7056
- import { Box as Box9, Text as Text9 } from "ink";
7057
- import React10 from "react";
7058
- function ShellConfirm({ command, allowPrefix, onChoose }) {
7059
- 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(
7060
7846
  SingleSelect,
7061
7847
  {
7062
7848
  initialValue: "run_once",
@@ -7113,8 +7899,8 @@ function derivePrefix(command) {
7113
7899
  }
7114
7900
 
7115
7901
  // src/cli/ui/SlashArgPicker.tsx
7116
- import { Box as Box10, Text as Text10 } from "ink";
7117
- import React11 from "react";
7902
+ import { Box as Box11, Text as Text11 } from "ink";
7903
+ import React12 from "react";
7118
7904
  function SlashArgPicker({
7119
7905
  matches,
7120
7906
  selectedIndex,
@@ -7123,11 +7909,11 @@ function SlashArgPicker({
7123
7909
  partial
7124
7910
  }) {
7125
7911
  if (kind === "hint") {
7126
- 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));
7127
7913
  }
7128
7914
  if (matches === null) return null;
7129
7915
  if (matches.length === 0) {
7130
- 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'));
7131
7917
  }
7132
7918
  const MAX = 8;
7133
7919
  const total = matches.length;
@@ -7135,26 +7921,26 @@ function SlashArgPicker({
7135
7921
  const shown = matches.slice(windowStart, windowStart + MAX);
7136
7922
  const hiddenAbove = windowStart;
7137
7923
  const hiddenBelow = total - windowStart - shown.length;
7138
- 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"));
7139
7925
  }
7140
7926
  function ArgRow({ value, isSelected }) {
7141
7927
  const marker = isSelected ? "\u25B8" : " ";
7142
7928
  if (isSelected) {
7143
- 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));
7144
7930
  }
7145
- 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));
7146
7932
  }
7147
7933
 
7148
7934
  // src/cli/ui/SlashSuggestions.tsx
7149
- import { Box as Box11, Text as Text11 } from "ink";
7150
- import React12 from "react";
7935
+ import { Box as Box12, Text as Text12 } from "ink";
7936
+ import React13 from "react";
7151
7937
  function SlashSuggestions({
7152
7938
  matches,
7153
7939
  selectedIndex
7154
7940
  }) {
7155
7941
  if (matches === null) return null;
7156
7942
  if (matches.length === 0) {
7157
- 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"));
7158
7944
  }
7159
7945
  const MAX = 8;
7160
7946
  const total = matches.length;
@@ -7162,21 +7948,21 @@ function SlashSuggestions({
7162
7948
  const shown = matches.slice(windowStart, windowStart + MAX);
7163
7949
  const hiddenAbove = windowStart;
7164
7950
  const hiddenBelow = total - windowStart - shown.length;
7165
- 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"));
7166
7952
  }
7167
7953
  function SuggestionRow({ spec, isSelected }) {
7168
7954
  const marker = isSelected ? "\u25B8" : " ";
7169
7955
  const name = `/${spec.cmd}`;
7170
7956
  const argsSuffix = spec.argsHint ? ` ${spec.argsHint}` : "";
7171
7957
  if (isSelected) {
7172
- 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));
7173
7959
  }
7174
- 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));
7175
7961
  }
7176
7962
 
7177
7963
  // src/cli/ui/StatsPanel.tsx
7178
- import { Box as Box12, Text as Text12, useStdout as useStdout2 } from "ink";
7179
- import React13 from "react";
7964
+ import { Box as Box13, Text as Text13, useStdout as useStdout3 } from "ink";
7965
+ import React14 from "react";
7180
7966
  var WORDMARK_STYLES = [
7181
7967
  { ch: "\u25C8", color: "#5eead4", isLogo: true },
7182
7968
  // teal — brand mark
@@ -7202,7 +7988,7 @@ function Wordmark({ busy }) {
7202
7988
  const tick = useTick();
7203
7989
  const period = busy ? 5 : 12;
7204
7990
  const bright = Math.floor(tick / period) % 2 === 0;
7205
- 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)));
7206
7992
  }
7207
7993
  var NARROW_BREAKPOINT = 120;
7208
7994
  var COLD_START_TURNS = 3;
@@ -7214,6 +8000,7 @@ function StatsPanel({
7214
8000
  branchBudget,
7215
8001
  reasoningEffort,
7216
8002
  planMode,
8003
+ editMode,
7217
8004
  balance,
7218
8005
  updateAvailable,
7219
8006
  busy
@@ -7221,11 +8008,11 @@ function StatsPanel({
7221
8008
  const branchOn = (branchBudget ?? 1) > 1;
7222
8009
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
7223
8010
  const ctxRatio = summary.lastPromptTokens / ctxMax;
7224
- const { stdout: stdout2 } = useStdout2();
8011
+ const { stdout: stdout2 } = useStdout3();
7225
8012
  const columns = stdout2?.columns ?? 80;
7226
8013
  const narrow = columns < NARROW_BREAKPOINT;
7227
8014
  const coldStart = summary.turns <= COLD_START_TURNS;
7228
- 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(
7229
8016
  Header,
7230
8017
  {
7231
8018
  model,
@@ -7235,12 +8022,13 @@ function StatsPanel({
7235
8022
  branchBudget: branchBudget ?? 1,
7236
8023
  reasoningEffort,
7237
8024
  planMode,
8025
+ editMode,
7238
8026
  turns: summary.turns,
7239
8027
  updateAvailable,
7240
8028
  narrow,
7241
8029
  busy: busy ?? false
7242
8030
  }
7243
- ), narrow ? /* @__PURE__ */ React13.createElement(
8031
+ ), narrow ? /* @__PURE__ */ React14.createElement(
7244
8032
  StackedMetrics,
7245
8033
  {
7246
8034
  summary,
@@ -7249,7 +8037,7 @@ function StatsPanel({
7249
8037
  balance,
7250
8038
  coldStart
7251
8039
  }
7252
- ) : /* @__PURE__ */ React13.createElement(
8040
+ ) : /* @__PURE__ */ React14.createElement(
7253
8041
  InlineMetrics,
7254
8042
  {
7255
8043
  summary,
@@ -7268,12 +8056,13 @@ function Header({
7268
8056
  branchBudget,
7269
8057
  reasoningEffort,
7270
8058
  planMode,
8059
+ editMode,
7271
8060
  turns,
7272
8061
  updateAvailable,
7273
8062
  narrow,
7274
8063
  busy
7275
8064
  }) {
7276
- 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`)));
7277
8066
  }
7278
8067
  function InlineMetrics({
7279
8068
  summary,
@@ -7282,7 +8071,7 @@ function InlineMetrics({
7282
8071
  balance,
7283
8072
  coldStart
7284
8073
  }) {
7285
- 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);
7286
8075
  }
7287
8076
  function StackedMetrics({
7288
8077
  summary,
@@ -7291,7 +8080,7 @@ function StackedMetrics({
7291
8080
  balance,
7292
8081
  coldStart
7293
8082
  }) {
7294
- 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(
7295
8084
  ContextCell,
7296
8085
  {
7297
8086
  ratio: ctxRatio,
@@ -7299,7 +8088,7 @@ function StackedMetrics({
7299
8088
  ctxMax,
7300
8089
  showBar: true
7301
8090
  }
7302
- ), 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 }));
7303
8092
  }
7304
8093
  function ContextCell({
7305
8094
  ratio,
@@ -7308,11 +8097,11 @@ function ContextCell({
7308
8097
  showBar
7309
8098
  }) {
7310
8099
  if (promptTokens === 0) {
7311
- 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)"));
7312
8101
  }
7313
8102
  const color = ratio >= 0.8 ? "red" : ratio >= 0.6 ? "yellow" : "green";
7314
8103
  const pct2 = Math.round(ratio * 100);
7315
- 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);
7316
8105
  }
7317
8106
  function CacheCell({
7318
8107
  hitRatio,
@@ -7321,33 +8110,33 @@ function CacheCell({
7321
8110
  }) {
7322
8111
  const pct2 = (hitRatio * 100).toFixed(1);
7323
8112
  if (turns === 0) {
7324
- 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"));
7325
8114
  }
7326
8115
  if (coldStart) {
7327
- 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)"));
7328
8117
  }
7329
8118
  const color = hitRatio >= 0.7 ? "green" : hitRatio >= 0.4 ? "yellow" : "red";
7330
- 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, "%"));
7331
8120
  }
7332
8121
  function CostCell({
7333
8122
  summary,
7334
8123
  coldStart
7335
8124
  }) {
7336
8125
  if (summary.turns === 0) {
7337
- 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"));
7338
8127
  }
7339
8128
  const primaryColor = coldStart ? void 0 : "green";
7340
- 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), ")"));
7341
8130
  }
7342
8131
  function BalanceCell({ balance }) {
7343
8132
  const color = balance.total < 1 ? "red" : balance.total < 5 ? "yellow" : "green";
7344
- 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}` : ""));
7345
8134
  }
7346
8135
  function Bar({ ratio, color }) {
7347
8136
  const cells = 10;
7348
8137
  const filled = Math.max(0, Math.min(cells, Math.round(ratio * cells)));
7349
8138
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(cells - filled);
7350
- return /* @__PURE__ */ React13.createElement(Text12, { color }, bar);
8139
+ return /* @__PURE__ */ React14.createElement(Text13, { color }, bar);
7351
8140
  }
7352
8141
  function formatTokens(n) {
7353
8142
  if (n < 1024) return String(n);
@@ -7783,7 +8572,7 @@ var SLASH_COMMANDS = [
7783
8572
  {
7784
8573
  cmd: "compact",
7785
8574
  argsHint: "[tokens]",
7786
- 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"
7787
8576
  },
7788
8577
  { cmd: "keys", summary: "show all keyboard shortcuts and prompt prefixes" },
7789
8578
  { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
@@ -7796,6 +8585,17 @@ var SLASH_COMMANDS = [
7796
8585
  { cmd: "apply", summary: "commit pending edit blocks to disk", contextual: "code" },
7797
8586
  { cmd: "discard", summary: "drop pending edit blocks without writing", contextual: "code" },
7798
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
+ },
7799
8599
  {
7800
8600
  cmd: "commit",
7801
8601
  argsHint: '"msg"',
@@ -7813,6 +8613,26 @@ var SLASH_COMMANDS = [
7813
8613
  cmd: "apply-plan",
7814
8614
  summary: "force-approve a pending / in-text plan (fallback if picker was missed)",
7815
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"
7816
8636
  }
7817
8637
  ];
7818
8638
  function suggestSlashCommands(prefix, codeMode = false) {
@@ -7881,6 +8701,8 @@ function handleSlash(cmd, args, loop, ctx = {}) {
7881
8701
  " Backspace delete left; Delete delete under cursor",
7882
8702
  " Esc abort the in-flight turn",
7883
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)",
7884
8706
  "",
7885
8707
  "Prompt prefixes:",
7886
8708
  " /<name> slash command; Tab/Enter picks from the suggestion list",
@@ -7929,10 +8751,16 @@ function handleSlash(cmd, args, loop, ctx = {}) {
7929
8751
  " /retry truncate & resend your last message (fresh sample from the model)",
7930
8752
  " /apply (code mode) commit the pending edit blocks to disk",
7931
8753
  " /discard (code mode) drop pending edits without writing",
7932
- " /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)",
7933
8757
  ' /commit "msg" (code mode) git add -A && git commit -m "msg"',
7934
8758
  " /plan [on|off] (code mode) toggle read-only plan mode; writes gated behind submit_plan + your approval",
7935
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)",
7936
8764
  " /sessions list saved sessions (current is marked with \u25B8)",
7937
8765
  " /forget delete the current session from disk",
7938
8766
  " /new start fresh: drop all context + clear scrollback",
@@ -8097,7 +8925,19 @@ ${entry.text}`
8097
8925
  info: "/undo is only available inside `reasonix code` \u2014 chat mode doesn't apply edits."
8098
8926
  };
8099
8927
  }
8100
- 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) };
8101
8941
  }
8102
8942
  case "apply": {
8103
8943
  if (!ctx.codeApply) {
@@ -8137,6 +8977,81 @@ ${entry.text}`
8137
8977
  info: "\u25B8 plan mode OFF \u2014 write tools are live again. Model can still propose plans autonomously for large tasks."
8138
8978
  };
8139
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
+ }
8140
9055
  case "apply-plan":
8141
9056
  case "applyplan": {
8142
9057
  if (!ctx.setPlanMode) {
@@ -8172,11 +9087,11 @@ ${entry.text}`
8172
9087
  const { healedCount, tokensSaved, charsSaved } = loop.compact(cap);
8173
9088
  if (healedCount === 0) {
8174
9089
  return {
8175
- 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.`
8176
9091
  };
8177
9092
  }
8178
9093
  return {
8179
- 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.`
8180
9095
  };
8181
9096
  }
8182
9097
  case "sessions": {
@@ -8282,6 +9197,7 @@ ${entry.text}`
8282
9197
  const mcpLine = ` mcp ${mcpCount} server(s), ${toolCount} tool(s) in registry`;
8283
9198
  const pendingLine = pending > 0 ? ` edits ${pending} pending (/apply to commit, /discard to drop)` : "";
8284
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)" : "";
8285
9201
  const lines = [
8286
9202
  ` model ${loop.model}`,
8287
9203
  ` flags harvest=${loop.harvestEnabled ? "on" : "off"} \xB7 branch=${branchBudget > 1 ? branchBudget : "off"} \xB7 stream=${loop.stream ? "on" : "off"} \xB7 effort=${loop.reasoningEffort}`,
@@ -8291,6 +9207,7 @@ ${entry.text}`
8291
9207
  ];
8292
9208
  if (pendingLine) lines.push(pendingLine);
8293
9209
  if (planLine) lines.push(planLine);
9210
+ if (modeLine) lines.push(modeLine);
8294
9211
  return { info: lines.join("\n") };
8295
9212
  }
8296
9213
  case "model": {
@@ -8377,14 +9294,18 @@ ${entry.text}`
8377
9294
  const raw = (args[0] ?? "").toLowerCase();
8378
9295
  if (raw === "") {
8379
9296
  return {
8380
- 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)`
8381
9298
  };
8382
9299
  }
8383
9300
  if (raw !== "high" && raw !== "max") {
8384
9301
  return { info: "usage: /effort <high|max>" };
8385
9302
  }
8386
9303
  loop.configure({ reasoningEffort: raw });
8387
- return { info: `reasoning_effort \u2192 ${raw}` };
9304
+ try {
9305
+ saveReasoningEffort(raw);
9306
+ } catch {
9307
+ }
9308
+ return { info: `reasoning_effort \u2192 ${raw} (persisted)` };
8388
9309
  }
8389
9310
  default:
8390
9311
  return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
@@ -8796,6 +9717,14 @@ function gitTail(res) {
8796
9717
 
8797
9718
  // src/cli/ui/App.tsx
8798
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
+ }
8799
9728
  var PLAIN_UI = process.env.REASONIX_UI === "plain";
8800
9729
  function App({
8801
9730
  model,
@@ -8811,36 +9740,66 @@ function App({
8811
9740
  codeMode
8812
9741
  }) {
8813
9742
  const { exit } = useApp();
8814
- const [historical, setHistorical] = useState5([]);
8815
- const [streaming, setStreaming] = useState5(null);
8816
- const [input, setInput] = useState5("");
8817
- 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);
8818
9747
  const abortedThisTurn = useRef2(false);
8819
- const [ongoingTool, setOngoingTool] = useState5(null);
8820
- const [toolProgress, setToolProgress] = useState5(null);
8821
- const [subagentActivity, setSubagentActivity] = useState5(null);
8822
- const [statusLine, setStatusLine] = useState5(null);
8823
- const [balance, setBalance] = useState5(null);
8824
- const [models, setModels] = useState5(null);
8825
- 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);
8826
9755
  const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
8827
- const [hookList, setHookList] = useState5(
9756
+ const [hookList, setHookList] = useState6(
8828
9757
  () => loadHooks({ projectRoot: codeMode?.rootDir })
8829
9758
  );
8830
9759
  const hookCwd = codeMode?.rootDir ?? process.cwd();
8831
- const lastEditSnapshots = useRef2(null);
9760
+ const editHistory = useRef2([]);
9761
+ const nextHistoryId = useRef2(1);
9762
+ const currentTurnEntry = useRef2(null);
8832
9763
  const pendingEdits = useRef2([]);
8833
- const [pendingShell, setPendingShell] = useState5(null);
8834
- const [pendingPlan, setPendingPlan] = useState5(null);
8835
- const [stagedInput, setStagedInput] = useState5(null);
8836
- const [planMode, setPlanMode] = useState5(false);
8837
- 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);
8838
9797
  const promptHistory = useRef2([]);
8839
9798
  const historyCursor = useRef2(-1);
8840
9799
  const assistantIterCounter = useRef2(0);
8841
9800
  const toolHistoryRef = useRef2([]);
8842
- const [slashSelected, setSlashSelected] = useState5(0);
8843
- const [summary, setSummary] = useState5({
9801
+ const [slashSelected, setSlashSelected] = useState6(0);
9802
+ const [summary, setSummary] = useState6({
8844
9803
  turns: 0,
8845
9804
  totalCostUsd: 0,
8846
9805
  totalInputCostUsd: 0,
@@ -8864,7 +9823,7 @@ function App({
8864
9823
  transcriptRef.current?.end();
8865
9824
  };
8866
9825
  }, []);
8867
- const slashMatches = useMemo(() => {
9826
+ const slashMatches = useMemo2(() => {
8868
9827
  if (!input.startsWith("/") || input.includes(" ")) return null;
8869
9828
  return suggestSlashCommands(input.slice(1), !!codeMode);
8870
9829
  }, [input, codeMode]);
@@ -8875,8 +9834,8 @@ function App({
8875
9834
  return prev;
8876
9835
  });
8877
9836
  }, [slashMatches]);
8878
- const [atSelected, setAtSelected] = useState5(0);
8879
- const atFiles = useMemo(() => {
9837
+ const [atSelected, setAtSelected] = useState6(0);
9838
+ const atFiles = useMemo2(() => {
8880
9839
  if (!codeMode?.rootDir) return [];
8881
9840
  try {
8882
9841
  return listFilesWithStatsSync(codeMode.rootDir, { maxResults: 500 });
@@ -8892,12 +9851,12 @@ function App({
8892
9851
  list.unshift(p);
8893
9852
  if (list.length > 20) list.length = 20;
8894
9853
  }, []);
8895
- const atPicker = useMemo(() => {
9854
+ const atPicker = useMemo2(() => {
8896
9855
  if (!codeMode?.rootDir) return null;
8897
9856
  if (slashMatches !== null) return null;
8898
9857
  return detectAtPicker(input);
8899
9858
  }, [codeMode?.rootDir, input, slashMatches]);
8900
- const atMatches = useMemo(() => {
9859
+ const atMatches = useMemo2(() => {
8901
9860
  if (!atPicker) return null;
8902
9861
  return rankPickerCandidates(atFiles, atPicker.query, {
8903
9862
  limit: 40,
@@ -8919,13 +9878,13 @@ function App({
8919
9878
  },
8920
9879
  [atPicker, input]
8921
9880
  );
8922
- const [slashArgSelected, setSlashArgSelected] = useState5(0);
8923
- const slashArgContext = useMemo(() => {
9881
+ const [slashArgSelected, setSlashArgSelected] = useState6(0);
9882
+ const slashArgContext = useMemo2(() => {
8924
9883
  if (!input.startsWith("/")) return null;
8925
9884
  if (slashMatches !== null) return null;
8926
9885
  return detectSlashArgContext(input, !!codeMode);
8927
9886
  }, [input, slashMatches, codeMode]);
8928
- const slashArgMatches = useMemo(() => {
9887
+ const slashArgMatches = useMemo2(() => {
8929
9888
  if (!slashArgContext || slashArgContext.kind !== "picker") return null;
8930
9889
  const completer = slashArgContext.spec.argCompleter;
8931
9890
  const partial = slashArgContext.partial;
@@ -8982,7 +9941,7 @@ function App({
8982
9941
  );
8983
9942
  const loopRef = useRef2(null);
8984
9943
  const subagentSinkRef = useRef2({ current: null });
8985
- const loop = useMemo(() => {
9944
+ const loop = useMemo2(() => {
8986
9945
  if (loopRef.current) return loopRef.current;
8987
9946
  const client = new DeepSeekClient();
8988
9947
  if (tools && !tools.has("run_skill")) {
@@ -9021,7 +9980,11 @@ function App({
9021
9980
  branch,
9022
9981
  session,
9023
9982
  hooks: hookList,
9024
- 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()
9025
9988
  });
9026
9989
  loopRef.current = l;
9027
9990
  return l;
@@ -9161,6 +10124,7 @@ function App({
9161
10124
  const restored = loadPendingEdits(session);
9162
10125
  if (restored && restored.length > 0) {
9163
10126
  pendingEdits.current = restored;
10127
+ syncPendingCount();
9164
10128
  setHistorical((prev) => [
9165
10129
  ...prev,
9166
10130
  {
@@ -9171,14 +10135,54 @@ function App({
9171
10135
  ]);
9172
10136
  }
9173
10137
  }
9174
- }, [session, loop, codeMode]);
9175
- 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) => {
9176
10151
  if (key.escape && busy) {
9177
10152
  if (abortedThisTurn.current) return;
9178
10153
  abortedThisTurn.current = true;
10154
+ const resolve8 = editReviewResolveRef.current;
10155
+ if (resolve8) {
10156
+ editReviewResolveRef.current = null;
10157
+ setPendingEditReview(null);
10158
+ resolve8("reject");
10159
+ }
9179
10160
  loop.abort();
9180
10161
  return;
9181
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
+ }
9182
10186
  if (busy) return;
9183
10187
  if (pendingShell) return;
9184
10188
  if (atMatches && atMatches.length > 0) {
@@ -9244,16 +10248,269 @@ function App({
9244
10248
  }
9245
10249
  }
9246
10250
  });
9247
- 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(() => {
9248
10344
  if (!codeMode) return "not in code mode";
9249
- const snaps = lastEditSnapshots.current;
9250
- if (!snaps || snaps.length === 0) {
9251
- 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
+ );
9252
10358
  }
9253
- const results = restoreSnapshots(snaps, codeMode.rootDir);
9254
- lastEditSnapshots.current = null;
9255
- 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");
9256
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]);
9257
10514
  const codeApply = useCallback(() => {
9258
10515
  if (!codeMode) return "not in code mode";
9259
10516
  const blocks = pendingEdits.current;
@@ -9263,18 +10520,20 @@ function App({
9263
10520
  const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
9264
10521
  const results = applyEditBlocks(blocks, codeMode.rootDir);
9265
10522
  const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
9266
- if (anyApplied) lastEditSnapshots.current = snaps;
10523
+ if (anyApplied) recordEdit("review-apply", blocks, results, snaps);
9267
10524
  pendingEdits.current = [];
9268
10525
  clearPendingEdits(session ?? null);
10526
+ syncPendingCount();
9269
10527
  return formatEditResults(results);
9270
- }, [codeMode, session]);
10528
+ }, [codeMode, session, syncPendingCount, recordEdit]);
9271
10529
  const codeDiscard = useCallback(() => {
9272
10530
  const count = pendingEdits.current.length;
9273
10531
  if (count === 0) return "nothing pending to discard.";
9274
10532
  pendingEdits.current = [];
9275
10533
  clearPendingEdits(session ?? null);
10534
+ syncPendingCount();
9276
10535
  return `\u25B8 discarded ${count} pending edit block(s). Nothing was written to disk.`;
9277
- }, [session]);
10536
+ }, [session, syncPendingCount]);
9278
10537
  const prefixHash = loop.prefix.fingerprint;
9279
10538
  const writeTranscript = useCallback(
9280
10539
  (ev) => {
@@ -9392,6 +10651,8 @@ function App({
9392
10651
  codeUndo: codeMode ? codeUndo : void 0,
9393
10652
  codeApply: codeMode ? codeApply : void 0,
9394
10653
  codeDiscard: codeMode ? codeDiscard : void 0,
10654
+ codeHistory: codeMode ? codeHistory : void 0,
10655
+ codeShowEdit: codeMode ? codeShowEdit : void 0,
9395
10656
  codeRoot: codeMode?.rootDir,
9396
10657
  pendingEditCount: codeMode ? pendingEdits.current.length : void 0,
9397
10658
  toolHistory: () => toolHistoryRef.current,
@@ -9399,6 +10660,13 @@ function App({
9399
10660
  planMode,
9400
10661
  setPlanMode: codeMode ? togglePlanMode : void 0,
9401
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
+ ]),
9402
10670
  reloadHooks: () => {
9403
10671
  const fresh = loadHooks({ projectRoot: codeMode?.rootDir });
9404
10672
  setHookList(fresh);
@@ -9435,6 +10703,7 @@ function App({
9435
10703
  if (codeMode) {
9436
10704
  pendingEdits.current = [];
9437
10705
  clearPendingEdits(session ?? null);
10706
+ syncPendingCount();
9438
10707
  }
9439
10708
  return;
9440
10709
  }
@@ -9443,6 +10712,7 @@ function App({
9443
10712
  if (codeMode) {
9444
10713
  pendingEdits.current = [];
9445
10714
  clearPendingEdits(session ?? null);
10715
+ syncPendingCount();
9446
10716
  }
9447
10717
  return;
9448
10718
  }
@@ -9505,6 +10775,10 @@ function App({
9505
10775
  setStreaming({ id: assistantId, role: "assistant", text: "", streaming: true });
9506
10776
  setBusy(true);
9507
10777
  abortedThisTurn.current = false;
10778
+ if (codeMode) {
10779
+ currentTurnEntry.current = null;
10780
+ }
10781
+ turnEditPolicyRef.current = "ask";
9508
10782
  const flush = () => {
9509
10783
  if (!contentBuf.current && !reasoningBuf.current && !toolCallBuildBuf.current) return;
9510
10784
  streamRef.text += contentBuf.current;
@@ -9618,16 +10892,37 @@ function App({
9618
10892
  if (codeMode && finalText && !ev.forcedSummary) {
9619
10893
  const blocks = parseEditBlocks(finalText);
9620
10894
  if (blocks.length > 0) {
9621
- pendingEdits.current = blocks;
9622
- savePendingEdits(session ?? null, blocks);
9623
- setHistorical((prev) => [
9624
- ...prev,
9625
- {
9626
- id: `pending-${Date.now()}`,
9627
- role: "info",
9628
- 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);
9629
10904
  }
9630
- ]);
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
+ }
9631
10926
  }
9632
10927
  }
9633
10928
  } else if (ev.role === "tool_start") {
@@ -9663,11 +10958,14 @@ function App({
9663
10958
  toolName: ev.toolName
9664
10959
  }
9665
10960
  ]);
9666
- 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) {
9667
10962
  try {
9668
10963
  const parsed = JSON.parse(ev.toolArgs);
9669
10964
  if (typeof parsed.command === "string" && parsed.command.trim()) {
9670
- setPendingShell(parsed.command.trim());
10965
+ setPendingShell({
10966
+ command: parsed.command.trim(),
10967
+ kind: ev.toolName
10968
+ });
9671
10969
  }
9672
10970
  } catch {
9673
10971
  }
@@ -9738,7 +11036,9 @@ function App({
9738
11036
  clearPendingPlan,
9739
11037
  codeApply,
9740
11038
  codeDiscard,
11039
+ codeHistory,
9741
11040
  codeMode,
11041
+ codeShowEdit,
9742
11042
  codeUndo,
9743
11043
  exit,
9744
11044
  hookCwd,
@@ -9761,13 +11061,18 @@ function App({
9761
11061
  slashArgSelected,
9762
11062
  pickSlashArg,
9763
11063
  togglePlanMode,
9764
- writeTranscript
11064
+ writeTranscript,
11065
+ recordEdit,
11066
+ armUndoBanner,
11067
+ editMode,
11068
+ syncPendingCount
9765
11069
  ]
9766
11070
  );
9767
11071
  const handleShellConfirm = useCallback(
9768
11072
  async (choice) => {
9769
- const cmd = pendingShell;
9770
- if (!cmd || !codeMode) return;
11073
+ const pending = pendingShell;
11074
+ if (!pending || !codeMode) return;
11075
+ const { command: cmd, kind } = pending;
9771
11076
  setPendingShell(null);
9772
11077
  let synthetic;
9773
11078
  if (choice === "deny") {
@@ -9791,23 +11096,65 @@ function App({
9791
11096
  }
9792
11097
  setHistorical((prev) => [
9793
11098
  ...prev,
9794
- { 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
+ }
9795
11104
  ]);
9796
- let body;
9797
- try {
9798
- const res = await runCommand(cmd, { cwd: codeMode.rootDir });
9799
- body = formatCommandResult(cmd, res);
9800
- } catch (err) {
9801
- 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}
9802
11148
  [failed to spawn] ${err.message}`;
9803
- }
9804
- setHistorical((prev) => [
9805
- ...prev,
9806
- { id: `sh-out-${Date.now()}`, role: "info", text: body }
9807
- ]);
9808
- 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:
9809
11155
 
9810
11156
  ${body}`;
11157
+ }
9811
11158
  }
9812
11159
  if (busy) {
9813
11160
  loop.abort();
@@ -9918,7 +11265,7 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
9918
11265
  if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
9919
11266
  setStagedInput(null);
9920
11267
  }, [stagedInput]);
9921
- 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(
9922
11269
  StatsPanel,
9923
11270
  {
9924
11271
  summary,
@@ -9928,32 +11275,56 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
9928
11275
  branchBudget: loop.branchOptions.budget,
9929
11276
  reasoningEffort: loop.reasoningEffort,
9930
11277
  planMode,
11278
+ editMode: codeMode ? editMode : void 0,
9931
11279
  balance,
9932
11280
  busy,
9933
11281
  updateAvailable
9934
11282
  }
9935
- ), /* @__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(
9936
11284
  PlanRefineInput,
9937
11285
  {
9938
11286
  mode: stagedInput.mode,
9939
11287
  onSubmit: handleStagedInputSubmit,
9940
11288
  onCancel: handleStagedInputCancel
9941
11289
  }
9942
- ) : pendingPlan ? /* @__PURE__ */ React14.createElement(
11290
+ ) : pendingPlan ? /* @__PURE__ */ React15.createElement(
9943
11291
  PlanConfirm,
9944
11292
  {
9945
11293
  plan: pendingPlan,
9946
11294
  onChoose: stableHandlePlanConfirm,
9947
11295
  projectRoot: hookCwd
9948
11296
  }
9949
- ) : pendingShell ? /* @__PURE__ */ React14.createElement(
11297
+ ) : pendingShell ? /* @__PURE__ */ React15.createElement(
9950
11298
  ShellConfirm,
9951
11299
  {
9952
- command: pendingShell,
9953
- allowPrefix: derivePrefix(pendingShell),
11300
+ command: pendingShell.command,
11301
+ allowPrefix: derivePrefix(pendingShell.command),
11302
+ kind: pendingShell.kind,
9954
11303
  onChoose: handleShellConfirm
9955
11304
  }
9956
- ) : /* @__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(
9957
11328
  PromptInput,
9958
11329
  {
9959
11330
  value: input,
@@ -9961,14 +11332,14 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
9961
11332
  onSubmit: handleSubmit,
9962
11333
  disabled: busy
9963
11334
  }
9964
- ), /* @__PURE__ */ React14.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }), /* @__PURE__ */ React14.createElement(
11335
+ ), /* @__PURE__ */ React15.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }), /* @__PURE__ */ React15.createElement(
9965
11336
  AtMentionSuggestions,
9966
11337
  {
9967
11338
  matches: atMatches,
9968
11339
  selectedIndex: atSelected,
9969
11340
  query: atPicker?.query ?? ""
9970
11341
  }
9971
- ), slashArgContext ? /* @__PURE__ */ React14.createElement(
11342
+ ), slashArgContext ? /* @__PURE__ */ React15.createElement(
9972
11343
  SlashArgPicker,
9973
11344
  {
9974
11345
  matches: slashArgMatches,
@@ -9983,14 +11354,44 @@ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834"
9983
11354
  function StatusRow({ text }) {
9984
11355
  const tick = useTick();
9985
11356
  const elapsed = useElapsedSeconds();
9986
- 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 }, ")"));
9987
11388
  }
9988
11389
  function SubagentRow({
9989
11390
  activity
9990
11391
  }) {
9991
11392
  const tick = useTick();
9992
11393
  const seconds = (activity.elapsedMs / 1e3).toFixed(1);
9993
- 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`));
9994
11395
  }
9995
11396
  function OngoingToolRow({
9996
11397
  tool,
@@ -9999,7 +11400,7 @@ function OngoingToolRow({
9999
11400
  const tick = useTick();
10000
11401
  const elapsed = useElapsedSeconds();
10001
11402
  const summary = summarizeToolArgs(tool.name, tool.args);
10002
- 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);
10003
11404
  }
10004
11405
  function renderProgressLine(p) {
10005
11406
  const msg = p.message ? ` ${p.message}` : "";
@@ -10069,13 +11470,12 @@ function formatPendingPreview(blocks) {
10069
11470
  const diffLines = formatAllBlockDiffs(blocks);
10070
11471
  return [header2, ...diffLines].join("\n");
10071
11472
  }
10072
- function formatUndoResults(results) {
10073
- const lines = results.map((r) => {
11473
+ function formatUndoRows(results) {
11474
+ return results.map((r) => {
10074
11475
  const mark = r.status === "applied" ? "\u2713" : "\u2717";
10075
11476
  const detail = r.message ? ` (${r.message})` : "";
10076
11477
  return ` ${mark} ${r.path}${detail}`;
10077
11478
  });
10078
- return [`\u25B8 undo: restored ${results.length} file(s) to pre-edit state`, ...lines].join("\n");
10079
11479
  }
10080
11480
  function describeRepair(repair) {
10081
11481
  const parts = [];
@@ -10086,15 +11486,15 @@ function describeRepair(repair) {
10086
11486
  }
10087
11487
 
10088
11488
  // src/cli/ui/SessionPicker.tsx
10089
- import { Box as Box14, Text as Text14 } from "ink";
10090
- import React15 from "react";
11489
+ import { Box as Box15, Text as Text15 } from "ink";
11490
+ import React16 from "react";
10091
11491
  function SessionPicker({
10092
11492
  sessionName,
10093
11493
  messageCount,
10094
11494
  lastActive,
10095
11495
  onChoose
10096
11496
  }) {
10097
- 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(
10098
11498
  SingleSelect,
10099
11499
  {
10100
11500
  initialValue: "new",
@@ -10117,7 +11517,7 @@ function SessionPicker({
10117
11517
  ],
10118
11518
  onSubmit: (v) => onChoose(v)
10119
11519
  }
10120
- ), /* @__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")));
10121
11521
  }
10122
11522
  function relativeTime(date) {
10123
11523
  const ms = Date.now() - date.getTime();
@@ -10133,12 +11533,12 @@ function relativeTime(date) {
10133
11533
  }
10134
11534
 
10135
11535
  // src/cli/ui/Setup.tsx
10136
- 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";
10137
11537
  import TextInput from "ink-text-input";
10138
- import React16, { useState as useState6 } from "react";
11538
+ import React17, { useState as useState7 } from "react";
10139
11539
  function Setup({ onReady }) {
10140
- const [value, setValue] = useState6("");
10141
- const [error, setError] = useState6(null);
11540
+ const [value, setValue] = useState7("");
11541
+ const [error, setError] = useState7(null);
10142
11542
  const { exit } = useApp2();
10143
11543
  const handleSubmit = (raw) => {
10144
11544
  const trimmed = raw.trim();
@@ -10159,7 +11559,7 @@ function Setup({ onReady }) {
10159
11559
  }
10160
11560
  onReady(trimmed);
10161
11561
  };
10162
- 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(
10163
11563
  TextInput,
10164
11564
  {
10165
11565
  value,
@@ -10168,7 +11568,7 @@ function Setup({ onReady }) {
10168
11568
  mask: "\u2022",
10169
11569
  placeholder: "sk-..."
10170
11570
  }
10171
- )), 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.)")));
10172
11572
  }
10173
11573
 
10174
11574
  // src/cli/commands/chat.tsx
@@ -10181,10 +11581,10 @@ function Root({
10181
11581
  sessionPreview,
10182
11582
  ...appProps
10183
11583
  }) {
10184
- const [key, setKey] = useState7(initialKey);
10185
- const [pending, setPending] = useState7(sessionPreview);
11584
+ const [key, setKey] = useState8(initialKey);
11585
+ const [pending, setPending] = useState8(sessionPreview);
10186
11586
  if (!key) {
10187
- return /* @__PURE__ */ React17.createElement(
11587
+ return /* @__PURE__ */ React18.createElement(
10188
11588
  Setup,
10189
11589
  {
10190
11590
  onReady: (k) => {
@@ -10196,7 +11596,7 @@ function Root({
10196
11596
  }
10197
11597
  process.env.DEEPSEEK_API_KEY = key;
10198
11598
  if (pending && appProps.session) {
10199
- return /* @__PURE__ */ React17.createElement(
11599
+ return /* @__PURE__ */ React18.createElement(
10200
11600
  SessionPicker,
10201
11601
  {
10202
11602
  sessionName: appProps.session,
@@ -10211,7 +11611,7 @@ function Root({
10211
11611
  }
10212
11612
  );
10213
11613
  }
10214
- return /* @__PURE__ */ React17.createElement(
11614
+ return /* @__PURE__ */ React18.createElement(
10215
11615
  App,
10216
11616
  {
10217
11617
  model: appProps.model,
@@ -10315,7 +11715,7 @@ async function chatCommand(opts) {
10315
11715
  rewriteSession(opts.session, []);
10316
11716
  }
10317
11717
  const { waitUntilExit } = render(
10318
- /* @__PURE__ */ React17.createElement(
11718
+ /* @__PURE__ */ React18.createElement(
10319
11719
  Root,
10320
11720
  {
10321
11721
  initialKey,
@@ -10339,13 +11739,14 @@ async function chatCommand(opts) {
10339
11739
  }
10340
11740
 
10341
11741
  // src/cli/commands/code.tsx
10342
- import { basename, resolve as resolve6 } from "path";
11742
+ import { basename, resolve as resolve7 } from "path";
10343
11743
  async function codeCommand(opts = {}) {
10344
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-75XLIUTO.js");
10345
- const rootDir = resolve6(opts.dir ?? process.cwd());
11744
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-OVVMCH5F.js");
11745
+ const rootDir = resolve7(opts.dir ?? process.cwd());
10346
11746
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
10347
11747
  const tools = new ToolRegistry();
10348
11748
  registerFilesystemTools(tools, { rootDir });
11749
+ const jobs = new JobRegistry();
10349
11750
  registerShellTools(tools, {
10350
11751
  rootDir,
10351
11752
  // Per-project "always allow" list persisted from prior ShellConfirm
@@ -10353,7 +11754,8 @@ async function codeCommand(opts = {}) {
10353
11754
  // GETTER form — re-read every dispatch so a prefix the user adds
10354
11755
  // via ShellConfirm mid-session takes effect on the next shell call
10355
11756
  // instead of waiting for `/new` or a relaunch.
10356
- extraAllowed: () => loadProjectShellAllowed(rootDir)
11757
+ extraAllowed: () => loadProjectShellAllowed(rootDir),
11758
+ jobs
10357
11759
  });
10358
11760
  registerPlanTool(tools);
10359
11761
  registerMemoryTools(tools, { projectRoot: rootDir });
@@ -10361,6 +11763,12 @@ async function codeCommand(opts = {}) {
10361
11763
  `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
10362
11764
  `
10363
11765
  );
11766
+ const sigShutdown = () => {
11767
+ void jobs.shutdown();
11768
+ };
11769
+ process.once("SIGINT", sigShutdown);
11770
+ process.once("SIGTERM", sigShutdown);
11771
+ process.once("exit", sigShutdown);
10364
11772
  await chatCommand({
10365
11773
  model: opts.model ?? "deepseek-v4-pro",
10366
11774
  harvest: opts.harvest ?? false,
@@ -10368,7 +11776,7 @@ async function codeCommand(opts = {}) {
10368
11776
  transcript: opts.transcript,
10369
11777
  session,
10370
11778
  seedTools: tools,
10371
- codeMode: { rootDir },
11779
+ codeMode: { rootDir, jobs },
10372
11780
  forceResume: opts.forceResume,
10373
11781
  forceNew: opts.forceNew
10374
11782
  });
@@ -10378,34 +11786,34 @@ async function codeCommand(opts = {}) {
10378
11786
  import { writeFileSync as writeFileSync6 } from "fs";
10379
11787
  import { basename as basename2 } from "path";
10380
11788
  import { render as render2 } from "ink";
10381
- import React20 from "react";
11789
+ import React21 from "react";
10382
11790
 
10383
11791
  // src/cli/ui/DiffApp.tsx
10384
- import { Box as Box17, Static as Static2, Text as Text17, useApp as useApp3, useInput as useInput5 } from "ink";
10385
- 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";
10386
11794
 
10387
11795
  // src/cli/ui/RecordView.tsx
10388
- import { Box as Box16, Text as Text16 } from "ink";
10389
- import React18 from "react";
11796
+ import { Box as Box17, Text as Text17 } from "ink";
11797
+ import React19 from "react";
10390
11798
  function RecordView({ rec, compact = false }) {
10391
11799
  const toolArgsMax = compact ? 120 : 200;
10392
11800
  const toolContentMax = compact ? 200 : 400;
10393
11801
  if (rec.role === "user") {
10394
- 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));
10395
11803
  }
10396
11804
  if (rec.role === "assistant_final") {
10397
- 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)"));
10398
11806
  }
10399
11807
  if (rec.role === "tool") {
10400
- 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)));
10401
11809
  }
10402
11810
  if (rec.role === "error") {
10403
- 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));
10404
11812
  }
10405
11813
  if (rec.role === "done" || rec.role === "assistant_delta") {
10406
11814
  return null;
10407
11815
  }
10408
- 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));
10409
11817
  }
10410
11818
  function CacheBadge({ usage }) {
10411
11819
  const hit = usage.prompt_cache_hit_tokens ?? 0;
@@ -10414,7 +11822,7 @@ function CacheBadge({ usage }) {
10414
11822
  if (total === 0) return null;
10415
11823
  const pct2 = hit / total * 100;
10416
11824
  const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
10417
- 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), "%"));
10418
11826
  }
10419
11827
  function truncate3(s, max) {
10420
11828
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
@@ -10425,8 +11833,8 @@ function DiffApp({ report }) {
10425
11833
  const { exit } = useApp3();
10426
11834
  const maxIdx = Math.max(0, report.pairs.length - 1);
10427
11835
  const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
10428
- const [idx, setIdx] = useState8(Math.max(0, initialIdx));
10429
- useInput5((input, key) => {
11836
+ const [idx, setIdx] = useState9(Math.max(0, initialIdx));
11837
+ useInput6((input, key) => {
10430
11838
  if (input === "q" || key.ctrl && input === "c") {
10431
11839
  exit();
10432
11840
  return;
@@ -10448,7 +11856,7 @@ function DiffApp({ report }) {
10448
11856
  }
10449
11857
  });
10450
11858
  const pair = report.pairs[idx];
10451
- 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")));
10452
11860
  }
10453
11861
  function DiffHeader({ report }) {
10454
11862
  const a = report.a;
@@ -10466,15 +11874,15 @@ function DiffHeader({ report }) {
10466
11874
  } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
10467
11875
  prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
10468
11876
  }
10469
- 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);
10470
11878
  }
10471
11879
  function Pane({
10472
11880
  label,
10473
11881
  headerColor,
10474
11882
  records
10475
11883
  }) {
10476
- return /* @__PURE__ */ React19.createElement(
10477
- Box17,
11884
+ return /* @__PURE__ */ React20.createElement(
11885
+ Box18,
10478
11886
  {
10479
11887
  flexDirection: "column",
10480
11888
  flexGrow: 1,
@@ -10482,21 +11890,21 @@ function Pane({
10482
11890
  borderStyle: "single",
10483
11891
  borderColor: headerColor
10484
11892
  },
10485
- /* @__PURE__ */ React19.createElement(Text17, { color: headerColor, bold: true }, label),
10486
- 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 }))
10487
11895
  );
10488
11896
  }
10489
11897
  function KindBadge({ kind }) {
10490
11898
  if (kind === "match") {
10491
- return /* @__PURE__ */ React19.createElement(Text17, { color: "green" }, "\u2713 match");
11899
+ return /* @__PURE__ */ React20.createElement(Text18, { color: "green" }, "\u2713 match");
10492
11900
  }
10493
11901
  if (kind === "diverge") {
10494
- return /* @__PURE__ */ React19.createElement(Text17, { color: "yellow" }, "\u2605 diverge");
11902
+ return /* @__PURE__ */ React20.createElement(Text18, { color: "yellow" }, "\u2605 diverge");
10495
11903
  }
10496
11904
  if (kind === "only_in_a") {
10497
- return /* @__PURE__ */ React19.createElement(Text17, { color: "blue" }, "\u2190 only in A");
11905
+ return /* @__PURE__ */ React20.createElement(Text18, { color: "blue" }, "\u2190 only in A");
10498
11906
  }
10499
- return /* @__PURE__ */ React19.createElement(Text17, { color: "magenta" }, "\u2192 only in B");
11907
+ return /* @__PURE__ */ React20.createElement(Text18, { color: "magenta" }, "\u2192 only in B");
10500
11908
  }
10501
11909
  function paneRecords(pair, side) {
10502
11910
  if (!pair) return [];
@@ -10527,7 +11935,7 @@ markdown report written to ${opts.mdPath}`);
10527
11935
  return;
10528
11936
  }
10529
11937
  if (wantTui) {
10530
- const { waitUntilExit } = render2(React20.createElement(DiffApp, { report }), {
11938
+ const { waitUntilExit } = render2(React21.createElement(DiffApp, { report }), {
10531
11939
  exitOnCtrlC: true,
10532
11940
  patchConsole: false
10533
11941
  });
@@ -10668,16 +12076,16 @@ function pad2(s, width) {
10668
12076
 
10669
12077
  // src/cli/commands/replay.ts
10670
12078
  import { render as render3 } from "ink";
10671
- import React22 from "react";
12079
+ import React23 from "react";
10672
12080
 
10673
12081
  // src/cli/ui/ReplayApp.tsx
10674
- import { Box as Box18, Static as Static3, Text as Text18, useApp as useApp4, useInput as useInput6 } from "ink";
10675
- 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";
10676
12084
  function ReplayApp({ meta, pages }) {
10677
12085
  const { exit } = useApp4();
10678
12086
  const maxIdx = Math.max(0, pages.length - 1);
10679
- const [idx, setIdx] = useState9(maxIdx);
10680
- useInput6((input, key) => {
12087
+ const [idx, setIdx] = useState10(maxIdx);
12088
+ useInput7((input, key) => {
10681
12089
  if (input === "q" || key.ctrl && input === "c") {
10682
12090
  exit();
10683
12091
  return;
@@ -10696,7 +12104,7 @@ function ReplayApp({ meta, pages }) {
10696
12104
  setIdx(maxIdx);
10697
12105
  }
10698
12106
  });
10699
- const cumStats = useMemo2(() => computeCumulativeStats(pages, idx), [pages, idx]);
12107
+ const cumStats = useMemo3(() => computeCumulativeStats(pages, idx), [pages, idx]);
10700
12108
  const summary = {
10701
12109
  turns: cumStats.turns,
10702
12110
  totalCostUsd: cumStats.totalCostUsd,
@@ -10711,14 +12119,14 @@ function ReplayApp({ meta, pages }) {
10711
12119
  const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
10712
12120
  const currentPage = pages[idx];
10713
12121
  const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
10714
- return /* @__PURE__ */ React21.createElement(Box18, { flexDirection: "column" }, /* @__PURE__ */ React21.createElement(
12122
+ return /* @__PURE__ */ React22.createElement(Box19, { flexDirection: "column" }, /* @__PURE__ */ React22.createElement(
10715
12123
  StatsPanel,
10716
12124
  {
10717
12125
  summary,
10718
12126
  model: cumStats.models[0] ?? meta?.model ?? "?",
10719
12127
  prefixHash
10720
12128
  }
10721
- ), /* @__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")));
10722
12130
  }
10723
12131
 
10724
12132
  // src/cli/commands/replay.ts
@@ -10730,7 +12138,7 @@ async function replayCommand(opts) {
10730
12138
  }
10731
12139
  const { parsed } = replayFromFile(opts.path);
10732
12140
  const pages = groupRecordsByTurn(parsed.records);
10733
- const { waitUntilExit } = render3(React22.createElement(ReplayApp, { meta: parsed.meta, pages }), {
12141
+ const { waitUntilExit } = render3(React23.createElement(ReplayApp, { meta: parsed.meta, pages }), {
10734
12142
  exitOnCtrlC: true,
10735
12143
  patchConsole: false
10736
12144
  });
@@ -11035,12 +12443,12 @@ function truncate4(s, max) {
11035
12443
 
11036
12444
  // src/cli/commands/setup.tsx
11037
12445
  import { render as render4 } from "ink";
11038
- import React24 from "react";
12446
+ import React25 from "react";
11039
12447
 
11040
12448
  // src/cli/ui/Wizard.tsx
11041
- 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";
11042
12450
  import TextInput2 from "ink-text-input";
11043
- import React23, { useState as useState10 } from "react";
12451
+ import React24, { useState as useState11 } from "react";
11044
12452
 
11045
12453
  // src/cli/ui/presets.ts
11046
12454
  var PRESETS = {
@@ -11072,19 +12480,19 @@ var PRESET_DESCRIPTIONS = {
11072
12480
  var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
11073
12481
  function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11074
12482
  const { exit } = useApp5();
11075
- const [step, setStep] = useState10(existingApiKey ? "preset" : "apiKey");
11076
- const [data, setData] = useState10({
12483
+ const [step, setStep] = useState11(existingApiKey ? "preset" : "apiKey");
12484
+ const [data, setData] = useState11({
11077
12485
  apiKey: existingApiKey ?? "",
11078
12486
  preset: initial?.preset ?? "fast",
11079
12487
  selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
11080
12488
  catalogArgs: {}
11081
12489
  });
11082
- const [error, setError] = useState10(null);
11083
- useInput7((_input, key) => {
12490
+ const [error, setError] = useState11(null);
12491
+ useInput8((_input, key) => {
11084
12492
  if (key.escape && step !== "saved" && onCancel) onCancel();
11085
12493
  });
11086
12494
  if (step === "apiKey") {
11087
- return /* @__PURE__ */ React23.createElement(
12495
+ return /* @__PURE__ */ React24.createElement(
11088
12496
  ApiKeyStep,
11089
12497
  {
11090
12498
  onSubmit: (key) => {
@@ -11098,7 +12506,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11098
12506
  );
11099
12507
  }
11100
12508
  if (step === "preset") {
11101
- 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(
11102
12510
  SingleSelect,
11103
12511
  {
11104
12512
  items: presetItems(),
@@ -11108,10 +12516,10 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11108
12516
  setStep("mcp");
11109
12517
  }
11110
12518
  }
11111
- ), /* @__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")));
11112
12520
  }
11113
12521
  if (step === "mcp") {
11114
- 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(
11115
12523
  MultiSelect,
11116
12524
  {
11117
12525
  items: mcpItems(),
@@ -11136,7 +12544,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11136
12544
  }
11137
12545
  const currentName = pending[0];
11138
12546
  const entry = CATALOG_BY_NAME.get(currentName);
11139
- return /* @__PURE__ */ React23.createElement(
12547
+ return /* @__PURE__ */ React24.createElement(
11140
12548
  McpArgsStep,
11141
12549
  {
11142
12550
  entry,
@@ -11154,7 +12562,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11154
12562
  }
11155
12563
  if (step === "review") {
11156
12564
  const specs = data.selectedCatalog.map((name) => buildSpec(name, data.catalogArgs));
11157
- 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(
11158
12566
  SummaryLine,
11159
12567
  {
11160
12568
  label: "MCP",
@@ -11162,8 +12570,8 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11162
12570
  }
11163
12571
  ), specs.map((spec, i) => (
11164
12572
  // biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
11165
- /* @__PURE__ */ React23.createElement(Box19, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, "\xB7 ", spec))
11166
- )), /* @__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(
11167
12575
  ReviewConfirm,
11168
12576
  {
11169
12577
  onConfirm: () => {
@@ -11189,15 +12597,15 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
11189
12597
  }
11190
12598
  ));
11191
12599
  }
11192
- 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 }));
11193
12601
  }
11194
12602
  function ApiKeyStep({
11195
12603
  onSubmit,
11196
12604
  error,
11197
12605
  onError
11198
12606
  }) {
11199
- const [value, setValue] = useState10("");
11200
- 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(
11201
12609
  TextInput2,
11202
12610
  {
11203
12611
  value,
@@ -11214,7 +12622,7 @@ function ApiKeyStep({
11214
12622
  mask: "\u2022",
11215
12623
  placeholder: "sk-..."
11216
12624
  }
11217
- )), 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);
11218
12626
  }
11219
12627
  function McpArgsStep({
11220
12628
  entry,
@@ -11222,8 +12630,8 @@ function McpArgsStep({
11222
12630
  onSubmit,
11223
12631
  onError
11224
12632
  }) {
11225
- const [value, setValue] = useState10("");
11226
- 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(
11227
12635
  TextInput2,
11228
12636
  {
11229
12637
  value,
@@ -11239,16 +12647,16 @@ function McpArgsStep({
11239
12647
  },
11240
12648
  placeholder: placeholderFor(entry)
11241
12649
  }
11242
- )), 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));
11243
12651
  }
11244
12652
  function ReviewConfirm({ onConfirm }) {
11245
- useInput7((_i, key) => {
12653
+ useInput8((_i, key) => {
11246
12654
  if (key.return) onConfirm();
11247
12655
  });
11248
12656
  return null;
11249
12657
  }
11250
12658
  function ExitOnEnter({ onExit }) {
11251
- useInput7((_i, key) => {
12659
+ useInput8((_i, key) => {
11252
12660
  if (key.return) onExit();
11253
12661
  });
11254
12662
  return null;
@@ -11259,10 +12667,10 @@ function StepFrame({
11259
12667
  total,
11260
12668
  children
11261
12669
  }) {
11262
- 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));
11263
12671
  }
11264
12672
  function SummaryLine({ label, value }) {
11265
- 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));
11266
12674
  }
11267
12675
  function presetItems() {
11268
12676
  return ["fast", "smart", "max"].map((name) => ({
@@ -11318,7 +12726,7 @@ async function setupCommand(_opts = {}) {
11318
12726
  const existingKey = loadApiKey();
11319
12727
  const existing = readConfig();
11320
12728
  const { waitUntilExit, unmount } = render4(
11321
- /* @__PURE__ */ React24.createElement(
12729
+ /* @__PURE__ */ React25.createElement(
11322
12730
  Wizard,
11323
12731
  {
11324
12732
  existingApiKey: existingKey,
@@ -11336,7 +12744,7 @@ async function setupCommand(_opts = {}) {
11336
12744
  }
11337
12745
 
11338
12746
  // src/cli/commands/update.ts
11339
- import { spawn as spawn4 } from "child_process";
12747
+ import { spawn as spawn5 } from "child_process";
11340
12748
  function planUpdate(input) {
11341
12749
  const diff = compareVersions(input.current, input.latest);
11342
12750
  if (diff > 0) {
@@ -11366,13 +12774,13 @@ function planUpdate(input) {
11366
12774
  };
11367
12775
  }
11368
12776
  function defaultSpawn(argv) {
11369
- return new Promise((resolve7, reject) => {
11370
- const child = spawn4(argv[0], argv.slice(1), {
12777
+ return new Promise((resolve8, reject) => {
12778
+ const child = spawn5(argv[0], argv.slice(1), {
11371
12779
  stdio: "inherit",
11372
12780
  shell: process.platform === "win32"
11373
12781
  });
11374
12782
  child.once("error", reject);
11375
- child.once("exit", (code) => resolve7(code ?? 1));
12783
+ child.once("exit", (code) => resolve8(code ?? 1));
11376
12784
  });
11377
12785
  }
11378
12786
  async function updateCommand(opts = {}) {