reasonix 0.4.27 → 0.5.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
@@ -233,6 +233,28 @@ var DeepSeekClient = class {
233
233
  return null;
234
234
  }
235
235
  }
236
+ /**
237
+ * Fetch the model catalog DeepSeek currently exposes. Today this is
238
+ * `deepseek-chat` (V3) and `deepseek-reasoner` (R1), but querying is
239
+ * the only way to learn about new ones without a Reasonix release.
240
+ * Returns null on any network/auth failure so callers can degrade
241
+ * gracefully — e.g. `/models` falls back to the hardcoded hint.
242
+ */
243
+ async listModels(opts = {}) {
244
+ try {
245
+ const resp = await this._fetch(`${this.baseUrl}/models`, {
246
+ method: "GET",
247
+ headers: { Authorization: `Bearer ${this.apiKey}` },
248
+ signal: opts.signal
249
+ });
250
+ if (!resp.ok) return null;
251
+ const data = await resp.json();
252
+ if (!data || !Array.isArray(data.data)) return null;
253
+ return data;
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
236
258
  async chat(opts) {
237
259
  const ctrl = new AbortController();
238
260
  const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
@@ -823,7 +845,8 @@ var ToolRegistry = class {
823
845
  }
824
846
  try {
825
847
  const result = await tool.fn(args, { signal: opts.signal });
826
- return typeof result === "string" ? result : JSON.stringify(result);
848
+ const str = typeof result === "string" ? result : JSON.stringify(result);
849
+ return opts.maxResultChars ? truncateForModel(str, opts.maxResultChars) : str;
827
850
  } catch (err) {
828
851
  const e = err;
829
852
  if (typeof e.toToolResult === "function") {
@@ -1393,8 +1416,8 @@ function countLines(path) {
1393
1416
 
1394
1417
  // src/telemetry.ts
1395
1418
  var DEEPSEEK_PRICING = {
1396
- "deepseek-chat": { inputCacheHit: 0.07, inputCacheMiss: 0.27, output: 1.1 },
1397
- "deepseek-reasoner": { inputCacheHit: 0.14, inputCacheMiss: 0.55, output: 2.19 }
1419
+ "deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 },
1420
+ "deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 }
1398
1421
  };
1399
1422
  var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
1400
1423
  var DEEPSEEK_CONTEXT_TOKENS = {
@@ -1949,6 +1972,24 @@ var CacheFirstLoop = class {
1949
1972
  return;
1950
1973
  }
1951
1974
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
1975
+ if (usage) {
1976
+ const ratio = usage.promptTokens / ctxMax;
1977
+ if (ratio > 0.6 && ratio <= 0.8) {
1978
+ const before = usage.promptTokens;
1979
+ const soft = this.compact(16e3);
1980
+ if (soft.healedCount > 0) {
1981
+ const approxSaved = Math.round(soft.charsSaved / 4);
1982
+ const after = Math.max(0, before - approxSaved);
1983
+ yield {
1984
+ turn: this._turn,
1985
+ role: "warning",
1986
+ content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
1987
+ ratio * 100
1988
+ )}%) \u2014 proactively compacted ${soft.healedCount} tool result(s) to 16k, saved ~${approxSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Staying ahead of the 80% guard.`
1989
+ };
1990
+ }
1991
+ }
1992
+ }
1952
1993
  if (usage && usage.promptTokens / ctxMax > 0.8) {
1953
1994
  const before = usage.promptTokens;
1954
1995
  const compactResult = this.compact(4e3);
@@ -2011,7 +2052,10 @@ var CacheFirstLoop = class {
2011
2052
  result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
2012
2053
  ${reason}`;
2013
2054
  } else {
2014
- result = await this.tools.dispatch(name, args, { signal });
2055
+ result = await this.tools.dispatch(name, args, {
2056
+ signal,
2057
+ maxResultChars: DEFAULT_MAX_RESULT_CHARS
2058
+ });
2015
2059
  const postReport = await runHooks({
2016
2060
  hooks: this.hooks,
2017
2061
  payload: {
@@ -4864,8 +4908,8 @@ function writeCache(entry, homeDirOverride) {
4864
4908
  async function getLatestVersion(opts = {}) {
4865
4909
  const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
4866
4910
  if (!opts.force) {
4867
- const cached = readCache(opts.homeDir);
4868
- if (cached && Date.now() - cached.checkedAt < ttl) return cached.version;
4911
+ const cached2 = readCache(opts.homeDir);
4912
+ if (cached2 && Date.now() - cached2.checkedAt < ttl) return cached2.version;
4869
4913
  }
4870
4914
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
4871
4915
  if (!fetchImpl) return null;
@@ -5117,7 +5161,7 @@ ${skill.body}${argsBlock}`;
5117
5161
  }
5118
5162
 
5119
5163
  // src/cli/ui/EventLog.tsx
5120
- import { Box as Box3, Text as Text3 } from "ink";
5164
+ import { Box as Box3, Text as Text3, useStdout } from "ink";
5121
5165
  import React4 from "react";
5122
5166
 
5123
5167
  // src/cli/ui/PlanStateBlock.tsx
@@ -5277,7 +5321,7 @@ function InlineMd({
5277
5321
  const status = citations?.get(url);
5278
5322
  if (status && !status.ok) {
5279
5323
  parts.push(
5280
- /* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u274C`)
5324
+ /* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u2717`)
5281
5325
  );
5282
5326
  } else {
5283
5327
  parts.push(
@@ -5583,7 +5627,7 @@ function Markdown({ text, projectRoot }) {
5583
5627
  function BrokenCitationsBlock({ items }) {
5584
5628
  return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { 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) => (
5585
5629
  // biome-ignore lint/suspicious/noArrayIndexKey: list is derived from a Map iteration order, stable per render
5586
- /* @__PURE__ */ React2.createElement(Text2, { key: `bc-${i}`, color: "red" }, ` \u274C ${b.url} \u2192 ${b.reason}`)
5630
+ /* @__PURE__ */ React2.createElement(Text2, { key: `bc-${i}`, color: "red" }, ` \u2717 ${b.url} \u2192 ${b.reason}`)
5587
5631
  )));
5588
5632
  }
5589
5633
 
@@ -5610,32 +5654,49 @@ function useElapsedSeconds() {
5610
5654
  }
5611
5655
 
5612
5656
  // src/cli/ui/EventLog.tsx
5657
+ var ROLE_GLYPH = {
5658
+ user: "\u25C7",
5659
+ assistant: "\u25C6",
5660
+ assistantPulse: "\u25C7",
5661
+ // pulse alternate for streaming state
5662
+ toolOk: "\u25A3",
5663
+ toolErr: "\u25A5",
5664
+ warning: "\u25B2",
5665
+ error: "\u2726"
5666
+ };
5667
+ function RoleGlyph({
5668
+ glyph,
5669
+ color
5670
+ }) {
5671
+ return /* @__PURE__ */ React4.createElement(Text3, { color, bold: true }, glyph);
5672
+ }
5613
5673
  var EventRow = React4.memo(function EventRow2({
5614
5674
  event,
5615
5675
  projectRoot
5616
5676
  }) {
5617
5677
  if (event.role === "user") {
5618
- return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React4.createElement(Text3, null, event.text));
5678
+ return /* @__PURE__ */ React4.createElement(Box3, { marginTop: event.leadSeparator ? 1 : 0 }, /* @__PURE__ */ React4.createElement(RoleGlyph, { glyph: ROLE_GLYPH.user, color: "cyan" }), /* @__PURE__ */ React4.createElement(Text3, null, " ", event.text));
5619
5679
  }
5620
5680
  if (event.role === "assistant") {
5621
5681
  if (event.streaming) return /* @__PURE__ */ React4.createElement(StreamingAssistant, { event });
5622
- return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React4.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React4.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React4.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React4.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React4.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React4.createElement(Text3, { color: "magenta" }, event.repair) : null);
5682
+ return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(RoleGlyph, { glyph: ROLE_GLYPH.assistant, color: "green" }), event.stats ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, ` ${event.stats.model}`) : null), /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, event.branch ? /* @__PURE__ */ React4.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React4.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React4.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React4.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React4.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React4.createElement(Text3, { color: "magenta" }, event.repair) : null));
5623
5683
  }
5624
5684
  if (event.role === "tool") {
5625
5685
  const isError = event.text.startsWith("ERROR:");
5626
5686
  const color = isError ? "red" : "yellow";
5687
+ const glyph = isError ? ROLE_GLYPH.toolErr : ROLE_GLYPH.toolOk;
5627
5688
  const marker = isError ? "\u2717" : "\u2192";
5628
5689
  const isEditFile = (event.toolName === "edit_file" || event.toolName?.endsWith("_edit_file")) && !isError;
5629
- return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text3, { color }, `tool<${event.toolName ?? "?"}> ${marker}`), isEditFile ? /* @__PURE__ */ React4.createElement(EditFileDiff, { text: event.text }) : /* @__PURE__ */ React4.createElement(Text3, { color: isError ? "red" : void 0, dimColor: !isError }, " ", truncate2(event.text, 400)));
5690
+ return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(RoleGlyph, { glyph, color }), /* @__PURE__ */ React4.createElement(Text3, { color, bold: true }, ` ${event.toolName ?? "?"}`), /* @__PURE__ */ React4.createElement(Text3, { color, dimColor: true }, ` ${marker}`)), /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", paddingLeft: 2, marginTop: 1 }, isEditFile ? /* @__PURE__ */ React4.createElement(EditFileDiff, { text: event.text }) : /* @__PURE__ */ React4.createElement(Text3, { color: isError ? "red" : void 0, dimColor: !isError }, truncate2(event.text, 400))));
5630
5691
  }
5631
5692
  if (event.role === "error") {
5632
- return /* @__PURE__ */ React4.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React4.createElement(Text3, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React4.createElement(Text3, { color: "red" }, event.text));
5693
+ return /* @__PURE__ */ React4.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React4.createElement(RoleGlyph, { glyph: ROLE_GLYPH.error, color: "red" }), /* @__PURE__ */ React4.createElement(Text3, { color: "red" }, " ", event.text));
5633
5694
  }
5634
5695
  if (event.role === "info") {
5635
5696
  return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, event.text));
5636
5697
  }
5637
5698
  if (event.role === "warning") {
5638
- return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { color: "yellow" }, "\u25B8 "), /* @__PURE__ */ React4.createElement(Text3, { color: "yellow" }, event.text));
5699
+ return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(RoleGlyph, { glyph: ROLE_GLYPH.warning, color: "yellow" }), /* @__PURE__ */ React4.createElement(Text3, { color: "yellow" }, " ", event.text));
5639
5700
  }
5640
5701
  return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, null, event.text));
5641
5702
  });
@@ -5659,13 +5720,13 @@ function BranchBlock({ branch }) {
5659
5720
  const t = (branch.temperatures[i] ?? 0).toFixed(1);
5660
5721
  return `${marker} #${i} T=${t} u=${u}`;
5661
5722
  }).join(" ");
5662
- return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { color: "blue" }, "\u{1F500} branched ", /* @__PURE__ */ React4.createElement(Text3, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, per)));
5723
+ return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { color: "blue" }, "\u2387 branched ", /* @__PURE__ */ React4.createElement(Text3, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, per)));
5663
5724
  }
5664
5725
  function ReasoningBlock({ reasoning }) {
5665
5726
  const max = 260;
5666
5727
  const flat = reasoning.replace(/\s+/g, " ").trim();
5667
5728
  const preview = flat.length <= max ? flat : `\u2026 (+${flat.length - max} earlier chars) ${flat.slice(-max)}`;
5668
- return /* @__PURE__ */ React4.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
5729
+ return /* @__PURE__ */ React4.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "\u258F "), /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, "thinking ", preview));
5669
5730
  }
5670
5731
  function Elapsed() {
5671
5732
  const s = useElapsedSeconds();
@@ -5673,14 +5734,19 @@ function Elapsed() {
5673
5734
  const ss = String(s % 60).padStart(2, "0");
5674
5735
  return /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, `${mm}:${ss}`);
5675
5736
  }
5737
+ function PulsingAssistantGlyph() {
5738
+ const tick = useTick();
5739
+ const on = Math.floor(tick / 4) % 2 === 0;
5740
+ return /* @__PURE__ */ React4.createElement(Text3, { color: "green", bold: true }, on ? ROLE_GLYPH.assistant : ROLE_GLYPH.assistantPulse);
5741
+ }
5676
5742
  function StreamingAssistant({ event }) {
5677
5743
  if (event.branchProgress) {
5678
5744
  const p = event.branchProgress;
5679
5745
  if (p.completed === 0) {
5680
- return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React4.createElement(Text3, { color: "blue" }, "\u{1F500} launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026", " "), /* @__PURE__ */ React4.createElement(Elapsed, null)), /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, " ", "spread across T=0.0/0.5/1.0 \xB7 typical wait 30-90s for reasoner"));
5746
+ return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React4.createElement(Text3, { color: "blue" }, " \u2387 launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026 "), /* @__PURE__ */ React4.createElement(Elapsed, null)), /* @__PURE__ */ React4.createElement(Text3, { color: "yellow" }, " ", "spread across T=0.0/0.5/1.0 \xB7 reasoner typically takes 30-90s \u2014 this is normal"));
5681
5747
  }
5682
5748
  const pct2 = Math.round(p.completed / p.total * 100);
5683
- return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React4.createElement(Text3, { color: "blue" }, "\u{1F500} branching ", p.completed, "/", p.total, " (", pct2, "%)", " "), /* @__PURE__ */ React4.createElement(Elapsed, null)), /* @__PURE__ */ React4.createElement(Text3, { 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"));
5749
+ return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React4.createElement(Text3, { color: "blue" }, " \u2387 branching ", p.completed, "/", p.total, " (", pct2, "%) "), /* @__PURE__ */ React4.createElement(Elapsed, null)), /* @__PURE__ */ React4.createElement(Text3, { 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"));
5684
5750
  }
5685
5751
  const tail = lastLine(event.text, 140);
5686
5752
  const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
@@ -5708,7 +5774,11 @@ function StreamingAssistant({ event }) {
5708
5774
  label = parts.join(" \xB7 ");
5709
5775
  labelColor = "green";
5710
5776
  }
5711
- return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React4.createElement(Pulse, null), /* @__PURE__ */ React4.createElement(Text3, { color: labelColor }, ` ${label} `), /* @__PURE__ */ React4.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "\u25B8 ", tail) : preFirstByte ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, " connection open, first byte typically in 5-60s depending on model + load") : reasoningOnly ? /* @__PURE__ */ React4.createElement(Text3, { color: "yellow", dimColor: true }, " R1 is thinking before it speaks \u2014 body text starts when reasoning completes (typically 20-90s).") : toolCallOnly ? /* @__PURE__ */ React4.createElement(Text3, { color: "magenta", dimColor: true }, " tool-call arguments streaming \u2014 the model is about to dispatch a tool") : event.reasoning ? /* @__PURE__ */ React4.createElement(Text3, { color: "yellow", dimColor: true }, " R1 still reasoning \u2014 body text or tool call arrives when thinking completes") : null);
5777
+ return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(PulsingAssistantGlyph, null), /* @__PURE__ */ React4.createElement(Text3, null, " "), /* @__PURE__ */ React4.createElement(Pulse, null), /* @__PURE__ */ React4.createElement(Text3, { color: labelColor }, ` ${label} `), /* @__PURE__ */ React4.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "\u25B8 ", tail) : preFirstByte ? (
5778
+ // Non-dim yellow: first-time users misread the dim version as
5779
+ // "app frozen". The reassurance has to be VISIBLE to do its job.
5780
+ /* @__PURE__ */ React4.createElement(Text3, { color: "yellow", italic: true }, " waiting for first byte \u2014 this is normal, typically 5-60s depending on model + load")
5781
+ ) : reasoningOnly ? /* @__PURE__ */ React4.createElement(Text3, { 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__ */ React4.createElement(Text3, { color: "magenta", italic: true }, " tool-call arguments streaming \u2014 the model is about to dispatch a tool") : event.reasoning ? /* @__PURE__ */ React4.createElement(Text3, { color: "yellow", italic: true }, " R1 still reasoning \u2014 body text or tool call arrives when thinking finishes") : null);
5712
5782
  }
5713
5783
  function Pulse() {
5714
5784
  const tick = useTick();
@@ -5722,10 +5792,26 @@ function lastLine(s, maxChars) {
5722
5792
  }
5723
5793
  function StatsLine({ stats }) {
5724
5794
  const hit = (stats.cacheHitRatio * 100).toFixed(1);
5725
- return /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, " \u21B3 cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, "\u2192", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6));
5795
+ return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "\u258F "), /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, " \u2192 ", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6)));
5726
5796
  }
5727
5797
  function truncate2(s, max) {
5728
- return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
5798
+ if (s.length <= max) return s;
5799
+ if (s.startsWith("ERROR:")) {
5800
+ const firstNl = s.indexOf("\n");
5801
+ const firstLine = firstNl === -1 ? s : s.slice(0, firstNl);
5802
+ if (firstLine.length >= max) {
5803
+ return `${firstLine.slice(0, max)}\u2026 (+${s.length - max} chars \u2014 /tool N for full)`;
5804
+ }
5805
+ const budget = max - firstLine.length - 10;
5806
+ const tail = s.slice(-budget);
5807
+ const skipped2 = s.length - firstLine.length - tail.length;
5808
+ return `${firstLine}
5809
+ \u2026 (+${skipped2} chars) \u2026
5810
+ ${tail}`;
5811
+ }
5812
+ const skipped = s.length - max;
5813
+ return `\u2026 (+${skipped} earlier chars \u2014 /tool N for full) \u2026
5814
+ ${s.slice(-max)}`;
5729
5815
  }
5730
5816
 
5731
5817
  // src/cli/ui/PlanConfirm.tsx
@@ -5739,7 +5825,8 @@ function SingleSelect({
5739
5825
  items,
5740
5826
  initialValue,
5741
5827
  onSubmit,
5742
- onCancel
5828
+ onCancel,
5829
+ footer
5743
5830
  }) {
5744
5831
  const initialIndex = Math.max(
5745
5832
  0,
@@ -5766,7 +5853,7 @@ function SingleSelect({
5766
5853
  active: i === index,
5767
5854
  marker: i === index ? "\u25B8" : " "
5768
5855
  }
5769
- )));
5856
+ )), footer ? /* @__PURE__ */ React5.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, footer)) : null);
5770
5857
  }
5771
5858
  function MultiSelect({
5772
5859
  items,
@@ -5842,7 +5929,7 @@ function PlanConfirm({ plan, onChoose, maxRenderedChars, projectRoot }) {
5842
5929
 
5843
5930
  \u2026 (${plan.length - cap} chars truncated \u2014 use /tool to view the full proposal)` : plan;
5844
5931
  const hasOpenQuestions = /^#{1,6}\s*(open[-\s]?questions?|risks?|unknowns?|assumptions?|unclear)/im.test(plan) || /^#{1,6}\s*(待确认|开放问题|风险|未知|假设|不确定)/im.test(plan);
5845
- 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" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Markdown, { text: visible, projectRoot })), hasOpenQuestions ? /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React6.createElement(Text5, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(
5932
+ 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" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { 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__ */ React6.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Markdown, { text: visible, projectRoot })), hasOpenQuestions ? /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React6.createElement(Text5, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(
5846
5933
  SingleSelect,
5847
5934
  {
5848
5935
  initialValue: hasOpenQuestions ? "refine" : "approve",
@@ -5863,7 +5950,9 @@ function PlanConfirm({ plan, onChoose, maxRenderedChars, projectRoot }) {
5863
5950
  hint: "Exit plan mode. Drop the plan; the model won't implement it."
5864
5951
  }
5865
5952
  ],
5866
- onSubmit: (v) => onChoose(v)
5953
+ onSubmit: (v) => onChoose(v),
5954
+ onCancel: () => onChoose("cancel"),
5955
+ footer: "[\u2191\u2193] navigate \xB7 [Enter] select \xB7 [Esc] cancel"
5867
5956
  }
5868
5957
  )));
5869
5958
  }
@@ -6054,7 +6143,7 @@ function PromptInput({
6054
6143
  },
6055
6144
  { isActive: !disabled }
6056
6145
  );
6057
- const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command \xB7 Ctrl+J for newline";
6146
+ const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026 \xB7 [Esc] to stop" : placeholder ?? "type a message, or /command \xB7 [Shift+Enter] / [Ctrl+J] newline";
6058
6147
  const lines = value.length > 0 ? value.split("\n") : [""];
6059
6148
  const borderColor = disabled ? "gray" : "cyan";
6060
6149
  const { line: cursorLine, col: cursorCol } = lineAndColumn(value, cursor);
@@ -6095,7 +6184,7 @@ function LineWithCursor({
6095
6184
  import { Box as Box8, Text as Text8 } from "ink";
6096
6185
  import React9 from "react";
6097
6186
  function ShellConfirm({ command, allowPrefix, onChoose }) {
6098
- return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "yellow" }, "\u25B8 model wants to run a shell command")), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "$ "), /* @__PURE__ */ React9.createElement(Text8, { color: "cyan" }, command))), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(
6187
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "yellow" }, "\u25B8 model wants to run a shell command")), /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { 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__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "$ "), /* @__PURE__ */ React9.createElement(Text8, { color: "cyan" }, command))), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(
6099
6188
  SingleSelect,
6100
6189
  {
6101
6190
  initialValue: "run_once",
@@ -6116,7 +6205,9 @@ function ShellConfirm({ command, allowPrefix, onChoose }) {
6116
6205
  hint: "Tell the model the user refused; it will continue without this command."
6117
6206
  }
6118
6207
  ],
6119
- onSubmit: (v) => onChoose(v)
6208
+ onSubmit: (v) => onChoose(v),
6209
+ onCancel: () => onChoose("deny"),
6210
+ footer: "[\u2191\u2193] navigate \xB7 [Enter] select \xB7 [Esc] deny"
6120
6211
  }
6121
6212
  )));
6122
6213
  }
@@ -6166,7 +6257,7 @@ function SlashSuggestions({
6166
6257
  const shown = matches.slice(windowStart, windowStart + MAX);
6167
6258
  const hiddenAbove = windowStart;
6168
6259
  const hiddenBelow = total - windowStart - shown.length;
6169
- return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React10.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2191/\u2193 navigate \xB7 Tab or Enter to pick"));
6260
+ return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React10.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick"));
6170
6261
  }
6171
6262
  function SuggestionRow({ spec, isSelected }) {
6172
6263
  const marker = isSelected ? "\u25B8" : " ";
@@ -6179,8 +6270,37 @@ function SuggestionRow({ spec, isSelected }) {
6179
6270
  }
6180
6271
 
6181
6272
  // src/cli/ui/StatsPanel.tsx
6182
- import { Box as Box10, Text as Text10 } from "ink";
6273
+ import { Box as Box10, Text as Text10, useStdout as useStdout2 } from "ink";
6183
6274
  import React11 from "react";
6275
+ var WORDMARK_STYLES = [
6276
+ { ch: "\u25C8", color: "#5eead4", isLogo: true },
6277
+ // teal — brand mark
6278
+ { ch: " ", color: "#5eead4", isLogo: false },
6279
+ { ch: "R", color: "#67e8f9", isLogo: false },
6280
+ // cyan
6281
+ { ch: "E", color: "#7dd3fc", isLogo: false },
6282
+ // sky
6283
+ { ch: "A", color: "#93c5fd", isLogo: false },
6284
+ // blue
6285
+ { ch: "S", color: "#a5b4fc", isLogo: false },
6286
+ // indigo
6287
+ { ch: "O", color: "#c4b5fd", isLogo: false },
6288
+ // violet
6289
+ { ch: "N", color: "#d8b4fe", isLogo: false },
6290
+ // purple
6291
+ { ch: "I", color: "#f0abfc", isLogo: false },
6292
+ // fuchsia
6293
+ { ch: "X", color: "#f0abfc", isLogo: false }
6294
+ // fuchsia
6295
+ ];
6296
+ function Wordmark({ busy }) {
6297
+ const tick = useTick();
6298
+ const period = busy ? 5 : 12;
6299
+ const bright = Math.floor(tick / period) % 2 === 0;
6300
+ return /* @__PURE__ */ React11.createElement(Text10, null, WORDMARK_STYLES.map((c) => /* @__PURE__ */ React11.createElement(Text10, { key: `${c.ch}-${c.color}`, color: c.color, bold: c.isLogo ? bright : true }, c.ch)));
6301
+ }
6302
+ var NARROW_BREAKPOINT = 120;
6303
+ var COLD_START_TURNS = 3;
6184
6304
  function StatsPanel({
6185
6305
  summary,
6186
6306
  model,
@@ -6189,27 +6309,313 @@ function StatsPanel({
6189
6309
  branchBudget,
6190
6310
  planMode,
6191
6311
  balance,
6192
- updateAvailable
6312
+ updateAvailable,
6313
+ busy
6193
6314
  }) {
6194
- const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
6195
- const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
6196
6315
  const branchOn = (branchBudget ?? 1) > 1;
6197
6316
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
6198
6317
  const ctxRatio = summary.lastPromptTokens / ctxMax;
6199
- const ctxColor = ctxRatio >= 0.8 ? "red" : ctxRatio >= 0.5 ? "yellow" : void 0;
6200
- return /* @__PURE__ */ React11.createElement(Box10, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React11.createElement(Box10, { justifyContent: "space-between" }, /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, ` v${VERSION}`), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React11.createElement(Text10, { color: "yellow" }, model), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React11.createElement(Text10, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React11.createElement(Text10, { color: "blue" }, " \xB7 branch", branchBudget) : null, planMode ? /* @__PURE__ */ React11.createElement(Text10, { color: "red", bold: true }, " ", "\xB7 PLAN") : null), /* @__PURE__ */ React11.createElement(Text10, null, updateAvailable ? /* @__PURE__ */ React11.createElement(Text10, { color: "yellow", bold: true }, `update: ${updateAvailable} \xB7 `) : null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help"))), /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache hit "), /* @__PURE__ */ React11.createElement(Text10, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cost "), /* @__PURE__ */ React11.createElement(Text10, { color: "green", bold: true }, "$", summary.totalCostUsd.toFixed(6)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (in ", "$", summary.totalInputCostUsd.toFixed(6), " \xB7 out ", "$", summary.totalOutputCostUsd.toFixed(6), ")")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "ctx "), /* @__PURE__ */ React11.createElement(Text10, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React11.createElement(Text10, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null, balance ? /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "balance "), /* @__PURE__ */ React11.createElement(Text10, { color: balance.total < 1 ? "red" : balance.total < 5 ? "yellow" : "green", bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : "")) : null));
6318
+ const { stdout: stdout2 } = useStdout2();
6319
+ const columns = stdout2?.columns ?? 80;
6320
+ const narrow = columns < NARROW_BREAKPOINT;
6321
+ const coldStart = summary.turns <= COLD_START_TURNS;
6322
+ return /* @__PURE__ */ React11.createElement(Box10, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React11.createElement(
6323
+ Header,
6324
+ {
6325
+ model,
6326
+ prefixHash,
6327
+ harvestOn,
6328
+ branchOn,
6329
+ branchBudget: branchBudget ?? 1,
6330
+ planMode,
6331
+ turns: summary.turns,
6332
+ updateAvailable,
6333
+ narrow,
6334
+ busy: busy ?? false
6335
+ }
6336
+ ), narrow ? /* @__PURE__ */ React11.createElement(
6337
+ StackedMetrics,
6338
+ {
6339
+ summary,
6340
+ ctxRatio,
6341
+ ctxMax,
6342
+ balance,
6343
+ coldStart
6344
+ }
6345
+ ) : /* @__PURE__ */ React11.createElement(
6346
+ InlineMetrics,
6347
+ {
6348
+ summary,
6349
+ ctxRatio,
6350
+ ctxMax,
6351
+ balance,
6352
+ coldStart
6353
+ }
6354
+ ));
6355
+ }
6356
+ function Header({
6357
+ model,
6358
+ prefixHash,
6359
+ harvestOn,
6360
+ branchOn,
6361
+ branchBudget,
6362
+ planMode,
6363
+ turns,
6364
+ updateAvailable,
6365
+ narrow,
6366
+ busy
6367
+ }) {
6368
+ return /* @__PURE__ */ React11.createElement(Box10, { justifyContent: "space-between" }, /* @__PURE__ */ React11.createElement(Box10, null, /* @__PURE__ */ React11.createElement(Wordmark, { busy }), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, ` v${VERSION}`), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React11.createElement(Text10, { color: "yellow" }, model), narrow ? null : /* @__PURE__ */ React11.createElement(React11.Fragment, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, prefixHash)), harvestOn ? /* @__PURE__ */ React11.createElement(Text10, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React11.createElement(Text10, { color: "blue" }, " \xB7 branch", branchBudget) : null, planMode ? /* @__PURE__ */ React11.createElement(Text10, { color: "red", bold: true }, " \xB7 PLAN") : null), /* @__PURE__ */ React11.createElement(Text10, null, updateAvailable ? /* @__PURE__ */ React11.createElement(Text10, { color: "yellow", bold: true }, `update: ${updateAvailable} \xB7 `) : null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, narrow ? `turn ${turns}` : `turn ${turns} \xB7 /help`)));
6369
+ }
6370
+ function InlineMetrics({
6371
+ summary,
6372
+ ctxRatio,
6373
+ ctxMax,
6374
+ balance,
6375
+ coldStart
6376
+ }) {
6377
+ return /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React11.createElement(ContextCell, { ratio: ctxRatio, promptTokens: summary.lastPromptTokens, ctxMax }), /* @__PURE__ */ React11.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React11.createElement(CostCell, { summary, coldStart }), balance ? /* @__PURE__ */ React11.createElement(BalanceCell, { balance }) : null);
6378
+ }
6379
+ function StackedMetrics({
6380
+ summary,
6381
+ ctxRatio,
6382
+ ctxMax,
6383
+ balance,
6384
+ coldStart
6385
+ }) {
6386
+ return /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React11.createElement(
6387
+ ContextCell,
6388
+ {
6389
+ ratio: ctxRatio,
6390
+ promptTokens: summary.lastPromptTokens,
6391
+ ctxMax,
6392
+ showBar: true
6393
+ }
6394
+ ), balance ? /* @__PURE__ */ React11.createElement(BalanceCell, { balance }) : null, /* @__PURE__ */ React11.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React11.createElement(CostCell, { summary, coldStart }));
6395
+ }
6396
+ function ContextCell({
6397
+ ratio,
6398
+ promptTokens,
6399
+ ctxMax,
6400
+ showBar
6401
+ }) {
6402
+ if (promptTokens === 0) {
6403
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "ctx "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "\u2014 (no turns yet)"));
6404
+ }
6405
+ const color = ratio >= 0.8 ? "red" : ratio >= 0.6 ? "yellow" : "green";
6406
+ const pct2 = Math.round(ratio * 100);
6407
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "ctx "), showBar ? /* @__PURE__ */ React11.createElement(Bar, { ratio, color }) : null, showBar ? /* @__PURE__ */ React11.createElement(Text10, null, " ") : null, /* @__PURE__ */ React11.createElement(Text10, { color, bold: true }, formatTokens(promptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (", pct2, "%)"), ratio >= 0.8 ? /* @__PURE__ */ React11.createElement(Text10, { color: "red", bold: true }, " \xB7 /compact") : null);
6408
+ }
6409
+ function CacheCell({
6410
+ hitRatio,
6411
+ coldStart,
6412
+ turns
6413
+ }) {
6414
+ const pct2 = (hitRatio * 100).toFixed(1);
6415
+ if (turns === 0) {
6416
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "\u2014"));
6417
+ }
6418
+ if (coldStart) {
6419
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, pct2, "% "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true, italic: true }, "(cold start)"));
6420
+ }
6421
+ const color = hitRatio >= 0.7 ? "green" : hitRatio >= 0.4 ? "yellow" : "red";
6422
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache "), /* @__PURE__ */ React11.createElement(Text10, { color, bold: true }, pct2, "%"));
6423
+ }
6424
+ function CostCell({
6425
+ summary,
6426
+ coldStart
6427
+ }) {
6428
+ if (summary.turns === 0) {
6429
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cost "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "\u2014"));
6430
+ }
6431
+ const primaryColor = coldStart ? void 0 : "green";
6432
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cost "), /* @__PURE__ */ React11.createElement(Text10, { color: primaryColor, bold: !coldStart, dimColor: coldStart }, "$", summary.totalCostUsd.toFixed(6)), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, " (in ", "$", summary.totalInputCostUsd.toFixed(6), " \xB7 out ", "$", summary.totalOutputCostUsd.toFixed(6), ")"));
6433
+ }
6434
+ function BalanceCell({ balance }) {
6435
+ const color = balance.total < 1 ? "red" : balance.total < 5 ? "yellow" : "green";
6436
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "balance "), /* @__PURE__ */ React11.createElement(Text10, { color, bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : ""));
6437
+ }
6438
+ function Bar({ ratio, color }) {
6439
+ const cells = 10;
6440
+ const filled = Math.max(0, Math.min(cells, Math.round(ratio * cells)));
6441
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(cells - filled);
6442
+ return /* @__PURE__ */ React11.createElement(Text10, { color }, bar);
6201
6443
  }
6202
6444
  function formatTokens(n) {
6203
- if (n < 1e3) return String(n);
6204
- const k = n / 1e3;
6205
- return k >= 100 ? `${k.toFixed(0)}k` : `${k.toFixed(1)}k`;
6445
+ if (n < 1024) return String(n);
6446
+ const k = n / 1024;
6447
+ return k >= 100 ? `${k.toFixed(0)}K` : `${k.toFixed(1)}K`;
6206
6448
  }
6207
6449
 
6208
6450
  // src/cli/ui/slash.ts
6209
6451
  import { spawnSync } from "child_process";
6210
6452
 
6453
+ // src/tokenizer.ts
6454
+ import { readFileSync as readFileSync10 } from "fs";
6455
+ import { createRequire } from "module";
6456
+ import { dirname as dirname7, join as join8 } from "path";
6457
+ import { fileURLToPath as fileURLToPath2 } from "url";
6458
+ import { gunzipSync } from "zlib";
6459
+ function buildByteToChar() {
6460
+ const result = new Array(256);
6461
+ const bs = [];
6462
+ for (let b = 33; b <= 126; b++) bs.push(b);
6463
+ for (let b = 161; b <= 172; b++) bs.push(b);
6464
+ for (let b = 174; b <= 255; b++) bs.push(b);
6465
+ const cs = bs.slice();
6466
+ let n = 0;
6467
+ for (let b = 0; b < 256; b++) {
6468
+ if (!bs.includes(b)) {
6469
+ bs.push(b);
6470
+ cs.push(256 + n);
6471
+ n++;
6472
+ }
6473
+ }
6474
+ for (let i = 0; i < bs.length; i++) {
6475
+ result[bs[i]] = String.fromCodePoint(cs[i]);
6476
+ }
6477
+ return result;
6478
+ }
6479
+ var cached = null;
6480
+ function resolveDataPath() {
6481
+ if (process.env.REASONIX_TOKENIZER_PATH) return process.env.REASONIX_TOKENIZER_PATH;
6482
+ try {
6483
+ const here = dirname7(fileURLToPath2(import.meta.url));
6484
+ return join8(here, "..", "data", "deepseek-tokenizer.json.gz");
6485
+ } catch {
6486
+ const req = createRequire(import.meta.url);
6487
+ return join8(
6488
+ dirname7(req.resolve("reasonix/package.json")),
6489
+ "data",
6490
+ "deepseek-tokenizer.json.gz"
6491
+ );
6492
+ }
6493
+ }
6494
+ function loadTokenizer() {
6495
+ if (cached) return cached;
6496
+ const buf = readFileSync10(resolveDataPath());
6497
+ const json = gunzipSync(buf).toString("utf8");
6498
+ const data = JSON.parse(json);
6499
+ const mergeRank = /* @__PURE__ */ new Map();
6500
+ for (let i = 0; i < data.model.merges.length; i++) {
6501
+ mergeRank.set(data.model.merges[i], i);
6502
+ }
6503
+ const splitRegexes = [];
6504
+ for (const p of data.pre_tokenizer.pretokenizers) {
6505
+ if (p.type === "Split") {
6506
+ splitRegexes.push(new RegExp(p.pattern.Regex, "gu"));
6507
+ }
6508
+ }
6509
+ const addedMap = /* @__PURE__ */ new Map();
6510
+ const addedContents = [];
6511
+ for (const t of data.added_tokens) {
6512
+ if (!t.special) {
6513
+ addedMap.set(t.content, t.id);
6514
+ addedContents.push(t.content);
6515
+ }
6516
+ }
6517
+ addedContents.sort((a, b) => b.length - a.length);
6518
+ const addedPattern = addedContents.length ? new RegExp(addedContents.map(escapeRegex).join("|"), "g") : null;
6519
+ cached = {
6520
+ vocab: data.model.vocab,
6521
+ mergeRank,
6522
+ splitRegexes,
6523
+ byteToChar: buildByteToChar(),
6524
+ addedPattern,
6525
+ addedMap
6526
+ };
6527
+ return cached;
6528
+ }
6529
+ function escapeRegex(s) {
6530
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6531
+ }
6532
+ function applySplit(chunks, re) {
6533
+ const out = [];
6534
+ for (const chunk of chunks) {
6535
+ if (!chunk) continue;
6536
+ re.lastIndex = 0;
6537
+ let last = 0;
6538
+ for (const m of chunk.matchAll(re)) {
6539
+ const idx = m.index ?? 0;
6540
+ if (idx > last) out.push(chunk.slice(last, idx));
6541
+ if (m[0].length > 0) out.push(m[0]);
6542
+ last = idx + m[0].length;
6543
+ }
6544
+ if (last < chunk.length) out.push(chunk.slice(last));
6545
+ }
6546
+ return out;
6547
+ }
6548
+ function byteLevelEncode(s, byteToChar) {
6549
+ const bytes = new TextEncoder().encode(s);
6550
+ let out = "";
6551
+ for (let i = 0; i < bytes.length; i++) out += byteToChar[bytes[i]];
6552
+ return out;
6553
+ }
6554
+ function bpeEncode(piece, mergeRank) {
6555
+ if (piece.length <= 1) return piece ? [piece] : [];
6556
+ let word = Array.from(piece);
6557
+ while (true) {
6558
+ let bestIdx = -1;
6559
+ let bestRank = Number.POSITIVE_INFINITY;
6560
+ for (let i = 0; i < word.length - 1; i++) {
6561
+ const pair = `${word[i]} ${word[i + 1]}`;
6562
+ const rank = mergeRank.get(pair);
6563
+ if (rank !== void 0 && rank < bestRank) {
6564
+ bestRank = rank;
6565
+ bestIdx = i;
6566
+ if (rank === 0) break;
6567
+ }
6568
+ }
6569
+ if (bestIdx < 0) break;
6570
+ word = [
6571
+ ...word.slice(0, bestIdx),
6572
+ word[bestIdx] + word[bestIdx + 1],
6573
+ ...word.slice(bestIdx + 2)
6574
+ ];
6575
+ if (word.length === 1) break;
6576
+ }
6577
+ return word;
6578
+ }
6579
+ function encode(text) {
6580
+ if (!text) return [];
6581
+ const t = loadTokenizer();
6582
+ const ids = [];
6583
+ const process2 = (segment) => {
6584
+ if (!segment) return;
6585
+ let chunks = [segment];
6586
+ for (const re of t.splitRegexes) chunks = applySplit(chunks, re);
6587
+ for (const chunk of chunks) {
6588
+ if (!chunk) continue;
6589
+ const byteLevel = byteLevelEncode(chunk, t.byteToChar);
6590
+ const pieces = bpeEncode(byteLevel, t.mergeRank);
6591
+ for (const p of pieces) {
6592
+ const id = t.vocab[p];
6593
+ if (id !== void 0) ids.push(id);
6594
+ }
6595
+ }
6596
+ };
6597
+ if (t.addedPattern) {
6598
+ t.addedPattern.lastIndex = 0;
6599
+ let last = 0;
6600
+ for (const m of text.matchAll(t.addedPattern)) {
6601
+ const idx = m.index ?? 0;
6602
+ if (idx > last) process2(text.slice(last, idx));
6603
+ const id = t.addedMap.get(m[0]);
6604
+ if (id !== void 0) ids.push(id);
6605
+ last = idx + m[0].length;
6606
+ }
6607
+ if (last < text.length) process2(text.slice(last));
6608
+ } else {
6609
+ process2(text);
6610
+ }
6611
+ return ids;
6612
+ }
6613
+ function countTokens(text) {
6614
+ return encode(text).length;
6615
+ }
6616
+
6211
6617
  // src/cli/commands/stats.ts
6212
- import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
6618
+ import { existsSync as existsSync7, readFileSync as readFileSync11 } from "fs";
6213
6619
  function statsCommand(opts) {
6214
6620
  if (opts.transcript) {
6215
6621
  transcriptSummary(opts.transcript);
@@ -6222,7 +6628,7 @@ function transcriptSummary(path) {
6222
6628
  console.error(`no such transcript: ${path}`);
6223
6629
  process.exit(1);
6224
6630
  }
6225
- const lines = readFileSync10(path, "utf8").split(/\r?\n/).filter(Boolean);
6631
+ const lines = readFileSync11(path, "utf8").split(/\r?\n/).filter(Boolean);
6226
6632
  let assistantTurns = 0;
6227
6633
  let toolCalls = 0;
6228
6634
  let lastTurn = 0;
@@ -6324,6 +6730,7 @@ var SLASH_COMMANDS = [
6324
6730
  summary: "one-tap model + harvest + branch bundle"
6325
6731
  },
6326
6732
  { cmd: "model", argsHint: "<id>", summary: "switch DeepSeek model id" },
6733
+ { cmd: "models", summary: "list available models fetched from DeepSeek /models" },
6327
6734
  { cmd: "harvest", argsHint: "[on|off]", summary: "toggle Pillar-2 plan-state extraction" },
6328
6735
  { cmd: "branch", argsHint: "<N|off>", summary: "run N parallel samples per turn (N>=2)" },
6329
6736
  { cmd: "mcp", summary: "list MCP servers + tools attached to this session" },
@@ -6352,6 +6759,10 @@ var SLASH_COMMANDS = [
6352
6759
  summary: "cross-session cost dashboard (today / week / month / all-time \xB7 cache hit \xB7 vs Claude)"
6353
6760
  },
6354
6761
  { cmd: "think", summary: "dump the last turn's full R1 reasoning (reasoner only)" },
6762
+ {
6763
+ cmd: "context",
6764
+ summary: "break down where context tokens are going: system / tools / per-turn log"
6765
+ },
6355
6766
  { cmd: "retry", summary: "truncate & resend your last message (fresh sample)" },
6356
6767
  { cmd: "compact", argsHint: "[cap]", summary: "shrink oversized tool results in the log" },
6357
6768
  { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
@@ -6696,6 +7107,66 @@ ${entry.text}`
6696
7107
  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?)`
6697
7108
  };
6698
7109
  }
7110
+ case "context": {
7111
+ const systemTokens = countTokens(loop.prefix.system);
7112
+ const toolsTokens = countTokens(JSON.stringify(loop.prefix.toolSpecs));
7113
+ const entries = loop.log.toMessages();
7114
+ let userTokens = 0;
7115
+ let assistantTokens = 0;
7116
+ let toolResultTokens = 0;
7117
+ let toolCallTokens = 0;
7118
+ const toolBreakdown = [];
7119
+ let logTurn = 0;
7120
+ for (const e of entries) {
7121
+ const content = typeof e.content === "string" ? e.content : "";
7122
+ if (e.role === "user") {
7123
+ userTokens += countTokens(content);
7124
+ logTurn += 1;
7125
+ } else if (e.role === "assistant") {
7126
+ assistantTokens += countTokens(content);
7127
+ if (Array.isArray(e.tool_calls) && e.tool_calls.length > 0) {
7128
+ toolCallTokens += countTokens(JSON.stringify(e.tool_calls));
7129
+ }
7130
+ } else if (e.role === "tool") {
7131
+ const n = countTokens(content);
7132
+ toolResultTokens += n;
7133
+ toolBreakdown.push({ name: e.name ?? "?", tokens: n, turn: logTurn });
7134
+ }
7135
+ }
7136
+ const logTokens = userTokens + assistantTokens + toolResultTokens + toolCallTokens;
7137
+ const total = systemTokens + toolsTokens + logTokens;
7138
+ const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
7139
+ const pct2 = (n) => total > 0 ? `${Math.round(n / total * 100)}%`.padStart(4) : " 0%";
7140
+ const row2 = (label, n, note = "") => ` ${label.padEnd(20)}${compactNum(n).padStart(8)} tokens ${pct2(n)}${note ? ` ${note}` : ""}`;
7141
+ const lines = [
7142
+ `Next-request estimate: ~${compactNum(total)} tokens of ${compactNum(ctxMax)} (${Math.round(
7143
+ total / ctxMax * 100
7144
+ )}% of window)`,
7145
+ "",
7146
+ row2("system prompt", systemTokens),
7147
+ row2("tool specs", toolsTokens, `(${loop.prefix.toolSpecs.length} tools)`),
7148
+ row2("log (all turns)", logTokens, `(${entries.length} messages)`),
7149
+ ` user ${compactNum(userTokens).padStart(8)} tokens`,
7150
+ ` assistant ${compactNum(assistantTokens).padStart(8)} tokens`,
7151
+ ` tool-call args ${compactNum(toolCallTokens).padStart(8)} tokens`,
7152
+ ` tool results ${compactNum(toolResultTokens).padStart(8)} tokens`
7153
+ ];
7154
+ if (toolBreakdown.length > 0) {
7155
+ const top = [...toolBreakdown].sort((a, b) => b.tokens - a.tokens).slice(0, 5);
7156
+ lines.push("");
7157
+ lines.push(`Top tool results by cost (of ${toolBreakdown.length} total):`);
7158
+ for (const t of top) {
7159
+ lines.push(
7160
+ ` turn ${String(t.turn).padStart(3)} ${t.name.padEnd(22)} ${compactNum(t.tokens).padStart(8)} tokens`
7161
+ );
7162
+ }
7163
+ }
7164
+ lines.push("");
7165
+ lines.push(
7166
+ "Count is a local estimate (DeepSeek V3 tokenizer, pure-TS port); server prompt_tokens may add ~3-6% for chat-template role markers."
7167
+ );
7168
+ return { info: lines.join("\n") };
7169
+ }
6699
7170
  case "status": {
6700
7171
  const branchBudget = loop.branchOptions.budget ?? 1;
6701
7172
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
@@ -6722,10 +7193,44 @@ ${entry.text}`
6722
7193
  }
6723
7194
  case "model": {
6724
7195
  const id = args[0];
6725
- if (!id) return { info: "usage: /model <id> (try deepseek-chat or deepseek-reasoner)" };
7196
+ const known = ctx.models ?? null;
7197
+ if (!id) {
7198
+ const hint = known && known.length > 0 ? known.join(" | ") : "try deepseek-chat or deepseek-reasoner \u2014 run /models to fetch the live list";
7199
+ return { info: `usage: /model <id> (${hint})` };
7200
+ }
6726
7201
  loop.configure({ model: id });
7202
+ if (known && known.length > 0 && !known.includes(id)) {
7203
+ return {
7204
+ info: `model \u2192 ${id} (\u26A0 not in the fetched catalog: ${known.join(", ")}. If this is wrong the next call will 400 \u2014 run /models to refresh.)`
7205
+ };
7206
+ }
6727
7207
  return { info: `model \u2192 ${id}` };
6728
7208
  }
7209
+ case "models": {
7210
+ const list = ctx.models ?? null;
7211
+ if (list === null) {
7212
+ ctx.refreshModels?.();
7213
+ return {
7214
+ info: "fetching /models from DeepSeek\u2026 run /models again in a moment. If it stays empty, your API key may lack permission or the network is blocked."
7215
+ };
7216
+ }
7217
+ if (list.length === 0) {
7218
+ return {
7219
+ info: "DeepSeek /models returned an empty list. Try /models again, or check your account status at api-docs.deepseek.com."
7220
+ };
7221
+ }
7222
+ const current = loop.model;
7223
+ const lines = list.map((id) => id === current ? `\u25B8 ${id} (current)` : ` ${id}`);
7224
+ return {
7225
+ info: [
7226
+ `Available models (DeepSeek /models \xB7 ${list.length} total):`,
7227
+ "",
7228
+ ...lines,
7229
+ "",
7230
+ "Switch with: /model <id>"
7231
+ ].join("\n")
7232
+ };
7233
+ }
6729
7234
  case "harvest": {
6730
7235
  const arg = (args[0] ?? "").toLowerCase();
6731
7236
  const on = arg === "" ? !loop.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
@@ -7137,9 +7642,9 @@ function formatToolList(history) {
7137
7642
  return lines.join("\n");
7138
7643
  }
7139
7644
  function compactNum(n) {
7140
- if (n < 1e3) return String(n);
7141
- const k = n / 1e3;
7142
- return k >= 100 ? `${Math.round(k)}k` : `${k.toFixed(1)}k`;
7645
+ if (n < 1024) return String(n);
7646
+ const k = n / 1024;
7647
+ return k >= 100 ? `${Math.round(k)}K` : `${k.toFixed(1)}K`;
7143
7648
  }
7144
7649
  function stripOuterQuotes(s) {
7145
7650
  if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
@@ -7201,6 +7706,7 @@ function App({
7201
7706
  const [subagentActivity, setSubagentActivity] = useState5(null);
7202
7707
  const [statusLine, setStatusLine] = useState5(null);
7203
7708
  const [balance, setBalance] = useState5(null);
7709
+ const [models, setModels] = useState5(null);
7204
7710
  const [latestVersion, setLatestVersion] = useState5(null);
7205
7711
  const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
7206
7712
  const [hookList, setHookList] = useState5(
@@ -7312,6 +7818,17 @@ function App({
7312
7818
  cancelled = true;
7313
7819
  };
7314
7820
  }, [loop]);
7821
+ useEffect2(() => {
7822
+ let cancelled = false;
7823
+ void (async () => {
7824
+ const list = await loop.client.listModels().catch(() => null);
7825
+ if (cancelled || !list) return;
7826
+ setModels(list.data.map((m) => m.id));
7827
+ })();
7828
+ return () => {
7829
+ cancelled = true;
7830
+ };
7831
+ }, [loop]);
7315
7832
  useEffect2(() => {
7316
7833
  let cancelled = false;
7317
7834
  void (async () => {
@@ -7356,7 +7873,7 @@ function App({
7356
7873
  }
7357
7874
  setSubagentActivity(null);
7358
7875
  const seconds = ((ev.elapsedMs ?? 0) / 1e3).toFixed(1);
7359
- const summary2 = ev.error ? `\u{1F9EC} subagent "${ev.task}" failed after ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \u2014 ${ev.error}` : `\u{1F9EC} subagent "${ev.task}" done in ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \xB7 ${ev.turns ?? 0} turn(s)`;
7876
+ 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)`;
7360
7877
  setHistorical((prev) => [
7361
7878
  ...prev,
7362
7879
  {
@@ -7540,6 +8057,13 @@ function App({
7540
8057
  const fresh = await getLatestVersion({ force: true });
7541
8058
  if (fresh) setLatestVersion(fresh);
7542
8059
  })();
8060
+ },
8061
+ models,
8062
+ refreshModels: () => {
8063
+ void (async () => {
8064
+ const list = await loop.client.listModels().catch(() => null);
8065
+ if (list) setModels(list.data.map((m) => m.id));
8066
+ })();
7543
8067
  }
7544
8068
  });
7545
8069
  if (result.exit) {
@@ -7596,7 +8120,14 @@ function App({
7596
8120
  if (promptReport.blocked) return;
7597
8121
  }
7598
8122
  promptHistory.current.push(text);
7599
- setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
8123
+ setHistorical((prev) => [
8124
+ ...prev,
8125
+ // `leadSeparator`: thin rule above this user turn when history
8126
+ // isn't empty — visual pacing for multi-turn sessions. First
8127
+ // user message leaves it off so the UI doesn't open with a
8128
+ // dangling divider.
8129
+ { id: `u-${Date.now()}`, role: "user", text, leadSeparator: prev.length > 0 }
8130
+ ]);
7600
8131
  const assistantId = `a-${Date.now()}`;
7601
8132
  const streamRef = { id: assistantId, text: "", reasoning: "" };
7602
8133
  const contentBuf = { current: "" };
@@ -7815,6 +8346,7 @@ function App({
7815
8346
  latestVersion,
7816
8347
  mcpSpecs,
7817
8348
  mcpServers,
8349
+ models,
7818
8350
  planMode,
7819
8351
  session,
7820
8352
  slashSelected,
@@ -7978,6 +8510,7 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
7978
8510
  branchBudget: loop.branchOptions.budget,
7979
8511
  planMode,
7980
8512
  balance,
8513
+ busy,
7981
8514
  updateAvailable
7982
8515
  }
7983
8516
  ), /* @__PURE__ */ React12.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React12.createElement(EventRow, { key: item.id, event: item, projectRoot: hookCwd })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && streaming ? /* @__PURE__ */ React12.createElement(Box11, { marginY: 1 }, /* @__PURE__ */ React12.createElement(EventRow, { event: streaming, projectRoot: hookCwd })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && ongoingTool ? /* @__PURE__ */ React12.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && subagentActivity ? /* @__PURE__ */ React12.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !ongoingTool && statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React12.createElement(
@@ -8015,7 +8548,7 @@ function SubagentRow({
8015
8548
  }) {
8016
8549
  const tick = useTick();
8017
8550
  const seconds = (activity.elapsedMs / 1e3).toFixed(1);
8018
- return /* @__PURE__ */ React12.createElement(Box11, { paddingLeft: 2 }, /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, ` \u{1F9EC} subagent: ${activity.task}`), /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, ` \xB7 iter ${activity.iter} \xB7 ${seconds}s`));
8551
+ return /* @__PURE__ */ React12.createElement(Box11, { paddingLeft: 2 }, /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, ` \u232C subagent: ${activity.task}`), /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, ` \xB7 iter ${activity.iter} \xB7 ${seconds}s`));
8019
8552
  }
8020
8553
  function OngoingToolRow({
8021
8554
  tool,
@@ -8396,8 +8929,7 @@ async function codeCommand(opts = {}) {
8396
8929
  );
8397
8930
  await chatCommand({
8398
8931
  model: opts.model ?? "deepseek-reasoner",
8399
- harvest: true,
8400
- // smart preset's harvest setting, always on for code
8932
+ harvest: opts.harvest ?? false,
8401
8933
  system: codeSystemPrompt2(rootDir),
8402
8934
  transcript: opts.transcript,
8403
8935
  session,
@@ -9517,15 +10049,19 @@ program.command("setup").description("Interactive wizard \u2014 API key, preset,
9517
10049
  await setupCommand({});
9518
10050
  });
9519
10051
  program.command("code [dir]").description(
9520
- "Code-editing chat \u2014 filesystem MCP auto-bridged at <dir> (default: cwd), coding system prompt, smart preset. Model proposes SEARCH/REPLACE blocks; Reasonix applies them to disk."
9521
- ).option("-m, --model <id>", "Override default reasoner model").option("--no-session", "Disable session persistence for this run").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option("-n, --new", "Skip the session picker \u2014 always wipe prior messages and start fresh").option("--transcript <path>", "Write a JSONL transcript to this path").action(async (dir, opts) => {
10052
+ "Code-editing chat \u2014 filesystem tools rooted at <dir> (default: cwd), coding system prompt, deepseek-reasoner. Model proposes SEARCH/REPLACE blocks; Reasonix applies them to disk."
10053
+ ).option("-m, --model <id>", "Override default reasoner model").option("--no-session", "Disable session persistence for this run").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option("-n, --new", "Skip the session picker \u2014 always wipe prior messages and start fresh").option("--transcript <path>", "Write a JSONL transcript to this path").option(
10054
+ "--harvest",
10055
+ "Extract typed plan state from R1 reasoning (Pillar 2). Adds ~10-15% cost per turn. Off by default in code mode."
10056
+ ).action(async (dir, opts) => {
9522
10057
  await codeCommand({
9523
10058
  dir,
9524
10059
  model: opts.model,
9525
10060
  noSession: opts.session === false,
9526
10061
  transcript: opts.transcript,
9527
10062
  forceResume: !!opts.resume,
9528
- forceNew: !!opts.new
10063
+ forceNew: !!opts.new,
10064
+ harvest: !!opts.harvest
9529
10065
  });
9530
10066
  });
9531
10067
  program.command("chat").description("Interactive Ink TUI with live cache/cost panel.").option("-m, --model <id>", "DeepSeek model id (overrides preset)").option("-s, --system <prompt>", "System prompt (pinned in the immutable prefix)", DEFAULT_SYSTEM).option("--transcript <path>", "Write a JSONL transcript to this path").option(