reasonix 0.5.13 → 0.5.20

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
@@ -1642,6 +1642,11 @@ function deleteSession(name) {
1642
1642
  const path = sessionPath(name);
1643
1643
  try {
1644
1644
  unlinkSync(path);
1645
+ const sidecar = path.replace(/\.jsonl$/, ".pending.json");
1646
+ try {
1647
+ unlinkSync(sidecar);
1648
+ } catch {
1649
+ }
1645
1650
  return true;
1646
1651
  } catch {
1647
1652
  return false;
@@ -1669,13 +1674,18 @@ function countLines(path) {
1669
1674
 
1670
1675
  // src/telemetry.ts
1671
1676
  var DEEPSEEK_PRICING = {
1672
- "deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 },
1673
- "deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 }
1677
+ "deepseek-v4-flash": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
1678
+ "deepseek-v4-pro": { inputCacheHit: 0.139, inputCacheMiss: 1.667, output: 3.333 },
1679
+ // Compat aliases — priced as v4-flash per the deprecation notice.
1680
+ "deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
1681
+ "deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 }
1674
1682
  };
1675
1683
  var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
1676
1684
  var DEEPSEEK_CONTEXT_TOKENS = {
1677
- "deepseek-chat": 131072,
1678
- "deepseek-reasoner": 131072
1685
+ "deepseek-v4-flash": 1e6,
1686
+ "deepseek-v4-pro": 1e6,
1687
+ "deepseek-chat": 1e6,
1688
+ "deepseek-reasoner": 1e6
1679
1689
  };
1680
1690
  var DEFAULT_CONTEXT_TOKENS = 131072;
1681
1691
  function costUsd(model, usage) {
@@ -2003,7 +2013,7 @@ var CacheFirstLoop = class {
2003
2013
  content: `aborted at iter ${iter}/${this.maxToolIters} \u2014 stopped without producing a summary (press \u2191 + Enter or /retry to resume)`
2004
2014
  };
2005
2015
  const stoppedMsg = "[aborted by user (Esc) \u2014 no summary produced. Ask again or /retry when ready; prior tool output is still in the log.]";
2006
- this.appendAndPersist({ role: "assistant", content: stoppedMsg });
2016
+ this.appendAndPersist(this.syntheticAssistantMessage(stoppedMsg));
2007
2017
  yield {
2008
2018
  turn: this._turn,
2009
2019
  role: "assistant_final",
@@ -2139,6 +2149,7 @@ var CacheFirstLoop = class {
2139
2149
  };
2140
2150
  } else if (this.stream) {
2141
2151
  const callBuf = /* @__PURE__ */ new Map();
2152
+ const readyIndices = /* @__PURE__ */ new Set();
2142
2153
  for await (const chunk of this.client.stream({
2143
2154
  model: this.model,
2144
2155
  messages,
@@ -2174,13 +2185,18 @@ var CacheFirstLoop = class {
2174
2185
  if (d.argumentsDelta)
2175
2186
  cur.function.arguments = (cur.function.arguments ?? "") + d.argumentsDelta;
2176
2187
  callBuf.set(d.index, cur);
2188
+ if (!readyIndices.has(d.index) && cur.function.name && looksLikeCompleteJson(cur.function.arguments ?? "")) {
2189
+ readyIndices.add(d.index);
2190
+ }
2177
2191
  if (cur.function.name) {
2178
2192
  yield {
2179
2193
  turn: this._turn,
2180
2194
  role: "tool_call_delta",
2181
2195
  content: "",
2182
2196
  toolName: cur.function.name,
2183
- toolCallArgsChars: (cur.function.arguments ?? "").length
2197
+ toolCallArgsChars: (cur.function.arguments ?? "").length,
2198
+ toolCallIndex: d.index,
2199
+ toolCallReadyCount: readyIndices.size
2184
2200
  };
2185
2201
  }
2186
2202
  }
@@ -2231,7 +2247,9 @@ var CacheFirstLoop = class {
2231
2247
  reasoningContent || null,
2232
2248
  assistantContent || null
2233
2249
  );
2234
- this.appendAndPersist(this.assistantMessage(assistantContent, repairedCalls));
2250
+ this.appendAndPersist(
2251
+ this.assistantMessage(assistantContent, repairedCalls, reasoningContent)
2252
+ );
2235
2253
  yield {
2236
2254
  turn: this._turn,
2237
2255
  role: "assistant_final",
@@ -2393,7 +2411,7 @@ ${reason}`;
2393
2411
 
2394
2412
  ${summary}`;
2395
2413
  const summaryStats = this.stats.record(this._turn, this.model, resp.usage ?? new Usage());
2396
- this.appendAndPersist({ role: "assistant", content: summary });
2414
+ this.appendAndPersist(this.assistantMessage(summary, [], resp.reasoningContent ?? void 0));
2397
2415
  yield {
2398
2416
  turn: this._turn,
2399
2417
  role: "assistant_final",
@@ -2422,12 +2440,35 @@ ${summary}`;
2422
2440
  }
2423
2441
  return final;
2424
2442
  }
2425
- assistantMessage(content, toolCalls) {
2443
+ assistantMessage(content, toolCalls, reasoningContent) {
2426
2444
  const msg = { role: "assistant", content };
2427
2445
  if (toolCalls.length > 0) msg.tool_calls = toolCalls;
2446
+ if (reasoningContent && reasoningContent.length > 0) {
2447
+ msg.reasoning_content = reasoningContent;
2448
+ }
2449
+ return msg;
2450
+ }
2451
+ /**
2452
+ * Build a synthetic assistant message we insert into the log without
2453
+ * a real API round trip (abort notices, future system injections).
2454
+ * Reasoner models reject follow-up requests whose assistant history
2455
+ * is missing `reasoning_content`, so we stamp an empty-string
2456
+ * placeholder on reasoner sessions to satisfy the validator. V3
2457
+ * doesn't care — field stays absent there.
2458
+ */
2459
+ syntheticAssistantMessage(content) {
2460
+ const msg = { role: "assistant", content };
2461
+ if (isThinkingModeModel(this.model)) {
2462
+ msg.reasoning_content = "";
2463
+ }
2428
2464
  return msg;
2429
2465
  }
2430
2466
  };
2467
+ function isThinkingModeModel(model) {
2468
+ if (model.includes("reasoner")) return true;
2469
+ if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return true;
2470
+ return false;
2471
+ }
2431
2472
  function stripHallucinatedToolMarkup(s) {
2432
2473
  let out = s;
2433
2474
  out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
@@ -2443,6 +2484,15 @@ function safeParseToolArgs(raw) {
2443
2484
  return raw;
2444
2485
  }
2445
2486
  }
2487
+ function looksLikeCompleteJson(s) {
2488
+ if (!s || !s.trim()) return false;
2489
+ try {
2490
+ JSON.parse(s);
2491
+ return true;
2492
+ } catch {
2493
+ return false;
2494
+ }
2495
+ }
2446
2496
  function* hookWarnings(outcomes, turn) {
2447
2497
  for (const o of outcomes) {
2448
2498
  if (o.decision === "pass") continue;
@@ -3457,11 +3507,14 @@ async function spawnSubagent(opts) {
3457
3507
  const maxToolIters = opts.maxToolIters ?? DEFAULT_MAX_ITERS;
3458
3508
  const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
3459
3509
  const sink = opts.sink;
3510
+ const skillName = opts.skillName;
3460
3511
  const startedAt = Date.now();
3461
3512
  const taskPreview = opts.task.length > 30 ? `${opts.task.slice(0, 30)}\u2026` : opts.task;
3462
3513
  sink?.current?.({
3463
3514
  kind: "start",
3464
3515
  task: taskPreview,
3516
+ skillName,
3517
+ model,
3465
3518
  iter: 0,
3466
3519
  elapsedMs: 0
3467
3520
  });
@@ -3491,6 +3544,8 @@ async function spawnSubagent(opts) {
3491
3544
  sink?.current?.({
3492
3545
  kind: "progress",
3493
3546
  task: taskPreview,
3547
+ skillName,
3548
+ model,
3494
3549
  iter: toolIter,
3495
3550
  elapsedMs: Date.now() - startedAt
3496
3551
  });
@@ -3513,17 +3568,22 @@ async function spawnSubagent(opts) {
3513
3568
  const elapsedMs = Date.now() - startedAt;
3514
3569
  const turns = childLoop.stats.turns.length;
3515
3570
  const costUsd2 = childLoop.stats.totalCost;
3571
+ const usage = aggregateChildUsage(childLoop);
3516
3572
  const truncated = final.length > maxResultChars ? `${final.slice(0, maxResultChars)}
3517
3573
 
3518
3574
  [\u2026truncated ${final.length - maxResultChars} chars; ask the subagent for a tighter summary if you need more.]` : final;
3519
3575
  sink?.current?.({
3520
3576
  kind: "end",
3521
3577
  task: taskPreview,
3578
+ skillName,
3579
+ model,
3522
3580
  iter: toolIter,
3523
3581
  elapsedMs,
3524
3582
  summary: errorMessage ? void 0 : truncated.slice(0, 120),
3525
3583
  error: errorMessage,
3526
- turns
3584
+ turns,
3585
+ costUsd: costUsd2,
3586
+ usage
3527
3587
  });
3528
3588
  return {
3529
3589
  success: !errorMessage,
@@ -3532,9 +3592,23 @@ async function spawnSubagent(opts) {
3532
3592
  turns,
3533
3593
  toolIters: toolIter,
3534
3594
  elapsedMs,
3535
- costUsd: costUsd2
3595
+ costUsd: costUsd2,
3596
+ model,
3597
+ skillName,
3598
+ usage
3536
3599
  };
3537
3600
  }
3601
+ function aggregateChildUsage(loop) {
3602
+ const agg = new Usage();
3603
+ for (const t of loop.stats.turns) {
3604
+ agg.promptTokens += t.usage.promptTokens;
3605
+ agg.completionTokens += t.usage.completionTokens;
3606
+ agg.totalTokens += t.usage.totalTokens;
3607
+ agg.promptCacheHitTokens += t.usage.promptCacheHitTokens;
3608
+ agg.promptCacheMissTokens += t.usage.promptCacheMissTokens;
3609
+ }
3610
+ return agg;
3611
+ }
3538
3612
  function formatSubagentResult(r) {
3539
3613
  if (!r.success) {
3540
3614
  return JSON.stringify({
@@ -5529,6 +5603,8 @@ function appendUsage(input) {
5529
5603
  costUsd: costUsd(input.model, input.usage),
5530
5604
  claudeEquivUsd: claudeEquivalentCost(input.usage)
5531
5605
  };
5606
+ if (input.kind === "subagent") record.kind = "subagent";
5607
+ if (input.subagent) record.subagent = input.subagent;
5532
5608
  const path = input.path ?? defaultUsageLogPath();
5533
5609
  try {
5534
5610
  mkdirSync5(dirname7(path), { recursive: true });
@@ -5602,6 +5678,10 @@ function aggregateUsage(records, opts = {}) {
5602
5678
  const sessionCounts = /* @__PURE__ */ new Map();
5603
5679
  let firstSeen = null;
5604
5680
  let lastSeen = null;
5681
+ const skillCounts = /* @__PURE__ */ new Map();
5682
+ let subagentTotal = 0;
5683
+ let subagentCost = 0;
5684
+ let subagentDuration = 0;
5605
5685
  for (const r of records) {
5606
5686
  addToBucket(all, r);
5607
5687
  if (r.ts >= today.since) addToBucket(today, r);
@@ -5612,15 +5692,34 @@ function aggregateUsage(records, opts = {}) {
5612
5692
  sessionCounts.set(sessKey, (sessionCounts.get(sessKey) ?? 0) + 1);
5613
5693
  if (firstSeen === null || r.ts < firstSeen) firstSeen = r.ts;
5614
5694
  if (lastSeen === null || r.ts > lastSeen) lastSeen = r.ts;
5695
+ if (r.kind === "subagent") {
5696
+ subagentTotal += 1;
5697
+ subagentCost += r.costUsd;
5698
+ const dur = r.subagent?.durationMs ?? 0;
5699
+ subagentDuration += dur;
5700
+ const key = r.subagent?.skillName?.trim() || "(adhoc)";
5701
+ const prev = skillCounts.get(key) ?? { count: 0, costUsd: 0, durationMs: 0 };
5702
+ prev.count += 1;
5703
+ prev.costUsd += r.costUsd;
5704
+ prev.durationMs += dur;
5705
+ skillCounts.set(key, prev);
5706
+ }
5615
5707
  }
5616
5708
  const byModel = Array.from(modelCounts.entries()).map(([model, turns]) => ({ model, turns })).sort((a, b) => b.turns - a.turns);
5617
5709
  const bySession = Array.from(sessionCounts.entries()).map(([session, turns]) => ({ session, turns })).sort((a, b) => b.turns - a.turns);
5710
+ const subagents = subagentTotal > 0 ? {
5711
+ total: subagentTotal,
5712
+ costUsd: subagentCost,
5713
+ totalDurationMs: subagentDuration,
5714
+ bySkill: Array.from(skillCounts.entries()).map(([skillName, v]) => ({ skillName, ...v })).sort((a, b) => b.count - a.count)
5715
+ } : void 0;
5618
5716
  return {
5619
5717
  buckets: [today, week, month, all],
5620
5718
  byModel,
5621
5719
  bySession,
5622
5720
  firstSeen,
5623
- lastSeen
5721
+ lastSeen,
5722
+ subagents
5624
5723
  };
5625
5724
  }
5626
5725
  function formatLogSize(path = defaultUsageLogPath()) {
@@ -5637,7 +5736,7 @@ function formatLogSize(path = defaultUsageLogPath()) {
5637
5736
  }
5638
5737
 
5639
5738
  // src/cli/commands/chat.tsx
5640
- import { existsSync as existsSync10, statSync as statSync6 } from "fs";
5739
+ import { existsSync as existsSync11, statSync as statSync6 } from "fs";
5641
5740
  import { render } from "ink";
5642
5741
  import React17, { useState as useState7 } from "react";
5643
5742
 
@@ -5714,6 +5813,58 @@ function capLines(lines, maxLines, indent) {
5714
5813
  return head;
5715
5814
  }
5716
5815
 
5816
+ // src/code/pending-edits.ts
5817
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
5818
+ import { dirname as dirname8, join as join9 } from "path";
5819
+ function pendingEditsPath(sessionName) {
5820
+ return join9(sessionsDir(), `${sanitizeName(sessionName)}.pending.json`);
5821
+ }
5822
+ function savePendingEdits(sessionName, blocks) {
5823
+ if (!sessionName) return;
5824
+ const path = pendingEditsPath(sessionName);
5825
+ try {
5826
+ if (blocks.length === 0) {
5827
+ if (existsSync9(path)) unlinkSync3(path);
5828
+ return;
5829
+ }
5830
+ mkdirSync6(dirname8(path), { recursive: true });
5831
+ writeFileSync5(path, JSON.stringify(blocks, null, 2), "utf8");
5832
+ } catch {
5833
+ }
5834
+ }
5835
+ function loadPendingEdits(sessionName) {
5836
+ if (!sessionName) return null;
5837
+ const path = pendingEditsPath(sessionName);
5838
+ if (!existsSync9(path)) return null;
5839
+ let raw;
5840
+ try {
5841
+ raw = readFileSync11(path, "utf8");
5842
+ } catch {
5843
+ return null;
5844
+ }
5845
+ try {
5846
+ const parsed = JSON.parse(raw);
5847
+ if (!Array.isArray(parsed)) return null;
5848
+ const out = [];
5849
+ for (const item of parsed) {
5850
+ if (item && typeof item === "object" && typeof item.path === "string" && typeof item.search === "string" && typeof item.replace === "string" && typeof item.offset === "number") {
5851
+ out.push(item);
5852
+ }
5853
+ }
5854
+ return out;
5855
+ } catch {
5856
+ return null;
5857
+ }
5858
+ }
5859
+ function clearPendingEdits(sessionName) {
5860
+ if (!sessionName) return;
5861
+ const path = pendingEditsPath(sessionName);
5862
+ try {
5863
+ if (existsSync9(path)) unlinkSync3(path);
5864
+ } catch {
5865
+ }
5866
+ }
5867
+
5717
5868
  // src/tools/skills.ts
5718
5869
  function registerSkillTools(registry, opts = {}) {
5719
5870
  const store = new SkillStore({
@@ -5835,8 +5986,8 @@ function PlanStateBlock({ planState }) {
5835
5986
  }
5836
5987
 
5837
5988
  // src/cli/ui/markdown.tsx
5838
- import { readFileSync as readFileSync11, statSync as statSync5 } from "fs";
5839
- import { isAbsolute as isAbsolute4, join as join9 } from "path";
5989
+ import { readFileSync as readFileSync12, statSync as statSync5 } from "fs";
5990
+ import { isAbsolute as isAbsolute4, join as join10 } from "path";
5840
5991
  import { Box as Box3, Text as Text3 } from "ink";
5841
5992
  import React3 from "react";
5842
5993
  var SUPERSCRIPT = {
@@ -5914,7 +6065,8 @@ function parseCitationUrl(url) {
5914
6065
  function validateCitation(url, projectRoot) {
5915
6066
  const parts = parseCitationUrl(url);
5916
6067
  if (!parts || !parts.path) return { ok: false, reason: "empty path" };
5917
- const fullPath = isAbsolute4(parts.path) ? parts.path : join9(projectRoot, parts.path);
6068
+ const normalized = parts.path.replace(/^[/\\]+/, "");
6069
+ const fullPath = isAbsolute4(normalized) ? normalized : join10(projectRoot, normalized);
5918
6070
  let stat;
5919
6071
  try {
5920
6072
  stat = statSync5(fullPath);
@@ -5925,7 +6077,7 @@ function validateCitation(url, projectRoot) {
5925
6077
  if (parts.startLine === void 0) return { ok: true };
5926
6078
  let lineCount;
5927
6079
  try {
5928
- lineCount = readFileSync11(fullPath, "utf8").split("\n").length;
6080
+ lineCount = readFileSync12(fullPath, "utf8").split("\n").length;
5929
6081
  } catch {
5930
6082
  return { ok: false, reason: "unreadable" };
5931
6083
  }
@@ -6417,13 +6569,15 @@ function StreamingAssistant({ event }) {
6417
6569
  label = `R1 reasoning \xB7 ${event.reasoning?.length ?? 0} chars of thought`;
6418
6570
  labelColor = "cyan";
6419
6571
  } else if (toolCallOnly) {
6420
- label = `assembling tool call <${toolCallBuild.name}> \xB7 ${toolCallBuild.chars} chars of arguments`;
6572
+ label = `assembling tool call${formatToolCallIndex(toolCallBuild)} <${toolCallBuild.name}> \xB7 ${toolCallBuild.chars} chars of arguments${formatReadyTail(toolCallBuild)}`;
6421
6573
  labelColor = "magenta";
6422
6574
  } else {
6423
6575
  const parts = [`writing response \xB7 ${event.text.length} chars`];
6424
6576
  if (event.reasoning) parts.push(`after ${event.reasoning.length} chars of reasoning`);
6425
6577
  if (toolCallBuild) {
6426
- parts.push(`building tool call <${toolCallBuild.name}> \xB7 ${toolCallBuild.chars} chars`);
6578
+ parts.push(
6579
+ `building tool call${formatToolCallIndex(toolCallBuild)} <${toolCallBuild.name}> \xB7 ${toolCallBuild.chars} chars${formatReadyTail(toolCallBuild)}`
6580
+ );
6427
6581
  }
6428
6582
  label = parts.join(" \xB7 ");
6429
6583
  labelColor = "green";
@@ -6439,6 +6593,16 @@ function Pulse() {
6439
6593
  const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
6440
6594
  return /* @__PURE__ */ React5.createElement(Text4, { color: "cyan" }, frames[Math.floor(tick / 4) % frames.length]);
6441
6595
  }
6596
+ function formatToolCallIndex(tb) {
6597
+ if (!tb || tb.index === void 0) return "";
6598
+ if (tb.index === 0 && (tb.readyCount ?? 0) === 0) return "";
6599
+ return ` (call ${tb.index + 1})`;
6600
+ }
6601
+ function formatReadyTail(tb) {
6602
+ const n = tb?.readyCount ?? 0;
6603
+ if (n <= 0) return "";
6604
+ return ` \xB7 ${n} ready`;
6605
+ }
6442
6606
  function lastLine(s, maxChars) {
6443
6607
  const flat = s.replace(/\s+/g, " ").trim();
6444
6608
  if (!flat) return "";
@@ -6576,12 +6740,34 @@ function findNextEnabled(items, from, step) {
6576
6740
 
6577
6741
  // src/cli/ui/PlanConfirm.tsx
6578
6742
  var DEFAULT_MAX_RENDERED = 2400;
6579
- function PlanConfirm({ plan, onChoose, maxRenderedChars, projectRoot }) {
6743
+ var PICKER_CHROME_ROWS = 18;
6744
+ var MARKDOWN_EXPANSION = 2;
6745
+ var MIN_BODY_ROWS = 4;
6746
+ function clampBodyByLines(text, maxLines) {
6747
+ const lines = text.split("\n");
6748
+ if (lines.length <= maxLines) return text;
6749
+ const kept = lines.slice(0, maxLines).join("\n");
6750
+ const dropped = lines.length - maxLines;
6751
+ return `${kept}
6752
+
6753
+ \u2026 (${dropped} more lines truncated \u2014 resize the terminal to see more, or /tool for the full proposal)`;
6754
+ }
6755
+ function PlanConfirmInner({
6756
+ plan,
6757
+ onChoose,
6758
+ maxRenderedChars,
6759
+ projectRoot,
6760
+ terminalRows
6761
+ }) {
6580
6762
  const cap = maxRenderedChars ?? DEFAULT_MAX_RENDERED;
6581
- const tooLong = plan.length > cap;
6582
- const visible = tooLong ? `${plan.slice(0, cap)}
6763
+ const charTrunc = plan.length > cap;
6764
+ const charCapped = charTrunc ? `${plan.slice(0, cap)}
6583
6765
 
6584
6766
  \u2026 (${plan.length - cap} chars truncated \u2014 use /tool to view the full proposal)` : plan;
6767
+ const rows = terminalRows ?? process.stdout?.rows ?? 24;
6768
+ const renderedBudget = Math.max(MIN_BODY_ROWS * MARKDOWN_EXPANSION, rows - PICKER_CHROME_ROWS);
6769
+ const sourceLineBudget = Math.max(MIN_BODY_ROWS, Math.floor(renderedBudget / MARKDOWN_EXPANSION));
6770
+ const visible = clampBodyByLines(charCapped, sourceLineBudget);
6585
6771
  const hasOpenQuestions = /^#{1,6}\s*(open[-\s]?questions?|risks?|unknowns?|assumptions?|unclear)/im.test(plan) || /^#{1,6}\s*(待确认|开放问题|风险|未知|假设|不确定)/im.test(plan);
6586
6772
  return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React7.createElement(Box6, null, /* @__PURE__ */ React7.createElement(Text6, { bold: true, color: "cyan" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React7.createElement(Box6, null, /* @__PURE__ */ React7.createElement(Text6, { color: "cyan", dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")), /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Markdown, { text: visible, projectRoot })), hasOpenQuestions ? /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React7.createElement(Text6, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(
6587
6773
  SingleSelect,
@@ -6610,6 +6796,7 @@ function PlanConfirm({ plan, onChoose, maxRenderedChars, projectRoot }) {
6610
6796
  }
6611
6797
  )));
6612
6798
  }
6799
+ var PlanConfirm = React7.memo(PlanConfirmInner);
6613
6800
 
6614
6801
  // src/cli/ui/PlanRefineInput.tsx
6615
6802
  import { Box as Box7, Text as Text7, useInput as useInput2 } from "ink";
@@ -7146,6 +7333,178 @@ function formatBangUserMessage(cmd, output) {
7146
7333
  ${output}`;
7147
7334
  }
7148
7335
 
7336
+ // src/cli/ui/mcp-browse.ts
7337
+ function formatResourceList(servers) {
7338
+ const lines = [];
7339
+ let total = 0;
7340
+ for (const s of servers) {
7341
+ if (!s.report.resources.supported) continue;
7342
+ const items = s.report.resources.items;
7343
+ if (items.length === 0) continue;
7344
+ lines.push(`[${s.label}] ${items.length} resource(s):`);
7345
+ for (const r of items.slice(0, 20)) {
7346
+ const name = r.name && r.name !== r.uri ? ` ${r.name}` : "";
7347
+ const mime = r.mimeType ? ` \xB7 ${r.mimeType}` : "";
7348
+ lines.push(` \xB7 ${r.uri}${name}${mime}`);
7349
+ total++;
7350
+ }
7351
+ if (items.length > 20) lines.push(` (+${items.length - 20} more)`);
7352
+ lines.push("");
7353
+ }
7354
+ if (total === 0) {
7355
+ return "No resources on any connected MCP server (or no servers connected). `/mcp` shows the current set.";
7356
+ }
7357
+ lines.push("Read one: `/resource <uri>` \u2014 or use Tab in the picker.");
7358
+ return lines.join("\n");
7359
+ }
7360
+ function formatPromptList(servers) {
7361
+ const lines = [];
7362
+ let total = 0;
7363
+ for (const s of servers) {
7364
+ if (!s.report.prompts.supported) continue;
7365
+ const items = s.report.prompts.items;
7366
+ if (items.length === 0) continue;
7367
+ lines.push(`[${s.label}] ${items.length} prompt(s):`);
7368
+ for (const p of items.slice(0, 20)) {
7369
+ const desc = p.description ? ` \u2014 ${p.description}` : "";
7370
+ const argHint = p.arguments && p.arguments.length > 0 ? ` (args: ${p.arguments.map((a) => a.name + (a.required ? "*" : "?")).join(", ")})` : "";
7371
+ lines.push(` \xB7 ${p.name}${argHint}${desc}`);
7372
+ total++;
7373
+ }
7374
+ if (items.length > 20) lines.push(` (+${items.length - 20} more)`);
7375
+ lines.push("");
7376
+ }
7377
+ if (total === 0) {
7378
+ return "No prompts on any connected MCP server (or no servers connected). `/mcp` shows the current set.";
7379
+ }
7380
+ lines.push(
7381
+ "Fetch one: `/prompt <name>` \u2014 args are not supported yet; prompts with required args will surface an error from the server."
7382
+ );
7383
+ return lines.join("\n");
7384
+ }
7385
+ function findServerForResource(servers, uri) {
7386
+ for (const s of servers) {
7387
+ if (!s.report.resources.supported) continue;
7388
+ if (s.report.resources.items.some((r) => r.uri === uri)) return s;
7389
+ }
7390
+ return null;
7391
+ }
7392
+ function findServerForPrompt(servers, name) {
7393
+ for (const s of servers) {
7394
+ if (!s.report.prompts.supported) continue;
7395
+ if (s.report.prompts.items.some((p) => p.name === name)) return s;
7396
+ }
7397
+ return null;
7398
+ }
7399
+ function formatResourceContents(uri, result) {
7400
+ const lines = [`Resource ${uri} (${result.contents.length} content block(s)):`, ""];
7401
+ for (let i = 0; i < result.contents.length; i++) {
7402
+ const c = result.contents[i];
7403
+ const header2 = `\u2014 block ${i + 1}${c.mimeType ? ` \xB7 ${c.mimeType}` : ""}`;
7404
+ lines.push(header2);
7405
+ lines.push(formatOneResourceContent(c));
7406
+ lines.push("");
7407
+ }
7408
+ return lines.join("\n").trimEnd();
7409
+ }
7410
+ function formatOneResourceContent(c) {
7411
+ if ("text" in c) {
7412
+ const MAX = 8e3;
7413
+ if (c.text.length > MAX) {
7414
+ return `${c.text.slice(0, MAX)}
7415
+
7416
+ [\u2026truncated ${c.text.length - MAX} chars; full contents available via McpClient.readResource in library mode.]`;
7417
+ }
7418
+ return c.text;
7419
+ }
7420
+ const bytes = typeof c.blob === "string" ? approximateBase64ByteSize(c.blob) : 0;
7421
+ return `[binary \xB7 ~${bytes.toLocaleString()} bytes \xB7 base64]`;
7422
+ }
7423
+ function approximateBase64ByteSize(b64) {
7424
+ const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
7425
+ return Math.floor(b64.length * 3 / 4) - padding;
7426
+ }
7427
+ function formatPromptMessages(name, result) {
7428
+ const lines = [
7429
+ `Prompt ${name}${result.description ? ` \u2014 ${result.description}` : ""}`,
7430
+ `(${result.messages.length} message(s))`,
7431
+ ""
7432
+ ];
7433
+ for (let i = 0; i < result.messages.length; i++) {
7434
+ const m = result.messages[i];
7435
+ lines.push(`\u2014 ${i + 1}. ${m.role}`);
7436
+ lines.push(formatOnePromptMessage(m));
7437
+ lines.push("");
7438
+ }
7439
+ return lines.join("\n").trimEnd();
7440
+ }
7441
+ function formatOnePromptMessage(m) {
7442
+ const block = m.content;
7443
+ if (block.type === "text" && typeof block.text === "string") return block.text;
7444
+ if (block.type === "resource" && block.resource) {
7445
+ return `[resource: ${block.resource.uri}]
7446
+ ${formatOneResourceContent(block.resource)}`;
7447
+ }
7448
+ return `[non-text content: ${block.type ?? "unknown"}]`;
7449
+ }
7450
+ async function handleMcpBrowseSlash(kind, arg, servers, setHistorical) {
7451
+ const ts = Date.now();
7452
+ const push = (role, text) => {
7453
+ setHistorical((prev) => [...prev, { id: `mcp-${role}-${ts}-${prev.length}`, role, text }]);
7454
+ };
7455
+ if (!arg) {
7456
+ push("info", kind === "resource" ? formatResourceList(servers) : formatPromptList(servers));
7457
+ return;
7458
+ }
7459
+ if (kind === "resource") {
7460
+ const server2 = findServerForResource(servers, arg);
7461
+ if (!server2) {
7462
+ push(
7463
+ "warning",
7464
+ `no server exposes resource "${arg}". \`/resource\` with no arg lists what's available.`
7465
+ );
7466
+ return;
7467
+ }
7468
+ const client2 = server2.client;
7469
+ if (!client2) {
7470
+ push(
7471
+ "warning",
7472
+ `server [${server2.label}] is not connected (display-only). Resource read requires a live MCP client.`
7473
+ );
7474
+ return;
7475
+ }
7476
+ try {
7477
+ const result = await client2.readResource(arg);
7478
+ push("info", formatResourceContents(arg, result));
7479
+ } catch (err) {
7480
+ push("warning", `readResource failed: ${err.message}`);
7481
+ }
7482
+ return;
7483
+ }
7484
+ const server = findServerForPrompt(servers, arg);
7485
+ if (!server) {
7486
+ push(
7487
+ "warning",
7488
+ `no server exposes prompt "${arg}". \`/prompt\` with no arg lists what's available.`
7489
+ );
7490
+ return;
7491
+ }
7492
+ const client = server.client;
7493
+ if (!client) {
7494
+ push(
7495
+ "warning",
7496
+ `server [${server.label}] is not connected (display-only). Prompt fetch requires a live MCP client.`
7497
+ );
7498
+ return;
7499
+ }
7500
+ try {
7501
+ const result = await client.getPrompt(arg);
7502
+ push("info", formatPromptMessages(arg, result));
7503
+ } catch (err) {
7504
+ push("warning", `getPrompt failed: ${err.message}`);
7505
+ }
7506
+ }
7507
+
7149
7508
  // src/cli/ui/paste-collapse.ts
7150
7509
  var DEFAULT_PASTE_LINE_THRESHOLD = 40;
7151
7510
  var DEFAULT_PASTE_CHAR_THRESHOLD = 2e3;
@@ -7182,7 +7541,7 @@ function formatBytes(n) {
7182
7541
  import { spawnSync } from "child_process";
7183
7542
 
7184
7543
  // src/cli/commands/stats.ts
7185
- import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
7544
+ import { existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
7186
7545
  function statsCommand(opts) {
7187
7546
  if (opts.transcript) {
7188
7547
  transcriptSummary(opts.transcript);
@@ -7191,11 +7550,11 @@ function statsCommand(opts) {
7191
7550
  dashboard(opts);
7192
7551
  }
7193
7552
  function transcriptSummary(path) {
7194
- if (!existsSync9(path)) {
7553
+ if (!existsSync10(path)) {
7195
7554
  console.error(`no such transcript: ${path}`);
7196
7555
  process.exit(1);
7197
7556
  }
7198
- const lines = readFileSync12(path, "utf8").split(/\r?\n/).filter(Boolean);
7557
+ const lines = readFileSync13(path, "utf8").split(/\r?\n/).filter(Boolean);
7199
7558
  let assistantTurns = 0;
7200
7559
  let toolCalls = 0;
7201
7560
  let lastTurn = 0;
@@ -7254,6 +7613,28 @@ function renderDashboard(agg, logPath) {
7254
7613
  if (agg.firstSeen) {
7255
7614
  lines.push(`tracked since: ${new Date(agg.firstSeen).toISOString().slice(0, 10)}`);
7256
7615
  }
7616
+ if (agg.subagents) {
7617
+ lines.push("");
7618
+ lines.push(renderSubagentSection(agg.subagents));
7619
+ }
7620
+ return lines.join("\n");
7621
+ }
7622
+ function renderSubagentSection(sub) {
7623
+ const lines = [];
7624
+ const seconds = (sub.totalDurationMs / 1e3).toFixed(1);
7625
+ lines.push(
7626
+ `subagent activity: ${sub.total} run(s) \xB7 $${sub.costUsd.toFixed(6)} \xB7 ${seconds}s total`
7627
+ );
7628
+ const top = sub.bySkill.slice(0, 5);
7629
+ for (const s of top) {
7630
+ const sec = (s.durationMs / 1e3).toFixed(1);
7631
+ lines.push(
7632
+ ` ${pad(s.skillName, 18)} ${pad(`${s.count}`, 4, "right")} $${s.costUsd.toFixed(6)} ${sec}s`
7633
+ );
7634
+ }
7635
+ if (sub.bySkill.length > top.length) {
7636
+ lines.push(` (+${sub.bySkill.length - top.length} more)`);
7637
+ }
7257
7638
  return lines.join("\n");
7258
7639
  }
7259
7640
  function header() {
@@ -7317,6 +7698,18 @@ var SLASH_COMMANDS = [
7317
7698
  argCompleter: ["off", "2", "3", "4", "5"]
7318
7699
  },
7319
7700
  { cmd: "mcp", summary: "list MCP servers + tools attached to this session" },
7701
+ {
7702
+ cmd: "resource",
7703
+ argsHint: "[uri]",
7704
+ summary: "browse + read MCP resources (no arg \u2192 list URIs; <uri> \u2192 fetch contents)",
7705
+ argCompleter: "mcp-resources"
7706
+ },
7707
+ {
7708
+ cmd: "prompt",
7709
+ argsHint: "[name]",
7710
+ summary: "browse + fetch MCP prompts (no arg \u2192 list names; <name> \u2192 render prompt)",
7711
+ argCompleter: "mcp-prompts"
7712
+ },
7320
7713
  { cmd: "tool", argsHint: "[N]", summary: "dump full output of the Nth tool call (1=latest)" },
7321
7714
  {
7322
7715
  cmd: "memory",
@@ -7461,6 +7854,11 @@ function handleSlash(cmd, args, loop, ctx = {}) {
7461
7854
  " Tab insert the highlighted item without submitting",
7462
7855
  " Enter insert and (slash) run it, (@) keep editing",
7463
7856
  "",
7857
+ "MCP exploration:",
7858
+ " /mcp servers + tool/resource/prompt counts",
7859
+ " /resource [uri] browse & read resources exposed by your MCP servers",
7860
+ " /prompt [name] browse & fetch prompts exposed by your MCP servers",
7861
+ "",
7464
7862
  "Useful slashes: /help \xB7 /context \xB7 /stats \xB7 /compact \xB7 /new \xB7 /exit"
7465
7863
  ].join("\n")
7466
7864
  };
@@ -7477,6 +7875,8 @@ function handleSlash(cmd, args, loop, ctx = {}) {
7477
7875
  " /harvest [on|off] Pillar 2: structured plan-state extraction",
7478
7876
  " /branch <N|off> run N parallel samples (N>=2), pick most confident",
7479
7877
  " /mcp list MCP servers + tools attached to this session",
7878
+ " /resource [uri] browse + read MCP resources (no arg \u2192 list URIs; <uri> \u2192 fetch)",
7879
+ " /prompt [name] browse + fetch MCP prompts (no arg \u2192 list names; <name> \u2192 render)",
7480
7880
  " /setup (exit + reconfigure) \u2192 run `reasonix setup`",
7481
7881
  " /compact [tokens] shrink large tool results in history (default 4000 tokens/result)",
7482
7882
  " /think dump the most recent turn's full R1 reasoning (reasoner only)",
@@ -7529,6 +7929,8 @@ function handleSlash(cmd, args, loop, ctx = {}) {
7529
7929
  }
7530
7930
  if (servers.length > 0) {
7531
7931
  const lines2 = [];
7932
+ let anyResources = false;
7933
+ let anyPrompts = false;
7532
7934
  for (const s of servers) {
7533
7935
  const { report } = s;
7534
7936
  const serverName = report.serverInfo.name || "(unknown)";
@@ -7537,11 +7939,20 @@ function handleSlash(cmd, args, loop, ctx = {}) {
7537
7939
  lines2.push(` tools ${s.toolCount}`);
7538
7940
  appendSection(lines2, "resources", report.resources);
7539
7941
  appendSection(lines2, "prompts ", report.prompts);
7942
+ if (report.resources.supported && report.resources.items.length > 0) anyResources = true;
7943
+ if (report.prompts.supported && report.prompts.items.length > 0) anyPrompts = true;
7540
7944
  lines2.push("");
7541
7945
  }
7542
- lines2.push(
7543
- "Chat mode consumes tools today; resources+prompts are surfaced here for awareness."
7544
- );
7946
+ if (anyResources || anyPrompts) {
7947
+ const hints = [];
7948
+ if (anyResources) hints.push("`/resource` to browse+read");
7949
+ if (anyPrompts) hints.push("`/prompt` to browse+fetch");
7950
+ lines2.push(hints.join(" \xB7 "));
7951
+ } else {
7952
+ lines2.push(
7953
+ "Chat mode consumes tools today; resources+prompts are surfaced here for awareness."
7954
+ );
7955
+ }
7545
7956
  lines2.push(
7546
7957
  "Full catalog: `reasonix mcp list` \xB7 deeper diagnosis: `reasonix mcp inspect <spec>`."
7547
7958
  );
@@ -8476,8 +8887,30 @@ function App({
8476
8887
  if (!partial) return all.slice(0, 40);
8477
8888
  return all.filter((m) => m.toLowerCase().includes(needle)).slice(0, 40);
8478
8889
  }
8890
+ if (completer === "mcp-resources") {
8891
+ const uris = [];
8892
+ const servers = mcpServers ?? [];
8893
+ for (const s of servers) {
8894
+ if (!s.report.resources.supported) continue;
8895
+ for (const r of s.report.resources.items) uris.push(r.uri);
8896
+ }
8897
+ if (partial && uris.some((u) => u.toLowerCase() === needle)) return null;
8898
+ if (!partial) return uris.slice(0, 40);
8899
+ return uris.filter((u) => u.toLowerCase().includes(needle)).slice(0, 40);
8900
+ }
8901
+ if (completer === "mcp-prompts") {
8902
+ const names = [];
8903
+ const servers = mcpServers ?? [];
8904
+ for (const s of servers) {
8905
+ if (!s.report.prompts.supported) continue;
8906
+ for (const p of s.report.prompts.items) names.push(p.name);
8907
+ }
8908
+ if (partial && names.some((n) => n.toLowerCase() === needle)) return null;
8909
+ if (!partial) return names.slice(0, 40);
8910
+ return names.filter((n) => n.toLowerCase().includes(needle)).slice(0, 40);
8911
+ }
8479
8912
  return null;
8480
- }, [slashArgContext, models]);
8913
+ }, [slashArgContext, models, mcpServers]);
8481
8914
  useEffect2(() => {
8482
8915
  setSlashArgSelected((prev) => {
8483
8916
  if (!slashArgMatches || slashArgMatches.length === 0) return 0;
@@ -8512,7 +8945,10 @@ function App({
8512
8945
  // Per-skill model override (frontmatter `model: ...`),
8513
8946
  // else falls through to spawnSubagent's default.
8514
8947
  model: skill.model,
8515
- sink: subagentSinkRef.current
8948
+ sink: subagentSinkRef.current,
8949
+ // Stamped onto every event so the TUI sink + usage log can
8950
+ // attribute the run to a skill without extra bookkeeping.
8951
+ skillName: skill.name
8516
8952
  });
8517
8953
  return formatSubagentResult(result);
8518
8954
  }
@@ -8606,7 +9042,8 @@ function App({
8606
9042
  }
8607
9043
  setSubagentActivity(null);
8608
9044
  const seconds = ((ev.elapsedMs ?? 0) / 1e3).toFixed(1);
8609
- const summary2 = ev.error ? `\u232C subagent "${ev.task}" failed after ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \u2014 ${ev.error}` : `\u232C subagent "${ev.task}" done in ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \xB7 ${ev.turns ?? 0} turn(s)`;
9045
+ const costTail = ev.costUsd !== void 0 && ev.costUsd > 0 ? ` \xB7 $${ev.costUsd.toFixed(4)}` : "";
9046
+ const summary2 = ev.error ? `\u232C subagent "${ev.task}" failed after ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \u2014 ${ev.error}` : `\u232C subagent "${ev.task}" done in ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \xB7 ${ev.turns ?? 0} turn(s)${costTail}`;
8610
9047
  setHistorical((prev) => [
8611
9048
  ...prev,
8612
9049
  {
@@ -8615,11 +9052,25 @@ function App({
8615
9052
  text: summary2
8616
9053
  }
8617
9054
  ]);
9055
+ if (!ev.error && ev.usage && ev.model) {
9056
+ appendUsage({
9057
+ session: session ?? null,
9058
+ model: ev.model,
9059
+ usage: ev.usage,
9060
+ kind: "subagent",
9061
+ subagent: {
9062
+ skillName: ev.skillName,
9063
+ taskPreview: ev.task.slice(0, 60),
9064
+ toolIters: ev.iter ?? 0,
9065
+ durationMs: ev.elapsedMs ?? 0
9066
+ }
9067
+ });
9068
+ }
8618
9069
  };
8619
9070
  return () => {
8620
9071
  subagentSinkRef.current.current = null;
8621
9072
  };
8622
- }, []);
9073
+ }, [session]);
8623
9074
  const sessionBannerShown = useRef2(false);
8624
9075
  useEffect2(() => {
8625
9076
  if (sessionBannerShown.current) return;
@@ -8652,7 +9103,21 @@ function App({
8652
9103
  }
8653
9104
  ]);
8654
9105
  }
8655
- }, [session, loop]);
9106
+ if (session && codeMode) {
9107
+ const restored = loadPendingEdits(session);
9108
+ if (restored && restored.length > 0) {
9109
+ pendingEdits.current = restored;
9110
+ setHistorical((prev) => [
9111
+ ...prev,
9112
+ {
9113
+ id: `sys-pending-${Date.now()}`,
9114
+ role: "info",
9115
+ text: `\u25B8 restored ${restored.length} pending edit block(s) from an interrupted prior run \u2014 /apply to commit or /discard to drop.`
9116
+ }
9117
+ ]);
9118
+ }
9119
+ }
9120
+ }, [session, loop, codeMode]);
8656
9121
  useInput4((_input, key) => {
8657
9122
  if (key.escape && busy) {
8658
9123
  if (abortedThisTurn.current) return;
@@ -8746,14 +9211,16 @@ function App({
8746
9211
  const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
8747
9212
  if (anyApplied) lastEditSnapshots.current = snaps;
8748
9213
  pendingEdits.current = [];
9214
+ clearPendingEdits(session ?? null);
8749
9215
  return formatEditResults(results);
8750
- }, [codeMode]);
9216
+ }, [codeMode, session]);
8751
9217
  const codeDiscard = useCallback(() => {
8752
9218
  const count = pendingEdits.current.length;
8753
9219
  if (count === 0) return "nothing pending to discard.";
8754
9220
  pendingEdits.current = [];
9221
+ clearPendingEdits(session ?? null);
8755
9222
  return `\u25B8 discarded ${count} pending edit block(s). Nothing was written to disk.`;
8756
- }, []);
9223
+ }, [session]);
8757
9224
  const prefixHash = loop.prefix.fingerprint;
8758
9225
  const writeTranscript = useCallback(
8759
9226
  (ev) => {
@@ -8833,7 +9300,7 @@ function App({
8833
9300
  ...prev,
8834
9301
  { id: `bang-o-${Date.now()}`, role: "info", text: formatted }
8835
9302
  ]);
8836
- loop.log.append({
9303
+ loop.appendAndPersist({
8837
9304
  role: "user",
8838
9305
  content: formatBangUserMessage(bangCmd, formatted)
8839
9306
  });
@@ -8851,6 +9318,18 @@ function App({
8851
9318
  }
8852
9319
  return;
8853
9320
  }
9321
+ const mcpBrowseMatch = /^\/(resource|prompt)(?:\s+([\s\S]*))?$/.exec(text);
9322
+ if (mcpBrowseMatch) {
9323
+ const kind = mcpBrowseMatch[1];
9324
+ const arg = mcpBrowseMatch[2]?.trim() ?? "";
9325
+ promptHistory.current.push(text);
9326
+ setHistorical((prev) => [
9327
+ ...prev,
9328
+ { id: `mcp-u-${Date.now()}`, role: "user", text, leadSeparator: prev.length > 0 }
9329
+ ]);
9330
+ await handleMcpBrowseSlash(kind, arg, mcpServers ?? [], setHistorical);
9331
+ return;
9332
+ }
8854
9333
  const slash = parseSlash(text);
8855
9334
  if (slash) {
8856
9335
  const result = handleSlash(slash.cmd, slash.args, loop, {
@@ -8899,10 +9378,18 @@ function App({
8899
9378
  text: result.info
8900
9379
  }
8901
9380
  ]);
9381
+ if (codeMode) {
9382
+ pendingEdits.current = [];
9383
+ clearPendingEdits(session ?? null);
9384
+ }
8902
9385
  return;
8903
9386
  }
8904
9387
  if (result.clear) {
8905
9388
  setHistorical([]);
9389
+ if (codeMode) {
9390
+ pendingEdits.current = [];
9391
+ clearPendingEdits(session ?? null);
9392
+ }
8906
9393
  return;
8907
9394
  }
8908
9395
  if (result.info) {
@@ -9017,7 +9504,9 @@ function App({
9017
9504
  if (ev.toolName) {
9018
9505
  toolCallBuildBuf.current = {
9019
9506
  name: ev.toolName,
9020
- chars: ev.toolCallArgsChars ?? 0
9507
+ chars: ev.toolCallArgsChars ?? 0,
9508
+ index: ev.toolCallIndex,
9509
+ readyCount: ev.toolCallReadyCount
9021
9510
  };
9022
9511
  }
9023
9512
  } else if (ev.role === "branch_start") {
@@ -9076,6 +9565,7 @@ function App({
9076
9565
  const blocks = parseEditBlocks(finalText);
9077
9566
  if (blocks.length > 0) {
9078
9567
  pendingEdits.current = blocks;
9568
+ savePendingEdits(session ?? null, blocks);
9079
9569
  setHistorical((prev) => [
9080
9570
  ...prev,
9081
9571
  {
@@ -9313,6 +9803,14 @@ ${body}`;
9313
9803
  },
9314
9804
  [pendingPlan, togglePlanMode, busy, loop, handleSubmit]
9315
9805
  );
9806
+ const handlePlanConfirmRef = useRef2(handlePlanConfirm);
9807
+ useEffect2(() => {
9808
+ handlePlanConfirmRef.current = handlePlanConfirm;
9809
+ }, [handlePlanConfirm]);
9810
+ const stableHandlePlanConfirm = useCallback(
9811
+ async (choice) => handlePlanConfirmRef.current(choice),
9812
+ []
9813
+ );
9316
9814
  const handleStagedInputSubmit = useCallback(
9317
9815
  async (feedback) => {
9318
9816
  const staged = stagedInput;
@@ -9366,7 +9864,7 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
9366
9864
  if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
9367
9865
  setStagedInput(null);
9368
9866
  }, [stagedInput]);
9369
- return /* @__PURE__ */ React14.createElement(TickerProvider, { disabled: PLAIN_UI }, /* @__PURE__ */ React14.createElement(Box13, { flexDirection: "column" }, /* @__PURE__ */ React14.createElement(
9867
+ return /* @__PURE__ */ React14.createElement(TickerProvider, { disabled: PLAIN_UI || !!pendingPlan || !!pendingShell }, /* @__PURE__ */ React14.createElement(Box13, { flexDirection: "column" }, /* @__PURE__ */ React14.createElement(
9370
9868
  StatsPanel,
9371
9869
  {
9372
9870
  summary,
@@ -9386,7 +9884,14 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
9386
9884
  onSubmit: handleStagedInputSubmit,
9387
9885
  onCancel: handleStagedInputCancel
9388
9886
  }
9389
- ) : pendingPlan ? /* @__PURE__ */ React14.createElement(PlanConfirm, { plan: pendingPlan, onChoose: handlePlanConfirm, projectRoot: hookCwd }) : pendingShell ? /* @__PURE__ */ React14.createElement(
9887
+ ) : pendingPlan ? /* @__PURE__ */ React14.createElement(
9888
+ PlanConfirm,
9889
+ {
9890
+ plan: pendingPlan,
9891
+ onChoose: stableHandlePlanConfirm,
9892
+ projectRoot: hookCwd
9893
+ }
9894
+ ) : pendingShell ? /* @__PURE__ */ React14.createElement(
9390
9895
  ShellConfirm,
9391
9896
  {
9392
9897
  command: pendingShell,
@@ -9717,7 +10222,8 @@ async function chatCommand(opts) {
9717
10222
  label,
9718
10223
  spec: raw,
9719
10224
  toolCount: bridge.registeredNames.length,
9720
- report
10225
+ report,
10226
+ client: mcp2
9721
10227
  });
9722
10228
  } catch (err) {
9723
10229
  const reason = err.message;
@@ -9747,7 +10253,7 @@ async function chatCommand(opts) {
9747
10253
  const prior = loadSessionMessages(opts.session);
9748
10254
  if (prior.length > 0) {
9749
10255
  const p = sessionPath(opts.session);
9750
- const mtime = existsSync10(p) ? statSync6(p).mtime : /* @__PURE__ */ new Date();
10256
+ const mtime = existsSync11(p) ? statSync6(p).mtime : /* @__PURE__ */ new Date();
9751
10257
  sessionPreview = { messageCount: prior.length, lastActive: mtime };
9752
10258
  }
9753
10259
  } else if (opts.session && opts.forceNew) {
@@ -9801,7 +10307,7 @@ async function codeCommand(opts = {}) {
9801
10307
  `
9802
10308
  );
9803
10309
  await chatCommand({
9804
- model: opts.model ?? "deepseek-reasoner",
10310
+ model: opts.model ?? "deepseek-v4-pro",
9805
10311
  harvest: opts.harvest ?? false,
9806
10312
  system: codeSystemPrompt2(rootDir),
9807
10313
  transcript: opts.transcript,
@@ -9814,7 +10320,7 @@ async function codeCommand(opts = {}) {
9814
10320
  }
9815
10321
 
9816
10322
  // src/cli/commands/diff.ts
9817
- import { writeFileSync as writeFileSync5 } from "fs";
10323
+ import { writeFileSync as writeFileSync6 } from "fs";
9818
10324
  import { basename as basename2 } from "path";
9819
10325
  import { render as render2 } from "ink";
9820
10326
  import React20 from "react";
@@ -9960,7 +10466,7 @@ async function diffCommand(opts) {
9960
10466
  if (wantMarkdown) {
9961
10467
  console.log(renderSummaryTable(report));
9962
10468
  const md = renderMarkdown(report);
9963
- writeFileSync5(opts.mdPath, md, "utf8");
10469
+ writeFileSync6(opts.mdPath, md, "utf8");
9964
10470
  console.log(`
9965
10471
  markdown report written to ${opts.mdPath}`);
9966
10472
  return;
@@ -10483,21 +10989,26 @@ import React23, { useState as useState10 } from "react";
10483
10989
 
10484
10990
  // src/cli/ui/presets.ts
10485
10991
  var PRESETS = {
10992
+ // `deepseek-chat` / `deepseek-reasoner` are retained as the fast /
10993
+ // smart models because they're deprecated-but-working compat aliases
10994
+ // for v4-flash's non-thinking and thinking modes respectively. Same
10995
+ // billing, smaller config churn for existing users. `max` promotes
10996
+ // to v4-pro — 12× flash on input/output, reserved for hard tasks.
10486
10997
  fast: { model: "deepseek-chat", harvest: false, branch: 1 },
10487
10998
  smart: { model: "deepseek-reasoner", harvest: true, branch: 1 },
10488
- max: { model: "deepseek-reasoner", harvest: true, branch: 3 }
10999
+ max: { model: "deepseek-v4-pro", harvest: true, branch: 3 }
10489
11000
  };
10490
11001
  var PRESET_DESCRIPTIONS = {
10491
11002
  fast: {
10492
- headline: "deepseek-chat, no reasoning harvest, no branching",
11003
+ headline: "deepseek-chat (= v4-flash non-thinking), no harvest, no branching",
10493
11004
  cost: "~1\xA2 per 100 turns \xB7 default"
10494
11005
  },
10495
11006
  smart: {
10496
- headline: "deepseek-reasoner + Pillar 2 harvest",
10497
- cost: "~10\xD7 cost vs fast \xB7 slower \xB7 better on multi-step tasks"
11007
+ headline: "deepseek-reasoner (= v4-flash thinking) + Pillar 2 harvest",
11008
+ cost: "same price as fast \xB7 slower \xB7 better on multi-step tasks"
10498
11009
  },
10499
11010
  max: {
10500
- headline: "reasoner + harvest + self-consistency (3 branches)",
11011
+ headline: "deepseek-v4-pro + harvest + self-consistency (3 branches)",
10501
11012
  cost: "~30\xD7 cost vs fast \xB7 slowest \xB7 for hard single-shots"
10502
11013
  }
10503
11014
  };