reasonix 0.7.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  memoryEnabled,
11
11
  readProjectMemory,
12
12
  sanitizeMemoryName
13
- } from "./chunk-5DZMZCCW.js";
13
+ } from "./chunk-DVBNMXA6.js";
14
14
 
15
15
  // src/cli/index.ts
16
16
  import { Command } from "commander";
@@ -7111,7 +7111,7 @@ function formatLogSize(path = defaultUsageLogPath()) {
7111
7111
  }
7112
7112
 
7113
7113
  // src/cli/commands/chat.tsx
7114
- import { existsSync as existsSync12, statSync as statSync7 } from "fs";
7114
+ import { existsSync as existsSync13, statSync as statSync7 } from "fs";
7115
7115
  import { render } from "ink";
7116
7116
  import React26, { useState as useState12 } from "react";
7117
7117
 
@@ -7991,7 +7991,8 @@ function formatAllBlockDiffs(blocks, opts = {}) {
7991
7991
  const added = countLines2(b.replace);
7992
7992
  const tag = b.search === "" ? "NEW " : " ";
7993
7993
  if (i > 0) out.push("");
7994
- out.push(` ${tag}${b.path} (-${removed} +${added} lines)`);
7994
+ const label = opts.numbered ? `[${i + 1}] ` : "";
7995
+ out.push(` ${label}${tag}${b.path} (-${removed} +${added} lines)`);
7995
7996
  out.push(...formatEditBlockDiff(b, opts));
7996
7997
  }
7997
7998
  return out;
@@ -8401,17 +8402,41 @@ function parseCitationUrl(url) {
8401
8402
  }
8402
8403
  return { path: trimmed };
8403
8404
  }
8405
+ var SIBLING_EXTENSIONS = /* @__PURE__ */ new Map([
8406
+ [".ts", [".tsx", ".mts", ".cts"]],
8407
+ [".tsx", [".ts"]],
8408
+ [".js", [".jsx", ".mjs", ".cjs"]],
8409
+ [".jsx", [".js"]],
8410
+ [".mjs", [".js", ".cjs"]],
8411
+ [".cjs", [".js", ".mjs"]],
8412
+ [".mts", [".ts"]],
8413
+ [".cts", [".ts"]]
8414
+ ]);
8415
+ function extOf(p) {
8416
+ const m = /\.[^./\\]+$/.exec(p);
8417
+ return m ? m[0] : "";
8418
+ }
8404
8419
  function validateCitation(url, projectRoot) {
8405
8420
  const parts = parseCitationUrl(url);
8406
8421
  if (!parts || !parts.path) return { ok: false, reason: "empty path" };
8407
8422
  const normalized = parts.path.replace(/^[/\\]+/, "");
8408
- const fullPath = isAbsolute4(normalized) ? normalized : join11(projectRoot, normalized);
8409
- let stat;
8410
- try {
8411
- stat = statSync6(fullPath);
8412
- } catch {
8413
- return { ok: false, reason: "file not found" };
8423
+ const baseFullPath = isAbsolute4(normalized) ? normalized : join11(projectRoot, normalized);
8424
+ const siblings = SIBLING_EXTENSIONS.get(extOf(baseFullPath)) ?? [];
8425
+ const candidates = [
8426
+ baseFullPath,
8427
+ ...siblings.map((ext) => baseFullPath.replace(/\.[^./\\]+$/, ext))
8428
+ ];
8429
+ let fullPath = baseFullPath;
8430
+ let stat = null;
8431
+ for (const candidate of candidates) {
8432
+ try {
8433
+ stat = statSync6(candidate);
8434
+ fullPath = candidate;
8435
+ break;
8436
+ } catch {
8437
+ }
8414
8438
  }
8439
+ if (!stat) return { ok: false, reason: "file not found" };
8415
8440
  if (!stat.isFile()) return { ok: false, reason: "not a file" };
8416
8441
  if (parts.startLine === void 0) return { ok: true };
8417
8442
  let lineCount;
@@ -9840,8 +9865,11 @@ function processMultilineKey(value, cursor, keyIn) {
9840
9865
  if (key.pageDown) {
9841
9866
  return cursor === value.length ? NOOP : { next: null, cursor: value.length, submit: false };
9842
9867
  }
9843
- if (value.length === 0 && (key.upArrow || key.downArrow)) {
9844
- return { ...NOOP, historyHandoff: key.upArrow ? "prev" : "next" };
9868
+ if (key.ctrl && key.input === "p") {
9869
+ return { ...NOOP, historyHandoff: "prev" };
9870
+ }
9871
+ if (key.ctrl && key.input === "n") {
9872
+ return { ...NOOP, historyHandoff: "next" };
9845
9873
  }
9846
9874
  if (key.leftArrow) {
9847
9875
  return { next: null, cursor: Math.max(0, cursor - 1), submit: false };
@@ -9851,13 +9879,11 @@ function processMultilineKey(value, cursor, keyIn) {
9851
9879
  }
9852
9880
  if (key.upArrow) {
9853
9881
  const moved = moveCursorUp(value, cursor);
9854
- if (moved === cursor) return { ...NOOP, historyHandoff: "prev" };
9855
- return { next: null, cursor: moved, submit: false };
9882
+ return moved === cursor ? NOOP : { next: null, cursor: moved, submit: false };
9856
9883
  }
9857
9884
  if (key.downArrow) {
9858
9885
  const moved = moveCursorDown(value, cursor);
9859
- if (moved === cursor) return { ...NOOP, historyHandoff: "next" };
9860
- return { next: null, cursor: moved, submit: false };
9886
+ return moved === cursor ? NOOP : { next: null, cursor: moved, submit: false };
9861
9887
  }
9862
9888
  if (key.ctrl && key.input === "a") {
9863
9889
  return { next: null, cursor: startOfLine(value, cursor), submit: false };
@@ -10258,15 +10284,12 @@ function PromptInput({
10258
10284
  const continuationIndent = BAR + " ".repeat(promptBody.length);
10259
10285
  const prefixCells = promptPrefix.length;
10260
10286
  const visibleCells = Math.max(8, cols - prefixCells - 3);
10261
- const placeholderActive = narrow ? "type a message, or /command" : "type a message, or /command \xB7 [Ctrl+J] newline (Shift+Enter where supported)";
10287
+ const placeholderActive = narrow ? "type a message, or /command" : "type a message, or /command \xB7 [Ctrl+J] newline \xB7 [Ctrl+P/N] history";
10262
10288
  const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? placeholderActive;
10263
10289
  const lines = value.length > 0 ? value.split("\n") : [""];
10264
10290
  const accentColor = disabled ? "gray" : "cyan";
10265
- const animate = !disabled && cols >= 100;
10266
- const tick = useTick();
10267
- const barOffset = animate ? Math.floor(tick / 6) : 0;
10268
- const barColorAt = (rowIdx) => disabled ? "gray" : GRADIENT[((rowIdx + barOffset) % GRADIENT.length + GRADIENT.length) % GRADIENT.length];
10269
- const cursorVisible = animate ? Math.floor(tick / 4) % 2 === 0 : true;
10291
+ const barColorAt = (rowIdx) => disabled ? "gray" : GRADIENT[(rowIdx % GRADIENT.length + GRADIENT.length) % GRADIENT.length];
10292
+ const cursorVisible = true;
10270
10293
  const { line: cursorLine, col: cursorCol } = lineAndColumn(value, cursor);
10271
10294
  const renderItems = collapseLinesForDisplay(lines, cursorLine);
10272
10295
  const showHugeBufferHints = lines.length > 20;
@@ -10599,14 +10622,11 @@ function SuggestionRow({ spec, isSelected }) {
10599
10622
  import { Box as Box19, Text as Text17, useStdout as useStdout6 } from "ink";
10600
10623
  import React21 from "react";
10601
10624
  var WORDMARK_LETTERS = ["R", "E", "A", "S", "O", "N", "I", "X"];
10602
- function Wordmark({ busy, animate }) {
10603
- const tick = useTick();
10604
- const pulsePeriod = busy ? 5 : 12;
10605
- const bright = animate ? Math.floor(tick / pulsePeriod) % 2 === 0 : true;
10606
- const rotateEvery = busy ? 2 : 4;
10607
- const offset = animate ? Math.floor(tick / rotateEvery) : 0;
10608
- const colorAt = (i) => GRADIENT[((i + offset) % GRADIENT.length + GRADIENT.length) % GRADIENT.length];
10609
- return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: colorAt(0), bold: bright }, "\u25C8"), /* @__PURE__ */ React21.createElement(Text17, null, " "), WORDMARK_LETTERS.map((letter, i) => /* @__PURE__ */ React21.createElement(Text17, { key: letter, color: colorAt(i + 1), bold: true }, letter)));
10625
+ function Wordmark({
10626
+ busy: _busy,
10627
+ animate: _animate
10628
+ }) {
10629
+ return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: GRADIENT[0], bold: true }, "\u25C8"), /* @__PURE__ */ React21.createElement(Text17, null, " "), WORDMARK_LETTERS.map((letter, i) => /* @__PURE__ */ React21.createElement(Text17, { key: letter, color: GRADIENT[i % GRADIENT.length], bold: true }, letter)));
10610
10630
  }
10611
10631
  var NARROW_BREAKPOINT = 120;
10612
10632
  var COLD_START_TURNS = 3;
@@ -10632,73 +10652,44 @@ function StatsPanel({
10632
10652
  const columns = stdout2?.columns ?? 80;
10633
10653
  const narrow = columns < NARROW_BREAKPOINT;
10634
10654
  const coldStart = summary.turns <= COLD_START_TURNS;
10635
- const ruleWidth = Math.max(20, columns - 2);
10636
- const animate = columns >= 100;
10637
- return (
10638
- // Borderless layout: no `borderStyle`, no rounded box. Bordered
10639
- // Boxes were the most visible amplifier of Ink's eraseLines
10640
- // miscount on Windows terminals. Visual weight here comes from
10641
- // truecolor gradient rules at the top and bottom (rendered as
10642
- // pure Text so they never trigger the eraseLines bug), the
10643
- // animated wordmark + pill row, and a soft inner padding.
10644
- /* @__PURE__ */ React21.createElement(Box19, { flexDirection: "column", paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React21.createElement(GradientRule, { width: ruleWidth, animate }), /* @__PURE__ */ React21.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React21.createElement(
10645
- Header,
10646
- {
10647
- model: model2,
10648
- prefixHash,
10649
- harvestOn,
10650
- branchOn,
10651
- branchBudget: branchBudget ?? 1,
10652
- reasoningEffort,
10653
- planMode,
10654
- editMode,
10655
- turns: summary.turns,
10656
- updateAvailable,
10657
- narrow,
10658
- busy: busy ?? false,
10659
- proArmed: proArmed ?? false,
10660
- escalated: escalated ?? false,
10661
- animate
10662
- }
10663
- )), narrow ? /* @__PURE__ */ React21.createElement(
10664
- StackedMetrics,
10665
- {
10666
- summary,
10667
- ctxRatio,
10668
- ctxMax,
10669
- balance,
10670
- coldStart
10671
- }
10672
- ) : /* @__PURE__ */ React21.createElement(
10673
- InlineMetrics,
10674
- {
10675
- summary,
10676
- ctxRatio,
10677
- ctxMax,
10678
- balance,
10679
- coldStart
10680
- }
10681
- ), /* @__PURE__ */ React21.createElement(Box19, { marginTop: 1 }, /* @__PURE__ */ React21.createElement(GradientRule, { width: ruleWidth, thin: true, animate })))
10682
- );
10683
- }
10684
- function GradientRule({
10685
- width,
10686
- thin,
10687
- animate
10688
- }) {
10689
- const tick = useTick();
10690
- const offset = animate ? Math.floor(tick / 6) : 0;
10691
- const ch = thin ? "\u2581" : "\u2584";
10692
- const len = GRADIENT.length;
10693
- return /* @__PURE__ */ React21.createElement(Box19, null, Array.from({ length: width }, (_, i) => {
10694
- const t = width === 1 ? 0 : i * (len - 1) / (width - 1);
10695
- const idx = (Math.round(t) + offset) % len;
10696
- const color = GRADIENT[(idx % len + len) % len];
10697
- return (
10698
- // biome-ignore lint/suspicious/noArrayIndexKey: fixed-width gradient cells never reorder
10699
- /* @__PURE__ */ React21.createElement(Text17, { key: `grule-${i}`, color }, ch)
10700
- );
10701
- }));
10655
+ return /* @__PURE__ */ React21.createElement(Box19, { flexDirection: "column", paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React21.createElement(
10656
+ Header,
10657
+ {
10658
+ model: model2,
10659
+ prefixHash,
10660
+ harvestOn,
10661
+ branchOn,
10662
+ branchBudget: branchBudget ?? 1,
10663
+ reasoningEffort,
10664
+ planMode,
10665
+ editMode,
10666
+ turns: summary.turns,
10667
+ updateAvailable,
10668
+ narrow,
10669
+ busy: busy ?? false,
10670
+ proArmed: proArmed ?? false,
10671
+ escalated: escalated ?? false,
10672
+ animate: false
10673
+ }
10674
+ ), narrow ? /* @__PURE__ */ React21.createElement(
10675
+ StackedMetrics,
10676
+ {
10677
+ summary,
10678
+ ctxRatio,
10679
+ ctxMax,
10680
+ balance,
10681
+ coldStart
10682
+ }
10683
+ ) : /* @__PURE__ */ React21.createElement(
10684
+ InlineMetrics,
10685
+ {
10686
+ summary,
10687
+ ctxRatio,
10688
+ ctxMax,
10689
+ balance,
10690
+ coldStart
10691
+ }
10692
+ ));
10702
10693
  }
10703
10694
  function Header({
10704
10695
  model: model2,
@@ -10827,9 +10818,9 @@ function WelcomeBanner({ inCodeMode }) {
10827
10818
  const { stdout: stdout2 } = useStdout7();
10828
10819
  const cols = stdout2?.columns ?? 80;
10829
10820
  const ruleWidth = Math.min(60, Math.max(28, cols - 4));
10830
- return /* @__PURE__ */ React22.createElement(Box20, { flexDirection: "column", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React22.createElement(GradientRule2, { width: ruleWidth }), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.brand }, "\u25C8 welcome"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " \xB7 type a message to start")), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.primary }, "quick start")), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/help", desc: "every command + keyboard shortcut" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/skill", desc: "invoke a stored playbook" }), inCodeMode ? /* @__PURE__ */ React22.createElement(React22.Fragment, null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "@path", desc: "inline a file in your message" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "!cmd", desc: "run a shell command, output goes to context" })) : null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "/exit", desc: "quit (Ctrl+C also works)" }), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { dimColor: true, italic: true }, "tip:"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " Ctrl+J inserts a newline \xB7 trailing \\ also continues")), /* @__PURE__ */ React22.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(GradientRule2, { width: ruleWidth, thin: true })));
10821
+ return /* @__PURE__ */ React22.createElement(Box20, { flexDirection: "column", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React22.createElement(GradientRule, { width: ruleWidth }), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.brand }, "\u25C8 welcome"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " \xB7 type a message to start")), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.primary }, "quick start")), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/help", desc: "every command + keyboard shortcut" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/skill", desc: "invoke a stored playbook" }), inCodeMode ? /* @__PURE__ */ React22.createElement(React22.Fragment, null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "@path", desc: "inline a file in your message" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "!cmd", desc: "run a shell command, output goes to context" })) : null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "/exit", desc: "quit (Ctrl+C also works)" }), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { dimColor: true, italic: true }, "tip:"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " Ctrl+J inserts a newline \xB7 trailing \\ also continues")), /* @__PURE__ */ React22.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(GradientRule, { width: ruleWidth, thin: true })));
10831
10822
  }
10832
- function GradientRule2({ width, thin }) {
10823
+ function GradientRule({ width, thin }) {
10833
10824
  const cells = gradientCells(width, thin ? "\u2581" : "\u2584");
10834
10825
  return /* @__PURE__ */ React22.createElement(Box20, null, cells.map((c, i) => (
10835
10826
  // biome-ignore lint/suspicious/noArrayIndexKey: fixed-width gradient row, never reordered
@@ -10876,10 +10867,50 @@ function formatEditResults(results) {
10876
10867
  return [header2, ...lines].join("\n");
10877
10868
  }
10878
10869
  function formatPendingPreview(blocks) {
10879
- const header2 = `\u25B8 ${blocks.length} pending edit block(s) \u2014 /apply (or y) to commit \xB7 /discard (or n) to drop`;
10880
- const diffLines = formatAllBlockDiffs(blocks);
10870
+ const partial = blocks.length > 1 ? " \xB7 /apply N or 1,3-4 for partial" : "";
10871
+ const header2 = `\u25B8 ${blocks.length} pending edit block(s) \u2014 /apply (or y) to commit \xB7 /discard (or n) to drop${partial}`;
10872
+ const diffLines = formatAllBlockDiffs(blocks, { numbered: blocks.length > 1 });
10881
10873
  return [header2, ...diffLines].join("\n");
10882
10874
  }
10875
+ function parseEditIndices(raw, max) {
10876
+ const trimmed = raw.trim();
10877
+ if (!trimmed) return { ok: [] };
10878
+ if (max <= 0) return { error: "no pending edits to address" };
10879
+ const seen = /* @__PURE__ */ new Set();
10880
+ const tokens = trimmed.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
10881
+ if (tokens.length === 0) return { ok: [] };
10882
+ for (const tok of tokens) {
10883
+ const range = tok.match(/^(\d+)-(\d+)$/);
10884
+ if (range) {
10885
+ const a = Number.parseInt(range[1] ?? "", 10);
10886
+ const b = Number.parseInt(range[2] ?? "", 10);
10887
+ if (!Number.isFinite(a) || !Number.isFinite(b) || a < 1 || b < 1) {
10888
+ return { error: `invalid range: "${tok}"` };
10889
+ }
10890
+ const lo = Math.min(a, b);
10891
+ const hi = Math.max(a, b);
10892
+ if (hi > max) return { error: `index ${hi} out of range (max ${max})` };
10893
+ for (let i = lo; i <= hi; i++) seen.add(i);
10894
+ continue;
10895
+ }
10896
+ if (!/^\d+$/.test(tok)) return { error: `invalid index: "${tok}"` };
10897
+ const n = Number.parseInt(tok, 10);
10898
+ if (!Number.isFinite(n) || n < 1) return { error: `invalid index: "${tok}"` };
10899
+ if (n > max) return { error: `index ${n} out of range (max ${max})` };
10900
+ seen.add(n);
10901
+ }
10902
+ return { ok: [...seen].sort((a, b) => a - b) };
10903
+ }
10904
+ function partitionEdits(edits, indices1Based) {
10905
+ const picked = new Set(indices1Based);
10906
+ const selected = [];
10907
+ const remaining = [];
10908
+ for (let i = 0; i < edits.length; i++) {
10909
+ if (picked.has(i + 1)) selected.push(edits[i]);
10910
+ else remaining.push(edits[i]);
10911
+ }
10912
+ return { selected, remaining };
10913
+ }
10883
10914
  function formatUndoRows(results) {
10884
10915
  return results.map((r) => {
10885
10916
  const mark = r.status === "applied" ? "\u2713" : "\u2717";
@@ -10895,6 +10926,45 @@ function describeRepair(repair) {
10895
10926
  return parts.length ? `[repair] ${parts.join(", ")}` : "";
10896
10927
  }
10897
10928
 
10929
+ // src/cli/ui/hash-memory.ts
10930
+ import { appendFileSync as appendFileSync3, existsSync as existsSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
10931
+ import { join as join12 } from "path";
10932
+ var NEW_FILE_HEADER = `# Reasonix project memory
10933
+
10934
+ Notes the user pinned via the \`#\` prompt prefix. The whole file is
10935
+ loaded into the immutable system prefix every session \u2014 keep it terse.
10936
+
10937
+ `;
10938
+ function detectHashMemory(text) {
10939
+ if (text.startsWith("\\#")) {
10940
+ return { kind: "escape", text: text.slice(1) };
10941
+ }
10942
+ if (!text.startsWith("#")) return null;
10943
+ if (text.startsWith("##")) return null;
10944
+ const body = text.slice(1).trim();
10945
+ if (!body) return null;
10946
+ return { kind: "memory", note: body };
10947
+ }
10948
+ function appendProjectMemory(rootDir, note) {
10949
+ const path = join12(rootDir, PROJECT_MEMORY_FILE);
10950
+ const trimmed = note.trim();
10951
+ if (!trimmed) throw new Error("note body cannot be empty");
10952
+ const bullet = `- ${trimmed}
10953
+ `;
10954
+ if (!existsSync11(path)) {
10955
+ writeFileSync7(path, `${NEW_FILE_HEADER}${bullet}`, "utf8");
10956
+ return { path, created: true };
10957
+ }
10958
+ let prefix = "";
10959
+ try {
10960
+ const existing = readFileSync14(path, "utf8");
10961
+ if (existing.length > 0 && !existing.endsWith("\n")) prefix = "\n";
10962
+ } catch {
10963
+ }
10964
+ appendFileSync3(path, `${prefix}${bullet}`, "utf8");
10965
+ return { path, created: false };
10966
+ }
10967
+
10898
10968
  // src/cli/ui/mcp-browse.ts
10899
10969
  function formatResourceList(servers) {
10900
10970
  const lines = [];
@@ -11202,8 +11272,18 @@ var SLASH_COMMANDS = [
11202
11272
  { cmd: "new", summary: "start a fresh conversation (clear context + scrollback)" },
11203
11273
  { cmd: "exit", summary: "quit the TUI" },
11204
11274
  // Code-mode only
11205
- { cmd: "apply", summary: "commit pending edit blocks to disk", contextual: "code" },
11206
- { cmd: "discard", summary: "drop pending edit blocks without writing", contextual: "code" },
11275
+ {
11276
+ cmd: "apply",
11277
+ argsHint: "[N|N,M|N-M]",
11278
+ summary: "commit pending edit blocks to disk (no arg \u2192 all; `1`, `1,3`, or `1-4` \u2192 that subset, rest stay pending)",
11279
+ contextual: "code"
11280
+ },
11281
+ {
11282
+ cmd: "discard",
11283
+ argsHint: "[N|N,M|N-M]",
11284
+ summary: "drop pending edit blocks without writing (no arg \u2192 all; indices \u2192 that subset)",
11285
+ contextual: "code"
11286
+ },
11207
11287
  { cmd: "undo", summary: "roll back the last applied edit batch", contextual: "code" },
11208
11288
  {
11209
11289
  cmd: "history",
@@ -11292,7 +11372,7 @@ function parseSlash(text) {
11292
11372
  }
11293
11373
 
11294
11374
  // src/cli/commands/stats.ts
11295
- import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
11375
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
11296
11376
  function statsCommand(opts) {
11297
11377
  if (opts.transcript) {
11298
11378
  transcriptSummary(opts.transcript);
@@ -11301,11 +11381,11 @@ function statsCommand(opts) {
11301
11381
  dashboard(opts);
11302
11382
  }
11303
11383
  function transcriptSummary(path) {
11304
- if (!existsSync11(path)) {
11384
+ if (!existsSync12(path)) {
11305
11385
  console.error(`no such transcript: ${path}`);
11306
11386
  process.exit(1);
11307
11387
  }
11308
- const lines = readFileSync14(path, "utf8").split(/\r?\n/).filter(Boolean);
11388
+ const lines = readFileSync15(path, "utf8").split(/\r?\n/).filter(Boolean);
11309
11389
  let assistantTurns = 0;
11310
11390
  let toolCalls = 0;
11311
11391
  let lastTurn = 0;
@@ -11569,6 +11649,8 @@ var keys = () => ({
11569
11649
  " Trailing `@\u2026` opens a file picker; \u2191/\u2193 navigate, Tab/Enter pick.",
11570
11650
  " !<cmd> run <cmd> as shell in the sandbox root; output goes into context",
11571
11651
  " so the model sees it next turn. No allowlist gate.",
11652
+ " #<note> append <note> to REASONIX.md so it pins into every future session.",
11653
+ " Use `\\#literal` if you actually want a `#` heading sent to the model.",
11572
11654
  "",
11573
11655
  "Pickers (slash + @-mention):",
11574
11656
  " \u2191 / \u2193 navigate the suggestion list",
@@ -11607,8 +11689,8 @@ var help = () => ({
11607
11689
  " /skill [sub] list / run user skills (project/.reasonix/skills + ~/.reasonix/skills).",
11608
11690
  " subs: list | show <name> | <name> [args] (injects skill body as user turn)",
11609
11691
  " /retry truncate & resend your last message (fresh sample from the model)",
11610
- " /apply (code mode) commit the pending edit blocks to disk",
11611
- " /discard (code mode) drop pending edits without writing",
11692
+ " /apply [N|1,3|1-4] (code mode) commit pending edit blocks (no arg \u2192 all; index \u2192 subset)",
11693
+ " /discard [N|1,3|1-4] (code mode) drop pending edits (no arg \u2192 all; index \u2192 subset)",
11612
11694
  " /undo (code mode) roll back the latest non-undone edit batch",
11613
11695
  " /history (code mode) list every edit batch this session",
11614
11696
  " /show [id] (code mode) dump a stored edit diff (newest when id omitted)",
@@ -11631,6 +11713,12 @@ var help = () => ({
11631
11713
  " No allowlist gate \u2014 user-typed = explicit consent.",
11632
11714
  " Example: !git status !ls src/ !npm test",
11633
11715
  "",
11716
+ "Quick memory:",
11717
+ " #<note> append <note> to REASONIX.md (pinned into every",
11718
+ " future session's prefix). Faster than /memory for",
11719
+ " one-liners. Example: #always use pnpm not npm",
11720
+ " Use `\\#text` to send a literal `#text` to the model.",
11721
+ "",
11634
11722
  "File references (code mode):",
11635
11723
  " @path/to/file inline file content under [Referenced files] on send.",
11636
11724
  " Type `@` to open the picker (\u2191\u2193 navigate, Tab/Enter pick).",
@@ -11800,22 +11888,33 @@ var show = (args, _loop, ctx) => {
11800
11888
  }
11801
11889
  return { info: ctx.codeShowEdit(args) };
11802
11890
  };
11803
- var apply = (_args, _loop, ctx) => {
11891
+ var apply = (args, _loop, ctx) => {
11804
11892
  if (!ctx.codeApply) {
11805
11893
  return {
11806
11894
  info: "/apply is only available inside `reasonix code` (nothing to apply here)."
11807
11895
  };
11808
11896
  }
11809
- return { info: ctx.codeApply() };
11897
+ const parsed = parseIndicesArg(args, ctx.pendingEditCount ?? 0);
11898
+ if ("error" in parsed) return { info: `/apply: ${parsed.error}` };
11899
+ return { info: ctx.codeApply(parsed.indices) };
11810
11900
  };
11811
- var discard = (_args, _loop, ctx) => {
11901
+ var discard = (args, _loop, ctx) => {
11812
11902
  if (!ctx.codeDiscard) {
11813
11903
  return {
11814
11904
  info: "/discard is only available inside `reasonix code`."
11815
11905
  };
11816
11906
  }
11817
- return { info: ctx.codeDiscard() };
11907
+ const parsed = parseIndicesArg(args, ctx.pendingEditCount ?? 0);
11908
+ if ("error" in parsed) return { info: `/discard: ${parsed.error}` };
11909
+ return { info: ctx.codeDiscard(parsed.indices) };
11818
11910
  };
11911
+ function parseIndicesArg(args, max) {
11912
+ const raw = args.join(",").replace(/,+/g, ",").replace(/^,|,$/g, "");
11913
+ if (!raw) return { indices: [] };
11914
+ const parsed = parseEditIndices(raw, max);
11915
+ if ("error" in parsed) return { error: parsed.error };
11916
+ return { indices: parsed.ok };
11917
+ }
11819
11918
  var plan = (args, _loop, ctx) => {
11820
11919
  if (!ctx.setPlanMode) {
11821
11920
  return {
@@ -13192,6 +13291,24 @@ function App({
13192
13291
  stdout2.write("\x1B[>4m");
13193
13292
  };
13194
13293
  }, [stdout2]);
13294
+ const [isResizing, setIsResizing] = useState10(false);
13295
+ useEffect6(() => {
13296
+ if (!stdout2 || !stdout2.isTTY) return;
13297
+ let timer = null;
13298
+ const onResize = () => {
13299
+ setIsResizing(true);
13300
+ if (timer) clearTimeout(timer);
13301
+ timer = setTimeout(() => {
13302
+ setIsResizing(false);
13303
+ timer = null;
13304
+ }, 400);
13305
+ };
13306
+ stdout2.on("resize", onResize);
13307
+ return () => {
13308
+ stdout2.off("resize", onResize);
13309
+ if (timer) clearTimeout(timer);
13310
+ };
13311
+ }, [stdout2]);
13195
13312
  const { activity: subagentActivity, sinkRef: subagentSinkRef } = useSubagent({
13196
13313
  session,
13197
13314
  setHistorical
@@ -13666,29 +13783,50 @@ function App({
13666
13783
  tools.setToolInterceptor(null);
13667
13784
  };
13668
13785
  }, [tools, codeMode, session, recordEdit, armUndoBanner, syncPendingCount, setEditMode]);
13669
- const codeApply = useCallback4(() => {
13670
- if (!codeMode) return "not in code mode";
13671
- const blocks = pendingEdits.current;
13672
- if (blocks.length === 0) {
13673
- return "nothing pending \u2014 the model hasn't proposed edits since the last /apply or /discard.";
13674
- }
13675
- const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
13676
- const results = applyEditBlocks(blocks, codeMode.rootDir);
13677
- const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
13678
- if (anyApplied) recordEdit("review-apply", blocks, results, snaps);
13679
- pendingEdits.current = [];
13680
- clearPendingEdits(session ?? null);
13681
- syncPendingCount();
13682
- return formatEditResults(results);
13683
- }, [codeMode, session, syncPendingCount, recordEdit]);
13684
- const codeDiscard = useCallback4(() => {
13685
- const count = pendingEdits.current.length;
13686
- if (count === 0) return "nothing pending to discard.";
13687
- pendingEdits.current = [];
13688
- clearPendingEdits(session ?? null);
13689
- syncPendingCount();
13690
- return `\u25B8 discarded ${count} pending edit block(s). Nothing was written to disk.`;
13691
- }, [session, syncPendingCount]);
13786
+ const codeApply = useCallback4(
13787
+ (indices) => {
13788
+ if (!codeMode) return "not in code mode";
13789
+ const blocks = pendingEdits.current;
13790
+ if (blocks.length === 0) {
13791
+ return "nothing pending \u2014 the model hasn't proposed edits since the last /apply or /discard.";
13792
+ }
13793
+ const useSubset = indices !== void 0 && indices.length > 0;
13794
+ const { selected, remaining } = useSubset ? partitionEdits(blocks, indices) : { selected: blocks, remaining: [] };
13795
+ if (selected.length === 0) {
13796
+ return "\u25B8 no edits matched those indices \u2014 nothing applied. Use /apply with no args to commit them all.";
13797
+ }
13798
+ const snaps = snapshotBeforeEdits(selected, codeMode.rootDir);
13799
+ const results = applyEditBlocks(selected, codeMode.rootDir);
13800
+ const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
13801
+ if (anyApplied) recordEdit("review-apply", selected, results, snaps);
13802
+ pendingEdits.current = remaining;
13803
+ if (remaining.length === 0) clearPendingEdits(session ?? null);
13804
+ else savePendingEdits(session ?? null, remaining);
13805
+ syncPendingCount();
13806
+ const tail = remaining.length > 0 ? `
13807
+ \u25B8 ${remaining.length} edit block(s) still pending \u2014 /apply or /discard to clear them.` : "";
13808
+ return formatEditResults(results) + tail;
13809
+ },
13810
+ [codeMode, session, syncPendingCount, recordEdit]
13811
+ );
13812
+ const codeDiscard = useCallback4(
13813
+ (indices) => {
13814
+ const blocks = pendingEdits.current;
13815
+ if (blocks.length === 0) return "nothing pending to discard.";
13816
+ const useSubset = indices !== void 0 && indices.length > 0;
13817
+ const { selected, remaining } = useSubset ? partitionEdits(blocks, indices) : { selected: blocks, remaining: [] };
13818
+ if (selected.length === 0) {
13819
+ return "\u25B8 no edits matched those indices \u2014 nothing discarded.";
13820
+ }
13821
+ pendingEdits.current = remaining;
13822
+ if (remaining.length === 0) clearPendingEdits(session ?? null);
13823
+ else savePendingEdits(session ?? null, remaining);
13824
+ syncPendingCount();
13825
+ const tail = remaining.length > 0 ? ` (${remaining.length} block(s) still pending)` : ". Nothing was written to disk.";
13826
+ return `\u25B8 discarded ${selected.length} pending edit block(s)${tail}`;
13827
+ },
13828
+ [session, syncPendingCount]
13829
+ );
13692
13830
  const prefixHash = loop.prefix.fingerprint;
13693
13831
  const writeTranscript = useCallback4(
13694
13832
  (ev) => {
@@ -13743,6 +13881,36 @@ function App({
13743
13881
  promptHistory.current.push(text);
13744
13882
  return;
13745
13883
  }
13884
+ const hashParse = detectHashMemory(text);
13885
+ if (hashParse?.kind === "memory") {
13886
+ const memRoot = codeMode?.rootDir ?? process.cwd();
13887
+ promptHistory.current.push(text);
13888
+ try {
13889
+ const result = appendProjectMemory(memRoot, hashParse.note);
13890
+ const verb = result.created ? "created" : "appended to";
13891
+ setHistorical((prev) => [
13892
+ ...prev,
13893
+ {
13894
+ id: `hash-${Date.now()}`,
13895
+ role: "info",
13896
+ text: `\u25B8 noted \u2014 ${verb} ${result.path}`
13897
+ }
13898
+ ]);
13899
+ } catch (err) {
13900
+ setHistorical((prev) => [
13901
+ ...prev,
13902
+ {
13903
+ id: `hash-e-${Date.now()}`,
13904
+ role: "warning",
13905
+ text: `# memory write failed: ${err.message}`
13906
+ }
13907
+ ]);
13908
+ }
13909
+ return;
13910
+ }
13911
+ if (hashParse?.kind === "escape") {
13912
+ text = hashParse.text;
13913
+ }
13746
13914
  const bangCmd = detectBangCommand(text);
13747
13915
  if (bangCmd !== null) {
13748
13916
  const bangRoot = codeMode?.rootDir ?? process.cwd();
@@ -14768,7 +14936,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
14768
14936
  return /* @__PURE__ */ React23.createElement(React23.Fragment, null, /* @__PURE__ */ React23.createElement(
14769
14937
  TickerProvider,
14770
14938
  {
14771
- disabled: PLAIN_UI || !!pendingPlan || !!pendingShell || !!pendingEditReview || !!pendingCheckpoint || !!stagedCheckpointRevise || !!pendingChoice || !!stagedChoiceCustom || !!pendingRevision
14939
+ disabled: PLAIN_UI || isResizing || !!pendingPlan || !!pendingShell || !!pendingEditReview || !!pendingCheckpoint || !!stagedCheckpointRevise || !!pendingChoice || !!stagedChoiceCustom || !!pendingRevision
14772
14940
  },
14773
14941
  /* @__PURE__ */ React23.createElement(Box21, { flexDirection: "column" }, /* @__PURE__ */ React23.createElement(
14774
14942
  StatsPanel,
@@ -15131,15 +15299,12 @@ async function chatCommand(opts) {
15131
15299
  const prior = loadSessionMessages(opts.session);
15132
15300
  if (prior.length > 0) {
15133
15301
  const p = sessionPath(opts.session);
15134
- const mtime = existsSync12(p) ? statSync7(p).mtime : /* @__PURE__ */ new Date();
15302
+ const mtime = existsSync13(p) ? statSync7(p).mtime : /* @__PURE__ */ new Date();
15135
15303
  sessionPreview = { messageCount: prior.length, lastActive: mtime };
15136
15304
  }
15137
15305
  } else if (opts.session && opts.forceNew) {
15138
15306
  rewriteSession(opts.session, []);
15139
15307
  }
15140
- if (process.stdout.isTTY) {
15141
- process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
15142
- }
15143
15308
  const { waitUntilExit } = render(
15144
15309
  /* @__PURE__ */ React26.createElement(
15145
15310
  Root,
@@ -15167,7 +15332,7 @@ async function chatCommand(opts) {
15167
15332
  // src/cli/commands/code.tsx
15168
15333
  import { basename as basename2, resolve as resolve7 } from "path";
15169
15334
  async function codeCommand(opts = {}) {
15170
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-2OABSPAW.js");
15335
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-POARCKKR.js");
15171
15336
  const rootDir = resolve7(opts.dir ?? process.cwd());
15172
15337
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
15173
15338
  const tools = new ToolRegistry();
@@ -15207,7 +15372,7 @@ async function codeCommand(opts = {}) {
15207
15372
  }
15208
15373
 
15209
15374
  // src/cli/commands/diff.ts
15210
- import { writeFileSync as writeFileSync7 } from "fs";
15375
+ import { writeFileSync as writeFileSync8 } from "fs";
15211
15376
  import { basename as basename3 } from "path";
15212
15377
  import { render as render2 } from "ink";
15213
15378
  import React29 from "react";
@@ -15354,7 +15519,7 @@ async function diffCommand(opts) {
15354
15519
  if (wantMarkdown) {
15355
15520
  console.log(renderSummaryTable(report));
15356
15521
  const md = renderMarkdown(report);
15357
- writeFileSync7(opts.mdPath, md, "utf8");
15522
+ writeFileSync8(opts.mdPath, md, "utf8");
15358
15523
  console.log(`
15359
15524
  markdown report written to ${opts.mdPath}`);
15360
15525
  return;
@@ -16290,6 +16455,16 @@ function resolveSession(flag, configSession) {
16290
16455
  if (typeof configSession === "string" && configSession.length > 0) return configSession;
16291
16456
  return "default";
16292
16457
  }
16458
+ function resolveContinueFlag(flag, fallbackSession, getLatestSession, warn = () => {
16459
+ }) {
16460
+ if (!flag) return { session: fallbackSession, forceResume: false };
16461
+ const latest = getLatestSession();
16462
+ if (!latest) {
16463
+ warn("\u25B8 -c/--continue: no saved sessions yet \u2014 starting a fresh one.");
16464
+ return { session: fallbackSession, forceResume: false };
16465
+ }
16466
+ return { session: latest.name, forceResume: true };
16467
+ }
16293
16468
 
16294
16469
  // src/cli/index.ts
16295
16470
  var DEFAULT_SYSTEM = `You are Reasonix, a helpful DeepSeek-powered assistant. Be concise and accurate. Use tools when available.
@@ -16314,21 +16489,32 @@ The signal isn't a topic list \u2014 it's: "if I'm wrong about this, is it becau
16314
16489
 
16315
16490
  ${ESCALATION_CONTRACT}`;
16316
16491
  var program = new Command();
16317
- program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION);
16318
- program.action(async () => {
16492
+ program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION).option(
16493
+ "-c, --continue",
16494
+ "Resume the most recently used chat session without showing the picker."
16495
+ );
16496
+ program.action(async (opts) => {
16319
16497
  const cfg = readConfig();
16320
16498
  if (!cfg.setupCompleted) {
16321
16499
  await setupCommand({});
16322
16500
  return;
16323
16501
  }
16324
16502
  const defaults = resolveDefaults({});
16503
+ const continueOpts = resolveContinueFlag(
16504
+ opts.continue,
16505
+ defaults.session,
16506
+ () => listSessions()[0],
16507
+ (msg) => process.stderr.write(`${msg}
16508
+ `)
16509
+ );
16325
16510
  await chatCommand({
16326
16511
  model: defaults.model,
16327
16512
  system: applyMemoryStack(DEFAULT_SYSTEM, process.cwd()),
16328
16513
  harvest: defaults.harvest,
16329
16514
  branch: defaults.branch,
16330
- session: defaults.session,
16331
- mcp: defaults.mcp
16515
+ session: continueOpts.session,
16516
+ mcp: defaults.mcp,
16517
+ forceResume: continueOpts.forceResume
16332
16518
  });
16333
16519
  });
16334
16520
  program.command("setup").description("Interactive wizard \u2014 API key, preset, MCP servers. Re-run any time to reconfigure.").action(async () => {
@@ -16360,7 +16546,10 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
16360
16546
  "--branch <n>",
16361
16547
  "Self-consistency: run N parallel samples per turn (N\xD7 cost). Manual only \u2014 never auto-enabled.",
16362
16548
  (v) => Number.parseInt(v, 10)
16363
- ).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(
16549
+ ).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(
16550
+ "-c, --continue",
16551
+ "Resume the most-recently-used session (any name) without showing the picker."
16552
+ ).option("-n, --new", "Skip the session picker \u2014 always wipe prior messages and start fresh").option(
16364
16553
  "--mcp <spec>",
16365
16554
  'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE transport). Overrides config.mcp when provided.',
16366
16555
  (value, previous = []) => [...previous, value],
@@ -16378,16 +16567,23 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
16378
16567
  preset: opts.preset,
16379
16568
  noConfig: opts.config === false
16380
16569
  });
16570
+ const continueOpts = opts.resume ? { session: defaults.session, forceResume: true } : resolveContinueFlag(
16571
+ opts.continue,
16572
+ defaults.session,
16573
+ () => listSessions()[0],
16574
+ (msg) => process.stderr.write(`${msg}
16575
+ `)
16576
+ );
16381
16577
  await chatCommand({
16382
16578
  model: defaults.model,
16383
16579
  system: applyMemoryStack(opts.system, process.cwd()),
16384
16580
  transcript: opts.transcript,
16385
16581
  harvest: defaults.harvest,
16386
16582
  branch: defaults.branch,
16387
- session: defaults.session,
16583
+ session: continueOpts.session,
16388
16584
  mcp: defaults.mcp,
16389
16585
  mcpPrefix: opts.mcpPrefix,
16390
- forceResume: !!opts.resume,
16586
+ forceResume: continueOpts.forceResume,
16391
16587
  forceNew: !!opts.new
16392
16588
  });
16393
16589
  });