reasonix 0.4.14 → 0.4.16

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
@@ -32,11 +32,33 @@ function loadApiKey(path = defaultConfigPath()) {
32
32
  if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
33
33
  return readConfig(path).apiKey;
34
34
  }
35
+ function searchEnabled(path = defaultConfigPath()) {
36
+ const env = process.env.REASONIX_SEARCH;
37
+ if (env === "off" || env === "false" || env === "0") return false;
38
+ const cfg = readConfig(path).search;
39
+ if (cfg === false) return false;
40
+ return true;
41
+ }
35
42
  function saveApiKey(key, path = defaultConfigPath()) {
36
43
  const cfg = readConfig(path);
37
44
  cfg.apiKey = key.trim();
38
45
  writeConfig(cfg, path);
39
46
  }
47
+ function loadProjectShellAllowed(rootDir, path = defaultConfigPath()) {
48
+ const cfg = readConfig(path);
49
+ return cfg.projects?.[rootDir]?.shellAllowed ?? [];
50
+ }
51
+ function addProjectShellAllowed(rootDir, prefix, path = defaultConfigPath()) {
52
+ const trimmed = prefix.trim();
53
+ if (!trimmed) return;
54
+ const cfg = readConfig(path);
55
+ if (!cfg.projects) cfg.projects = {};
56
+ if (!cfg.projects[rootDir]) cfg.projects[rootDir] = {};
57
+ const existing = cfg.projects[rootDir].shellAllowed ?? [];
58
+ if (existing.includes(trimmed)) return;
59
+ cfg.projects[rootDir].shellAllowed = [...existing, trimmed];
60
+ writeConfig(cfg, path);
61
+ }
40
62
  function isPlausibleKey(key) {
41
63
  const trimmed = key.trim();
42
64
  return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
@@ -96,8 +118,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
96
118
  }
97
119
  function sleep(ms, signal) {
98
120
  if (ms <= 0) return Promise.resolve();
99
- return new Promise((resolve5, reject) => {
100
- const timer = setTimeout(resolve5, ms);
121
+ return new Promise((resolve6, reject) => {
122
+ const timer = setTimeout(resolve6, ms);
101
123
  if (signal) {
102
124
  const onAbort = () => {
103
125
  clearTimeout(timer);
@@ -1537,8 +1559,8 @@ var CacheFirstLoop = class {
1537
1559
  }
1538
1560
  );
1539
1561
  for (let k = 0; k < budget; k++) {
1540
- const sample = queue.shift() ?? await new Promise((resolve5) => {
1541
- waiter = resolve5;
1562
+ const sample = queue.shift() ?? await new Promise((resolve6) => {
1563
+ waiter = resolve6;
1542
1564
  });
1543
1565
  yield {
1544
1566
  turn: this._turn,
@@ -2232,13 +2254,409 @@ function lineDiff(a, b) {
2232
2254
  return out;
2233
2255
  }
2234
2256
 
2257
+ // src/tools/shell.ts
2258
+ import { spawn } from "child_process";
2259
+ import * as pathMod2 from "path";
2260
+ var DEFAULT_TIMEOUT_SEC = 60;
2261
+ var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
2262
+ var BUILTIN_ALLOWLIST = [
2263
+ // Repo inspection
2264
+ "git status",
2265
+ "git diff",
2266
+ "git log",
2267
+ "git show",
2268
+ "git blame",
2269
+ "git branch",
2270
+ "git remote",
2271
+ "git rev-parse",
2272
+ "git config --get",
2273
+ // Filesystem inspection
2274
+ "ls",
2275
+ "pwd",
2276
+ "cat",
2277
+ "head",
2278
+ "tail",
2279
+ "wc",
2280
+ "file",
2281
+ "tree",
2282
+ "find",
2283
+ "grep",
2284
+ "rg",
2285
+ // Language version probes
2286
+ "node --version",
2287
+ "node -v",
2288
+ "npm --version",
2289
+ "npx --version",
2290
+ "python --version",
2291
+ "python3 --version",
2292
+ "cargo --version",
2293
+ "go version",
2294
+ "rustc --version",
2295
+ "deno --version",
2296
+ "bun --version",
2297
+ // Test runners (non-destructive by convention)
2298
+ "npm test",
2299
+ "npm run test",
2300
+ "npx vitest run",
2301
+ "npx vitest",
2302
+ "npx jest",
2303
+ "pytest",
2304
+ "python -m pytest",
2305
+ "cargo test",
2306
+ "cargo check",
2307
+ "cargo clippy",
2308
+ "go test",
2309
+ "go vet",
2310
+ "deno test",
2311
+ "bun test",
2312
+ // Linters / typecheckers (read-only by convention)
2313
+ "npm run lint",
2314
+ "npm run typecheck",
2315
+ "npx tsc --noEmit",
2316
+ "npx biome check",
2317
+ "npx eslint",
2318
+ "npx prettier --check",
2319
+ "ruff",
2320
+ "mypy"
2321
+ ];
2322
+ function tokenizeCommand(cmd) {
2323
+ const out = [];
2324
+ let cur = "";
2325
+ let quote = null;
2326
+ for (let i = 0; i < cmd.length; i++) {
2327
+ const ch = cmd[i];
2328
+ if (quote) {
2329
+ if (ch === quote) {
2330
+ quote = null;
2331
+ } else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
2332
+ cur += cmd[++i];
2333
+ } else {
2334
+ cur += ch;
2335
+ }
2336
+ continue;
2337
+ }
2338
+ if (ch === '"' || ch === "'") {
2339
+ quote = ch;
2340
+ continue;
2341
+ }
2342
+ if (ch === " " || ch === " ") {
2343
+ if (cur.length > 0) {
2344
+ out.push(cur);
2345
+ cur = "";
2346
+ }
2347
+ continue;
2348
+ }
2349
+ cur += ch;
2350
+ }
2351
+ if (quote) throw new Error(`unclosed ${quote} in command`);
2352
+ if (cur.length > 0) out.push(cur);
2353
+ return out;
2354
+ }
2355
+ function isAllowed(cmd, extra = []) {
2356
+ const normalized = cmd.trim().replace(/\s+/g, " ");
2357
+ const allowlist = [...BUILTIN_ALLOWLIST, ...extra];
2358
+ for (const prefix of allowlist) {
2359
+ if (normalized === prefix) return true;
2360
+ if (normalized.startsWith(`${prefix} `)) return true;
2361
+ }
2362
+ return false;
2363
+ }
2364
+ async function runCommand(cmd, opts) {
2365
+ const argv = tokenizeCommand(cmd);
2366
+ if (argv.length === 0) throw new Error("run_command: empty command");
2367
+ const timeoutMs = (opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
2368
+ const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
2369
+ const spawnOpts = {
2370
+ cwd: opts.cwd,
2371
+ shell: false,
2372
+ // no shell-expansion — see header comment
2373
+ windowsHide: true,
2374
+ env: process.env
2375
+ };
2376
+ return await new Promise((resolve6, reject) => {
2377
+ let child;
2378
+ try {
2379
+ child = spawn(argv[0], argv.slice(1), spawnOpts);
2380
+ } catch (err) {
2381
+ reject(err);
2382
+ return;
2383
+ }
2384
+ let buf = "";
2385
+ let timedOut = false;
2386
+ const killTimer = setTimeout(() => {
2387
+ timedOut = true;
2388
+ child.kill("SIGKILL");
2389
+ }, timeoutMs);
2390
+ const onAbort = () => child.kill("SIGKILL");
2391
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
2392
+ const onData = (chunk) => {
2393
+ buf += chunk.toString();
2394
+ if (buf.length > maxChars * 2) buf = `${buf.slice(0, maxChars * 2)}`;
2395
+ };
2396
+ child.stdout?.on("data", onData);
2397
+ child.stderr?.on("data", onData);
2398
+ child.on("error", (err) => {
2399
+ clearTimeout(killTimer);
2400
+ opts.signal?.removeEventListener("abort", onAbort);
2401
+ reject(err);
2402
+ });
2403
+ child.on("close", (code) => {
2404
+ clearTimeout(killTimer);
2405
+ opts.signal?.removeEventListener("abort", onAbort);
2406
+ const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
2407
+
2408
+ [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
2409
+ resolve6({ exitCode: code, output, timedOut });
2410
+ });
2411
+ });
2412
+ }
2413
+ var NeedsConfirmationError = class extends Error {
2414
+ command;
2415
+ constructor(command) {
2416
+ super(
2417
+ `run_command: "${command}" needs the user's approval before it runs. STOP calling tools now \u2014 the TUI has already prompted the user to press y (run) or n (deny). Wait for their next message; it will either be the command's output (if they approved) or an instruction to continue without it (if they denied). Don't retry the command or call other shell commands in the meantime.`
2418
+ );
2419
+ this.name = "NeedsConfirmationError";
2420
+ this.command = command;
2421
+ }
2422
+ };
2423
+ function registerShellTools(registry, opts) {
2424
+ const rootDir = pathMod2.resolve(opts.rootDir);
2425
+ const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
2426
+ const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
2427
+ const extraAllowed = opts.extraAllowed ?? [];
2428
+ const allowAll = opts.allowAll ?? false;
2429
+ registry.register({
2430
+ name: "run_command",
2431
+ description: "Run a shell command in the project root and return its combined stdout+stderr. Read-only and test commands (git status, ls, npm test, pytest, cargo test, grep, etc.) run immediately. Anything that could mutate state (npm install, git commit, rm, chmod) is refused and the user has to confirm in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
2432
+ parameters: {
2433
+ type: "object",
2434
+ properties: {
2435
+ command: {
2436
+ type: "string",
2437
+ description: "Full command line, e.g. 'npm test' or 'git diff src/foo.ts'. Tokenized with POSIX-ish quoting; no shell expansion, no pipes, no redirects."
2438
+ },
2439
+ timeoutSec: {
2440
+ type: "integer",
2441
+ description: `Override the default ${timeoutSec}s timeout for a single command.`
2442
+ }
2443
+ },
2444
+ required: ["command"]
2445
+ },
2446
+ fn: async (args, ctx) => {
2447
+ const cmd = args.command.trim();
2448
+ if (!cmd) throw new Error("run_command: empty command");
2449
+ if (!allowAll && !isAllowed(cmd, extraAllowed)) {
2450
+ throw new NeedsConfirmationError(cmd);
2451
+ }
2452
+ const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
2453
+ const result = await runCommand(cmd, {
2454
+ cwd: rootDir,
2455
+ timeoutSec: effectiveTimeout,
2456
+ maxOutputChars,
2457
+ signal: ctx?.signal
2458
+ });
2459
+ return formatCommandResult(cmd, result);
2460
+ }
2461
+ });
2462
+ return registry;
2463
+ }
2464
+ function formatCommandResult(cmd, r) {
2465
+ const header = r.timedOut ? `$ ${cmd}
2466
+ [killed after timeout]` : `$ ${cmd}
2467
+ [exit ${r.exitCode ?? "?"}]`;
2468
+ return r.output ? `${header}
2469
+ ${r.output}` : header;
2470
+ }
2471
+
2472
+ // src/tools/web.ts
2473
+ var DEFAULT_FETCH_MAX_CHARS = 32e3;
2474
+ var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
2475
+ var DEFAULT_TOPK = 5;
2476
+ var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
2477
+ var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
2478
+ async function webSearch(query, opts = {}) {
2479
+ const topK = Math.max(1, Math.min(10, opts.topK ?? DEFAULT_TOPK));
2480
+ const resp = await fetch(`${MOJEEK_ENDPOINT}?q=${encodeURIComponent(query)}`, {
2481
+ headers: {
2482
+ "User-Agent": USER_AGENT,
2483
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9",
2484
+ "Accept-Language": "en-US,en;q=0.9"
2485
+ },
2486
+ signal: opts.signal,
2487
+ redirect: "follow"
2488
+ });
2489
+ if (!resp.ok) throw new Error(`web_search ${resp.status}`);
2490
+ const html = await resp.text();
2491
+ const results = parseMojeekResults(html).slice(0, topK);
2492
+ if (results.length === 0) {
2493
+ if (/no results found|did not match any documents/i.test(html)) return [];
2494
+ if (/captcha|verify you are human|access denied|forbidden/i.test(html)) {
2495
+ throw new Error("web_search: Mojeek anti-bot page \u2014 rate-limited or blocked");
2496
+ }
2497
+ throw new Error(
2498
+ `web_search: 0 results but response doesn't look like a real empty page (${html.length} chars, first 120: ${html.slice(0, 120).replace(/\s+/g, " ")})`
2499
+ );
2500
+ }
2501
+ return results;
2502
+ }
2503
+ function parseMojeekResults(html) {
2504
+ const titles = [];
2505
+ const titleAnchorRe = /<a\b[^>]*\bclass="title"[^>]*>[\s\S]*?<\/a>/g;
2506
+ let m;
2507
+ while (true) {
2508
+ m = titleAnchorRe.exec(html);
2509
+ if (m === null) break;
2510
+ titles.push(m[0]);
2511
+ }
2512
+ const snippets = [];
2513
+ const snippetRe = /<p\b[^>]*\bclass="s"[^>]*>([\s\S]*?)<\/p>/g;
2514
+ while (true) {
2515
+ m = snippetRe.exec(html);
2516
+ if (m === null) break;
2517
+ snippets.push(m[1] ?? "");
2518
+ }
2519
+ const hrefRe = /href="([^"]+)"/;
2520
+ const innerRe = /<a\b[^>]*>([\s\S]*?)<\/a>/;
2521
+ const results = [];
2522
+ for (let i = 0; i < titles.length; i++) {
2523
+ const anchor = titles[i];
2524
+ const hrefMatch = anchor.match(hrefRe);
2525
+ const innerMatch = anchor.match(innerRe);
2526
+ if (!hrefMatch?.[1]) continue;
2527
+ results.push({
2528
+ title: decodeHtmlEntities(stripHtml(innerMatch?.[1] ?? "")).trim(),
2529
+ url: hrefMatch[1],
2530
+ snippet: decodeHtmlEntities(stripHtml(snippets[i] ?? "")).replace(/\s+/g, " ").trim()
2531
+ });
2532
+ }
2533
+ return results;
2534
+ }
2535
+ async function webFetch(url, opts = {}) {
2536
+ const maxChars = opts.maxChars ?? DEFAULT_FETCH_MAX_CHARS;
2537
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
2538
+ const ctl = new AbortController();
2539
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
2540
+ const cancel = () => ctl.abort();
2541
+ opts.signal?.addEventListener("abort", cancel, { once: true });
2542
+ let resp;
2543
+ try {
2544
+ resp = await fetch(url, {
2545
+ headers: { "User-Agent": USER_AGENT, Accept: "text/html,text/plain,*/*" },
2546
+ signal: ctl.signal,
2547
+ redirect: "follow"
2548
+ });
2549
+ } finally {
2550
+ clearTimeout(timer);
2551
+ opts.signal?.removeEventListener("abort", cancel);
2552
+ }
2553
+ if (!resp.ok) throw new Error(`web_fetch ${resp.status} for ${url}`);
2554
+ const contentType = resp.headers.get("content-type") ?? "";
2555
+ const raw = await resp.text();
2556
+ const title = extractTitle(raw);
2557
+ const text = contentType.includes("text/html") ? htmlToText(raw) : raw;
2558
+ const truncated = text.length > maxChars;
2559
+ const finalText = truncated ? `${text.slice(0, maxChars)}
2560
+
2561
+ [\u2026 truncated ${text.length - maxChars} chars \u2026]` : text;
2562
+ return { url, title, text: finalText, truncated };
2563
+ }
2564
+ function htmlToText(html) {
2565
+ let s = html;
2566
+ s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
2567
+ s = s.replace(/<style[\s\S]*?<\/style>/gi, "");
2568
+ s = s.replace(/<noscript[\s\S]*?<\/noscript>/gi, "");
2569
+ s = s.replace(/<nav[\s\S]*?<\/nav>/gi, "");
2570
+ s = s.replace(/<footer[\s\S]*?<\/footer>/gi, "");
2571
+ s = s.replace(/<aside[\s\S]*?<\/aside>/gi, "");
2572
+ s = s.replace(/<svg[\s\S]*?<\/svg>/gi, "");
2573
+ s = s.replace(/<\/?(p|div|br|h[1-6]|li|tr|section|article)\b[^>]*>/gi, "\n");
2574
+ s = s.replace(/<[^>]+>/g, "");
2575
+ s = decodeHtmlEntities(s);
2576
+ s = s.replace(/[ \t]+/g, " ");
2577
+ s = s.replace(/\n[ \t]+/g, "\n");
2578
+ s = s.replace(/\n{3,}/g, "\n\n");
2579
+ return s.trim();
2580
+ }
2581
+ function stripHtml(s) {
2582
+ return s.replace(/<[^>]+>/g, "");
2583
+ }
2584
+ function decodeHtmlEntities(s) {
2585
+ return s.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
2586
+ }
2587
+ function extractTitle(html) {
2588
+ const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
2589
+ if (!m?.[1]) return void 0;
2590
+ return m[1].replace(/\s+/g, " ").trim() || void 0;
2591
+ }
2592
+ function registerWebTools(registry, opts = {}) {
2593
+ const defaultTopK = opts.defaultTopK ?? DEFAULT_TOPK;
2594
+ const maxFetchChars = opts.maxFetchChars ?? DEFAULT_FETCH_MAX_CHARS;
2595
+ registry.register({
2596
+ name: "web_search",
2597
+ description: "Search the public web. Returns ranked results with title, url, and snippet. Use this when the question needs information more current than your training data, when you're unsure of a factual detail, or when the user asks about a specific webpage/library/release you haven't seen.",
2598
+ parameters: {
2599
+ type: "object",
2600
+ properties: {
2601
+ query: { type: "string", description: "Natural-language search query." },
2602
+ topK: {
2603
+ type: "integer",
2604
+ description: `Number of results to return (1..10). Default ${defaultTopK}.`
2605
+ }
2606
+ },
2607
+ required: ["query"]
2608
+ },
2609
+ fn: async (args, ctx) => {
2610
+ const results = await webSearch(args.query, {
2611
+ topK: args.topK ?? defaultTopK,
2612
+ signal: ctx?.signal
2613
+ });
2614
+ return formatSearchResults(args.query, results);
2615
+ }
2616
+ });
2617
+ registry.register({
2618
+ name: "web_fetch",
2619
+ description: "Download a URL and return its visible text content (HTML pages get scripts/styles/nav stripped). Truncated at the tool-result cap. Use after web_search when a snippet isn't enough.",
2620
+ parameters: {
2621
+ type: "object",
2622
+ properties: {
2623
+ url: { type: "string", description: "Absolute http:// or https:// URL." }
2624
+ },
2625
+ required: ["url"]
2626
+ },
2627
+ fn: async (args, ctx) => {
2628
+ if (!/^https?:\/\//i.test(args.url)) {
2629
+ throw new Error("web_fetch: url must start with http:// or https://");
2630
+ }
2631
+ const page = await webFetch(args.url, { maxChars: maxFetchChars, signal: ctx?.signal });
2632
+ const header = page.title ? `${page.title}
2633
+ ${page.url}` : page.url;
2634
+ return `${header}
2635
+
2636
+ ${page.text}`;
2637
+ }
2638
+ });
2639
+ return registry;
2640
+ }
2641
+ function formatSearchResults(query, results) {
2642
+ const lines = [`query: ${query}`, `
2643
+ results (${results.length}):`];
2644
+ results.forEach((r, i) => {
2645
+ lines.push(`
2646
+ ${i + 1}. ${r.title}`);
2647
+ lines.push(` ${r.url}`);
2648
+ if (r.snippet) lines.push(` ${r.snippet}`);
2649
+ });
2650
+ return lines.join("\n");
2651
+ }
2652
+
2235
2653
  // src/env.ts
2236
2654
  import { readFileSync as readFileSync3 } from "fs";
2237
- import { resolve as resolve2 } from "path";
2655
+ import { resolve as resolve3 } from "path";
2238
2656
  function loadDotenv(path = ".env") {
2239
2657
  let raw;
2240
2658
  try {
2241
- raw = readFileSync3(resolve2(process.cwd(), path), "utf8");
2659
+ raw = readFileSync3(resolve3(process.cwd(), path), "utf8");
2242
2660
  } catch {
2243
2661
  return;
2244
2662
  }
@@ -2951,7 +3369,7 @@ var McpClient = class {
2951
3369
  const id = this.nextId++;
2952
3370
  const frame = { jsonrpc: "2.0", id, method, params };
2953
3371
  let abortHandler = null;
2954
- const promise = new Promise((resolve5, reject) => {
3372
+ const promise = new Promise((resolve6, reject) => {
2955
3373
  const timeout = setTimeout(() => {
2956
3374
  this.pending.delete(id);
2957
3375
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -2960,7 +3378,7 @@ var McpClient = class {
2960
3378
  );
2961
3379
  }, this.requestTimeoutMs);
2962
3380
  this.pending.set(id, {
2963
- resolve: resolve5,
3381
+ resolve: resolve6,
2964
3382
  reject,
2965
3383
  timeout
2966
3384
  });
@@ -3042,7 +3460,7 @@ var McpClient = class {
3042
3460
  };
3043
3461
 
3044
3462
  // src/mcp/stdio.ts
3045
- import { spawn } from "child_process";
3463
+ import { spawn as spawn2 } from "child_process";
3046
3464
  var StdioTransport = class {
3047
3465
  child;
3048
3466
  queue = [];
@@ -3057,14 +3475,14 @@ var StdioTransport = class {
3057
3475
  opts.command,
3058
3476
  ...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
3059
3477
  ].join(" ");
3060
- this.child = spawn(line, [], {
3478
+ this.child = spawn2(line, [], {
3061
3479
  env,
3062
3480
  cwd: opts.cwd,
3063
3481
  stdio: ["pipe", "pipe", "inherit"],
3064
3482
  shell: true
3065
3483
  });
3066
3484
  } else {
3067
- this.child = spawn(opts.command, opts.args ?? [], {
3485
+ this.child = spawn2(opts.command, opts.args ?? [], {
3068
3486
  env,
3069
3487
  cwd: opts.cwd,
3070
3488
  stdio: ["pipe", "pipe", "inherit"]
@@ -3083,12 +3501,12 @@ var StdioTransport = class {
3083
3501
  }
3084
3502
  async send(message) {
3085
3503
  if (this.closed) throw new Error("MCP transport is closed");
3086
- return new Promise((resolve5, reject) => {
3504
+ return new Promise((resolve6, reject) => {
3087
3505
  const line = `${JSON.stringify(message)}
3088
3506
  `;
3089
3507
  this.child.stdin.write(line, "utf8", (err) => {
3090
3508
  if (err) reject(err);
3091
- else resolve5();
3509
+ else resolve6();
3092
3510
  });
3093
3511
  });
3094
3512
  }
@@ -3099,8 +3517,8 @@ var StdioTransport = class {
3099
3517
  continue;
3100
3518
  }
3101
3519
  if (this.closed) return;
3102
- const next = await new Promise((resolve5) => {
3103
- this.waiters.push(resolve5);
3520
+ const next = await new Promise((resolve6) => {
3521
+ this.waiters.push(resolve6);
3104
3522
  });
3105
3523
  if (next === null) return;
3106
3524
  yield next;
@@ -3166,8 +3584,8 @@ var SseTransport = class {
3166
3584
  constructor(opts) {
3167
3585
  this.url = opts.url;
3168
3586
  this.headers = opts.headers ?? {};
3169
- this.endpointReady = new Promise((resolve5, reject) => {
3170
- this.resolveEndpoint = resolve5;
3587
+ this.endpointReady = new Promise((resolve6, reject) => {
3588
+ this.resolveEndpoint = resolve6;
3171
3589
  this.rejectEndpoint = reject;
3172
3590
  });
3173
3591
  this.endpointReady.catch(() => void 0);
@@ -3194,8 +3612,8 @@ var SseTransport = class {
3194
3612
  continue;
3195
3613
  }
3196
3614
  if (this.closed) return;
3197
- const next = await new Promise((resolve5) => {
3198
- this.waiters.push(resolve5);
3615
+ const next = await new Promise((resolve6) => {
3616
+ this.waiters.push(resolve6);
3199
3617
  });
3200
3618
  if (next === null) return;
3201
3619
  yield next;
@@ -3395,7 +3813,7 @@ async function trySection(load) {
3395
3813
 
3396
3814
  // src/code/edit-blocks.ts
3397
3815
  import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
3398
- import { dirname as dirname4, resolve as resolve3 } from "path";
3816
+ import { dirname as dirname4, resolve as resolve4 } from "path";
3399
3817
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
3400
3818
  function parseEditBlocks(text) {
3401
3819
  const out = [];
@@ -3413,8 +3831,8 @@ function parseEditBlocks(text) {
3413
3831
  return out;
3414
3832
  }
3415
3833
  function applyEditBlock(block, rootDir) {
3416
- const absRoot = resolve3(rootDir);
3417
- const absTarget = resolve3(absRoot, block.path);
3834
+ const absRoot = resolve4(rootDir);
3835
+ const absTarget = resolve4(absRoot, block.path);
3418
3836
  if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
3419
3837
  return {
3420
3838
  path: block.path,
@@ -3464,13 +3882,13 @@ function applyEditBlocks(blocks, rootDir) {
3464
3882
  return blocks.map((b) => applyEditBlock(b, rootDir));
3465
3883
  }
3466
3884
  function snapshotBeforeEdits(blocks, rootDir) {
3467
- const absRoot = resolve3(rootDir);
3885
+ const absRoot = resolve4(rootDir);
3468
3886
  const seen = /* @__PURE__ */ new Set();
3469
3887
  const snapshots = [];
3470
3888
  for (const b of blocks) {
3471
3889
  if (seen.has(b.path)) continue;
3472
3890
  seen.add(b.path);
3473
- const abs = resolve3(absRoot, b.path);
3891
+ const abs = resolve4(absRoot, b.path);
3474
3892
  if (!existsSync2(abs)) {
3475
3893
  snapshots.push({ path: b.path, prevContent: null });
3476
3894
  continue;
@@ -3484,9 +3902,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
3484
3902
  return snapshots;
3485
3903
  }
3486
3904
  function restoreSnapshots(snapshots, rootDir) {
3487
- const absRoot = resolve3(rootDir);
3905
+ const absRoot = resolve4(rootDir);
3488
3906
  return snapshots.map((snap) => {
3489
- const abs = resolve3(absRoot, snap.path);
3907
+ const abs = resolve4(absRoot, snap.path);
3490
3908
  if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
3491
3909
  return {
3492
3910
  path: snap.path,
@@ -3519,15 +3937,16 @@ function sep() {
3519
3937
  }
3520
3938
 
3521
3939
  // src/index.ts
3522
- var VERSION = "0.4.14";
3940
+ var VERSION = "0.4.16";
3523
3941
 
3524
3942
  // src/cli/commands/chat.tsx
3943
+ import { existsSync as existsSync3, statSync as statSync2 } from "fs";
3525
3944
  import { render } from "ink";
3526
- import React10, { useState as useState4 } from "react";
3945
+ import React13, { useState as useState6 } from "react";
3527
3946
 
3528
3947
  // src/cli/ui/App.tsx
3529
- import { Box as Box7, Static, Text as Text7, useApp, useInput as useInput2 } from "ink";
3530
- import React8, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
3948
+ import { Box as Box9, Static, Text as Text9, useApp, useInput as useInput3 } from "ink";
3949
+ import React10, { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState4 } from "react";
3531
3950
 
3532
3951
  // src/cli/ui/EventLog.tsx
3533
3952
  import { Box as Box3, Text as Text3 } from "ink";
@@ -3600,7 +4019,7 @@ function stripMath(s) {
3600
4019
  (_m, n, k) => `C(${n.trim()},${k.trim()})`
3601
4020
  ).replace(/\\sqrt\s*\{([^{}]+)\}/g, (_m, g) => `\u221A(${g.trim()})`).replace(/\\boxed\s*\{([^{}]+)\}/g, (_m, g) => `\u3010${g.trim()}\u3011`).replace(/\\text\s*\{([^{}]+)\}/g, (_m, g) => g.trim()).replace(/\\overline\s*\{([^{}]+)\}/g, (_m, g) => `${g.trim()}\u0304`).replace(/\\hat\s*\{([^{}]+)\}/g, (_m, g) => `${g.trim()}\u0302`).replace(/\\vec\s*\{([^{}]+)\}/g, (_m, g) => `\u2192${g.trim()}`).replace(/\\cdot/g, "\xB7").replace(/\\times/g, "\xD7").replace(/\\div/g, "\xF7").replace(/\\pm/g, "\xB1").replace(/\\mp/g, "\u2213").replace(/\\leq/g, "\u2264").replace(/\\geq/g, "\u2265").replace(/\\neq/g, "\u2260").replace(/\\approx/g, "\u2248").replace(/\\in\b/g, "\u2208").replace(/\\notin\b/g, "\u2209").replace(/\\infty/g, "\u221E").replace(/\\sum\b/g, "\u03A3").replace(/\\prod\b/g, "\u03A0").replace(/\\int\b/g, "\u222B").replace(/\\alpha/g, "\u03B1").replace(/\\beta/g, "\u03B2").replace(/\\gamma/g, "\u03B3").replace(/\\delta/g, "\u03B4").replace(/\\theta/g, "\u03B8").replace(/\\lambda/g, "\u03BB").replace(/\\mu/g, "\u03BC").replace(/\\pi/g, "\u03C0").replace(/\\sigma/g, "\u03C3").replace(/\\phi/g, "\u03C6").replace(/\\omega/g, "\u03C9").replace(/\\implies\b/g, "\u21D2").replace(/\\iff\b/g, "\u21D4").replace(/\\to\b/g, "\u2192").replace(/\\rightarrow/g, "\u2192").replace(/\\Rightarrow/g, "\u21D2").replace(/\\leftarrow/g, "\u2190").replace(/\\Leftarrow/g, "\u21D0").replace(/\\ldots/g, "\u2026").replace(/\\cdots/g, "\u22EF").replace(/\\quad/g, " ").replace(/\\qquad/g, " ").replace(/\\,/g, " ").replace(/\\;/g, " ").replace(/\\!/g, "").replace(/\\\\/g, "\n").replace(/\^\{([\w+-]+)\}/g, (_m, g) => toSuperscript(g)).replace(/\^([0-9+\-n])/g, (_m, g) => toSuperscript(g)).replace(/_\{([\w+-]+)\}/g, (_m, g) => toSubscript(g)).replace(/_([0-9+\-])/g, (_m, g) => toSubscript(g)).replace(/\\[a-zA-Z]+\s*\{([^{}]+)\}\s*\{([^{}]+)\}/g, "($1)/($2)").replace(/\\[a-zA-Z]+\s*\{([^{}]+)\}/g, "$1").replace(/\\[a-zA-Z]+/g, "").replace(/[ \t]{2,}/g, " ");
3602
4021
  }
3603
- var INLINE_RE = /(\*\*([^*\n]+?)\*\*|`([^`\n]+?)`|(?<![*\w])\*([^*\n]+?)\*(?!\w))/g;
4022
+ var INLINE_RE = /(\*\*([^*\n]+?)\*\*|```([^\n]+?)```|`([^`\n]+?)`|(?<![*\w])\*([^*\n]+?)\*(?!\w))/g;
3604
4023
  function InlineMd({ text }) {
3605
4024
  const parts = [];
3606
4025
  let last = 0;
@@ -3615,12 +4034,17 @@ function InlineMd({ text }) {
3615
4034
  /* @__PURE__ */ React2.createElement(Text2, { key: `b${idx++}`, bold: true }, m[2])
3616
4035
  );
3617
4036
  } else if (m[3] !== void 0) {
4037
+ const stripped = m[3].replace(/^(\w+)\s+/, "");
3618
4038
  parts.push(
3619
- /* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, m[3])
4039
+ /* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, stripped)
3620
4040
  );
3621
4041
  } else if (m[4] !== void 0) {
3622
4042
  parts.push(
3623
- /* @__PURE__ */ React2.createElement(Text2, { key: `i${idx++}`, italic: true }, m[4])
4043
+ /* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, m[4])
4044
+ );
4045
+ } else if (m[5] !== void 0) {
4046
+ parts.push(
4047
+ /* @__PURE__ */ React2.createElement(Text2, { key: `i${idx++}`, italic: true }, m[5])
3624
4048
  );
3625
4049
  }
3626
4050
  last = start + m[0].length;
@@ -3638,6 +4062,7 @@ function parseBlocks(raw) {
3638
4062
  let codeLang = "";
3639
4063
  let codeBuf = [];
3640
4064
  let listBuf = null;
4065
+ let codeFence = "";
3641
4066
  const flushPara = () => {
3642
4067
  if (para.length) {
3643
4068
  out.push({ kind: "paragraph", text: para.join(" ") });
@@ -3683,22 +4108,37 @@ function parseBlocks(raw) {
3683
4108
  para.push(filename);
3684
4109
  }
3685
4110
  }
3686
- const fence = line.match(/^```(\w*)/);
3687
- if (fence) {
3688
- if (inCode) {
4111
+ if (!inCode) {
4112
+ const open = line.match(/^ {0,3}(`{3,})(\w*)\s*(.*)$/);
4113
+ if (open) {
4114
+ const fence = open[1];
4115
+ const lang = open[2] ?? "";
4116
+ const rest = open[3] ?? "";
4117
+ const closeOnSame = rest.match(new RegExp(`^(.*?)${fence}\\s*$`));
4118
+ if (closeOnSame) {
4119
+ flushPara();
4120
+ flushList();
4121
+ out.push({ kind: "code", lang, text: (closeOnSame[1] ?? "").trim() });
4122
+ continue;
4123
+ }
4124
+ flushPara();
4125
+ flushList();
4126
+ inCode = true;
4127
+ codeLang = lang;
4128
+ codeFence = fence;
4129
+ if (rest.length > 0) codeBuf.push(rest);
4130
+ continue;
4131
+ }
4132
+ } else {
4133
+ const close = line.match(/^ {0,3}(`{3,})\s*$/);
4134
+ if (close && close[1].length >= codeFence.length) {
3689
4135
  out.push({ kind: "code", lang: codeLang, text: codeBuf.join("\n") });
3690
4136
  codeBuf = [];
3691
4137
  codeLang = "";
4138
+ codeFence = "";
3692
4139
  inCode = false;
3693
- } else {
3694
- flushPara();
3695
- flushList();
3696
- inCode = true;
3697
- codeLang = fence[1] ?? "";
4140
+ continue;
3698
4141
  }
3699
- continue;
3700
- }
3701
- if (inCode) {
3702
4142
  codeBuf.push(rawLine);
3703
4143
  continue;
3704
4144
  }
@@ -3960,7 +4400,7 @@ function StreamingAssistant({ event }) {
3960
4400
  label = parts.join(" \xB7 ");
3961
4401
  labelColor = "green";
3962
4402
  }
3963
- 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) : 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).") : /* @__PURE__ */ React4.createElement(Text3, { dimColor: true, italic: true }, " connection open, first byte typically in 5-60s depending on model + load"));
4403
+ 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);
3964
4404
  }
3965
4405
  function Pulse() {
3966
4406
  const tick = useTick();
@@ -3982,40 +4422,111 @@ function truncate2(s, max) {
3982
4422
 
3983
4423
  // src/cli/ui/PromptInput.tsx
3984
4424
  import { Box as Box4, Text as Text4, useInput } from "ink";
3985
- import React5 from "react";
4425
+ import React5, { useRef, useState as useState2 } from "react";
3986
4426
 
3987
4427
  // src/cli/ui/multiline-keys.ts
3988
4428
  var BACKSLASH_SUFFIX = /\\$/;
3989
- function processMultilineKey(value, key) {
3990
- if (key.tab || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.escape || key.pageUp || key.pageDown) {
3991
- return { next: null, submit: false };
4429
+ var NOOP = { next: null, cursor: null, submit: false };
4430
+ function processMultilineKey(value, cursor, key) {
4431
+ if (key.tab || key.escape || key.pageUp || key.pageDown) {
4432
+ return NOOP;
4433
+ }
4434
+ if (value.length === 0 && (key.upArrow || key.downArrow)) {
4435
+ return NOOP;
4436
+ }
4437
+ if (key.leftArrow) {
4438
+ return { next: null, cursor: Math.max(0, cursor - 1), submit: false };
4439
+ }
4440
+ if (key.rightArrow) {
4441
+ return { next: null, cursor: Math.min(value.length, cursor + 1), submit: false };
4442
+ }
4443
+ if (key.upArrow) {
4444
+ const moved = moveCursorUp(value, cursor);
4445
+ return moved === cursor ? NOOP : { next: null, cursor: moved, submit: false };
4446
+ }
4447
+ if (key.downArrow) {
4448
+ const moved = moveCursorDown(value, cursor);
4449
+ return moved === cursor ? NOOP : { next: null, cursor: moved, submit: false };
4450
+ }
4451
+ if (key.ctrl && key.input === "a") {
4452
+ return { next: null, cursor: startOfLine(value, cursor), submit: false };
4453
+ }
4454
+ if (key.ctrl && key.input === "e") {
4455
+ return { next: null, cursor: endOfLine(value, cursor), submit: false };
3992
4456
  }
3993
4457
  if (key.input === "\n" || key.ctrl && key.input === "j") {
3994
- return { next: `${value}
3995
- `, submit: false };
4458
+ return insertAt(value, cursor, "\n");
3996
4459
  }
3997
4460
  if (key.return) {
3998
- if (key.shift) {
3999
- return { next: `${value}
4000
- `, submit: false };
4001
- }
4002
- if (BACKSLASH_SUFFIX.test(value)) {
4003
- return { next: `${value.slice(0, -1)}
4004
- `, submit: false };
4461
+ if (key.shift) return insertAt(value, cursor, "\n");
4462
+ if (cursor === value.length && BACKSLASH_SUFFIX.test(value)) {
4463
+ const replaced = `${value.slice(0, -1)}
4464
+ `;
4465
+ return { next: replaced, cursor: replaced.length, submit: false };
4005
4466
  }
4006
- return { next: null, submit: true, submitValue: value };
4467
+ return { next: null, cursor: null, submit: true, submitValue: value };
4007
4468
  }
4008
- if (key.backspace || key.delete) {
4009
- if (value.length === 0) return { next: null, submit: false };
4010
- return { next: value.slice(0, -1), submit: false };
4469
+ if (key.backspace || key.delete || key.input === "\x7F" || key.input === "\b") {
4470
+ if (cursor === 0) return NOOP;
4471
+ return {
4472
+ next: value.slice(0, cursor - 1) + value.slice(cursor),
4473
+ cursor: cursor - 1,
4474
+ submit: false
4475
+ };
4011
4476
  }
4012
- if ((key.ctrl || key.meta) && key.input.length === 0) {
4013
- return { next: null, submit: false };
4477
+ if ((key.ctrl || key.meta) && key.input.length === 0) return NOOP;
4478
+ if (key.ctrl || key.meta) return NOOP;
4479
+ if (key.input.length > 0) {
4480
+ return insertAt(value, cursor, key.input);
4014
4481
  }
4015
- if (key.input.length > 0 && !key.ctrl && !key.meta) {
4016
- return { next: value + key.input, submit: false };
4482
+ return NOOP;
4483
+ }
4484
+ function insertAt(value, cursor, insert) {
4485
+ return {
4486
+ next: value.slice(0, cursor) + insert + value.slice(cursor),
4487
+ cursor: cursor + insert.length,
4488
+ submit: false
4489
+ };
4490
+ }
4491
+ function lineAndColumn(value, cursor) {
4492
+ let line = 0;
4493
+ let col = 0;
4494
+ const n = Math.min(cursor, value.length);
4495
+ for (let i = 0; i < n; i++) {
4496
+ if (value[i] === "\n") {
4497
+ line++;
4498
+ col = 0;
4499
+ } else {
4500
+ col++;
4501
+ }
4017
4502
  }
4018
- return { next: null, submit: false };
4503
+ return { line, col };
4504
+ }
4505
+ function startOfLine(value, cursor) {
4506
+ return value.lastIndexOf("\n", cursor - 1) + 1;
4507
+ }
4508
+ function endOfLine(value, cursor) {
4509
+ const nl = value.indexOf("\n", cursor);
4510
+ return nl === -1 ? value.length : nl;
4511
+ }
4512
+ function moveCursorUp(value, cursor) {
4513
+ const curStart = startOfLine(value, cursor);
4514
+ if (curStart === 0) return cursor;
4515
+ const col = cursor - curStart;
4516
+ const prevEnd = curStart - 1;
4517
+ const prevStart = value.lastIndexOf("\n", prevEnd - 1) + 1;
4518
+ const prevLen = prevEnd - prevStart;
4519
+ return prevStart + Math.min(col, prevLen);
4520
+ }
4521
+ function moveCursorDown(value, cursor) {
4522
+ const nextNl = value.indexOf("\n", cursor);
4523
+ if (nextNl === -1) return cursor;
4524
+ const curStart = startOfLine(value, cursor);
4525
+ const col = cursor - curStart;
4526
+ const nextStart = nextNl + 1;
4527
+ const followingNl = value.indexOf("\n", nextStart);
4528
+ const nextLen = (followingNl === -1 ? value.length : followingNl) - nextStart;
4529
+ return nextStart + Math.min(col, nextLen);
4019
4530
  }
4020
4531
 
4021
4532
  // src/cli/ui/PromptInput.tsx
@@ -4026,11 +4537,19 @@ function PromptInput({
4026
4537
  disabled,
4027
4538
  placeholder
4028
4539
  }) {
4540
+ const [cursor, setCursor] = useState2(value.length);
4541
+ const lastLocalValueRef = useRef(value);
4542
+ if (value !== lastLocalValueRef.current) {
4543
+ lastLocalValueRef.current = value;
4544
+ if (cursor !== value.length) {
4545
+ setCursor(value.length);
4546
+ }
4547
+ }
4029
4548
  const tick = useTick();
4030
4549
  const showCursor = disabled ? false : Math.floor(tick / 4) % 2 === 0;
4031
4550
  useInput(
4032
4551
  (input, key) => {
4033
- const keyEvent = {
4552
+ const ke = {
4034
4553
  input,
4035
4554
  return: key.return,
4036
4555
  shift: key.shift,
@@ -4047,8 +4566,14 @@ function PromptInput({
4047
4566
  pageUp: key.pageUp,
4048
4567
  pageDown: key.pageDown
4049
4568
  };
4050
- const action = processMultilineKey(value, keyEvent);
4051
- if (action.next !== null) onChange(action.next);
4569
+ const action = processMultilineKey(value, cursor, ke);
4570
+ if (action.next !== null) {
4571
+ lastLocalValueRef.current = action.next;
4572
+ onChange(action.next);
4573
+ }
4574
+ if (action.cursor !== null) {
4575
+ setCursor(action.cursor);
4576
+ }
4052
4577
  if (action.submit) onSubmit(action.submitValue ?? value);
4053
4578
  },
4054
4579
  { isActive: !disabled }
@@ -4056,27 +4581,211 @@ function PromptInput({
4056
4581
  const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command \xB7 Ctrl+J for newline";
4057
4582
  const lines = value.length > 0 ? value.split("\n") : [""];
4058
4583
  const borderColor = disabled ? "gray" : "cyan";
4584
+ const { line: cursorLine, col: cursorCol } = lineAndColumn(value, cursor);
4059
4585
  return /* @__PURE__ */ React5.createElement(Box4, { borderStyle: "round", borderColor, paddingX: 1, flexDirection: "column" }, lines.map((line, i) => {
4060
- const isLast = i === lines.length - 1;
4061
4586
  const isFirst = i === 0;
4062
4587
  const showPlaceholder = isFirst && value.length === 0;
4588
+ const isCursorLine = i === cursorLine;
4063
4589
  return (
4064
4590
  // biome-ignore lint/suspicious/noArrayIndexKey: stable by construction — lines are derived from `value.split("\n")` and never reordered
4065
- /* @__PURE__ */ React5.createElement(Box4, { key: i }, isFirst ? /* @__PURE__ */ React5.createElement(Text4, { bold: true, color: borderColor }, "you \u203A", " ") : /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, " "), showPlaceholder && isLast && !disabled ? /* @__PURE__ */ React5.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " ") : null, showPlaceholder ? /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, effectivePlaceholder) : /* @__PURE__ */ React5.createElement(Text4, null, line), !showPlaceholder && isLast && !disabled ? /* @__PURE__ */ React5.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " ") : null)
4591
+ /* @__PURE__ */ React5.createElement(Box4, { key: i }, isFirst ? /* @__PURE__ */ React5.createElement(Text4, { bold: true, color: borderColor }, "you \u203A", " ") : /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, " "), showPlaceholder ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, isCursorLine && !disabled ? /* @__PURE__ */ React5.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " ") : null, /* @__PURE__ */ React5.createElement(Text4, { dimColor: true }, effectivePlaceholder)) : isCursorLine && !disabled ? /* @__PURE__ */ React5.createElement(
4592
+ LineWithCursor,
4593
+ {
4594
+ line,
4595
+ col: cursorCol,
4596
+ showCursor,
4597
+ borderColor
4598
+ }
4599
+ ) : /* @__PURE__ */ React5.createElement(Text4, null, line))
4066
4600
  );
4067
4601
  }));
4068
4602
  }
4603
+ function LineWithCursor({
4604
+ line,
4605
+ col,
4606
+ showCursor,
4607
+ borderColor
4608
+ }) {
4609
+ const before = line.slice(0, col);
4610
+ const atCursor = line.slice(col, col + 1);
4611
+ const after = line.slice(col + 1);
4612
+ if (atCursor.length === 0) {
4613
+ return /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text4, null, before), /* @__PURE__ */ React5.createElement(Text4, { color: borderColor }, showCursor ? "\u258C" : " "));
4614
+ }
4615
+ return /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text4, null, before), /* @__PURE__ */ React5.createElement(Text4, { inverse: showCursor }, atCursor), /* @__PURE__ */ React5.createElement(Text4, null, after));
4616
+ }
4617
+
4618
+ // src/cli/ui/ShellConfirm.tsx
4619
+ import { Box as Box6, Text as Text6 } from "ink";
4620
+ import React7 from "react";
4621
+
4622
+ // src/cli/ui/Select.tsx
4623
+ import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
4624
+ import React6, { useState as useState3 } from "react";
4625
+ function SingleSelect({
4626
+ items,
4627
+ initialValue,
4628
+ onSubmit,
4629
+ onCancel
4630
+ }) {
4631
+ const initialIndex = Math.max(
4632
+ 0,
4633
+ items.findIndex((i) => i.value === initialValue && !i.disabled)
4634
+ );
4635
+ const [index, setIndex] = useState3(initialIndex === -1 ? 0 : initialIndex);
4636
+ useInput2((_input, key) => {
4637
+ if (key.upArrow) {
4638
+ setIndex((i) => findNextEnabled(items, i, -1));
4639
+ } else if (key.downArrow) {
4640
+ setIndex((i) => findNextEnabled(items, i, 1));
4641
+ } else if (key.return) {
4642
+ const chosen = items[index];
4643
+ if (chosen && !chosen.disabled) onSubmit(chosen.value);
4644
+ } else if (key.escape && onCancel) {
4645
+ onCancel();
4646
+ }
4647
+ });
4648
+ return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React6.createElement(
4649
+ SelectRow,
4650
+ {
4651
+ key: item.value,
4652
+ item,
4653
+ active: i === index,
4654
+ marker: i === index ? "\u25B8" : " "
4655
+ }
4656
+ )));
4657
+ }
4658
+ function MultiSelect({
4659
+ items,
4660
+ initialSelected = [],
4661
+ onSubmit,
4662
+ onCancel,
4663
+ footer
4664
+ }) {
4665
+ const [index, setIndex] = useState3(() => {
4666
+ const first = items.findIndex((i) => !i.disabled);
4667
+ return first === -1 ? 0 : first;
4668
+ });
4669
+ const [selected, setSelected] = useState3(new Set(initialSelected));
4670
+ useInput2((input, key) => {
4671
+ if (key.upArrow) {
4672
+ setIndex((i) => findNextEnabled(items, i, -1));
4673
+ } else if (key.downArrow) {
4674
+ setIndex((i) => findNextEnabled(items, i, 1));
4675
+ } else if (input === " ") {
4676
+ const item = items[index];
4677
+ if (!item || item.disabled) return;
4678
+ setSelected((prev) => {
4679
+ const next = new Set(prev);
4680
+ if (next.has(item.value)) next.delete(item.value);
4681
+ else next.add(item.value);
4682
+ return next;
4683
+ });
4684
+ } else if (key.return) {
4685
+ const ordered = items.filter((i) => selected.has(i.value)).map((i) => i.value);
4686
+ onSubmit(ordered);
4687
+ } else if (key.escape && onCancel) {
4688
+ onCancel();
4689
+ }
4690
+ });
4691
+ return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column" }, items.map((item, i) => {
4692
+ const checked = selected.has(item.value);
4693
+ const marker = checked ? "[x]" : "[ ]";
4694
+ return /* @__PURE__ */ React6.createElement(
4695
+ SelectRow,
4696
+ {
4697
+ key: item.value,
4698
+ item,
4699
+ active: i === index,
4700
+ marker: `${i === index ? "\u25B8" : " "} ${marker}`
4701
+ }
4702
+ );
4703
+ }), footer ? /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, footer)) : null);
4704
+ }
4705
+ function SelectRow({
4706
+ item,
4707
+ active,
4708
+ marker
4709
+ }) {
4710
+ const color = item.disabled ? "gray" : active ? "cyan" : void 0;
4711
+ return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React6.createElement(Box5, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, item.hint)) : null);
4712
+ }
4713
+ function findNextEnabled(items, from, step) {
4714
+ if (items.length === 0) return 0;
4715
+ let i = from;
4716
+ for (let tries = 0; tries < items.length; tries++) {
4717
+ i = (i + step + items.length) % items.length;
4718
+ if (!items[i]?.disabled) return i;
4719
+ }
4720
+ return from;
4721
+ }
4722
+
4723
+ // src/cli/ui/ShellConfirm.tsx
4724
+ function ShellConfirm({ command, allowPrefix, onChoose }) {
4725
+ return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React7.createElement(Box6, null, /* @__PURE__ */ React7.createElement(Text6, { bold: true, color: "yellow" }, "\u25B8 model wants to run a shell command")), /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text6, null, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "$ "), /* @__PURE__ */ React7.createElement(Text6, { color: "cyan" }, command))), /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(
4726
+ SingleSelect,
4727
+ {
4728
+ initialValue: "run_once",
4729
+ items: [
4730
+ {
4731
+ value: "run_once",
4732
+ label: "Run once",
4733
+ hint: "Execute this command, don't remember it."
4734
+ },
4735
+ {
4736
+ value: "always_allow",
4737
+ label: `Always allow "${allowPrefix}" in this project`,
4738
+ hint: "Save the prefix to ~/.reasonix/config.json; future matches auto-run."
4739
+ },
4740
+ {
4741
+ value: "deny",
4742
+ label: "Deny",
4743
+ hint: "Tell the model the user refused; it will continue without this command."
4744
+ }
4745
+ ],
4746
+ onSubmit: (v) => onChoose(v)
4747
+ }
4748
+ )));
4749
+ }
4750
+ function derivePrefix(command) {
4751
+ const tokens = command.trim().split(/\s+/).filter(Boolean);
4752
+ if (tokens.length === 0) return "";
4753
+ if (tokens.length === 1) return tokens[0];
4754
+ const first = tokens[0];
4755
+ const TWO_TOKEN_WRAPPERS = /* @__PURE__ */ new Set([
4756
+ "npm",
4757
+ "npx",
4758
+ "pnpm",
4759
+ "yarn",
4760
+ "bun",
4761
+ "git",
4762
+ "cargo",
4763
+ "go",
4764
+ "docker",
4765
+ "kubectl",
4766
+ "python",
4767
+ "python3",
4768
+ "deno",
4769
+ "pip",
4770
+ "pip3",
4771
+ "make",
4772
+ "rake",
4773
+ "bundle",
4774
+ "gem"
4775
+ ]);
4776
+ return TWO_TOKEN_WRAPPERS.has(first) ? `${first} ${tokens[1]}` : first;
4777
+ }
4069
4778
 
4070
4779
  // src/cli/ui/SlashSuggestions.tsx
4071
- import { Box as Box5, Text as Text5 } from "ink";
4072
- import React6 from "react";
4780
+ import { Box as Box7, Text as Text7 } from "ink";
4781
+ import React8 from "react";
4073
4782
  function SlashSuggestions({
4074
4783
  matches,
4075
4784
  selectedIndex
4076
4785
  }) {
4077
4786
  if (matches === null) return null;
4078
4787
  if (matches.length === 0) {
4079
- return /* @__PURE__ */ React6.createElement(Box5, { paddingX: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, "no slash command matches that prefix"), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " \u2014 Backspace to edit, or /help for the full list"));
4788
+ return /* @__PURE__ */ React8.createElement(Box7, { paddingX: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "yellow" }, "no slash command matches that prefix"), /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \u2014 Backspace to edit, or /help for the full list"));
4080
4789
  }
4081
4790
  const MAX = 8;
4082
4791
  const total = matches.length;
@@ -4084,21 +4793,21 @@ function SlashSuggestions({
4084
4793
  const shown = matches.slice(windowStart, windowStart + MAX);
4085
4794
  const hiddenAbove = windowStart;
4086
4795
  const hiddenBelow = total - windowStart - shown.length;
4087
- return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React6.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, " \u2191/\u2193 navigate \xB7 Tab or Enter to pick"));
4796
+ return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", paddingX: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((spec, i) => /* @__PURE__ */ React8.createElement(SuggestionRow, { key: spec.cmd, spec, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \u2191/\u2193 navigate \xB7 Tab or Enter to pick"));
4088
4797
  }
4089
4798
  function SuggestionRow({ spec, isSelected }) {
4090
4799
  const marker = isSelected ? "\u25B8" : " ";
4091
4800
  const name = `/${spec.cmd}`;
4092
4801
  const argsSuffix = spec.argsHint ? ` ${spec.argsHint}` : "";
4093
4802
  if (isSelected) {
4094
- return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16)), /* @__PURE__ */ React6.createElement(Text5, { color: "cyan" }, " ", spec.summary));
4803
+ return /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "cyan" }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16)), /* @__PURE__ */ React8.createElement(Text7, { color: "cyan" }, " ", spec.summary));
4095
4804
  }
4096
- return /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16), " ", spec.summary));
4805
+ return /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, marker, " ", name.padEnd(12), argsSuffix.padEnd(16), " ", spec.summary));
4097
4806
  }
4098
4807
 
4099
4808
  // src/cli/ui/StatsPanel.tsx
4100
- import { Box as Box6, Text as Text6 } from "ink";
4101
- import React7 from "react";
4809
+ import { Box as Box8, Text as Text8 } from "ink";
4810
+ import React9 from "react";
4102
4811
  function StatsPanel({
4103
4812
  summary,
4104
4813
  model,
@@ -4113,7 +4822,7 @@ function StatsPanel({
4113
4822
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
4114
4823
  const ctxRatio = summary.lastPromptTokens / ctxMax;
4115
4824
  const ctxColor = ctxRatio >= 0.8 ? "red" : ctxRatio >= 0.5 ? "yellow" : void 0;
4116
- return /* @__PURE__ */ React7.createElement(Box6, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React7.createElement(Box6, { justifyContent: "space-between" }, /* @__PURE__ */ React7.createElement(Text6, null, /* @__PURE__ */ React7.createElement(Text6, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React7.createElement(Text6, { color: "yellow" }, model), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React7.createElement(Text6, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React7.createElement(Text6, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React7.createElement(Box6, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React7.createElement(Text6, null, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "cache hit "), /* @__PURE__ */ React7.createElement(Text6, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React7.createElement(Text6, null, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "cost "), /* @__PURE__ */ React7.createElement(Text6, { color: "green", bold: true }, "$", summary.totalCostUsd.toFixed(6)), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, " (in ", "$", summary.totalInputCostUsd.toFixed(6), " \xB7 out ", "$", summary.totalOutputCostUsd.toFixed(6), ")")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React7.createElement(Text6, null, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "ctx "), /* @__PURE__ */ React7.createElement(Text6, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React7.createElement(Text6, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null, balance ? /* @__PURE__ */ React7.createElement(Text6, null, /* @__PURE__ */ React7.createElement(Text6, { dimColor: true }, "balance "), /* @__PURE__ */ React7.createElement(Text6, { 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));
4825
+ return /* @__PURE__ */ React9.createElement(Box8, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React9.createElement(Box8, { justifyContent: "space-between" }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, model), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React9.createElement(Text8, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "cache hit "), /* @__PURE__ */ React9.createElement(Text8, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "cost "), /* @__PURE__ */ React9.createElement(Text8, { color: "green", bold: true }, "$", summary.totalCostUsd.toFixed(6)), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " (in ", "$", summary.totalInputCostUsd.toFixed(6), " \xB7 out ", "$", summary.totalOutputCostUsd.toFixed(6), ")")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "ctx "), /* @__PURE__ */ React9.createElement(Text8, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React9.createElement(Text8, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null, balance ? /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "balance "), /* @__PURE__ */ React9.createElement(Text8, { 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));
4117
4826
  }
4118
4827
  function formatTokens(n) {
4119
4828
  if (n < 1e3) return String(n);
@@ -4577,23 +5286,25 @@ function App({
4577
5286
  codeMode
4578
5287
  }) {
4579
5288
  const { exit } = useApp();
4580
- const [historical, setHistorical] = useState2([]);
4581
- const [streaming, setStreaming] = useState2(null);
4582
- const [input, setInput] = useState2("");
4583
- const [busy, setBusy] = useState2(false);
4584
- const abortedThisTurn = useRef(false);
4585
- const [ongoingTool, setOngoingTool] = useState2(null);
4586
- const [toolProgress, setToolProgress] = useState2(null);
4587
- const [statusLine, setStatusLine] = useState2(null);
4588
- const [balance, setBalance] = useState2(null);
4589
- const lastEditSnapshots = useRef(null);
4590
- const pendingEdits = useRef([]);
4591
- const promptHistory = useRef([]);
4592
- const historyCursor = useRef(-1);
4593
- const assistantIterCounter = useRef(0);
4594
- const toolHistoryRef = useRef([]);
4595
- const [slashSelected, setSlashSelected] = useState2(0);
4596
- const [summary, setSummary] = useState2({
5289
+ const [historical, setHistorical] = useState4([]);
5290
+ const [streaming, setStreaming] = useState4(null);
5291
+ const [input, setInput] = useState4("");
5292
+ const [busy, setBusy] = useState4(false);
5293
+ const abortedThisTurn = useRef2(false);
5294
+ const [ongoingTool, setOngoingTool] = useState4(null);
5295
+ const [toolProgress, setToolProgress] = useState4(null);
5296
+ const [statusLine, setStatusLine] = useState4(null);
5297
+ const [balance, setBalance] = useState4(null);
5298
+ const lastEditSnapshots = useRef2(null);
5299
+ const pendingEdits = useRef2([]);
5300
+ const [pendingShell, setPendingShell] = useState4(null);
5301
+ const [queuedSubmit, setQueuedSubmit] = useState4(null);
5302
+ const promptHistory = useRef2([]);
5303
+ const historyCursor = useRef2(-1);
5304
+ const assistantIterCounter = useRef2(0);
5305
+ const toolHistoryRef = useRef2([]);
5306
+ const [slashSelected, setSlashSelected] = useState4(0);
5307
+ const [summary, setSummary] = useState4({
4597
5308
  turns: 0,
4598
5309
  totalCostUsd: 0,
4599
5310
  totalInputCostUsd: 0,
@@ -4603,7 +5314,7 @@ function App({
4603
5314
  cacheHitRatio: 0,
4604
5315
  lastPromptTokens: 0
4605
5316
  });
4606
- const transcriptRef = useRef(null);
5317
+ const transcriptRef = useRef2(null);
4607
5318
  if (transcript && !transcriptRef.current) {
4608
5319
  transcriptRef.current = openTranscriptFile(transcript, {
4609
5320
  version: 1,
@@ -4628,7 +5339,7 @@ function App({
4628
5339
  return prev;
4629
5340
  });
4630
5341
  }, [slashMatches]);
4631
- const loopRef = useRef(null);
5342
+ const loopRef = useRef2(null);
4632
5343
  const loop = useMemo(() => {
4633
5344
  if (loopRef.current) return loopRef.current;
4634
5345
  const client = new DeepSeekClient();
@@ -4665,7 +5376,7 @@ function App({
4665
5376
  if (progressSink.current) progressSink.current = null;
4666
5377
  };
4667
5378
  }, [progressSink]);
4668
- const sessionBannerShown = useRef(false);
5379
+ const sessionBannerShown = useRef2(false);
4669
5380
  useEffect2(() => {
4670
5381
  if (sessionBannerShown.current) return;
4671
5382
  sessionBannerShown.current = true;
@@ -4698,7 +5409,7 @@ function App({
4698
5409
  ]);
4699
5410
  }
4700
5411
  }, [session, loop]);
4701
- useInput2((_input, key) => {
5412
+ useInput3((_input, key) => {
4702
5413
  if (key.escape && busy) {
4703
5414
  if (abortedThisTurn.current) return;
4704
5415
  abortedThisTurn.current = true;
@@ -4706,6 +5417,7 @@ function App({
4706
5417
  return;
4707
5418
  }
4708
5419
  if (busy) return;
5420
+ if (pendingShell) return;
4709
5421
  if (slashMatches && slashMatches.length > 0) {
4710
5422
  if (key.upArrow) {
4711
5423
  setSlashSelected((i) => Math.max(0, i - 1));
@@ -4721,20 +5433,22 @@ function App({
4721
5433
  return;
4722
5434
  }
4723
5435
  }
4724
- const hist = promptHistory.current;
4725
- if (key.upArrow) {
4726
- if (hist.length === 0) return;
4727
- const nextCursor = Math.min(historyCursor.current + 1, hist.length - 1);
4728
- historyCursor.current = nextCursor;
4729
- setInput(hist[hist.length - 1 - nextCursor] ?? "");
4730
- return;
4731
- }
4732
- if (key.downArrow) {
4733
- if (historyCursor.current < 0) return;
4734
- const nextCursor = historyCursor.current - 1;
4735
- historyCursor.current = nextCursor;
4736
- setInput(nextCursor < 0 ? "" : hist[hist.length - 1 - nextCursor] ?? "");
4737
- return;
5436
+ if (input.length === 0) {
5437
+ const hist = promptHistory.current;
5438
+ if (key.upArrow) {
5439
+ if (hist.length === 0) return;
5440
+ const nextCursor = Math.min(historyCursor.current + 1, hist.length - 1);
5441
+ historyCursor.current = nextCursor;
5442
+ setInput(hist[hist.length - 1 - nextCursor] ?? "");
5443
+ return;
5444
+ }
5445
+ if (key.downArrow) {
5446
+ if (historyCursor.current < 0) return;
5447
+ const nextCursor = historyCursor.current - 1;
5448
+ historyCursor.current = nextCursor;
5449
+ setInput(nextCursor < 0 ? "" : hist[hist.length - 1 - nextCursor] ?? "");
5450
+ return;
5451
+ }
4738
5452
  }
4739
5453
  });
4740
5454
  const codeUndo = useCallback(() => {
@@ -4973,6 +5687,15 @@ function App({
4973
5687
  toolName: ev.toolName
4974
5688
  }
4975
5689
  ]);
5690
+ if (codeMode && ev.toolName === "run_command" && ev.content.includes('"NeedsConfirmationError:') && ev.toolArgs) {
5691
+ try {
5692
+ const parsed = JSON.parse(ev.toolArgs);
5693
+ if (typeof parsed.command === "string" && parsed.command.trim()) {
5694
+ setPendingShell(parsed.command.trim());
5695
+ }
5696
+ } catch {
5697
+ }
5698
+ }
4976
5699
  } else if (ev.role === "error") {
4977
5700
  setHistorical((prev) => [
4978
5701
  ...prev,
@@ -5017,7 +5740,68 @@ function App({
5017
5740
  writeTranscript
5018
5741
  ]
5019
5742
  );
5020
- return /* @__PURE__ */ React8.createElement(TickerProvider, { disabled: PLAIN_UI }, /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column" }, /* @__PURE__ */ React8.createElement(
5743
+ const handleShellConfirm = useCallback(
5744
+ async (choice) => {
5745
+ const cmd = pendingShell;
5746
+ if (!cmd || !codeMode) return;
5747
+ setPendingShell(null);
5748
+ let synthetic;
5749
+ if (choice === "deny") {
5750
+ setHistorical((prev) => [
5751
+ ...prev,
5752
+ { id: `sh-deny-${Date.now()}`, role: "info", text: `\u25B8 denied: ${cmd}` }
5753
+ ]);
5754
+ synthetic = `I denied running \`${cmd}\`. Please continue without running it.`;
5755
+ } else {
5756
+ if (choice === "always_allow") {
5757
+ const prefix = derivePrefix(cmd);
5758
+ addProjectShellAllowed(codeMode.rootDir, prefix);
5759
+ setHistorical((prev) => [
5760
+ ...prev,
5761
+ {
5762
+ id: `sh-allow-${Date.now()}`,
5763
+ role: "info",
5764
+ text: `\u25B8 always allowed "${prefix}" for ${codeMode.rootDir}`
5765
+ }
5766
+ ]);
5767
+ }
5768
+ setHistorical((prev) => [
5769
+ ...prev,
5770
+ { id: `sh-run-${Date.now()}`, role: "info", text: `\u25B8 running: ${cmd}` }
5771
+ ]);
5772
+ let body;
5773
+ try {
5774
+ const res = await runCommand(cmd, { cwd: codeMode.rootDir });
5775
+ body = formatCommandResult(cmd, res);
5776
+ } catch (err) {
5777
+ body = `$ ${cmd}
5778
+ [failed to spawn] ${err.message}`;
5779
+ }
5780
+ setHistorical((prev) => [
5781
+ ...prev,
5782
+ { id: `sh-out-${Date.now()}`, role: "info", text: body }
5783
+ ]);
5784
+ synthetic = `I ran the command you requested. Output:
5785
+
5786
+ ${body}`;
5787
+ }
5788
+ if (busy) {
5789
+ loop.abort();
5790
+ setQueuedSubmit(synthetic);
5791
+ } else {
5792
+ await handleSubmit(synthetic);
5793
+ }
5794
+ },
5795
+ [pendingShell, codeMode, handleSubmit, busy, loop]
5796
+ );
5797
+ useEffect2(() => {
5798
+ if (!busy && queuedSubmit !== null) {
5799
+ const text = queuedSubmit;
5800
+ setQueuedSubmit(null);
5801
+ void handleSubmit(text);
5802
+ }
5803
+ }, [busy, queuedSubmit, handleSubmit]);
5804
+ return /* @__PURE__ */ React10.createElement(TickerProvider, { disabled: PLAIN_UI }, /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React10.createElement(
5021
5805
  StatsPanel,
5022
5806
  {
5023
5807
  summary,
@@ -5027,13 +5811,28 @@ function App({
5027
5811
  branchBudget: loop.branchOptions.budget,
5028
5812
  balance
5029
5813
  }
5030
- ), /* @__PURE__ */ React8.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React8.createElement(EventRow, { key: item.id, event: item })), !PLAIN_UI && streaming ? /* @__PURE__ */ React8.createElement(Box7, { marginY: 1 }, /* @__PURE__ */ React8.createElement(EventRow, { event: streaming })) : null, !PLAIN_UI && ongoingTool ? /* @__PURE__ */ React8.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !ongoingTool && statusLine ? /* @__PURE__ */ React8.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React8.createElement(StatusRow, { text: "processing\u2026" }) : null, /* @__PURE__ */ React8.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React8.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected })));
5814
+ ), /* @__PURE__ */ React10.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React10.createElement(EventRow, { key: item.id, event: item })), !PLAIN_UI && !pendingShell && streaming ? /* @__PURE__ */ React10.createElement(Box9, { marginY: 1 }, /* @__PURE__ */ React10.createElement(EventRow, { event: streaming })) : null, !PLAIN_UI && !pendingShell && ongoingTool ? /* @__PURE__ */ React10.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !ongoingTool && statusLine ? /* @__PURE__ */ React10.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && !pendingShell && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React10.createElement(StatusRow, { text: "processing\u2026" }) : null, pendingShell ? /* @__PURE__ */ React10.createElement(
5815
+ ShellConfirm,
5816
+ {
5817
+ command: pendingShell,
5818
+ allowPrefix: derivePrefix(pendingShell),
5819
+ onChoose: handleShellConfirm
5820
+ }
5821
+ ) : /* @__PURE__ */ React10.createElement(React10.Fragment, null, /* @__PURE__ */ React10.createElement(
5822
+ PromptInput,
5823
+ {
5824
+ value: input,
5825
+ onChange: setInput,
5826
+ onSubmit: handleSubmit,
5827
+ disabled: busy
5828
+ }
5829
+ ), /* @__PURE__ */ React10.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }))));
5031
5830
  }
5032
5831
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
5033
5832
  function StatusRow({ text }) {
5034
5833
  const tick = useTick();
5035
5834
  const elapsed = useElapsedSeconds();
5036
- return /* @__PURE__ */ React8.createElement(Box7, { marginY: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React8.createElement(Text7, { color: "magenta" }, ` ${text}`), /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, ` ${elapsed}s`));
5835
+ return /* @__PURE__ */ React10.createElement(Box9, { marginY: 1 }, /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, ` ${text}`), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, ` ${elapsed}s`));
5037
5836
  }
5038
5837
  function OngoingToolRow({
5039
5838
  tool,
@@ -5042,7 +5841,7 @@ function OngoingToolRow({
5042
5841
  const tick = useTick();
5043
5842
  const elapsed = useElapsedSeconds();
5044
5843
  const summary = summarizeToolArgs(tool.name, tool.args);
5045
- return /* @__PURE__ */ React8.createElement(Box7, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { color: "cyan" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React8.createElement(Text7, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, ` ${elapsed}s`)), progress ? /* @__PURE__ */ React8.createElement(Box7, { paddingLeft: 2 }, /* @__PURE__ */ React8.createElement(Text7, { color: "cyan" }, renderProgressLine(progress))) : null, summary ? /* @__PURE__ */ React8.createElement(Box7, { paddingLeft: 2 }, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, summary)) : null);
5844
+ return /* @__PURE__ */ React10.createElement(Box9, { marginY: 1, flexDirection: "column" }, /* @__PURE__ */ React10.createElement(Box9, null, /* @__PURE__ */ React10.createElement(Text9, { color: "cyan" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React10.createElement(Text9, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, ` ${elapsed}s`)), progress ? /* @__PURE__ */ React10.createElement(Box9, { paddingLeft: 2 }, /* @__PURE__ */ React10.createElement(Text9, { color: "cyan" }, renderProgressLine(progress))) : null, summary ? /* @__PURE__ */ React10.createElement(Box9, { paddingLeft: 2 }, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, summary)) : null);
5046
5845
  }
5047
5846
  function renderProgressLine(p) {
5048
5847
  const msg = p.message ? ` ${p.message}` : "";
@@ -5137,13 +5936,60 @@ function describeRepair(repair) {
5137
5936
  return parts.length ? `[repair] ${parts.join(", ")}` : "";
5138
5937
  }
5139
5938
 
5939
+ // src/cli/ui/SessionPicker.tsx
5940
+ import { Box as Box10, Text as Text10 } from "ink";
5941
+ import React11 from "react";
5942
+ function SessionPicker({
5943
+ sessionName,
5944
+ messageCount,
5945
+ lastActive,
5946
+ onChoose
5947
+ }) {
5948
+ return /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React11.createElement(Box10, { marginBottom: 1 }, /* @__PURE__ */ React11.createElement(Text10, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, ` \xB7 last active ${relativeTime(lastActive)}`)), /* @__PURE__ */ React11.createElement(
5949
+ SingleSelect,
5950
+ {
5951
+ initialValue: "new",
5952
+ items: [
5953
+ {
5954
+ value: "new",
5955
+ label: "Start new conversation",
5956
+ hint: "Previous messages kept on disk; your turn starts fresh."
5957
+ },
5958
+ {
5959
+ value: "resume",
5960
+ label: "Resume",
5961
+ hint: `Continue where you left off (${messageCount} messages in context).`
5962
+ },
5963
+ {
5964
+ value: "delete",
5965
+ label: "Delete and start new",
5966
+ hint: "Wipes the session file irreversibly. Other sessions untouched."
5967
+ }
5968
+ ],
5969
+ onSubmit: (v) => onChoose(v)
5970
+ }
5971
+ ), /* @__PURE__ */ React11.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text10, { dimColor: true }, "\u2191\u2193 to move \xB7 Enter to pick")));
5972
+ }
5973
+ function relativeTime(date) {
5974
+ const ms = Date.now() - date.getTime();
5975
+ const mins = Math.floor(ms / 6e4);
5976
+ if (mins < 1) return "just now";
5977
+ if (mins < 60) return `${mins}m ago`;
5978
+ const hours = Math.floor(mins / 60);
5979
+ if (hours < 24) return `${hours}h ago`;
5980
+ const days = Math.floor(hours / 24);
5981
+ if (days === 1) return "yesterday";
5982
+ if (days < 7) return `${days}d ago`;
5983
+ return date.toISOString().slice(0, 10);
5984
+ }
5985
+
5140
5986
  // src/cli/ui/Setup.tsx
5141
- import { Box as Box8, Text as Text8, useApp as useApp2 } from "ink";
5987
+ import { Box as Box11, Text as Text11, useApp as useApp2 } from "ink";
5142
5988
  import TextInput from "ink-text-input";
5143
- import React9, { useState as useState3 } from "react";
5989
+ import React12, { useState as useState5 } from "react";
5144
5990
  function Setup({ onReady }) {
5145
- const [value, setValue] = useState3("");
5146
- const [error, setError] = useState3(null);
5991
+ const [value, setValue] = useState5("");
5992
+ const [error, setError] = useState5(null);
5147
5993
  const { exit } = useApp2();
5148
5994
  const handleSubmit = (raw) => {
5149
5995
  const trimmed = raw.trim();
@@ -5164,7 +6010,7 @@ function Setup({ onReady }) {
5164
6010
  }
5165
6011
  onReady(trimmed);
5166
6012
  };
5167
- return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React9.createElement(
6013
+ return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text11, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React12.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text11, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React12.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text11, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React12.createElement(
5168
6014
  TextInput,
5169
6015
  {
5170
6016
  value,
@@ -5173,14 +6019,23 @@ function Setup({ onReady }) {
5173
6019
  mask: "\u2022",
5174
6020
  placeholder: "sk-..."
5175
6021
  }
5176
- )), error ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "red" }, error)) : value ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "(Type /exit to abort.)")));
6022
+ )), error ? /* @__PURE__ */ React12.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text11, { color: "red" }, error)) : value ? /* @__PURE__ */ React12.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React12.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, "(Type /exit to abort.)")));
5177
6023
  }
5178
6024
 
5179
6025
  // src/cli/commands/chat.tsx
5180
- function Root({ initialKey, tools, mcpSpecs, mcpServers, progressSink, ...appProps }) {
5181
- const [key, setKey] = useState4(initialKey);
6026
+ function Root({
6027
+ initialKey,
6028
+ tools,
6029
+ mcpSpecs,
6030
+ mcpServers,
6031
+ progressSink,
6032
+ sessionPreview,
6033
+ ...appProps
6034
+ }) {
6035
+ const [key, setKey] = useState6(initialKey);
6036
+ const [pending, setPending] = useState6(sessionPreview);
5182
6037
  if (!key) {
5183
- return /* @__PURE__ */ React10.createElement(
6038
+ return /* @__PURE__ */ React13.createElement(
5184
6039
  Setup,
5185
6040
  {
5186
6041
  onReady: (k) => {
@@ -5191,7 +6046,23 @@ function Root({ initialKey, tools, mcpSpecs, mcpServers, progressSink, ...appPro
5191
6046
  );
5192
6047
  }
5193
6048
  process.env.DEEPSEEK_API_KEY = key;
5194
- return /* @__PURE__ */ React10.createElement(
6049
+ if (pending && appProps.session) {
6050
+ return /* @__PURE__ */ React13.createElement(
6051
+ SessionPicker,
6052
+ {
6053
+ sessionName: appProps.session,
6054
+ messageCount: pending.messageCount,
6055
+ lastActive: pending.lastActive,
6056
+ onChoose: (choice) => {
6057
+ if (choice === "new" || choice === "delete") {
6058
+ rewriteSession(appProps.session, []);
6059
+ }
6060
+ setPending(void 0);
6061
+ }
6062
+ }
6063
+ );
6064
+ }
6065
+ return /* @__PURE__ */ React13.createElement(
5195
6066
  App,
5196
6067
  {
5197
6068
  model: appProps.model,
@@ -5274,8 +6145,23 @@ async function chatCommand(opts) {
5274
6145
  }
5275
6146
  }
5276
6147
  const mcpSpecs = successfulSpecs;
6148
+ if (searchEnabled()) {
6149
+ if (!tools) tools = new ToolRegistry();
6150
+ registerWebTools(tools);
6151
+ }
6152
+ let sessionPreview;
6153
+ if (opts.session && !opts.forceResume && !opts.forceNew) {
6154
+ const prior = loadSessionMessages(opts.session);
6155
+ if (prior.length > 0) {
6156
+ const p = sessionPath(opts.session);
6157
+ const mtime = existsSync3(p) ? statSync2(p).mtime : /* @__PURE__ */ new Date();
6158
+ sessionPreview = { messageCount: prior.length, lastActive: mtime };
6159
+ }
6160
+ } else if (opts.session && opts.forceNew) {
6161
+ rewriteSession(opts.session, []);
6162
+ }
5277
6163
  const { waitUntilExit } = render(
5278
- /* @__PURE__ */ React10.createElement(
6164
+ /* @__PURE__ */ React13.createElement(
5279
6165
  Root,
5280
6166
  {
5281
6167
  initialKey,
@@ -5283,6 +6169,7 @@ async function chatCommand(opts) {
5283
6169
  mcpSpecs,
5284
6170
  mcpServers,
5285
6171
  progressSink,
6172
+ sessionPreview,
5286
6173
  ...opts
5287
6174
  }
5288
6175
  ),
@@ -5298,15 +6185,21 @@ async function chatCommand(opts) {
5298
6185
  }
5299
6186
 
5300
6187
  // src/cli/commands/code.tsx
5301
- import { basename, resolve as resolve4 } from "path";
6188
+ import { basename, resolve as resolve5 } from "path";
5302
6189
  async function codeCommand(opts = {}) {
5303
6190
  const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-MMANQ36Z.js");
5304
- const rootDir = resolve4(opts.dir ?? process.cwd());
6191
+ const rootDir = resolve5(opts.dir ?? process.cwd());
5305
6192
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
5306
6193
  const tools = new ToolRegistry();
5307
6194
  registerFilesystemTools(tools, { rootDir });
6195
+ registerShellTools(tools, {
6196
+ rootDir,
6197
+ // Per-project "always allow" list persisted from prior ShellConfirm
6198
+ // choices; merged on top of the built-in allowlist in shell.ts.
6199
+ extraAllowed: loadProjectShellAllowed(rootDir)
6200
+ });
5308
6201
  process.stderr.write(
5309
- `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native fs tool(s)
6202
+ `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
5310
6203
  `
5311
6204
  );
5312
6205
  await chatCommand({
@@ -5317,7 +6210,9 @@ async function codeCommand(opts = {}) {
5317
6210
  transcript: opts.transcript,
5318
6211
  session,
5319
6212
  seedTools: tools,
5320
- codeMode: { rootDir }
6213
+ codeMode: { rootDir },
6214
+ forceResume: opts.forceResume,
6215
+ forceNew: opts.forceNew
5321
6216
  });
5322
6217
  }
5323
6218
 
@@ -5325,34 +6220,34 @@ async function codeCommand(opts = {}) {
5325
6220
  import { writeFileSync as writeFileSync4 } from "fs";
5326
6221
  import { basename as basename2 } from "path";
5327
6222
  import { render as render2 } from "ink";
5328
- import React13 from "react";
6223
+ import React16 from "react";
5329
6224
 
5330
6225
  // src/cli/ui/DiffApp.tsx
5331
- import { Box as Box10, Static as Static2, Text as Text10, useApp as useApp3, useInput as useInput3 } from "ink";
5332
- import React12, { useState as useState5 } from "react";
6226
+ import { Box as Box13, Static as Static2, Text as Text13, useApp as useApp3, useInput as useInput4 } from "ink";
6227
+ import React15, { useState as useState7 } from "react";
5333
6228
 
5334
6229
  // src/cli/ui/RecordView.tsx
5335
- import { Box as Box9, Text as Text9 } from "ink";
5336
- import React11 from "react";
6230
+ import { Box as Box12, Text as Text12 } from "ink";
6231
+ import React14 from "react";
5337
6232
  function RecordView({ rec, compact = false }) {
5338
6233
  const toolArgsMax = compact ? 120 : 200;
5339
6234
  const toolContentMax = compact ? 200 : 400;
5340
6235
  if (rec.role === "user") {
5341
- return /* @__PURE__ */ React11.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text9, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React11.createElement(Text9, null, rec.content));
6236
+ return /* @__PURE__ */ React14.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React14.createElement(Text12, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React14.createElement(Text12, null, rec.content));
5342
6237
  }
5343
6238
  if (rec.role === "assistant_final") {
5344
- return /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React11.createElement(Box9, null, /* @__PURE__ */ React11.createElement(Text9, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React11.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React11.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React11.createElement(Text9, null, rec.content) : /* @__PURE__ */ React11.createElement(Text9, { dimColor: true, italic: true }, "(tool-call response only)"));
6239
+ return /* @__PURE__ */ React14.createElement(Box12, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React14.createElement(Box12, null, /* @__PURE__ */ React14.createElement(Text12, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React14.createElement(Text12, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React14.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React14.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React14.createElement(Text12, null, rec.content) : /* @__PURE__ */ React14.createElement(Text12, { dimColor: true, italic: true }, "(tool-call response only)"));
5345
6240
  }
5346
6241
  if (rec.role === "tool") {
5347
- return /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text9, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
6242
+ return /* @__PURE__ */ React14.createElement(Box12, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React14.createElement(Text12, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React14.createElement(Text12, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React14.createElement(Text12, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
5348
6243
  }
5349
6244
  if (rec.role === "error") {
5350
- return /* @__PURE__ */ React11.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React11.createElement(Text9, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React11.createElement(Text9, { color: "red" }, rec.error ?? rec.content));
6245
+ return /* @__PURE__ */ React14.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React14.createElement(Text12, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React14.createElement(Text12, { color: "red" }, rec.error ?? rec.content));
5351
6246
  }
5352
6247
  if (rec.role === "done" || rec.role === "assistant_delta") {
5353
6248
  return null;
5354
6249
  }
5355
- return /* @__PURE__ */ React11.createElement(Box9, null, /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, "[", rec.role, "] ", rec.content));
6250
+ return /* @__PURE__ */ React14.createElement(Box12, null, /* @__PURE__ */ React14.createElement(Text12, { dimColor: true }, "[", rec.role, "] ", rec.content));
5356
6251
  }
5357
6252
  function CacheBadge({ usage }) {
5358
6253
  const hit = usage.prompt_cache_hit_tokens ?? 0;
@@ -5361,7 +6256,7 @@ function CacheBadge({ usage }) {
5361
6256
  if (total === 0) return null;
5362
6257
  const pct2 = hit / total * 100;
5363
6258
  const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
5364
- return /* @__PURE__ */ React11.createElement(Text9, null, /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React11.createElement(Text9, { color }, pct2.toFixed(1), "%"));
6259
+ return /* @__PURE__ */ React14.createElement(Text12, null, /* @__PURE__ */ React14.createElement(Text12, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React14.createElement(Text12, { color }, pct2.toFixed(1), "%"));
5365
6260
  }
5366
6261
  function truncate3(s, max) {
5367
6262
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
@@ -5372,8 +6267,8 @@ function DiffApp({ report }) {
5372
6267
  const { exit } = useApp3();
5373
6268
  const maxIdx = Math.max(0, report.pairs.length - 1);
5374
6269
  const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
5375
- const [idx, setIdx] = useState5(Math.max(0, initialIdx));
5376
- useInput3((input, key) => {
6270
+ const [idx, setIdx] = useState7(Math.max(0, initialIdx));
6271
+ useInput4((input, key) => {
5377
6272
  if (input === "q" || key.ctrl && input === "c") {
5378
6273
  exit();
5379
6274
  return;
@@ -5395,7 +6290,7 @@ function DiffApp({ report }) {
5395
6290
  }
5396
6291
  });
5397
6292
  const pair = report.pairs[idx];
5398
- return /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(DiffHeader, { report }), /* @__PURE__ */ React12.createElement(Box10, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React12.createElement(Text10, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React12.createElement(Text10, null, pair ? /* @__PURE__ */ React12.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React12.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React12.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React12.createElement(Box10, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text10, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React12.createElement(Text10, null, pair.divergenceNote)) : null, /* @__PURE__ */ React12.createElement(Box10, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "j"), "/", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "k"), "/", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "N"), "/", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "g"), "/", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "q"), " ", "quit")));
6293
+ return /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "column" }, /* @__PURE__ */ React15.createElement(DiffHeader, { report }), /* @__PURE__ */ React15.createElement(Box13, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React15.createElement(Text13, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React15.createElement(Text13, null, pair ? /* @__PURE__ */ React15.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React15.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React15.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React15.createElement(Box13, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text13, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React15.createElement(Text13, null, pair.divergenceNote)) : null, /* @__PURE__ */ React15.createElement(Box13, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React15.createElement(Text13, { dimColor: true }, /* @__PURE__ */ React15.createElement(Text13, { bold: true }, "j"), "/", /* @__PURE__ */ React15.createElement(Text13, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React15.createElement(Text13, { bold: true }, "k"), "/", /* @__PURE__ */ React15.createElement(Text13, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React15.createElement(Text13, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React15.createElement(Text13, { bold: true }, "N"), "/", /* @__PURE__ */ React15.createElement(Text13, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React15.createElement(Text13, { bold: true }, "g"), "/", /* @__PURE__ */ React15.createElement(Text13, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React15.createElement(Text13, { bold: true }, "q"), " ", "quit")));
5399
6294
  }
5400
6295
  function DiffHeader({ report }) {
5401
6296
  const a = report.a;
@@ -5413,15 +6308,15 @@ function DiffHeader({ report }) {
5413
6308
  } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
5414
6309
  prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
5415
6310
  }
5416
- return /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React12.createElement(Box10, { justifyContent: "space-between" }, /* @__PURE__ */ React12.createElement(Text10, null, /* @__PURE__ */ React12.createElement(Text10, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React12.createElement(Text10, { color: "blue" }, a.label), /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, " vs B="), /* @__PURE__ */ React12.createElement(Text10, { color: "magenta" }, b.label)), /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React12.createElement(Box10, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React12.createElement(Text10, null, /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, "cache "), /* @__PURE__ */ React12.createElement(Text10, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React12.createElement(Text10, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React12.createElement(Text10, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React12.createElement(Text10, null, /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, "cost "), /* @__PURE__ */ React12.createElement(Text10, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React12.createElement(Text10, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React12.createElement(Text10, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React12.createElement(Text10, null, /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, "model calls "), /* @__PURE__ */ React12.createElement(Text10, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React12.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text10, { dimColor: true, italic: true }, prefixLine)) : null);
6311
+ return /* @__PURE__ */ React15.createElement(Box13, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Box13, { justifyContent: "space-between" }, /* @__PURE__ */ React15.createElement(Text13, null, /* @__PURE__ */ React15.createElement(Text13, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React15.createElement(Text13, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React15.createElement(Text13, { color: "blue" }, a.label), /* @__PURE__ */ React15.createElement(Text13, { dimColor: true }, " vs B="), /* @__PURE__ */ React15.createElement(Text13, { color: "magenta" }, b.label)), /* @__PURE__ */ React15.createElement(Text13, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React15.createElement(Box13, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React15.createElement(Text13, null, /* @__PURE__ */ React15.createElement(Text13, { dimColor: true }, "cache "), /* @__PURE__ */ React15.createElement(Text13, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React15.createElement(Text13, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React15.createElement(Text13, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React15.createElement(Text13, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React15.createElement(Text13, null, /* @__PURE__ */ React15.createElement(Text13, { dimColor: true }, "cost "), /* @__PURE__ */ React15.createElement(Text13, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React15.createElement(Text13, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React15.createElement(Text13, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React15.createElement(Text13, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React15.createElement(Text13, null, /* @__PURE__ */ React15.createElement(Text13, { dimColor: true }, "model calls "), /* @__PURE__ */ React15.createElement(Text13, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React15.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text13, { dimColor: true, italic: true }, prefixLine)) : null);
5417
6312
  }
5418
6313
  function Pane({
5419
6314
  label,
5420
6315
  headerColor,
5421
6316
  records
5422
6317
  }) {
5423
- return /* @__PURE__ */ React12.createElement(
5424
- Box10,
6318
+ return /* @__PURE__ */ React15.createElement(
6319
+ Box13,
5425
6320
  {
5426
6321
  flexDirection: "column",
5427
6322
  flexGrow: 1,
@@ -5429,21 +6324,21 @@ function Pane({
5429
6324
  borderStyle: "single",
5430
6325
  borderColor: headerColor
5431
6326
  },
5432
- /* @__PURE__ */ React12.createElement(Text10, { color: headerColor, bold: true }, label),
5433
- records.length === 0 ? /* @__PURE__ */ React12.createElement(Box10, { marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text10, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React12.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React12.createElement(RecordView, { key, rec, compact: true }))
6327
+ /* @__PURE__ */ React15.createElement(Text13, { color: headerColor, bold: true }, label),
6328
+ records.length === 0 ? /* @__PURE__ */ React15.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text13, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React15.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React15.createElement(RecordView, { key, rec, compact: true }))
5434
6329
  );
5435
6330
  }
5436
6331
  function KindBadge({ kind }) {
5437
6332
  if (kind === "match") {
5438
- return /* @__PURE__ */ React12.createElement(Text10, { color: "green" }, "\u2713 match");
6333
+ return /* @__PURE__ */ React15.createElement(Text13, { color: "green" }, "\u2713 match");
5439
6334
  }
5440
6335
  if (kind === "diverge") {
5441
- return /* @__PURE__ */ React12.createElement(Text10, { color: "yellow" }, "\u2605 diverge");
6336
+ return /* @__PURE__ */ React15.createElement(Text13, { color: "yellow" }, "\u2605 diverge");
5442
6337
  }
5443
6338
  if (kind === "only_in_a") {
5444
- return /* @__PURE__ */ React12.createElement(Text10, { color: "blue" }, "\u2190 only in A");
6339
+ return /* @__PURE__ */ React15.createElement(Text13, { color: "blue" }, "\u2190 only in A");
5445
6340
  }
5446
- return /* @__PURE__ */ React12.createElement(Text10, { color: "magenta" }, "\u2192 only in B");
6341
+ return /* @__PURE__ */ React15.createElement(Text13, { color: "magenta" }, "\u2192 only in B");
5447
6342
  }
5448
6343
  function paneRecords(pair, side) {
5449
6344
  if (!pair) return [];
@@ -5474,7 +6369,7 @@ markdown report written to ${opts.mdPath}`);
5474
6369
  return;
5475
6370
  }
5476
6371
  if (wantTui) {
5477
- const { waitUntilExit } = render2(React13.createElement(DiffApp, { report }), {
6372
+ const { waitUntilExit } = render2(React16.createElement(DiffApp, { report }), {
5478
6373
  exitOnCtrlC: true,
5479
6374
  patchConsole: false
5480
6375
  });
@@ -5615,16 +6510,16 @@ function pad(s, width) {
5615
6510
 
5616
6511
  // src/cli/commands/replay.ts
5617
6512
  import { render as render3 } from "ink";
5618
- import React15 from "react";
6513
+ import React18 from "react";
5619
6514
 
5620
6515
  // src/cli/ui/ReplayApp.tsx
5621
- import { Box as Box11, Static as Static3, Text as Text11, useApp as useApp4, useInput as useInput4 } from "ink";
5622
- import React14, { useMemo as useMemo2, useState as useState6 } from "react";
6516
+ import { Box as Box14, Static as Static3, Text as Text14, useApp as useApp4, useInput as useInput5 } from "ink";
6517
+ import React17, { useMemo as useMemo2, useState as useState8 } from "react";
5623
6518
  function ReplayApp({ meta, pages }) {
5624
6519
  const { exit } = useApp4();
5625
6520
  const maxIdx = Math.max(0, pages.length - 1);
5626
- const [idx, setIdx] = useState6(maxIdx);
5627
- useInput4((input, key) => {
6521
+ const [idx, setIdx] = useState8(maxIdx);
6522
+ useInput5((input, key) => {
5628
6523
  if (input === "q" || key.ctrl && input === "c") {
5629
6524
  exit();
5630
6525
  return;
@@ -5658,14 +6553,14 @@ function ReplayApp({ meta, pages }) {
5658
6553
  const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
5659
6554
  const currentPage = pages[idx];
5660
6555
  const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
5661
- return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React14.createElement(
6556
+ return /* @__PURE__ */ React17.createElement(Box14, { flexDirection: "column" }, /* @__PURE__ */ React17.createElement(
5662
6557
  StatsPanel,
5663
6558
  {
5664
6559
  summary,
5665
6560
  model: cumStats.models[0] ?? meta?.model ?? "?",
5666
6561
  prefixHash
5667
6562
  }
5668
- ), /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React14.createElement(Box11, { justifyContent: "space-between" }, /* @__PURE__ */ React14.createElement(Text11, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React14.createElement(Text11, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React14.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React14.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React14.createElement(Text11, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React14.createElement(Box11, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React14.createElement(Text11, { dimColor: true }, /* @__PURE__ */ React14.createElement(Text11, { bold: true }, "j"), "/", /* @__PURE__ */ React14.createElement(Text11, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React14.createElement(Text11, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React14.createElement(Text11, { bold: true }, "k"), "/", /* @__PURE__ */ React14.createElement(Text11, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React14.createElement(Text11, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React14.createElement(Text11, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React14.createElement(Text11, { bold: true }, "q"), " quit")));
6563
+ ), /* @__PURE__ */ React17.createElement(Box14, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React17.createElement(Box14, { justifyContent: "space-between" }, /* @__PURE__ */ React17.createElement(Text14, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React17.createElement(Text14, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React17.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React17.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React17.createElement(Text14, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React17.createElement(Box14, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React17.createElement(Text14, { dimColor: true }, /* @__PURE__ */ React17.createElement(Text14, { bold: true }, "j"), "/", /* @__PURE__ */ React17.createElement(Text14, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React17.createElement(Text14, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React17.createElement(Text14, { bold: true }, "k"), "/", /* @__PURE__ */ React17.createElement(Text14, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React17.createElement(Text14, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React17.createElement(Text14, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React17.createElement(Text14, { bold: true }, "q"), " quit")));
5669
6564
  }
5670
6565
 
5671
6566
  // src/cli/commands/replay.ts
@@ -5677,7 +6572,7 @@ async function replayCommand(opts) {
5677
6572
  }
5678
6573
  const { parsed } = replayFromFile(opts.path);
5679
6574
  const pages = groupRecordsByTurn(parsed.records);
5680
- const { waitUntilExit } = render3(React15.createElement(ReplayApp, { meta: parsed.meta, pages }), {
6575
+ const { waitUntilExit } = render3(React18.createElement(ReplayApp, { meta: parsed.meta, pages }), {
5681
6576
  exitOnCtrlC: true,
5682
6577
  patchConsole: false
5683
6578
  });
@@ -5804,7 +6699,7 @@ async function ensureApiKey() {
5804
6699
  rl.close();
5805
6700
  }
5806
6701
  }
5807
- async function runCommand(opts) {
6702
+ async function runCommand2(opts) {
5808
6703
  loadDotenv();
5809
6704
  const apiKey = await ensureApiKey();
5810
6705
  process.env.DEEPSEEK_API_KEY = apiKey;
@@ -5979,113 +6874,12 @@ function truncate4(s, max) {
5979
6874
 
5980
6875
  // src/cli/commands/setup.tsx
5981
6876
  import { render as render4 } from "ink";
5982
- import React18 from "react";
6877
+ import React20 from "react";
5983
6878
 
5984
6879
  // src/cli/ui/Wizard.tsx
5985
- import { Box as Box13, Text as Text13, useApp as useApp5, useInput as useInput6 } from "ink";
6880
+ import { Box as Box15, Text as Text15, useApp as useApp5, useInput as useInput6 } from "ink";
5986
6881
  import TextInput2 from "ink-text-input";
5987
- import React17, { useState as useState8 } from "react";
5988
-
5989
- // src/cli/ui/Select.tsx
5990
- import { Box as Box12, Text as Text12, useInput as useInput5 } from "ink";
5991
- import React16, { useState as useState7 } from "react";
5992
- function SingleSelect({
5993
- items,
5994
- initialValue,
5995
- onSubmit,
5996
- onCancel
5997
- }) {
5998
- const initialIndex = Math.max(
5999
- 0,
6000
- items.findIndex((i) => i.value === initialValue && !i.disabled)
6001
- );
6002
- const [index, setIndex] = useState7(initialIndex === -1 ? 0 : initialIndex);
6003
- useInput5((_input, key) => {
6004
- if (key.upArrow) {
6005
- setIndex((i) => findNextEnabled(items, i, -1));
6006
- } else if (key.downArrow) {
6007
- setIndex((i) => findNextEnabled(items, i, 1));
6008
- } else if (key.return) {
6009
- const chosen = items[index];
6010
- if (chosen && !chosen.disabled) onSubmit(chosen.value);
6011
- } else if (key.escape && onCancel) {
6012
- onCancel();
6013
- }
6014
- });
6015
- return /* @__PURE__ */ React16.createElement(Box12, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React16.createElement(
6016
- SelectRow,
6017
- {
6018
- key: item.value,
6019
- item,
6020
- active: i === index,
6021
- marker: i === index ? "\u25B8" : " "
6022
- }
6023
- )));
6024
- }
6025
- function MultiSelect({
6026
- items,
6027
- initialSelected = [],
6028
- onSubmit,
6029
- onCancel,
6030
- footer
6031
- }) {
6032
- const [index, setIndex] = useState7(() => {
6033
- const first = items.findIndex((i) => !i.disabled);
6034
- return first === -1 ? 0 : first;
6035
- });
6036
- const [selected, setSelected] = useState7(new Set(initialSelected));
6037
- useInput5((input, key) => {
6038
- if (key.upArrow) {
6039
- setIndex((i) => findNextEnabled(items, i, -1));
6040
- } else if (key.downArrow) {
6041
- setIndex((i) => findNextEnabled(items, i, 1));
6042
- } else if (input === " ") {
6043
- const item = items[index];
6044
- if (!item || item.disabled) return;
6045
- setSelected((prev) => {
6046
- const next = new Set(prev);
6047
- if (next.has(item.value)) next.delete(item.value);
6048
- else next.add(item.value);
6049
- return next;
6050
- });
6051
- } else if (key.return) {
6052
- const ordered = items.filter((i) => selected.has(i.value)).map((i) => i.value);
6053
- onSubmit(ordered);
6054
- } else if (key.escape && onCancel) {
6055
- onCancel();
6056
- }
6057
- });
6058
- return /* @__PURE__ */ React16.createElement(Box12, { flexDirection: "column" }, items.map((item, i) => {
6059
- const checked = selected.has(item.value);
6060
- const marker = checked ? "[x]" : "[ ]";
6061
- return /* @__PURE__ */ React16.createElement(
6062
- SelectRow,
6063
- {
6064
- key: item.value,
6065
- item,
6066
- active: i === index,
6067
- marker: `${i === index ? "\u25B8" : " "} ${marker}`
6068
- }
6069
- );
6070
- }), footer ? /* @__PURE__ */ React16.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React16.createElement(Text12, { dimColor: true }, footer)) : null);
6071
- }
6072
- function SelectRow({
6073
- item,
6074
- active,
6075
- marker
6076
- }) {
6077
- const color = item.disabled ? "gray" : active ? "cyan" : void 0;
6078
- return /* @__PURE__ */ React16.createElement(Box12, { flexDirection: "column" }, /* @__PURE__ */ React16.createElement(Box12, null, /* @__PURE__ */ React16.createElement(Text12, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React16.createElement(Box12, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React16.createElement(Text12, { dimColor: true }, item.hint)) : null);
6079
- }
6080
- function findNextEnabled(items, from, step) {
6081
- if (items.length === 0) return 0;
6082
- let i = from;
6083
- for (let tries = 0; tries < items.length; tries++) {
6084
- i = (i + step + items.length) % items.length;
6085
- if (!items[i]?.disabled) return i;
6086
- }
6087
- return from;
6088
- }
6882
+ import React19, { useState as useState9 } from "react";
6089
6883
 
6090
6884
  // src/cli/ui/presets.ts
6091
6885
  var PRESETS = {
@@ -6112,19 +6906,19 @@ var PRESET_DESCRIPTIONS = {
6112
6906
  var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
6113
6907
  function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
6114
6908
  const { exit } = useApp5();
6115
- const [step, setStep] = useState8(existingApiKey ? "preset" : "apiKey");
6116
- const [data, setData] = useState8({
6909
+ const [step, setStep] = useState9(existingApiKey ? "preset" : "apiKey");
6910
+ const [data, setData] = useState9({
6117
6911
  apiKey: existingApiKey ?? "",
6118
6912
  preset: initial?.preset ?? "fast",
6119
6913
  selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
6120
6914
  catalogArgs: {}
6121
6915
  });
6122
- const [error, setError] = useState8(null);
6916
+ const [error, setError] = useState9(null);
6123
6917
  useInput6((_input, key) => {
6124
6918
  if (key.escape && step !== "saved" && onCancel) onCancel();
6125
6919
  });
6126
6920
  if (step === "apiKey") {
6127
- return /* @__PURE__ */ React17.createElement(
6921
+ return /* @__PURE__ */ React19.createElement(
6128
6922
  ApiKeyStep,
6129
6923
  {
6130
6924
  onSubmit: (key) => {
@@ -6138,7 +6932,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
6138
6932
  );
6139
6933
  }
6140
6934
  if (step === "preset") {
6141
- return /* @__PURE__ */ React17.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React17.createElement(
6935
+ return /* @__PURE__ */ React19.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React19.createElement(
6142
6936
  SingleSelect,
6143
6937
  {
6144
6938
  items: presetItems(),
@@ -6148,10 +6942,10 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
6148
6942
  setStep("mcp");
6149
6943
  }
6150
6944
  }
6151
- ), /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
6945
+ ), /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
6152
6946
  }
6153
6947
  if (step === "mcp") {
6154
- return /* @__PURE__ */ React17.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React17.createElement(
6948
+ return /* @__PURE__ */ React19.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React19.createElement(
6155
6949
  MultiSelect,
6156
6950
  {
6157
6951
  items: mcpItems(),
@@ -6176,7 +6970,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
6176
6970
  }
6177
6971
  const currentName = pending[0];
6178
6972
  const entry = CATALOG_BY_NAME.get(currentName);
6179
- return /* @__PURE__ */ React17.createElement(
6973
+ return /* @__PURE__ */ React19.createElement(
6180
6974
  McpArgsStep,
6181
6975
  {
6182
6976
  entry,
@@ -6194,7 +6988,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
6194
6988
  }
6195
6989
  if (step === "review") {
6196
6990
  const specs = data.selectedCatalog.map((name) => buildSpec(name, data.catalogArgs));
6197
- return /* @__PURE__ */ React17.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React17.createElement(Box13, { flexDirection: "column" }, /* @__PURE__ */ React17.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React17.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React17.createElement(
6991
+ return /* @__PURE__ */ React19.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React19.createElement(Box15, { flexDirection: "column" }, /* @__PURE__ */ React19.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React19.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React19.createElement(
6198
6992
  SummaryLine,
6199
6993
  {
6200
6994
  label: "MCP",
@@ -6202,8 +6996,8 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
6202
6996
  }
6203
6997
  ), specs.map((spec, i) => (
6204
6998
  // biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
6205
- /* @__PURE__ */ React17.createElement(Box13, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, "\xB7 ", spec))
6206
- )), /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { color: "red" }, error)) : null, /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, "enter save \xB7 esc cancel"))), /* @__PURE__ */ React17.createElement(
6999
+ /* @__PURE__ */ React19.createElement(Box15, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React19.createElement(Text15, { dimColor: true }, "\xB7 ", spec))
7000
+ )), /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, { color: "red" }, error)) : null, /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, { dimColor: true }, "enter save \xB7 esc cancel"))), /* @__PURE__ */ React19.createElement(
6207
7001
  ReviewConfirm,
6208
7002
  {
6209
7003
  onConfirm: () => {
@@ -6229,15 +7023,15 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
6229
7023
  }
6230
7024
  ));
6231
7025
  }
6232
- return /* @__PURE__ */ React17.createElement(Box13, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React17.createElement(Text13, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, "Press enter to exit.")), /* @__PURE__ */ React17.createElement(ExitOnEnter, { onExit: exit }));
7026
+ return /* @__PURE__ */ React19.createElement(Box15, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React19.createElement(Text15, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, { dimColor: true }, "Press enter to exit.")), /* @__PURE__ */ React19.createElement(ExitOnEnter, { onExit: exit }));
6233
7027
  }
6234
7028
  function ApiKeyStep({
6235
7029
  onSubmit,
6236
7030
  error,
6237
7031
  onError
6238
7032
  }) {
6239
- const [value, setValue] = useState8("");
6240
- return /* @__PURE__ */ React17.createElement(Box13, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React17.createElement(Text13, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React17.createElement(
7033
+ const [value, setValue] = useState9("");
7034
+ return /* @__PURE__ */ React19.createElement(Box15, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React19.createElement(Text15, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React19.createElement(Text15, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React19.createElement(Text15, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React19.createElement(
6241
7035
  TextInput2,
6242
7036
  {
6243
7037
  value,
@@ -6254,7 +7048,7 @@ function ApiKeyStep({
6254
7048
  mask: "\u2022",
6255
7049
  placeholder: "sk-..."
6256
7050
  }
6257
- )), error ? /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { color: "red" }, error)) : value ? /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, "preview: ", redactKey(value))) : null);
7051
+ )), error ? /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, { color: "red" }, error)) : value ? /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, { dimColor: true }, "preview: ", redactKey(value))) : null);
6258
7052
  }
6259
7053
  function McpArgsStep({
6260
7054
  entry,
@@ -6262,8 +7056,8 @@ function McpArgsStep({
6262
7056
  onSubmit,
6263
7057
  onError
6264
7058
  }) {
6265
- const [value, setValue] = useState8("");
6266
- return /* @__PURE__ */ React17.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React17.createElement(Box13, { flexDirection: "column" }, /* @__PURE__ */ React17.createElement(Text13, null, entry.summary), entry.note ? /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, null, "Required parameter: "), /* @__PURE__ */ React17.createElement(Text13, { bold: true }, entry.userArgs)), /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React17.createElement(
7059
+ const [value, setValue] = useState9("");
7060
+ return /* @__PURE__ */ React19.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React19.createElement(Box15, { flexDirection: "column" }, /* @__PURE__ */ React19.createElement(Text15, null, entry.summary), entry.note ? /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, null, "Required parameter: "), /* @__PURE__ */ React19.createElement(Text15, { bold: true }, entry.userArgs)), /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React19.createElement(
6267
7061
  TextInput2,
6268
7062
  {
6269
7063
  value,
@@ -6279,7 +7073,7 @@ function McpArgsStep({
6279
7073
  },
6280
7074
  placeholder: placeholderFor(entry)
6281
7075
  }
6282
- )), error ? /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React17.createElement(Text13, { color: "red" }, error)) : null));
7076
+ )), error ? /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1 }, /* @__PURE__ */ React19.createElement(Text15, { color: "red" }, error)) : null));
6283
7077
  }
6284
7078
  function ReviewConfirm({ onConfirm }) {
6285
7079
  useInput6((_i, key) => {
@@ -6299,10 +7093,10 @@ function StepFrame({
6299
7093
  total,
6300
7094
  children
6301
7095
  }) {
6302
- return /* @__PURE__ */ React17.createElement(Box13, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React17.createElement(Box13, null, /* @__PURE__ */ React17.createElement(Text13, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React17.createElement(Text13, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React17.createElement(Box13, { marginTop: 1, flexDirection: "column" }, children));
7096
+ return /* @__PURE__ */ React19.createElement(Box15, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React19.createElement(Box15, null, /* @__PURE__ */ React19.createElement(Text15, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React19.createElement(Text15, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React19.createElement(Box15, { marginTop: 1, flexDirection: "column" }, children));
6303
7097
  }
6304
7098
  function SummaryLine({ label, value }) {
6305
- return /* @__PURE__ */ React17.createElement(Box13, null, /* @__PURE__ */ React17.createElement(Text13, null, label.padEnd(12)), /* @__PURE__ */ React17.createElement(Text13, { bold: true }, value));
7099
+ return /* @__PURE__ */ React19.createElement(Box15, null, /* @__PURE__ */ React19.createElement(Text15, null, label.padEnd(12)), /* @__PURE__ */ React19.createElement(Text15, { bold: true }, value));
6306
7100
  }
6307
7101
  function presetItems() {
6308
7102
  return ["fast", "smart", "max"].map((name) => ({
@@ -6358,7 +7152,7 @@ async function setupCommand(_opts = {}) {
6358
7152
  const existingKey = loadApiKey();
6359
7153
  const existing = readConfig();
6360
7154
  const { waitUntilExit, unmount } = render4(
6361
- /* @__PURE__ */ React18.createElement(
7155
+ /* @__PURE__ */ React20.createElement(
6362
7156
  Wizard,
6363
7157
  {
6364
7158
  existingApiKey: existingKey,
@@ -6376,9 +7170,9 @@ async function setupCommand(_opts = {}) {
6376
7170
  }
6377
7171
 
6378
7172
  // src/cli/commands/stats.ts
6379
- import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
7173
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
6380
7174
  function statsCommand(opts) {
6381
- if (!existsSync3(opts.transcript)) {
7175
+ if (!existsSync4(opts.transcript)) {
6382
7176
  console.error(`no such transcript: ${opts.transcript}`);
6383
7177
  process.exit(1);
6384
7178
  }
@@ -6465,12 +7259,14 @@ program.command("setup").description("Interactive wizard \u2014 API key, preset,
6465
7259
  });
6466
7260
  program.command("code [dir]").description(
6467
7261
  "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."
6468
- ).option("-m, --model <id>", "Override default reasoner model").option("--no-session", "Disable session persistence for this run").option("--transcript <path>", "Write a JSONL transcript to this path").action(async (dir, opts) => {
7262
+ ).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) => {
6469
7263
  await codeCommand({
6470
7264
  dir,
6471
7265
  model: opts.model,
6472
7266
  noSession: opts.session === false,
6473
- transcript: opts.transcript
7267
+ transcript: opts.transcript,
7268
+ forceResume: !!opts.resume,
7269
+ forceNew: !!opts.new
6474
7270
  });
6475
7271
  });
6476
7272
  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(
@@ -6483,7 +7279,7 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
6483
7279
  "--branch <n>",
6484
7280
  "Self-consistency: run N parallel samples per turn and pick the most confident (disables streaming; enables harvest)",
6485
7281
  (v) => Number.parseInt(v, 10)
6486
- ).option("--session <name>", "Use a named session (default: from config, usually 'default').").option("--no-session", "Disable session persistence for this run (ephemeral chat)").option(
7282
+ ).option("--session <name>", "Use a named session (default: from config, usually 'default').").option("--no-session", "Disable session persistence for this run (ephemeral chat)").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(
6487
7283
  "--mcp <spec>",
6488
7284
  'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE transport). Overrides config.mcp when provided.',
6489
7285
  (value, previous = []) => [...previous, value],
@@ -6509,7 +7305,9 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
6509
7305
  branch: defaults.branch,
6510
7306
  session: defaults.session,
6511
7307
  mcp: defaults.mcp,
6512
- mcpPrefix: opts.mcpPrefix
7308
+ mcpPrefix: opts.mcpPrefix,
7309
+ forceResume: !!opts.resume,
7310
+ forceNew: !!opts.new
6513
7311
  });
6514
7312
  });
6515
7313
  program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id (overrides preset)").option("-s, --system <prompt>", "System prompt", DEFAULT_SYSTEM).option("--preset <name>", "Bundle of model + harvest + branch: fast | smart | max").option("--harvest", "Extract typed plan state from R1 reasoning (Pillar 2)").option(
@@ -6533,7 +7331,7 @@ program.command("run <task>").description("Run a single task non-interactively,
6533
7331
  preset: opts.preset,
6534
7332
  noConfig: opts.config === false
6535
7333
  });
6536
- await runCommand({
7334
+ await runCommand2({
6537
7335
  task,
6538
7336
  model: defaults.model,
6539
7337
  system: opts.system,