reasonix 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  memoryEnabled,
11
11
  readProjectMemory,
12
12
  sanitizeMemoryName
13
- } from "./chunk-NXYPGKA3.js";
13
+ } from "./chunk-5DZMZCCW.js";
14
14
 
15
15
  // src/cli/index.ts
16
16
  import { Command } from "commander";
@@ -443,7 +443,7 @@ async function harvest(reasoningContent, client, options = {}, signal) {
443
443
  const minLen = options.minReasoningLen ?? 40;
444
444
  const trimmed = reasoningContent.trim();
445
445
  if (trimmed.length < minLen) return emptyPlanState();
446
- const model2 = options.model ?? "deepseek-chat";
446
+ const model2 = options.model ?? "deepseek-v4-flash";
447
447
  const maxItems = options.maxItems ?? 5;
448
448
  const maxItemLen = options.maxItemLen ?? 80;
449
449
  const system = SYSTEM_PROMPT.replace("{maxItems}", String(maxItems)).replace(
@@ -1838,8 +1838,9 @@ var ARGS_COMPACT_THRESHOLD_TOKENS = 800;
1838
1838
  var TURN_END_RESULT_CAP_TOKENS = 3e3;
1839
1839
  var FAILURE_ESCALATION_THRESHOLD = 3;
1840
1840
  var ESCALATION_MODEL = "deepseek-v4-pro";
1841
- var NEEDS_PRO_MARKER = "<<<NEEDS_PRO>>>";
1842
- var NEEDS_PRO_BUFFER_CHARS = 80;
1841
+ var NEEDS_PRO_MARKER_PREFIX = "<<<NEEDS_PRO";
1842
+ var NEEDS_PRO_MARKER_RE = /^<<<NEEDS_PRO(?::\s*([^>]*))?>>>/;
1843
+ var NEEDS_PRO_BUFFER_CHARS = 256;
1843
1844
  var CacheFirstLoop = class {
1844
1845
  client;
1845
1846
  prefix;
@@ -1905,6 +1906,14 @@ var CacheFirstLoop = class {
1905
1906
  * the user doesn't watch flash retry the same edit 5 times.
1906
1907
  */
1907
1908
  _turnFailureCount = 0;
1909
+ /**
1910
+ * Per-type breakdown of failure signals counted toward the turn's
1911
+ * auto-escalation threshold. Surfaced in the warning when the
1912
+ * threshold trips so the user sees what kind of trouble flash
1913
+ * actually hit ("3× search-mismatch, 2× truncated") rather than
1914
+ * just a bare count. Reset alongside _turnFailureCount.
1915
+ */
1916
+ _turnFailureTypes = {};
1908
1917
  constructor(opts) {
1909
1918
  this.client = opts.client;
1910
1919
  this.prefix = opts.prefix;
@@ -1932,10 +1941,11 @@ var CacheFirstLoop = class {
1932
1941
  this.sessionName = opts.session ?? null;
1933
1942
  if (this.sessionName) {
1934
1943
  const prior = loadSessionMessages(this.sessionName);
1935
- const { messages, healedCount, tokensSaved } = healLoadedMessagesByTokens(
1936
- prior,
1937
- DEFAULT_MAX_RESULT_TOKENS
1938
- );
1944
+ const shrunk = healLoadedMessagesByTokens(prior, DEFAULT_MAX_RESULT_TOKENS);
1945
+ const stamped = stampMissingReasoningForThinkingMode(shrunk.messages, this.model);
1946
+ const messages = stamped.messages;
1947
+ const healedCount = shrunk.healedCount + stamped.stampedCount;
1948
+ const tokensSaved = shrunk.tokensSaved;
1939
1949
  for (const msg of messages) this.log.append(msg);
1940
1950
  this.resumedMessageCount = messages.length;
1941
1951
  if (healedCount > 0) {
@@ -2162,15 +2172,41 @@ var CacheFirstLoop = class {
2162
2172
  return this._escalateThisTurn ? ESCALATION_MODEL : this.model;
2163
2173
  }
2164
2174
  /**
2165
- * True when the assistant's content is a self-reported escalation
2166
- * request. Only the FIRST line matters the model is instructed
2167
- * to emit the marker as the first output token if at all. Matching
2168
- * anywhere else in the text is a normal content reference (e.g.
2169
- * the user asked about the marker itself, or prose that happens
2170
- * to contain angle-brackets).
2175
+ * Parse the escalation marker out of the model's leading content.
2176
+ * Returns `{ matched: true, reason? }` for both bare and reason-
2177
+ * carrying forms. Only the FIRST line matters the model is
2178
+ * instructed to emit the marker as the first output token if at
2179
+ * all. Matches anywhere else in the text are normal content
2180
+ * references (e.g. the user asked about the marker itself).
2171
2181
  */
2182
+ parseEscalationMarker(content) {
2183
+ const m = NEEDS_PRO_MARKER_RE.exec(content.trimStart());
2184
+ if (!m) return { matched: false };
2185
+ const reason = m[1]?.trim();
2186
+ return { matched: true, reason: reason || void 0 };
2187
+ }
2188
+ /** Convenience boolean — same gate the streaming path used to call. */
2172
2189
  isEscalationRequest(content) {
2173
- return content.trimStart().startsWith(NEEDS_PRO_MARKER);
2190
+ return this.parseEscalationMarker(content).matched;
2191
+ }
2192
+ /**
2193
+ * Could `buf` STILL plausibly become the full marker as more chunks
2194
+ * arrive? Drives the streaming buffer's flush decision: while this
2195
+ * is true we keep accumulating; once it's false (or the buffer
2196
+ * exceeds the byte limit) we flush so the user isn't staring at a
2197
+ * delayed display for arbitrary content that just happens to start
2198
+ * with `<`.
2199
+ */
2200
+ looksLikePartialEscalationMarker(buf) {
2201
+ const t = buf.trimStart();
2202
+ if (t.length === 0) return true;
2203
+ if (t.length <= NEEDS_PRO_MARKER_PREFIX.length) {
2204
+ return NEEDS_PRO_MARKER_PREFIX.startsWith(t);
2205
+ }
2206
+ if (!t.startsWith(NEEDS_PRO_MARKER_PREFIX)) return false;
2207
+ const rest = t.slice(NEEDS_PRO_MARKER_PREFIX.length);
2208
+ if (rest[0] !== ">" && rest[0] !== ":") return false;
2209
+ return true;
2174
2210
  }
2175
2211
  /**
2176
2212
  * Check whether a tool result string looks like a "flash struggled"
@@ -2184,16 +2220,18 @@ var CacheFirstLoop = class {
2184
2220
  */
2185
2221
  noteToolFailureSignal(resultJson, repair) {
2186
2222
  let bumped = false;
2187
- if (resultJson.includes('"error"') && resultJson.includes("search text not found")) {
2188
- this._turnFailureCount += 1;
2223
+ const bump = (kind, by = 1) => {
2224
+ this._turnFailureCount += by;
2225
+ this._turnFailureTypes[kind] = (this._turnFailureTypes[kind] ?? 0) + by;
2189
2226
  bumped = true;
2227
+ };
2228
+ if (resultJson.includes('"error"') && resultJson.includes("search text not found")) {
2229
+ bump("search-mismatch");
2190
2230
  }
2191
2231
  if (repair) {
2192
- const repairs = repair.scavenged + repair.truncationsFixed + repair.stormsBroken;
2193
- if (repairs > 0) {
2194
- this._turnFailureCount += repairs;
2195
- bumped = true;
2196
- }
2232
+ if (repair.scavenged > 0) bump("scavenged", repair.scavenged);
2233
+ if (repair.truncationsFixed > 0) bump("truncated", repair.truncationsFixed);
2234
+ if (repair.stormsBroken > 0) bump("storm-broken", repair.stormsBroken);
2197
2235
  }
2198
2236
  if (bumped && !this._escalateThisTurn && this._turnFailureCount >= FAILURE_ESCALATION_THRESHOLD) {
2199
2237
  this._escalateThisTurn = true;
@@ -2201,6 +2239,16 @@ var CacheFirstLoop = class {
2201
2239
  }
2202
2240
  return false;
2203
2241
  }
2242
+ /**
2243
+ * Render `_turnFailureTypes` as a comma-separated breakdown like
2244
+ * "2× search-mismatch, 1× truncated" for the auto-escalation
2245
+ * warning. Empty if no types have been recorded yet (defensive —
2246
+ * the warning sites only call this after a bump).
2247
+ */
2248
+ formatFailureBreakdown() {
2249
+ const parts = Object.entries(this._turnFailureTypes).filter(([, n]) => n > 0).map(([kind, n]) => `${n}\xD7 ${kind}`);
2250
+ return parts.length > 0 ? parts.join(", ") : `${this._turnFailureCount} repair/error signal(s)`;
2251
+ }
2204
2252
  buildMessages(pendingUser) {
2205
2253
  const healed = healLoadedMessages(this.log.toMessages(), DEFAULT_MAX_RESULT_CHARS);
2206
2254
  const msgs = [...this.prefix.toMessages(), ...healed.messages];
@@ -2256,6 +2304,7 @@ var CacheFirstLoop = class {
2256
2304
  this.scratch.reset();
2257
2305
  this.repair.resetStorm();
2258
2306
  this._turnFailureCount = 0;
2307
+ this._turnFailureTypes = {};
2259
2308
  this._escalateThisTurn = false;
2260
2309
  let armedConsumed = false;
2261
2310
  if (this._proArmedForNextTurn) {
@@ -2444,7 +2493,7 @@ var CacheFirstLoop = class {
2444
2493
  if (this.isEscalationRequest(escalationBuf)) {
2445
2494
  break;
2446
2495
  }
2447
- if (escalationBuf.length >= NEEDS_PRO_BUFFER_CHARS || escalationBuf.includes("\n")) {
2496
+ if (escalationBuf.length >= NEEDS_PRO_BUFFER_CHARS || !this.looksLikePartialEscalationMarker(escalationBuf)) {
2448
2497
  escalationBufFlushed = true;
2449
2498
  yield {
2450
2499
  turn: this._turn,
@@ -2539,11 +2588,13 @@ var CacheFirstLoop = class {
2539
2588
  return;
2540
2589
  }
2541
2590
  if (this.modelForCurrentCall() !== ESCALATION_MODEL && this.isEscalationRequest(assistantContent)) {
2591
+ const { reason } = this.parseEscalationMarker(assistantContent);
2542
2592
  this._escalateThisTurn = true;
2593
+ const reasonSuffix = reason ? ` \u2014 ${reason}` : "";
2543
2594
  yield {
2544
2595
  turn: this._turn,
2545
2596
  role: "warning",
2546
- content: `\u21E7 flash requested escalation \u2014 retrying this turn on ${ESCALATION_MODEL}`
2597
+ content: `\u21E7 flash requested escalation \u2014 retrying this turn on ${ESCALATION_MODEL}${reasonSuffix}`
2547
2598
  };
2548
2599
  assistantContent = "";
2549
2600
  reasoningContent = "";
@@ -2578,7 +2629,12 @@ var CacheFirstLoop = class {
2578
2629
  assistantContent || null
2579
2630
  );
2580
2631
  this.appendAndPersist(
2581
- this.assistantMessage(assistantContent, repairedCalls, reasoningContent)
2632
+ this.assistantMessage(
2633
+ assistantContent,
2634
+ repairedCalls,
2635
+ this.modelForCurrentCall(),
2636
+ reasoningContent
2637
+ )
2582
2638
  );
2583
2639
  yield {
2584
2640
  turn: this._turn,
@@ -2593,7 +2649,7 @@ var CacheFirstLoop = class {
2593
2649
  yield {
2594
2650
  turn: this._turn,
2595
2651
  role: "warning",
2596
- content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this._turnFailureCount} repair/error signals. Next turn falls back to ${this.model} unless /pro is armed.`
2652
+ content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this.formatFailureBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
2597
2653
  };
2598
2654
  }
2599
2655
  if (report.stormsBroken > 0) {
@@ -2717,7 +2773,7 @@ ${reason}`;
2717
2773
  yield {
2718
2774
  turn: this._turn,
2719
2775
  role: "warning",
2720
- content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this._turnFailureCount} edit failure(s). Next turn falls back to ${this.model} unless /pro is armed.`
2776
+ content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this.formatFailureBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
2721
2777
  };
2722
2778
  }
2723
2779
  yield {
@@ -2761,7 +2817,9 @@ ${reason}`;
2761
2817
 
2762
2818
  ${summary}`;
2763
2819
  const summaryStats = this.stats.record(this._turn, summaryModel, resp.usage ?? new Usage());
2764
- this.appendAndPersist(this.assistantMessage(summary, [], resp.reasoningContent ?? void 0));
2820
+ this.appendAndPersist(
2821
+ this.assistantMessage(summary, [], summaryModel, resp.reasoningContent)
2822
+ );
2765
2823
  yield {
2766
2824
  turn: this._turn,
2767
2825
  role: "assistant_final",
@@ -2792,28 +2850,39 @@ ${summary}`;
2792
2850
  }
2793
2851
  return final;
2794
2852
  }
2795
- assistantMessage(content, toolCalls, reasoningContent) {
2853
+ /**
2854
+ * Build an assistant message for the log. The `producingModel` arg is
2855
+ * the model that actually generated this turn (flash, pro, the
2856
+ * forced-summary flash call, `this.model` for synthetics, etc.) —
2857
+ * NOT `this.model`, because escalation + forced-summary can both
2858
+ * route a single turn to a different model.
2859
+ *
2860
+ * The single invariant this encodes: if the producing model is
2861
+ * thinking-mode, `reasoning_content` MUST be present on the
2862
+ * persisted message — even as an empty string. DeepSeek's validator
2863
+ * 400s the NEXT request if any historical thinking-mode assistant
2864
+ * turn is missing it. We used to gate on `reasoning.length > 0`,
2865
+ * which silently dropped the field whenever the stream emitted zero
2866
+ * reasoning deltas or the API returned `reasoning_content: null` —
2867
+ * both legitimate edge cases the 0.5.15/0.5.18 fixes missed.
2868
+ */
2869
+ assistantMessage(content, toolCalls, producingModel, reasoningContent) {
2796
2870
  const msg = { role: "assistant", content };
2797
2871
  if (toolCalls.length > 0) msg.tool_calls = toolCalls;
2798
- if (reasoningContent && reasoningContent.length > 0) {
2799
- msg.reasoning_content = reasoningContent;
2872
+ if (isThinkingModeModel(producingModel)) {
2873
+ msg.reasoning_content = reasoningContent ?? "";
2800
2874
  }
2801
2875
  return msg;
2802
2876
  }
2803
2877
  /**
2804
- * Build a synthetic assistant message we insert into the log without
2805
- * a real API round trip (abort notices, future system injections).
2806
- * Reasoner models reject follow-up requests whose assistant history
2807
- * is missing `reasoning_content`, so we stamp an empty-string
2808
- * placeholder on reasoner sessions to satisfy the validator. V3
2809
- * doesn't care — field stays absent there.
2878
+ * Synthetic assistant message (abort notices, future system injections)
2879
+ * no real API round trip. Delegates to {@link assistantMessage} with
2880
+ * `this.model` as the stand-in producer, so the same thinking-mode
2881
+ * invariant applies: reasoner sessions get an empty-string
2882
+ * `reasoning_content`; V3 sessions get nothing.
2810
2883
  */
2811
2884
  syntheticAssistantMessage(content) {
2812
- const msg = { role: "assistant", content };
2813
- if (isThinkingModeModel(this.model)) {
2814
- msg.reasoning_content = "";
2815
- }
2816
- return msg;
2885
+ return this.assistantMessage(content, [], this.model, "");
2817
2886
  }
2818
2887
  };
2819
2888
  function isThinkingModeModel(model2) {
@@ -3006,6 +3075,19 @@ function healLoadedMessages(messages, maxChars) {
3006
3075
  const healedCount = shrunk.healedCount + paired.droppedAssistantCalls + paired.droppedStrayTools;
3007
3076
  return { messages: paired.messages, healedCount, healedFrom: shrunk.healedFrom };
3008
3077
  }
3078
+ function stampMissingReasoningForThinkingMode(messages, model2) {
3079
+ if (!isThinkingModeModel(model2)) {
3080
+ return { messages, stampedCount: 0 };
3081
+ }
3082
+ let stampedCount = 0;
3083
+ const out = messages.map((msg) => {
3084
+ if (msg.role !== "assistant") return msg;
3085
+ if (Object.hasOwn(msg, "reasoning_content")) return msg;
3086
+ stampedCount += 1;
3087
+ return { ...msg, reasoning_content: "" };
3088
+ });
3089
+ return { messages: out, stampedCount };
3090
+ }
3009
3091
  function healLoadedMessagesByTokens(messages, maxTokens) {
3010
3092
  const shrunk = shrinkOversizedToolResultsByTokens(messages, maxTokens);
3011
3093
  const paired = fixToolCallPairing(shrunk.messages);
@@ -3963,28 +4045,233 @@ function registerMemoryTools(registry, opts = {}) {
3963
4045
  return registry;
3964
4046
  }
3965
4047
 
3966
- // src/tools/plan.ts
4048
+ // src/tools/choice.ts
4049
+ var ChoiceRequestedError = class extends Error {
4050
+ question;
4051
+ options;
4052
+ allowCustom;
4053
+ constructor(question, options, allowCustom) {
4054
+ super(
4055
+ "ChoiceRequestedError: choice submitted. STOP calling tools now \u2014 the TUI has shown the options to the user. Wait for their next message; it will either be 'user picked <id>' (carry on with that branch), 'user answered: <text>' (custom free-form reply; read and proceed), or 'user cancelled the choice' (drop the question and ask what they want instead). Don't call any tools in the meantime."
4056
+ );
4057
+ this.name = "ChoiceRequestedError";
4058
+ this.question = question;
4059
+ this.options = options;
4060
+ this.allowCustom = allowCustom;
4061
+ }
4062
+ toToolResult() {
4063
+ return {
4064
+ error: `${this.name}: ${this.message}`,
4065
+ question: this.question,
4066
+ options: this.options,
4067
+ allowCustom: this.allowCustom
4068
+ };
4069
+ }
4070
+ };
4071
+ function sanitizeOptions(raw) {
4072
+ if (!Array.isArray(raw)) return [];
4073
+ const out = [];
4074
+ const seen = /* @__PURE__ */ new Set();
4075
+ for (const entry of raw) {
4076
+ if (!entry || typeof entry !== "object") continue;
4077
+ const e = entry;
4078
+ const id = typeof e.id === "string" ? e.id.trim() : "";
4079
+ const title = typeof e.title === "string" ? e.title.trim() : "";
4080
+ if (!id || !title) continue;
4081
+ if (seen.has(id)) continue;
4082
+ seen.add(id);
4083
+ const summary = typeof e.summary === "string" ? e.summary.trim() || void 0 : void 0;
4084
+ const opt = { id, title };
4085
+ if (summary) opt.summary = summary;
4086
+ out.push(opt);
4087
+ }
4088
+ return out;
4089
+ }
4090
+ function registerChoiceTool(registry, opts = {}) {
4091
+ registry.register({
4092
+ name: "ask_choice",
4093
+ description: "Present 2\u20136 alternatives to the user. The principle: if the user is supposed to pick, the tool picks \u2014 you don't enumerate the choices as prose. Prose menus have no picker in this TUI, so the user gets a wall of text to scroll through and a letter to type, strictly worse than the magenta picker this tool renders. Call it whenever (a) the user has asked for options, (b) you've analyzed multiple approaches and the final call is theirs, or (c) it's a preference fork you can't resolve without them. Skip it when one option is clearly best (just do it, or submit_plan) or a free-form text answer fits (ask in prose). Keep option ids short and stable (A/B/C). Each option: title + optional summary. allowCustom=true when their real answer might not fit. Max 6 options \u2014 narrow first if more. A one-sentence lead-in before the call is fine; don't repeat the options in it.",
4094
+ readOnly: true,
4095
+ parameters: {
4096
+ type: "object",
4097
+ properties: {
4098
+ question: {
4099
+ type: "string",
4100
+ description: "The question to put in front of the user. One sentence. Don't repeat the options in the question text \u2014 the picker renders them separately."
4101
+ },
4102
+ options: {
4103
+ type: "array",
4104
+ description: "2\u20134 alternatives. Each needs a stable id and a short title; summary is optional.",
4105
+ items: {
4106
+ type: "object",
4107
+ properties: {
4108
+ id: { type: "string", description: "Short stable id (A, B, C, or option-1)." },
4109
+ title: { type: "string", description: "One-line title shown as the option label." },
4110
+ summary: {
4111
+ type: "string",
4112
+ description: "Optional. A second dimmed line with more detail. Keep under ~80 chars."
4113
+ }
4114
+ },
4115
+ required: ["id", "title"]
4116
+ }
4117
+ },
4118
+ allowCustom: {
4119
+ type: "boolean",
4120
+ description: "If true, the picker shows a 'Let me type my own answer' escape hatch. Default false. Turn on when the user's real answer might not fit any of your pre-defined options."
4121
+ }
4122
+ },
4123
+ required: ["question", "options"]
4124
+ },
4125
+ fn: async (args) => {
4126
+ const question = (args?.question ?? "").trim();
4127
+ if (!question) {
4128
+ throw new Error(
4129
+ "ask_choice: question is required \u2014 write one sentence explaining the decision."
4130
+ );
4131
+ }
4132
+ const options = sanitizeOptions(args?.options);
4133
+ if (options.length < 2) {
4134
+ throw new Error(
4135
+ "ask_choice: need at least 2 well-formed options (each with a non-empty id and title). If you just need a text answer, ask the user in plain assistant text instead."
4136
+ );
4137
+ }
4138
+ if (options.length > 6) {
4139
+ throw new Error(
4140
+ "ask_choice: too many options (max 6). If you really have this many branches, split into two sequential ask_choice calls or narrow down first."
4141
+ );
4142
+ }
4143
+ const allowCustom = args?.allowCustom === true;
4144
+ opts.onChoiceRequested?.(question, options);
4145
+ throw new ChoiceRequestedError(question, options, allowCustom);
4146
+ }
4147
+ });
4148
+ return registry;
4149
+ }
4150
+
4151
+ // src/tools/plan-errors.ts
3967
4152
  var PlanProposedError = class extends Error {
3968
4153
  plan;
3969
- constructor(plan2) {
4154
+ steps;
4155
+ summary;
4156
+ constructor(plan2, steps, summary) {
3970
4157
  super(
3971
4158
  "PlanProposedError: plan submitted. STOP calling tools now \u2014 the TUI has shown the plan to the user. Wait for their next message; it will either approve (you'll then implement the plan), request a refinement (you should explore more and submit an updated plan), or cancel (drop the plan and ask what they want instead). Don't call any tools in the meantime."
3972
4159
  );
3973
4160
  this.name = "PlanProposedError";
3974
4161
  this.plan = plan2;
4162
+ this.steps = steps;
4163
+ this.summary = summary;
3975
4164
  }
3976
4165
  /**
3977
4166
  * Structured tool-result shape. Consumed by the TUI to extract the
3978
- * plan without regex-scraping the error message.
4167
+ * plan without regex-scraping the error message. Optional fields
4168
+ * are omitted from the payload when absent so consumers don't see
4169
+ * `undefined` keys in the JSON.
3979
4170
  */
3980
4171
  toToolResult() {
3981
- return { error: `${this.name}: ${this.message}`, plan: this.plan };
4172
+ const payload = {
4173
+ error: `${this.name}: ${this.message}`,
4174
+ plan: this.plan
4175
+ };
4176
+ if (this.steps && this.steps.length > 0) payload.steps = this.steps;
4177
+ if (this.summary) payload.summary = this.summary;
4178
+ return payload;
3982
4179
  }
3983
4180
  };
3984
- function registerPlanTool(registry, opts = {}) {
4181
+ var PlanCheckpointError = class extends Error {
4182
+ stepId;
4183
+ title;
4184
+ result;
4185
+ notes;
4186
+ constructor(update2) {
4187
+ super(
4188
+ "PlanCheckpointError: step complete \u2014 STOP calling tools. The TUI has paused the plan for user review. Wait for the next user message; it will either say continue (proceed to the next step), request a revision (adjust the remaining plan), or stop (summarize and end)."
4189
+ );
4190
+ this.name = "PlanCheckpointError";
4191
+ this.stepId = update2.stepId;
4192
+ this.title = update2.title;
4193
+ this.result = update2.result;
4194
+ this.notes = update2.notes;
4195
+ }
4196
+ toToolResult() {
4197
+ const payload = {
4198
+ error: `${this.name}: ${this.message}`,
4199
+ kind: "step_completed",
4200
+ stepId: this.stepId,
4201
+ result: this.result
4202
+ };
4203
+ if (this.title) payload.title = this.title;
4204
+ if (this.notes) payload.notes = this.notes;
4205
+ return payload;
4206
+ }
4207
+ };
4208
+ var PlanRevisionProposedError = class extends Error {
4209
+ reason;
4210
+ remainingSteps;
4211
+ summary;
4212
+ constructor(reason, remainingSteps, summary) {
4213
+ super(
4214
+ "PlanRevisionProposedError: revision submitted. STOP calling tools now \u2014 the TUI has paused for the user to review your proposed change. Wait for their next message; it will say 'revision accepted' (proceed with the new step list), 'revision rejected' (keep the original plan and continue), or 'revision cancelled' (drop the proposal entirely). Don't call any tools in the meantime."
4215
+ );
4216
+ this.name = "PlanRevisionProposedError";
4217
+ this.reason = reason;
4218
+ this.remainingSteps = remainingSteps;
4219
+ this.summary = summary;
4220
+ }
4221
+ toToolResult() {
4222
+ const payload = {
4223
+ error: `${this.name}: ${this.message}`,
4224
+ reason: this.reason,
4225
+ remainingSteps: this.remainingSteps
4226
+ };
4227
+ if (this.summary) payload.summary = this.summary;
4228
+ return payload;
4229
+ }
4230
+ };
4231
+
4232
+ // src/tools/plan-core.ts
4233
+ var SUBMIT_PLAN_DESCRIPTION = "Submit ONE concrete plan you've already decided on. Use this for tasks that warrant a review gate \u2014 multi-file refactors, architecture changes, anything that would be expensive or confusing to undo. Skip it for small fixes (one-line typo, obvious bug with a clear fix) \u2014 just make the change. The user will either approve (you then implement it), ask for refinement, or cancel. If the user has already enabled /plan mode, writes are blocked at dispatch and you MUST use this. CRITICAL: do NOT use submit_plan to present alternative routes (A/B/C, option 1/2/3) for the user to pick from \u2014 the picker only exposes approve/refine/cancel, so a menu plan strands the user with no way to choose. For branching decisions, call `ask_choice` instead; only call submit_plan once the user has picked a direction and you have a single actionable plan. Write the plan as markdown with a one-line summary, a bulleted list of files to touch and what will change, and any risks or open questions. STRONGLY PREFERRED: pass `steps` \u2014 an array of {id, title, action, risk?} \u2014 so the UI renders a structured step list above the approval picker and tracks per-step progress. Use risk='high' for steps that touch prod data / break public APIs / are hard to undo; 'med' for non-trivial but reversible (multi-file edits, schema tweaks); 'low' for safe local work. After each step, call `mark_step_complete` so the user sees progress ticks.";
4234
+ var MARK_STEP_COMPLETE_DESCRIPTION = "Mark one step of the approved plan as done AND pause for the user to review. Call this after finishing each step. The TUI shows a \u2713 progress row and mounts a Continue / Revise / Stop picker \u2014 you MUST stop calling tools after this fires and wait for the next user message. Pass the `stepId` from the plan's steps array, a short `result` (what you did), and optional `notes` for anything surprising (errors, scope changes, follow-ups). This tool doesn't change any files. Don't call it if the plan didn't include structured steps, and don't invent ids that weren't in the original plan.";
4235
+ var REVISE_PLAN_DESCRIPTION = "Surgically replace the REMAINING steps of an in-flight plan. Call this when the user has given feedback at a checkpoint that warrants a structured plan change \u2014 skip a step, swap two steps, add a new step, change risk, etc. Pass: `reason` (one sentence why), `remainingSteps` (the new tail of the plan, replacing whatever steps haven't been done yet), and optional `summary` (updated one-line plan summary). Done steps are NEVER touched \u2014 keep them out of `remainingSteps`. The TUI shows a diff (removed in red, kept in gray, added in green) and the user accepts or rejects. Don't call this for trivial mid-step adjustments \u2014 just keep executing. Don't call submit_plan for revisions either \u2014 that resets the whole plan including completed steps. Use submit_plan only when the entire approach has changed; use revise_plan when the tail needs editing.";
4236
+ var STEP_ITEM_SCHEMA = {
4237
+ type: "object",
4238
+ properties: {
4239
+ id: { type: "string", description: "Stable id, e.g. step-1." },
4240
+ title: { type: "string", description: "Short imperative title." },
4241
+ action: { type: "string", description: "One-sentence description of the concrete action." },
4242
+ risk: {
4243
+ type: "string",
4244
+ enum: ["low", "med", "high"],
4245
+ description: "Self-assessed risk. 'high' = hard-to-undo / touches prod / breaks API; 'med' = non-trivial but reversible; 'low' = safe local work. The UI shows a colored dot per step so the user knows where to focus review. Omit if you're unsure."
4246
+ }
4247
+ },
4248
+ required: ["id", "title", "action"]
4249
+ };
4250
+ function sanitizeRisk(raw) {
4251
+ if (raw === "low" || raw === "med" || raw === "high") return raw;
4252
+ return void 0;
4253
+ }
4254
+ function sanitizeSteps(raw) {
4255
+ if (!Array.isArray(raw)) return void 0;
4256
+ const steps = [];
4257
+ for (const entry of raw) {
4258
+ if (!entry || typeof entry !== "object") continue;
4259
+ const e = entry;
4260
+ const id = typeof e.id === "string" ? e.id.trim() : "";
4261
+ const title = typeof e.title === "string" ? e.title.trim() : "";
4262
+ const action = typeof e.action === "string" ? e.action.trim() : "";
4263
+ if (!id || !title || !action) continue;
4264
+ const step = { id, title, action };
4265
+ const risk = sanitizeRisk(e.risk);
4266
+ if (risk) step.risk = risk;
4267
+ steps.push(step);
4268
+ }
4269
+ return steps.length > 0 ? steps : void 0;
4270
+ }
4271
+ function registerSubmitPlan(registry, opts) {
3985
4272
  registry.register({
3986
4273
  name: "submit_plan",
3987
- description: "Submit a concrete plan to the user for review before executing. Use this for tasks that warrant a review gate \u2014 multi-file refactors, architecture changes, anything that would be expensive or confusing to undo. Skip it for small fixes (one-line typo, obvious bug with a clear fix) \u2014 just make the change. The user will either approve (you then implement it), ask for refinement, or cancel. If the user has already enabled /plan mode, writes are blocked at dispatch and you MUST use this. Write the plan as markdown with a one-line summary, a bulleted list of files to touch and what will change, and any risks or open questions.",
4274
+ description: SUBMIT_PLAN_DESCRIPTION,
3988
4275
  readOnly: true,
3989
4276
  parameters: {
3990
4277
  type: "object",
@@ -3992,6 +4279,15 @@ function registerPlanTool(registry, opts = {}) {
3992
4279
  plan: {
3993
4280
  type: "string",
3994
4281
  description: "Markdown-formatted plan. Lead with a one-sentence summary. Then a file-by-file breakdown of what you'll change and why. Flag any risks or open questions at the end so the user can weigh in before you start."
4282
+ },
4283
+ steps: {
4284
+ type: "array",
4285
+ description: "Structured step list (strongly recommended). When provided, the UI renders a compact step list above the approval picker AND tracks per-step progress via `mark_step_complete`. Use stable ids (step-1, step-2, ...). Skip only for tiny one-step plans where the markdown body is enough.",
4286
+ items: STEP_ITEM_SCHEMA
4287
+ },
4288
+ summary: {
4289
+ type: "string",
4290
+ description: "Optional. One-sentence human-friendly title for the plan, ~80 chars max. Surfaces in the PlanConfirm picker header and in /plans listings ('\u25B8 refactor auth into signed tokens \xB7 2/5 done'). Skip for trivial plans where the first line of the markdown body is already short and clear."
3995
4291
  }
3996
4292
  },
3997
4293
  required: ["plan"]
@@ -4001,10 +4297,108 @@ function registerPlanTool(registry, opts = {}) {
4001
4297
  if (!plan2) {
4002
4298
  throw new Error("submit_plan: empty plan \u2014 write a markdown plan and try again.");
4003
4299
  }
4004
- opts.onPlanSubmitted?.(plan2);
4005
- throw new PlanProposedError(plan2);
4300
+ const steps = sanitizeSteps(args?.steps);
4301
+ const summary = typeof args?.summary === "string" ? args.summary.trim() || void 0 : void 0;
4302
+ opts.onPlanSubmitted?.(plan2, steps);
4303
+ throw new PlanProposedError(plan2, steps, summary);
4304
+ }
4305
+ });
4306
+ }
4307
+ function registerMarkStepComplete(registry, opts) {
4308
+ registry.register({
4309
+ name: "mark_step_complete",
4310
+ description: MARK_STEP_COMPLETE_DESCRIPTION,
4311
+ readOnly: true,
4312
+ parameters: {
4313
+ type: "object",
4314
+ properties: {
4315
+ stepId: {
4316
+ type: "string",
4317
+ description: "The id of the step being marked complete. Must match one from submit_plan's steps array."
4318
+ },
4319
+ title: {
4320
+ type: "string",
4321
+ description: "Optional. The step's title, echoed back for the UI. If omitted, the UI falls back to the id."
4322
+ },
4323
+ result: {
4324
+ type: "string",
4325
+ description: "One-sentence summary of what was done for this step."
4326
+ },
4327
+ notes: {
4328
+ type: "string",
4329
+ description: "Optional. Anything surprising \u2014 blockers hit, assumptions revised, follow-ups for later steps."
4330
+ }
4331
+ },
4332
+ required: ["stepId", "result"]
4333
+ },
4334
+ fn: async (args) => {
4335
+ const stepId = (args?.stepId ?? "").trim();
4336
+ const result = (args?.result ?? "").trim();
4337
+ if (!stepId) {
4338
+ throw new Error("mark_step_complete: stepId is required.");
4339
+ }
4340
+ if (!result) {
4341
+ throw new Error(
4342
+ "mark_step_complete: result is required \u2014 say in one sentence what you did."
4343
+ );
4344
+ }
4345
+ const title = typeof args?.title === "string" ? args.title.trim() || void 0 : void 0;
4346
+ const notes = typeof args?.notes === "string" ? args.notes.trim() || void 0 : void 0;
4347
+ const update2 = { kind: "step_completed", stepId, result };
4348
+ if (title) update2.title = title;
4349
+ if (notes) update2.notes = notes;
4350
+ opts.onStepCompleted?.(update2);
4351
+ throw new PlanCheckpointError({ stepId, title, result, notes });
4352
+ }
4353
+ });
4354
+ }
4355
+ function registerRevisePlan(registry, opts) {
4356
+ registry.register({
4357
+ name: "revise_plan",
4358
+ description: REVISE_PLAN_DESCRIPTION,
4359
+ readOnly: true,
4360
+ parameters: {
4361
+ type: "object",
4362
+ properties: {
4363
+ reason: {
4364
+ type: "string",
4365
+ description: "One sentence explaining why you're revising \u2014 what the user asked for, what changed your assessment."
4366
+ },
4367
+ remainingSteps: {
4368
+ type: "array",
4369
+ description: "The new tail of the plan \u2014 what should run from here on. Each entry: {id, title, action, risk?}. Use stable ids; reuse old ids when a step is just being adjusted, generate new ones for genuinely new steps.",
4370
+ items: STEP_ITEM_SCHEMA
4371
+ },
4372
+ summary: {
4373
+ type: "string",
4374
+ description: "Optional. Updated one-line plan summary if the overall framing has shifted."
4375
+ }
4376
+ },
4377
+ required: ["reason", "remainingSteps"]
4378
+ },
4379
+ fn: async (args) => {
4380
+ const reason = (args?.reason ?? "").trim();
4381
+ if (!reason) {
4382
+ throw new Error(
4383
+ "revise_plan: reason is required \u2014 write one sentence explaining the change."
4384
+ );
4385
+ }
4386
+ const remainingSteps = sanitizeSteps(args?.remainingSteps);
4387
+ if (!remainingSteps || remainingSteps.length === 0) {
4388
+ throw new Error(
4389
+ "revise_plan: remainingSteps must be a non-empty array of well-formed steps. If the user wants to STOP rather than continue, don't revise \u2014 the picker has its own Stop option."
4390
+ );
4391
+ }
4392
+ const summary = typeof args?.summary === "string" ? args.summary.trim() || void 0 : void 0;
4393
+ opts.onPlanRevisionProposed?.(reason, remainingSteps, summary);
4394
+ throw new PlanRevisionProposedError(reason, remainingSteps, summary);
4006
4395
  }
4007
4396
  });
4397
+ }
4398
+ function registerPlanTool(registry, opts = {}) {
4399
+ registerSubmitPlan(registry, opts);
4400
+ registerMarkStepComplete(registry, opts);
4401
+ registerRevisePlan(registry, opts);
4008
4402
  return registry;
4009
4403
  }
4010
4404
 
@@ -6700,13 +7094,13 @@ function formatLogSize(path = defaultUsageLogPath()) {
6700
7094
  }
6701
7095
 
6702
7096
  // src/cli/commands/chat.tsx
6703
- import { existsSync as existsSync11, statSync as statSync6 } from "fs";
7097
+ import { existsSync as existsSync12, statSync as statSync7 } from "fs";
6704
7098
  import { render } from "ink";
6705
- import React19, { useState as useState12 } from "react";
7099
+ import React23, { useState as useState12 } from "react";
6706
7100
 
6707
7101
  // src/cli/ui/App.tsx
6708
- import { Box as Box15, Static, useApp, useInput as useInput5 } from "ink";
6709
- import React16, { useCallback as useCallback4, useEffect as useEffect5, useMemo as useMemo3, useRef as useRef5, useState as useState10 } from "react";
7102
+ import { Box as Box19, Static, useApp, useInput as useInput5 } from "ink";
7103
+ import React20, { useCallback as useCallback4, useEffect as useEffect5, useMemo as useMemo3, useRef as useRef5, useState as useState10 } from "react";
6710
7104
 
6711
7105
  // src/code/pending-edits.ts
6712
7106
  import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
@@ -6760,6 +7154,166 @@ function clearPendingEdits(sessionName) {
6760
7154
  }
6761
7155
  }
6762
7156
 
7157
+ // src/code/plan-store.ts
7158
+ import {
7159
+ existsSync as existsSync10,
7160
+ mkdirSync as mkdirSync7,
7161
+ readFileSync as readFileSync12,
7162
+ readdirSync as readdirSync3,
7163
+ renameSync,
7164
+ statSync as statSync5,
7165
+ unlinkSync as unlinkSync4,
7166
+ writeFileSync as writeFileSync6
7167
+ } from "fs";
7168
+ import { dirname as dirname9, join as join10 } from "path";
7169
+ function planStatePath(sessionName) {
7170
+ return join10(sessionsDir(), `${sanitizeName(sessionName)}.plan.json`);
7171
+ }
7172
+ function loadPlanState(sessionName) {
7173
+ const path = planStatePath(sessionName);
7174
+ if (!existsSync10(path)) return null;
7175
+ try {
7176
+ const raw = readFileSync12(path, "utf8");
7177
+ const parsed = JSON.parse(raw);
7178
+ if (!parsed || typeof parsed !== "object") return null;
7179
+ if (parsed.version !== 1) return null;
7180
+ if (!Array.isArray(parsed.steps)) return null;
7181
+ if (!Array.isArray(parsed.completedStepIds)) return null;
7182
+ if (typeof parsed.updatedAt !== "string") return null;
7183
+ const steps = [];
7184
+ for (const s of parsed.steps) {
7185
+ if (!s || typeof s !== "object") continue;
7186
+ const e = s;
7187
+ if (typeof e.id !== "string" || !e.id) continue;
7188
+ if (typeof e.title !== "string" || !e.title) continue;
7189
+ if (typeof e.action !== "string" || !e.action) continue;
7190
+ const step = { id: e.id, title: e.title, action: e.action };
7191
+ if (e.risk === "low" || e.risk === "med" || e.risk === "high") step.risk = e.risk;
7192
+ steps.push(step);
7193
+ }
7194
+ if (steps.length === 0) return null;
7195
+ const completedStepIds = parsed.completedStepIds.filter(
7196
+ (id) => typeof id === "string" && id.length > 0
7197
+ );
7198
+ const out = {
7199
+ version: 1,
7200
+ steps,
7201
+ completedStepIds,
7202
+ updatedAt: parsed.updatedAt
7203
+ };
7204
+ if (typeof parsed.body === "string" && parsed.body) out.body = parsed.body;
7205
+ if (typeof parsed.summary === "string" && parsed.summary) out.summary = parsed.summary;
7206
+ return out;
7207
+ } catch {
7208
+ return null;
7209
+ }
7210
+ }
7211
+ function savePlanState(sessionName, steps, completedStepIds, extras) {
7212
+ const path = planStatePath(sessionName);
7213
+ try {
7214
+ mkdirSync7(dirname9(path), { recursive: true });
7215
+ const state = {
7216
+ version: 1,
7217
+ steps,
7218
+ completedStepIds: [...completedStepIds],
7219
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7220
+ };
7221
+ if (extras?.body) state.body = extras.body;
7222
+ if (extras?.summary) state.summary = extras.summary;
7223
+ writeFileSync6(path, `${JSON.stringify(state, null, 2)}
7224
+ `, "utf8");
7225
+ } catch (err) {
7226
+ process.stderr.write(
7227
+ `\u25B8 plan-store: failed to save plan for "${sessionName}": ${err.message}
7228
+ `
7229
+ );
7230
+ }
7231
+ }
7232
+ function clearPlanState(sessionName) {
7233
+ const path = planStatePath(sessionName);
7234
+ try {
7235
+ if (existsSync10(path)) unlinkSync4(path);
7236
+ } catch {
7237
+ }
7238
+ }
7239
+ function archivePlanState(sessionName) {
7240
+ const active = planStatePath(sessionName);
7241
+ if (!existsSync10(active)) return null;
7242
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7243
+ const suffix = Math.random().toString(36).slice(2, 6);
7244
+ const archive = join10(
7245
+ sessionsDir(),
7246
+ `${sanitizeName(sessionName)}.plan.${stamp}-${suffix}.done.json`
7247
+ );
7248
+ try {
7249
+ renameSync(active, archive);
7250
+ return archive;
7251
+ } catch (err) {
7252
+ process.stderr.write(
7253
+ `\u25B8 plan-store: failed to archive plan for "${sessionName}": ${err.message}
7254
+ `
7255
+ );
7256
+ return null;
7257
+ }
7258
+ }
7259
+ function listPlanArchives(sessionName) {
7260
+ const dir = sessionsDir();
7261
+ if (!existsSync10(dir)) return [];
7262
+ const prefix = `${sanitizeName(sessionName)}.plan.`;
7263
+ const suffix = ".done.json";
7264
+ let entries;
7265
+ try {
7266
+ entries = readdirSync3(dir);
7267
+ } catch {
7268
+ return [];
7269
+ }
7270
+ const summaries = [];
7271
+ for (const name of entries) {
7272
+ if (!name.startsWith(prefix) || !name.endsWith(suffix)) continue;
7273
+ const full = join10(dir, name);
7274
+ try {
7275
+ const raw = readFileSync12(full, "utf8");
7276
+ const parsed = JSON.parse(raw);
7277
+ if (parsed.version !== 1) continue;
7278
+ if (!Array.isArray(parsed.steps) || parsed.steps.length === 0) continue;
7279
+ const steps = parsed.steps.filter(
7280
+ (s) => !!s && typeof s === "object" && typeof s.id === "string" && typeof s.title === "string" && typeof s.action === "string"
7281
+ );
7282
+ if (steps.length === 0) continue;
7283
+ const completedStepIds = Array.isArray(parsed.completedStepIds) ? parsed.completedStepIds.filter((id) => typeof id === "string" && !!id) : [];
7284
+ let completedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "";
7285
+ if (!completedAt || Number.isNaN(Date.parse(completedAt))) {
7286
+ try {
7287
+ completedAt = statSync5(full).mtime.toISOString();
7288
+ } catch {
7289
+ completedAt = (/* @__PURE__ */ new Date(0)).toISOString();
7290
+ }
7291
+ }
7292
+ const entry = { path: full, completedAt, steps, completedStepIds };
7293
+ if (typeof parsed.body === "string" && parsed.body) entry.body = parsed.body;
7294
+ if (typeof parsed.summary === "string" && parsed.summary) entry.summary = parsed.summary;
7295
+ summaries.push(entry);
7296
+ } catch {
7297
+ }
7298
+ }
7299
+ summaries.sort((a, b) => b.completedAt.localeCompare(a.completedAt));
7300
+ return summaries;
7301
+ }
7302
+ function relativeTime(updatedAt, now = Date.now()) {
7303
+ const t = Date.parse(updatedAt);
7304
+ if (Number.isNaN(t)) return updatedAt;
7305
+ const diffMs = Math.max(0, now - t);
7306
+ const sec = Math.floor(diffMs / 1e3);
7307
+ if (sec < 60) return `${sec}s ago`;
7308
+ const min = Math.floor(sec / 60);
7309
+ if (min < 60) return `${min}m ago`;
7310
+ const hr = Math.floor(min / 60);
7311
+ if (hr < 24) return `${hr}h ago`;
7312
+ const day = Math.floor(hr / 24);
7313
+ if (day < 7) return `${day}d ago`;
7314
+ return updatedAt.slice(0, 10);
7315
+ }
7316
+
6763
7317
  // src/tools/skills.ts
6764
7318
  function registerSkillTools(registry, opts = {}) {
6765
7319
  const store = new SkillStore({
@@ -6869,51 +7423,195 @@ function FileRow({ path, isSelected }) {
6869
7423
  return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, marker, " ", base, dir ? ` ${dir}` : ""));
6870
7424
  }
6871
7425
 
6872
- // src/cli/ui/EditConfirm.tsx
6873
- import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
6874
- import React2, { useMemo, useState } from "react";
7426
+ // src/cli/ui/ChoiceConfirm.tsx
7427
+ import { Box as Box3, Text as Text3 } from "ink";
7428
+ import React3 from "react";
6875
7429
 
6876
- // src/code/diff-preview.ts
6877
- function formatEditBlockDiff(block, opts = {}) {
6878
- const contextLines = Math.max(0, opts.contextLines ?? 2);
6879
- const maxLines = Math.max(4, opts.maxLines ?? 20);
6880
- const indent = opts.indent ?? " ";
6881
- const search = block.search === "" ? [] : block.search.split("\n");
6882
- const replace = block.replace.split("\n");
6883
- if (search.length === 0) {
6884
- return renderAllPlus(replace, indent, maxLines);
6885
- }
6886
- let leading = 0;
6887
- while (leading < search.length && leading < replace.length && search[leading] === replace[leading]) {
6888
- leading++;
6889
- }
6890
- let trailing = 0;
6891
- while (trailing < search.length - leading && trailing < replace.length - leading && search[search.length - 1 - trailing] === replace[replace.length - 1 - trailing]) {
6892
- trailing++;
6893
- }
6894
- const searchMiddle = search.slice(leading, search.length - trailing);
6895
- const replaceMiddle = replace.slice(leading, replace.length - trailing);
6896
- const leadShown = search.slice(Math.max(0, leading - contextLines), leading);
6897
- const leadHidden = leading - leadShown.length;
6898
- const trailShown = search.slice(
6899
- search.length - trailing,
6900
- search.length - trailing + contextLines
7430
+ // src/cli/ui/Select.tsx
7431
+ import { Box as Box2, Text as Text2, useInput } from "ink";
7432
+ import React2, { useState } from "react";
7433
+ function SingleSelect({
7434
+ items,
7435
+ initialValue,
7436
+ onSubmit,
7437
+ onCancel,
7438
+ footer
7439
+ }) {
7440
+ const initialIndex = Math.max(
7441
+ 0,
7442
+ items.findIndex((i) => i.value === initialValue && !i.disabled)
6901
7443
  );
6902
- const trailHidden = trailing - trailShown.length;
6903
- const out = [];
6904
- if (leadHidden > 0) {
6905
- out.push(`${indent} \u2026 ${leadHidden} unchanged line${leadHidden === 1 ? "" : "s"} above`);
6906
- }
6907
- for (const l of leadShown) out.push(`${indent} ${l}`);
6908
- for (const l of searchMiddle) out.push(`${indent}- ${l}`);
6909
- for (const l of replaceMiddle) out.push(`${indent}+ ${l}`);
6910
- for (const l of trailShown) out.push(`${indent} ${l}`);
6911
- if (trailHidden > 0) {
6912
- out.push(`${indent} \u2026 ${trailHidden} unchanged line${trailHidden === 1 ? "" : "s"} below`);
6913
- }
6914
- return capLines(out, maxLines, indent);
6915
- }
6916
- function formatAllBlockDiffs(blocks, opts = {}) {
7444
+ const [index, setIndex] = useState(initialIndex === -1 ? 0 : initialIndex);
7445
+ useInput((_input, key) => {
7446
+ if (key.upArrow) {
7447
+ setIndex((i) => findNextEnabled(items, i, -1));
7448
+ } else if (key.downArrow) {
7449
+ setIndex((i) => findNextEnabled(items, i, 1));
7450
+ } else if (key.return) {
7451
+ const chosen = items[index];
7452
+ if (chosen && !chosen.disabled) onSubmit(chosen.value);
7453
+ } else if (key.escape && onCancel) {
7454
+ onCancel();
7455
+ }
7456
+ });
7457
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React2.createElement(
7458
+ SelectRow,
7459
+ {
7460
+ key: item.value,
7461
+ item,
7462
+ active: i === index,
7463
+ marker: i === index ? "\u25B8" : " "
7464
+ }
7465
+ )), footer ? /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, footer)) : null);
7466
+ }
7467
+ function MultiSelect({
7468
+ items,
7469
+ initialSelected = [],
7470
+ onSubmit,
7471
+ onCancel,
7472
+ footer
7473
+ }) {
7474
+ const [index, setIndex] = useState(() => {
7475
+ const first = items.findIndex((i) => !i.disabled);
7476
+ return first === -1 ? 0 : first;
7477
+ });
7478
+ const [selected, setSelected] = useState(new Set(initialSelected));
7479
+ useInput((input, key) => {
7480
+ if (key.upArrow) {
7481
+ setIndex((i) => findNextEnabled(items, i, -1));
7482
+ } else if (key.downArrow) {
7483
+ setIndex((i) => findNextEnabled(items, i, 1));
7484
+ } else if (input === " ") {
7485
+ const item = items[index];
7486
+ if (!item || item.disabled) return;
7487
+ setSelected((prev) => {
7488
+ const next = new Set(prev);
7489
+ if (next.has(item.value)) next.delete(item.value);
7490
+ else next.add(item.value);
7491
+ return next;
7492
+ });
7493
+ } else if (key.return) {
7494
+ const ordered = items.filter((i) => selected.has(i.value)).map((i) => i.value);
7495
+ onSubmit(ordered);
7496
+ } else if (key.escape && onCancel) {
7497
+ onCancel();
7498
+ }
7499
+ });
7500
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, items.map((item, i) => {
7501
+ const checked = selected.has(item.value);
7502
+ const marker = checked ? "[x]" : "[ ]";
7503
+ return /* @__PURE__ */ React2.createElement(
7504
+ SelectRow,
7505
+ {
7506
+ key: item.value,
7507
+ item,
7508
+ active: i === index,
7509
+ marker: `${i === index ? "\u25B8" : " "} ${marker}`
7510
+ }
7511
+ );
7512
+ }), footer ? /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, footer)) : null);
7513
+ }
7514
+ function SelectRow({
7515
+ item,
7516
+ active,
7517
+ marker
7518
+ }) {
7519
+ const color = item.disabled ? "gray" : active ? "cyan" : void 0;
7520
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React2.createElement(Box2, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, item.hint)) : null);
7521
+ }
7522
+ function findNextEnabled(items, from, step) {
7523
+ if (items.length === 0) return 0;
7524
+ let i = from;
7525
+ for (let tries = 0; tries < items.length; tries++) {
7526
+ i = (i + step + items.length) % items.length;
7527
+ if (!items[i]?.disabled) return i;
7528
+ }
7529
+ return from;
7530
+ }
7531
+
7532
+ // src/cli/ui/ChoiceConfirm.tsx
7533
+ var CUSTOM_VALUE = "__custom__";
7534
+ var CANCEL_VALUE = "__cancel__";
7535
+ function ChoiceConfirmInner({ question, options, allowCustom, onChoose }) {
7536
+ const items = options.map((opt) => ({
7537
+ value: opt.id,
7538
+ label: `${opt.id} \xB7 ${opt.title}`,
7539
+ hint: opt.summary
7540
+ }));
7541
+ if (allowCustom) {
7542
+ items.push({
7543
+ value: CUSTOM_VALUE,
7544
+ label: "Let me type my own answer",
7545
+ hint: "None of the above fits \u2014 type a free-form reply. The model reads it verbatim."
7546
+ });
7547
+ }
7548
+ items.push({
7549
+ value: CANCEL_VALUE,
7550
+ label: "Cancel \u2014 drop the question",
7551
+ hint: "Model stops and asks what you want instead."
7552
+ });
7553
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "magenta" }, "\u{1F500} the model is asking you to pick")), /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, question)), /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(
7554
+ SingleSelect,
7555
+ {
7556
+ initialValue: options[0]?.id,
7557
+ items,
7558
+ onSubmit: (v) => {
7559
+ if (v === CUSTOM_VALUE) onChoose({ kind: "custom" });
7560
+ else if (v === CANCEL_VALUE) onChoose({ kind: "cancel" });
7561
+ else onChoose({ kind: "pick", optionId: v });
7562
+ },
7563
+ onCancel: () => onChoose({ kind: "cancel" }),
7564
+ footer: "[\u2191\u2193] navigate \xB7 [Enter] select \xB7 [Esc] cancel"
7565
+ }
7566
+ )));
7567
+ }
7568
+ var ChoiceConfirm = React3.memo(ChoiceConfirmInner);
7569
+
7570
+ // src/cli/ui/EditConfirm.tsx
7571
+ import { Box as Box4, Text as Text4, useInput as useInput2, useStdout } from "ink";
7572
+ import React4, { useMemo, useState as useState2 } from "react";
7573
+
7574
+ // src/code/diff-preview.ts
7575
+ function formatEditBlockDiff(block, opts = {}) {
7576
+ const contextLines = Math.max(0, opts.contextLines ?? 2);
7577
+ const maxLines = Math.max(4, opts.maxLines ?? 20);
7578
+ const indent = opts.indent ?? " ";
7579
+ const search = block.search === "" ? [] : block.search.split("\n");
7580
+ const replace = block.replace.split("\n");
7581
+ if (search.length === 0) {
7582
+ return renderAllPlus(replace, indent, maxLines);
7583
+ }
7584
+ let leading = 0;
7585
+ while (leading < search.length && leading < replace.length && search[leading] === replace[leading]) {
7586
+ leading++;
7587
+ }
7588
+ let trailing = 0;
7589
+ while (trailing < search.length - leading && trailing < replace.length - leading && search[search.length - 1 - trailing] === replace[replace.length - 1 - trailing]) {
7590
+ trailing++;
7591
+ }
7592
+ const searchMiddle = search.slice(leading, search.length - trailing);
7593
+ const replaceMiddle = replace.slice(leading, replace.length - trailing);
7594
+ const leadShown = search.slice(Math.max(0, leading - contextLines), leading);
7595
+ const leadHidden = leading - leadShown.length;
7596
+ const trailShown = search.slice(
7597
+ search.length - trailing,
7598
+ search.length - trailing + contextLines
7599
+ );
7600
+ const trailHidden = trailing - trailShown.length;
7601
+ const out = [];
7602
+ if (leadHidden > 0) {
7603
+ out.push(`${indent} \u2026 ${leadHidden} unchanged line${leadHidden === 1 ? "" : "s"} above`);
7604
+ }
7605
+ for (const l of leadShown) out.push(`${indent} ${l}`);
7606
+ for (const l of searchMiddle) out.push(`${indent}- ${l}`);
7607
+ for (const l of replaceMiddle) out.push(`${indent}+ ${l}`);
7608
+ for (const l of trailShown) out.push(`${indent} ${l}`);
7609
+ if (trailHidden > 0) {
7610
+ out.push(`${indent} \u2026 ${trailHidden} unchanged line${trailHidden === 1 ? "" : "s"} below`);
7611
+ }
7612
+ return capLines(out, maxLines, indent);
7613
+ }
7614
+ function formatAllBlockDiffs(blocks, opts = {}) {
6917
7615
  const out = [];
6918
7616
  for (let i = 0; i < blocks.length; i++) {
6919
7617
  const b = blocks[i];
@@ -6953,10 +7651,10 @@ function EditConfirm({ block, onChoose }) {
6953
7651
  () => formatEditBlockDiff(block, { contextLines: 2, maxLines: 1e5, indent: " " }),
6954
7652
  [block]
6955
7653
  );
6956
- const [scroll, setScroll] = useState(0);
7654
+ const [scroll, setScroll] = useState2(0);
6957
7655
  const maxScroll = Math.max(0, allLines.length - budget);
6958
7656
  const effectiveScroll = Math.min(scroll, maxScroll);
6959
- useInput((input, key) => {
7657
+ useInput2((input, key) => {
6960
7658
  if (key.return || input === "y") {
6961
7659
  onChoose("apply");
6962
7660
  return;
@@ -7007,18 +7705,18 @@ function EditConfirm({ block, onChoose }) {
7007
7705
  const hiddenBelow = Math.max(0, allLines.length - effectiveScroll - budget);
7008
7706
  const totalLines = allLines.length;
7009
7707
  const showScrollHud = hiddenAbove + hiddenBelow > 0;
7010
- 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(
7011
- Text2,
7708
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "cyan" }, "\u25B8 model wants to edit a file")), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, { 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__ */ React4.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { color: isNew ? "green" : "yellow", bold: true }, `[${tag}] `), /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, block.path), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, ` (-${removed} +${added} lines)`), showScrollHud ? /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, ` \xB7 viewing ${effectiveScroll + 1}-${effectiveScroll + visibleLines.length}/${totalLines}`) : null)), hiddenAbove > 0 ? /* @__PURE__ */ React4.createElement(
7709
+ Text4,
7012
7710
  {
7013
7711
  dimColor: true
7014
7712
  },
7015
7713
  ` \u2191 ${hiddenAbove} line${hiddenAbove === 1 ? "" : "s"} above (\u2191/k or PgUp)`
7016
- ) : null, /* @__PURE__ */ React2.createElement(Box2, { marginTop: hiddenAbove > 0 ? 0 : 1, flexDirection: "column" }, visibleLines.map((line, i) => {
7714
+ ) : null, /* @__PURE__ */ React4.createElement(Box4, { marginTop: hiddenAbove > 0 ? 0 : 1, flexDirection: "column" }, visibleLines.map((line, i) => {
7017
7715
  const trimmed = line.trimStart();
7018
7716
  const color = trimmed.startsWith("+") ? "green" : trimmed.startsWith("-") ? "red" : void 0;
7019
7717
  const dim = !color;
7020
- return /* @__PURE__ */ React2.createElement(
7021
- Text2,
7718
+ return /* @__PURE__ */ React4.createElement(
7719
+ Text4,
7022
7720
  {
7023
7721
  key: `diff-${effectiveScroll}-${i}`,
7024
7722
  color,
@@ -7026,22 +7724,22 @@ function EditConfirm({ block, onChoose }) {
7026
7724
  },
7027
7725
  line
7028
7726
  );
7029
- })), hiddenBelow > 0 ? /* @__PURE__ */ React2.createElement(
7030
- Text2,
7727
+ })), hiddenBelow > 0 ? /* @__PURE__ */ React4.createElement(
7728
+ Text4,
7031
7729
  {
7032
7730
  dimColor: true
7033
7731
  },
7034
7732
  ` \u2193 ${hiddenBelow} line${hiddenBelow === 1 ? "" : "s"} below (\u2193/j or Space/PgDn)`
7035
- ) : 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")));
7733
+ ) : null, /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "[", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan", bold: true }, "y"), "/Enter] apply \xB7 [", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan", bold: true }, "n"), "] reject \xB7 [", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan", bold: true }, "a"), "] apply rest \xB7 [", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan", bold: true }, "A"), "] flip AUTO \xB7 [", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan", bold: true }, "\u2191\u2193/Space"), "] scroll \xB7 [Esc] abort")));
7036
7734
  }
7037
7735
 
7038
7736
  // src/cli/ui/EventLog.tsx
7039
- import { Box as Box5, Text as Text5, useStdout as useStdout2 } from "ink";
7040
- import React6 from "react";
7737
+ import { Box as Box8, Text as Text8, useStdout as useStdout2 } from "ink";
7738
+ import React9 from "react";
7041
7739
 
7042
7740
  // src/cli/ui/PlanStateBlock.tsx
7043
- import { Box as Box3, Text as Text3 } from "ink";
7044
- import React3 from "react";
7741
+ import { Box as Box5, Text as Text5 } from "ink";
7742
+ import React5 from "react";
7045
7743
  function PlanStateBlock({ planState }) {
7046
7744
  const fields = [];
7047
7745
  if (planState.subgoals.length) fields.push(["subgoals", planState.subgoals, "cyan", false]);
@@ -7052,14 +7750,69 @@ function PlanStateBlock({ planState }) {
7052
7750
  if (planState.rejectedPaths.length)
7053
7751
  fields.push(["rejected", planState.rejectedPaths, "red", true]);
7054
7752
  if (fields.length === 0) return null;
7055
- 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 ")}`))));
7753
+ return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", marginBottom: 1 }, fields.map(([label, items, color, dim]) => /* @__PURE__ */ React5.createElement(Text5, { key: label }, /* @__PURE__ */ React5.createElement(Text5, { color, bold: true, dimColor: dim }, "\u2039 ", label), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, ` (${items.length})`), /* @__PURE__ */ React5.createElement(Text5, null, `: ${items.join(" \xB7 ")}`))));
7056
7754
  }
7057
7755
 
7756
+ // src/cli/ui/PlanStepList.tsx
7757
+ import { Box as Box6, Text as Text6 } from "ink";
7758
+ import React6 from "react";
7759
+ function riskDots(risk) {
7760
+ switch (risk) {
7761
+ case "high":
7762
+ return { dots: "\u25CF\u25CF\u25CF", color: "red" };
7763
+ case "med":
7764
+ return { dots: "\u25CF\u25CF ", color: "yellow" };
7765
+ case "low":
7766
+ return { dots: "\u25CF ", color: "green" };
7767
+ default:
7768
+ return { dots: " ", color: "gray" };
7769
+ }
7770
+ }
7771
+ function statusGlyph(status2) {
7772
+ switch (status2) {
7773
+ case "done":
7774
+ return { glyph: "\u2713", color: "green" };
7775
+ case "running":
7776
+ return { glyph: "\u25B6", color: "cyan" };
7777
+ case "skipped":
7778
+ return { glyph: "\u2014", color: "gray" };
7779
+ default:
7780
+ return { glyph: " ", color: "yellow" };
7781
+ }
7782
+ }
7783
+ function getStatus(stepId, statuses) {
7784
+ if (!statuses) return "pending";
7785
+ if (statuses instanceof Map) {
7786
+ return statuses.get(stepId) ?? "pending";
7787
+ }
7788
+ return statuses[stepId] ?? "pending";
7789
+ }
7790
+ function PlanStepListInner({ steps, statuses, focusStepId }) {
7791
+ if (steps.length === 0) return null;
7792
+ const hasAnyRisk = steps.some((s) => s.risk !== void 0);
7793
+ const doneCount = Array.from(
7794
+ { length: steps.length },
7795
+ (_, i) => getStatus(steps[i].id, statuses)
7796
+ ).filter((s) => s === "done").length;
7797
+ return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Box6, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, `${steps.length} step${steps.length === 1 ? "" : "s"}`, doneCount > 0 ? ` \xB7 ${doneCount} done` : "", hasAnyRisk ? " \xB7 risk: " : ""), hasAnyRisk ? /* @__PURE__ */ React6.createElement(RiskLegend, null) : null), /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", marginTop: 1 }, steps.map((step) => {
7798
+ const status2 = getStatus(step.id, statuses);
7799
+ const focus = focusStepId === step.id;
7800
+ const risk = riskDots(step.risk);
7801
+ const glyph = statusGlyph(status2);
7802
+ const titleDim = status2 === "done" || status2 === "skipped";
7803
+ return /* @__PURE__ */ React6.createElement(Box6, { key: step.id }, /* @__PURE__ */ React6.createElement(Text6, { color: focus ? "cyan" : "gray", bold: focus }, focus ? "\u203A " : " "), /* @__PURE__ */ React6.createElement(Text6, { color: risk.color, bold: true }, risk.dots), /* @__PURE__ */ React6.createElement(Text6, { color: glyph.color, bold: true }, ` ${glyph.glyph} `), /* @__PURE__ */ React6.createElement(Text6, { dimColor: titleDim }, `${step.id} \xB7 ${step.title}`));
7804
+ })));
7805
+ }
7806
+ function RiskLegend() {
7807
+ return /* @__PURE__ */ React6.createElement(Box6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "green" }, "\u25CF"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " low "), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, "\u25CF\u25CF"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " med "), /* @__PURE__ */ React6.createElement(Text6, { color: "red" }, "\u25CF\u25CF\u25CF"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " high"));
7808
+ }
7809
+ var PlanStepList = React6.memo(PlanStepListInner);
7810
+
7058
7811
  // src/cli/ui/markdown.tsx
7059
- import { readFileSync as readFileSync12, statSync as statSync5 } from "fs";
7060
- import { isAbsolute as isAbsolute4, join as join10 } from "path";
7061
- import { Box as Box4, Text as Text4 } from "ink";
7062
- import React4 from "react";
7812
+ import { readFileSync as readFileSync13, statSync as statSync6 } from "fs";
7813
+ import { isAbsolute as isAbsolute4, join as join11 } from "path";
7814
+ import { Box as Box7, Text as Text7 } from "ink";
7815
+ import React7 from "react";
7063
7816
  var SUPERSCRIPT = {
7064
7817
  "0": "\u2070",
7065
7818
  "1": "\xB9",
@@ -7136,10 +7889,10 @@ function validateCitation(url, projectRoot) {
7136
7889
  const parts = parseCitationUrl(url);
7137
7890
  if (!parts || !parts.path) return { ok: false, reason: "empty path" };
7138
7891
  const normalized = parts.path.replace(/^[/\\]+/, "");
7139
- const fullPath = isAbsolute4(normalized) ? normalized : join10(projectRoot, normalized);
7892
+ const fullPath = isAbsolute4(normalized) ? normalized : join11(projectRoot, normalized);
7140
7893
  let stat;
7141
7894
  try {
7142
- stat = statSync5(fullPath);
7895
+ stat = statSync6(fullPath);
7143
7896
  } catch {
7144
7897
  return { ok: false, reason: "file not found" };
7145
7898
  }
@@ -7147,7 +7900,7 @@ function validateCitation(url, projectRoot) {
7147
7900
  if (parts.startLine === void 0) return { ok: true };
7148
7901
  let lineCount;
7149
7902
  try {
7150
- lineCount = readFileSync12(fullPath, "utf8").split("\n").length;
7903
+ lineCount = readFileSync13(fullPath, "utf8").split("\n").length;
7151
7904
  } catch {
7152
7905
  return { ok: false, reason: "unreadable" };
7153
7906
  }
@@ -7184,57 +7937,57 @@ function InlineMd({
7184
7937
  for (const m of text.matchAll(INLINE_RE)) {
7185
7938
  const start = m.index ?? 0;
7186
7939
  if (start > last) {
7187
- parts.push(/* @__PURE__ */ React4.createElement(Text4, { key: `t${idx++}` }, text.slice(last, start)));
7940
+ parts.push(/* @__PURE__ */ React7.createElement(Text7, { key: `t${idx++}` }, text.slice(last, start)));
7188
7941
  }
7189
7942
  if (m[2] !== void 0 && m[3] !== void 0) {
7190
7943
  const linkText = m[2];
7191
7944
  const url = m[3];
7192
7945
  if (isExternalUrl(url)) {
7193
7946
  parts.push(
7194
- /* @__PURE__ */ React4.createElement(Text4, { key: `l${idx++}`, color: "blue", underline: true }, linkText)
7947
+ /* @__PURE__ */ React7.createElement(Text7, { key: `l${idx++}`, color: "blue", underline: true }, linkText)
7195
7948
  );
7196
7949
  } else {
7197
7950
  const status2 = citations?.get(url);
7198
7951
  if (status2 && !status2.ok) {
7199
7952
  parts.push(
7200
- /* @__PURE__ */ React4.createElement(Text4, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u2717`)
7953
+ /* @__PURE__ */ React7.createElement(Text7, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u2717`)
7201
7954
  );
7202
7955
  } else {
7203
7956
  parts.push(
7204
- /* @__PURE__ */ React4.createElement(Text4, { key: `l${idx++}`, color: "cyan", underline: true }, linkText)
7957
+ /* @__PURE__ */ React7.createElement(Text7, { key: `l${idx++}`, color: "cyan", underline: true }, linkText)
7205
7958
  );
7206
7959
  }
7207
7960
  }
7208
7961
  } else if (m[4] !== void 0) {
7209
7962
  parts.push(
7210
- /* @__PURE__ */ React4.createElement(Text4, { key: `b${idx++}`, bold: true }, m[4])
7963
+ /* @__PURE__ */ React7.createElement(Text7, { key: `b${idx++}`, bold: true }, m[4])
7211
7964
  );
7212
7965
  } else if (m[5] !== void 0) {
7213
7966
  const stripped = m[5].replace(/^(\w+)\s+/, "");
7214
7967
  parts.push(
7215
- /* @__PURE__ */ React4.createElement(Text4, { key: `c${idx++}`, color: "yellow" }, stripped)
7968
+ /* @__PURE__ */ React7.createElement(Text7, { key: `c${idx++}`, color: "yellow" }, stripped)
7216
7969
  );
7217
7970
  } else if (m[6] !== void 0) {
7218
7971
  parts.push(
7219
- /* @__PURE__ */ React4.createElement(Text4, { key: `c${idx++}`, color: "yellow" }, m[6])
7972
+ /* @__PURE__ */ React7.createElement(Text7, { key: `c${idx++}`, color: "yellow" }, m[6])
7220
7973
  );
7221
7974
  } else if (m[7] !== void 0) {
7222
7975
  parts.push(
7223
- /* @__PURE__ */ React4.createElement(Text4, { key: `i${idx++}`, italic: true }, m[7])
7976
+ /* @__PURE__ */ React7.createElement(Text7, { key: `i${idx++}`, italic: true }, m[7])
7224
7977
  );
7225
7978
  }
7226
7979
  last = start + m[0].length;
7227
7980
  }
7228
7981
  if (last < text.length) {
7229
- parts.push(/* @__PURE__ */ React4.createElement(Text4, { key: `t${idx++}` }, text.slice(last)));
7982
+ parts.push(/* @__PURE__ */ React7.createElement(Text7, { key: `t${idx++}` }, text.slice(last)));
7230
7983
  }
7231
7984
  if (padTo !== void 0) {
7232
7985
  const seen = visibleWidth(text);
7233
7986
  if (seen < padTo) {
7234
- parts.push(/* @__PURE__ */ React4.createElement(Text4, { key: `pad${idx++}` }, " ".repeat(padTo - seen)));
7987
+ parts.push(/* @__PURE__ */ React7.createElement(Text7, { key: `pad${idx++}` }, " ".repeat(padTo - seen)));
7235
7988
  }
7236
7989
  }
7237
- return /* @__PURE__ */ React4.createElement(Text4, null, parts);
7990
+ return /* @__PURE__ */ React7.createElement(Text7, null, parts);
7238
7991
  }
7239
7992
  function stripInlineMarkup(s) {
7240
7993
  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");
@@ -7430,19 +8183,19 @@ function parseBlocks(raw) {
7430
8183
  function BlockView({ block, citations }) {
7431
8184
  switch (block.kind) {
7432
8185
  case "heading":
7433
- return /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "cyan" }, /* @__PURE__ */ React4.createElement(InlineMd, { text: block.text, citations }));
8186
+ return /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "cyan" }, /* @__PURE__ */ React7.createElement(InlineMd, { text: block.text, citations }));
7434
8187
  case "paragraph":
7435
- return /* @__PURE__ */ React4.createElement(InlineMd, { text: block.text, citations });
8188
+ return /* @__PURE__ */ React7.createElement(InlineMd, { text: block.text, citations });
7436
8189
  case "bullet":
7437
- 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 }))));
8190
+ return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React7.createElement(Box7, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React7.createElement(InlineMd, { text: item, citations }))));
7438
8191
  case "code":
7439
- return /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, block.text));
8192
+ return /* @__PURE__ */ React7.createElement(Box7, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React7.createElement(Text7, { color: "yellow" }, block.text));
7440
8193
  case "edit-block":
7441
- return /* @__PURE__ */ React4.createElement(EditBlockRow, { block });
8194
+ return /* @__PURE__ */ React7.createElement(EditBlockRow, { block });
7442
8195
  case "table":
7443
- return /* @__PURE__ */ React4.createElement(TableBlockRow, { block, citations });
8196
+ return /* @__PURE__ */ React7.createElement(TableBlockRow, { block, citations });
7444
8197
  case "hr":
7445
- 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");
8198
+ return /* @__PURE__ */ React7.createElement(Text7, { 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");
7446
8199
  }
7447
8200
  }
7448
8201
  function splitTableRow(line) {
@@ -7460,14 +8213,14 @@ function TableBlockRow({ block, citations }) {
7460
8213
  widths.push(Math.min(40, Math.max(3, ...cellLengths)));
7461
8214
  }
7462
8215
  const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u253C\u2500");
7463
- return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Box4, null, block.header.map((cell, ci) => (
8216
+ return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Box7, null, block.header.map((cell, ci) => (
7464
8217
  // biome-ignore lint/suspicious/noArrayIndexKey: table columns never reorder — derived from a static header array
7465
- /* @__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 " : "")
7466
- ))), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, separator), block.rows.map((row2, ri) => (
8218
+ /* @__PURE__ */ React7.createElement(Text7, { key: `h-${ci}`, bold: true, color: "cyan" }, /* @__PURE__ */ React7.createElement(InlineMd, { text: cell, padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
8219
+ ))), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, separator), block.rows.map((row2, ri) => (
7467
8220
  // biome-ignore lint/suspicious/noArrayIndexKey: table rows render in source order and don't reorder
7468
- /* @__PURE__ */ React4.createElement(Box4, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
8221
+ /* @__PURE__ */ React7.createElement(Box7, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
7469
8222
  // biome-ignore lint/suspicious/noArrayIndexKey: same — column axis is fixed by the table shape
7470
- /* @__PURE__ */ React4.createElement(Text4, { key: `c-${ri}-${ci}` }, /* @__PURE__ */ React4.createElement(InlineMd, { text: row2[ci] ?? "", padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
8223
+ /* @__PURE__ */ React7.createElement(Text7, { key: `c-${ri}-${ci}` }, /* @__PURE__ */ React7.createElement(InlineMd, { text: row2[ci] ?? "", padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
7471
8224
  )))
7472
8225
  )));
7473
8226
  }
@@ -7487,48 +8240,169 @@ function EditBlockRow({ block }) {
7487
8240
  const isNewFile = block.search.length === 0;
7488
8241
  const searchLines = block.search.split("\n");
7489
8242
  const replaceLines = block.replace.split("\n");
7490
- 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}`))));
8243
+ return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React7.createElement(Box7, null, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "cyan" }, block.filename), isNewFile ? /* @__PURE__ */ React7.createElement(Text7, { color: "green", bold: true }, " (new file)") : null), isNewFile ? null : /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", marginTop: 1 }, searchLines.map((line, i) => /* @__PURE__ */ React7.createElement(Text7, { key: `s-${i}-${line.length}`, color: "red" }, `- ${line}`))), /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", marginTop: isNewFile ? 1 : 0 }, replaceLines.map((line, i) => /* @__PURE__ */ React7.createElement(Text7, { key: `r-${i}-${line.length}`, color: "green" }, `+ ${line}`))));
7491
8244
  }
7492
8245
  function Markdown({ text, projectRoot }) {
7493
8246
  const cleaned = stripMath(text);
7494
8247
  const root = projectRoot ?? process.cwd();
7495
- const citations = React4.useMemo(() => collectCitations(cleaned, root), [cleaned, root]);
7496
- const blocks = React4.useMemo(() => parseBlocks(cleaned), [cleaned]);
8248
+ const citations = React7.useMemo(() => collectCitations(cleaned, root), [cleaned, root]);
8249
+ const blocks = React7.useMemo(() => parseBlocks(cleaned), [cleaned]);
7497
8250
  const broken = [];
7498
8251
  for (const [url, status2] of citations) {
7499
8252
  if (!status2.ok) broken.push({ url, reason: status2.reason });
7500
8253
  }
7501
- 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);
8254
+ return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React7.createElement(BlockView, { key: `${i}-${b.kind}`, block: b, citations })), broken.length > 0 ? /* @__PURE__ */ React7.createElement(BrokenCitationsBlock, { items: broken }) : null);
7502
8255
  }
7503
8256
  function BrokenCitationsBlock({ items }) {
7504
- 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) => (
8257
+ return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1 }, /* @__PURE__ */ React7.createElement(Text7, { 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) => (
7505
8258
  // biome-ignore lint/suspicious/noArrayIndexKey: list is derived from a Map iteration order, stable per render
7506
- /* @__PURE__ */ React4.createElement(Text4, { key: `bc-${i}`, color: "red" }, ` \u2717 ${b.url} \u2192 ${b.reason}`)
8259
+ /* @__PURE__ */ React7.createElement(Text7, { key: `bc-${i}`, color: "red" }, ` \u2717 ${b.url} \u2192 ${b.reason}`)
7507
8260
  )));
7508
8261
  }
7509
8262
 
7510
8263
  // src/cli/ui/ticker.tsx
7511
- import React5, { createContext, useContext, useEffect, useState as useState2 } from "react";
8264
+ import React8, { createContext, useContext, useEffect, useState as useState3 } from "react";
7512
8265
  var TICK_MS = 120;
7513
8266
  var TickContext = createContext(0);
7514
8267
  function TickerProvider({ children, disabled }) {
7515
- const [tick, setTick] = useState2(0);
8268
+ const [tick, setTick] = useState3(0);
7516
8269
  useEffect(() => {
7517
8270
  if (disabled) return;
7518
8271
  const id = setInterval(() => setTick((t) => t + 1), TICK_MS);
7519
8272
  return () => clearInterval(id);
7520
8273
  }, [disabled]);
7521
- return /* @__PURE__ */ React5.createElement(TickContext.Provider, { value: tick }, children);
8274
+ return /* @__PURE__ */ React8.createElement(TickContext.Provider, { value: tick }, children);
7522
8275
  }
7523
8276
  function useTick() {
7524
8277
  return useContext(TickContext);
7525
8278
  }
7526
8279
  function useElapsedSeconds() {
7527
- const [start] = useState2(() => Date.now());
8280
+ const [start] = useState3(() => Date.now());
7528
8281
  useTick();
7529
8282
  return Math.floor((Date.now() - start) / 1e3);
7530
8283
  }
7531
8284
 
8285
+ // src/cli/ui/tool-summary.ts
8286
+ var MAX_SUMMARY_CHARS = 80;
8287
+ var TRAILING_ELLIPSIS = "\u2026";
8288
+ function clip(s, max) {
8289
+ if (s.length <= max) return s;
8290
+ return s.slice(0, Math.max(1, max - TRAILING_ELLIPSIS.length)) + TRAILING_ELLIPSIS;
8291
+ }
8292
+ function firstNonEmptyLine(text) {
8293
+ for (const line of text.split(/\r?\n/)) {
8294
+ const trimmed = line.trim();
8295
+ if (trimmed) return trimmed;
8296
+ }
8297
+ return "";
8298
+ }
8299
+ function formatDuration(ms) {
8300
+ if (!Number.isFinite(ms) || ms < 0) return "";
8301
+ if (ms < 100) return `${Math.round(ms)}ms`;
8302
+ if (ms < 1e3) return `${(ms / 1e3).toFixed(1)}s`;
8303
+ if (ms < 1e4) return `${(ms / 1e3).toFixed(1)}s`;
8304
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
8305
+ const totalSec = Math.round(ms / 1e3);
8306
+ const m = Math.floor(totalSec / 60);
8307
+ const s = totalSec % 60;
8308
+ return s === 0 ? `${m}m` : `${m}m${s}s`;
8309
+ }
8310
+ function formatBytes(n) {
8311
+ if (n < 1e3) return `${n}B`;
8312
+ if (n < 1e6) return `${(n / 1e3).toFixed(1)}KB`;
8313
+ return `${(n / 1e6).toFixed(1)}MB`;
8314
+ }
8315
+ function formatLineCount(text) {
8316
+ const lines = text.split(/\r?\n/).length;
8317
+ return `${lines} line${lines === 1 ? "" : "s"}`;
8318
+ }
8319
+ function summarizeStructured(content) {
8320
+ const trimmed = content.trim();
8321
+ if (!trimmed.startsWith("{")) return null;
8322
+ try {
8323
+ const parsed = JSON.parse(trimmed);
8324
+ if (!parsed || typeof parsed !== "object") return null;
8325
+ const obj = parsed;
8326
+ if (typeof obj.error === "string") {
8327
+ const tag = obj.error.split(":", 1)[0]?.trim() ?? "error";
8328
+ const detail = obj.error.slice(tag.length + 1).trim();
8329
+ const summary = detail ? `${tag} \u2014 ${detail}` : tag;
8330
+ const isControlSignal = tag === "PlanProposedError" || tag === "PlanCheckpointError" || tag === "PlanRevisionProposedError" || tag === "ChoiceRequestedError" || tag === "NeedsConfirmationError";
8331
+ return { summary: clip(summary, MAX_SUMMARY_CHARS), isError: !isControlSignal };
8332
+ }
8333
+ if (obj.kind === "step_completed" && typeof obj.stepId === "string") {
8334
+ const result = typeof obj.result === "string" ? obj.result : "";
8335
+ return {
8336
+ summary: clip(`\u2713 ${obj.stepId}: ${result}`, MAX_SUMMARY_CHARS),
8337
+ isError: false
8338
+ };
8339
+ }
8340
+ return null;
8341
+ } catch {
8342
+ return null;
8343
+ }
8344
+ }
8345
+ function summarizeKnownTool(toolName, content) {
8346
+ if (toolName === "read_file") {
8347
+ const lines = formatLineCount(content);
8348
+ const bytes = formatBytes(content.length);
8349
+ const head = clip(
8350
+ firstNonEmptyLine(content),
8351
+ MAX_SUMMARY_CHARS - lines.length - bytes.length - 8
8352
+ );
8353
+ return {
8354
+ summary: head ? `${head} \xB7 ${lines} \xB7 ${bytes}` : `${lines} \xB7 ${bytes}`,
8355
+ isError: false
8356
+ };
8357
+ }
8358
+ if (toolName === "list_directory" || toolName === "directory_tree") {
8359
+ const entries = content.split(/\r?\n/).filter((l) => l.trim()).length;
8360
+ return { summary: `${entries} entr${entries === 1 ? "y" : "ies"}`, isError: false };
8361
+ }
8362
+ if (toolName === "search_files" || toolName === "search_content") {
8363
+ const matches = content.split(/\r?\n/).filter((l) => l.trim()).length;
8364
+ if (matches === 0) return { summary: "no matches", isError: false };
8365
+ const first = firstNonEmptyLine(content);
8366
+ return {
8367
+ summary: clip(`${matches} match${matches === 1 ? "" : "es"} \xB7 ${first}`, MAX_SUMMARY_CHARS),
8368
+ isError: false
8369
+ };
8370
+ }
8371
+ if (toolName === "run_command" || toolName === "run_background") {
8372
+ const exitMatch = content.match(/exit (?:code )?(-?\d+)/i);
8373
+ const first = firstNonEmptyLine(content);
8374
+ if (exitMatch) {
8375
+ const code = exitMatch[1];
8376
+ const isError = code !== "0";
8377
+ return {
8378
+ summary: clip(`exit ${code} \xB7 ${first}`, MAX_SUMMARY_CHARS),
8379
+ isError
8380
+ };
8381
+ }
8382
+ return { summary: clip(first || "(no output)", MAX_SUMMARY_CHARS), isError: false };
8383
+ }
8384
+ return null;
8385
+ }
8386
+ function summarizeToolResult(toolName, content) {
8387
+ const isExplicitError = content.startsWith("ERROR:");
8388
+ if (isExplicitError) {
8389
+ const stripped = content.slice("ERROR:".length).trim();
8390
+ return { summary: clip(stripped || "(unknown error)", MAX_SUMMARY_CHARS), isError: true };
8391
+ }
8392
+ const structured = summarizeStructured(content);
8393
+ if (structured) return structured;
8394
+ const known = summarizeKnownTool(toolName, content);
8395
+ if (known) return known;
8396
+ const first = firstNonEmptyLine(content);
8397
+ if (!content.trim()) return { summary: "(empty)", isError: false };
8398
+ if (content.length <= MAX_SUMMARY_CHARS) {
8399
+ return { summary: clip(first, MAX_SUMMARY_CHARS), isError: false };
8400
+ }
8401
+ const sizeHint = formatBytes(content.length);
8402
+ const head = clip(first, MAX_SUMMARY_CHARS - sizeHint.length - 3);
8403
+ return { summary: `${head} \xB7 ${sizeHint}`, isError: false };
8404
+ }
8405
+
7532
8406
  // src/cli/ui/EventLog.tsx
7533
8407
  var ROLE_GLYPH = {
7534
8408
  user: "\u25C7",
@@ -7544,53 +8418,98 @@ function RoleGlyph({
7544
8418
  glyph,
7545
8419
  color
7546
8420
  }) {
7547
- return /* @__PURE__ */ React6.createElement(Text5, { color, bold: true }, glyph);
8421
+ return /* @__PURE__ */ React9.createElement(Text8, { color, bold: true }, glyph);
7548
8422
  }
7549
- var EventRow = React6.memo(function EventRow2({
8423
+ var EventRow = React9.memo(function EventRow2({
7550
8424
  event,
7551
8425
  projectRoot
7552
8426
  }) {
7553
8427
  if (event.role === "user") {
7554
- 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));
8428
+ return /* @__PURE__ */ React9.createElement(Box8, { marginTop: event.leadSeparator ? 1 : 0 }, /* @__PURE__ */ React9.createElement(RoleGlyph, { glyph: ROLE_GLYPH.user, color: "cyan" }), /* @__PURE__ */ React9.createElement(Text8, null, " ", event.text));
7555
8429
  }
7556
8430
  if (event.role === "assistant") {
7557
- if (event.streaming) return /* @__PURE__ */ React6.createElement(StreamingAssistant, { event });
7558
- 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));
8431
+ if (event.streaming) return /* @__PURE__ */ React9.createElement(StreamingAssistant, { event });
8432
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(RoleGlyph, { glyph: ROLE_GLYPH.assistant, color: "green" }), event.stats ? /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, ` ${event.stats.model}`) : null), /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, event.branch ? /* @__PURE__ */ React9.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React9.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React9.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React9.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React9.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React9.createElement(Text8, { color: "magenta" }, event.repair) : null));
7559
8433
  }
7560
8434
  if (event.role === "tool") {
7561
- const isError = event.text.startsWith("ERROR:");
7562
- const color = isError ? "red" : "yellow";
7563
- const glyph = isError ? ROLE_GLYPH.toolErr : ROLE_GLYPH.toolOk;
7564
- const marker = isError ? "\u2717" : "\u2192";
7565
- const isEditFile = (event.toolName === "edit_file" || event.toolName?.endsWith("_edit_file")) && !isError;
7566
- 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))));
8435
+ const isExplicitError = event.text.startsWith("ERROR:");
8436
+ const isEditFile = (event.toolName === "edit_file" || event.toolName?.endsWith("_edit_file")) && !isExplicitError;
8437
+ if (isEditFile) {
8438
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(RoleGlyph, { glyph: ROLE_GLYPH.toolOk, color: "yellow" }), /* @__PURE__ */ React9.createElement(Text8, { color: "yellow", bold: true }, ` ${event.toolName ?? "?"}`), /* @__PURE__ */ React9.createElement(Text8, { color: "yellow", dimColor: true }, " \u2192")), /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, /* @__PURE__ */ React9.createElement(EditFileDiff, { text: event.text })));
8439
+ }
8440
+ const summary = summarizeToolResult(event.toolName ?? "?", event.text);
8441
+ const color = summary.isError ? "red" : "yellow";
8442
+ const glyph = summary.isError ? ROLE_GLYPH.toolErr : ROLE_GLYPH.toolOk;
8443
+ const marker = summary.isError ? "\u2717" : "\u2192";
8444
+ const durationLabel = event.durationMs !== void 0 && event.durationMs >= 100 ? ` (${formatDuration(event.durationMs)})` : "";
8445
+ const indexHint = event.toolIndex !== void 0 ? ` /tool ${event.toolIndex}` : "";
8446
+ return /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(RoleGlyph, { glyph, color }), /* @__PURE__ */ React9.createElement(Text8, { color, bold: true }, ` ${event.toolName ?? "?"}`), durationLabel ? /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, durationLabel) : null, /* @__PURE__ */ React9.createElement(Text8, { color, dimColor: true }, ` ${marker} `), /* @__PURE__ */ React9.createElement(Text8, { color: summary.isError ? "red" : void 0, dimColor: !summary.isError }, summary.summary), indexHint ? /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, indexHint) : null);
7567
8447
  }
7568
8448
  if (event.role === "error") {
7569
- 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));
8449
+ return /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(RoleGlyph, { glyph: ROLE_GLYPH.error, color: "red" }), /* @__PURE__ */ React9.createElement(Text8, { color: "red" }, " ", event.text));
7570
8450
  }
7571
8451
  if (event.role === "info") {
7572
- return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, event.text));
8452
+ return /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, event.text));
7573
8453
  }
7574
8454
  if (event.role === "plan") {
7575
- return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, "\u{1F4CB} plan proposed \u2014 pick a choice below")), /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Markdown, { text: event.text, projectRoot })));
8455
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "cyan" }, "\u{1F4CB} plan proposed \u2014 pick a choice below")), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Markdown, { text: event.text, projectRoot })));
8456
+ }
8457
+ if (event.role === "step-progress") {
8458
+ const sp = event.stepProgress;
8459
+ const counter = sp && sp.total > 0 ? ` (${sp.completed}/${sp.total})` : "";
8460
+ const label = sp?.title ? `${sp.stepId} \xB7 ${sp.title}` : sp?.stepId ?? "";
8461
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { color: "green", bold: true }, "\u2713"), /* @__PURE__ */ React9.createElement(Text8, { color: "green" }, ` ${label}`), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, counter)), event.text ? /* @__PURE__ */ React9.createElement(Box8, { paddingLeft: 2 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, event.text)) : null, sp?.notes ? /* @__PURE__ */ React9.createElement(Box8, { paddingLeft: 2 }, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow", dimColor: true }, `note: ${sp.notes}`)) : null);
8462
+ }
8463
+ if (event.role === "plan-resumed") {
8464
+ const rp = event.resumedPlan;
8465
+ if (!rp || rp.steps.length === 0) return null;
8466
+ const total = rp.steps.length;
8467
+ const done = rp.completedStepIds.length;
8468
+ const completedSet = new Set(rp.completedStepIds);
8469
+ const statuses = new Map(
8470
+ rp.steps.map((s) => [
8471
+ s.id,
8472
+ completedSet.has(s.id) ? "done" : "pending"
8473
+ ])
8474
+ );
8475
+ const nextStep = rp.steps.find((s) => !completedSet.has(s.id));
8476
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "cyan" }, "\u25B8 resumed plan"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, ` ${done}/${total} done \xB7 last touched ${rp.relativeTime}`)), rp.summary ? /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { color: "cyan" }, ` ${rp.summary}`)) : null), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React9.createElement(PlanStepList, { steps: rp.steps, statuses, focusStepId: nextStep?.id })));
8477
+ }
8478
+ if (event.role === "plan-replay") {
8479
+ const r = event.replayPlan;
8480
+ if (!r || r.steps.length === 0) return null;
8481
+ const total = r.steps.length;
8482
+ const completedSet = new Set(r.completedStepIds);
8483
+ const done = completedSet.size;
8484
+ const statuses = new Map(
8485
+ r.steps.map((s) => [s.id, completedSet.has(s.id) ? "done" : "pending"])
8486
+ );
8487
+ const navHint = r.total > 1 ? ` \xB7 ${r.index}/${r.total}` : "";
8488
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, dimColor: true }, "\u23EA replay"), /* @__PURE__ */ React9.createElement(
8489
+ Text8,
8490
+ {
8491
+ dimColor: true
8492
+ },
8493
+ ` completed ${r.relativeTime} \xB7 ${done}/${total} done${navHint}`
8494
+ )), r.summary ? /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, ` ${r.summary}`)) : null, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, ` ${r.archiveBasename}`))), r.body ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Markdown, { text: r.body, projectRoot })) : null, /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React9.createElement(PlanStepList, { steps: r.steps, statuses })), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, r.total > 1 ? `(read-only \xB7 /replay ${r.index === 1 ? 2 : 1} for the ${r.index === 1 ? "next" : "newest"} archive)` : "(read-only \xB7 this is an archived plan)")));
7576
8495
  }
7577
8496
  if (event.role === "warning") {
7578
- return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(RoleGlyph, { glyph: ROLE_GLYPH.warning, color: "yellow" }), /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, " ", event.text));
8497
+ return /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(RoleGlyph, { glyph: ROLE_GLYPH.warning, color: "yellow" }), /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, " ", event.text));
7579
8498
  }
7580
- return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, null, event.text));
8499
+ return /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, null, event.text));
7581
8500
  });
7582
8501
  function EditFileDiff({ text }) {
7583
8502
  const lines = text.split(/\r?\n/);
7584
8503
  const [statusHeader, hunkHeader, ...body] = lines;
7585
- 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) => {
8504
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, ` ${statusHeader ?? ""}`), hunkHeader !== void 0 ? /* @__PURE__ */ React9.createElement(Text8, { color: "cyan", bold: true }, hunkHeader) : null, body.map((line, i) => {
7586
8505
  const key = `${i}-${line.slice(0, 32)}`;
7587
8506
  if (line.startsWith("- ")) {
7588
- return /* @__PURE__ */ React6.createElement(Text5, { key, color: "red" }, line);
8507
+ return /* @__PURE__ */ React9.createElement(Text8, { key, color: "red" }, line);
7589
8508
  }
7590
8509
  if (line.startsWith("+ ")) {
7591
- return /* @__PURE__ */ React6.createElement(Text5, { key, color: "green" }, line);
8510
+ return /* @__PURE__ */ React9.createElement(Text8, { key, color: "green" }, line);
7592
8511
  }
7593
- return /* @__PURE__ */ React6.createElement(Text5, { key, dimColor: true }, line);
8512
+ return /* @__PURE__ */ React9.createElement(Text8, { key, dimColor: true }, line);
7594
8513
  }));
7595
8514
  }
7596
8515
  function BranchBlock({ branch: branch2 }) {
@@ -7599,33 +8518,33 @@ function BranchBlock({ branch: branch2 }) {
7599
8518
  const t = (branch2.temperatures[i] ?? 0).toFixed(1);
7600
8519
  return `${marker} #${i} T=${t} u=${u}`;
7601
8520
  }).join(" ");
7602
- return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { color: "blue" }, "\u2387 branched ", /* @__PURE__ */ React6.createElement(Text5, { bold: true }, branch2.budget), ` samples \u2192 picked #${branch2.chosenIndex} `, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, per)));
8521
+ return /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, "\u2387 branched ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, branch2.budget), ` samples \u2192 picked #${branch2.chosenIndex} `, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, per)));
7603
8522
  }
7604
8523
  function ReasoningBlock({ reasoning }) {
7605
8524
  const max = 260;
7606
8525
  const flat = reasoning.replace(/\s+/g, " ").trim();
7607
8526
  const preview = flat.length <= max ? flat : `\u2026 (+${flat.length - max} earlier chars) ${flat.slice(-max)}`;
7608
- return /* @__PURE__ */ React6.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "\u258F "), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true, italic: true }, "thinking ", preview));
8527
+ return /* @__PURE__ */ React9.createElement(Box8, { marginBottom: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "\u258F "), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, "thinking ", preview));
7609
8528
  }
7610
8529
  function Elapsed() {
7611
8530
  const s = useElapsedSeconds();
7612
8531
  const mm = String(Math.floor(s / 60)).padStart(2, "0");
7613
8532
  const ss = String(s % 60).padStart(2, "0");
7614
- return /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, `${mm}:${ss}`);
8533
+ return /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, `${mm}:${ss}`);
7615
8534
  }
7616
8535
  function PulsingAssistantGlyph() {
7617
8536
  const tick = useTick();
7618
8537
  const on = Math.floor(tick / 4) % 2 === 0;
7619
- return /* @__PURE__ */ React6.createElement(Text5, { color: "green", bold: true }, on ? ROLE_GLYPH.assistant : ROLE_GLYPH.assistantPulse);
8538
+ return /* @__PURE__ */ React9.createElement(Text8, { color: "green", bold: true }, on ? ROLE_GLYPH.assistant : ROLE_GLYPH.assistantPulse);
7620
8539
  }
7621
8540
  function StreamingAssistant({ event }) {
7622
8541
  if (event.branchProgress) {
7623
8542
  const p = event.branchProgress;
7624
8543
  if (p.completed === 0) {
7625
- 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"));
8544
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, " \u2387 launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026 "), /* @__PURE__ */ React9.createElement(Elapsed, null)), /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, " ", "spread across T=0.0/0.5/1.0 \xB7 reasoner typically takes 30-90s \u2014 this is normal"));
7626
8545
  }
7627
8546
  const pct2 = Math.round(p.completed / p.total * 100);
7628
- 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"));
8547
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, " \u2387 branching ", p.completed, "/", p.total, " (", pct2, "%) "), /* @__PURE__ */ React9.createElement(Elapsed, null)), /* @__PURE__ */ React9.createElement(Text8, { 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"));
7629
8548
  }
7630
8549
  const tail = lastLine(event.text, 140);
7631
8550
  const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
@@ -7655,16 +8574,16 @@ function StreamingAssistant({ event }) {
7655
8574
  label = parts.join(" \xB7 ");
7656
8575
  labelColor = "green";
7657
8576
  }
7658
- 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 ? (
8577
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React9.createElement(Text8, null, " "), /* @__PURE__ */ React9.createElement(Pulse, null), /* @__PURE__ */ React9.createElement(Text8, { color: labelColor }, ` ${label} `), /* @__PURE__ */ React9.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "\u25B8 ", tail) : preFirstByte ? (
7659
8578
  // Non-dim yellow: first-time users misread the dim version as
7660
8579
  // "app frozen". The reassurance has to be VISIBLE to do its job.
7661
- /* @__PURE__ */ React6.createElement(Text5, { color: "yellow", italic: true }, " waiting for first byte \u2014 this is normal, typically 5-60s depending on model + load")
7662
- ) : 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);
8580
+ /* @__PURE__ */ React9.createElement(Text8, { color: "yellow", italic: true }, " waiting for first byte \u2014 this is normal, typically 5-60s depending on model + load")
8581
+ ) : reasoningOnly ? /* @__PURE__ */ React9.createElement(Text8, { 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__ */ React9.createElement(Text8, { color: "magenta", italic: true }, " tool-call arguments streaming \u2014 the model is about to dispatch a tool") : event.reasoning ? /* @__PURE__ */ React9.createElement(Text8, { color: "yellow", italic: true }, " R1 still reasoning \u2014 body text or tool call arrives when thinking finishes") : null);
7663
8582
  }
7664
8583
  function Pulse() {
7665
8584
  const tick = useTick();
7666
8585
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
7667
- return /* @__PURE__ */ React6.createElement(Text5, { color: "cyan" }, frames[Math.floor(tick / 4) % frames.length]);
8586
+ return /* @__PURE__ */ React9.createElement(Text8, { color: "cyan" }, frames[Math.floor(tick / 4) % frames.length]);
7668
8587
  }
7669
8588
  function formatToolCallIndex(tb) {
7670
8589
  if (!tb || tb.index === void 0) return "";
@@ -7683,36 +8602,17 @@ function lastLine(s, maxChars) {
7683
8602
  }
7684
8603
  function StatsLine({ stats: stats2 }) {
7685
8604
  const hit = (stats2.cacheHitRatio * 100).toFixed(1);
7686
- return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "\u258F "), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "cache ", hit, "% \xB7 tokens ", stats2.usage.promptTokens, " \u2192 ", stats2.usage.completionTokens, " \xB7 $", stats2.cost.toFixed(6)));
7687
- }
7688
- function truncate2(s, max) {
7689
- if (s.length <= max) return s;
7690
- if (s.startsWith("ERROR:")) {
7691
- const firstNl = s.indexOf("\n");
7692
- const firstLine = firstNl === -1 ? s : s.slice(0, firstNl);
7693
- if (firstLine.length >= max) {
7694
- return `${firstLine.slice(0, max)}\u2026 (+${s.length - max} chars \u2014 /tool N for full)`;
7695
- }
7696
- const budget = max - firstLine.length - 10;
7697
- const tail = s.slice(-budget);
7698
- const skipped2 = s.length - firstLine.length - tail.length;
7699
- return `${firstLine}
7700
- \u2026 (+${skipped2} chars) \u2026
7701
- ${tail}`;
7702
- }
7703
- const skipped = s.length - max;
7704
- return `\u2026 (+${skipped} earlier chars \u2014 /tool N for full) \u2026
7705
- ${s.slice(-max)}`;
8605
+ return /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "\u258F "), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "cache ", hit, "% \xB7 tokens ", stats2.usage.promptTokens, " \u2192 ", stats2.usage.completionTokens, " \xB7 $", stats2.cost.toFixed(6)));
7706
8606
  }
7707
8607
 
7708
8608
  // src/cli/ui/LiveRows.tsx
7709
- import { Box as Box6, Text as Text6 } from "ink";
7710
- import React7 from "react";
8609
+ import { Box as Box9, Text as Text9 } from "ink";
8610
+ import React10 from "react";
7711
8611
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
7712
8612
  function StatusRow({ text }) {
7713
8613
  const tick = useTick();
7714
8614
  const elapsed = useElapsedSeconds();
7715
- return /* @__PURE__ */ React7.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React7.createElement(Text6, { color: "magenta" }, ` ${text}`), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, ` ${elapsed}s`));
8615
+ return /* @__PURE__ */ React10.createElement(Box9, { marginY: 1 }, /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, ` ${text}`), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, ` ${elapsed}s`));
7716
8616
  }
7717
8617
  function ModeStatusBar({
7718
8618
  editMode,
@@ -7724,15 +8624,15 @@ function ModeStatusBar({
7724
8624
  }) {
7725
8625
  useTick();
7726
8626
  const running = jobs2?.runningCount() ?? 0;
7727
- const jobsTag = running > 0 ? /* @__PURE__ */ React7.createElement(Text6, { color: "yellow", bold: true }, ` \xB7 \u23F5 ${running} job${running === 1 ? "" : "s"}`) : null;
8627
+ const jobsTag = running > 0 ? /* @__PURE__ */ React10.createElement(Text9, { color: "yellow", bold: true }, ` \xB7 \u23F5 ${running} job${running === 1 ? "" : "s"}`) : null;
7728
8628
  if (planMode) {
7729
- return /* @__PURE__ */ React7.createElement(Box6, { paddingX: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color: "red", bold: true, inverse: flash }, "\u25B8 PLAN"), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, " writes gated \u2014 submit_plan + approval required \xB7 /plan off to leave"), jobsTag);
8629
+ return /* @__PURE__ */ React10.createElement(Box9, { paddingX: 1 }, /* @__PURE__ */ React10.createElement(Text9, { color: "red", bold: true, inverse: flash }, "\u25B8 PLAN"), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " writes gated \u2014 submit_plan + approval required \xB7 /plan off to leave"), jobsTag);
7730
8630
  }
7731
8631
  const label = editMode === "auto" ? "AUTO" : "review";
7732
8632
  const color = editMode === "auto" ? "magenta" : "cyan";
7733
8633
  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";
7734
8634
  const flip = editMode === "auto" ? "Shift+Tab \u2192 review" : "Shift+Tab \u2192 AUTO";
7735
- return /* @__PURE__ */ React7.createElement(Box6, { paddingX: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color, bold: true, inverse: flash }, `\u25B8 ${label}`), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, ` ${mid} \xB7 ${flip}`), jobsTag);
8635
+ return /* @__PURE__ */ React10.createElement(Box9, { paddingX: 1 }, /* @__PURE__ */ React10.createElement(Text9, { color, bold: true, inverse: flash }, `\u25B8 ${label}`), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, ` ${mid} \xB7 ${flip}`), jobsTag);
7736
8636
  }
7737
8637
  function UndoBanner({
7738
8638
  banner
@@ -7742,14 +8642,14 @@ function UndoBanner({
7742
8642
  const remainingSec = Math.ceil(remainingMs / 1e3);
7743
8643
  const ok = banner.results.filter((r) => r.status === "applied" || r.status === "created").length;
7744
8644
  const total = banner.results.length;
7745
- return /* @__PURE__ */ React7.createElement(Box6, { marginY: 1, borderStyle: "round", borderColor: "magenta", paddingX: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color: "magenta", bold: true }, "\u2713 auto-applied "), /* @__PURE__ */ React7.createElement(Text6, { color: "magenta" }, `${ok}/${total} edit${total === 1 ? "" : "s"}`), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, " \xB7 press "), /* @__PURE__ */ React7.createElement(Text6, { color: "magenta", bold: true }, "u"), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, " to undo ("), /* @__PURE__ */ React7.createElement(Text6, { color: remainingSec <= 1 ? "red" : "magenta" }, `${remainingSec}s`), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, ")"));
8645
+ return /* @__PURE__ */ React10.createElement(Box9, { marginY: 1, borderStyle: "round", borderColor: "magenta", paddingX: 1 }, /* @__PURE__ */ React10.createElement(Text9, { color: "magenta", bold: true }, "\u2713 auto-applied "), /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, `${ok}/${total} edit${total === 1 ? "" : "s"}`), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \xB7 press "), /* @__PURE__ */ React10.createElement(Text9, { color: "magenta", bold: true }, "u"), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " to undo ("), /* @__PURE__ */ React10.createElement(Text9, { color: remainingSec <= 1 ? "red" : "magenta" }, `${remainingSec}s`), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, ")"));
7746
8646
  }
7747
8647
  function SubagentRow({
7748
8648
  activity
7749
8649
  }) {
7750
8650
  const tick = useTick();
7751
8651
  const seconds = (activity.elapsedMs / 1e3).toFixed(1);
7752
- return /* @__PURE__ */ React7.createElement(Box6, { paddingLeft: 2 }, /* @__PURE__ */ React7.createElement(Text6, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React7.createElement(Text6, { color: "magenta" }, ` \u232C subagent: ${activity.task}`), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, ` \xB7 iter ${activity.iter} \xB7 ${seconds}s`));
8652
+ return /* @__PURE__ */ React10.createElement(Box9, { paddingLeft: 2 }, /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, ` \u232C subagent: ${activity.task}`), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, ` \xB7 iter ${activity.iter} \xB7 ${seconds}s`));
7753
8653
  }
7754
8654
  function OngoingToolRow({
7755
8655
  tool: tool2,
@@ -7758,7 +8658,7 @@ function OngoingToolRow({
7758
8658
  const tick = useTick();
7759
8659
  const elapsed = useElapsedSeconds();
7760
8660
  const summary = summarizeToolArgs(tool2.name, tool2.args);
7761
- return /* @__PURE__ */ React7.createElement(Box6, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Box6, null, /* @__PURE__ */ React7.createElement(Text6, { color: "cyan" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React7.createElement(Text6, { color: "yellow" }, ` tool<${tool2.name}> running\u2026`), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, ` ${elapsed}s`)), progress ? /* @__PURE__ */ React7.createElement(Box6, { paddingLeft: 2 }, /* @__PURE__ */ React7.createElement(Text6, { color: "cyan" }, renderProgressLine(progress))) : null, summary ? /* @__PURE__ */ React7.createElement(Box6, { paddingLeft: 2 }, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, summary)) : null);
8661
+ return /* @__PURE__ */ React10.createElement(Box9, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React10.createElement(Box9, null, /* @__PURE__ */ React10.createElement(Text9, { color: "cyan" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React10.createElement(Text9, { color: "yellow" }, ` tool<${tool2.name}> running\u2026`), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, ` ${elapsed}s`)), progress ? /* @__PURE__ */ React10.createElement(Box9, { paddingLeft: 2 }, /* @__PURE__ */ React10.createElement(Text9, { color: "cyan" }, renderProgressLine(progress))) : null, summary ? /* @__PURE__ */ React10.createElement(Box9, { paddingLeft: 2 }, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, summary)) : null);
7762
8662
  }
7763
8663
  function renderProgressLine(p) {
7764
8664
  const msg = p.message ? ` ${p.message}` : "";
@@ -7813,116 +8713,71 @@ function summarizeToolArgs(name, args) {
7813
8713
  return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
7814
8714
  }
7815
8715
 
7816
- // src/cli/ui/PlanConfirm.tsx
7817
- import { Box as Box8, Text as Text8 } from "ink";
7818
- import React9 from "react";
7819
-
7820
- // src/cli/ui/Select.tsx
7821
- import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
7822
- import React8, { useState as useState3 } from "react";
7823
- function SingleSelect({
7824
- items,
7825
- initialValue,
7826
- onSubmit,
7827
- onCancel,
7828
- footer
8716
+ // src/cli/ui/PlanCheckpointConfirm.tsx
8717
+ import { Box as Box10, Text as Text10 } from "ink";
8718
+ import React11 from "react";
8719
+ function PlanCheckpointConfirmInner({
8720
+ stepId,
8721
+ title,
8722
+ completed,
8723
+ total,
8724
+ steps,
8725
+ completedStepIds,
8726
+ onChoose
7829
8727
  }) {
7830
- const initialIndex = Math.max(
7831
- 0,
7832
- items.findIndex((i) => i.value === initialValue && !i.disabled)
7833
- );
7834
- const [index, setIndex] = useState3(initialIndex === -1 ? 0 : initialIndex);
7835
- useInput2((_input, key) => {
7836
- if (key.upArrow) {
7837
- setIndex((i) => findNextEnabled(items, i, -1));
7838
- } else if (key.downArrow) {
7839
- setIndex((i) => findNextEnabled(items, i, 1));
7840
- } else if (key.return) {
7841
- const chosen = items[index];
7842
- if (chosen && !chosen.disabled) onSubmit(chosen.value);
7843
- } else if (key.escape && onCancel) {
7844
- onCancel();
7845
- }
7846
- });
7847
- return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React8.createElement(
7848
- SelectRow,
8728
+ const label = title ? `${stepId} \xB7 ${title}` : stepId;
8729
+ const counter = total > 0 ? ` (${completed}/${total})` : "";
8730
+ const isLast = total > 0 && completed >= total;
8731
+ const statuses = buildStatusMap(steps, completedStepIds, stepId, isLast);
8732
+ return /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Text10, { bold: true, color: "green" }, "\u25B8 checkpoint \u2014 step done"), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, ` ${label}${counter}`)), steps && steps.length > 0 ? /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React11.createElement(PlanStepList, { steps, statuses, focusStepId: stepId })) : null, /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React11.createElement(
8733
+ SingleSelect,
7849
8734
  {
7850
- key: item.value,
7851
- item,
7852
- active: i === index,
7853
- marker: i === index ? "\u25B8" : " "
8735
+ initialValue: isLast ? "stop" : "continue",
8736
+ items: [
8737
+ {
8738
+ value: "continue",
8739
+ label: "Continue \u2014 run the next step",
8740
+ hint: "Model resumes with the next step. Use this when the result looks right and you don't need to tweak the remaining plan."
8741
+ },
8742
+ {
8743
+ value: "revise",
8744
+ label: "Revise \u2014 give feedback before the next step",
8745
+ hint: "Stay paused, type guidance (scope changes, skip steps, alternative approach). The model adjusts the remaining plan based on your message."
8746
+ },
8747
+ {
8748
+ value: "stop",
8749
+ label: "Stop \u2014 end the plan here",
8750
+ hint: "Model summarizes what was done and ends. Remaining steps are skipped."
8751
+ }
8752
+ ],
8753
+ onSubmit: (v) => onChoose(v),
8754
+ onCancel: () => onChoose("stop"),
8755
+ footer: "[\u2191\u2193] navigate \xB7 [Enter] select \xB7 [Esc] stop"
7854
8756
  }
7855
- )), footer ? /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, footer)) : null);
8757
+ )));
7856
8758
  }
7857
- function MultiSelect({
7858
- items,
7859
- initialSelected = [],
7860
- onSubmit,
7861
- onCancel,
7862
- footer
7863
- }) {
7864
- const [index, setIndex] = useState3(() => {
7865
- const first = items.findIndex((i) => !i.disabled);
7866
- return first === -1 ? 0 : first;
7867
- });
7868
- const [selected, setSelected] = useState3(new Set(initialSelected));
7869
- useInput2((input, key) => {
7870
- if (key.upArrow) {
7871
- setIndex((i) => findNextEnabled(items, i, -1));
7872
- } else if (key.downArrow) {
7873
- setIndex((i) => findNextEnabled(items, i, 1));
7874
- } else if (input === " ") {
7875
- const item = items[index];
7876
- if (!item || item.disabled) return;
7877
- setSelected((prev) => {
7878
- const next = new Set(prev);
7879
- if (next.has(item.value)) next.delete(item.value);
7880
- else next.add(item.value);
7881
- return next;
7882
- });
7883
- } else if (key.return) {
7884
- const ordered = items.filter((i) => selected.has(i.value)).map((i) => i.value);
7885
- onSubmit(ordered);
7886
- } else if (key.escape && onCancel) {
7887
- onCancel();
8759
+ var PlanCheckpointConfirm = React11.memo(PlanCheckpointConfirmInner);
8760
+ function buildStatusMap(steps, completedStepIds, currentStepId, isLast) {
8761
+ const map = /* @__PURE__ */ new Map();
8762
+ if (!steps) return map;
8763
+ for (const step of steps) {
8764
+ if (completedStepIds?.has(step.id) || step.id === currentStepId) {
8765
+ map.set(step.id, "done");
8766
+ } else {
8767
+ map.set(step.id, "pending");
7888
8768
  }
7889
- });
7890
- return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column" }, items.map((item, i) => {
7891
- const checked = selected.has(item.value);
7892
- const marker = checked ? "[x]" : "[ ]";
7893
- return /* @__PURE__ */ React8.createElement(
7894
- SelectRow,
7895
- {
7896
- key: item.value,
7897
- item,
7898
- active: i === index,
7899
- marker: `${i === index ? "\u25B8" : " "} ${marker}`
7900
- }
7901
- );
7902
- }), footer ? /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, footer)) : null);
7903
- }
7904
- function SelectRow({
7905
- item,
7906
- active,
7907
- marker
7908
- }) {
7909
- const color = item.disabled ? "gray" : active ? "cyan" : void 0;
7910
- return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column" }, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React8.createElement(Box7, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, item.hint)) : null);
7911
- }
7912
- function findNextEnabled(items, from, step) {
7913
- if (items.length === 0) return 0;
7914
- let i = from;
7915
- for (let tries = 0; tries < items.length; tries++) {
7916
- i = (i + step + items.length) % items.length;
7917
- if (!items[i]?.disabled) return i;
7918
8769
  }
7919
- return from;
8770
+ if (isLast) {
8771
+ }
8772
+ return map;
7920
8773
  }
7921
8774
 
7922
8775
  // src/cli/ui/PlanConfirm.tsx
7923
- function PlanConfirmInner({ plan: plan2, onChoose }) {
8776
+ import { Box as Box11, Text as Text11 } from "ink";
8777
+ import React12 from "react";
8778
+ function PlanConfirmInner({ plan: plan2, steps, summary, onChoose }) {
7924
8779
  const hasOpenQuestions = /^#{1,6}\s*(open[-\s]?questions?|risks?|unknowns?|assumptions?|unclear)/im.test(plan2) || /^#{1,6}\s*(待确认|开放问题|风险|未知|假设|不确定)/im.test(plan2);
7925
- return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "cyan" }, "\u25B8 plan proposed (full text above) \u2014 approve / refine / cancel")), hasOpenQuestions ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u25B2 the plan flags open questions or risks \u2014 pick", " ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(
8780
+ return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Text11, { bold: true, color: "cyan" }, "\u25B8 plan proposed (full text above) \u2014 approve / refine / cancel")), summary ? /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Text11, { color: "cyan" }, ` ${summary}`)) : null), hasOpenQuestions ? /* @__PURE__ */ React12.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text11, { color: "yellow" }, "\u25B2 the plan flags open questions or risks \u2014 pick", " ", /* @__PURE__ */ React12.createElement(Text11, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, steps && steps.length > 0 ? /* @__PURE__ */ React12.createElement(Box11, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React12.createElement(PlanStepList, { steps })) : null, /* @__PURE__ */ React12.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(
7926
8781
  SingleSelect,
7927
8782
  {
7928
8783
  initialValue: hasOpenQuestions ? "refine" : "approve",
@@ -7949,11 +8804,11 @@ function PlanConfirmInner({ plan: plan2, onChoose }) {
7949
8804
  }
7950
8805
  )));
7951
8806
  }
7952
- var PlanConfirm = React9.memo(PlanConfirmInner);
8807
+ var PlanConfirm = React12.memo(PlanConfirmInner);
7953
8808
 
7954
8809
  // src/cli/ui/PlanRefineInput.tsx
7955
- import { Box as Box9, Text as Text9, useInput as useInput3 } from "ink";
7956
- import React10, { useState as useState4 } from "react";
8810
+ import { Box as Box12, Text as Text12, useInput as useInput3 } from "ink";
8811
+ import React13, { useState as useState4 } from "react";
7957
8812
  function PlanRefineInput({ mode: mode2, onSubmit, onCancel }) {
7958
8813
  const [value, setValue] = useState4("");
7959
8814
  useInput3((input, key) => {
@@ -7973,15 +8828,84 @@ function PlanRefineInput({ mode: mode2, onSubmit, onCancel }) {
7973
8828
  setValue((v) => v + input);
7974
8829
  }
7975
8830
  });
7976
- const title = mode2 === "approve" ? "\u25B8 approving \u2014 any last instructions or answers to open questions?" : "\u25B8 refining \u2014 what should the model change?";
7977
- const hint = mode2 === "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.";
7978
- const blankHint = mode2 === "approve" ? " (Enter with blank = approve without extra instructions.)" : " (Enter with blank = ask the model to list concrete questions.)";
7979
- 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" }, title)), /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, hint, " Enter to send \xB7 Esc to return to the picker.", value === "" ? blankHint : "")), /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(Text9, null, /* @__PURE__ */ React10.createElement(Text9, { color: "yellow" }, "\u203A "), /* @__PURE__ */ React10.createElement(Text9, null, value || " "), /* @__PURE__ */ React10.createElement(Text9, { color: "yellow" }, "\u258D"))));
8831
+ const title = mode2 === "approve" ? "\u25B8 approving \u2014 any last instructions or answers to open questions?" : mode2 === "checkpoint-revise" ? "\u25B8 revising \u2014 what should change before the next step?" : mode2 === "choice-custom" ? "\u25B8 custom answer \u2014 type whatever fits" : "\u25B8 refining \u2014 what should the model change?";
8832
+ const hint = mode2 === "approve" ? "Answer questions the plan raised, add constraints, or just press Enter to approve as-is." : mode2 === "checkpoint-revise" ? "Scope change, skip steps, alternative approach \u2014 the model will adjust the remaining plan based on this." : mode2 === "choice-custom" ? "Free-form reply. The model reads it verbatim and proceeds \u2014 no need to match the listed options." : "Describe what's wrong or missing, or answer questions the plan raised.";
8833
+ const blankHint = mode2 === "approve" ? " (Enter with blank = approve without extra instructions.)" : mode2 === "checkpoint-revise" ? " (Enter with blank = continue with the current plan.)" : mode2 === "choice-custom" ? " (Enter with blank = ask the model what you actually want.)" : " (Enter with blank = ask the model to list concrete questions.)";
8834
+ return /* @__PURE__ */ React13.createElement(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React13.createElement(Box12, null, /* @__PURE__ */ React13.createElement(Text12, { bold: true, color: "yellow" }, title)), /* @__PURE__ */ React13.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, hint, " Enter to send \xB7 Esc to return to the picker.", value === "" ? blankHint : "")), /* @__PURE__ */ React13.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React13.createElement(Text12, null, /* @__PURE__ */ React13.createElement(Text12, { color: "yellow" }, "\u203A "), /* @__PURE__ */ React13.createElement(Text12, null, value || " "), /* @__PURE__ */ React13.createElement(Text12, { color: "yellow" }, "\u258D"))));
7980
8835
  }
7981
8836
 
8837
+ // src/cli/ui/PlanReviseConfirm.tsx
8838
+ import { Box as Box13, Text as Text13 } from "ink";
8839
+ import React14 from "react";
8840
+ function computeDiff(oldSteps, newSteps) {
8841
+ const oldIds = new Set(oldSteps.map((s) => s.id));
8842
+ const newIds = new Set(newSteps.map((s) => s.id));
8843
+ const rows = [];
8844
+ for (const s of oldSteps) {
8845
+ if (!newIds.has(s.id)) rows.push({ kind: "removed", step: s });
8846
+ }
8847
+ for (const s of newSteps) {
8848
+ rows.push({ kind: oldIds.has(s.id) ? "kept" : "added", step: s });
8849
+ }
8850
+ return rows;
8851
+ }
8852
+ function riskDots2(risk) {
8853
+ switch (risk) {
8854
+ case "high":
8855
+ return { dots: "\u25CF\u25CF\u25CF", color: "red" };
8856
+ case "med":
8857
+ return { dots: "\u25CF\u25CF ", color: "yellow" };
8858
+ case "low":
8859
+ return { dots: "\u25CF ", color: "green" };
8860
+ default:
8861
+ return { dots: " ", color: "gray" };
8862
+ }
8863
+ }
8864
+ function PlanReviseConfirmInner({
8865
+ reason,
8866
+ oldRemaining,
8867
+ newRemaining,
8868
+ summary,
8869
+ onChoose
8870
+ }) {
8871
+ const rows = computeDiff(oldRemaining, newRemaining);
8872
+ const removedCount = rows.filter((r) => r.kind === "removed").length;
8873
+ const addedCount = rows.filter((r) => r.kind === "added").length;
8874
+ const keptCount = rows.filter((r) => r.kind === "kept").length;
8875
+ return /* @__PURE__ */ React14.createElement(Box13, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React14.createElement(Box13, null, /* @__PURE__ */ React14.createElement(Text13, { bold: true, color: "yellow" }, "\u270F plan revision proposed"), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, ` \u2212${removedCount} +${addedCount} (${keptCount} kept)`)), /* @__PURE__ */ React14.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React14.createElement(Text13, null, reason)), summary ? /* @__PURE__ */ React14.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, `updated summary: ${summary}`)) : null, /* @__PURE__ */ React14.createElement(Box13, { marginTop: 1, flexDirection: "column" }, rows.map((row2) => {
8876
+ const risk = riskDots2(row2.step.risk);
8877
+ const prefix = row2.kind === "removed" ? "\u2212" : row2.kind === "added" ? "+" : " ";
8878
+ const prefixColor = row2.kind === "removed" ? "red" : row2.kind === "added" ? "green" : "gray";
8879
+ const dim = row2.kind === "kept";
8880
+ const strike = row2.kind === "removed";
8881
+ return /* @__PURE__ */ React14.createElement(Box13, { key: `${row2.kind}-${row2.step.id}` }, /* @__PURE__ */ React14.createElement(Text13, { color: prefixColor, bold: true }, `${prefix} `), /* @__PURE__ */ React14.createElement(Text13, { color: risk.color, bold: true, dimColor: dim }, risk.dots), /* @__PURE__ */ React14.createElement(Text13, { dimColor: dim, strikethrough: strike }, ` ${row2.step.id} \xB7 ${row2.step.title}`));
8882
+ })), /* @__PURE__ */ React14.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React14.createElement(
8883
+ SingleSelect,
8884
+ {
8885
+ initialValue: "accept",
8886
+ items: [
8887
+ {
8888
+ value: "accept",
8889
+ label: "Accept revision \u2014 apply the new step list",
8890
+ hint: "Replaces the remaining plan with the proposed steps. Done steps are untouched."
8891
+ },
8892
+ {
8893
+ value: "reject",
8894
+ label: "Reject \u2014 keep the original plan",
8895
+ hint: "Drops the proposal. Model continues with the original remaining steps."
8896
+ }
8897
+ ],
8898
+ onSubmit: (v) => onChoose(v),
8899
+ onCancel: () => onChoose("reject"),
8900
+ footer: "[\u2191\u2193] navigate \xB7 [Enter] select \xB7 [Esc] reject"
8901
+ }
8902
+ )));
8903
+ }
8904
+ var PlanReviseConfirm = React14.memo(PlanReviseConfirmInner);
8905
+
7982
8906
  // src/cli/ui/PromptInput.tsx
7983
- import { Box as Box10, Text as Text10, useInput as useInput4 } from "ink";
7984
- import React11, { useRef, useState as useState5 } from "react";
8907
+ import { Box as Box14, Text as Text14, useInput as useInput4 } from "ink";
8908
+ import React15, { useRef, useState as useState5 } from "react";
7985
8909
 
7986
8910
  // src/cli/ui/multiline-keys.ts
7987
8911
  var BACKSLASH_SUFFIX = /\\$/;
@@ -8141,13 +9065,13 @@ function PromptInput({
8141
9065
  const lines = value.length > 0 ? value.split("\n") : [""];
8142
9066
  const borderColor = disabled ? "gray" : "cyan";
8143
9067
  const { line: cursorLine, col: cursorCol } = lineAndColumn(value, cursor);
8144
- return /* @__PURE__ */ React11.createElement(Box10, { borderStyle: "round", borderColor, paddingX: 1, flexDirection: "column" }, lines.map((line, i) => {
9068
+ return /* @__PURE__ */ React15.createElement(Box14, { borderStyle: "round", borderColor, paddingX: 1, flexDirection: "column" }, lines.map((line, i) => {
8145
9069
  const isFirst = i === 0;
8146
9070
  const showPlaceholder = isFirst && value.length === 0;
8147
9071
  const isCursorLine = i === cursorLine;
8148
9072
  return (
8149
9073
  // biome-ignore lint/suspicious/noArrayIndexKey: stable by construction — lines are derived from `value.split("\n")` and never reordered
8150
- /* @__PURE__ */ React11.createElement(Box10, { key: i }, isFirst ? /* @__PURE__ */ React11.createElement(Text10, { bold: true, color: borderColor }, "you \u203A", " ") : /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " "), showPlaceholder ? /* @__PURE__ */ React11.createElement(React11.Fragment, null, isCursorLine && !disabled ? /* @__PURE__ */ React11.createElement(Text10, { color: borderColor }, showCursor ? "\u258C" : " ") : null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, effectivePlaceholder)) : isCursorLine && !disabled ? /* @__PURE__ */ React11.createElement(
9074
+ /* @__PURE__ */ React15.createElement(Box14, { key: i }, isFirst ? /* @__PURE__ */ React15.createElement(Text14, { bold: true, color: borderColor }, "you \u203A", " ") : /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, " "), showPlaceholder ? /* @__PURE__ */ React15.createElement(React15.Fragment, null, isCursorLine && !disabled ? /* @__PURE__ */ React15.createElement(Text14, { color: borderColor }, showCursor ? "\u258C" : " ") : null, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, effectivePlaceholder)) : isCursorLine && !disabled ? /* @__PURE__ */ React15.createElement(
8151
9075
  LineWithCursor,
8152
9076
  {
8153
9077
  line,
@@ -8155,7 +9079,7 @@ function PromptInput({
8155
9079
  showCursor,
8156
9080
  borderColor
8157
9081
  }
8158
- ) : /* @__PURE__ */ React11.createElement(Text10, null, line))
9082
+ ) : /* @__PURE__ */ React15.createElement(Text14, null, line))
8159
9083
  );
8160
9084
  }));
8161
9085
  }
@@ -8169,17 +9093,17 @@ function LineWithCursor({
8169
9093
  const atCursor = line.slice(col, col + 1);
8170
9094
  const after = line.slice(col + 1);
8171
9095
  if (atCursor.length === 0) {
8172
- return /* @__PURE__ */ React11.createElement(React11.Fragment, null, /* @__PURE__ */ React11.createElement(Text10, null, before), /* @__PURE__ */ React11.createElement(Text10, { color: borderColor }, showCursor ? "\u258C" : " "));
9096
+ return /* @__PURE__ */ React15.createElement(React15.Fragment, null, /* @__PURE__ */ React15.createElement(Text14, null, before), /* @__PURE__ */ React15.createElement(Text14, { color: borderColor }, showCursor ? "\u258C" : " "));
8173
9097
  }
8174
- return /* @__PURE__ */ React11.createElement(React11.Fragment, null, /* @__PURE__ */ React11.createElement(Text10, null, before), /* @__PURE__ */ React11.createElement(Text10, { inverse: showCursor }, atCursor), /* @__PURE__ */ React11.createElement(Text10, null, after));
9098
+ return /* @__PURE__ */ React15.createElement(React15.Fragment, null, /* @__PURE__ */ React15.createElement(Text14, null, before), /* @__PURE__ */ React15.createElement(Text14, { inverse: showCursor }, atCursor), /* @__PURE__ */ React15.createElement(Text14, null, after));
8175
9099
  }
8176
9100
 
8177
9101
  // src/cli/ui/ShellConfirm.tsx
8178
- import { Box as Box11, Text as Text11 } from "ink";
8179
- import React12 from "react";
9102
+ import { Box as Box15, Text as Text15 } from "ink";
9103
+ import React16 from "react";
8180
9104
  function ShellConfirm({ command, allowPrefix, kind, onChoose }) {
8181
9105
  const isBackground = kind === "run_background";
8182
- return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Text11, { bold: true, color: "yellow" }, isBackground ? "\u25B8 model wants to start a BACKGROUND process" : "\u25B8 model wants to run a shell command")), isBackground ? /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, " (long-running: dev server / watcher; keeps running after approval, /kill to stop)")) : null, /* @__PURE__ */ React12.createElement(Box11, null, /* @__PURE__ */ React12.createElement(Text11, { 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__ */ React12.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text11, null, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, "$ "), /* @__PURE__ */ React12.createElement(Text11, { color: "cyan" }, command))), /* @__PURE__ */ React12.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(
9106
+ return /* @__PURE__ */ React16.createElement(Box15, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React16.createElement(Box15, null, /* @__PURE__ */ React16.createElement(Text15, { bold: true, color: "yellow" }, isBackground ? "\u25B8 model wants to start a BACKGROUND process" : "\u25B8 model wants to run a shell command")), isBackground ? /* @__PURE__ */ React16.createElement(Box15, null, /* @__PURE__ */ React16.createElement(Text15, { dimColor: true }, " (long-running: dev server / watcher; keeps running after approval, /kill to stop)")) : null, /* @__PURE__ */ React16.createElement(Box15, null, /* @__PURE__ */ React16.createElement(Text15, { 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__ */ React16.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text15, null, /* @__PURE__ */ React16.createElement(Text15, { dimColor: true }, "$ "), /* @__PURE__ */ React16.createElement(Text15, { color: "cyan" }, command))), /* @__PURE__ */ React16.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(
8183
9107
  SingleSelect,
8184
9108
  {
8185
9109
  initialValue: "run_once",
@@ -8236,8 +9160,8 @@ function derivePrefix(command) {
8236
9160
  }
8237
9161
 
8238
9162
  // src/cli/ui/SlashArgPicker.tsx
8239
- import { Box as Box12, Text as Text12 } from "ink";
8240
- import React13 from "react";
9163
+ import { Box as Box16, Text as Text16 } from "ink";
9164
+ import React17 from "react";
8241
9165
  function SlashArgPicker({
8242
9166
  matches,
8243
9167
  selectedIndex,
@@ -8246,11 +9170,11 @@ function SlashArgPicker({
8246
9170
  partial
8247
9171
  }) {
8248
9172
  if (kind === "hint") {
8249
- return /* @__PURE__ */ React13.createElement(Box12, { paddingX: 1 }, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " ", /* @__PURE__ */ React13.createElement(Text12, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary));
9173
+ return /* @__PURE__ */ React17.createElement(Box16, { paddingX: 1 }, /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, " ", /* @__PURE__ */ React17.createElement(Text16, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary));
8250
9174
  }
8251
9175
  if (matches === null) return null;
8252
9176
  if (matches.length === 0) {
8253
- return /* @__PURE__ */ React13.createElement(Box12, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " ", /* @__PURE__ */ React13.createElement(Text12, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary), /* @__PURE__ */ React13.createElement(Text12, { color: "yellow" }, ' no match for "', partial, '" \u2014 keep typing, or Backspace to edit'));
9177
+ return /* @__PURE__ */ React17.createElement(Box16, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, " ", /* @__PURE__ */ React17.createElement(Text16, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary), /* @__PURE__ */ React17.createElement(Text16, { color: "yellow" }, ' no match for "', partial, '" \u2014 keep typing, or Backspace to edit'));
8254
9178
  }
8255
9179
  const MAX = 8;
8256
9180
  const total = matches.length;
@@ -8258,26 +9182,26 @@ function SlashArgPicker({
8258
9182
  const shown = matches.slice(windowStart, windowStart + MAX);
8259
9183
  const hiddenAbove = windowStart;
8260
9184
  const hiddenBelow = total - windowStart - shown.length;
8261
- return /* @__PURE__ */ React13.createElement(Box12, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " ", /* @__PURE__ */ React13.createElement(Text12, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary), hiddenAbove > 0 ? /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((value, i) => /* @__PURE__ */ React13.createElement(ArgRow, { key: value, value, 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"));
9185
+ return /* @__PURE__ */ React17.createElement(Box16, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, " ", /* @__PURE__ */ React17.createElement(Text16, { bold: true }, "/", spec.cmd), spec.argsHint ? ` ${spec.argsHint}` : "", " \u2014 ", spec.summary), hiddenAbove > 0 ? /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((value, i) => /* @__PURE__ */ React17.createElement(ArgRow, { key: value, value, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick"));
8262
9186
  }
8263
9187
  function ArgRow({ value, isSelected }) {
8264
9188
  const marker = isSelected ? "\u25B8" : " ";
8265
9189
  if (isSelected) {
8266
- return /* @__PURE__ */ React13.createElement(Box12, null, /* @__PURE__ */ React13.createElement(Text12, { bold: true, color: "cyan" }, marker, " ", value));
9190
+ return /* @__PURE__ */ React17.createElement(Box16, null, /* @__PURE__ */ React17.createElement(Text16, { bold: true, color: "cyan" }, marker, " ", value));
8267
9191
  }
8268
- return /* @__PURE__ */ React13.createElement(Box12, null, /* @__PURE__ */ React13.createElement(Text12, { dimColor: true }, marker, " ", value));
9192
+ return /* @__PURE__ */ React17.createElement(Box16, null, /* @__PURE__ */ React17.createElement(Text16, { dimColor: true }, marker, " ", value));
8269
9193
  }
8270
9194
 
8271
9195
  // src/cli/ui/SlashSuggestions.tsx
8272
- import { Box as Box13, Text as Text13 } from "ink";
8273
- import React14 from "react";
9196
+ import { Box as Box17, Text as Text17 } from "ink";
9197
+ import React18 from "react";
8274
9198
  function SlashSuggestions({
8275
9199
  matches,
8276
9200
  selectedIndex
8277
9201
  }) {
8278
9202
  if (matches === null) return null;
8279
9203
  if (matches.length === 0) {
8280
- return /* @__PURE__ */ React14.createElement(Box13, { paddingX: 1 }, /* @__PURE__ */ React14.createElement(Text13, { color: "yellow" }, "no slash command matches that prefix"), /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " \u2014 Backspace to edit, or /help for the full list"));
9204
+ return /* @__PURE__ */ React18.createElement(Box17, { paddingX: 1 }, /* @__PURE__ */ React18.createElement(Text17, { color: "yellow" }, "no slash command matches that prefix"), /* @__PURE__ */ React18.createElement(Text17, { dimColor: true }, " \u2014 Backspace to edit, or /help for the full list"));
8281
9205
  }
8282
9206
  const MAX = 8;
8283
9207
  const total = matches.length;
@@ -8285,21 +9209,21 @@ function SlashSuggestions({
8285
9209
  const shown = matches.slice(windowStart, windowStart + MAX);
8286
9210
  const hiddenAbove = windowStart;
8287
9211
  const hiddenBelow = total - windowStart - shown.length;
8288
- return /* @__PURE__ */ React14.createElement(Box13, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React14.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick"));
9212
+ return /* @__PURE__ */ React18.createElement(Box17, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React18.createElement(Text17, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React18.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React18.createElement(Text17, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React18.createElement(Text17, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick"));
8289
9213
  }
8290
9214
  function SuggestionRow({ spec, isSelected }) {
8291
9215
  const marker = isSelected ? "\u25B8" : " ";
8292
9216
  const name = `/${spec.cmd}`;
8293
9217
  const argsSuffix = spec.argsHint ? ` ${spec.argsHint}` : "";
8294
9218
  if (isSelected) {
8295
- return /* @__PURE__ */ React14.createElement(Box13, null, /* @__PURE__ */ React14.createElement(Text13, { bold: true, color: "cyan" }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16)), /* @__PURE__ */ React14.createElement(Text13, { color: "cyan" }, " ", spec.summary));
9219
+ return /* @__PURE__ */ React18.createElement(Box17, null, /* @__PURE__ */ React18.createElement(Text17, { bold: true, color: "cyan" }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16)), /* @__PURE__ */ React18.createElement(Text17, { color: "cyan" }, " ", spec.summary));
8296
9220
  }
8297
- return /* @__PURE__ */ React14.createElement(Box13, null, /* @__PURE__ */ React14.createElement(Text13, { dimColor: true }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16), " ", spec.summary));
9221
+ return /* @__PURE__ */ React18.createElement(Box17, null, /* @__PURE__ */ React18.createElement(Text17, { dimColor: true }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16), " ", spec.summary));
8298
9222
  }
8299
9223
 
8300
9224
  // src/cli/ui/StatsPanel.tsx
8301
- import { Box as Box14, Text as Text14, useStdout as useStdout3 } from "ink";
8302
- import React15 from "react";
9225
+ import { Box as Box18, Text as Text18, useStdout as useStdout3 } from "ink";
9226
+ import React19 from "react";
8303
9227
  var WORDMARK_STYLES = [
8304
9228
  { ch: "\u25C8", color: "#5eead4", isLogo: true },
8305
9229
  // teal — brand mark
@@ -8325,7 +9249,7 @@ function Wordmark({ busy }) {
8325
9249
  const tick = useTick();
8326
9250
  const period = busy ? 5 : 12;
8327
9251
  const bright = Math.floor(tick / period) % 2 === 0;
8328
- return /* @__PURE__ */ React15.createElement(Text14, null, WORDMARK_STYLES.map((c) => /* @__PURE__ */ React15.createElement(Text14, { key: `${c.ch}-${c.color}`, color: c.color, bold: c.isLogo ? bright : true }, c.ch)));
9252
+ return /* @__PURE__ */ React19.createElement(Text18, null, WORDMARK_STYLES.map((c) => /* @__PURE__ */ React19.createElement(Text18, { key: `${c.ch}-${c.color}`, color: c.color, bold: c.isLogo ? bright : true }, c.ch)));
8329
9253
  }
8330
9254
  var NARROW_BREAKPOINT = 120;
8331
9255
  var COLD_START_TURNS = 3;
@@ -8351,7 +9275,7 @@ function StatsPanel({
8351
9275
  const columns = stdout2?.columns ?? 80;
8352
9276
  const narrow = columns < NARROW_BREAKPOINT;
8353
9277
  const coldStart = summary.turns <= COLD_START_TURNS;
8354
- return /* @__PURE__ */ React15.createElement(Box14, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React15.createElement(
9278
+ return /* @__PURE__ */ React19.createElement(Box18, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React19.createElement(
8355
9279
  Header,
8356
9280
  {
8357
9281
  model: model2,
@@ -8369,7 +9293,7 @@ function StatsPanel({
8369
9293
  proArmed: proArmed ?? false,
8370
9294
  escalated: escalated ?? false
8371
9295
  }
8372
- ), narrow ? /* @__PURE__ */ React15.createElement(
9296
+ ), narrow ? /* @__PURE__ */ React19.createElement(
8373
9297
  StackedMetrics,
8374
9298
  {
8375
9299
  summary,
@@ -8378,7 +9302,7 @@ function StatsPanel({
8378
9302
  balance,
8379
9303
  coldStart
8380
9304
  }
8381
- ) : /* @__PURE__ */ React15.createElement(
9305
+ ) : /* @__PURE__ */ React19.createElement(
8382
9306
  InlineMetrics,
8383
9307
  {
8384
9308
  summary,
@@ -8405,7 +9329,7 @@ function Header({
8405
9329
  proArmed,
8406
9330
  escalated
8407
9331
  }) {
8408
- return /* @__PURE__ */ React15.createElement(Box14, { justifyContent: "space-between" }, /* @__PURE__ */ React15.createElement(Box14, null, /* @__PURE__ */ React15.createElement(Wordmark, { busy }), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, ` v${VERSION}`), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React15.createElement(Text14, { color: "yellow" }, model2), narrow ? null : /* @__PURE__ */ React15.createElement(React15.Fragment, null, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, prefixHash)), harvestOn ? /* @__PURE__ */ React15.createElement(Text14, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React15.createElement(Text14, { color: "blue" }, " \xB7 branch", branchBudget) : null, reasoningEffort === "max" ? /* @__PURE__ */ React15.createElement(Text14, { color: "green" }, " \xB7 max") : null, reasoningEffort === "high" ? /* @__PURE__ */ React15.createElement(Text14, { color: "yellow" }, " \xB7 high") : null, planMode ? /* @__PURE__ */ React15.createElement(Text14, { color: "red", bold: true }, " \xB7 PLAN") : null, editMode ? /* @__PURE__ */ React15.createElement(Text14, { color: editMode === "auto" ? "magenta" : "cyan", bold: true }, editMode === "auto" ? " \xB7 AUTO" : " \xB7 review") : null, escalated ? /* @__PURE__ */ React15.createElement(Text14, { color: "red", bold: true }, " \xB7 \u21E7 pro escalated") : proArmed ? /* @__PURE__ */ React15.createElement(Text14, { color: "yellow", bold: true }, " \xB7 \u21E7 pro armed") : null), /* @__PURE__ */ React15.createElement(Text14, null, updateAvailable ? /* @__PURE__ */ React15.createElement(Text14, { color: "yellow", bold: true }, `update: ${updateAvailable} \xB7 `) : null, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, narrow ? `turn ${turns}` : `turn ${turns} \xB7 /help`)));
9332
+ return /* @__PURE__ */ React19.createElement(Box18, { justifyContent: "space-between" }, /* @__PURE__ */ React19.createElement(Box18, null, /* @__PURE__ */ React19.createElement(Wordmark, { busy }), /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, ` v${VERSION}`), /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React19.createElement(Text18, { color: "yellow" }, model2), narrow ? null : /* @__PURE__ */ React19.createElement(React19.Fragment, null, /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, prefixHash)), harvestOn ? /* @__PURE__ */ React19.createElement(Text18, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React19.createElement(Text18, { color: "blue" }, " \xB7 branch", branchBudget) : null, reasoningEffort === "max" ? /* @__PURE__ */ React19.createElement(Text18, { color: "green" }, " \xB7 max") : null, reasoningEffort === "high" ? /* @__PURE__ */ React19.createElement(Text18, { color: "yellow" }, " \xB7 high") : null, planMode ? /* @__PURE__ */ React19.createElement(Text18, { color: "red", bold: true }, " \xB7 PLAN") : null, editMode ? /* @__PURE__ */ React19.createElement(Text18, { color: editMode === "auto" ? "magenta" : "cyan", bold: true }, editMode === "auto" ? " \xB7 AUTO" : " \xB7 review") : null, escalated ? /* @__PURE__ */ React19.createElement(Text18, { color: "red", bold: true }, " \xB7 \u21E7 pro escalated") : proArmed ? /* @__PURE__ */ React19.createElement(Text18, { color: "yellow", bold: true }, " \xB7 \u21E7 pro armed") : null), /* @__PURE__ */ React19.createElement(Text18, null, updateAvailable ? /* @__PURE__ */ React19.createElement(Text18, { color: "yellow", bold: true }, `update: ${updateAvailable} \xB7 `) : null, /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, narrow ? `turn ${turns}` : `turn ${turns} \xB7 /help`)));
8409
9333
  }
8410
9334
  function InlineMetrics({
8411
9335
  summary,
@@ -8414,7 +9338,7 @@ function InlineMetrics({
8414
9338
  balance,
8415
9339
  coldStart
8416
9340
  }) {
8417
- return /* @__PURE__ */ React15.createElement(Box14, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React15.createElement(ContextCell, { ratio: ctxRatio, promptTokens: summary.lastPromptTokens, ctxMax }), /* @__PURE__ */ React15.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React15.createElement(CostCell, { summary, coldStart }), balance ? /* @__PURE__ */ React15.createElement(BalanceCell, { balance }) : null);
9341
+ return /* @__PURE__ */ React19.createElement(Box18, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React19.createElement(ContextCell, { ratio: ctxRatio, promptTokens: summary.lastPromptTokens, ctxMax }), /* @__PURE__ */ React19.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React19.createElement(CostCell, { summary, coldStart }), balance ? /* @__PURE__ */ React19.createElement(BalanceCell, { balance }) : null);
8418
9342
  }
8419
9343
  function StackedMetrics({
8420
9344
  summary,
@@ -8423,7 +9347,7 @@ function StackedMetrics({
8423
9347
  balance,
8424
9348
  coldStart
8425
9349
  }) {
8426
- return /* @__PURE__ */ React15.createElement(Box14, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React15.createElement(
9350
+ return /* @__PURE__ */ React19.createElement(Box18, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React19.createElement(
8427
9351
  ContextCell,
8428
9352
  {
8429
9353
  ratio: ctxRatio,
@@ -8431,7 +9355,7 @@ function StackedMetrics({
8431
9355
  ctxMax,
8432
9356
  showBar: true
8433
9357
  }
8434
- ), balance ? /* @__PURE__ */ React15.createElement(BalanceCell, { balance }) : null, /* @__PURE__ */ React15.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React15.createElement(CostCell, { summary, coldStart }));
9358
+ ), balance ? /* @__PURE__ */ React19.createElement(BalanceCell, { balance }) : null, /* @__PURE__ */ React19.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React19.createElement(CostCell, { summary, coldStart }));
8435
9359
  }
8436
9360
  function ContextCell({
8437
9361
  ratio,
@@ -8440,11 +9364,11 @@ function ContextCell({
8440
9364
  showBar
8441
9365
  }) {
8442
9366
  if (promptTokens === 0) {
8443
- return /* @__PURE__ */ React15.createElement(Text14, null, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "ctx "), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "\u2014 (no turns yet)"));
9367
+ return /* @__PURE__ */ React19.createElement(Text18, null, /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, "ctx "), /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, "\u2014 (no turns yet)"));
8444
9368
  }
8445
9369
  const color = ratio >= 0.8 ? "red" : ratio >= 0.6 ? "yellow" : "green";
8446
9370
  const pct2 = Math.round(ratio * 100);
8447
- return /* @__PURE__ */ React15.createElement(Text14, null, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "ctx "), showBar ? /* @__PURE__ */ React15.createElement(Bar, { ratio, color }) : null, showBar ? /* @__PURE__ */ React15.createElement(Text14, null, " ") : null, /* @__PURE__ */ React15.createElement(Text14, { color, bold: true }, formatTokens(promptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, " (", pct2, "%)"), ratio >= 0.8 ? /* @__PURE__ */ React15.createElement(Text14, { color: "red", bold: true }, " \xB7 /compact") : null);
9371
+ return /* @__PURE__ */ React19.createElement(Text18, null, /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, "ctx "), showBar ? /* @__PURE__ */ React19.createElement(Bar, { ratio, color }) : null, showBar ? /* @__PURE__ */ React19.createElement(Text18, null, " ") : null, /* @__PURE__ */ React19.createElement(Text18, { color, bold: true }, formatTokens(promptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, " (", pct2, "%)"), ratio >= 0.8 ? /* @__PURE__ */ React19.createElement(Text18, { color: "red", bold: true }, " \xB7 /compact") : null);
8448
9372
  }
8449
9373
  function CacheCell({
8450
9374
  hitRatio,
@@ -8453,13 +9377,13 @@ function CacheCell({
8453
9377
  }) {
8454
9378
  const pct2 = (hitRatio * 100).toFixed(1);
8455
9379
  if (turns === 0) {
8456
- return /* @__PURE__ */ React15.createElement(Text14, null, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "cache "), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "\u2014"));
9380
+ return /* @__PURE__ */ React19.createElement(Text18, null, /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, "cache "), /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, "\u2014"));
8457
9381
  }
8458
9382
  if (coldStart) {
8459
- return /* @__PURE__ */ React15.createElement(Text14, null, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "cache "), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, pct2, "% "), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true, italic: true }, "(cold start)"));
9383
+ return /* @__PURE__ */ React19.createElement(Text18, null, /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, "cache "), /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, pct2, "% "), /* @__PURE__ */ React19.createElement(Text18, { dimColor: true, italic: true }, "(cold start)"));
8460
9384
  }
8461
9385
  const color = hitRatio >= 0.7 ? "green" : hitRatio >= 0.4 ? "yellow" : "red";
8462
- return /* @__PURE__ */ React15.createElement(Text14, null, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "cache "), /* @__PURE__ */ React15.createElement(Text14, { color, bold: true }, pct2, "%"));
9386
+ return /* @__PURE__ */ React19.createElement(Text18, null, /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, "cache "), /* @__PURE__ */ React19.createElement(Text18, { color, bold: true }, pct2, "%"));
8463
9387
  }
8464
9388
  function turnCostColor(cost) {
8465
9389
  if (cost <= 0) return void 0;
@@ -8478,21 +9402,21 @@ function CostCell({
8478
9402
  coldStart
8479
9403
  }) {
8480
9404
  if (summary.turns === 0) {
8481
- return /* @__PURE__ */ React15.createElement(Text14, null, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "cost "), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "\u2014"));
9405
+ return /* @__PURE__ */ React19.createElement(Text18, null, /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, "cost "), /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, "\u2014"));
8482
9406
  }
8483
9407
  const turnColor = coldStart ? void 0 : turnCostColor(summary.lastTurnCostUsd);
8484
9408
  const sessionColor = coldStart ? void 0 : sessionCostColor(summary.totalCostUsd);
8485
- return /* @__PURE__ */ React15.createElement(Text14, null, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "turn "), /* @__PURE__ */ React15.createElement(Text14, { color: turnColor, bold: !coldStart, dimColor: coldStart }, "$", summary.lastTurnCostUsd.toFixed(4)), /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, " \xB7 session "), /* @__PURE__ */ React15.createElement(Text14, { color: sessionColor, bold: !coldStart, dimColor: coldStart }, "$", summary.totalCostUsd.toFixed(4)));
9409
+ return /* @__PURE__ */ React19.createElement(Text18, null, /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, "turn "), /* @__PURE__ */ React19.createElement(Text18, { color: turnColor, bold: !coldStart, dimColor: coldStart }, "$", summary.lastTurnCostUsd.toFixed(4)), /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, " \xB7 session "), /* @__PURE__ */ React19.createElement(Text18, { color: sessionColor, bold: !coldStart, dimColor: coldStart }, "$", summary.totalCostUsd.toFixed(4)));
8486
9410
  }
8487
9411
  function BalanceCell({ balance }) {
8488
9412
  const color = balance.total < 1 ? "red" : balance.total < 5 ? "yellow" : "green";
8489
- return /* @__PURE__ */ React15.createElement(Text14, null, /* @__PURE__ */ React15.createElement(Text14, { dimColor: true }, "balance "), /* @__PURE__ */ React15.createElement(Text14, { color, bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : ""));
9413
+ return /* @__PURE__ */ React19.createElement(Text18, null, /* @__PURE__ */ React19.createElement(Text18, { dimColor: true }, "balance "), /* @__PURE__ */ React19.createElement(Text18, { color, bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : ""));
8490
9414
  }
8491
9415
  function Bar({ ratio, color }) {
8492
9416
  const cells = 10;
8493
9417
  const filled = Math.max(0, Math.min(cells, Math.round(ratio * cells)));
8494
9418
  const bar = "\u2588".repeat(filled) + "\u2591".repeat(cells - filled);
8495
- return /* @__PURE__ */ React15.createElement(Text14, { color }, bar);
9419
+ return /* @__PURE__ */ React19.createElement(Text18, { color }, bar);
8496
9420
  }
8497
9421
  function formatTokens(n) {
8498
9422
  if (n < 1024) return String(n);
@@ -8738,7 +9662,7 @@ function formatLongPaste(input, opts = {}) {
8738
9662
  if (originalChars <= charCap && originalLines <= lineCap) {
8739
9663
  return { displayText: input, collapsed: false, originalChars, originalLines };
8740
9664
  }
8741
- const header2 = `\u25B8 pasted ${formatBytes(originalChars)} (${originalLines} lines) \u2014 first ${Math.min(headN, originalLines)} shown, full text sent to model`;
9665
+ const header2 = `\u25B8 pasted ${formatBytes2(originalChars)} (${originalLines} lines) \u2014 first ${Math.min(headN, originalLines)} shown, full text sent to model`;
8742
9666
  const head = lines.slice(0, headN).join("\n");
8743
9667
  const remaining = originalLines - headN;
8744
9668
  const footer = remaining > 0 ? `\u2026 (${remaining} more line${remaining === 1 ? "" : "s"})` : "";
@@ -8748,7 +9672,7 @@ ${footer}` : `${header2}
8748
9672
  ${head}`;
8749
9673
  return { displayText, collapsed: true, originalChars, originalLines };
8750
9674
  }
8751
- function formatBytes(n) {
9675
+ function formatBytes2(n) {
8752
9676
  if (n < 1024) return `${n} B`;
8753
9677
  const kb = n / 1024;
8754
9678
  if (kb < 1024) return `${kb.toFixed(kb >= 10 ? 0 : 1)} KB`;
@@ -8846,6 +9770,12 @@ var SLASH_COMMANDS = [
8846
9770
  summary: "shrink oversized tool results AND tool-call args (edit_file search/replace) in the log; cap in tokens, default 4000"
8847
9771
  },
8848
9772
  { cmd: "keys", summary: "show all keyboard shortcuts and prompt prefixes" },
9773
+ { cmd: "plans", summary: "list this session's active + archived plans, newest first" },
9774
+ {
9775
+ cmd: "replay",
9776
+ summary: "load an archived plan as a read-only Time Travel snapshot (default: newest)",
9777
+ argsHint: "[N]"
9778
+ },
8849
9779
  { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
8850
9780
  { cmd: "forget", summary: "delete the current session from disk" },
8851
9781
  { cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
@@ -8943,7 +9873,7 @@ function parseSlash(text) {
8943
9873
  }
8944
9874
 
8945
9875
  // src/cli/commands/stats.ts
8946
- import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
9876
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
8947
9877
  function statsCommand(opts) {
8948
9878
  if (opts.transcript) {
8949
9879
  transcriptSummary(opts.transcript);
@@ -8952,11 +9882,11 @@ function statsCommand(opts) {
8952
9882
  dashboard(opts);
8953
9883
  }
8954
9884
  function transcriptSummary(path) {
8955
- if (!existsSync10(path)) {
9885
+ if (!existsSync11(path)) {
8956
9886
  console.error(`no such transcript: ${path}`);
8957
9887
  process.exit(1);
8958
9888
  }
8959
- const lines = readFileSync13(path, "utf8").split(/\r?\n/).filter(Boolean);
9889
+ const lines = readFileSync14(path, "utf8").split(/\r?\n/).filter(Boolean);
8960
9890
  let assistantTurns = 0;
8961
9891
  let toolCalls = 0;
8962
9892
  let lastTurn = 0;
@@ -9800,7 +10730,7 @@ var model = (args, loop, ctx) => {
9800
10730
  const id = args[0];
9801
10731
  const known = ctx.models ?? null;
9802
10732
  if (!id) {
9803
- const hint = known && known.length > 0 ? known.join(" | ") : "try deepseek-chat or deepseek-reasoner \u2014 run /models to fetch the live list";
10733
+ const hint = known && known.length > 0 ? known.join(" | ") : "try deepseek-v4-flash or deepseek-v4-pro \u2014 run /models to fetch the live list";
9804
10734
  return { info: `usage: /model <id> (${hint})` };
9805
10735
  }
9806
10736
  loop.configure({ model: id });
@@ -9961,7 +10891,7 @@ var think = (_args, loop) => {
9961
10891
  const raw = loop.scratch.reasoning;
9962
10892
  if (!raw || !raw.trim()) {
9963
10893
  return {
9964
- info: "no reasoning cached. `/think` shows the full R1 thought for the most recent turn \u2014 only `deepseek-reasoner` produces it, and only once the turn completes."
10894
+ info: "no reasoning cached. `/think` shows the full thinking-mode thought for the most recent turn \u2014 only thinking-mode models (deepseek-v4-flash / -v4-pro / -reasoner) produce it, and only once the turn completes."
9965
10895
  };
9966
10896
  }
9967
10897
  return { info: `\u21B3 full thinking (${raw.length} chars):
@@ -10108,6 +11038,90 @@ var handlers8 = {
10108
11038
  compact
10109
11039
  };
10110
11040
 
11041
+ // src/cli/ui/slash/handlers/plans.ts
11042
+ import { basename } from "path";
11043
+ var plans = (_args, loop) => {
11044
+ const sessionName = loop.sessionName;
11045
+ if (!sessionName) {
11046
+ return {
11047
+ info: "no session attached \u2014 `/plans` is per-session. Run `reasonix code` in a project to get a session."
11048
+ };
11049
+ }
11050
+ const lines = [];
11051
+ const active = loadPlanState(sessionName);
11052
+ if (active && active.steps.length > 0) {
11053
+ const total = active.steps.length;
11054
+ const done = active.completedStepIds.length;
11055
+ const when = relativeTime(active.updatedAt);
11056
+ const label = active.summary ? `: ${active.summary}` : "";
11057
+ lines.push(
11058
+ `\u25B8 active plan${label} \u2014 ${done}/${total} step${total === 1 ? "" : "s"} done \xB7 last touched ${when}`
11059
+ );
11060
+ } else {
11061
+ lines.push("\u25B8 active plan: (none)");
11062
+ }
11063
+ const archives = listPlanArchives(sessionName);
11064
+ if (archives.length === 0) {
11065
+ lines.push("");
11066
+ lines.push(
11067
+ "no archived plans yet for this session \u2014 they auto-archive when every step is done"
11068
+ );
11069
+ return { info: lines.join("\n") };
11070
+ }
11071
+ lines.push("");
11072
+ lines.push(`Archived (${archives.length}):`);
11073
+ for (const a of archives) {
11074
+ const when = relativeTime(a.completedAt);
11075
+ const total = a.steps.length;
11076
+ const done = a.completedStepIds.length;
11077
+ const completion = done >= total ? "complete" : `${done}/${total}`;
11078
+ const label = a.summary ?? a.path.split(/[\\/]/).pop() ?? a.path;
11079
+ lines.push(
11080
+ ` \u2713 ${when.padEnd(10)} ${total} step${total === 1 ? "" : "s"} \xB7 ${completion} ${label}`
11081
+ );
11082
+ }
11083
+ return { info: lines.join("\n") };
11084
+ };
11085
+ var replay = (args, loop) => {
11086
+ const sessionName = loop.sessionName;
11087
+ if (!sessionName) {
11088
+ return {
11089
+ info: "no session attached \u2014 `/replay` is per-session. Run `reasonix code` in a project to get a session."
11090
+ };
11091
+ }
11092
+ const archives = listPlanArchives(sessionName);
11093
+ if (archives.length === 0) {
11094
+ return {
11095
+ info: "no archived plans yet for this session \u2014 `/replay` lights up once a plan completes (auto-archives when every step is done)."
11096
+ };
11097
+ }
11098
+ const arg = args[0]?.trim() ?? "";
11099
+ const index = arg ? Number.parseInt(arg, 10) : 1;
11100
+ if (!Number.isFinite(index) || index < 1 || index > archives.length) {
11101
+ return {
11102
+ info: `invalid index \u2014 \`/replay\` takes 1..${archives.length} (newest = 1). Use \`/plans\` to see the list.`
11103
+ };
11104
+ }
11105
+ const a = archives[index - 1];
11106
+ return {
11107
+ replayPlan: {
11108
+ summary: a.summary,
11109
+ body: a.body,
11110
+ steps: a.steps,
11111
+ completedStepIds: a.completedStepIds,
11112
+ completedAt: a.completedAt,
11113
+ relativeTime: relativeTime(a.completedAt),
11114
+ archiveBasename: basename(a.path),
11115
+ index,
11116
+ total: archives.length
11117
+ }
11118
+ };
11119
+ };
11120
+ var handlers9 = {
11121
+ plans,
11122
+ replay
11123
+ };
11124
+
10111
11125
  // src/cli/ui/slash/handlers/sessions.ts
10112
11126
  var sessions = (_args, loop) => {
10113
11127
  const items = listSessions();
@@ -10139,7 +11153,7 @@ var forget = (_args, loop) => {
10139
11153
  info: ok ? `\u25B8 deleted session "${name}" \u2014 current screen still shows the conversation, but next launch starts fresh` : `could not delete session "${name}" (already gone?)`
10140
11154
  };
10141
11155
  };
10142
- var handlers9 = {
11156
+ var handlers10 = {
10143
11157
  sessions,
10144
11158
  forget
10145
11159
  };
@@ -10215,7 +11229,7 @@ ${found.body}${argsLine}`;
10215
11229
  resubmit: payload
10216
11230
  };
10217
11231
  };
10218
- var handlers10 = {
11232
+ var handlers11 = {
10219
11233
  skill,
10220
11234
  skills: skill
10221
11235
  };
@@ -10231,7 +11245,8 @@ var HANDLERS = {
10231
11245
  ...handlers7,
10232
11246
  ...handlers8,
10233
11247
  ...handlers9,
10234
- ...handlers10
11248
+ ...handlers10,
11249
+ ...handlers11
10235
11250
  };
10236
11251
  function handleSlash(cmd, args, loop, ctx = {}) {
10237
11252
  const h = HANDLERS[cmd];
@@ -10788,6 +11803,11 @@ function App({
10788
11803
  const [pendingShell, setPendingShell] = useState10(null);
10789
11804
  const [pendingPlan, setPendingPlan] = useState10(null);
10790
11805
  const [stagedInput, setStagedInput] = useState10(null);
11806
+ const [pendingCheckpoint, setPendingCheckpoint] = useState10(null);
11807
+ const [stagedCheckpointRevise, setStagedCheckpointRevise] = useState10(null);
11808
+ const [pendingRevision, setPendingRevision] = useState10(null);
11809
+ const [pendingChoice, setPendingChoice] = useState10(null);
11810
+ const [stagedChoiceCustom, setStagedChoiceCustom] = useState10(null);
10791
11811
  const [planMode, setPlanMode] = useState10(false);
10792
11812
  const [proArmed, setProArmed] = useState10(false);
10793
11813
  const [turnOnPro, setTurnOnPro] = useState10(false);
@@ -10796,6 +11816,23 @@ function App({
10796
11816
  const historyCursor = useRef5(-1);
10797
11817
  const assistantIterCounter = useRef5(0);
10798
11818
  const toolHistoryRef = useRef5([]);
11819
+ const planStepsRef = useRef5(null);
11820
+ const completedStepIdsRef = useRef5(/* @__PURE__ */ new Set());
11821
+ const planBodyRef = useRef5(null);
11822
+ const planSummaryRef = useRef5(null);
11823
+ const toolStartedAtRef = useRef5(null);
11824
+ const persistPlanState = useCallback4(() => {
11825
+ if (!session) return;
11826
+ const steps = planStepsRef.current;
11827
+ if (!steps || steps.length === 0) {
11828
+ clearPlanState(session);
11829
+ return;
11830
+ }
11831
+ const extras = {};
11832
+ if (planBodyRef.current) extras.body = planBodyRef.current;
11833
+ if (planSummaryRef.current) extras.summary = planSummaryRef.current;
11834
+ savePlanState(session, steps, completedStepIdsRef.current, extras);
11835
+ }, [session]);
10799
11836
  const [summary, setSummary] = useState10({
10800
11837
  turns: 0,
10801
11838
  totalCostUsd: 0,
@@ -10958,6 +11995,30 @@ function App({
10958
11995
  ]);
10959
11996
  }
10960
11997
  }
11998
+ if (session) {
11999
+ const restoredPlan = loadPlanState(session);
12000
+ if (restoredPlan && restoredPlan.steps.length > 0) {
12001
+ planStepsRef.current = restoredPlan.steps;
12002
+ completedStepIdsRef.current = new Set(restoredPlan.completedStepIds);
12003
+ planBodyRef.current = restoredPlan.body ?? null;
12004
+ planSummaryRef.current = restoredPlan.summary ?? null;
12005
+ const when = relativeTime(restoredPlan.updatedAt);
12006
+ setHistorical((prev) => [
12007
+ ...prev,
12008
+ {
12009
+ id: `sys-plan-${Date.now()}`,
12010
+ role: "plan-resumed",
12011
+ text: "",
12012
+ resumedPlan: {
12013
+ steps: restoredPlan.steps,
12014
+ completedStepIds: restoredPlan.completedStepIds,
12015
+ relativeTime: when,
12016
+ summary: restoredPlan.summary
12017
+ }
12018
+ }
12019
+ ]);
12020
+ }
12021
+ }
10961
12022
  if (codeMode && !editModeHintShown()) {
10962
12023
  setHistorical((prev) => [
10963
12024
  ...prev,
@@ -10983,7 +12044,7 @@ function App({
10983
12044
  loop.abort();
10984
12045
  return;
10985
12046
  }
10986
- if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview) {
12047
+ if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision) {
10987
12048
  setEditMode((m) => {
10988
12049
  const next = m === "auto" ? "review" : "auto";
10989
12050
  setHistorical((prev) => [
@@ -10998,7 +12059,7 @@ function App({
10998
12059
  });
10999
12060
  return;
11000
12061
  }
11001
- 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
12062
+ if (codeMode && input.length === 0 && (chKey === "u" || chKey === "U") && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision && // Fire when EITHER the banner is up OR there's any non-undone
11002
12063
  // history entry — the keybind is useful long after the 5-second
11003
12064
  // banner expires, which users rightly want.
11004
12065
  (undoBanner || hasUndoable())) {
@@ -11364,6 +12425,27 @@ function App({
11364
12425
  }
11365
12426
  ]);
11366
12427
  }
12428
+ if (result.replayPlan) {
12429
+ const rp = result.replayPlan;
12430
+ setHistorical((prev) => [
12431
+ ...prev,
12432
+ {
12433
+ id: `replay-${Date.now()}-${Math.random()}`,
12434
+ role: "plan-replay",
12435
+ text: "",
12436
+ replayPlan: {
12437
+ summary: rp.summary,
12438
+ body: rp.body,
12439
+ steps: rp.steps,
12440
+ completedStepIds: rp.completedStepIds,
12441
+ relativeTime: rp.relativeTime,
12442
+ archiveBasename: rp.archiveBasename,
12443
+ index: rp.index,
12444
+ total: rp.total
12445
+ }
12446
+ }
12447
+ ]);
12448
+ }
11367
12449
  if (result.resubmit) {
11368
12450
  text = result.resubmit;
11369
12451
  } else {
@@ -11570,6 +12652,7 @@ function App({
11570
12652
  } else if (ev.role === "tool_start") {
11571
12653
  setOngoingTool({ name: ev.toolName ?? "?", args: ev.toolArgs });
11572
12654
  setToolProgress(null);
12655
+ toolStartedAtRef.current = Date.now();
11573
12656
  if (codeMode && ev.toolArgs) {
11574
12657
  try {
11575
12658
  const parsed = JSON.parse(ev.toolArgs);
@@ -11587,19 +12670,28 @@ function App({
11587
12670
  flush();
11588
12671
  setOngoingTool(null);
11589
12672
  setToolProgress(null);
11590
- toolHistoryRef.current.push({
11591
- toolName: ev.toolName ?? "?",
11592
- text: ev.content
11593
- });
11594
- setHistorical((prev) => [
11595
- ...prev,
11596
- {
11597
- id: `t-${Date.now()}-${Math.random()}`,
11598
- role: "tool",
11599
- text: ev.content,
11600
- toolName: ev.toolName
11601
- }
11602
- ]);
12673
+ const isStepProgressTool = ev.toolName === "mark_step_complete";
12674
+ const startedAt = toolStartedAtRef.current;
12675
+ const durationMs = startedAt !== null ? Date.now() - startedAt : void 0;
12676
+ toolStartedAtRef.current = null;
12677
+ if (!isStepProgressTool) {
12678
+ toolHistoryRef.current.push({
12679
+ toolName: ev.toolName ?? "?",
12680
+ text: ev.content
12681
+ });
12682
+ const toolIndex = toolHistoryRef.current.length;
12683
+ setHistorical((prev) => [
12684
+ ...prev,
12685
+ {
12686
+ id: `t-${Date.now()}-${Math.random()}`,
12687
+ role: "tool",
12688
+ text: ev.content,
12689
+ toolName: ev.toolName,
12690
+ toolIndex,
12691
+ durationMs
12692
+ }
12693
+ ]);
12694
+ }
11603
12695
  if (codeMode && (ev.toolName === "run_command" || ev.toolName === "run_background") && ev.content.includes('"NeedsConfirmationError:') && ev.toolArgs) {
11604
12696
  try {
11605
12697
  const parsed = JSON.parse(ev.toolArgs);
@@ -11618,6 +12710,12 @@ function App({
11618
12710
  if (typeof parsed.plan === "string" && parsed.plan.trim()) {
11619
12711
  const planText = parsed.plan.trim();
11620
12712
  setPendingPlan(planText);
12713
+ const steps = Array.isArray(parsed.steps) ? parsed.steps : null;
12714
+ planStepsRef.current = steps;
12715
+ completedStepIdsRef.current = /* @__PURE__ */ new Set();
12716
+ planBodyRef.current = planText;
12717
+ planSummaryRef.current = typeof parsed.summary === "string" && parsed.summary.trim() ? parsed.summary.trim() : null;
12718
+ persistPlanState();
11621
12719
  setHistorical((prev) => [
11622
12720
  ...prev,
11623
12721
  {
@@ -11630,6 +12728,85 @@ function App({
11630
12728
  } catch {
11631
12729
  }
11632
12730
  }
12731
+ if (ev.toolName === "revise_plan" && ev.content.includes('"PlanRevisionProposedError:')) {
12732
+ try {
12733
+ const parsed = JSON.parse(ev.content);
12734
+ const reason = typeof parsed.reason === "string" ? parsed.reason.trim() : "";
12735
+ const remainingSteps = Array.isArray(parsed.remainingSteps) ? parsed.remainingSteps.filter(
12736
+ (s) => s && typeof s.id === "string" && s.id.trim() && typeof s.title === "string" && s.title.trim() && typeof s.action === "string" && s.action.trim()
12737
+ ) : [];
12738
+ if (reason && remainingSteps.length > 0) {
12739
+ const summary2 = typeof parsed.summary === "string" ? parsed.summary.trim() || void 0 : void 0;
12740
+ setPendingRevision({ reason, remainingSteps, summary: summary2 });
12741
+ }
12742
+ } catch {
12743
+ }
12744
+ }
12745
+ if (ev.toolName === "ask_choice" && ev.content.includes('"ChoiceRequestedError:')) {
12746
+ try {
12747
+ const parsed = JSON.parse(ev.content);
12748
+ const question = typeof parsed.question === "string" ? parsed.question.trim() : "";
12749
+ const options = Array.isArray(parsed.options) ? parsed.options.filter(
12750
+ (o) => o && typeof o.id === "string" && o.id.trim() && typeof o.title === "string" && o.title.trim()
12751
+ ) : [];
12752
+ if (question && options.length >= 2) {
12753
+ setPendingChoice({
12754
+ question,
12755
+ options,
12756
+ allowCustom: parsed.allowCustom === true
12757
+ });
12758
+ }
12759
+ } catch {
12760
+ }
12761
+ }
12762
+ if (ev.toolName === "mark_step_complete") {
12763
+ try {
12764
+ const parsed = JSON.parse(ev.content);
12765
+ const stepId = parsed.stepId;
12766
+ if (parsed.kind === "step_completed" && typeof stepId === "string") {
12767
+ completedStepIdsRef.current.add(stepId);
12768
+ persistPlanState();
12769
+ const total = planStepsRef.current?.length ?? 0;
12770
+ const completed = completedStepIdsRef.current.size;
12771
+ const stepFromPlan = planStepsRef.current?.find((s) => s.id === stepId);
12772
+ const title = parsed.title ?? stepFromPlan?.title;
12773
+ const result = typeof parsed.result === "string" ? parsed.result : "";
12774
+ const notes = parsed.notes;
12775
+ setHistorical((prev) => [
12776
+ ...prev,
12777
+ {
12778
+ id: `step-${stepId}-${Date.now()}-${Math.random()}`,
12779
+ role: "step-progress",
12780
+ text: result,
12781
+ stepProgress: {
12782
+ stepId,
12783
+ title,
12784
+ completed,
12785
+ total,
12786
+ notes
12787
+ }
12788
+ }
12789
+ ]);
12790
+ if (session && total > 0 && completed >= total) {
12791
+ const archive = archivePlanState(session);
12792
+ if (archive) {
12793
+ setHistorical((prev) => [
12794
+ ...prev,
12795
+ {
12796
+ id: `plan-archived-${Date.now()}`,
12797
+ role: "info",
12798
+ text: `\u25B8 plan complete \u2014 all ${total} step${total === 1 ? "" : "s"} done \xB7 archived`
12799
+ }
12800
+ ]);
12801
+ }
12802
+ }
12803
+ if (typeof parsed.error === "string" && parsed.error.startsWith("PlanCheckpointError:")) {
12804
+ setPendingCheckpoint({ stepId, title, completed, total });
12805
+ }
12806
+ }
12807
+ } catch {
12808
+ }
12809
+ }
11633
12810
  } else if (ev.role === "error") {
11634
12811
  setHistorical((prev) => [
11635
12812
  ...prev,
@@ -11717,7 +12894,8 @@ function App({
11717
12894
  refreshBalance,
11718
12895
  refreshLatestVersion,
11719
12896
  refreshModels,
11720
- proArmed
12897
+ proArmed,
12898
+ persistPlanState
11721
12899
  ]
11722
12900
  );
11723
12901
  const handleShellConfirm = useCallback4(
@@ -11840,6 +13018,11 @@ ${body}`;
11840
13018
  return;
11841
13019
  }
11842
13020
  setPendingPlan(null);
13021
+ planStepsRef.current = null;
13022
+ completedStepIdsRef.current = /* @__PURE__ */ new Set();
13023
+ planBodyRef.current = null;
13024
+ planSummaryRef.current = null;
13025
+ persistPlanState();
11843
13026
  togglePlanMode(false);
11844
13027
  const marker = "\u25B8 plan cancelled";
11845
13028
  const synthetic = "The plan was cancelled. Drop it entirely. Ask me what I actually want before proposing another plan or making any changes.";
@@ -11854,7 +13037,7 @@ ${body}`;
11854
13037
  await handleSubmit(synthetic);
11855
13038
  }
11856
13039
  },
11857
- [pendingPlan, togglePlanMode, busy, loop, handleSubmit]
13040
+ [pendingPlan, togglePlanMode, busy, loop, handleSubmit, persistPlanState]
11858
13041
  );
11859
13042
  const handlePlanConfirmRef = useRef5(handlePlanConfirm);
11860
13043
  useEffect5(() => {
@@ -11917,104 +13100,364 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
11917
13100
  if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
11918
13101
  setStagedInput(null);
11919
13102
  }, [stagedInput]);
11920
- return /* @__PURE__ */ React16.createElement(TickerProvider, { disabled: PLAIN_UI || !!pendingPlan || !!pendingShell || !!pendingEditReview }, /* @__PURE__ */ React16.createElement(Box15, { flexDirection: "column" }, /* @__PURE__ */ React16.createElement(
11921
- StatsPanel,
11922
- {
11923
- summary,
11924
- model: loop.model,
11925
- prefixHash,
11926
- harvestOn: loop.harvestEnabled,
11927
- branchBudget: loop.branchOptions.budget,
11928
- reasoningEffort: loop.reasoningEffort,
11929
- planMode,
11930
- editMode: codeMode ? editMode : void 0,
11931
- balance,
11932
- busy,
11933
- updateAvailable,
11934
- proArmed,
11935
- escalated: turnOnPro
11936
- }
11937
- ), /* @__PURE__ */ React16.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React16.createElement(EventRow, { key: item.id, event: item, projectRoot: hookCwd })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && streaming ? /* @__PURE__ */ React16.createElement(Box15, { marginY: 1 }, /* @__PURE__ */ React16.createElement(EventRow, { event: streaming, projectRoot: hookCwd })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && ongoingTool ? /* @__PURE__ */ React16.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && subagentActivity ? /* @__PURE__ */ React16.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !ongoingTool && statusLine ? /* @__PURE__ */ React16.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && undoBanner && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview ? /* @__PURE__ */ React16.createElement(UndoBanner, { banner: undoBanner }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React16.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React16.createElement(
11938
- PlanRefineInput,
11939
- {
11940
- mode: stagedInput.mode,
11941
- onSubmit: handleStagedInputSubmit,
11942
- onCancel: handleStagedInputCancel
11943
- }
11944
- ) : pendingPlan ? /* @__PURE__ */ React16.createElement(
11945
- PlanConfirm,
11946
- {
11947
- plan: pendingPlan,
11948
- onChoose: stableHandlePlanConfirm,
11949
- projectRoot: hookCwd
11950
- }
11951
- ) : pendingShell ? /* @__PURE__ */ React16.createElement(
11952
- ShellConfirm,
11953
- {
11954
- command: pendingShell.command,
11955
- allowPrefix: derivePrefix(pendingShell.command),
11956
- kind: pendingShell.kind,
11957
- onChoose: handleShellConfirm
11958
- }
11959
- ) : pendingEditReview ? /* @__PURE__ */ React16.createElement(
11960
- EditConfirm,
11961
- {
11962
- block: pendingEditReview,
11963
- onChoose: (choice) => {
11964
- const resolve8 = editReviewResolveRef.current;
11965
- if (resolve8) {
11966
- editReviewResolveRef.current = null;
11967
- resolve8(choice);
13103
+ const handleCheckpointConfirm = useCallback4(
13104
+ async (choice) => {
13105
+ const snap = pendingCheckpoint;
13106
+ if (!snap) return;
13107
+ setPendingCheckpoint(null);
13108
+ if (choice === "revise") {
13109
+ setStagedCheckpointRevise(snap);
13110
+ return;
13111
+ }
13112
+ const label = snap.title ? `${snap.stepId} \xB7 ${snap.title}` : snap.stepId;
13113
+ const counter = snap.total > 0 ? ` (${snap.completed}/${snap.total})` : "";
13114
+ const { marker, synthetic } = choice === "continue" ? {
13115
+ marker: `\u25B8 continuing after ${label}${counter}`,
13116
+ synthetic: `Step ${label} is complete. Proceed with the next step of the approved plan. If no steps remain, summarize the whole run.`
13117
+ } : {
13118
+ marker: `\u25B8 plan stopped at ${label}${counter}`,
13119
+ synthetic: `The user stopped the plan after step ${label}. Do not run any more steps and do not call any tools. Write a short summary of what was completed across all finished steps and what's left unfinished.`
13120
+ };
13121
+ setHistorical((prev) => [
13122
+ ...prev,
13123
+ { id: `cp-${choice}-${Date.now()}`, role: "info", text: marker }
13124
+ ]);
13125
+ if (busy) {
13126
+ loop.abort();
13127
+ setQueuedSubmit(synthetic);
13128
+ } else {
13129
+ await handleSubmit(synthetic);
13130
+ }
13131
+ },
13132
+ [pendingCheckpoint, busy, loop, handleSubmit]
13133
+ );
13134
+ const handleCheckpointConfirmRef = useRef5(handleCheckpointConfirm);
13135
+ useEffect5(() => {
13136
+ handleCheckpointConfirmRef.current = handleCheckpointConfirm;
13137
+ }, [handleCheckpointConfirm]);
13138
+ const stableHandleCheckpointConfirm = useCallback4(
13139
+ async (choice) => handleCheckpointConfirmRef.current(choice),
13140
+ []
13141
+ );
13142
+ const handleCheckpointReviseSubmit = useCallback4(
13143
+ async (feedback) => {
13144
+ const snap = stagedCheckpointRevise;
13145
+ setStagedCheckpointRevise(null);
13146
+ if (!snap) return;
13147
+ const label = snap.title ? `${snap.stepId} \xB7 ${snap.title}` : snap.stepId;
13148
+ const trimmed = feedback.trim();
13149
+ const synthetic = trimmed ? `Step ${label} is complete. Before running the next step, adjust based on this user feedback:
13150
+
13151
+ ${trimmed}
13152
+
13153
+ If the feedback only tweaks how you execute (extra constraints, style preferences), continue with the updated guidance. If it changes which steps run (skip a step, swap two steps, add a new step), call \`revise_plan\` with the updated remainingSteps \u2014 that pops a diff picker the user can accept or reject. Only call submit_plan again if the entire approach has fundamentally changed.` : `Step ${label} is complete. Continue with the current plan.`;
13154
+ const marker = trimmed ? `\u25B8 revising after ${label} \u2014 ${trimmed.length > 50 ? `${trimmed.slice(0, 50)}\u2026` : trimmed}` : `\u25B8 continuing after ${label}`;
13155
+ setHistorical((prev) => [
13156
+ ...prev,
13157
+ { id: `cp-revise-${Date.now()}`, role: "info", text: marker }
13158
+ ]);
13159
+ if (busy) {
13160
+ loop.abort();
13161
+ setQueuedSubmit(synthetic);
13162
+ } else {
13163
+ await handleSubmit(synthetic);
13164
+ }
13165
+ },
13166
+ [stagedCheckpointRevise, busy, loop, handleSubmit]
13167
+ );
13168
+ const handleCheckpointReviseCancel = useCallback4(() => {
13169
+ const snap = stagedCheckpointRevise;
13170
+ setStagedCheckpointRevise(null);
13171
+ if (snap) setPendingCheckpoint(snap);
13172
+ }, [stagedCheckpointRevise]);
13173
+ const handleChoiceConfirm = useCallback4(
13174
+ async (choice) => {
13175
+ const snap = pendingChoice;
13176
+ if (!snap) return;
13177
+ setPendingChoice(null);
13178
+ if (choice.kind === "custom") {
13179
+ setStagedChoiceCustom(snap);
13180
+ return;
13181
+ }
13182
+ if (choice.kind === "cancel") {
13183
+ const synthetic2 = "The user cancelled the choice. Don't act on any of the options you presented. Ask what they actually want before doing anything else.";
13184
+ setHistorical((prev) => [
13185
+ ...prev,
13186
+ { id: `choice-cancel-${Date.now()}`, role: "info", text: "\u25B8 choice cancelled" }
13187
+ ]);
13188
+ if (busy) {
13189
+ loop.abort();
13190
+ setQueuedSubmit(synthetic2);
13191
+ } else {
13192
+ await handleSubmit(synthetic2);
11968
13193
  }
13194
+ return;
11969
13195
  }
11970
- }
11971
- ) : /* @__PURE__ */ React16.createElement(React16.Fragment, null, codeMode ? /* @__PURE__ */ React16.createElement(
11972
- ModeStatusBar,
11973
- {
11974
- editMode,
11975
- pendingCount,
11976
- flash: modeFlash,
11977
- planMode,
11978
- undoArmed: !!undoBanner || hasUndoable(),
11979
- jobs: codeMode.jobs
11980
- }
11981
- ) : null, /* @__PURE__ */ React16.createElement(
11982
- PromptInput,
11983
- {
11984
- value: input,
11985
- onChange: setInput,
11986
- onSubmit: handleSubmit,
11987
- disabled: busy
11988
- }
11989
- ), /* @__PURE__ */ React16.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }), /* @__PURE__ */ React16.createElement(
11990
- AtMentionSuggestions,
11991
- {
11992
- matches: atMatches,
11993
- selectedIndex: atSelected,
11994
- query: atPicker?.query ?? ""
11995
- }
11996
- ), slashArgContext ? /* @__PURE__ */ React16.createElement(
11997
- SlashArgPicker,
13196
+ const picked = snap.options.find((o) => o.id === choice.optionId);
13197
+ const label = picked ? `${picked.id} \xB7 ${picked.title}` : choice.optionId;
13198
+ const synthetic = `The user picked option ${choice.optionId}${picked ? ` ("${picked.title}")` : ""}. Proceed with that branch. Do not re-ask the same question.`;
13199
+ setHistorical((prev) => [
13200
+ ...prev,
13201
+ { id: `choice-pick-${Date.now()}`, role: "info", text: `\u25B8 chose ${label}` }
13202
+ ]);
13203
+ if (busy) {
13204
+ loop.abort();
13205
+ setQueuedSubmit(synthetic);
13206
+ } else {
13207
+ await handleSubmit(synthetic);
13208
+ }
13209
+ },
13210
+ [pendingChoice, busy, loop, handleSubmit]
13211
+ );
13212
+ const handleChoiceConfirmRef = useRef5(handleChoiceConfirm);
13213
+ useEffect5(() => {
13214
+ handleChoiceConfirmRef.current = handleChoiceConfirm;
13215
+ }, [handleChoiceConfirm]);
13216
+ const stableHandleChoiceConfirm = useCallback4(
13217
+ async (choice) => handleChoiceConfirmRef.current(choice),
13218
+ []
13219
+ );
13220
+ const handleChoiceCustomSubmit = useCallback4(
13221
+ async (answer) => {
13222
+ setStagedChoiceCustom(null);
13223
+ const trimmed = answer.trim();
13224
+ const synthetic = trimmed ? `The user answered with a custom reply (none of the pre-defined options fit):
13225
+
13226
+ ${trimmed}
13227
+
13228
+ Read it carefully and proceed \u2014 don't snap back to the options you listed unless the user's reply clearly maps to one.` : "The user pressed Enter without typing anything. Ask what they actually want.";
13229
+ const marker = trimmed ? `\u25B8 custom answer \u2014 ${trimmed.length > 50 ? `${trimmed.slice(0, 50)}\u2026` : trimmed}` : "\u25B8 custom answer \u2014 (blank)";
13230
+ setHistorical((prev) => [
13231
+ ...prev,
13232
+ { id: `choice-custom-${Date.now()}`, role: "info", text: marker }
13233
+ ]);
13234
+ if (busy) {
13235
+ loop.abort();
13236
+ setQueuedSubmit(synthetic);
13237
+ } else {
13238
+ await handleSubmit(synthetic);
13239
+ }
13240
+ },
13241
+ [busy, loop, handleSubmit]
13242
+ );
13243
+ const handleChoiceCustomCancel = useCallback4(() => {
13244
+ const snap = stagedChoiceCustom;
13245
+ setStagedChoiceCustom(null);
13246
+ if (snap) setPendingChoice(snap);
13247
+ }, [stagedChoiceCustom]);
13248
+ const handleReviseConfirm = useCallback4(
13249
+ async (choice) => {
13250
+ const snap = pendingRevision;
13251
+ if (!snap) return;
13252
+ setPendingRevision(null);
13253
+ if (choice === "reject") {
13254
+ const synthetic2 = "The user rejected the proposed plan revision. Don't apply it. Continue executing the original plan from the next pending step. If you genuinely cannot proceed without the change, stop and explain in plain text why.";
13255
+ setHistorical((prev) => [
13256
+ ...prev,
13257
+ { id: `revise-reject-${Date.now()}`, role: "info", text: "\u25B8 revision rejected" }
13258
+ ]);
13259
+ if (busy) {
13260
+ loop.abort();
13261
+ setQueuedSubmit(synthetic2);
13262
+ } else {
13263
+ await handleSubmit(synthetic2);
13264
+ }
13265
+ return;
13266
+ }
13267
+ const completed = completedStepIdsRef.current;
13268
+ const oldSteps = planStepsRef.current ?? [];
13269
+ const donePrefix = oldSteps.filter((s) => completed.has(s.id));
13270
+ const merged = [...donePrefix];
13271
+ for (const s of snap.remainingSteps) {
13272
+ if (completed.has(s.id)) continue;
13273
+ merged.push(s);
13274
+ }
13275
+ planStepsRef.current = merged;
13276
+ persistPlanState();
13277
+ const removedCount = oldSteps.filter(
13278
+ (s) => !completed.has(s.id) && !snap.remainingSteps.some((n) => n.id === s.id)
13279
+ ).length;
13280
+ const addedCount = snap.remainingSteps.filter(
13281
+ (s) => !oldSteps.some((o) => o.id === s.id)
13282
+ ).length;
13283
+ const marker = `\u25B8 revision accepted \u2014 \u2212${removedCount} +${addedCount}: ${snap.reason}`;
13284
+ setHistorical((prev) => [
13285
+ ...prev,
13286
+ { id: `revise-accept-${Date.now()}`, role: "info", text: marker }
13287
+ ]);
13288
+ const synthetic = `Revision accepted. The remaining plan is now:
13289
+ ${snap.remainingSteps.map((s, i) => ` ${i + 1}. ${s.id} \xB7 ${s.title} \u2014 ${s.action}`).join(
13290
+ "\n"
13291
+ )}
13292
+
13293
+ Continue executing from the next pending step. Call mark_step_complete after each one as before.`;
13294
+ if (busy) {
13295
+ loop.abort();
13296
+ setQueuedSubmit(synthetic);
13297
+ } else {
13298
+ await handleSubmit(synthetic);
13299
+ }
13300
+ },
13301
+ [pendingRevision, busy, loop, handleSubmit, persistPlanState]
13302
+ );
13303
+ const handleReviseConfirmRef = useRef5(handleReviseConfirm);
13304
+ useEffect5(() => {
13305
+ handleReviseConfirmRef.current = handleReviseConfirm;
13306
+ }, [handleReviseConfirm]);
13307
+ const stableHandleReviseConfirm = useCallback4(
13308
+ async (choice) => handleReviseConfirmRef.current(choice),
13309
+ []
13310
+ );
13311
+ return /* @__PURE__ */ React20.createElement(
13312
+ TickerProvider,
11998
13313
  {
11999
- matches: slashArgMatches,
12000
- selectedIndex: slashArgSelected,
12001
- spec: slashArgContext.spec,
12002
- kind: slashArgContext.kind,
12003
- partial: slashArgContext.partial
12004
- }
12005
- ) : null)));
13314
+ disabled: PLAIN_UI || !!pendingPlan || !!pendingShell || !!pendingEditReview || !!pendingCheckpoint || !!stagedCheckpointRevise || !!pendingChoice || !!stagedChoiceCustom || !!pendingRevision
13315
+ },
13316
+ /* @__PURE__ */ React20.createElement(Box19, { flexDirection: "column" }, /* @__PURE__ */ React20.createElement(
13317
+ StatsPanel,
13318
+ {
13319
+ summary,
13320
+ model: loop.model,
13321
+ prefixHash,
13322
+ harvestOn: loop.harvestEnabled,
13323
+ branchBudget: loop.branchOptions.budget,
13324
+ reasoningEffort: loop.reasoningEffort,
13325
+ planMode,
13326
+ editMode: codeMode ? editMode : void 0,
13327
+ balance,
13328
+ busy,
13329
+ updateAvailable,
13330
+ proArmed,
13331
+ escalated: turnOnPro
13332
+ }
13333
+ ), /* @__PURE__ */ React20.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React20.createElement(EventRow, { key: item.id, event: item, projectRoot: hookCwd })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && streaming ? /* @__PURE__ */ React20.createElement(Box19, { marginY: 1 }, /* @__PURE__ */ React20.createElement(EventRow, { event: streaming, projectRoot: hookCwd })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && ongoingTool ? /* @__PURE__ */ React20.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && subagentActivity ? /* @__PURE__ */ React20.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !ongoingTool && statusLine ? /* @__PURE__ */ React20.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && undoBanner && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision ? /* @__PURE__ */ React20.createElement(UndoBanner, { banner: undoBanner }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React20.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React20.createElement(
13334
+ PlanRefineInput,
13335
+ {
13336
+ mode: stagedInput.mode,
13337
+ onSubmit: handleStagedInputSubmit,
13338
+ onCancel: handleStagedInputCancel
13339
+ }
13340
+ ) : stagedCheckpointRevise ? /* @__PURE__ */ React20.createElement(
13341
+ PlanRefineInput,
13342
+ {
13343
+ mode: "checkpoint-revise",
13344
+ onSubmit: handleCheckpointReviseSubmit,
13345
+ onCancel: handleCheckpointReviseCancel
13346
+ }
13347
+ ) : stagedChoiceCustom ? /* @__PURE__ */ React20.createElement(
13348
+ PlanRefineInput,
13349
+ {
13350
+ mode: "choice-custom",
13351
+ onSubmit: handleChoiceCustomSubmit,
13352
+ onCancel: handleChoiceCustomCancel
13353
+ }
13354
+ ) : pendingChoice ? /* @__PURE__ */ React20.createElement(
13355
+ ChoiceConfirm,
13356
+ {
13357
+ question: pendingChoice.question,
13358
+ options: pendingChoice.options,
13359
+ allowCustom: pendingChoice.allowCustom,
13360
+ onChoose: stableHandleChoiceConfirm
13361
+ }
13362
+ ) : pendingRevision ? /* @__PURE__ */ React20.createElement(
13363
+ PlanReviseConfirm,
13364
+ {
13365
+ reason: pendingRevision.reason,
13366
+ oldRemaining: (planStepsRef.current ?? []).filter(
13367
+ (s) => !completedStepIdsRef.current.has(s.id)
13368
+ ),
13369
+ newRemaining: pendingRevision.remainingSteps,
13370
+ summary: pendingRevision.summary,
13371
+ onChoose: stableHandleReviseConfirm
13372
+ }
13373
+ ) : pendingCheckpoint ? /* @__PURE__ */ React20.createElement(
13374
+ PlanCheckpointConfirm,
13375
+ {
13376
+ stepId: pendingCheckpoint.stepId,
13377
+ title: pendingCheckpoint.title,
13378
+ completed: pendingCheckpoint.completed,
13379
+ total: pendingCheckpoint.total,
13380
+ steps: planStepsRef.current ?? void 0,
13381
+ completedStepIds: completedStepIdsRef.current,
13382
+ onChoose: stableHandleCheckpointConfirm
13383
+ }
13384
+ ) : pendingPlan ? /* @__PURE__ */ React20.createElement(
13385
+ PlanConfirm,
13386
+ {
13387
+ plan: pendingPlan,
13388
+ steps: planStepsRef.current ?? void 0,
13389
+ summary: planSummaryRef.current ?? void 0,
13390
+ onChoose: stableHandlePlanConfirm,
13391
+ projectRoot: hookCwd
13392
+ }
13393
+ ) : pendingShell ? /* @__PURE__ */ React20.createElement(
13394
+ ShellConfirm,
13395
+ {
13396
+ command: pendingShell.command,
13397
+ allowPrefix: derivePrefix(pendingShell.command),
13398
+ kind: pendingShell.kind,
13399
+ onChoose: handleShellConfirm
13400
+ }
13401
+ ) : pendingEditReview ? /* @__PURE__ */ React20.createElement(
13402
+ EditConfirm,
13403
+ {
13404
+ block: pendingEditReview,
13405
+ onChoose: (choice) => {
13406
+ const resolve8 = editReviewResolveRef.current;
13407
+ if (resolve8) {
13408
+ editReviewResolveRef.current = null;
13409
+ resolve8(choice);
13410
+ }
13411
+ }
13412
+ }
13413
+ ) : /* @__PURE__ */ React20.createElement(React20.Fragment, null, codeMode ? /* @__PURE__ */ React20.createElement(
13414
+ ModeStatusBar,
13415
+ {
13416
+ editMode,
13417
+ pendingCount,
13418
+ flash: modeFlash,
13419
+ planMode,
13420
+ undoArmed: !!undoBanner || hasUndoable(),
13421
+ jobs: codeMode.jobs
13422
+ }
13423
+ ) : null, /* @__PURE__ */ React20.createElement(
13424
+ PromptInput,
13425
+ {
13426
+ value: input,
13427
+ onChange: setInput,
13428
+ onSubmit: handleSubmit,
13429
+ disabled: busy
13430
+ }
13431
+ ), /* @__PURE__ */ React20.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }), /* @__PURE__ */ React20.createElement(
13432
+ AtMentionSuggestions,
13433
+ {
13434
+ matches: atMatches,
13435
+ selectedIndex: atSelected,
13436
+ query: atPicker?.query ?? ""
13437
+ }
13438
+ ), slashArgContext ? /* @__PURE__ */ React20.createElement(
13439
+ SlashArgPicker,
13440
+ {
13441
+ matches: slashArgMatches,
13442
+ selectedIndex: slashArgSelected,
13443
+ spec: slashArgContext.spec,
13444
+ kind: slashArgContext.kind,
13445
+ partial: slashArgContext.partial
13446
+ }
13447
+ ) : null))
13448
+ );
12006
13449
  }
12007
13450
 
12008
13451
  // src/cli/ui/SessionPicker.tsx
12009
- import { Box as Box16, Text as Text15 } from "ink";
12010
- import React17 from "react";
13452
+ import { Box as Box20, Text as Text19 } from "ink";
13453
+ import React21 from "react";
12011
13454
  function SessionPicker({
12012
13455
  sessionName,
12013
13456
  messageCount,
12014
13457
  lastActive,
12015
13458
  onChoose
12016
13459
  }) {
12017
- return /* @__PURE__ */ React17.createElement(Box16, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React17.createElement(Box16, { marginBottom: 1 }, /* @__PURE__ */ React17.createElement(Text15, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React17.createElement(Text15, { dimColor: true }, ` \xB7 last active ${relativeTime(lastActive)}`)), /* @__PURE__ */ React17.createElement(
13460
+ return /* @__PURE__ */ React21.createElement(Box20, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React21.createElement(Box20, { marginBottom: 1 }, /* @__PURE__ */ React21.createElement(Text19, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React21.createElement(Text19, { dimColor: true }, ` \xB7 last active ${relativeTime2(lastActive)}`)), /* @__PURE__ */ React21.createElement(
12018
13461
  SingleSelect,
12019
13462
  {
12020
13463
  initialValue: "new",
@@ -12037,9 +13480,9 @@ function SessionPicker({
12037
13480
  ],
12038
13481
  onSubmit: (v) => onChoose(v)
12039
13482
  }
12040
- ), /* @__PURE__ */ React17.createElement(Box16, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text15, { dimColor: true }, "\u2191\u2193 to move \xB7 Enter to pick")));
13483
+ ), /* @__PURE__ */ React21.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React21.createElement(Text19, { dimColor: true }, "\u2191\u2193 to move \xB7 Enter to pick")));
12041
13484
  }
12042
- function relativeTime(date) {
13485
+ function relativeTime2(date) {
12043
13486
  const ms = Date.now() - date.getTime();
12044
13487
  const mins = Math.floor(ms / 6e4);
12045
13488
  if (mins < 1) return "just now";
@@ -12053,9 +13496,9 @@ function relativeTime(date) {
12053
13496
  }
12054
13497
 
12055
13498
  // src/cli/ui/Setup.tsx
12056
- import { Box as Box17, Text as Text16, useApp as useApp2 } from "ink";
13499
+ import { Box as Box21, Text as Text20, useApp as useApp2 } from "ink";
12057
13500
  import TextInput from "ink-text-input";
12058
- import React18, { useState as useState11 } from "react";
13501
+ import React22, { useState as useState11 } from "react";
12059
13502
  function Setup({ onReady }) {
12060
13503
  const [value, setValue] = useState11("");
12061
13504
  const [error, setError] = useState11(null);
@@ -12079,7 +13522,7 @@ function Setup({ onReady }) {
12079
13522
  }
12080
13523
  onReady(trimmed);
12081
13524
  };
12082
- return /* @__PURE__ */ React18.createElement(Box17, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React18.createElement(Text16, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React18.createElement(Box17, { marginTop: 1 }, /* @__PURE__ */ React18.createElement(Text16, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React18.createElement(Text16, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React18.createElement(Text16, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React18.createElement(Box17, { marginTop: 1 }, /* @__PURE__ */ React18.createElement(Text16, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React18.createElement(
13525
+ return /* @__PURE__ */ React22.createElement(Box21, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React22.createElement(Text20, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React22.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text20, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React22.createElement(Text20, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React22.createElement(Text20, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React22.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text20, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React22.createElement(
12083
13526
  TextInput,
12084
13527
  {
12085
13528
  value,
@@ -12088,7 +13531,7 @@ function Setup({ onReady }) {
12088
13531
  mask: "\u2022",
12089
13532
  placeholder: "sk-..."
12090
13533
  }
12091
- )), error ? /* @__PURE__ */ React18.createElement(Box17, { marginTop: 1 }, /* @__PURE__ */ React18.createElement(Text16, { color: "red" }, error)) : value ? /* @__PURE__ */ React18.createElement(Box17, { marginTop: 1 }, /* @__PURE__ */ React18.createElement(Text16, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React18.createElement(Box17, { marginTop: 1 }, /* @__PURE__ */ React18.createElement(Text16, { dimColor: true }, "(Type /exit to abort.)")));
13534
+ )), error ? /* @__PURE__ */ React22.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text20, { color: "red" }, error)) : value ? /* @__PURE__ */ React22.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text20, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React22.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(Text20, { dimColor: true }, "(Type /exit to abort.)")));
12092
13535
  }
12093
13536
 
12094
13537
  // src/cli/commands/chat.tsx
@@ -12104,7 +13547,7 @@ function Root({
12104
13547
  const [key, setKey] = useState12(initialKey);
12105
13548
  const [pending, setPending] = useState12(sessionPreview);
12106
13549
  if (!key) {
12107
- return /* @__PURE__ */ React19.createElement(
13550
+ return /* @__PURE__ */ React23.createElement(
12108
13551
  Setup,
12109
13552
  {
12110
13553
  onReady: (k) => {
@@ -12116,7 +13559,7 @@ function Root({
12116
13559
  }
12117
13560
  process.env.DEEPSEEK_API_KEY = key;
12118
13561
  if (pending && appProps.session) {
12119
- return /* @__PURE__ */ React19.createElement(
13562
+ return /* @__PURE__ */ React23.createElement(
12120
13563
  SessionPicker,
12121
13564
  {
12122
13565
  sessionName: appProps.session,
@@ -12131,7 +13574,7 @@ function Root({
12131
13574
  }
12132
13575
  );
12133
13576
  }
12134
- return /* @__PURE__ */ React19.createElement(
13577
+ return /* @__PURE__ */ React23.createElement(
12135
13578
  App,
12136
13579
  {
12137
13580
  model: appProps.model,
@@ -12222,20 +13665,21 @@ async function chatCommand(opts) {
12222
13665
  if (!opts.seedTools) {
12223
13666
  if (!tools) tools = new ToolRegistry();
12224
13667
  registerMemoryTools(tools, {});
13668
+ registerChoiceTool(tools);
12225
13669
  }
12226
13670
  let sessionPreview;
12227
13671
  if (opts.session && !opts.forceResume && !opts.forceNew) {
12228
13672
  const prior = loadSessionMessages(opts.session);
12229
13673
  if (prior.length > 0) {
12230
13674
  const p = sessionPath(opts.session);
12231
- const mtime = existsSync11(p) ? statSync6(p).mtime : /* @__PURE__ */ new Date();
13675
+ const mtime = existsSync12(p) ? statSync7(p).mtime : /* @__PURE__ */ new Date();
12232
13676
  sessionPreview = { messageCount: prior.length, lastActive: mtime };
12233
13677
  }
12234
13678
  } else if (opts.session && opts.forceNew) {
12235
13679
  rewriteSession(opts.session, []);
12236
13680
  }
12237
13681
  const { waitUntilExit } = render(
12238
- /* @__PURE__ */ React19.createElement(
13682
+ /* @__PURE__ */ React23.createElement(
12239
13683
  Root,
12240
13684
  {
12241
13685
  initialKey,
@@ -12259,11 +13703,11 @@ async function chatCommand(opts) {
12259
13703
  }
12260
13704
 
12261
13705
  // src/cli/commands/code.tsx
12262
- import { basename, resolve as resolve7 } from "path";
13706
+ import { basename as basename2, resolve as resolve7 } from "path";
12263
13707
  async function codeCommand(opts = {}) {
12264
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-KX6A4DVX.js");
13708
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-2OABSPAW.js");
12265
13709
  const rootDir = resolve7(opts.dir ?? process.cwd());
12266
- const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
13710
+ const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
12267
13711
  const tools = new ToolRegistry();
12268
13712
  registerFilesystemTools(tools, { rootDir });
12269
13713
  const jobs2 = new JobRegistry();
@@ -12278,6 +13722,7 @@ async function codeCommand(opts = {}) {
12278
13722
  jobs: jobs2
12279
13723
  });
12280
13724
  registerPlanTool(tools);
13725
+ registerChoiceTool(tools);
12281
13726
  registerMemoryTools(tools, { projectRoot: rootDir });
12282
13727
  process.stderr.write(
12283
13728
  `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
@@ -12303,37 +13748,37 @@ async function codeCommand(opts = {}) {
12303
13748
  }
12304
13749
 
12305
13750
  // src/cli/commands/diff.ts
12306
- import { writeFileSync as writeFileSync6 } from "fs";
12307
- import { basename as basename2 } from "path";
13751
+ import { writeFileSync as writeFileSync7 } from "fs";
13752
+ import { basename as basename3 } from "path";
12308
13753
  import { render as render2 } from "ink";
12309
- import React22 from "react";
13754
+ import React26 from "react";
12310
13755
 
12311
13756
  // src/cli/ui/DiffApp.tsx
12312
- import { Box as Box19, Static as Static2, Text as Text18, useApp as useApp3, useInput as useInput6 } from "ink";
12313
- import React21, { useState as useState13 } from "react";
13757
+ import { Box as Box23, Static as Static2, Text as Text22, useApp as useApp3, useInput as useInput6 } from "ink";
13758
+ import React25, { useState as useState13 } from "react";
12314
13759
 
12315
13760
  // src/cli/ui/RecordView.tsx
12316
- import { Box as Box18, Text as Text17 } from "ink";
12317
- import React20 from "react";
13761
+ import { Box as Box22, Text as Text21 } from "ink";
13762
+ import React24 from "react";
12318
13763
  function RecordView({ rec, compact: compact2 = false }) {
12319
13764
  const toolArgsMax = compact2 ? 120 : 200;
12320
13765
  const toolContentMax = compact2 ? 200 : 400;
12321
13766
  if (rec.role === "user") {
12322
- return /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1 }, /* @__PURE__ */ React20.createElement(Text17, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React20.createElement(Text17, null, rec.content));
13767
+ return /* @__PURE__ */ React24.createElement(Box22, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text21, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React24.createElement(Text21, null, rec.content));
12323
13768
  }
12324
13769
  if (rec.role === "assistant_final") {
12325
- return /* @__PURE__ */ React20.createElement(Box18, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React20.createElement(Box18, null, /* @__PURE__ */ React20.createElement(Text17, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React20.createElement(Text17, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React20.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React20.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React20.createElement(Text17, null, rec.content) : /* @__PURE__ */ React20.createElement(Text17, { dimColor: true, italic: true }, "(tool-call response only)"));
13770
+ return /* @__PURE__ */ React24.createElement(Box22, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React24.createElement(Box22, null, /* @__PURE__ */ React24.createElement(Text21, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React24.createElement(Text21, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React24.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React24.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React24.createElement(Text21, null, rec.content) : /* @__PURE__ */ React24.createElement(Text21, { dimColor: true, italic: true }, "(tool-call response only)"));
12326
13771
  }
12327
13772
  if (rec.role === "tool") {
12328
- return /* @__PURE__ */ React20.createElement(Box18, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React20.createElement(Text17, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React20.createElement(Text17, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React20.createElement(Text17, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
13773
+ return /* @__PURE__ */ React24.createElement(Box22, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text21, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React24.createElement(Text21, { dimColor: true }, " args: ", truncate2(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React24.createElement(Text21, { dimColor: true }, " \u2192 ", truncate2(rec.content, toolContentMax)));
12329
13774
  }
12330
13775
  if (rec.role === "error") {
12331
- return /* @__PURE__ */ React20.createElement(Box18, { marginTop: 1 }, /* @__PURE__ */ React20.createElement(Text17, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React20.createElement(Text17, { color: "red" }, rec.error ?? rec.content));
13776
+ return /* @__PURE__ */ React24.createElement(Box22, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text21, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React24.createElement(Text21, { color: "red" }, rec.error ?? rec.content));
12332
13777
  }
12333
13778
  if (rec.role === "done" || rec.role === "assistant_delta") {
12334
13779
  return null;
12335
13780
  }
12336
- return /* @__PURE__ */ React20.createElement(Box18, null, /* @__PURE__ */ React20.createElement(Text17, { dimColor: true }, "[", rec.role, "] ", rec.content));
13781
+ return /* @__PURE__ */ React24.createElement(Box22, null, /* @__PURE__ */ React24.createElement(Text21, { dimColor: true }, "[", rec.role, "] ", rec.content));
12337
13782
  }
12338
13783
  function CacheBadge({ usage }) {
12339
13784
  const hit = usage.prompt_cache_hit_tokens ?? 0;
@@ -12342,9 +13787,9 @@ function CacheBadge({ usage }) {
12342
13787
  if (total === 0) return null;
12343
13788
  const pct2 = hit / total * 100;
12344
13789
  const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
12345
- return /* @__PURE__ */ React20.createElement(Text17, null, /* @__PURE__ */ React20.createElement(Text17, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React20.createElement(Text17, { color }, pct2.toFixed(1), "%"));
13790
+ return /* @__PURE__ */ React24.createElement(Text21, null, /* @__PURE__ */ React24.createElement(Text21, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React24.createElement(Text21, { color }, pct2.toFixed(1), "%"));
12346
13791
  }
12347
- function truncate3(s, max) {
13792
+ function truncate2(s, max) {
12348
13793
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
12349
13794
  }
12350
13795
 
@@ -12376,7 +13821,7 @@ function DiffApp({ report }) {
12376
13821
  }
12377
13822
  });
12378
13823
  const pair = report.pairs[idx];
12379
- return /* @__PURE__ */ React21.createElement(Box19, { flexDirection: "column" }, /* @__PURE__ */ React21.createElement(DiffHeader, { report }), /* @__PURE__ */ React21.createElement(Box19, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React21.createElement(Text18, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React21.createElement(Text18, null, pair ? /* @__PURE__ */ React21.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React21.createElement(Box19, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React21.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React21.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React21.createElement(Box19, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React21.createElement(Text18, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React21.createElement(Text18, null, pair.divergenceNote)) : null, /* @__PURE__ */ React21.createElement(Box19, { 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"), " next \xB7 ", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "k"), "/", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "N"), "/", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "g"), "/", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React21.createElement(Text18, { bold: true }, "q"), " ", "quit")));
13824
+ return /* @__PURE__ */ React25.createElement(Box23, { flexDirection: "column" }, /* @__PURE__ */ React25.createElement(DiffHeader, { report }), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React25.createElement(Text22, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React25.createElement(Text22, null, pair ? /* @__PURE__ */ React25.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React25.createElement(Box23, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React25.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React25.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React25.createElement(Text22, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React25.createElement(Text22, null, pair.divergenceNote)) : null, /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React25.createElement(Text22, { dimColor: true }, /* @__PURE__ */ React25.createElement(Text22, { bold: true }, "j"), "/", /* @__PURE__ */ React25.createElement(Text22, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React25.createElement(Text22, { bold: true }, "k"), "/", /* @__PURE__ */ React25.createElement(Text22, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React25.createElement(Text22, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React25.createElement(Text22, { bold: true }, "N"), "/", /* @__PURE__ */ React25.createElement(Text22, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React25.createElement(Text22, { bold: true }, "g"), "/", /* @__PURE__ */ React25.createElement(Text22, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React25.createElement(Text22, { bold: true }, "q"), " ", "quit")));
12380
13825
  }
12381
13826
  function DiffHeader({ report }) {
12382
13827
  const a = report.a;
@@ -12394,15 +13839,15 @@ function DiffHeader({ report }) {
12394
13839
  } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
12395
13840
  prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
12396
13841
  }
12397
- return /* @__PURE__ */ React21.createElement(Box19, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React21.createElement(Box19, { justifyContent: "space-between" }, /* @__PURE__ */ React21.createElement(Text18, null, /* @__PURE__ */ React21.createElement(Text18, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React21.createElement(Text18, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React21.createElement(Text18, { color: "blue" }, a.label), /* @__PURE__ */ React21.createElement(Text18, { dimColor: true }, " vs B="), /* @__PURE__ */ React21.createElement(Text18, { color: "magenta" }, b.label)), /* @__PURE__ */ React21.createElement(Text18, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React21.createElement(Box19, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React21.createElement(Text18, null, /* @__PURE__ */ React21.createElement(Text18, { dimColor: true }, "cache "), /* @__PURE__ */ React21.createElement(Text18, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React21.createElement(Text18, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React21.createElement(Text18, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React21.createElement(Text18, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React21.createElement(Text18, null, /* @__PURE__ */ React21.createElement(Text18, { dimColor: true }, "cost "), /* @__PURE__ */ React21.createElement(Text18, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React21.createElement(Text18, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React21.createElement(Text18, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React21.createElement(Text18, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React21.createElement(Text18, null, /* @__PURE__ */ React21.createElement(Text18, { dimColor: true }, "model calls "), /* @__PURE__ */ React21.createElement(Text18, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React21.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React21.createElement(Text18, { dimColor: true, italic: true }, prefixLine)) : null);
13842
+ return /* @__PURE__ */ React25.createElement(Box23, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React25.createElement(Box23, { justifyContent: "space-between" }, /* @__PURE__ */ React25.createElement(Text22, null, /* @__PURE__ */ React25.createElement(Text22, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React25.createElement(Text22, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React25.createElement(Text22, { color: "blue" }, a.label), /* @__PURE__ */ React25.createElement(Text22, { dimColor: true }, " vs B="), /* @__PURE__ */ React25.createElement(Text22, { color: "magenta" }, b.label)), /* @__PURE__ */ React25.createElement(Text22, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React25.createElement(Text22, null, /* @__PURE__ */ React25.createElement(Text22, { dimColor: true }, "cache "), /* @__PURE__ */ React25.createElement(Text22, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React25.createElement(Text22, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React25.createElement(Text22, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React25.createElement(Text22, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React25.createElement(Text22, null, /* @__PURE__ */ React25.createElement(Text22, { dimColor: true }, "cost "), /* @__PURE__ */ React25.createElement(Text22, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React25.createElement(Text22, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React25.createElement(Text22, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React25.createElement(Text22, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React25.createElement(Text22, null, /* @__PURE__ */ React25.createElement(Text22, { dimColor: true }, "model calls "), /* @__PURE__ */ React25.createElement(Text22, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text22, { dimColor: true, italic: true }, prefixLine)) : null);
12398
13843
  }
12399
13844
  function Pane({
12400
13845
  label,
12401
13846
  headerColor,
12402
13847
  records
12403
13848
  }) {
12404
- return /* @__PURE__ */ React21.createElement(
12405
- Box19,
13849
+ return /* @__PURE__ */ React25.createElement(
13850
+ Box23,
12406
13851
  {
12407
13852
  flexDirection: "column",
12408
13853
  flexGrow: 1,
@@ -12410,21 +13855,21 @@ function Pane({
12410
13855
  borderStyle: "single",
12411
13856
  borderColor: headerColor
12412
13857
  },
12413
- /* @__PURE__ */ React21.createElement(Text18, { color: headerColor, bold: true }, label),
12414
- records.length === 0 ? /* @__PURE__ */ React21.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React21.createElement(Text18, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React21.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React21.createElement(RecordView, { key, rec, compact: true }))
13858
+ /* @__PURE__ */ React25.createElement(Text22, { color: headerColor, bold: true }, label),
13859
+ records.length === 0 ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text22, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React25.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React25.createElement(RecordView, { key, rec, compact: true }))
12415
13860
  );
12416
13861
  }
12417
13862
  function KindBadge({ kind }) {
12418
13863
  if (kind === "match") {
12419
- return /* @__PURE__ */ React21.createElement(Text18, { color: "green" }, "\u2713 match");
13864
+ return /* @__PURE__ */ React25.createElement(Text22, { color: "green" }, "\u2713 match");
12420
13865
  }
12421
13866
  if (kind === "diverge") {
12422
- return /* @__PURE__ */ React21.createElement(Text18, { color: "yellow" }, "\u2605 diverge");
13867
+ return /* @__PURE__ */ React25.createElement(Text22, { color: "yellow" }, "\u2605 diverge");
12423
13868
  }
12424
13869
  if (kind === "only_in_a") {
12425
- return /* @__PURE__ */ React21.createElement(Text18, { color: "blue" }, "\u2190 only in A");
13870
+ return /* @__PURE__ */ React25.createElement(Text22, { color: "blue" }, "\u2190 only in A");
12426
13871
  }
12427
- return /* @__PURE__ */ React21.createElement(Text18, { color: "magenta" }, "\u2192 only in B");
13872
+ return /* @__PURE__ */ React25.createElement(Text22, { color: "magenta" }, "\u2192 only in B");
12428
13873
  }
12429
13874
  function paneRecords(pair, side) {
12430
13875
  if (!pair) return [];
@@ -12440,8 +13885,8 @@ async function diffCommand(opts) {
12440
13885
  const aParsed = readTranscript(opts.a);
12441
13886
  const bParsed = readTranscript(opts.b);
12442
13887
  const report = diffTranscripts(
12443
- { label: opts.labelA ?? basename2(opts.a), parsed: aParsed },
12444
- { label: opts.labelB ?? basename2(opts.b), parsed: bParsed }
13888
+ { label: opts.labelA ?? basename3(opts.a), parsed: aParsed },
13889
+ { label: opts.labelB ?? basename3(opts.b), parsed: bParsed }
12445
13890
  );
12446
13891
  const wantMarkdown = !!opts.mdPath;
12447
13892
  const wantPrint = opts.print || !process.stdout.isTTY;
@@ -12449,13 +13894,13 @@ async function diffCommand(opts) {
12449
13894
  if (wantMarkdown) {
12450
13895
  console.log(renderSummaryTable(report));
12451
13896
  const md = renderMarkdown(report);
12452
- writeFileSync6(opts.mdPath, md, "utf8");
13897
+ writeFileSync7(opts.mdPath, md, "utf8");
12453
13898
  console.log(`
12454
13899
  markdown report written to ${opts.mdPath}`);
12455
13900
  return;
12456
13901
  }
12457
13902
  if (wantTui) {
12458
- const { waitUntilExit } = render2(React22.createElement(DiffApp, { report }), {
13903
+ const { waitUntilExit } = render2(React26.createElement(DiffApp, { report }), {
12459
13904
  exitOnCtrlC: true,
12460
13905
  patchConsole: false
12461
13906
  });
@@ -12596,11 +14041,11 @@ function pad2(s, width) {
12596
14041
 
12597
14042
  // src/cli/commands/replay.ts
12598
14043
  import { render as render3 } from "ink";
12599
- import React24 from "react";
14044
+ import React28 from "react";
12600
14045
 
12601
14046
  // src/cli/ui/ReplayApp.tsx
12602
- import { Box as Box20, Static as Static3, Text as Text19, useApp as useApp4, useInput as useInput7 } from "ink";
12603
- import React23, { useMemo as useMemo4, useState as useState14 } from "react";
14047
+ import { Box as Box24, Static as Static3, Text as Text23, useApp as useApp4, useInput as useInput7 } from "ink";
14048
+ import React27, { useMemo as useMemo4, useState as useState14 } from "react";
12604
14049
  function ReplayApp({ meta, pages }) {
12605
14050
  const { exit: exit2 } = useApp4();
12606
14051
  const maxIdx = Math.max(0, pages.length - 1);
@@ -12640,14 +14085,14 @@ function ReplayApp({ meta, pages }) {
12640
14085
  const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
12641
14086
  const currentPage = pages[idx];
12642
14087
  const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
12643
- return /* @__PURE__ */ React23.createElement(Box20, { flexDirection: "column" }, /* @__PURE__ */ React23.createElement(
14088
+ return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column" }, /* @__PURE__ */ React27.createElement(
12644
14089
  StatsPanel,
12645
14090
  {
12646
14091
  summary,
12647
14092
  model: cumStats.models[0] ?? meta?.model ?? "?",
12648
14093
  prefixHash
12649
14094
  }
12650
- ), /* @__PURE__ */ React23.createElement(Box20, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React23.createElement(Box20, { justifyContent: "space-between" }, /* @__PURE__ */ React23.createElement(Text19, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React23.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React23.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React23.createElement(Text19, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React23.createElement(Box20, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, /* @__PURE__ */ React23.createElement(Text19, { bold: true }, "j"), "/", /* @__PURE__ */ React23.createElement(Text19, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React23.createElement(Text19, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React23.createElement(Text19, { bold: true }, "k"), "/", /* @__PURE__ */ React23.createElement(Text19, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React23.createElement(Text19, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React23.createElement(Text19, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React23.createElement(Text19, { bold: true }, "q"), " quit")));
14095
+ ), /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React27.createElement(Box24, { justifyContent: "space-between" }, /* @__PURE__ */ React27.createElement(Text23, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React27.createElement(Text23, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React27.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React27.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React27.createElement(Text23, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React27.createElement(Text23, { dimColor: true }, /* @__PURE__ */ React27.createElement(Text23, { bold: true }, "j"), "/", /* @__PURE__ */ React27.createElement(Text23, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React27.createElement(Text23, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React27.createElement(Text23, { bold: true }, "k"), "/", /* @__PURE__ */ React27.createElement(Text23, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React27.createElement(Text23, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React27.createElement(Text23, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React27.createElement(Text23, { bold: true }, "q"), " quit")));
12651
14096
  }
12652
14097
 
12653
14098
  // src/cli/commands/replay.ts
@@ -12659,7 +14104,7 @@ async function replayCommand(opts) {
12659
14104
  }
12660
14105
  const { parsed } = replayFromFile(opts.path);
12661
14106
  const pages = groupRecordsByTurn(parsed.records);
12662
- const { waitUntilExit } = render3(React24.createElement(ReplayApp, { meta: parsed.meta, pages }), {
14107
+ const { waitUntilExit } = render3(React28.createElement(ReplayApp, { meta: parsed.meta, pages }), {
12663
14108
  exitOnCtrlC: true,
12664
14109
  patchConsole: false
12665
14110
  });
@@ -12944,32 +14389,32 @@ function renderMessage(msg, turnIdx, verbose) {
12944
14389
  if (verbose && msg.tool_calls?.length) {
12945
14390
  for (const tc of msg.tool_calls) {
12946
14391
  console.log(
12947
- ` \u2192 call ${tc.function?.name} ${truncate4(tc.function?.arguments ?? "", 80)}`
14392
+ ` \u2192 call ${tc.function?.name} ${truncate3(tc.function?.arguments ?? "", 80)}`
12948
14393
  );
12949
14394
  }
12950
14395
  }
12951
14396
  } else if (msg.role === "tool") {
12952
- console.log(`${turn} TOOL ${msg.name ?? "?"}: ${truncate4(flat, 160)}`);
14397
+ console.log(`${turn} TOOL ${msg.name ?? "?"}: ${truncate3(flat, 160)}`);
12953
14398
  } else if (msg.role === "system") {
12954
- if (verbose) console.log(`${turn} SYSTEM: ${truncate4(flat, 160)}`);
14399
+ if (verbose) console.log(`${turn} SYSTEM: ${truncate3(flat, 160)}`);
12955
14400
  }
12956
14401
  }
12957
14402
  function oneLine3(s, max = 200) {
12958
14403
  const collapsed = s.replace(/\s+/g, " ").trim();
12959
14404
  return collapsed.length > max ? `${collapsed.slice(0, max)}\u2026` : collapsed;
12960
14405
  }
12961
- function truncate4(s, max) {
14406
+ function truncate3(s, max) {
12962
14407
  return s.length <= max ? s : `${s.slice(0, max)}\u2026`;
12963
14408
  }
12964
14409
 
12965
14410
  // src/cli/commands/setup.tsx
12966
14411
  import { render as render4 } from "ink";
12967
- import React26 from "react";
14412
+ import React30 from "react";
12968
14413
 
12969
14414
  // src/cli/ui/Wizard.tsx
12970
- import { Box as Box21, Text as Text20, useApp as useApp5, useInput as useInput8 } from "ink";
14415
+ import { Box as Box25, Text as Text24, useApp as useApp5, useInput as useInput8 } from "ink";
12971
14416
  import TextInput2 from "ink-text-input";
12972
- import React25, { useState as useState15 } from "react";
14417
+ import React29, { useState as useState15 } from "react";
12973
14418
 
12974
14419
  // src/cli/ui/presets.ts
12975
14420
  var PRESETS = {
@@ -13017,7 +14462,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
13017
14462
  if (key.escape && step !== "saved" && onCancel) onCancel();
13018
14463
  });
13019
14464
  if (step === "apiKey") {
13020
- return /* @__PURE__ */ React25.createElement(
14465
+ return /* @__PURE__ */ React29.createElement(
13021
14466
  ApiKeyStep,
13022
14467
  {
13023
14468
  onSubmit: (key) => {
@@ -13031,7 +14476,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
13031
14476
  );
13032
14477
  }
13033
14478
  if (step === "preset") {
13034
- return /* @__PURE__ */ React25.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React25.createElement(
14479
+ return /* @__PURE__ */ React29.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React29.createElement(
13035
14480
  SingleSelect,
13036
14481
  {
13037
14482
  items: presetItems(),
@@ -13041,10 +14486,10 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
13041
14486
  setStep("mcp");
13042
14487
  }
13043
14488
  }
13044
- ), /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
14489
+ ), /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
13045
14490
  }
13046
14491
  if (step === "mcp") {
13047
- return /* @__PURE__ */ React25.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React25.createElement(
14492
+ return /* @__PURE__ */ React29.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React29.createElement(
13048
14493
  MultiSelect,
13049
14494
  {
13050
14495
  items: mcpItems(),
@@ -13069,7 +14514,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
13069
14514
  }
13070
14515
  const currentName = pending[0];
13071
14516
  const entry = CATALOG_BY_NAME.get(currentName);
13072
- return /* @__PURE__ */ React25.createElement(
14517
+ return /* @__PURE__ */ React29.createElement(
13073
14518
  McpArgsStep,
13074
14519
  {
13075
14520
  entry,
@@ -13087,7 +14532,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
13087
14532
  }
13088
14533
  if (step === "review") {
13089
14534
  const specs = data.selectedCatalog.map((name) => buildSpec(name, data.catalogArgs));
13090
- return /* @__PURE__ */ React25.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React25.createElement(Box21, { flexDirection: "column" }, /* @__PURE__ */ React25.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React25.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React25.createElement(
14535
+ return /* @__PURE__ */ React29.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React29.createElement(Box25, { flexDirection: "column" }, /* @__PURE__ */ React29.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React29.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React29.createElement(
13091
14536
  SummaryLine,
13092
14537
  {
13093
14538
  label: "MCP",
@@ -13095,8 +14540,8 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
13095
14540
  }
13096
14541
  ), specs.map((spec, i) => (
13097
14542
  // biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
13098
- /* @__PURE__ */ React25.createElement(Box21, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "\xB7 ", spec))
13099
- )), /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { color: "red" }, error)) : null, /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "enter save \xB7 esc cancel"))), /* @__PURE__ */ React25.createElement(
14543
+ /* @__PURE__ */ React29.createElement(Box25, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, "\xB7 ", spec))
14544
+ )), /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: "red" }, error)) : null, /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, "enter save \xB7 esc cancel"))), /* @__PURE__ */ React29.createElement(
13100
14545
  ReviewConfirm,
13101
14546
  {
13102
14547
  onConfirm: () => {
@@ -13122,7 +14567,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
13122
14567
  }
13123
14568
  ));
13124
14569
  }
13125
- return /* @__PURE__ */ React25.createElement(Box21, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React25.createElement(Text20, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "Press enter to exit.")), /* @__PURE__ */ React25.createElement(ExitOnEnter, { onExit: exit2 }));
14570
+ return /* @__PURE__ */ React29.createElement(Box25, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React29.createElement(Text24, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, "Press enter to exit.")), /* @__PURE__ */ React29.createElement(ExitOnEnter, { onExit: exit2 }));
13126
14571
  }
13127
14572
  function ApiKeyStep({
13128
14573
  onSubmit,
@@ -13130,7 +14575,7 @@ function ApiKeyStep({
13130
14575
  onError
13131
14576
  }) {
13132
14577
  const [value, setValue] = useState15("");
13133
- return /* @__PURE__ */ React25.createElement(Box21, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React25.createElement(Text20, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React25.createElement(
14578
+ return /* @__PURE__ */ React29.createElement(Box25, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React29.createElement(Text24, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React29.createElement(
13134
14579
  TextInput2,
13135
14580
  {
13136
14581
  value,
@@ -13147,7 +14592,7 @@ function ApiKeyStep({
13147
14592
  mask: "\u2022",
13148
14593
  placeholder: "sk-..."
13149
14594
  }
13150
- )), error ? /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { color: "red" }, error)) : value ? /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "preview: ", redactKey(value))) : null);
14595
+ )), error ? /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: "red" }, error)) : value ? /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, "preview: ", redactKey(value))) : null);
13151
14596
  }
13152
14597
  function McpArgsStep({
13153
14598
  entry,
@@ -13156,7 +14601,7 @@ function McpArgsStep({
13156
14601
  onError
13157
14602
  }) {
13158
14603
  const [value, setValue] = useState15("");
13159
- return /* @__PURE__ */ React25.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React25.createElement(Box21, { flexDirection: "column" }, /* @__PURE__ */ React25.createElement(Text20, null, entry.summary), entry.note ? /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, null, "Required parameter: "), /* @__PURE__ */ React25.createElement(Text20, { bold: true }, entry.userArgs)), /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React25.createElement(
14604
+ return /* @__PURE__ */ React29.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React29.createElement(Box25, { flexDirection: "column" }, /* @__PURE__ */ React29.createElement(Text24, null, entry.summary), entry.note ? /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, null, "Required parameter: "), /* @__PURE__ */ React29.createElement(Text24, { bold: true }, entry.userArgs)), /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React29.createElement(
13160
14605
  TextInput2,
13161
14606
  {
13162
14607
  value,
@@ -13172,7 +14617,7 @@ function McpArgsStep({
13172
14617
  },
13173
14618
  placeholder: placeholderFor(entry)
13174
14619
  }
13175
- )), error ? /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { color: "red" }, error)) : null));
14620
+ )), error ? /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: "red" }, error)) : null));
13176
14621
  }
13177
14622
  function ReviewConfirm({ onConfirm }) {
13178
14623
  useInput8((_i, key) => {
@@ -13192,10 +14637,10 @@ function StepFrame({
13192
14637
  total,
13193
14638
  children
13194
14639
  }) {
13195
- return /* @__PURE__ */ React25.createElement(Box21, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React25.createElement(Box21, null, /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React25.createElement(Text20, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React25.createElement(Box21, { marginTop: 1, flexDirection: "column" }, children));
14640
+ return /* @__PURE__ */ React29.createElement(Box25, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React29.createElement(Box25, null, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React29.createElement(Text24, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React29.createElement(Box25, { marginTop: 1, flexDirection: "column" }, children));
13196
14641
  }
13197
14642
  function SummaryLine({ label, value }) {
13198
- return /* @__PURE__ */ React25.createElement(Box21, null, /* @__PURE__ */ React25.createElement(Text20, null, label.padEnd(12)), /* @__PURE__ */ React25.createElement(Text20, { bold: true }, value));
14643
+ return /* @__PURE__ */ React29.createElement(Box25, null, /* @__PURE__ */ React29.createElement(Text24, null, label.padEnd(12)), /* @__PURE__ */ React29.createElement(Text24, { bold: true }, value));
13199
14644
  }
13200
14645
  function presetItems() {
13201
14646
  return ["fast", "smart", "max"].map((name) => ({
@@ -13251,7 +14696,7 @@ async function setupCommand(_opts = {}) {
13251
14696
  const existingKey = loadApiKey();
13252
14697
  const existing = readConfig();
13253
14698
  const { waitUntilExit, unmount } = render4(
13254
- /* @__PURE__ */ React26.createElement(
14699
+ /* @__PURE__ */ React30.createElement(
13255
14700
  Wizard,
13256
14701
  {
13257
14702
  existingApiKey: existingKey,