reasonix 0.4.28 → 0.5.2

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);
@@ -674,6 +696,170 @@ async function runHooks(opts) {
674
696
  return { event, outcomes, blocked };
675
697
  }
676
698
 
699
+ // src/tokenizer.ts
700
+ import { readFileSync as readFileSync3 } from "fs";
701
+ import { createRequire } from "module";
702
+ import { dirname as dirname2, join as join3 } from "path";
703
+ import { fileURLToPath } from "url";
704
+ import { gunzipSync } from "zlib";
705
+ function buildByteToChar() {
706
+ const result = new Array(256);
707
+ const bs = [];
708
+ for (let b = 33; b <= 126; b++) bs.push(b);
709
+ for (let b = 161; b <= 172; b++) bs.push(b);
710
+ for (let b = 174; b <= 255; b++) bs.push(b);
711
+ const cs = bs.slice();
712
+ let n = 0;
713
+ for (let b = 0; b < 256; b++) {
714
+ if (!bs.includes(b)) {
715
+ bs.push(b);
716
+ cs.push(256 + n);
717
+ n++;
718
+ }
719
+ }
720
+ for (let i = 0; i < bs.length; i++) {
721
+ result[bs[i]] = String.fromCodePoint(cs[i]);
722
+ }
723
+ return result;
724
+ }
725
+ var cached = null;
726
+ function resolveDataPath() {
727
+ if (process.env.REASONIX_TOKENIZER_PATH) return process.env.REASONIX_TOKENIZER_PATH;
728
+ try {
729
+ const here = dirname2(fileURLToPath(import.meta.url));
730
+ return join3(here, "..", "data", "deepseek-tokenizer.json.gz");
731
+ } catch {
732
+ const req = createRequire(import.meta.url);
733
+ return join3(
734
+ dirname2(req.resolve("reasonix/package.json")),
735
+ "data",
736
+ "deepseek-tokenizer.json.gz"
737
+ );
738
+ }
739
+ }
740
+ function loadTokenizer() {
741
+ if (cached) return cached;
742
+ const buf = readFileSync3(resolveDataPath());
743
+ const json = gunzipSync(buf).toString("utf8");
744
+ const data = JSON.parse(json);
745
+ const mergeRank = /* @__PURE__ */ new Map();
746
+ for (let i = 0; i < data.model.merges.length; i++) {
747
+ mergeRank.set(data.model.merges[i], i);
748
+ }
749
+ const splitRegexes = [];
750
+ for (const p of data.pre_tokenizer.pretokenizers) {
751
+ if (p.type === "Split") {
752
+ splitRegexes.push(new RegExp(p.pattern.Regex, "gu"));
753
+ }
754
+ }
755
+ const addedMap = /* @__PURE__ */ new Map();
756
+ const addedContents = [];
757
+ for (const t of data.added_tokens) {
758
+ if (!t.special) {
759
+ addedMap.set(t.content, t.id);
760
+ addedContents.push(t.content);
761
+ }
762
+ }
763
+ addedContents.sort((a, b) => b.length - a.length);
764
+ const addedPattern = addedContents.length ? new RegExp(addedContents.map(escapeRegex).join("|"), "g") : null;
765
+ cached = {
766
+ vocab: data.model.vocab,
767
+ mergeRank,
768
+ splitRegexes,
769
+ byteToChar: buildByteToChar(),
770
+ addedPattern,
771
+ addedMap
772
+ };
773
+ return cached;
774
+ }
775
+ function escapeRegex(s) {
776
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
777
+ }
778
+ function applySplit(chunks, re) {
779
+ const out = [];
780
+ for (const chunk of chunks) {
781
+ if (!chunk) continue;
782
+ re.lastIndex = 0;
783
+ let last = 0;
784
+ for (const m of chunk.matchAll(re)) {
785
+ const idx = m.index ?? 0;
786
+ if (idx > last) out.push(chunk.slice(last, idx));
787
+ if (m[0].length > 0) out.push(m[0]);
788
+ last = idx + m[0].length;
789
+ }
790
+ if (last < chunk.length) out.push(chunk.slice(last));
791
+ }
792
+ return out;
793
+ }
794
+ function byteLevelEncode(s, byteToChar) {
795
+ const bytes = new TextEncoder().encode(s);
796
+ let out = "";
797
+ for (let i = 0; i < bytes.length; i++) out += byteToChar[bytes[i]];
798
+ return out;
799
+ }
800
+ function bpeEncode(piece, mergeRank) {
801
+ if (piece.length <= 1) return piece ? [piece] : [];
802
+ let word = Array.from(piece);
803
+ while (true) {
804
+ let bestIdx = -1;
805
+ let bestRank = Number.POSITIVE_INFINITY;
806
+ for (let i = 0; i < word.length - 1; i++) {
807
+ const pair = `${word[i]} ${word[i + 1]}`;
808
+ const rank = mergeRank.get(pair);
809
+ if (rank !== void 0 && rank < bestRank) {
810
+ bestRank = rank;
811
+ bestIdx = i;
812
+ if (rank === 0) break;
813
+ }
814
+ }
815
+ if (bestIdx < 0) break;
816
+ word = [
817
+ ...word.slice(0, bestIdx),
818
+ word[bestIdx] + word[bestIdx + 1],
819
+ ...word.slice(bestIdx + 2)
820
+ ];
821
+ if (word.length === 1) break;
822
+ }
823
+ return word;
824
+ }
825
+ function encode(text) {
826
+ if (!text) return [];
827
+ const t = loadTokenizer();
828
+ const ids = [];
829
+ const process2 = (segment) => {
830
+ if (!segment) return;
831
+ let chunks = [segment];
832
+ for (const re of t.splitRegexes) chunks = applySplit(chunks, re);
833
+ for (const chunk of chunks) {
834
+ if (!chunk) continue;
835
+ const byteLevel = byteLevelEncode(chunk, t.byteToChar);
836
+ const pieces = bpeEncode(byteLevel, t.mergeRank);
837
+ for (const p of pieces) {
838
+ const id = t.vocab[p];
839
+ if (id !== void 0) ids.push(id);
840
+ }
841
+ }
842
+ };
843
+ if (t.addedPattern) {
844
+ t.addedPattern.lastIndex = 0;
845
+ let last = 0;
846
+ for (const m of text.matchAll(t.addedPattern)) {
847
+ const idx = m.index ?? 0;
848
+ if (idx > last) process2(text.slice(last, idx));
849
+ const id = t.addedMap.get(m[0]);
850
+ if (id !== void 0) ids.push(id);
851
+ last = idx + m[0].length;
852
+ }
853
+ if (last < text.length) process2(text.slice(last));
854
+ } else {
855
+ process2(text);
856
+ }
857
+ return ids;
858
+ }
859
+ function countTokens(text) {
860
+ return encode(text).length;
861
+ }
862
+
677
863
  // src/repair/flatten.ts
678
864
  function analyzeSchema(schema) {
679
865
  if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
@@ -823,7 +1009,15 @@ var ToolRegistry = class {
823
1009
  }
824
1010
  try {
825
1011
  const result = await tool.fn(args, { signal: opts.signal });
826
- return typeof result === "string" ? result : JSON.stringify(result);
1012
+ const str = typeof result === "string" ? result : JSON.stringify(result);
1013
+ let clipped = str;
1014
+ if (opts.maxResultTokens !== void 0) {
1015
+ clipped = truncateForModelByTokens(clipped, opts.maxResultTokens);
1016
+ }
1017
+ if (opts.maxResultChars !== void 0) {
1018
+ clipped = truncateForModel(clipped, opts.maxResultChars);
1019
+ }
1020
+ return clipped;
827
1021
  } catch (err) {
828
1022
  const e = err;
829
1023
  if (typeof e.toToolResult === "function") {
@@ -857,6 +1051,7 @@ function hasDotKey(obj) {
857
1051
 
858
1052
  // src/mcp/registry.ts
859
1053
  var DEFAULT_MAX_RESULT_CHARS = 32e3;
1054
+ var DEFAULT_MAX_RESULT_TOKENS = 8e3;
860
1055
  async function bridgeMcpTools(client, opts = {}) {
861
1056
  const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
862
1057
  const prefix = opts.namePrefix ?? "";
@@ -913,6 +1108,61 @@ function truncateForModel(s, maxChars) {
913
1108
 
914
1109
  ${tail}`;
915
1110
  }
1111
+ function truncateForModelByTokens(s, maxTokens) {
1112
+ if (maxTokens <= 0) return "";
1113
+ if (s.length <= maxTokens) return s;
1114
+ if (s.length <= maxTokens * 4) {
1115
+ const tokens = countTokens(s);
1116
+ if (tokens <= maxTokens) return s;
1117
+ }
1118
+ const markerOverhead = 48;
1119
+ const contentBudget = Math.max(0, maxTokens - markerOverhead);
1120
+ const tailBudget = Math.min(256, Math.floor(contentBudget * 0.1));
1121
+ const headBudget = Math.max(0, contentBudget - tailBudget);
1122
+ const head = sizePrefixToTokens(s, headBudget);
1123
+ const tail = sizeSuffixToTokens(s, tailBudget);
1124
+ const droppedChars = s.length - head.length - tail.length;
1125
+ const headTokens = head ? countTokens(head) : 0;
1126
+ const tailTokens = tail ? countTokens(tail) : 0;
1127
+ const sampleChars = head.length + tail.length;
1128
+ const sampleTokens = headTokens + tailTokens;
1129
+ const ratio = sampleChars > 0 ? sampleTokens / sampleChars : 0.3;
1130
+ const estTotalTokens = Math.ceil(s.length * ratio);
1131
+ const droppedTokens = Math.max(0, estTotalTokens - sampleTokens);
1132
+ return `${head}
1133
+
1134
+ [\u2026truncated ~${droppedTokens} tokens (${droppedChars} chars) \u2014 raise BridgeOptions.maxResultTokens, or call the tool with a narrower scope (filter, head, pagination)\u2026]
1135
+
1136
+ ${tail}`;
1137
+ }
1138
+ function sizePrefixToTokens(s, budget) {
1139
+ if (budget <= 0 || s.length === 0) return "";
1140
+ let size = Math.min(s.length, budget * 4);
1141
+ for (let iter = 0; iter < 6; iter++) {
1142
+ if (size <= 0) return "";
1143
+ const slice = s.slice(0, size);
1144
+ const count = countTokens(slice);
1145
+ if (count <= budget) return slice;
1146
+ const next = Math.floor(size * (budget / count) * 0.95);
1147
+ if (next >= size) return s.slice(0, Math.max(0, size - 1));
1148
+ size = next;
1149
+ }
1150
+ return s.slice(0, Math.max(0, size));
1151
+ }
1152
+ function sizeSuffixToTokens(s, budget) {
1153
+ if (budget <= 0 || s.length === 0) return "";
1154
+ let size = Math.min(s.length, budget * 4);
1155
+ for (let iter = 0; iter < 6; iter++) {
1156
+ if (size <= 0) return "";
1157
+ const slice = s.slice(-size);
1158
+ const count = countTokens(slice);
1159
+ if (count <= budget) return slice;
1160
+ const next = Math.floor(size * (budget / count) * 0.95);
1161
+ if (next >= size) return s.slice(-Math.max(0, size - 1));
1162
+ size = next;
1163
+ }
1164
+ return s.slice(-Math.max(0, size));
1165
+ }
916
1166
  function blockToString(block) {
917
1167
  if (block.type === "text") return block.text;
918
1168
  if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
@@ -1298,19 +1548,19 @@ import {
1298
1548
  chmodSync as chmodSync2,
1299
1549
  existsSync as existsSync2,
1300
1550
  mkdirSync as mkdirSync2,
1301
- readFileSync as readFileSync3,
1551
+ readFileSync as readFileSync4,
1302
1552
  readdirSync,
1303
1553
  statSync,
1304
1554
  unlinkSync,
1305
1555
  writeFileSync as writeFileSync2
1306
1556
  } from "fs";
1307
1557
  import { homedir as homedir3 } from "os";
1308
- import { dirname as dirname2, join as join3 } from "path";
1558
+ import { dirname as dirname3, join as join4 } from "path";
1309
1559
  function sessionsDir() {
1310
- return join3(homedir3(), ".reasonix", "sessions");
1560
+ return join4(homedir3(), ".reasonix", "sessions");
1311
1561
  }
1312
1562
  function sessionPath(name) {
1313
- return join3(sessionsDir(), `${sanitizeName(name)}.jsonl`);
1563
+ return join4(sessionsDir(), `${sanitizeName(name)}.jsonl`);
1314
1564
  }
1315
1565
  function sanitizeName(name) {
1316
1566
  const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
@@ -1320,7 +1570,7 @@ function loadSessionMessages(name) {
1320
1570
  const path = sessionPath(name);
1321
1571
  if (!existsSync2(path)) return [];
1322
1572
  try {
1323
- const raw = readFileSync3(path, "utf8");
1573
+ const raw = readFileSync4(path, "utf8");
1324
1574
  const out = [];
1325
1575
  for (const line of raw.split(/\r?\n/)) {
1326
1576
  const trimmed = line.trim();
@@ -1338,7 +1588,7 @@ function loadSessionMessages(name) {
1338
1588
  }
1339
1589
  function appendSessionMessage(name, message) {
1340
1590
  const path = sessionPath(name);
1341
- mkdirSync2(dirname2(path), { recursive: true });
1591
+ mkdirSync2(dirname3(path), { recursive: true });
1342
1592
  appendFileSync(path, `${JSON.stringify(message)}
1343
1593
  `, "utf8");
1344
1594
  try {
@@ -1352,7 +1602,7 @@ function listSessions() {
1352
1602
  try {
1353
1603
  const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
1354
1604
  return files.map((file) => {
1355
- const path = join3(dir, file);
1605
+ const path = join4(dir, file);
1356
1606
  const stat = statSync(path);
1357
1607
  const name = file.replace(/\.jsonl$/, "");
1358
1608
  const messageCount = countLines(path);
@@ -1373,7 +1623,7 @@ function deleteSession(name) {
1373
1623
  }
1374
1624
  function rewriteSession(name, messages) {
1375
1625
  const path = sessionPath(name);
1376
- mkdirSync2(dirname2(path), { recursive: true });
1626
+ mkdirSync2(dirname3(path), { recursive: true });
1377
1627
  const body = messages.map((m) => JSON.stringify(m)).join("\n");
1378
1628
  writeFileSync2(path, body ? `${body}
1379
1629
  ` : "", "utf8");
@@ -1384,7 +1634,7 @@ function rewriteSession(name, messages) {
1384
1634
  }
1385
1635
  function countLines(path) {
1386
1636
  try {
1387
- const raw = readFileSync3(path, "utf8");
1637
+ const raw = readFileSync4(path, "utf8");
1388
1638
  return raw.split(/\r?\n/).filter((l) => l.trim()).length;
1389
1639
  } catch {
1390
1640
  return 0;
@@ -1393,8 +1643,8 @@ function countLines(path) {
1393
1643
 
1394
1644
  // src/telemetry.ts
1395
1645
  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 }
1646
+ "deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 },
1647
+ "deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 }
1398
1648
  };
1399
1649
  var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
1400
1650
  var DEEPSEEK_CONTEXT_TOKENS = {
@@ -1949,6 +2199,24 @@ var CacheFirstLoop = class {
1949
2199
  return;
1950
2200
  }
1951
2201
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
2202
+ if (usage) {
2203
+ const ratio = usage.promptTokens / ctxMax;
2204
+ if (ratio > 0.6 && ratio <= 0.8) {
2205
+ const before = usage.promptTokens;
2206
+ const soft = this.compact(16e3);
2207
+ if (soft.healedCount > 0) {
2208
+ const approxSaved = Math.round(soft.charsSaved / 4);
2209
+ const after = Math.max(0, before - approxSaved);
2210
+ yield {
2211
+ turn: this._turn,
2212
+ role: "warning",
2213
+ content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
2214
+ ratio * 100
2215
+ )}%) \u2014 proactively compacted ${soft.healedCount} tool result(s) to 16k, saved ~${approxSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Staying ahead of the 80% guard.`
2216
+ };
2217
+ }
2218
+ }
2219
+ }
1952
2220
  if (usage && usage.promptTokens / ctxMax > 0.8) {
1953
2221
  const before = usage.promptTokens;
1954
2222
  const compactResult = this.compact(4e3);
@@ -2011,7 +2279,10 @@ var CacheFirstLoop = class {
2011
2279
  result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
2012
2280
  ${reason}`;
2013
2281
  } else {
2014
- result = await this.tools.dispatch(name, args, { signal });
2282
+ result = await this.tools.dispatch(name, args, {
2283
+ signal,
2284
+ maxResultTokens: DEFAULT_MAX_RESULT_TOKENS
2285
+ });
2015
2286
  const postReport = await runHooks({
2016
2287
  hooks: this.hooks,
2017
2288
  payload: {
@@ -3118,6 +3389,50 @@ function tokenizeCommand(cmd) {
3118
3389
  if (cur.length > 0) out.push(cur);
3119
3390
  return out;
3120
3391
  }
3392
+ function detectShellOperator(cmd) {
3393
+ const opPrefix = /^(?:2>&1|&>|\|{1,2}|&{1,2}|2>{1,2}|>{1,2}|<{1,2})/;
3394
+ let cur = "";
3395
+ let curQuoted = false;
3396
+ let quote = null;
3397
+ const check = () => {
3398
+ if (cur.length === 0 && !curQuoted) return null;
3399
+ if (!curQuoted) {
3400
+ const m = opPrefix.exec(cur);
3401
+ if (m) return m[0] ?? null;
3402
+ }
3403
+ return null;
3404
+ };
3405
+ for (let i = 0; i < cmd.length; i++) {
3406
+ const ch = cmd[i];
3407
+ if (quote) {
3408
+ if (ch === quote) {
3409
+ quote = null;
3410
+ } else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
3411
+ cur += cmd[++i];
3412
+ curQuoted = true;
3413
+ } else {
3414
+ cur += ch;
3415
+ curQuoted = true;
3416
+ }
3417
+ continue;
3418
+ }
3419
+ if (ch === '"' || ch === "'") {
3420
+ quote = ch;
3421
+ curQuoted = true;
3422
+ continue;
3423
+ }
3424
+ if (ch === " " || ch === " ") {
3425
+ const op = check();
3426
+ if (op) return op;
3427
+ cur = "";
3428
+ curQuoted = false;
3429
+ continue;
3430
+ }
3431
+ cur += ch;
3432
+ }
3433
+ if (quote) return null;
3434
+ return check();
3435
+ }
3121
3436
  function isAllowed(cmd, extra = []) {
3122
3437
  const normalized = cmd.trim().replace(/\s+/g, " ");
3123
3438
  const allowlist = [...BUILTIN_ALLOWLIST, ...extra];
@@ -3130,6 +3445,12 @@ function isAllowed(cmd, extra = []) {
3130
3445
  async function runCommand(cmd, opts) {
3131
3446
  const argv = tokenizeCommand(cmd);
3132
3447
  if (argv.length === 0) throw new Error("run_command: empty command");
3448
+ const operator = detectShellOperator(cmd);
3449
+ if (operator !== null) {
3450
+ throw new Error(
3451
+ `run_command: shell operator "${operator}" is not supported \u2014 this tool spawns one process, no shell expansion. Split into separate run_command calls and combine the output in your reasoning (e.g. instead of \`grep foo *.ts | wc -l\`, call \`grep -c foo *.ts\` or two separate commands). To pass "${operator}" as a literal argument, wrap it in quotes.`
3452
+ );
3453
+ }
3133
3454
  const timeoutMs = (opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
3134
3455
  const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
3135
3456
  const spawnOpts = {
@@ -3307,7 +3628,7 @@ function registerShellTools(registry, opts) {
3307
3628
  properties: {
3308
3629
  command: {
3309
3630
  type: "string",
3310
- description: "Full command line. Tokenized with POSIX-ish quoting; no shell expansion, no pipes, no redirects."
3631
+ description: 'Full command line. Tokenized with POSIX-ish quoting; no shell expansion. Pipes (`|`), redirects (`>`, `<`, `2>`), and `&&`/`||` chaining are rejected with an error \u2014 split into separate calls instead. To pass an operator character as a literal argument (e.g. a regex), wrap it in quotes: `grep "a|b" file.txt`.'
3311
3632
  },
3312
3633
  timeoutSec: {
3313
3634
  type: "integer",
@@ -3526,12 +3847,12 @@ ${i + 1}. ${r.title}`);
3526
3847
  }
3527
3848
 
3528
3849
  // src/env.ts
3529
- import { readFileSync as readFileSync4 } from "fs";
3850
+ import { readFileSync as readFileSync5 } from "fs";
3530
3851
  import { resolve as resolve3 } from "path";
3531
3852
  function loadDotenv(path = ".env") {
3532
3853
  let raw;
3533
3854
  try {
3534
- raw = readFileSync4(resolve3(process.cwd(), path), "utf8");
3855
+ raw = readFileSync5(resolve3(process.cwd(), path), "utf8");
3535
3856
  } catch {
3536
3857
  return;
3537
3858
  }
@@ -3550,7 +3871,7 @@ function loadDotenv(path = ".env") {
3550
3871
  }
3551
3872
 
3552
3873
  // src/transcript.ts
3553
- import { createWriteStream, readFileSync as readFileSync5 } from "fs";
3874
+ import { createWriteStream, readFileSync as readFileSync6 } from "fs";
3554
3875
  function recordFromLoopEvent(ev, extra) {
3555
3876
  const rec = {
3556
3877
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3601,7 +3922,7 @@ function openTranscriptFile(path, meta) {
3601
3922
  return stream;
3602
3923
  }
3603
3924
  function readTranscript(path) {
3604
- const raw = readFileSync5(path, "utf8");
3925
+ const raw = readFileSync6(path, "utf8");
3605
3926
  return parseTranscript(raw);
3606
3927
  }
3607
3928
  function isPlanStateEmptyShape(s) {
@@ -4687,8 +5008,8 @@ async function trySection(load) {
4687
5008
  }
4688
5009
 
4689
5010
  // src/code/edit-blocks.ts
4690
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
4691
- import { dirname as dirname4, resolve as resolve4 } from "path";
5011
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync7, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
5012
+ import { dirname as dirname5, resolve as resolve4 } from "path";
4692
5013
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
4693
5014
  function parseEditBlocks(text) {
4694
5015
  const out = [];
@@ -4726,11 +5047,11 @@ function applyEditBlock(block, rootDir) {
4726
5047
  message: "file does not exist; to create it, use an empty SEARCH block"
4727
5048
  };
4728
5049
  }
4729
- mkdirSync3(dirname4(absTarget), { recursive: true });
5050
+ mkdirSync3(dirname5(absTarget), { recursive: true });
4730
5051
  writeFileSync3(absTarget, block.replace, "utf8");
4731
5052
  return { path: block.path, status: "created" };
4732
5053
  }
4733
- const content = readFileSync6(absTarget, "utf8");
5054
+ const content = readFileSync7(absTarget, "utf8");
4734
5055
  if (searchEmpty) {
4735
5056
  return {
4736
5057
  path: block.path,
@@ -4769,7 +5090,7 @@ function snapshotBeforeEdits(blocks, rootDir) {
4769
5090
  continue;
4770
5091
  }
4771
5092
  try {
4772
- snapshots.push({ path: b.path, prevContent: readFileSync6(abs, "utf8") });
5093
+ snapshots.push({ path: b.path, prevContent: readFileSync7(abs, "utf8") });
4773
5094
  } catch {
4774
5095
  snapshots.push({ path: b.path, prevContent: null });
4775
5096
  }
@@ -4812,25 +5133,25 @@ function sep() {
4812
5133
  }
4813
5134
 
4814
5135
  // src/version.ts
4815
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
5136
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
4816
5137
  import { homedir as homedir4 } from "os";
4817
- import { dirname as dirname5, join as join5 } from "path";
4818
- import { fileURLToPath } from "url";
5138
+ import { dirname as dirname6, join as join6 } from "path";
5139
+ import { fileURLToPath as fileURLToPath2 } from "url";
4819
5140
  var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
4820
5141
  var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
4821
5142
  var LATEST_FETCH_TIMEOUT_MS = 2e3;
4822
5143
  function readPackageVersion() {
4823
5144
  try {
4824
- let dir = dirname5(fileURLToPath(import.meta.url));
5145
+ let dir = dirname6(fileURLToPath2(import.meta.url));
4825
5146
  for (let i = 0; i < 6; i++) {
4826
- const p = join5(dir, "package.json");
5147
+ const p = join6(dir, "package.json");
4827
5148
  if (existsSync5(p)) {
4828
- const pkg = JSON.parse(readFileSync7(p, "utf8"));
5149
+ const pkg = JSON.parse(readFileSync8(p, "utf8"));
4829
5150
  if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
4830
5151
  return pkg.version;
4831
5152
  }
4832
5153
  }
4833
- const parent = dirname5(dir);
5154
+ const parent = dirname6(dir);
4834
5155
  if (parent === dir) break;
4835
5156
  dir = parent;
4836
5157
  }
@@ -4840,11 +5161,11 @@ function readPackageVersion() {
4840
5161
  }
4841
5162
  var VERSION = readPackageVersion();
4842
5163
  function cachePath(homeDirOverride) {
4843
- return join5(homeDirOverride ?? homedir4(), ".reasonix", "version-cache.json");
5164
+ return join6(homeDirOverride ?? homedir4(), ".reasonix", "version-cache.json");
4844
5165
  }
4845
5166
  function readCache(homeDirOverride) {
4846
5167
  try {
4847
- const raw = readFileSync7(cachePath(homeDirOverride), "utf8");
5168
+ const raw = readFileSync8(cachePath(homeDirOverride), "utf8");
4848
5169
  const parsed = JSON.parse(raw);
4849
5170
  if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
4850
5171
  return parsed;
@@ -4856,7 +5177,7 @@ function readCache(homeDirOverride) {
4856
5177
  function writeCache(entry, homeDirOverride) {
4857
5178
  try {
4858
5179
  const p = cachePath(homeDirOverride);
4859
- mkdirSync4(dirname5(p), { recursive: true });
5180
+ mkdirSync4(dirname6(p), { recursive: true });
4860
5181
  writeFileSync4(p, JSON.stringify(entry), "utf8");
4861
5182
  } catch {
4862
5183
  }
@@ -4864,8 +5185,8 @@ function writeCache(entry, homeDirOverride) {
4864
5185
  async function getLatestVersion(opts = {}) {
4865
5186
  const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
4866
5187
  if (!opts.force) {
4867
- const cached = readCache(opts.homeDir);
4868
- if (cached && Date.now() - cached.checkedAt < ttl) return cached.version;
5188
+ const cached2 = readCache(opts.homeDir);
5189
+ if (cached2 && Date.now() - cached2.checkedAt < ttl) return cached2.version;
4869
5190
  }
4870
5191
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
4871
5192
  if (!fetchImpl) return null;
@@ -4913,11 +5234,11 @@ function isNpxInstall() {
4913
5234
  }
4914
5235
 
4915
5236
  // src/usage.ts
4916
- import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
5237
+ import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
4917
5238
  import { homedir as homedir5 } from "os";
4918
- import { dirname as dirname6, join as join6 } from "path";
5239
+ import { dirname as dirname7, join as join7 } from "path";
4919
5240
  function defaultUsageLogPath(homeDirOverride) {
4920
- return join6(homeDirOverride ?? homedir5(), ".reasonix", "usage.jsonl");
5241
+ return join7(homeDirOverride ?? homedir5(), ".reasonix", "usage.jsonl");
4921
5242
  }
4922
5243
  function appendUsage(input) {
4923
5244
  const record = {
@@ -4933,7 +5254,7 @@ function appendUsage(input) {
4933
5254
  };
4934
5255
  const path = input.path ?? defaultUsageLogPath();
4935
5256
  try {
4936
- mkdirSync5(dirname6(path), { recursive: true });
5257
+ mkdirSync5(dirname7(path), { recursive: true });
4937
5258
  appendFileSync2(path, `${JSON.stringify(record)}
4938
5259
  `, "utf8");
4939
5260
  } catch {
@@ -4944,7 +5265,7 @@ function readUsageLog(path = defaultUsageLogPath()) {
4944
5265
  if (!existsSync6(path)) return [];
4945
5266
  let raw;
4946
5267
  try {
4947
- raw = readFileSync8(path, "utf8");
5268
+ raw = readFileSync9(path, "utf8");
4948
5269
  } catch {
4949
5270
  return [];
4950
5271
  }
@@ -5117,7 +5438,7 @@ ${skill.body}${argsBlock}`;
5117
5438
  }
5118
5439
 
5119
5440
  // src/cli/ui/EventLog.tsx
5120
- import { Box as Box3, Text as Text3 } from "ink";
5441
+ import { Box as Box3, Text as Text3, useStdout } from "ink";
5121
5442
  import React4 from "react";
5122
5443
 
5123
5444
  // src/cli/ui/PlanStateBlock.tsx
@@ -5137,8 +5458,8 @@ function PlanStateBlock({ planState }) {
5137
5458
  }
5138
5459
 
5139
5460
  // src/cli/ui/markdown.tsx
5140
- import { readFileSync as readFileSync9, statSync as statSync4 } from "fs";
5141
- import { isAbsolute as isAbsolute3, join as join7 } from "path";
5461
+ import { readFileSync as readFileSync10, statSync as statSync4 } from "fs";
5462
+ import { isAbsolute as isAbsolute3, join as join8 } from "path";
5142
5463
  import { Box as Box2, Text as Text2 } from "ink";
5143
5464
  import React2 from "react";
5144
5465
  var SUPERSCRIPT = {
@@ -5216,7 +5537,7 @@ function parseCitationUrl(url) {
5216
5537
  function validateCitation(url, projectRoot) {
5217
5538
  const parts = parseCitationUrl(url);
5218
5539
  if (!parts || !parts.path) return { ok: false, reason: "empty path" };
5219
- const fullPath = isAbsolute3(parts.path) ? parts.path : join7(projectRoot, parts.path);
5540
+ const fullPath = isAbsolute3(parts.path) ? parts.path : join8(projectRoot, parts.path);
5220
5541
  let stat;
5221
5542
  try {
5222
5543
  stat = statSync4(fullPath);
@@ -5227,7 +5548,7 @@ function validateCitation(url, projectRoot) {
5227
5548
  if (parts.startLine === void 0) return { ok: true };
5228
5549
  let lineCount;
5229
5550
  try {
5230
- lineCount = readFileSync9(fullPath, "utf8").split("\n").length;
5551
+ lineCount = readFileSync10(fullPath, "utf8").split("\n").length;
5231
5552
  } catch {
5232
5553
  return { ok: false, reason: "unreadable" };
5233
5554
  }
@@ -5277,7 +5598,7 @@ function InlineMd({
5277
5598
  const status = citations?.get(url);
5278
5599
  if (status && !status.ok) {
5279
5600
  parts.push(
5280
- /* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u274C`)
5601
+ /* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u2717`)
5281
5602
  );
5282
5603
  } else {
5283
5604
  parts.push(
@@ -5583,7 +5904,7 @@ function Markdown({ text, projectRoot }) {
5583
5904
  function BrokenCitationsBlock({ items }) {
5584
5905
  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
5906
  // 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}`)
5907
+ /* @__PURE__ */ React2.createElement(Text2, { key: `bc-${i}`, color: "red" }, ` \u2717 ${b.url} \u2192 ${b.reason}`)
5587
5908
  )));
5588
5909
  }
5589
5910
 
@@ -5610,32 +5931,49 @@ function useElapsedSeconds() {
5610
5931
  }
5611
5932
 
5612
5933
  // src/cli/ui/EventLog.tsx
5934
+ var ROLE_GLYPH = {
5935
+ user: "\u25C7",
5936
+ assistant: "\u25C6",
5937
+ assistantPulse: "\u25C7",
5938
+ // pulse alternate for streaming state
5939
+ toolOk: "\u25A3",
5940
+ toolErr: "\u25A5",
5941
+ warning: "\u25B2",
5942
+ error: "\u2726"
5943
+ };
5944
+ function RoleGlyph({
5945
+ glyph,
5946
+ color
5947
+ }) {
5948
+ return /* @__PURE__ */ React4.createElement(Text3, { color, bold: true }, glyph);
5949
+ }
5613
5950
  var EventRow = React4.memo(function EventRow2({
5614
5951
  event,
5615
5952
  projectRoot
5616
5953
  }) {
5617
5954
  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));
5955
+ 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
5956
  }
5620
5957
  if (event.role === "assistant") {
5621
5958
  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);
5959
+ 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
5960
  }
5624
5961
  if (event.role === "tool") {
5625
5962
  const isError = event.text.startsWith("ERROR:");
5626
5963
  const color = isError ? "red" : "yellow";
5964
+ const glyph = isError ? ROLE_GLYPH.toolErr : ROLE_GLYPH.toolOk;
5627
5965
  const marker = isError ? "\u2717" : "\u2192";
5628
5966
  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)));
5967
+ 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
5968
  }
5631
5969
  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));
5970
+ 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
5971
  }
5634
5972
  if (event.role === "info") {
5635
5973
  return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, event.text));
5636
5974
  }
5637
5975
  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));
5976
+ 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
5977
  }
5640
5978
  return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, null, event.text));
5641
5979
  });
@@ -5659,13 +5997,13 @@ function BranchBlock({ branch }) {
5659
5997
  const t = (branch.temperatures[i] ?? 0).toFixed(1);
5660
5998
  return `${marker} #${i} T=${t} u=${u}`;
5661
5999
  }).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)));
6000
+ 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
6001
  }
5664
6002
  function ReasoningBlock({ reasoning }) {
5665
6003
  const max = 260;
5666
6004
  const flat = reasoning.replace(/\s+/g, " ").trim();
5667
6005
  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));
6006
+ 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
6007
  }
5670
6008
  function Elapsed() {
5671
6009
  const s = useElapsedSeconds();
@@ -5673,14 +6011,19 @@ function Elapsed() {
5673
6011
  const ss = String(s % 60).padStart(2, "0");
5674
6012
  return /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, `${mm}:${ss}`);
5675
6013
  }
6014
+ function PulsingAssistantGlyph() {
6015
+ const tick = useTick();
6016
+ const on = Math.floor(tick / 4) % 2 === 0;
6017
+ return /* @__PURE__ */ React4.createElement(Text3, { color: "green", bold: true }, on ? ROLE_GLYPH.assistant : ROLE_GLYPH.assistantPulse);
6018
+ }
5676
6019
  function StreamingAssistant({ event }) {
5677
6020
  if (event.branchProgress) {
5678
6021
  const p = event.branchProgress;
5679
6022
  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"));
6023
+ 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
6024
  }
5682
6025
  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"));
6026
+ 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
6027
  }
5685
6028
  const tail = lastLine(event.text, 140);
5686
6029
  const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
@@ -5708,7 +6051,11 @@ function StreamingAssistant({ event }) {
5708
6051
  label = parts.join(" \xB7 ");
5709
6052
  labelColor = "green";
5710
6053
  }
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);
6054
+ 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 ? (
6055
+ // Non-dim yellow: first-time users misread the dim version as
6056
+ // "app frozen". The reassurance has to be VISIBLE to do its job.
6057
+ /* @__PURE__ */ React4.createElement(Text3, { color: "yellow", italic: true }, " waiting for first byte \u2014 this is normal, typically 5-60s depending on model + load")
6058
+ ) : 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
6059
  }
5713
6060
  function Pulse() {
5714
6061
  const tick = useTick();
@@ -5722,10 +6069,26 @@ function lastLine(s, maxChars) {
5722
6069
  }
5723
6070
  function StatsLine({ stats }) {
5724
6071
  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));
6072
+ 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
6073
  }
5727
6074
  function truncate2(s, max) {
5728
- return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
6075
+ if (s.length <= max) return s;
6076
+ if (s.startsWith("ERROR:")) {
6077
+ const firstNl = s.indexOf("\n");
6078
+ const firstLine = firstNl === -1 ? s : s.slice(0, firstNl);
6079
+ if (firstLine.length >= max) {
6080
+ return `${firstLine.slice(0, max)}\u2026 (+${s.length - max} chars \u2014 /tool N for full)`;
6081
+ }
6082
+ const budget = max - firstLine.length - 10;
6083
+ const tail = s.slice(-budget);
6084
+ const skipped2 = s.length - firstLine.length - tail.length;
6085
+ return `${firstLine}
6086
+ \u2026 (+${skipped2} chars) \u2026
6087
+ ${tail}`;
6088
+ }
6089
+ const skipped = s.length - max;
6090
+ return `\u2026 (+${skipped} earlier chars \u2014 /tool N for full) \u2026
6091
+ ${s.slice(-max)}`;
5729
6092
  }
5730
6093
 
5731
6094
  // src/cli/ui/PlanConfirm.tsx
@@ -5739,7 +6102,8 @@ function SingleSelect({
5739
6102
  items,
5740
6103
  initialValue,
5741
6104
  onSubmit,
5742
- onCancel
6105
+ onCancel,
6106
+ footer
5743
6107
  }) {
5744
6108
  const initialIndex = Math.max(
5745
6109
  0,
@@ -5766,7 +6130,7 @@ function SingleSelect({
5766
6130
  active: i === index,
5767
6131
  marker: i === index ? "\u25B8" : " "
5768
6132
  }
5769
- )));
6133
+ )), footer ? /* @__PURE__ */ React5.createElement(Box4, { marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, footer)) : null);
5770
6134
  }
5771
6135
  function MultiSelect({
5772
6136
  items,
@@ -5842,7 +6206,7 @@ function PlanConfirm({ plan, onChoose, maxRenderedChars, projectRoot }) {
5842
6206
 
5843
6207
  \u2026 (${plan.length - cap} chars truncated \u2014 use /tool to view the full proposal)` : plan;
5844
6208
  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(
6209
+ 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
6210
  SingleSelect,
5847
6211
  {
5848
6212
  initialValue: hasOpenQuestions ? "refine" : "approve",
@@ -5863,7 +6227,9 @@ function PlanConfirm({ plan, onChoose, maxRenderedChars, projectRoot }) {
5863
6227
  hint: "Exit plan mode. Drop the plan; the model won't implement it."
5864
6228
  }
5865
6229
  ],
5866
- onSubmit: (v) => onChoose(v)
6230
+ onSubmit: (v) => onChoose(v),
6231
+ onCancel: () => onChoose("cancel"),
6232
+ footer: "[\u2191\u2193] navigate \xB7 [Enter] select \xB7 [Esc] cancel"
5867
6233
  }
5868
6234
  )));
5869
6235
  }
@@ -6054,7 +6420,7 @@ function PromptInput({
6054
6420
  },
6055
6421
  { isActive: !disabled }
6056
6422
  );
6057
- const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command \xB7 Ctrl+J for newline";
6423
+ 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
6424
  const lines = value.length > 0 ? value.split("\n") : [""];
6059
6425
  const borderColor = disabled ? "gray" : "cyan";
6060
6426
  const { line: cursorLine, col: cursorCol } = lineAndColumn(value, cursor);
@@ -6095,7 +6461,7 @@ function LineWithCursor({
6095
6461
  import { Box as Box8, Text as Text8 } from "ink";
6096
6462
  import React9 from "react";
6097
6463
  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(
6464
+ 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
6465
  SingleSelect,
6100
6466
  {
6101
6467
  initialValue: "run_once",
@@ -6116,7 +6482,9 @@ function ShellConfirm({ command, allowPrefix, onChoose }) {
6116
6482
  hint: "Tell the model the user refused; it will continue without this command."
6117
6483
  }
6118
6484
  ],
6119
- onSubmit: (v) => onChoose(v)
6485
+ onSubmit: (v) => onChoose(v),
6486
+ onCancel: () => onChoose("deny"),
6487
+ footer: "[\u2191\u2193] navigate \xB7 [Enter] select \xB7 [Esc] deny"
6120
6488
  }
6121
6489
  )));
6122
6490
  }
@@ -6166,7 +6534,7 @@ function SlashSuggestions({
6166
6534
  const shown = matches.slice(windowStart, windowStart + MAX);
6167
6535
  const hiddenAbove = windowStart;
6168
6536
  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"));
6537
+ 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
6538
  }
6171
6539
  function SuggestionRow({ spec, isSelected }) {
6172
6540
  const marker = isSelected ? "\u25B8" : " ";
@@ -6179,8 +6547,37 @@ function SuggestionRow({ spec, isSelected }) {
6179
6547
  }
6180
6548
 
6181
6549
  // src/cli/ui/StatsPanel.tsx
6182
- import { Box as Box10, Text as Text10 } from "ink";
6550
+ import { Box as Box10, Text as Text10, useStdout as useStdout2 } from "ink";
6183
6551
  import React11 from "react";
6552
+ var WORDMARK_STYLES = [
6553
+ { ch: "\u25C8", color: "#5eead4", isLogo: true },
6554
+ // teal — brand mark
6555
+ { ch: " ", color: "#5eead4", isLogo: false },
6556
+ { ch: "R", color: "#67e8f9", isLogo: false },
6557
+ // cyan
6558
+ { ch: "E", color: "#7dd3fc", isLogo: false },
6559
+ // sky
6560
+ { ch: "A", color: "#93c5fd", isLogo: false },
6561
+ // blue
6562
+ { ch: "S", color: "#a5b4fc", isLogo: false },
6563
+ // indigo
6564
+ { ch: "O", color: "#c4b5fd", isLogo: false },
6565
+ // violet
6566
+ { ch: "N", color: "#d8b4fe", isLogo: false },
6567
+ // purple
6568
+ { ch: "I", color: "#f0abfc", isLogo: false },
6569
+ // fuchsia
6570
+ { ch: "X", color: "#f0abfc", isLogo: false }
6571
+ // fuchsia
6572
+ ];
6573
+ function Wordmark({ busy }) {
6574
+ const tick = useTick();
6575
+ const period = busy ? 5 : 12;
6576
+ const bright = Math.floor(tick / period) % 2 === 0;
6577
+ 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)));
6578
+ }
6579
+ var NARROW_BREAKPOINT = 120;
6580
+ var COLD_START_TURNS = 3;
6184
6581
  function StatsPanel({
6185
6582
  summary,
6186
6583
  model,
@@ -6189,27 +6586,149 @@ function StatsPanel({
6189
6586
  branchBudget,
6190
6587
  planMode,
6191
6588
  balance,
6192
- updateAvailable
6589
+ updateAvailable,
6590
+ busy
6193
6591
  }) {
6194
- const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
6195
- const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
6196
6592
  const branchOn = (branchBudget ?? 1) > 1;
6197
6593
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
6198
6594
  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));
6595
+ const { stdout: stdout2 } = useStdout2();
6596
+ const columns = stdout2?.columns ?? 80;
6597
+ const narrow = columns < NARROW_BREAKPOINT;
6598
+ const coldStart = summary.turns <= COLD_START_TURNS;
6599
+ return /* @__PURE__ */ React11.createElement(Box10, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React11.createElement(
6600
+ Header,
6601
+ {
6602
+ model,
6603
+ prefixHash,
6604
+ harvestOn,
6605
+ branchOn,
6606
+ branchBudget: branchBudget ?? 1,
6607
+ planMode,
6608
+ turns: summary.turns,
6609
+ updateAvailable,
6610
+ narrow,
6611
+ busy: busy ?? false
6612
+ }
6613
+ ), narrow ? /* @__PURE__ */ React11.createElement(
6614
+ StackedMetrics,
6615
+ {
6616
+ summary,
6617
+ ctxRatio,
6618
+ ctxMax,
6619
+ balance,
6620
+ coldStart
6621
+ }
6622
+ ) : /* @__PURE__ */ React11.createElement(
6623
+ InlineMetrics,
6624
+ {
6625
+ summary,
6626
+ ctxRatio,
6627
+ ctxMax,
6628
+ balance,
6629
+ coldStart
6630
+ }
6631
+ ));
6632
+ }
6633
+ function Header({
6634
+ model,
6635
+ prefixHash,
6636
+ harvestOn,
6637
+ branchOn,
6638
+ branchBudget,
6639
+ planMode,
6640
+ turns,
6641
+ updateAvailable,
6642
+ narrow,
6643
+ busy
6644
+ }) {
6645
+ 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`)));
6646
+ }
6647
+ function InlineMetrics({
6648
+ summary,
6649
+ ctxRatio,
6650
+ ctxMax,
6651
+ balance,
6652
+ coldStart
6653
+ }) {
6654
+ 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);
6655
+ }
6656
+ function StackedMetrics({
6657
+ summary,
6658
+ ctxRatio,
6659
+ ctxMax,
6660
+ balance,
6661
+ coldStart
6662
+ }) {
6663
+ return /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React11.createElement(
6664
+ ContextCell,
6665
+ {
6666
+ ratio: ctxRatio,
6667
+ promptTokens: summary.lastPromptTokens,
6668
+ ctxMax,
6669
+ showBar: true
6670
+ }
6671
+ ), balance ? /* @__PURE__ */ React11.createElement(BalanceCell, { balance }) : null, /* @__PURE__ */ React11.createElement(CacheCell, { hitRatio: summary.cacheHitRatio, coldStart, turns: summary.turns }), /* @__PURE__ */ React11.createElement(CostCell, { summary, coldStart }));
6672
+ }
6673
+ function ContextCell({
6674
+ ratio,
6675
+ promptTokens,
6676
+ ctxMax,
6677
+ showBar
6678
+ }) {
6679
+ if (promptTokens === 0) {
6680
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "ctx "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "\u2014 (no turns yet)"));
6681
+ }
6682
+ const color = ratio >= 0.8 ? "red" : ratio >= 0.6 ? "yellow" : "green";
6683
+ const pct2 = Math.round(ratio * 100);
6684
+ 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);
6685
+ }
6686
+ function CacheCell({
6687
+ hitRatio,
6688
+ coldStart,
6689
+ turns
6690
+ }) {
6691
+ const pct2 = (hitRatio * 100).toFixed(1);
6692
+ if (turns === 0) {
6693
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "\u2014"));
6694
+ }
6695
+ if (coldStart) {
6696
+ 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)"));
6697
+ }
6698
+ const color = hitRatio >= 0.7 ? "green" : hitRatio >= 0.4 ? "yellow" : "red";
6699
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cache "), /* @__PURE__ */ React11.createElement(Text10, { color, bold: true }, pct2, "%"));
6700
+ }
6701
+ function CostCell({
6702
+ summary,
6703
+ coldStart
6704
+ }) {
6705
+ if (summary.turns === 0) {
6706
+ return /* @__PURE__ */ React11.createElement(Text10, null, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "cost "), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "\u2014"));
6707
+ }
6708
+ const primaryColor = coldStart ? void 0 : "green";
6709
+ 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), ")"));
6710
+ }
6711
+ function BalanceCell({ balance }) {
6712
+ const color = balance.total < 1 ? "red" : balance.total < 5 ? "yellow" : "green";
6713
+ 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}` : ""));
6714
+ }
6715
+ function Bar({ ratio, color }) {
6716
+ const cells = 10;
6717
+ const filled = Math.max(0, Math.min(cells, Math.round(ratio * cells)));
6718
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(cells - filled);
6719
+ return /* @__PURE__ */ React11.createElement(Text10, { color }, bar);
6201
6720
  }
6202
6721
  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`;
6722
+ if (n < 1024) return String(n);
6723
+ const k = n / 1024;
6724
+ return k >= 100 ? `${k.toFixed(0)}K` : `${k.toFixed(1)}K`;
6206
6725
  }
6207
6726
 
6208
6727
  // src/cli/ui/slash.ts
6209
6728
  import { spawnSync } from "child_process";
6210
6729
 
6211
6730
  // src/cli/commands/stats.ts
6212
- import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
6731
+ import { existsSync as existsSync7, readFileSync as readFileSync11 } from "fs";
6213
6732
  function statsCommand(opts) {
6214
6733
  if (opts.transcript) {
6215
6734
  transcriptSummary(opts.transcript);
@@ -6222,7 +6741,7 @@ function transcriptSummary(path) {
6222
6741
  console.error(`no such transcript: ${path}`);
6223
6742
  process.exit(1);
6224
6743
  }
6225
- const lines = readFileSync10(path, "utf8").split(/\r?\n/).filter(Boolean);
6744
+ const lines = readFileSync11(path, "utf8").split(/\r?\n/).filter(Boolean);
6226
6745
  let assistantTurns = 0;
6227
6746
  let toolCalls = 0;
6228
6747
  let lastTurn = 0;
@@ -6324,6 +6843,7 @@ var SLASH_COMMANDS = [
6324
6843
  summary: "one-tap model + harvest + branch bundle"
6325
6844
  },
6326
6845
  { cmd: "model", argsHint: "<id>", summary: "switch DeepSeek model id" },
6846
+ { cmd: "models", summary: "list available models fetched from DeepSeek /models" },
6327
6847
  { cmd: "harvest", argsHint: "[on|off]", summary: "toggle Pillar-2 plan-state extraction" },
6328
6848
  { cmd: "branch", argsHint: "<N|off>", summary: "run N parallel samples per turn (N>=2)" },
6329
6849
  { cmd: "mcp", summary: "list MCP servers + tools attached to this session" },
@@ -6352,6 +6872,10 @@ var SLASH_COMMANDS = [
6352
6872
  summary: "cross-session cost dashboard (today / week / month / all-time \xB7 cache hit \xB7 vs Claude)"
6353
6873
  },
6354
6874
  { cmd: "think", summary: "dump the last turn's full R1 reasoning (reasoner only)" },
6875
+ {
6876
+ cmd: "context",
6877
+ summary: "break down where context tokens are going: system / tools / per-turn log"
6878
+ },
6355
6879
  { cmd: "retry", summary: "truncate & resend your last message (fresh sample)" },
6356
6880
  { cmd: "compact", argsHint: "[cap]", summary: "shrink oversized tool results in the log" },
6357
6881
  { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
@@ -6696,6 +7220,66 @@ ${entry.text}`
6696
7220
  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
7221
  };
6698
7222
  }
7223
+ case "context": {
7224
+ const systemTokens = countTokens(loop.prefix.system);
7225
+ const toolsTokens = countTokens(JSON.stringify(loop.prefix.toolSpecs));
7226
+ const entries = loop.log.toMessages();
7227
+ let userTokens = 0;
7228
+ let assistantTokens = 0;
7229
+ let toolResultTokens = 0;
7230
+ let toolCallTokens = 0;
7231
+ const toolBreakdown = [];
7232
+ let logTurn = 0;
7233
+ for (const e of entries) {
7234
+ const content = typeof e.content === "string" ? e.content : "";
7235
+ if (e.role === "user") {
7236
+ userTokens += countTokens(content);
7237
+ logTurn += 1;
7238
+ } else if (e.role === "assistant") {
7239
+ assistantTokens += countTokens(content);
7240
+ if (Array.isArray(e.tool_calls) && e.tool_calls.length > 0) {
7241
+ toolCallTokens += countTokens(JSON.stringify(e.tool_calls));
7242
+ }
7243
+ } else if (e.role === "tool") {
7244
+ const n = countTokens(content);
7245
+ toolResultTokens += n;
7246
+ toolBreakdown.push({ name: e.name ?? "?", tokens: n, turn: logTurn });
7247
+ }
7248
+ }
7249
+ const logTokens = userTokens + assistantTokens + toolResultTokens + toolCallTokens;
7250
+ const total = systemTokens + toolsTokens + logTokens;
7251
+ const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
7252
+ const pct2 = (n) => total > 0 ? `${Math.round(n / total * 100)}%`.padStart(4) : " 0%";
7253
+ const row2 = (label, n, note = "") => ` ${label.padEnd(20)}${compactNum(n).padStart(8)} tokens ${pct2(n)}${note ? ` ${note}` : ""}`;
7254
+ const lines = [
7255
+ `Next-request estimate: ~${compactNum(total)} tokens of ${compactNum(ctxMax)} (${Math.round(
7256
+ total / ctxMax * 100
7257
+ )}% of window)`,
7258
+ "",
7259
+ row2("system prompt", systemTokens),
7260
+ row2("tool specs", toolsTokens, `(${loop.prefix.toolSpecs.length} tools)`),
7261
+ row2("log (all turns)", logTokens, `(${entries.length} messages)`),
7262
+ ` user ${compactNum(userTokens).padStart(8)} tokens`,
7263
+ ` assistant ${compactNum(assistantTokens).padStart(8)} tokens`,
7264
+ ` tool-call args ${compactNum(toolCallTokens).padStart(8)} tokens`,
7265
+ ` tool results ${compactNum(toolResultTokens).padStart(8)} tokens`
7266
+ ];
7267
+ if (toolBreakdown.length > 0) {
7268
+ const top = [...toolBreakdown].sort((a, b) => b.tokens - a.tokens).slice(0, 5);
7269
+ lines.push("");
7270
+ lines.push(`Top tool results by cost (of ${toolBreakdown.length} total):`);
7271
+ for (const t of top) {
7272
+ lines.push(
7273
+ ` turn ${String(t.turn).padStart(3)} ${t.name.padEnd(22)} ${compactNum(t.tokens).padStart(8)} tokens`
7274
+ );
7275
+ }
7276
+ }
7277
+ lines.push("");
7278
+ lines.push(
7279
+ "Count is a local estimate (DeepSeek V3 tokenizer, pure-TS port); server prompt_tokens may add ~3-6% for chat-template role markers."
7280
+ );
7281
+ return { info: lines.join("\n") };
7282
+ }
6699
7283
  case "status": {
6700
7284
  const branchBudget = loop.branchOptions.budget ?? 1;
6701
7285
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
@@ -6722,10 +7306,44 @@ ${entry.text}`
6722
7306
  }
6723
7307
  case "model": {
6724
7308
  const id = args[0];
6725
- if (!id) return { info: "usage: /model <id> (try deepseek-chat or deepseek-reasoner)" };
7309
+ const known = ctx.models ?? null;
7310
+ if (!id) {
7311
+ const hint = known && known.length > 0 ? known.join(" | ") : "try deepseek-chat or deepseek-reasoner \u2014 run /models to fetch the live list";
7312
+ return { info: `usage: /model <id> (${hint})` };
7313
+ }
6726
7314
  loop.configure({ model: id });
7315
+ if (known && known.length > 0 && !known.includes(id)) {
7316
+ return {
7317
+ 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.)`
7318
+ };
7319
+ }
6727
7320
  return { info: `model \u2192 ${id}` };
6728
7321
  }
7322
+ case "models": {
7323
+ const list = ctx.models ?? null;
7324
+ if (list === null) {
7325
+ ctx.refreshModels?.();
7326
+ return {
7327
+ 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."
7328
+ };
7329
+ }
7330
+ if (list.length === 0) {
7331
+ return {
7332
+ info: "DeepSeek /models returned an empty list. Try /models again, or check your account status at api-docs.deepseek.com."
7333
+ };
7334
+ }
7335
+ const current = loop.model;
7336
+ const lines = list.map((id) => id === current ? `\u25B8 ${id} (current)` : ` ${id}`);
7337
+ return {
7338
+ info: [
7339
+ `Available models (DeepSeek /models \xB7 ${list.length} total):`,
7340
+ "",
7341
+ ...lines,
7342
+ "",
7343
+ "Switch with: /model <id>"
7344
+ ].join("\n")
7345
+ };
7346
+ }
6729
7347
  case "harvest": {
6730
7348
  const arg = (args[0] ?? "").toLowerCase();
6731
7349
  const on = arg === "" ? !loop.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
@@ -7137,9 +7755,9 @@ function formatToolList(history) {
7137
7755
  return lines.join("\n");
7138
7756
  }
7139
7757
  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`;
7758
+ if (n < 1024) return String(n);
7759
+ const k = n / 1024;
7760
+ return k >= 100 ? `${Math.round(k)}K` : `${k.toFixed(1)}K`;
7143
7761
  }
7144
7762
  function stripOuterQuotes(s) {
7145
7763
  if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
@@ -7201,6 +7819,7 @@ function App({
7201
7819
  const [subagentActivity, setSubagentActivity] = useState5(null);
7202
7820
  const [statusLine, setStatusLine] = useState5(null);
7203
7821
  const [balance, setBalance] = useState5(null);
7822
+ const [models, setModels] = useState5(null);
7204
7823
  const [latestVersion, setLatestVersion] = useState5(null);
7205
7824
  const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
7206
7825
  const [hookList, setHookList] = useState5(
@@ -7312,6 +7931,17 @@ function App({
7312
7931
  cancelled = true;
7313
7932
  };
7314
7933
  }, [loop]);
7934
+ useEffect2(() => {
7935
+ let cancelled = false;
7936
+ void (async () => {
7937
+ const list = await loop.client.listModels().catch(() => null);
7938
+ if (cancelled || !list) return;
7939
+ setModels(list.data.map((m) => m.id));
7940
+ })();
7941
+ return () => {
7942
+ cancelled = true;
7943
+ };
7944
+ }, [loop]);
7315
7945
  useEffect2(() => {
7316
7946
  let cancelled = false;
7317
7947
  void (async () => {
@@ -7356,7 +7986,7 @@ function App({
7356
7986
  }
7357
7987
  setSubagentActivity(null);
7358
7988
  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)`;
7989
+ 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
7990
  setHistorical((prev) => [
7361
7991
  ...prev,
7362
7992
  {
@@ -7540,6 +8170,13 @@ function App({
7540
8170
  const fresh = await getLatestVersion({ force: true });
7541
8171
  if (fresh) setLatestVersion(fresh);
7542
8172
  })();
8173
+ },
8174
+ models,
8175
+ refreshModels: () => {
8176
+ void (async () => {
8177
+ const list = await loop.client.listModels().catch(() => null);
8178
+ if (list) setModels(list.data.map((m) => m.id));
8179
+ })();
7543
8180
  }
7544
8181
  });
7545
8182
  if (result.exit) {
@@ -7596,7 +8233,14 @@ function App({
7596
8233
  if (promptReport.blocked) return;
7597
8234
  }
7598
8235
  promptHistory.current.push(text);
7599
- setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
8236
+ setHistorical((prev) => [
8237
+ ...prev,
8238
+ // `leadSeparator`: thin rule above this user turn when history
8239
+ // isn't empty — visual pacing for multi-turn sessions. First
8240
+ // user message leaves it off so the UI doesn't open with a
8241
+ // dangling divider.
8242
+ { id: `u-${Date.now()}`, role: "user", text, leadSeparator: prev.length > 0 }
8243
+ ]);
7600
8244
  const assistantId = `a-${Date.now()}`;
7601
8245
  const streamRef = { id: assistantId, text: "", reasoning: "" };
7602
8246
  const contentBuf = { current: "" };
@@ -7815,6 +8459,7 @@ function App({
7815
8459
  latestVersion,
7816
8460
  mcpSpecs,
7817
8461
  mcpServers,
8462
+ models,
7818
8463
  planMode,
7819
8464
  session,
7820
8465
  slashSelected,
@@ -7978,6 +8623,7 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
7978
8623
  branchBudget: loop.branchOptions.budget,
7979
8624
  planMode,
7980
8625
  balance,
8626
+ busy,
7981
8627
  updateAvailable
7982
8628
  }
7983
8629
  ), /* @__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 +8661,7 @@ function SubagentRow({
8015
8661
  }) {
8016
8662
  const tick = useTick();
8017
8663
  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`));
8664
+ 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
8665
  }
8020
8666
  function OngoingToolRow({
8021
8667
  tool,