# Changelog

All notable changes to Reasonix. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.46.0] — 2026-05-17

**Breaking — Rust renderer removed.** Reasonix is back to a pure Ink/Node
TUI. The `reasonix-render` ratatui crate, the five
`@reasonix/render-{platform}-{arch}` optional sub-packages, the NAPI
loader (`src/cli/ui/scene/`), and the `--node` opt-out flag are all
gone. Cross-terminal compat issues (Termux #1149 / #1026, mac eager-spawn
chain, Windows alt-screen handoff, integrated-mode keyboard collisions)
disappear with them — at the cost of the streaming/animation perf the
rust renderer was supposed to buy us.

Notes for users:
- `REASONIX_RENDERER`, `REASONIX_RENDER_BIN`, `REASONIX_RENDER_CMD`,
  `REASONIX_INPUT_CMD`, `REASONIX_RENDERER_INTEGRATED` env vars are no-ops
  — drop them from your shell config.
- `--node` flag is no-op (and unrecognized; will error). Just drop it.
- `--no-alt-screen` / `--no-mouse` flags also gone — Ink defaults take
  over end-to-end.
- `npm install reasonix@0.46.0` no longer pulls a per-platform binary —
  one dep tree on every OS, no optional resolution step.

**TUI overhaul** — same pass that removed the rust path also cleared
the Ink-side workarounds we'd accumulated on top of it:
- Card primitive drops the left `▎` stripe; cards are plain column
  boxes with a top margin
- CardHeader drops the colored pill background behind titles
- Single consistent glyph vocabulary: `●` running, `✓` done, `✗`
  failed, `⊘` aborted, `○` queued, `⚠` warn, `⎿` child row, `█`/`░`
  bars, braille spinner
- Composer wrapped in a rounded border box; status row moved to the
  bottom; on narrow terminals status pills wrap to a new line instead
  of being truncated away (#1149)
- `/dashboard` URL surfaces as a startup info row (was easy to miss)
- `/copy` permanently in the startup hint list — vim-style copy mode
  was undiscoverable before

## [0.44.2-rc.2] — 2026-05-17

**Fix:** macOS hang on `npx reasonix@next code` — keep-alive interval
from rc.1 prevented Node from exiting, but the rust child was never
actually spawned. Root cause: spawn was triggered by a React
`useEffect` (`useSceneTrace` → `emitSceneMessage` → `trace.ts`
`ensureInitialized`), and that effect simply never fired in some
macOS npx contexts. Node sat alive with the keep-alive holding the
event loop open, nothing on screen, no rust process.

Fix: spawn rust eagerly in `chat.tsx` via new
`ensureSceneTraceReady()` export, after `setIntegratedEventHandler` so
the integrated event callback is wired before spawn. Also:
- `renderer-process.ts` synthesizes an `exit` event when the rust child
  dies on its own (panic / SIGKILL / terminal close) so the integrated
  event handler tears Node down instead of hanging forever.
- `trace.ts` now logs a clear warning to stderr when `resolveRenderer`
  returns no usable command, instead of bailing silently — anyone
  hitting "TUI never appears" can find the cause in
  `~/.reasonix/rust-render-stderr.log`.

## [0.44.2-rc.1] — 2026-05-17

**Fix:** macOS `npx reasonix code` (default rust + integrated TUI) exited
back to the shell prompt immediately without rendering. Root cause:
`makeNullStdin` / `makeNullStdout` are pure-JS Node streams with no
underlying libuv handles, so they don't keep the event loop alive. The
rust trace child is spawned by a React `useEffect` (via `useSceneTrace`
→ `emitSceneMessage`) — that effect is enqueued microseconds AFTER
`render()` returns, but on macOS the event loop sees no active handles
in that window and exits before the effect runs. Linux/Windows happen
to keep the loop alive via other handles in the boot path; macOS
doesn't.

Defensive `setInterval(()=>{}, 0x7fffffff)` keep-alive held for the
lifetime of `waitUntilExit()` and cleared in `finally`. Same renderer
binary as 0.44.0; only chat.tsx changed.

## [0.44.1] — 2026-05-17

**Fix:** Mac/Linux `npx reasonix@latest` failed with `EACCES` when spawning
the rust renderer (`spawn ... reasonix-render EACCES`). `actions/download-artifact@v4`
strips Unix file modes during the release pipeline's artifact round-trip,
so the binaries in the published 0.44.0 subpackages landed without the
executable bit. Two-layer fix:

- Each `@reasonix/render-*` subpackage now declares its binary in the
  `bin` field of `package.json`, so `npm install` chmod +x's the file
  during extraction (standard npm behavior for bin-declared files).
- The publish workflow explicitly `chmod +x`'s the non-Windows binaries
  after staging from artifacts, so the tarball itself ships with the
  right mode regardless of npm version.

No code changes — same renderer binary as 0.44.0, just a republish with
correct file modes.

## [0.44.0] — 2026-05-17

**Headline:** The Rust TUI is now the default TUI. `npx reasonix@latest`
on any supported platform (win32-x64, linux-x64, linux-arm64, darwin-x64,
darwin-arm64) pulls a pre-built ratatui binary as an `optionalDependencies`
sub-package and renders the full agent loop natively — no cargo, no
toolchain setup, no opt-in flag. The Ink/Node TUI is still shipped and
reachable via `--node` for users who hit a regression or run on an
unsupported platform; the renderer-resolver auto-falls-back to Ink with
a one-line stderr hint when no rust binary is locatable. Inside the rust
TUI, **integrated mode** is the default too (rust owns keyboard + mouse +
composer directly), which fixes the multi-press Ctrl+D + dropped preset
clicks + Ctrl+C terminal-state leak that the split keystroke-bus path had.
Bare `reasonix` (no subcommand) routes to `code` in the cwd instead of
chat — explicit `reasonix chat` still works.

**Note:** Existing users with `REASONIX_RENDERER=rust` or
`REASONIX_RENDERER_INTEGRATED=1` set in their shell rc will see no
behavior change (both are now no-ops with the value `=node` / `=0`
opting back to the old behaviors).

**Features:**

- feat(rust): rust TUI is the default. `--node` flag (or
  `REASONIX_RENDERER=node`) opts back to the Ink/Node renderer. Auto-
  fallback to Ink with a stderr hint when no rust binary is found
  (#1081)
- feat(rust): integrated mode is the default once the rust renderer
  is active. `REASONIX_RENDERER_INTEGRATED=0` opts back to the
  --emit-input split keystroke bus (debug only) (#1081)
- feat(cli): bare `reasonix` (no subcommand) now launches `code` in
  the cwd. Drops the project-marker heuristic that used to route to
  `chat` outside of project dirs; `reasonix chat` stays explicit (#1081)
- feat(scene): renderer-resolver picks the binary by priority chain —
  `REASONIX_RENDER_CMD` / `REASONIX_INPUT_CMD` env (full-command
  override) > `REASONIX_RENDER_BIN` env (single binary path) >
  `@reasonix/render-{platform}-{arch}` optional dependency > source-
  tree `target/release/reasonix-render(.exe)` > `target/debug/...` >
  `cargo run --bin reasonix-render` (source tree + cargo on PATH) >
  missing → Ink fallback (#1081)
- feat(scene): Setup wizard works end-to-end under the rust renderer.
  First-launch with no saved API key shows the masked input prompt in
  rust and accepts keystrokes via the input child / integrated event
  path (#1081)
- feat(scene): dashboard URL with a long token wraps inside the boot
  panel instead of bleeding into the sidebar. `paint_link_wrapped`
  emits each wrapped chunk as its own OSC 8 hyperlink so click-to-open
  still hits the full URL (#1081)
- feat(release): per-platform `@reasonix/render-*` subpackages
  published as `optionalDependencies` of the main package. CI workflow
  cross-compiles the rust renderer for 5 targets on tag push, uploads
  one binary per subpackage, and `npm publish`es the 5 subpackages +
  the main package together. `scripts/sync-render-versions.mjs` keeps
  every subpackage version + the main `optionalDependencies` pins
  lockstep, guarded by a `verify` + `prepublishOnly` `--check` (#1084)
- feat(dev): `npm run dev` auto-runs `cargo build --release` ahead of
  tsx so contributors never spawn a stale rust binary after editing
  rust source. Silent no-op when cargo is missing (TS-only devs).
  Added `npm run rust:build` for explicit rebuilds (#1081)
- feat(ui): Ctrl+D wired to quit in the Ink TUI as well — the boot
  banner already advertised it but the binding was never connected.
  Integrated rust mode already handled it directly (#1081)

**Fixes (inherited from #1070, included for 0.44.0 baseline):**

- fix(rust): zero-width character handling + tab expansion in the
  paint engine — control chars no longer overwrite adjacent cells,
  combining marks / ZWJ / variation selectors are skipped, `\t`
  expands to 2 spaces. Resolves the `tau-benc / tatus]` style card-
  body fragmentation seen in 0.43.0 (#1070)

## [0.43.0] — 2026-05-14

**Headline:** Desktop client graduates — `v0.42.0-*` prereleases hardened
the bundle pipeline (sync-from-tag versioning, signed Windows + signed
+ notarized macOS, Linux deb/AppImage, bundled-Node sidecar, R2-mirrored
installers reachable from the new GitHub Pages landing), and 0.43.0 is
the first stable cut where the installer is the recommended path for
non-Node users. The subagent loop turns its iteration cap from a hard
budget into a checkpoint cadence — pause/resume lets long jobs survive
restarts, and the cap goes 32 → 256 (validated end-to-end on real API
runs). Skills pick up a `max-iters` frontmatter knob so per-skill budget
overrides don't need a config edit, and `/skill` arg picker
autocompletes skill names. The CLI gets `--profile` for V8 CPU profiles
(send the file with a perf bug report; no instrumentation needed). On
the daily-driver side: `/btw` arg picker and `/skill` autocomplete in
the slash menu, a context-usage pill in the TUI status row, full i18n
sweep (settings UI, dashboard tool descriptions, residual hardcoded
strings), and ~20 desktop UX fixes (HiDPI window clamping, @-mention
drag-drop + tree picker, font scale + family pickers, language picker
synced to `<html lang>`, theme toggle, skill-running indicator, memory
panel mid-turn refresh, plan-checkpoint modal in dashboard, and several
keyboard / pause-state regressions caught in the field).

**Note:** 0.41.0's `ReadOnlyLoopTracker` (auto-escalate flash → pro on
read-only sequences) is removed in 0.43.0 — the heuristic fired on
legitimate skim-mode reads and the cost ceiling didn't justify the
behavior change. Preset escalation now happens only on explicit
`/preset` or `--auto-escalate`. (#860)

**Features:**

- feat(subagent): pause/resume on the subagent loop. The iteration cap
  is now a checkpoint cadence — when it hits, the loop persists state
  (including `partial_summary` in the paused result) and a follow-up
  call resumes from there. Cap raised 32 → 256, validated end-to-end
  against the real API. (#822, #823, #838, #839)
- feat(skills): `max-iters` frontmatter on skill files raises the
  subagent tool budget for that skill only — no config edit, no global
  bump. (#791)
- feat(cli): `--profile <path>` records a V8 CPU profile for the
  session and writes it on exit (flushed before `useQuit`'s
  `process.exit` so the file actually lands). Attach to perf bug
  reports without rebuilding with instrumentation. (#846, #847)
- feat(slash): `/skill` arg picker autocompletes skill names. (#805)
- feat(tui): context-usage pill in the status row — see remaining
  context budget at a glance instead of doing the math from cache
  hit %. (#828, #837)
- feat(web): landing + download site, ported from the design mockup
  and served from `docs/` via GitHub Pages. Installer links resolve to
  the R2-mirrored bundles. (#815, #817)
- feat(desktop): @-mention UX — drag-drop files, tree picker, and a
  TTL'd file index so large repos don't re-walk on every keystroke.
  (#826)
- feat(desktop): localize settings UI; language picker syncs
  `<html lang>` so the OS-level locale fall-through works. (#841,
  #842)
- feat(desktop): font scale slider + family picker, with API key /
  base URL always visible in general settings (was hidden behind a
  toggle that surprised first-run users). (#877, #879)
- feat(desktop): visible "skill running" indicator on the composer
  while a user skill executes — no more silent waits when a skill
  takes 10+ seconds. (#884)
- feat(i18n): residual hardcoded CLI + desktop strings extracted into
  the i18n system; dashboard tool descriptions translated to zh-CN.
  (#850, #858)
- feat(skills): `/btw` and `/search-engine` zh-CN keys filled; `/help`
  hanging-indent wrap fixed for long arg lists. (#875)

**Fixes:**

- fix(desktop): catch unknown `PauseKind` in the IPC bridge so the
  agent loop doesn't silently hang when the desktop ships ahead of a
  new pause variant. (#873)
- fix(desktop): per-tab keyboard shortcuts gate on `active` — a
  background tab no longer reacts to global hotkeys meant for the
  foreground session. (#874)
- fix(desktop): bundled CLI loads on installed targets (was looking
  for the dev path); CDN fonts dropped in favor of the @fontsource
  bundle so first paint works offline. (#806)
- fix(desktop): kill the bundled Node child on Tauri exit — added
  `rpc_kill` so the front-end can request a clean shutdown. (#792)
- fix(desktop): clamp the initial window to fit HiDPI / small
  monitors — startup no longer opens off-screen on 1366×768 laptops.
  (#833)
- fix(desktop): refresh the memory panel mid-turn after `remember` /
  `forget` — the right-panel was stale until the next user input.
  (#829)
- fix(desktop): @-mention UX — surface user skills in the `/`
  autocomplete popup; normalize workspace path so the session sidebar
  filter matches sessions opened from a different cwd casing. (#881,
  #883)
- fix(desktop): wire `path_access` gate and scope the right-panel to
  the active session — cross-session leakage on workdir switch.
  (#787)
- fix(desktop): composer-foot overflows on narrow center column.
  (cfd9db2)
- fix(desktop): honor preset `autoEscalate` when building the loop —
  was always reading the global default. (#819)
- fix(loop): preserve skill bodies verbatim across context fold — a
  fold that cut into a skill body could drop the body's leading
  fence, breaking the next subagent run. (#871)
- fix(loop): yield reasoning before content so transition chunks
  don't fragment — long reasoning runs no longer get split across two
  message bubbles. (#859)
- fix(tui): drop ESC-stripped SGR mouse reports instead of inserting
  them as keypresses. (#872)
- fix(tui): strip bidi + zero-width controls from pasted text — RTL
  override and zero-width joiners no longer survive the round-trip.
  (#876)
- fix(shell): route `2>nul` and `2>/dev/null` to the OS null device
  cross-platform, so the same shell line works in WSL and Windows
  cmd. (#821)
- fix(code): honor the configured preset instead of hardcoding flash.
  (#824)
- fix(cli): route bare project launches (`reasonix` with no
  subcommand, in a project directory) to `code` mode. (#812)
- fix(policy): `--yolo` bypasses `path_access` too, parity with the
  shell `allowAll` shortcut. (#786)
- fix(dashboard): plans API timed out on users with many sessions —
  switched to a paginated query + cache. (#880)
- fix(dashboard): broadcast plan-checkpoint pause so the web UI
  actually shows the modal (was only firing in the desktop). (#832,
  #840)
- fix(dashboard): swallow third-party-origin errors in the global
  overlay — extension scripts no longer surface as Reasonix crashes.
  (#825)

**Polish / refactor:**

- chore(desktop): theme toggle in settings; deduplicate the settings
  entry point so the dialog renders the same component everywhere.
  (#852)
- chore(desktop): drop the fake traffic-light dots from the titlebar
  — they didn't wire to any window controls and read as broken on
  Linux. (#830, #831)
- perf(desktop): paint the UI shell before `buildCodeToolset`
  finishes — first frame ~200 ms earlier on cold start. (#836)
- perf(desktop): memoize the message list so historical messages skip
  re-renders on each new chunk. (#844, #845)
- perf(ui): incremental wrap cache for streaming cards — long
  reasoning blocks no longer re-wrap from scratch on every chunk.
  (#800)
- chore(release): sync desktop version from tag (single source of
  truth for tauri.conf.json + Cargo.toml + desktop/package.json), set
  minimum CSP. (#789)
- ci(release): split the bundle step so unsigned builds skip the
  codesign envs; expose secret presence via job-level env so step
  `if:` can read it; pass `GITHUB_TOKEN` to tauri-action; switch
  macOS to universal-apple-darwin and drop the macos-13 shard
  (queue waits ran to hours). (#794, #795, #796, #799)
- ci(release): mirror published releases to R2 (Gitee mirror tried
  for ~6 fixes, dropped — 90 MB cap, 200+null on missing tags, 3-min
  timeout per upload all stacked up; R2 is enough). (#804, #808,
  #809, #810, #811, #813, #814)
- docs: render CLI reference and architecture pages in-site; repoint
  configuration CTAs at the new in-site pages. (#848, 06bae63)
- docs(readme): announce the desktop client (prerelease at the time);
  install instructions. (#788, #793)

## [0.41.0] — 2026-05-13

**Headline:** ACP graduates — three stages of work on the headless
entrypoint (NDJSON framing, `tool_call` streaming, `session/request_permission`
bridging) plus three flags (`--transcript`, `--yolo`, `--mcp` /
`--mcp-prefix`) make `reasonix acp` ready for non-interactive callers
like CI harnesses and cost-tracking adapters. The loop picked up
auto-escalation on read-only sequences (caps the dead-end fishing-trip
cost), `read_file` now outlines four more languages, and `wait_for_job`
got an exit-only mode so chatty long jobs (curl, wget, big installs)
stop burning one tool call per progress tick. On the daily-driver side:
`/btw` for side questions that don't pollute context, user-defined
memory types with priority + expiry, status-bar field toggles, and
field-reported fixes for `/new` silently keeping the old REASONIX.md,
the dashboard search toggle's one-way-trap UX, preset/effort
persistence on auto-preset switch, plus a stack of desktop UI surfaces
gaining real data.

**Features:**

- feat(acp): three-stage rollout of `reasonix acp`, the headless ACP
  entrypoint. Stage 1 brings the NDJSON framing + `initialize` /
  `session/new` / `session/prompt` round-trip with `agent_thought_chunk`
  / `agent_message_chunk` streaming. Stage 2 wires `tool_call` /
  `tool_call_update` notifications. Stage 3 bridges shell + plan
  permission gates to `session/request_permission`. (#714, #715, #717)
- feat(acp): `--transcript <path>` writes JSONL receipts with `usage`
  / `cost` / `prefixHash`, mirroring the same flag on `chat` / `code`.
  (#766)
- feat(acp): `--yolo` flag bypasses plan checkpoints without mutating
  `~/.reasonix/config.json` — process-per-task callers don't have to
  rewrite user config to skip the modal. (#767)
- feat(acp): `--mcp <spec>` (repeatable) + `--mcp-prefix <str>` for
  headless callers — domain-specific tools (custom MCP servers,
  downstream cost-tracking adapters) bridge cleanly into the cache
  key without config mutation. Shared `loadMcpServers` helper
  de-duplicates the spec/transport logic across `acp` / `chat` /
  `run`. (#780)
- feat(loop): auto-escalate flash → pro on consecutive read-only tool
  calls. Caps the cost of dead-end fishing-trip turns where flash
  loops on file reads without ever committing; pro takes over. (#681,
  #768)
- feat(read_file): outline support extended to Python, Go, Rust, and
  Markdown — the same skim-mode that worked on TypeScript / JavaScript
  files now answers "what's in here?" for the rest of the stack. (#769)
- feat(bg-jobs): `wait_for_job` gains a `waitFor: 'exit'` mode
  (default) that doesn't wake on every output chunk — a 5-minute
  download costs ONE tool call, not one per progress line.
  `timeoutMs` cap raised to 300_000. (#777)
- feat(chat): `/btw <question>` asks a side question without
  polluting the main turn's context — the answer shows in scrollback,
  but the message log stays clean. (#736)
- feat(chat): hint when no MCP servers are configured — the
  marketplace pointer is visible from inside `chat` so users don't
  have to discover `/mcp marketplace` cold. (#716)
- feat(memory): user-defined memory types with priority + expires.
  Type tags steer recall order and per-entry expiry kicks in so stale
  notes don't outlive the work they describe. (#711)
- feat(config): `statusBar` toggles for each individual field
  (`showBalance` / `showSessionCost` / `showTurnCost` / `showCacheHit`
  / `showVersion` / `showFeedbackHint`) — hide what doesn't matter,
  keep the bar dense for what does. (#626, #712)
- feat(desktop): rebuild the UI on a card-based design system with
  splash intro, cmdk palette, workdir popover, statusbar workspace
  switch, and the right-panel Files / Tools / Memory tabs now showing
  real data (file status dots, dir expand/collapse, per-server MCP
  status, memory store entries). Reserved-tokens bucket added to the
  context meter; model cards updated to v4 names + 1M context. (#710,
  #718, #722, #741, #748, #750, #751, #758, #759, #761, #762)
- feat(i18n): zh-CN overlay covers the new marketplace flow and
  closes remaining TUI gaps from the prior round. (#706)

**Fixes:**

- fix(loop): `/new` now re-runs the system-prompt builder, so editing
  REASONIX.md and starting a fresh conversation actually picks up the
  new content. Previously the prompt was baked into
  `ImmutablePrefix.system` at startup and `clearLog()` never touched
  it — users had to restart. Cache-key invalidation happens only when
  the file actually changed. (#781)
- fix(settings): dashboard search toggle no longer one-way-traps users
  whose config has no `"search"` field. The runtime default is on,
  but the missing field made the button render ON before any explicit
  value was set, so a single "enable it" click flipped to OFF. GET
  now persists the default on first read; subsequent clicks toggle a
  real boolean. (#782)
- fix(preset): switching preset (e.g. `/preset auto`) no longer
  clobbers the persisted `reasoningEffort` — a `/effort high` set
  before a preset switch used to silently revert. (#775)
- fix(healing): bare `tool_calls` without an `id` now get a fallback
  stamp so the next DeepSeek call doesn't 400 on validation. (#713)
- fix(markdown): emit OSC-8 hyperlinks for `[text](url)` so the
  rendered markdown is clickable in supporting terminals (iTerm2,
  WezTerm, Kitty, recent Windows Terminal). (#730)
- fix(wallet): pick the largest balance from `balance_infos[]`
  instead of trusting `[0]` — the upstream can return multiple
  buckets in any order. (#731)
- fix(embed): tolerate providers that drop inputs from a batch
  embedding response — fall back to per-input retry instead of
  failing the whole batch. (#732)
- fix(composer): keep the prompt cursor visible when the ticker
  suspends. (#733)
- fix(plan): stabilize the expanded approval modal layout so body,
  options, and footer no longer reflow on rerender. (#734)
- fix(session): persist new sessions opened via `/session <name>`
  instead of dropping them on the next launch. (#740)
- fix(jobs): settle the running state on `exit` (not just `close`)
  and after a taskkill timeout, so Windows + Node ≥ 24 no longer
  ghost-run a job after `stop_job` returns. (#746)
- fix(mcp): silence the race-leg derivative on initialize timeout —
  the loser of the timer-vs-init race no longer logs a spurious
  abort. (#747)
- fix(at-mention): allow CJK and other Unicode letters in
  `@`-mention paths — previously the regex narrowed to ASCII and
  silently dropped any path with non-ASCII characters. (#764)
- fix(ui): capture keystrokes while scrolled up in history — typing
  no longer disappears when the user is browsing the scrollback.
  (#760)
- fix(desktop): drop stale literals from preset, plan badge, and
  tool card — copy reads from i18n consistently. (#771)
- fix(desktop): replace MCP "loading" copy with "configured" once
  the server actually loads. (#752)
- fix(web): align the Changes page chat with `ChatPanel` core logic
  + poll `/overview` every 2.5s. (#719, #720)
- fix(dashboard): memoise chat feed + rails to kill input-typing lag
  — large message histories no longer make the composer feel glued.
  (#729)
- fix(changes-ui): remove dead `+` button, widen editor / diff line
  number gutters, file tab click exits review mode. (#707)

**Polish / refactor:**

- refactor(ui): extract `HistoryTypingCapture` so `AppInner` stops
  subscribing to `pinned` — fewer renders, same behavior. (#763)
- chore(plan): document magic numbers + swap the hand-rolled
  ANSI-strip for the standard `strip-ansi` package. (#738)
- chore: oosmetrics badges + weekly health-check workflow; point
  Star History at the canonical DeepSeek-Reasonix slug. (#772, #773)
- docs(readme): badge color pass; thank AIGC Link for XiaoHongShu
  promotions. (#726)

## [0.40.0] — 2026-05-12

**Headline:** npm-only release. The repo also gained a Tauri desktop
client (`reasonix desktop`) wired against the agent loop with multi-
tab concurrent runtimes — the source is here but the macOS / Windows /
Linux installer bundles are NOT shipping this round; they'll get their
own release once the signing pipeline is settled. The CLI side picked
up a session-scoped checkpoint API + a git-changes panel in the
dashboard, a shared pause-gate policy module so the desktop's
auto-resolve rules don't drift from the CLI TUI's, plus a wave of
field-reported fixes: the long-standing `Maximum update depth` crash
inside CardStream is finally gone (this time for good — quantized
window), `reasonix code` no longer boots dead when a user has only
configured their key via `reasonix setup`, and a half-dozen MCP /
shell / stdin / scroll papercuts are gone.

**Features:**

- feat(desktop): new Tauri client with multi-tab concurrent runtimes,
  bundled Node, native filesystem tools, and one persistent agent
  loop per tab. Source ships with this release; installer bundles
  ship separately. (#689)
- feat(desktop): wallet balance + version chip in the sidebar, active-
  plan rail (collapsible, per-step risk + completion results),
  abortable pause-gates, checkpoint / revision cards, ⌘K palette,
  edit-gate pill, en + zh-CN i18n with a live toggle in Settings.
  (#701)
- feat(changes): in-process checkpoint API + a git-changes panel in
  the embedded dashboard — zero external git deps, snapshots are
  session-scoped, restore is one click. (#682)
- feat(sandbox): outside-sandbox file access (reads or writes that
  resolve outside the project root) is now gated behind an approval
  modal — protects against the model wandering into `/etc`, `~`, or
  a sibling repo by mistake. (#696)
- feat(mcp): loading pill on each MCP server while tools are still
  being inspected; tool dispatch is gated on readiness so the model
  can't fire a call into a half-loaded server. (#687)
- feat(loop): `--escalate-after <n>` flag +
  `escalation.failureThreshold` config field to tune how many repair
  signals it takes before flash → pro auto-escalates. Defaults
  unchanged. (#699)
- feat(status): preset is now visible in the status bar; the model
  pin survives manual `/model` picks instead of being clobbered on
  the next preset reload. (#668)
- feat(tools, loop): `readOnlyCheck` thrown errors are warned instead
  of silently swallowed — bugs in custom tool definitions surface
  immediately. (#670, #673)
- feat(tools/web): `web_search` / `web_fetch` now bucket 5xx errors
  separately with a transient-retry hint so the model retries instead
  of giving up on a status code. (#676)
- feat(i18n): zh-CN coverage extended to MCP lifecycle events,
  slow-network toast, external-editor messages. (#679)

**Fixes:**

- fix(tui): CardStream window position is now quantized to
  `VISIBLE_BUFFER_ROWS` buckets — sub-bucket `scrollRows` /
  `outer.height` wiggles no longer flip a boundary card live↔spacer,
  which was the residual `Maximum update depth exceeded` crash inside
  `useBoxMetrics` (the #549 fix only patched one instance of the
  pattern). (#700, #702)
- fix(code): `reasonix code` now calls `loadDotenv()` + bridges
  `~/.reasonix/config.json` → `process.env.DEEPSEEK_API_KEY` like
  `chat` / `desktop` / `run` already did. Subagent's `DeepSeekClient`
  is constructed lazily so a missing key doesn't kill boot before the
  setup wizard can prompt. (#703)
- fix(scroll): pinned-mode shrinks now coalesce — a burst of card-
  teardown re-measurements during an Esc-abort used to snap
  scrollRows N times, producing a visible flicker. Trailing-edge
  flush. (#653, #666)
- fix(stdin): generic modifyOtherKeys / Kitty CSI envelopes are now
  decoded correctly (Ctrl/Alt + character keys on terminals that
  wrap modifier presses in `\e[27;` or `\e[u` sequences). (#692)
- fix(ui/shell-confirm): long command previews clamp to the available
  width so the Allow / Allow-prefix / Deny options stay visible
  instead of scrolling off-screen. (#691)
- fix(skills, memory): frontmatter parser handles BOM, folded lines,
  and quoted values; previously a BOM-prefixed `REASONIX.md` was
  silently dropped. (#690)
- fix(mcp): inspect failures are classified into network / TLS / HTTP
  buckets — the error in the UI is now actionable instead of a raw
  Node `fetch failed`. (#688)
- fix(mcp): debug log when the bridge receives a malformed JSON-RPC
  frame instead of crashing the parser silently. (#669)
- fix(i18n): session-pruned banners and overflow errors now point at
  `/new` and `/sessions` (the live commands) rather than the
  deprecated `/forget` and `/sessions delete`. (#698)
- fix(prompt): system prompt explicitly documents that the model may
  see OS-absolute paths (Windows `C:\...`) and forbids filesystem-
  refusal hallucinations like "I don't have access to your filesystem"
  when filesystem tools are clearly registered. (#667)

**Polish:**

- card border + composer cursor + shell modal info-row alignment all
  hand-tuned; nothing structural, just paper-cuts. (#672)

## [0.39.1] — 2026-05-11

**Hotfix:** `0.39.0` shipped with a `postinstall: patch-package` hook
but neither `patch-package` (in `devDependencies` only) nor the
`patches/` directory (omitted from `files`) made it into the published
tarball. Fresh installs failed with `'patch-package' is not recognized
as an internal or external command`, taking the whole install down.
`0.39.0` is deprecated on npm.

The `patch-package` approach was wrong by design — it patches
`./node_modules/<dep>`, but npm hoists `ink` to the consumer's
top-level `node_modules/ink`, so patch-package never finds the target
when reasonix is installed as a dependency. The install-time
machinery is removed entirely. The ink alt-screen render fix (#639)
will return via a forked `ink` package and an `npm:` alias in a later
release; until then the alt-screen ghosting on CJK terminals matches
0.38.0 behavior. Tracked at #663.

Three feature/fix PRs that were in flight piggyback on this release.

**Features:**

- feat(composer): Ctrl+X opens `$EDITOR` with the current input —
  drafts long prompts in vim/code/etc. instead of fighting the
  in-terminal editor. (#647, #661)
- feat(composer): Ctrl+P / Ctrl+N navigate input-prefix pickers when
  open — slash, `@`-mention, slash-arg pickers move up/down with the
  same keys as ↑/↓ when active, fall through to history recall when
  closed. (#647, #662)

**Fixes:**

- fix(pkg): drop `postinstall: patch-package`, the `patches/`
  directory and the `patch-package` dependency. Fresh installs no
  longer fail on the missing patch-package binary.
- fix(web): `web_search` / `web_fetch` errors now carry an actionable
  ` — try: …` tail so the model self-corrects (back off on 429,
  switch engine on 403, pick a smaller URL on oversize, etc.) instead
  of giving up on a bare status code. (#16, #632)

## [0.39.0] — 2026-05-11

**Headline:** field-bug week — a wave of user-reported rendering, tool
and network bugs all landed with focused fixes. Two structural wins:
the open `AGENTS.md` spec now works without anyone having to create a
separate `REASONIX.md`, and `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY`
are honored across every fetch path (DeepSeek API, web search, MCP,
doctor) — Node's built-in fetch silently ignored them before. zh-CN
coverage is now essentially complete for the TUI surface.

**Features:**

- feat(memory): `readProjectMemory` walks `REASONIX.md` → `AGENTS.md` →
  `AGENT.md` and picks the first that exists; writes target whichever
  file is already on disk. Projects on the open
  [agents.md](https://agents.md) spec no longer need a separate
  reasonix-only memory file. (#635, #636)
- feat(net): HTTPS_PROXY / HTTP_PROXY / ALL_PROXY are now honored across
  every fetch path — undici's global dispatcher gets a `ProxyAgent` at
  CLI entry, before any client constructs. `/doctor` reports the active
  proxy URL (credentials redacted). (#646, #650)
- feat(mcp): per-server env config via `mcpEnv` — set custom env vars
  on individual MCP server invocations instead of relying on the shell
  inherited environment. (#627)
- feat(dashboard): `--dashboard-port <port>` flag + `dashboard.port`
  config field to pin the embedded dashboard to a fixed port; required
  for stable SSH tunnels. (#624, #625)
- feat(doctor): `--json` flag for structured machine-readable output —
  scriptable / pipeable for CI checks and aggregator dashboards. (#620)
- feat(tools): JSON-schema validation for tool-call arguments before
  dispatch. (#621)
- feat(plan): `/plans done <stepId>` and `/plans done all` — manual
  escape valve when the model forgets to call `mark_step_complete`, so
  the resume banner doesn't get stuck at `0/N done`. (#641, #645)
- feat(i18n): zh-CN now covers setup wizard, theme picker, plan editor,
  MCP hub, MCP marketplace, MCP browser, checkpoint picker, plan
  revise, diff/replay — every modal, picker, marketplace, and tool
  surface routes through `t()` with consistent translations. (#622,
  #654)

**Fixes:**

- fix(render): force per-frame clear in alt-screen — kills `log-update`
  line-count drift that produced duplicated status bars at the bottom
  of the viewport for users with CJK / ambiguous-width content on East
  Asian terminals (Windows Terminal at 120×30 + zh-CN was the field
  repro). Patched via `patch-package` against ink@7.0.2. (#639, #640)
- fix(ui): reset chat scroll to bottom when a confirm modal mounts —
  shell-command, plan, checkpoint, revision, and ask_choice pickers all
  appeared off-screen for users who had scrolled up, and arrow-key
  inputs were captured by the picker so the user looked stuck. (#642,
  #643)
- fix(ui): user-message cards render verbatim, not through Markdown —
  pasted code / stack traces / paths no longer get silently reformatted
  (asterisks → italic, leading `#` → H1, `[label](url)` → link with URL
  hidden, etc.). Assistant turns keep Markdown rendering. (#655, #656)
- fix(markdown): decode HTML entities in code blocks and inline code —
  models sometimes HTML-escape JSON / HTML / XML output (`&quot;`
  instead of `"`); terminals don't render entities so they leaked
  visibly. Common named + numeric forms decode at the rendering
  boundary; unknown names pass through. (#657, #658)
- fix(config): `loadEditMode` preserves `yolo` instead of demoting it
  to `review` — the load path had only recognized `auto` and dropped
  every other value back to the safe default, so `/mode yolo` survived
  one process but reset on the next launch and the shell tool's
  `allowAll` getter never returned true. (#644, #648)
- fix(net): same as above but listed under features — the proxy fix is
  both a new feature (we never had support) and a fix for a class of
  silent network failures.
- fix(tools): short-circuit a 2nd consecutive identical malformed tool
  call. The model was caught calling `read_file({})` twice in a row
  with the same missing-required-param error in between; the existing
  `StormBreaker` threshold of 3 missed this shape. `ToolRegistry` now
  tracks per-tool fingerprints of validation failures and returns a
  sharper "DO NOT retry with identical args" error on the second
  identical malformed call. (#651, #652)
- fix(ci): `issue-labeler.yml` now uses `/pattern/i` literal regex form
  instead of the PCRE `(?i)` inline flag, which JavaScript's RegExp
  doesn't accept. The auto-label workflow was dying on every issue
  before this. (#637)

**Internal:**

- chore(ci): add issue-triage workflows — auto-label by topic match,
  surface similar prior issues on new reports. (#634)

## [0.38.0] — 2026-05-10

**Headline:** new `/copy` slash command — a vim/tmux-style copy mode
that gives users a keyboard path to yank chat text from the alt-screen
buffer, where terminal drag-select can't extend past the visible
viewport. Plus a long-overdue `docs/CLI-REFERENCE.md` covering every
shell subcommand, every slash, and every keybinding, linked from both
READMEs and the website footer.

**Features:**

- feat(ui): `/copy` enters a frozen-snapshot copy mode. `j`/`k` (or
  arrows) move the cursor by line; `v` toggles a selection anchor;
  `y`/`Enter` yanks via the existing OSC 52 path (with the temp-file
  fallback for >75 KB or terminals that don't honour OSC 52); `g`/`G`
  jump to top/bottom; `q`/`Esc` exits without yanking. Snapshot spans
  user / streaming / reasoning cards — tool / diff / etc. are skipped;
  headers are navigable but excluded from yank, so cross-card
  selections come out clean. Solves the SSH / mosh / tmux drag-select
  pain where alt-screen has nothing scrollable above the viewport for
  the terminal to extend the selection into. (#614, #616)

**Docs:**

- docs: `docs/CLI-REFERENCE.md` mirrors `/help` + `/keys` so the surface
  is greppable from the repo, indexable on the website, and printable
  for offline reference. Linked from `README.md`, `README.zh-CN.md`,
  `docs/index.html` (footer), and `docs/configuration.html` (outro CTA),
  with EN + zh strings in both website i18n dictionaries. (#616)

## [0.37.0] — 2026-05-10

**Headline:** boot splash + zh-CN status bar, MCP-handshake stall on
launch is gone (bridging deferred to first paint), card virtualization
keeps long sessions snappy, and four field-reported bugs that all
shared a "silent failure" shape — `/new` was overwriting the live
session file so prior transcripts vanished from the Sessions tab,
flat-format skills (`<dir>/<name>.md`) didn't appear in the dashboard
even though `/skill <name>` ran them, skills missing a `description:`
frontmatter were silently dropped from the prefix index so a new
session claimed they didn't exist, and the escalation contract told
every session it was running on flash so `/preset pro` self-reported
as flash when asked.

**Fixes:**

- fix(loop): `/new` truncated `~/.reasonix/sessions/code-<project>.jsonl`
  in place — multiple `/new`s in a project produced exactly one
  Sessions row and every prior turn was destroyed without warning.
  `clearLog` now rotates the live jsonl plus sidecars to
  `<name>__archive_<ts>` via `archiveSession` so the prior conversation
  survives in the dashboard. The `__archive_` infix sits outside the
  `${name}-` resume-prefix matcher so archives don't auto-resume on
  next launch. `sessionName` is unchanged so the cache-first prefix
  invariant holds. (#587, #590)

- fix(dashboard): `/api/skills` only walked folder-format skills
  (`<dir>/<name>/SKILL.md`); flat-format skills (`<dir>/<name>.md`)
  worked from `/skill <name>` in the TUI but the dashboard tab was
  silently empty for users who installed them flat. The listing now
  dispatches on `Dirent` and resolves both layouts; read / save /
  delete share the same resolver so a flat skill can be edited or
  removed from the dashboard without spawning a duplicate folder
  entry. (#586, #589)

- fix(skills): a skill whose frontmatter omitted `description:` worked
  in the install session (because `/skill <name>` calls `store.read`
  directly) and silently disappeared the next session (because
  `applySkillsIndex` filtered it out of the prefix). Two-layer fix:
  the dashboard install POST validates frontmatter via the new
  `validateSkillFrontmatter()` and returns 400 instead of writing a
  skill the model will never see; `applySkillsIndex` now lists blank-
  description skills with a placeholder line so the model can name
  them and tell the user how to fix the frontmatter. (#583, #591)

- fix(prompt): `ESCALATION_CONTRACT` was a module-level const with
  `deepseek-v4-flash` baked into the literal — interpolated into
  `DEFAULT_SYSTEM`, `CODE_SYSTEM_PROMPT`, and `DEFAULT_SUBAGENT_SYSTEM`
  at module load. A pro session got told it was running on flash and
  answered honestly when asked which model it was. `escalationContract`
  is now a function: pro tier gets a short "you are the escalation
  tier; <<<NEEDS_PRO>>> is a no-op" note (no ladder, since pro can't
  escalate to itself), other tiers get the full contract with the
  actual model id interpolated plus an explicit "if asked which model
  you are, answer `<id>`" line. The three system-prompt sites thread
  the resolved session model through. The public `CODE_SYSTEM_PROMPT`
  const is preserved for backward compat. (#582, #592)

- fix(ui): pressing `/` on the empty home screen left the bordered
  WelcomeBanner mounted while `SlashSuggestions` rendered below — both
  occupied the same flex column so the frame buffer interleaved them
  and the welcome card border drew through the menu rows. The empty-
  state guard now also requires `slashMatches === null`, so the
  welcome card yields the moment the menu opens and returns when it
  closes. (#594)

- fix(ui): wheel-up felt laggy because `schedule()` was trailing-edge —
  every tick paid a 16 ms timer before any visual feedback, and on
  top of Ink reconcile + Yoga layout a single tick cost 30-50 ms
  before the frame moved. `schedule()` is now leading-edge so the
  first delta lands immediately; subsequent calls inside the window
  accumulate. Wheel/PgUp/PgDn step jumps from 3 → 8 rows so each
  tick travels roughly a third of a viewport. (#571)

- fix(ui): the default frame flush was 16ms (60Hz), which on
  winpty / MINTTY / ConEmu / tmux / high-latency SSH couldn't
  atomically swap the cursor-up rewrite — the previous frame's
  bottom rows briefly bled through every redraw, visible as
  vertical bobbing. Default is now 50ms (20Hz); still reads as
  continuous streaming, no bob on any affected terminal. The
  `REASONIX_UI=plain` escape hatch (which suppressed every live row)
  is removed since the new default addresses the same terminals
  without losing the spinner / status line / live cards. Override
  via `REASONIX_FLUSH_MS=16` for terminals with atomic frame swap.
  (#570)

**Features:**

- feat(ui): boot splash for `reasonix code` / `reasonix chat`. Cold
  launch used to flash the alt-screen blank for a few hundred ms
  before AppInner's first paint completed; users read that as a
  freeze. The splash holds for one whale-spout cycle (~1.4s) so the
  REASONIX wordmark lands cleanly and AppInner's heavy first-paint
  cost (~150 hooks + several disk reads) hides under it. ANSI Shadow
  block letters in brand color; three-tone shaded whale silhouette
  with a 7-frame spout cycle and a shifting wave below. Setup screen
  and SessionPicker bypass the splash. (#588)

- feat(i18n): status bar, input placeholder, edit-mode hints, and
  composer prompts route through `t()` with zh-CN coverage. Final
  pieces of the chat surface that were still hardcoded English —
  turn / cache / spent / left / slow / disconnect labels in
  StatusRow, the "ask anything..." placeholder and "⏎ send · ^C quit"
  hint in PromptInput, and the REVIEW / AUTO / YOLO mode label in
  LiveRows. (#584)

**Perf:**

- perf(boot): MCP bridging moved from `chatCommand`'s pre-render
  serial loop to an App.tsx mount-time effect that runs in the
  background. Each `runtime.addSpec(raw)` handshake is 100ms-2s; users
  with several servers configured used to watch a black alt-screen
  until the last one finished. The UI now paints immediately, MCP
  lifecycle events surface as in-app toasts via `log.pushInfo` /
  `log.pushWarning`, and `loop.prefix.addTool` hot-adds tools as
  they bridge — first turn after bridging is one cache-miss, same as
  the existing `/mcp browse install` path. (#585)

- perf(ui): card virtualization. Yoga used to lay out every card in
  CardStream's inner Box on every scroll tick — for a 50-card
  history that's hundreds of rows re-measured per tick. Each card
  now reports its measured height to the chat-scroll store and
  CardStream collapses off-viewport ranges into a single spacer Box,
  so only the 5-10 cards under the viewport (± a 30-row buffer) go
  through Yoga per scroll. Streaming and freshly-mounted cards always
  render live for measurement. (#574)

- perf(ui): scroll state isolated from App.tsx via
  `chat-scroll-store` (same `useSyncExternalStore` pattern as the
  agent store). Wheel/arrow ticks no longer re-render AppInner's
  3,800 lines / 122 hooks per tick — only `CardStream` and the
  position indicator. The static `↑ earlier` hint is now a live
  position indicator (`↑ N / M rows above — K more`) that briefly
  highlights on each applied delta so the user gets instant
  confirmation. (#573)

## [0.36.2] — 2026-05-09

**Headline:** stability sweep on field-reported crashes and freezes —
TUI no longer tears down on `/model` / `/sessions`, Esc and `/new`
recover from a stuck plan checkpoint, the dashboard chat tab survives
long streaming turns, plan-card spinners can't strand themselves on a
missed end-event, and the model can't infer its identity from a
foreign agent platform's data dir at the workspace root. New `/theme`
picker for one-keystroke theme switching.

**Fixes:**

- fix(tui): a card-stream layout feedback loop (the `↑ earlier` hint
  conditionally rendered as a sibling of the measured outer Box) tied
  `outer.height` to `scrollRows`. Opening `/model` or `/sessions` —
  which mounts a picker that shrinks the outer column by 10+ rows in
  a single commit — could stack the cycle deep enough to trip React's
  `MAX_NESTED_UPDATES = 50`, raising "Maximum update depth exceeded"
  inside ink's `useBoxMetrics` and tearing down the TUI. The hint row
  is now reserved unconditionally so its visibility no longer feeds
  back into measurement. (#549)
- fix(tui): `pauseGate.ask` ignored AbortSignal — when a tool was
  awaiting the gate (e.g. `mark_step_complete` → `plan_checkpoint`)
  and the user pressed Esc, the gate's promise stayed pending forever,
  `busy` stayed true, the prompt stayed disabled, and `/new` was
  silently dropped by `handleSubmit`'s `if (busy) return` guard. New
  `pauseGate.cancelAll()` resolves every outstanding request with its
  kind's safe-cancel verdict; Esc-during-busy and `/new` both flush
  pending modals through it so the awaiting tool fn returns cleanly
  and the user can recover. (#552)
- fix(prompt): when the workspace root contained another agent
  platform's config (`SOUL.md`, `skills/`, `memories/`, a foreign
  `REASONIX.md`) the model would browse those files and claim a
  layered architectural relationship — "the underlying runtime is
  Hermes Agent" or similar. Top-of-prompt identity guard names the
  failure mode: workspace files describe the user's project, never
  what Reasonix is; identity questions are answered from the prompt,
  not from `ls`. Plus a launch-time detector that warns when those
  markers sit at the workspace root, suggesting `--dir <real-project>`.
  (#555)
- fix(dashboard): the embedded chat tab triggered Chrome's "Page not
  responding" dialog during long sessions and concurrent jobs. Each
  `assistant_delta` (~20/sec, more under fan-in) called setState
  synchronously, re-rendering every historical `ChatMessage` with no
  memoization — every delta re-ran `marked.parse` and `hljs.highlight`
  on unchanged content. Memoized `ChatMessage` via `preact/compat`
  `memo`, stabilised the per-row `streaming` prop so memo's shallow
  compare actually bails out, and rAF-coalesced delta accumulation so
  the streaming bubble re-renders at most once per frame regardless
  of delta volume. (#560)
- fix(loop): tool-card spinners occasionally kept spinning after the
  underlying work had finished — the `running` flag was set
  imperatively from paired events, and any exit path that forgot to
  emit the closing event (storm-breaker, network drop, parent abort
  propagating, hook block) left the card stuck. Replaced with a
  finally-guaranteed `InflightSet` on the loop: tools are added at
  dispatch entry and deleted in `finally` regardless of how the call
  exits. UI tool cards consult the set via `useIsInflight(card.id)`
  for the spinner, decoupling running-or-not from end-event delivery.
  (#566)

**Features:**

- feat(ui): bare `/theme` opens a SingleSelect picker listing `auto`
  + every registered theme; `/theme <name>` keeps its existing
  persist-and-report behaviour. (#543, contributed by @J3y0r;
  re-landed via #567 after rebasing onto current main)

## [0.36.1] — 2026-05-09

**Fixes:**

- fix(slash): the slash-suggestion picker sorts by usage frequency, but
  the Enter-time substitution recomputed the list without that sort,
  so the shared selection index dereferenced a differently-ordered
  list — the highlighted row and the command that ran could disagree.
  Both calls now share the same ordering. (#547)

## [0.36.0] — 2026-05-09

**Headline:** terminal-compatibility + interaction-loss fixes from
0.35.0 field reports. Mouse wheel now scrolls chat on cloud / web /
SSH terminals (xterm.js, code-server, Cloud Shell, mobile SSH apps,
tmux without `mouse on`) via DECSET 1007 alternate-scroll, with
native drag-to-select restored on Konsole and friends — no Shift
bypass needed because we're not enabling full mouse tracking.
Render ghosting on CJK / emoji-heavy output goes away (Ink
incrementalRendering off so each frame is a single full-screen redraw
inside the BSU/ESU envelope). Pasting a multi-line block stops firing
one agent call per line on hosts where bracketed-paste markers get
stripped — the parser now wraps unbracketed multi-line chunks in
synthetic markers so the existing accumulator delivers exactly one
paste event. Plan-mode Refine finally pipes the user's typed feedback
to the model instead of dropping it on the floor (PlanVerdict was
missing a feedback field, the rich `synthetic` text was built and
discarded). Web dashboard recovers canonical state on SSE reconnect
so a missed end-of-turn event no longer wedges the page on busy=true
forever.

Plus a setup-wizard theme-picker step with live preview, "did you
mean /…?" suggestions on slash typos, install-source-aware
`reasonix update` (no more forced `npm install -g` for bun/pnpm
users), zh-CN coverage extended to the card components, Windows PATH
normalized before `spawn`, slash-popover windowing stabilized, semver
compare on the dashboard up-to-date check, and self-hosted DeepSeek
endpoints with non-standard key prefixes accepted.

**Features:**

- feat(ui): nearest-slash-command suggestion on typos. Slash typos
  produce an inline "did you mean `/<closest>`?" hint instead of
  silently dropping. (#302)

- feat(wizard): theme-picker step with live preview during setup.
  Previously users had to learn `/theme` after the fact and try
  themes blind. (#518)

- feat(update): `reasonix update` respects the install source
  (npm / yarn / pnpm / bun) instead of always forcing `npm install
  -g`. Stops bun-installed users from getting a stale global from a
  different package manager. (#511)

- feat(i18n): card component labels route through zh-CN. Final TUI
  surface (status / context / streaming / tool / search / reasoning
  / sub-agent / usage cards) localized — closes the English-residue
  gap from prior i18n passes. (#526)

**Fixes:**

- fix(slash): hoist hooks above early returns. SlashSuggestions had
  `useColor` / `useStdout` / `useState` before two early-return
  branches and `useEffect` after, so when matches flipped between
  non-empty and null/empty across renders React saw a different hook
  count and threw "Rendered more hooks than during the previous
  render", killing the entire TUI mid-session. Triggered by everyday
  slash editing (typo → backspace → typing again). Hoisted the
  effect + windowStart math above the returns. (#538)

- fix(tui): wheel scroll on cloud / web / SSH terminals via DECSET
  1007. Old code relied on the implicit "terminal translates
  wheel→↑/↓ in alt-screen" behavior — only on by default in xterm /
  iTerm / Windows Terminal / Alacritty / Kitty. Web/cloud terminals
  ship with it off, leaving the wheel as a dead key. Explicit DECSET
  1007 alternate-scroll routes wheel through the existing ↑/↓ chat-
  scroll handler without enabling full mouse tracking, so native
  drag-select + right-click stay 100% intact (no Shift bypass).
  Paired with `incrementalRendering: false` to drop render ghosting
  on CJK / emoji-heavy output. `--no-mouse` opts out. (#529, partial
  mitigation for #412, fixes #519, #531)

- fix(tui): rescue unbracketed pastes so multi-line content stops
  firing N submits. Bracketed-paste markers (DECSET 2004) don't
  reach the parser on every host — multiplexers strip them, some
  web-SSH gateways drop them, certain Windows pipes never forward
  them. Without them, each `\r` in a paste fires an Enter event
  and the loop submits the partial buffer per line. Heuristic at
  the parser entry wraps multi-line chunks in synthetic paste
  markers when 2+ line breaks (or 1 break with text on both sides)
  are present and no ESC bytes appear. Bare `\r` and `\r\n` stay
  typed-Enter; "abc\r" stays type-then-Enter. (#536, closes #522)

- fix(plan): pipe user feedback through the Refine / Approve /
  Cancel gate. PlanVerdict didn't carry a `feedback` field, so
  the rich text typed in PlanRefineInput was built into a
  `synthetic` string and never sent. Model received bare "user
  requested refinement" tool error and proposed a near-identical
  plan, looking like the suggestion was ignored. PlanVerdict now
  matches CheckpointVerdict's shape and surfaces feedback as the
  tool result string. (#534, closes #533)

- fix(dashboard): resync canonical state on SSE reconnect. The
  `/api/events` stream snapshots only `busy-change` on (re)connect.
  When the connection dropped during a long task — proxy timeout,
  browser background-tab throttle, Node event loop blocked past
  the 25s ping window during heavy work — every assistant_delta /
  assistant_final / tool / modal event fired during the disconnect
  window was lost. If the disconnect happened before
  `busy-change(false)`, the UI wedged on busy forever. EventSource
  `onopen` now refetches `/api/messages` + `/api/modal` on every
  reconnect. (#532, closes #521)

- fix(tui): drop xterm mouse tracking — restore native copy/paste,
  rebind keys. Multiple users reported they couldn't copy text or
  scroll with SGR mouse-tracking modes enabled. ↑/↓ always scroll
  chat now; Ctrl+P / Ctrl+N take over what ↑/↓ used to do in
  PromptInput (cursor up/down inside multi-line draft, falls back
  to prompt history). Pickers still own ↑/↓ while open. Superseded
  by #529's DECSET 1007 approach but the rebinding stands. (#514)

- fix(shell): normalize Windows PATH env before spawn. PowerShell
  passed PATH with trailing semicolons that broke `where` and
  downstream tool resolution on certain Windows builds. (#525,
  closes #520)

- fix(slash): stabilize suggestions windowing + isolate status row
  layout. Slash-suggestion popover was reflowing on every typed
  character; status row width changes were leaking up into the
  composer. (#516)

- fix(config): honor `config.baseUrl` + accept self-hosted key
  formats. Self-hosted DeepSeek-compatible endpoints with non-
  standard key prefixes were rejected by client-side validation.
  (#513)

- fix(dashboard): use semver compare for up-to-date check. Lexical
  string compare flagged 0.35.0 as older than 0.5.10. (#512)

- fix(semantic): unblock Build when daemon is up but binary lookup
  fails. Build path was throwing on daemon start when the embedding
  binary wasn't where the registry expected it. (#507)

**Performance:**

- perf(tui): streaming flush rate tuned to 60Hz default. Earlier
  landed at 20Hz to suppress repaint glitches on fragile terminals
  then raised to 60Hz once frame pacing was proven stable.
  `REASONIX_FLUSH_MS` overrides for hosts that need it. (#515, #517)

## [0.35.0] — 2026-05-09

**Headline:** the agent gains the ability to extend itself from chat,
and bug reporting collapses from a multi-tab scavenger hunt into one
slash. `create_skill` and `add_mcp_server` are first-class tools — "add
a skill that runs typecheck before commits" or "wire up a postgres MCP
server" now works as a normal chat request, with structured args
(description / `runAs` / `allowed-tools` / `model` for skills; transport
+ command + args + catalog hydrate for MCP) so the model never writes
raw YAML or hand-crafts a `name=…` spec. Both reuse the same
persistence paths the wizard / `/skill new` already use, so on-disk
shape stays one source of truth.

`/feedback` opens GitHub's new-issue page with an 11-field diagnostic
block (version + latest-version compare + platform + terminal env
markers including WT_SESSION/TMUX/SSH/WSL + cols×rows + theme + edit /
plan mode + MCP count + session) **pre-filled in the textarea via
`?body=`** — clipboard stays as belt-and-suspenders. The status row
shows a `v<VERSION> · ⚑ /feedback` chip at cols ≥ 100 for
discoverability. Diagnostic block is locked by a test that pins the
exact field set so future additions can't sneak in unannounced.

Plan mode finally surfaces the open-questions block it was already
flagging. The banner detected `Open Questions` / `Risks` / `Unknowns`
headings since 0.30, but the actual questions were swallowed by either
the step list or the 24-line body cap. Now the extracted block renders
under the banner regardless, and refines pre-fill the questions above
the input. Whole plan flow (PlanConfirm / PlanRefineInput /
PlanCheckpointConfirm / PlanStepList) moves through `t()` — the i18n
gap the issue called out is closed.

Read tooling gets sharper: `read_file` auto-preview now embeds a
top-level export outline so callers can pick a `range` without a
follow-up grep, and `search_content` adds a per-file cap + a histogram
fallback so a single high-frequency hit can't drown the result. The
subagent loop now sees its own iter budget and gets a near-cap
countdown.

Plus: dashboard typography pass (sidebar 240→260px column, body
12.5→15px, section headers tightened), cache-hit percentages now show
1-decimal precision across CLI + dashboard, Usage panel chart fully
i18n'd, `spawn_subagent` tool result body finally renders as markdown
instead of literal `**`/`##`/code-fences in the JSON envelope.

**Features:**

- feat(tools): `create_skill` + `add_mcp_server` — let the model
  scaffold from chat. `create_skill` pre-fills frontmatter
  (`description` / `runAs` / `allowed-tools` / `model`) from structured
  args; `add_mcp_server` builds `name=…` specs for stdio / sse /
  streamable-http with `from_catalog` shortcut for bundled entries,
  runs the existing preflight, refuses name collisions. Both register
  alongside native filesystem / shell tools in `reasonix code`.
  (#498, closes #494)

- feat(ui): `/feedback` + version badge in the status row. Slash
  collects an 11-field diagnostic (terminal env / size / theme / edit
  + plan mode / MCP / model + effort / version-vs-latest / session),
  opens GitHub's new-issue URL with the body pre-filled via
  `?body=<urlencoded>`, falls back to clipboard. StatusRow shows
  `v<VERSION>` at cols ≥ 70 and adds a `· ⚑ /feedback` hint at
  cols ≥ 100. Field set is locked by test. (#501, closes #499)

- feat(tools): `read_file` auto-preview embeds a top-level export
  outline. When the file is > 200 lines and no `head` / `tail` /
  `range` was given, the elision marker now also lists function /
  class / const / interface / type / enum names with their line
  numbers (capped at 30 entries with elision). Callers can pick a
  meaningful `range` without a follow-up `search_content`. (#490,
  closes #487)

- feat(search): `search_content` per-file cap + histogram fallback.
  When a single file dominates the result (typical: a generated lock
  file or a long log), the new per-file cap clips its share and the
  histogram footer shows the per-file distribution so callers can
  re-query against a specific file instead of widening the cast. (#495,
  closes #489)

- feat(subagent): tell the child its iter budget; warn near the cap.
  The child loop now sees its `maxToolIters` budget in the system
  prompt (replaces the static "Cap at 6-8 tool calls" prose), and the
  parent injects a remaining-iter hint into tool results once budget
  is tight (`[budget: 3 of 20 tool calls left — wrap up soon]`).
  Stops the explore-burns-17-iters-then-truncates-mid-thought failure
  mode. (#493, closes #488)

**Fixes:**

- fix(plan): surface the open-questions block under the banner; i18n
  the plan flow. The `Open Questions` / `Risks` / `Unknowns`
  detection regex already fired but the block was swallowed by the
  step list or the 24-line body cap. Extract via
  `extractOpenQuestionsSection` and render under the banner regardless
  of `steps` / cap; thread the questions into `PlanRefineInput`
  above the input on `mode === "refine"`. Move `PlanConfirm` /
  `PlanRefineInput` / `PlanCheckpointConfirm` / `PlanStepList` strings
  through `t()` under a new `planFlow` namespace in EN + zh-CN.
  Replace the blank-refine synthetic that asks the model to re-derive
  questions with one that tells it to pick safe defaults. (#497,
  closes #477)

- fix(ui): render `spawn_subagent` tool result body as markdown.
  `formatSubagentResult` returns a JSON envelope with the child's
  final answer in `output`; `ToolCard` rendered the JSON-stringified
  body as raw `<Text>`, so `## headers`, `**bold**`, fenced code
  blocks all leaked through as literal characters. Special-case
  `card.name === "spawn_subagent"`: parse the envelope, pass `output`
  through the same `Markdown` component the streaming reply uses;
  fall back to the line-tail loop on parse failures and `success:
  false`. (#496, closes #491)

- fix(dashboard): bump doc-chrome typography; widen sidebar column.
  Sidebar 240 → 260px (so 2–3 word section labels fit without
  mid-word wraps), section headers 10 → 12px with tracking 0.14em →
  0.08em, links 12.5 → 14px with `line-height: 1.4` and
  `overflow-wrap: anywhere`, body copy 12.5 → 15px, `.swatch .hex` /
  `.scale-row .lbl` 10.5 → 11.5px. Mirrored verbatim into
  `docs/design/agent-dashboard.html`. (#500, closes #461)

- fix(ui): improve cache hit percentage display + Usage chart i18n.
  Cache-hit ratio now shows 1-decimal precision (85.6% rather than
  86%) across the dashboard sidebar, the Stats panel, and `/status`.
  Usage panel chart axes (`USD` / `turns` / `time`) and series labels
  (`cost` / `cache saved` / `turns`) move through `t()` — they were
  hardcoded English. Adds the missing `colWindow` header (was an
  empty `<th>`), promotes numeric columns to right-aligned tabular
  numerals at the header level, not just the body. Thanks
  @kabaka9527. (#503)

## [0.34.1] — 2026-05-09

**Headline:** scroll lag fix for long sessions. `useChatScroll` was
calling `setScrollRows` synchronously on every PgUp / PgDn / arrow /
wheel tick, so a single mouse-wheel gesture (10–30 events on Windows)
triggered 10–30 full Yoga layout passes over the entire `CardStream`
subtree. Layout cost scales linearly with card count — that's why the
lag worsened the longer the session ran. Coalesce deltas into a ref
and flush once per ~16ms; one scroll burst now produces one render
regardless of event volume. `End` (`jumpToBottom`) cancels any
pending delta so it stays instant. Reported in #482 by @GyroChen.

The deeper fix — pre-rendering cards to a row buffer so Yoga isn't on
the scroll/streaming hot path at all — is tracked separately and
covers the streaming-redraw lag too.

**Fixes:**

- `chat-scroll`: coalesce wheel/key events into one render per ~16ms
  frame; long-session scroll no longer scales O(history) (#485, closes
  #482)

## [0.34.0] — 2026-05-09

**Headline:** two big UX shifts in the composer. The `@`-mention picker
is rebuilt as a streaming file browser — `@` alone shows the immediate
directory listing, anything you type fires a cancelable walk that
streams matches in as it finds them, with a `searching… N scanned`
footer. Fixes the unusable-on-large-repos behavior reported by
@xlingyun8-maker (5000 files would evict 90% before ranking, picker
showed nothing). The mouse wheel now scrolls chat history regardless of
where the cursor is, via SGR mouse tracking — wheel events route
through `mouseScrollUp/Down` instead of being mistranslated as ↑/↓ by
Windows Terminal / ConPTY.

The supporting cast: a structured `TipCard` variant replaces the
multi-line text crammed into a step-progress card (the existing
edit-gate hint reported as ugly), a real `/keys` command with the full
keyboard + mouse + copy-paste reference (was a dangling reference in
the edit-gate tip footer for months), and a one-time mouse/clipboard
tip on first launch so users don't think the prompt is broken when
right-click stops doing the terminal's native paste.

Critical bug fix at the bottom: dashboard was silently overwriting
CLI-side `/language` changes by pushing localStorage back to the
server on every page load.

**Features:**

- feat(at-picker): rebuild as file browser with streaming search.
  Empty / trailing-slash queries (`@`, `@some/dir/`) browse one
  directory level via a single `readdir` — folders selectable, drill
  with Tab. Any non-slash filter (`@foo`, `@auth/log`) kicks off a
  cancelable streaming walk across the full tree, matches batch into
  the popup as the walker finds them, footer shows scan progress
  in flight. Drops the 500-file walker cap; cancellation bounds work
  instead. New public API: `walkFilesStream` (streaming + abort),
  `listDirectory` (single-level browse), `parseAtQuery` (dir/filter
  split with trailing-slash awareness). `expandAtUrls` + helpers
  split into `at-mentions-url.ts` to keep `at-mentions.ts` under the
  800-line ceiling. (#479, closes #478)

- fix(scroll): route mouse wheel via SGR mouse tracking. Enable
  DECSET 1006 + 1000 at startup so the terminal reports wheel events
  as `\x1b[<btn;col;row;M` mouse sequences instead of translating
  them to ↑/↓ key presses. The chat-scroll handler routes the
  resulting `mouseScrollUp/Down` events to scrollback, bypassing the
  arrow-key path entirely. ↑/↓ keys retain their existing PromptInput
  bindings (history recall on empty buffer, cursor motion otherwise).
  The SGR mouse parser already lived in `stdin-reader.ts`; this just
  turns on the terminal-side feature. Cost: terminal-native drag-to-
  select needs a modifier (Shift on Windows Terminal / Alacritty /
  WezTerm, Option on iTerm2) — same convention as tmux, Claude Code,
  Cursor's terminal. (#479)

- feat(ui): structured TipCard variant for onboarding hints. The
  edit-gate one-time tip rendered as raw multi-line text inside a
  `stepProgress` LiveCard — `✓` glyph (success semantic, wrong for
  educational content) plus a manually-inlined `▸ TIP:` prefix,
  columns aligned with hand-counted spaces that wrap badly on narrow
  terminals. Replaces with a dedicated `TipCard` kind: single `ⓘ`
  glyph in accent color, topic + "shown once" badge in a justified
  header row, each row gets its own `<Text>` with column alignment
  driven by `string-width` (CJK-correct), footer separated from body
  by a blank row, no border. (#480)

- feat(ui): `/keys` reference + first-run mouse/clipboard tip.
  `/keys` was already referenced in the edit-gate tip's footer ("Run
  /keys anytime for the full list") but no handler existed; typing
  `/keys` hit the unknown-command branch. Adds a multi-section
  TipCard with the full keyboard / mouse / copy-paste / edit-gate
  reference. Adds a first-run mouse + clipboard tip mirroring the
  edit-gate pattern (suppressed thereafter via a
  `mouseClipboardHintShown` flag) so users don't think the prompt
  is broken when right-click stops doing the terminal's native
  paste. TipCard now supports multiple sections; existing single-
  section tips are unchanged. New i18n helper `tObj<T>(path)` for
  structured translation entries. (#481)

**Bug fixes:**

- fix(dashboard): stop pushing localStorage lang back to server on
  init. The dashboard's `initLangFromServer()` had a one-way sync
  rule: when localStorage's lang differed from server config AND
  localStorage was tagged "explicit", it POSTed localStorage's value
  back, silently clobbering CLI-side `/language` changes whenever the
  dashboard tab next loaded (including auto-restored tabs from
  previous browser sessions). Server config is the single source of
  truth now; localStorage stays as a render-cache to avoid first-paint
  flicker but is never pushed back. Removes `EXPLICIT_KEY` /
  `isExplicit` / `markExplicit` entirely. (#483)

## [0.33.2] — 2026-05-09

**Headline:** two bug fixes for #468 reported by @dacec354.

**Bug fixes:**

- fix(ui): ↑/↓ on an empty buffer recalls prompt history again. The
  binding was unbound from arrows back in 9254d3a because Windows
  Terminal + ConPTY can translate mouse-wheel events to ↑/↓
  keystrokes (wheel-up was clobbering the prompt with a recalled
  message); history moved to Ctrl+P / Ctrl+N. That was right for
  legacy ConPTY but broke the universal CLI convention for
  everyone else (bash / zsh / fish all bind ↑ to history). Restored
  ↑/↓ on empty buffer = history; Ctrl+P / Ctrl+N stays as the
  wheel-immune fallback. Dead `chatScrollHandoff` plumbing dropped.
  (#475, closes part 1 of #468)

- fix(doctor): tokenizer check now finds the file. The runtime
  resolver in `tokenizer.ts` had three candidates including a
  `createRequire("reasonix/package.json")` probe and worked
  reliably; the doctor had its own copy of the path math that
  walked `dist/cli/commands/doctor.js → ../../../data/`. After the
  lazy-import refactor in #467 the doctor compiles to
  `dist/cli/doctor-HASH.js` (one level shallower), so three `..`
  walked above the package root and reported "tokenizer not
  found" even when the npm tarball had it. Reuse the runtime
  resolver so the two paths can never disagree. (#475, closes part
  2 of #468)

## [0.33.1] — 2026-05-09

**Headline:** the bottom status row now shows the wallet. Both
`status.balance` and `status.sessionCost` were already being
populated by the reducer (refreshed on every submit), and a
`balanceColor()` helper with red/orange thresholds had been sitting
unused in the theme — but `StatusRow` only ever rendered the
per-turn cost and cache-hit pills. Pure plumbing gap; users had
to type `/cost` to see the running spend or remaining DeepSeek
balance. Plus a small polish pass on the prompt input footer.

**UI:**

- feat(ui): wallet pill on the status row. New segment renders
  right of the cache pill: `⛁ ¥1.20 spent  /  ¥45.32 left`. Spent
  shows when `sessionCost > 0`, balance shows when known; the
  separator only renders when both are present. Balance is colored
  via `balanceColor()` (red <¥5, orange <¥20, brand otherwise).
  Hidden on terminals narrower than 90 cols so the row doesn't
  wrap. (#473)

- feat(ui): friendlier prompt input. Placeholder reads "ask
  anything · slash for commands · at-sign for files" instead of
  "type a message". Hint footer extracted into a `HintRow`
  component with keycap/label spacing — keys (⏎ ⇧⏎ ↑↓ esc ^C) in
  `FG.meta`, labels in `FG.faint`. Replaces `shift/alt+⏎` with
  `⇧⏎` and `ctrl-c` with `^C`. (#473)

## [0.33.0] — 2026-05-09

**Headline:** the filesystem toolbelt grew a hand. Three new tools —
`multi_edit` for atomic multi-site SEARCH/REPLACE in one file (or
across files in one call), `todo_write` for lightweight in-session
intent tracking, and `glob` for mtime-sorted file walks with
picomatch syntax — close the gaps where the model was either
round-tripping eight `edit_file` calls or losing its plan to a
context fold. `search_content` also gains `-C N` context lines.

The other half is cold-start surgery (#464). Stage 1 adds a zero-cost
profiler gated behind `REASONIX_PROFILE_STARTUP=1`. Stage 2 lazy-
imports every per-command module and the dashboard server, paying
for the chat UI only when `reasonix code` actually runs. `reasonix
version` and `reasonix --help` drop ~290ms (~440ms → ~140ms);
`reasonix code` is unchanged on the hot path. Critical bug fix at
the bottom: a long-session OOM where every tool result was retained
indefinitely in a useRef array left behind when `/tool` was deleted.

**Features:**

- feat(tools): `multi_edit` — atomic batch SEARCH/REPLACE. N edits
  apply sequentially against an in-memory buffer with one write at
  the end; any failure (empty edits, search not found, ambiguous
  match) leaves the file untouched. Edit N+1 can match text inserted
  by edit N (composable refactors). Cuts the round-trip cost of
  multi-site rewrites and removes the half-applied-edit failure mode
  of looping `edit_file`. (#458)

- feat(tools): `multi_edit` cross-file mode. Same atomicity guarantee
  extended across files: dry-run all targets, then write. One failure
  rolls the whole batch back. (#462)

- feat(tools): `todo_write` — in-session task tracker. Replace-set
  semantics (full list every call), no approval gate, no file writes.
  Each item is `{ content, status, activeForm }` with `status: pending
  | in_progress | completed`. Validated: at most one `in_progress` at
  a time. Empty list signals work-done. Sits between `submit_plan`
  (heavy: approval + checkpoints) and prose lists (lost on history
  fold). Stays callable in plan mode (`readOnly: true`). (#460)

- feat(tools): `glob` — mtime-sorted file walks. Picomatch syntax
  (`**/`, `*.{ts,tsx}`); defaults to `sort: "mtime"` so "what did I
  touch lately" works without arguments. `sort: "name"` for
  deterministic listings. Skips deps by default, capped at 200 (1000
  max) with overflow notice. (#462)

- feat(tools): `search_content` gains `context: N`. Semantics match
  `grep -C N`; output uses ripgrep convention (`:` after match line,
  `-` after context). (#462)

**Performance:**

- perf(cli): `REASONIX_PROFILE_STARTUP=1` cold-start profiler. Marks
  at `cli_module_loaded`, `chat_command_enter`, `config_loaded`,
  `mcp_launch`, `mcp_connected_M_of_N`, `code_command_enter`,
  `semantic_bootstrap_start`/`_done_*`, `ink_render_complete`. Single
  env-var read when off; dumps to stderr at first paint when on.
  Stage 1 of #464. (#466)

- perf(cli): lazy-import every per-command module. Each
  `reasonix <subcommand>` only loads its own command's chunk. tsup
  splits, Node loads on first invocation. `reasonix version` and
  `reasonix --help` drop ~290ms (~440ms → ~140ms); `reasonix code`
  hot path unchanged (within noise). Stage 2 of #464. (#467)

- perf(cli): lazy-import dashboard server. ~4200 LOC of HTTP / static
  asset code (`startDashboardServer`) moved to a dynamic
  `await import()` inside the App startup IIFE — only loads when the
  user actually opens the dashboard. Two new App marks
  (`app_render_start`, `app_inner_start`) clarify the first-paint
  delta. (#469)

**Bug fixes:**

- fix(ui): drop dead `toolHistoryRef` leak. `/tool` was removed in
  #453 but its supporting plumbing stayed behind: every tool result
  was being pushed into a useRef array with no consumer reading it,
  so long sessions retained the full text of every Read / Grep /
  Bash call indefinitely. Reported by @trytsomile as a
  `FATAL ERROR: Ineffective mark-compacts near heap limit` crash
  after ~2.6h on v0.31.0 (V8's 4GB ceiling). 48 lines deleted across
  4 files; `state.cards[].output` (which actually drives scrollback
  rendering) is untouched. (#471, closes #465)

- fix(/cwd): re-bootstrap `semantic_search` on workspace swap.
  FS / shell / memory tools re-registered against the new root, but
  `semantic_search` kept pointing at the old one — queries silently
  hit the previous project's index, or the tool stayed registered
  when the new directory had no index. Split the async re-bootstrap
  out of the sync `reregisterTools` callback; App.tsx fires
  `void reBootstrapSemantic(root).then(postInfo)` so the slash
  dispatch returns synchronously. Tail of #459. (#470)

- fix(ux): fuzzy `@`-mention ranking. The picker's substring-only
  ranker rejected typo'd subsequences — `@atmnt` returned nothing for
  `at-mentions.ts`. Adds a fuzzy-subsequence fallback that triggers
  only when the substring lookup misses; substring hits still win
  (classes 0/1/2 cap at 29_999, subseq starts at 30_000). Also adds
  the `/cwd` slash for in-session workspace swap. Parts 1+2 of #459.
  (#463)

## [0.32.0] — 2026-05-08

**Headline:** the slash surface lost weight. Eleven redundant
commands gone (`/clear`, `/keys`, `/models`, `/effort`, `/rename`,
`/forget`, `/think`, `/tool`, `/apply-plan`, `/semantic`,
`/resume`), the unified preset+model picker replaces three
near-identical commands, and the two heaviest features almost
nobody opted into — `/harvest` (Pillar-2 plan-state extraction)
and `/branch` (parallel-sample selector) — are deleted along with
their backing modules, events, transcript fields, and CLI flags.
The four-pillar architecture collapses to three. The slash registry
now carries a `group` tag (chat / setup / info / session / extend
/ code / jobs / advanced) and bare-`/` suggestions render those
groups with advanced rows hidden behind a `+ N advanced · type to
search` footer. A new `~/.reasonix/slash-usage.json` counter
sorts frequent commands first within a prefix.

The other half of the release is plan-mode UX. PlanLiveRow had
nothing to dock — a code path that should have materialized an
"active" plan card on approval was missing, so the bottom strip
stayed empty after `/plan`. Fixed. And the per-step "Checkpoint —
step done" picker fired in auto/yolo too, defeating the whole
point of those modes; auto/yolo now resolve "continue" without
prompting while still creating per-step rollback snapshots so
`/restore` granularity stays intact. Plus a long-standing
`@`-mention bug: typing `@docs/` produced an empty `not-file`
placeholder. It now expands to a recursive `<directory>` listing
respecting the project's gitignore, and symlinked source files
finally appear in the `@`-picker.

**Features:**

- feat(semantic): OpenAI-compatible embedding provider. Configure
  custom API URL / key / model / request body for embeddings,
  replacing the Ollama-only setup. Dashboard semantic panel adds
  a provider dropdown with "OpenAI-Compatible" alongside Ollama,
  clearer status messages, and detailed indexing-job phases
  (scanning / embedding / writing). Community contribution from
  @kabaka9527. (#424)

- feat(slash): unified preset+model picker. `ModelPicker` shows the
  three presets at the top with cost/headline copy and the model
  catalog below; cursor lands on the active row (auto-detects which
  preset matches the loop's current model + effort + autoEscalate).
  Both `/preset` (no arg) and `/model` (no arg) open it. (#453)

- feat(slash): grouped suggestions + usage telemetry.
  `SlashCommandSpec` gains a `group` field; suggestion palette
  renders section headers on bare `/` with advanced rows hidden
  behind a footer. New `~/.reasonix/slash-usage.json` counter
  (read-modify-write, atomic rename) feeds `suggestSlashCommands`
  so frequent commands sort first; `slash.invoked` events emit to
  events.jsonl for cross-session analysis. `/help` walks the same
  grouped registry so there's one source of truth. (#453)

**Bug fixes:**

- fix(plan): dock active plan card. `case "plan_proposed"` had
  been dropping the gate payload's `steps`/`summary`, and the
  approve path never dispatched `plan.show` — so no card with
  `variant: "active"` ever existed and `isActivePlanInFlight`
  returned null. PlanLiveRow now docks correctly after approval,
  and the dock tracks tail rewrites on revise-accept. (#454)

- fix(plan): auto/yolo skip the per-step checkpoint picker. The
  "Checkpoint — step done" picker fired after every
  `mark_step_complete` regardless of edit mode — shell and
  edit-gate already self-skip in auto/yolo, but plan checkpoints
  kept stopping the model. The gate handler now checks
  `editModeRef` and resolves "continue" without UI; per-step
  rollback snapshot still runs so `/restore` granularity is
  preserved. `review` mode is unchanged. (#454)

- fix(at-mentions): `@<dir>` expands to a recursive listing.
  Was treated as a `not-file` skip, leaving the model with an
  empty placeholder. Walks the project root with the existing
  gitignore layers, filters to entries under the directory, and
  inlines a `<directory path="..." entries="N">` block capped at
  `DEFAULT_AT_DIR_MAX_ENTRIES` (200). `@docs/` and `@docs`
  resolve identically. (#455, closes #451)

- fix(at-mentions): symlinks-to-files appear in the `@`-picker.
  `Dirent.isFile()` returns false for symlinks, so symlinked
  source files never showed up in completions. Both
  `listFilesWithStatsSync` and `listFilesWithStatsAsync` now stat
  through symlinks; symlinks-to-files come back, symlinks-to-dirs
  stay dropped (cycle risk), broken links stay dropped (nothing
  to point at). (#455, closes #451)

**Removals — slash commands:**

- `/clear` (merged into `/new` as alias — was the most common
  source of "what's the difference?" confusion)
- `/models` (picker covers it)
- `/keys` (folded into `/help`)
- `/resume` (sessions picker has switch action)
- `/semantic` (folded into `/doctor`)
- `/effort` (preset locks effort)
- `/rename` and `/forget` (sessions picker actions)
- `/apply-plan` (plan picker handles the fallback path)
- `/think` and `/tool` (debug-only; events.jsonl records both)
- `/mcp browse` entry (handler still routes `["browse"]`)

**Removals — features:**

- `/harvest` (Pillar-2 plan-state extraction): `src/harvest.ts`,
  `--harvest` CLI flag, `harvestedTurns` transcript field.
- `/branch` (parallel-sample selector): `src/consistency.ts`,
  `src/loop/branch.ts`, `BranchCard`,
  `branch_start/progress/done` events, `--branch` CLI flag.
- `benchmarks/harvest/` deleted; `ARCHITECTURE.md` collapses from
  four pillars to three; README + zh-CN + `dashboard/PARITY.md`
  updated.

**Removals — public API:**

- `src/index.ts` drops `harvest`, `runBranches`,
  `aggregateBranchUsage`, `defaultSelector`, `emptyPlanState`,
  `isPlanStateEmpty`, and the `TypedPlanState`, `HarvestOptions`,
  `BranchSample`, `BranchSummary`, `BranchProgress`,
  `BranchOptions`, `BranchResult`, `BranchSelector` types.
  Consumers depending on these break intentionally — they were
  experimental from the start and never met the cache-first
  cost target this project gates on.

## [0.31.0] — 2026-05-08

**Headline:** a Mac user reported a DeepSeek 503 day where Reasonix
showed a wall of raw `DeepSeek 503: <html>...` and they couldn't tell
if our agent had crashed or the upstream API was down. Two threads of
work fell out of that single bug: a friendly outage notice with a
1.5s reachability probe to `/user/balance` (so we can say "DS main
API answered, but /chat/completions is failing — their problem") and
a full sweep of every hardcoded English string a Chinese user could
hit. ~150 strings across 8 files moved into the `loop.*` / `errors.*`
/ `app.*` / `hooks.*` / `summary.*` / `wizard.*` namespaces with
zh-CN translations. The setup wizard now opens with a language picker
defaulting to `detectSystemLanguage()` — for the case where
`Intl.DateTimeFormat().resolvedOptions().locale` returns the wrong
locale and a user shouldn't have to discover `/language` after
finishing setup in English.

The other half of the release is dashboard parity work — picker
modals (sessions / checkpoint / MCP marketplace), viewer modal for
`/replay`, plus cockpit / budget gauge / model picker / loop control
panel / `/pro` one-shot — closing buckets B-E of the #369 web-parity
tracker.

**Features:**

- feat(loop): friendly DeepSeek 5xx error with reachability probe.
  When the chat endpoint returns 5xx (after retry.ts has already
  retried 4× with backoff), `formatLoopError` now spawns a 1.5s
  `/user/balance` probe and renders one of three messages: no probe
  (generic outage notice), reachable (main API up but /chat dying),
  unreachable (DS or your network is down). All three say "this is
  a DeepSeek-side problem, not Reasonix" and link
  https://status.deepseek.com. Removes the misleading file header in
  `loop/errors.ts` that claimed retry.ts swallowed all 5xx — it
  doesn't, and never did. (#440)
- feat(wizard): first-launch language picker + full i18n. New
  `language` step before `apiKey`, cursor defaults to
  `detectSystemLanguage()` marked `(detected)`. Selection saves
  immediately so all later wizard screens render in the chosen
  language. Re-running `reasonix setup` opens at the same step with
  the cursor on the saved language so Enter is a no-op. The wizard's
  ~30 hardcoded strings (welcome, prompts, validation errors, MCP
  catalog hints, review labels, save errors, saved screen) all moved
  to a new `wizard.*` namespace with zh-CN. (#442)
- feat(dashboard): picker modal protocol for web parity. New
  `picker` modal kind drives sessions, checkpoint, and MCP
  marketplace pickers from the same protocol. Closes the gap where
  TUI-only modals stayed inaccessible from `/dashboard`. (#417,
  #418, #419, #420)
- feat(dashboard): viewer modal kind for `/replay`. Loads an archived
  plan into a read-only time-travel snapshot, mirrors the TUI replay
  experience. (#421)
- feat(dashboard): cockpit tile + budget gauge + 14-day cost trend.
  At-a-glance current-session telemetry on the overview panel. (#431)
- feat(dashboard): editable model picker in settings + `/pro`
  one-shot panel + loop control (start / stop / countdown). The
  settings tab is now the single place to flip model preset, arm
  `/pro` for the next turn, or start an autonomous loop without
  switching to the TUI. (#430, #432, #433)
- feat(dashboard): server surface for `/pro` / `/budget` / `/model`
  / `/loop`. POST endpoints under `/api/cockpit/*` carry the
  mutations the panels above need. (#429)

**i18n sweep:**

- i18n(loop/errors): localize DeepSeek error messages — context
  overflow, 401/402/422/400, 5xx (with the new reachability probe
  variants), reason prefixes for budget/aborted/context-guard/stuck.
  20 keys + zh-CN. (#444)
- i18n(loop): 14 user-facing yields in `step()` — budget exhausted /
  80% warning, /pro armed, aborted-at-iter, tool-budget warning,
  preflight fold/no-fold, flash + auto escalation, storm-broken,
  history compaction (regular + aggressive), forcing-summary. The
  `loop.*` namespace had 7 dead keys defined but never wired —
  removed and replaced with 20 that match the actual yield shapes.
  (#445)
- i18n(hooks/summary): hook outcome formatter (`hook PreToolUse/Bash
  \`cmd\` block (output truncated at 256KB)`) and the force-summary
  status / hallucinated-fallback / failed-fallback strings now go
  through `t()`. New `hooks.*` and `summary.*` namespaces. (#446)
- i18n(app): ~26 hardcoded strings in `App.tsx` plus seven existing
  `ui.*` keys that had been declared but never called (same dead-key
  pattern as `loop.*`). New `app.*` namespace covers walk modal,
  edit-mode cycle (review/auto/yolo), edit gate, dashboard stopped,
  hash-memory note, bash-mode failures, hook header rows,
  @mentions / @url, shell confirm, checkpoint saved, plan
  continue / stop / revise. (#447)
- i18n(slash): four lagging slash handlers — `web-search-engine.ts`
  was 0% localized, plus `mcp.ts` / `plans.ts` / `semantic.ts` had
  small gaps. ~22 new keys. (#448)
- i18n(dashboard): translate the plan `idle` status pill — the
  `active` / `done` pills already used `t()` but the third branch
  was hardcoded English. (#443)

**Bug fixes:**

- fix(search): honor abort during recursive fs scans — Esc during a
  large `search` tool call now exits promptly instead of finishing
  the walk. (#400)
- fix(ui): refresh model badge on dashboard preset change and /pro
  turns — the header pill stayed stale across server-side
  preset/pro switches. (#403)
- fix(permissions): match Windows project keys case-insensitively —
  the project allowlist hashed `C:\Foo` differently from `c:\foo`,
  causing entries to "disappear" depending on which case the cwd
  carried. (#402)
- fix(prompt): inline short single-line pastes verbatim — the long-
  paste collapser was firing on tiny one-liners and burying them
  behind a "(N chars pasted)" placeholder. (#397)

**Tests / refactor / docs:**

- test(mcp): cover startup summary states (#396)
- chore: improve loop.ts tests (#271)
- refactor(ui): quiet chat-screen chrome — fewer always-on rows on
  the welcome card so the prompt stays close to the top. (#411)
- docs(readme): canonical install + subcommand cheatsheet (#408)
- docs(issues): split off display/rendering template, collect
  terminal host info inline. (#412)
- docs(dashboard): PARITY.md audit — bucket E of #369. (#439)

## [0.30.5] — 2026-05-07

**Headline:** three contributor-led follow-ups from the #350 RFC plus
the #366 onboarding piece. The repeat-loop storm guard now exempts
obviously-safe inspector tools (`read_file`, `list_directory`,
`job_output`, `list_jobs`) so a model intentionally re-reading state
isn't flagged as stuck. A new `wait_for_job(jobId, timeoutMs?)` tool
replaces N-iteration polling loops with a single blocking call —
returns the moment the job exits or emits new output. And `/skill
new <name>` finally provides the missing creation entry-point for
user skills, scaffolding a stub with the right frontmatter so
first-time users don't have to read the source to author a skill.

**Features:**

- feat(storm): add `stormExempt` flag on `ToolDefinition`, set on
  `read_file`, `list_directory`, `job_output`, `list_jobs`. Cheap
  state-inspection no longer trips the repeat-loop guard. Mutating
  tools and unknown tools still go through the existing window-and-
  threshold check. (#350, PR #388 by @ctharvey)
- feat(jobs): new `wait_for_job(jobId, timeoutMs?)` shell tool —
  blocks until the job exits or emits new output, bounded by
  `timeoutMs` (default 5000, clamped to 0..30000). Returns
  `{ exited, exitCode, latestOutput }`; `latestOutput` is the
  delta since the call started, not the full buffer. Rides the
  existing job registry's exit + output events; one call replaces
  N polling iterations and is token-cheaper than the prior
  re-call-job_output loop. (#350, PR #390 by @ctharvey)
- feat(skills): `/skill new <name>` scaffolds a stub at
  `<project>/.reasonix/skills/<name>.md` with minimal frontmatter
  + a comment block listing the optional knobs (`runAs`,
  `allowed-tools`, `model`). `/skill new <name> --global` writes
  under `~/.reasonix/skills/` for cross-project use; auto-falls-
  back to global when there's no project root. The empty
  `/skill list` now ends with an explicit "no remote registry yet
  — scaffold one with `/skill new <name>`" line so users don't
  hunt for a marketplace that doesn't exist. (#366, PR #394)

**Bug fixes:**

- fix(skills): atomic create with `wx` flag — close the TOCTOU
  race between `existsSync(...)` and `writeFileSync(...)` that
  CodeQL flagged. The existence check IS the atomic write now;
  `EEXIST` from a parallel writer surfaces as the same "skill
  already exists" error instead of silently overwriting. (PR #394)

## [0.30.4] — 2026-05-07

**Headline:** sweep of the user-reported bug + onboarding queue from
the 0.30.2 / 0.30.3 launch day. Resume now restores the full session
state (cache hit %, cost, last context bar — previously they all
showed zero on a fresh boot until the first turn landed). The model
pill on assistant cards reflects the model that actually answered
after `/model` or `/preset` switches it. Bare `/model` opens an
interactive picker — typed-id entry stays for power users.
PowerShell users get Shift+Tab back via three additional encodings
(modifier-encoded back-tab, modifyOtherKeys, Kitty keyboard). And a
class of "junk text after exit" on Linux/fish (terminal-feature
replies leaking into the parent shell) gets a defensive stdin drain
in the exit path.

`--dir` is now discoverable for beginners — surfaced in the welcome
banner, the `/status` panel, the filesystem sandbox-escape error,
and a Getting Started callout in both READMEs.

**Bug fixes:**

- fix(stats): persist cache totals + `lastPromptTokens` across
  resume. `SessionMeta` only carried `totalCostUsd` / `turnCount`,
  so on every resume `/status` showed 0 context + 0% cache hit until
  the first turn actually fired (even though the prefix was already
  cached, costing $0.01 per turn). Three new fields are persisted
  per-turn and seeded into `SessionStats` on resume; the existing
  carryover plumbing now covers cache + last context.
  (#364, PR #384)
- fix(ui): `/model <id>` and `/preset {auto,flash,pro}` now update
  the active model in the agent store so the next assistant card
  pill reflects the new selection. Previously `state.session.model`
  was set once in `initialState()` and never mutated, so the pill
  showed the launch-time model regardless of what actually answered
  the turn. New `session.model.change` event; cards already opened
  keep their captured model so mid-turn auto-escalation doesn't
  retroactively relabel. (#372, PR #385)
- fix(input): recognize three additional Shift+Tab encodings for
  PowerShell hosts and modern terminals — `\x1b[1;2Z` (modifier-
  encoded back-tab some PowerShell hosts emit), `\x1b[27;2;9~`
  (modifyOtherKeys level 2, which we already enable on startup),
  `\x1b[9;2u` (Kitty keyboard envelope). Without these the edit-
  mode cycle was silently dropped on PowerShell.
  `/mode` typed fallback continues to work. (#373, PR #386)
- fix(tty): drain pending feature-detection replies on exit. Linux
  reporters saw `^[]11;rgb:...^[\^[[33;1R^[[?62;1;4c` printed by
  fish / bash after exiting reasonix — those bytes are responses to
  OSC 11 / CPR / DA1 queries the runtime emits during startup that
  sit in stdin's queue until exit. New `drainTtyResponses(50ms)`
  reads-and-discards anything queued before control returns to the
  parent shell. Layered on top of 0.30.3's alt-screen mitigation
  (`--no-alt-screen` users get the fix too). (#365, PR #391)

**Features:**

- feat(ui): bare `/model` opens an interactive model picker — arrow-
  key list, current model marked, `[r]` refreshes the catalog, esc
  cancels. Seeds from the live DeepSeek catalog
  (`useSessionInfo.listModels()`); falls back to the four known
  DeepSeek ids when the catalog hasn't loaded yet so the picker
  isn't empty on first open. The current id is always included even
  when the API didn't return it. `/model <id>` typed entry stays
  for power users. (#371, PR #387)
- feat(ui): surface `--dir` / pinned workspace for first-time users.
  WelcomeBanner shows the workspace + relaunch hint in code mode;
  `/status` adds a `workspace <path> · pinned at launch` line; the
  filesystem sandbox-escape error points at `reasonix code --dir
  <path>` instead of just dropping a raw error; both READMEs gain a
  Getting Started subsection on `--dir`. No new slash command —
  mid-session retargeting is intentionally not supported (the
  message log + memory paths get tangled with stale roots).
  (#370, PR #389)

## [0.30.3] — 2026-05-07

**Headline:** the chat scroll rewrite lands. Ink 5.2 → 7.0.2 / React
18.3 → 19.2, the cell-diff renderer is retired, and `reasonix code` /
`reasonix chat` default to alt-screen with row-precision virtual
scroll. PgUp / PgDn / mouse wheel scroll history; an empty prompt + ↑
also scrolls (Ctrl+P / Ctrl+N still recalls prompt history). When
scrolled away from bottom, the prompt hides and a `📖 reading
history — End / PgDn to return` hint appears. Resize-ghost dividers
and `<Static>`-related scroll-yank artifacts are gone with the
renderer that produced them. `--no-alt-screen` keeps the legacy
in-shell-scrollback behavior.

`web_search` gains a configurable backend — Mojeek stays the default,
but `/web-search-engine searxng <url>` switches to a self-hosted
SearXNG instance for users whose network blocks Mojeek. And the MCP
filesystem sandbox now fails with an actionable
`mkdir -p '<path>'` hint instead of a raw Node stack when the
configured directory doesn't exist; the wizard offers to create it
inline at config time.

**Features:**

- feat(ui): row-precision virtual scroll on Ink 7 + React 19.
  `<Static>` retired (incompatible with alt-screen reflow);
  `React.memo(CardRenderer)` plus reference-stable cards in the
  reducer skip the reconciler on unchanged history. `useChatScroll`
  drives an outer `overflow=hidden` clip + inner `marginTop=-N`
  slide; `useBoxMetrics` reports inner / outer heights so bounds
  clamp and auto-pin to bottom on new content. `App` owns
  PgUp/PgDn/End/wheel; PromptInput hands off ↑/↓ on empty buffer
  when pinned + idle. Ticker migrated to Ink 7's shared
  `useAnimation`. (PR #380)
- feat(web): configurable `web_search` backend with SearXNG support.
  `/web-search-engine` shows / switches the active engine; URL is
  persisted to `~/.reasonix/config.json`. Mojeek remains the default;
  the original Mojeek path is preserved as `searchMojeek()`. Protocol
  auto-normalizes (`localhost:8080` → `http://...`); an unreachable
  SearXNG endpoint surfaces an install hint instead of a raw fetch
  error. (PR #338)

**Bug fixes:**

- fix(mcp): preflight the filesystem sandbox directory before
  spawning `@modelcontextprotocol/server-filesystem`. Missing
  directories now throw `MCP filesystem sandbox '<path>' does not
  exist — create it with: mkdir -p '<path>'` instead of a raw Node
  stack from inside `npx`'s child. The init wizard adds an inline
  `[Y] create it (mkdir -p) / [N] enter a different path` confirm
  step when the user types a path that doesn't exist, so bad config
  never reaches disk. Spawn-time path deliberately does not
  auto-mkdir — by then the user may not remember writing the
  config. (#362, PR #379)
- fix(readme): website URLs corrected from `/reasonix/` to
  `/DeepSeek-Reasonix/`. (PR #375)

**Chores:**

- chore(issue-template): bug template now asks for shell + terminal,
  and the model-id examples track the current DeepSeek model
  lineup. (PR #378)

## [0.30.2] — 2026-05-07

**Headline:** five user-visible polish items from the @dacec354 triage
batch. The streaming reply now carries a live `42 t/s` throughput pill
(plus a `1.2k tok · 42 t/s` summary on settled), and `ctrl-o` toggles a
full-tail view so a long plan / todo can be read while it's still
being written. The auto-mode undo banner gains a `space`-to-pause
keybind for users who want a beat to think before the 5-second window
expires. SessionPicker and the dashboard's session-cost displays both
respect the user's wallet currency now — USD wallets see `$0.05`, CNY
wallets see `¥0.36` end-to-end. And a long-standing scrollback bug
that left the "reasoning…" spinner spinning forever after reasoning
ended is fixed.

**Features:**

- feat(ui): live `42 t/s` pill on the streaming reply card; settled
  card shows `1.2k tok · 42 t/s` summary. Computed via the bundled
  DeepSeek tokenizer; gated below 4 tokens / 500 ms so the first
  chunk doesn't print bogus rates. Re-renders ride the slow tick so
  the rate keeps updating during chunk silence. (#334, PR #356)
- feat(ui): `space` toggles pause / resume on the auto-mode 5-second
  undo countdown. While paused the bar freezes at the captured
  fraction, the badge swaps to `Ns · paused`, and pressing `space`
  again resumes from where it stopped. The `u` and `space` keybinds
  share the same modal-and-prompt-empty gating. (#337, PR #356)
- feat(ui): `ctrl-o` toggles "expanded" mode on the live streaming
  card. Expanded shows up to 60 visual lines (capped so the card
  can't swallow the whole viewport) plus a `⋯ N earlier lines above`
  hint when content overflows. Auto-resets to collapsed at turn end.
  A `expanded ⌃o` / `preview ⌃o` pill in the card header advertises
  the keybind. (#335, #337, PR #359)

**Bug fixes:**

- fix(ui): `splitCardStream` only treated the LAST card as live,
  committing every earlier card to Ink's `<Static>`. When the model
  streamed reasoning then content (or kicked off a tool card), the
  reasoning card was no longer last — it got frozen into `<Static>`
  while still `streaming: true`. `<Static>` doesn't re-render frozen
  items, so when `reasoning.end` later set `streaming: false`, the
  spinner kept spinning forever. The split now scans for the first
  unsettled card and keeps everything from that index onward live;
  a card only commits to `<Static>` once it's settled AND every
  earlier card is too. (PR #358)
- fix(ui): SessionPicker hardcoded `¥` and ran USD → CNY itself, so
  USD-wallet users saw `¥X.XX` in the session list. `SessionMeta`
  gains `balanceCurrency`; App.tsx writes the live wallet currency
  alongside `totalCostUsd` on each turn save. Picker accepts a
  `walletCurrency` prop and falls back to each row's stored
  currency. Cost rendering routes through the shared `formatCost()`
  helper. (#312, PR #357)
- fix(dashboard): cost displays were hardcoded to `$` via `fmtUsd()`,
  so a CNY-wallet user saw `session $0.5190` in the dashboard while
  the same session read `¥0.024` in the CLI — both the symbol AND
  the magnitude diverged because no conversion happened. Dashboard
  now has its own `fmtCost(usd, currency)` mirroring the CLI's
  conversion (CNY × 7.2). Overview current-session cost, cost-trend
  day average, and the chat panel rail / status-bar costs all
  thread the wallet currency from the cockpit balance. Claude-
  equivalent comparisons in `usage.ts` stay USD by design — Claude's
  API is USD-priced regardless of the user's wallet. (PR #360)

## [0.30.1] — 2026-05-07

**Headline:** two TUI ghost-rendering fixes for issues that only showed
up on the published binary. The CLI bundle now uses real Ink in
production instead of the cell-diff renderer that source mode never
exercised, eliminating a whole class of bugs invisible to `npx tsx`
repros. The `submit_plan` approval picker no longer leaves a
duplicated row behind when arrow-navigating choices — the live tool
card above the modal is suppressed while the picker owns the screen.

**Bug fixes:**

- fix(renderer): drop the `tsup` `ink → ink-compat` alias and the
  `noExternal` for `ink` / `ink-text-input`. The CLI bundle keeps
  `from "ink"` external; `ink` and `ink-text-input` move to runtime
  `dependencies` so npm install pulls the real package. The
  cell-diff renderer is no longer on the user-facing path; it's
  retained only for direct test imports. Same behavior as `npx tsx
  src/cli/index.ts` mode — TUI bug repros from source mode are now
  valid for the published binary again. (#346, PR #354)
- fix(ui): `CardStream` accepts a `suppressLive` flag; `App.tsx`
  computes a `modalOpen` flag from the union of pending modal states
  and passes it through. While any picker / confirm modal owns the
  screen, the unsettled live tool card above it stops repainting,
  removing the rerender competition that left stale rows during
  arrow-key navigation. (#352, PR #353 — thanks @ctharvey)

## [0.30.0] — 2026-05-06

**Headline:** slash commands grow first-class aliases, and the
cell-diff renderer hardens column targeting against per-cell width
miscounts. `/quit` and `/q` now resolve to `/exit` from a single
declaration on the spec instead of ad-hoc handler mirrors; `/?` →
`/help`, `/reset` → `/new`, `/lang` → `/language` follow the same
path. The renderer's `moveTo()` now uses CHA absolute (`\x1b[N+1G`)
for column targeting instead of CUF relative (`\x1b[NC`), making the
diff stream immune to the cursor-drift class of bug Anthropic
documented in `claude-code#14208`.

**Features:**

- feat(slash): `aliases?: readonly string[]` on `SlashCommandSpec`.
  Adding a new alias is now a one-line edit to the canonical command
  — dispatch, autocomplete, arg-context resolution, and the
  dashboard `/api/slash` response all route through one
  `resolveSlashAlias()` map built from `SLASH_COMMANDS` at module
  init. Suggestion rows display aliases dimly (` · /quit /q`) so
  they stay discoverable without doubling the autocomplete list.
  Removes the per-handler alias mirrors that used to live in
  `handlers/basic.ts` and `handlers/language.ts`. (#332, PR #347)

**Bug fixes:**

- fix(renderer): switch the X-axis branch of `moveTo()` from CUF
  relative (`\x1b[NC`) to CHA absolute (`\x1b[N+1G`). Y-axis stays
  on CUU/CUD since we don't track absolute terminal rows. Relative
  column moves accumulate drift across frames whenever an earlier
  write miscounts cell width — `▸` (U+25B8) rendered 2-cell on
  fonts with East Asian fallback, ambiguous-width chars on
  terminals that font-detect width, OSC8 hyperlinks parsed as
  visible chars, etc. The next CUF lands at the wrong column,
  ghost rows leak into adjacent hint lines, and the modal "shifts"
  as users navigate. CHA targets the absolute column regardless of
  what the terminal thinks — immune to the desync chain. Same fix
  Anthropic shipped in claude-code per their issue #14208
  post-mortem. (#346, PR #348)

## [0.29.1] — 2026-05-06

**Headline:** four user-reported bugs from the 0.29.0 release window.
The markdown renderer no longer turns English abbreviations like
`e.g.` into broken hyperlinks (which on cmd.exe / non-OSC-8 terminals
showed up as visible `]8;;file://e.g…` garbage and on the cell-diff
side desynced the renderer's prev-frame model). The cell-diff
renderer now defensively trail-clears any row whose content shrank
between frames. Resumed sessions keep their cumulative session cost
instead of resetting to `$0`. The Approve plan modal now shows the
plan body inline when the model didn't supply structured steps.
Wide markdown tables fall back to row-grouped key/value lines
instead of the previous column-grouped output.

**Bug fixes:**

- fix(markdown): stop linkifying English abbreviations + drop OSC 8
  escape emission. The `FILE_REF_RE` extension class was too loose
  (`{1,6}`), so `e.g`, `i.e`, `a.m` matched as file paths; `osc8()`
  baked OSC 8 escape bytes into Text content, which the cell-diff
  renderer's wrapLine stripped of zero-width chars but kept the
  printable body — producing visible `]8;;file://e.ge.g]8;;` garbage
  on every terminal. Tightened the regex (now requires path-shape,
  line-number suffix, or extension >= 2 chars) and removed the OSC 8
  escape — file refs still stand out via color + underline. (#330,
  PR #341)
- fix(renderer): trail-clear rows that shrank between frames in the
  cell-diff diff. The diff skipped cells where prev and next were
  byte-equal (including trailing EMPTY cells), so any earlier ANSI
  desync left stale chars in shrunken rows — manifested as the
  shell-confirm modal showing `allow always` + `mand, ask again next
  time` after Up/Down navigation. New `clearToEOL` patch type and a
  per-row sweep after `diffEach`. (#330, PR #341)
- fix(stats): carry session cost / turn count across resume. The
  TUI's `$X session` figure reset to `$0` on every resume even
  though the disk meta still held the cumulative `totalCostUsd`.
  `SessionStats` gains `seedCarryover()`; `CacheFirstLoop` reads the
  meta on resume and seeds the carryover when prior messages exist.
  (#333, PR #342)
- fix(plan): show the plan body in the Approve plan modal. When the
  model called `submit_plan` with a markdown body but no structured
  `steps`, the modal showed only the choice list — users had no way
  to see what they were approving without scrolling back. The modal
  now renders the body via `MarkdownView`, capped at 24 lines with
  an overflow hint. (#336, PR #343)
- fix(markdown): row-group the table fallback layout. When a table
  was too wide for the viewport, the fallback flattened it as N
  "Component:" lines, then N "What:" lines, then N "Manual TCs:"
  lines — the reader couldn't tell which value belonged to which
  row. Swapped to row-first iteration with a blank separator
  between rows. (#340, PR #344)

## [0.29.0] — 2026-05-06

**Headline:** tool dispatch is no longer strictly serial. When the model
emits multiple `parallelSafe`-annotated tool calls in one turn (multiple
`read_file`, multiple `spawn_subagent`, etc.), the loop now races them
together via `Promise.allSettled`; a non-`parallelSafe` call ends the
chunk and runs alone, so read-after-write ordering still holds. Tool
yields and history append still land in declared order regardless of
which call settles first — the model and UI see the same shape they
would under serial dispatch. The TUI's `SubagentRow` becomes
`SubagentLiveStack`, rendering 1 → rich card, 2..max → compact rows,
> max → "+N more running…" fold. Closes umbrella #325.

**Tool dispatch:**

- feat(tools): `ToolDefinition.parallelSafe?: boolean` — opt-in
  annotation, default `false`. `ToolRegistry.isParallelSafe(name)` for
  the dispatcher to query; unknown / unannotated tools resolve to
  `false` so third-party MCP tools must explicitly opt in. Built-in
  read-only filesystem (`read_file`, `list_directory`,
  `directory_tree`, `search_files`, `search_content`,
  `get_file_info`), web (`web_search`, `web_fetch`), `recall_memory`,
  `semantic_search`, isolated child loops (`run_skill`,
  `spawn_subagent`), and in-memory job queries (`job_output`,
  `list_jobs`) are annotated. Mutating tools stay default. (PR #326)
- feat(loop): chunked parallel tool dispatch. Replaces `for...of +
  await` in the dispatch loop with a chunking loop that groups
  consecutive `parallelSafe` calls and races them; unsafe calls form
  serial barriers. `runOneToolCall` extracts per-call lifecycle
  (PreToolUse + dispatch + PostToolUse) so the chunk can fan out via
  `Promise.allSettled` while the loop body keeps yielding events in
  declared order. Two new env knobs: `REASONIX_PARALLEL_MAX` (chunk
  size cap, default 3, hard max 16) and `REASONIX_TOOL_DISPATCH=serial`
  (escape hatch). Tests cover parallel timing, serial barrier on mixed
  safe/unsafe, declared-order yields under racey completion, and both
  env-knob overrides. (PR #327)

**TUI:**

- feat(ui): `SubagentEvent` carries a stable `runId` per spawn so the
  sink can key concurrent runs apart instead of overwriting one shared
  row. `useSubagent` keeps an array of in-flight activities;
  `SubagentLiveStack` renders 1 → rich card (unchanged), 2..max →
  compact rows with per-row spinner + iter + last tool, > max →
  compact rows + "+N more running…" fold. (PR #327)

**Docs:**

- docs(architecture): `docs/ARCHITECTURE.md` Pillar 1 gains a
  "Parallel tool dispatch" section explaining the chunking rule, both
  env knobs, and the list of built-in tools that opt in. (PR #328)

## [0.28.0] — 2026-05-06

**Headline:** subagent capability sharpened on three axes — skills can
now scope a child to a specific tool subset via `allowed-tools`
frontmatter, callers can request a per-spawn iter budget via the new
`max_iters` arg (clamped 1-32), and two built-in personas (`explore`,
`verify`) are selectable inline via a `type` arg without writing a
skill. Closes umbrella #316.

**Subagent:**

- feat(subagent): honor skill `allowed-tools` frontmatter when forking
  the child registry. The field was parsed but ignored ("Unused in v1");
  now it scopes the subagent to the named tools only. New
  `forkRegistryWithAllowList` helper alongside `forkRegistryExcluding`;
  `NEVER_INHERITED` (`spawn_subagent` / `submit_plan`) still wins so
  depth=1 + plan-mode guarantees hold even if a skill names them. An
  allow-list naming a tool the parent doesn't have returns a structured
  error result (no API call burned). (#317, PR #320)
- feat(subagent): expose `max_iters` on the `spawn_subagent` tool
  schema. Clamped to 1-32 at the boundary; floats round down; non-numeric
  / missing falls back to the registration-time default (still 16).
  Verify-style tasks can ask for 6-8, explore-style can ask for 24+.
  (#318, PR #321)
- feat(subagent): two built-in personas selectable via `type` arg —
  `explore` (wide-net read-only investigation, 20-iter budget) and
  `verify` (narrow yes/no with evidence, 8-iter budget). Caller's
  explicit `system` / `max_iters` override the type's defaults. Prompts
  live in new `src/tools/subagent-types.ts` so `subagent.ts` stays
  under the 500-line target. (#319, PR #322)

## [0.27.3] — 2026-05-06

**Headline:** USD-account users now see `$` instead of `¥` everywhere
money is shown in the TUI — wallet balance, turn cost, session cost,
top-bar cost label, subagent end-event cost suffix, and the UsageCard
header / body / wallet line. Pre-fix a USD wallet rendered
`¥0.0352 turn · ¥0.461 session · wallet ¥0.91`; now it renders
`$0.0308 turn · $0.064 session · wallet $0.91`. The display follows
the wallet currency reported by the DeepSeek API (`currency: "USD"|"CNY"`),
not the UI language — a CNY account on an English UI still sees `¥`,
and vice versa. Originally reported in #278 by @Explosion-Scratch.

**UI / currency:**

- fix(ui): USD wallets render `$` for wallet balance, turn cost, and
  session cost. State + event schemas now carry `balanceCurrency`
  through `App.tsx → reducer → StatusBar` so every render site sees
  the wallet symbol the API reported. Originally drafted by @wviana
  in #272; the TUI plumbing through state.ts / cards.ts / events.ts /
  reducer.ts / useScrollback.ts / slash/types.ts was the bulk of the
  fix.
- fix(ui): balance color threshold checks USD against the CNY scale
  (`$0.91 ≈ ¥6.55`) rather than treating `0.91` as `< ¥5 → red`. USD
  wallets now correctly show yellow at low-but-not-empty balances.
- fix(ui): `StatsPanel.ChromeRow` cost label and `useSubagent`
  end-event cost suffix follow the wallet currency too — pre-fix
  these always rendered `$`. (#313)
- refactor(ui): seven currency helpers in `theme/tokens.ts`
  (`formatCNY` / `formatBalance` / `formatBalanceLabel` /
  `formatWalletDisplay` / `formatCost` / `balanceColorCny` /
  `balanceColorForBalance`) collapsed to three: `formatBalance`,
  `formatCost`, `balanceColor`. Undefined currency defaults to CNY
  (matches pre-fix unconditional `¥`) so the transient first-turn
  case where balance arrived but currency hasn't is consistent.
- chore(ui): remove orphan `ChromeBar.tsx` (258 lines). `App.tsx`
  mounts `StatsPanel`'s diverged `ChromeRow`, which is the bar users
  actually see. The two formatter helpers ChromeBar once owned now
  live in `theme/tokens.ts`. (#314)

**Loop:**

- refactor(loop): `loop.ts` 1331 → 1219 (−112). Three sibling files
  under `src/loop/`: `messages.ts` (pure ChatMessage builders),
  `turn-failure-tracker.ts` (per-turn failure count + threshold
  tipping), `force-summary.ts` (forced-summary generator behind a
  small DI context). Continues the #308 / #309 cadence — small
  per-helper extractions, no behavior change. (#311)

**Known follow-up:** `SessionPicker` still hardcodes `¥` for
per-session cost in the session-history list, tracked in #312
(good-first-issue).

## [0.25.1] — 2026-05-05

**Headline:** `run_command` learns the four common shell chain
operators (`|`, `||`, `&&`, `;`) and the seven file redirect
operators (`>`, `>>`, `<`, `2>`, `2>>`, `2>&1`, `&>`). Parsed and
spawned natively — no shell is invoked, so semantics are identical
on Windows / macOS / Linux; PowerShell 5.1's `&&` parse error and
the object-vs-bytes pipe gap are sidestepped. Each chain segment is
allowlist-checked independently, so `git status | grep main` now
auto-runs when both halves are individually allowed. Driven by
discussion #231.

**Shell:**

- feat(shell): support `|`, `||`, `&&`, `;` chain operators in
  `run_command` via split-and-spawn. The chain is segmented at
  whitespace-bounded operators (preserves embedded `&` / `|` inside
  arg values like `--flag=1&2`), each segment runs through the
  existing lenient tokenizer, and segments are executed with proper
  short-circuit semantics for `&&` / `||`. Each segment hits the
  allowlist independently — `git status | grep main` runs when both
  halves are allowed individually. (#233, #234)
- feat(shell): support file redirects in `run_command` — `>` (truncate),
  `>>` (append), `<` (stdin from file), `2>` (stderr truncate), `2>>`
  (stderr append), `2>&1` (merge stderr into wherever stdout points),
  `&>` (both → file). Targets resolve relative to the project root.
  Mid-pipe `2>&1` correctly merges stderr into the next segment's
  stdin without truncating on stdout-end. (#235)
- fix(shell): chain parser stays consistent with the project's
  long-standing lenient tokenizer — `cargo run -- --flag=1&2` and
  similar embedded-operator args stay literal instead of getting
  POSIX-strict-rejected. shell-quote dependency dropped;
  `splitOnChainOps` is whitespace-bounded like the existing
  `detectShellOperator`. (#234)

## [0.24.1] — 2026-05-04

**Headline:** Two TUI fixes on top of the 0.24.0 cell-diff renderer.
Frame writes are now wrapped in DEC 2026 synchronized-output markers so
supporting terminals can't paint a half-cleared intermediate state, and
`marked` is bumped to v15 to stop pre-escaping inline text into HTML
entities — which both displayed wrong and miscalculated wrap widths.

**Renderer:**

- fix(renderer): wrap commit writes in DEC 2026 sync to suppress
  flicker. The commit / static / resize paths buffered bytes into a
  single write but the terminal could still paint the cleared-then-
  repainted intermediate state. Each frame now goes out wrapped in
  `\x1b[?2026h…l`; supporting terminals (Windows Terminal ≥1.18,
  iTerm2, Kitty, Wezterm, alacritty, foot) swap frames atomically,
  others ignore the unknown CSI. Resize's screen clear is also folded
  into the next commit so clear+repaint is one sync block. Closes #225.

**Markdown:**

- fix(deps): bump `marked` to v15 — v12 pre-escaped inline text to HTML
  entities (`<` → `&lt;`, `"` → `&quot;`), which displayed wrong in the
  TUI and miscalculated cell widths so content past the wrap edge could
  be clipped. v15 keeps `token.text` literal and only escapes at the
  HTML renderer layer, which matches our actual rendering path.

## [0.23.1] — 2026-05-02

**Headline:** Two follow-up fixes to 0.23.0 — the `ReasoningCard` and
`StreamingCard` get a card-aligned redesign so they share the
`CardBox` + `Pill` primitives the rest of the run cards already use,
and the repair-storm detector now grants the loop one self-correction
attempt on the first storm before bailing the turn.

**TUI:**

- fix(tui): redesign reasoning + streaming cards. Both cards now sit
  inside the shared `CardBox` with a tier-aware accent and a `Pill`
  header, replacing the ad-hoc layout that didn't line up with
  `ToolCard` / done-assistant rendering. New `primitives/CardBox.tsx`
  and `primitives/Pill.tsx` are reused by the broader card family.
  Closes #133. (#136)

**Loop:**

- fix(loop): repair-storm detector now self-corrects once before
  stopping. A single short repeat-loop sequence (e.g. one retry of
  the same tool call) used to abort an otherwise recoverable turn;
  the loop now gets one self-correction attempt and only bails on
  the second storm. (#134)

## [0.23.0] — 2026-05-02

**Headline:** TUI quality-of-life pass driven by RFC discussion #20.
A read-only **context sidebar** on the right surfaces the active plan
+ running tools (`Ctrl+\` toggle, plan-only auto-show), assistant
replies get a left **accent bar** so long answers are scannable in
scrollback, the viewport gains a single **row-budget allocator** that
ends the jitter when an approval modal mounts mid-stream, the prompt
input grows a full **readline vocabulary** (`Home` / `End` / `Ctrl+K`
/ `Alt+B/F` / `Alt+Backspace`), and the `@`-picker honors **nested
`.gitignore`** instead of dropping files past a 500-result cap on
Flutter / iOS projects.

**TUI:**

- feat(tui): right-side context panel showing the active plan
  (windowed ±5 around the running step) and any running tool /
  subagent. Auto-shows when a plan starts running, hides on cancel
  via a new `plan.drop` reducer action; manual `Ctrl+\` toggle
  persists in `~/.reasonix/config.json.sidebarOpen`. Refuses below 88
  cols total; sidebar divider uses `borderTop` so the line auto-fills
  the panel width. (#127)
- feat(cards): done assistant Markdown gets a brand-toned `borderLeft`
  accent. Picked over `backgroundColor` because Ink's `<Box>` doesn't
  accept it — a left bar works on light + dark themes equally per
  lamyc's RFC #20 callout. (#126)
- fix(tui): `StreamingCard`, `EditConfirm`, `ShellConfirm`,
  `PlanCheckpointConfirm`, `PlanConfirm`, `ChoiceConfirm`,
  `PromptInput` now declare their height to a single
  `ViewportBudgetProvider` instead of each reading `stdout.rows` and
  guessing. Modal-vs-streaming row race that produced visible
  vertical jitter mid-turn (lamyc's video) is gone. Pure allocator in
  `src/cli/ui/layout/viewport-budget.tsx` is priority-greedy
  (`modal > plan-card > status > input > stream`). (#124)
- feat(prompt): full readline shortcut set wired into the prompt
  input — `Home` / `End` (line jumps, joins existing `Ctrl+A` /
  `Ctrl+E`), `Ctrl+K` (kill to end of line), `Alt+B` / `Alt+F` (word
  back / forward), `Alt+Backspace` (alias for the existing `Ctrl+W`).
  `Ctrl+U` keeps Reasonix's "clear whole buffer" behaviour, not
  readline's "kill to start" — clearing a large paste needs a single
  ergonomic key. (#123)

**Bug fixes:**

- fix(at-mention): @-picker walker now honors **nested** `.gitignore`
  (root + every subdirectory, layered like git itself) and bumps the
  default result cap from 500 → 2000. On Flutter / iOS projects with
  a built `ios/Pods/` directory the alphabetical walk used to burn
  the cap before reaching `lib/` and every `@` query returned "no
  files match". The new `src/gitignore.ts` util is shared with the
  semantic chunker — single source of truth for "walk a dir
  respecting `.gitignore`". Supports negation (`!keep.log`) and
  `respectGitignore: false` opt-out. (#129)

**Internal:**

- test: focused unit coverage for `resolvePreset` /
  `canonicalPresetName` + invariant check that every preset keeps
  `harvest: false` and `branch: 1` (the rule that branch and harvest
  are never silently auto-enabled). (#125)

## [0.22.0] — 2026-05-02

**Headline:** Live MCP-server reconnect — `/mcp reconnect <name>` (and the
`r` keybind in the `/mcp` browser modal) tear down a stuck client, hand-
shake a fresh one, and accept either identity or append-drift mid-session
without breaking the prompt prefix cache. The `d` keybind in the same
modal persists `mcpDisabled` for the selected server.

The reconnect work was driven by an empirical DeepSeek cache spike
(`benchmarks/spike-mcp-reconnect/`) that overturned the original RFC's
"any drift = full miss" framing — the cache is chunk-keyed, so an
appended tool costs only the new chunks (~95% hit retained). The full
graduated-permissive design lives in #110.

**MCP UX:**

- feat(mcp): new `/mcp reconnect <name>` slash subcommand. Re-handshakes
  the named server's transport and swaps the underlying `McpClient`
  through a new `McpClientHost` indirection so existing tool closures
  keep working without re-bridging. Identity-drift is always accepted;
  append-drift (server added new tools at the end of its tool list) is
  accepted mid-session via `applyMcpAppend`, which calls
  `prefix.addTool` + `registry.register` for each new tool. Edit /
  reorder / remove drift is refused with a clear "restart Reasonix to
  apply" message — those are catastrophic for the cache and would need
  new `ImmutablePrefix` API surface (`replaceTool` / `removeTool`).
  (#115, #117)
- feat(mcp): activate `r` (reconnect) and `d` (disable) keybinds in the
  `/mcp` browser modal. Both surfaces now route through one shared
  helper (`kickOffMcpReconnect` / `toggleMcpDisabled`) so the slash
  command and the modal stay byte-identical in behaviour. (#116, #118)
- feat(mcp): new `reconnect` lifecycle state added to the formatter —
  `⌘ MCP · <name>          ↻ reconnect…   tearing down · re-handshake
  · listing tools` per design §37.

**Internal architecture:**

- `src/mcp/registry.ts` — extracted `registerSingleMcpTool(mcpTool, env)`
  + new `BridgeEnv` type. `bridgeMcpTools` now exposes a `host`
  parameter (mutable client holder) and returns the resolved env so
  reconnect can register newly-added tools with the same options. (#115)
- `src/mcp/reconnect.ts` (new) — opens a fresh transport, classifies
  drift via `classifyToolListDrift`, swaps `host.client` only on
  accepted drift kinds, closes the new client cleanly on refusal so
  the old one stays untouched.
- `src/mcp/drift.ts` (new) — `classifyToolListDrift(before, after)`
  returns `{ kind, added, removed, edited }` over the five drift
  taxonomy buckets (identity / append / edit / reorder / remove).
  Pure function. (#114)
- `McpServerSummary.client?: McpClient` replaced by `host:
  McpClientHost` + `bridgeEnv: BridgeEnv`. Internal-only (the type
  isn't in the public package surface).

**Tests / spikes:**

- `tests/mcp-reconnect-prefix-invariant.test.ts` (new) — six structural
  cases pinning `ImmutablePrefix.fingerprint` behaviour under every
  drift the reconnect path can produce. Locks the bytes-equal claim
  the design rests on. (#112)
- `benchmarks/spike-mcp-reconnect/` (new) — live `deepseek-chat` spike
  + captured results: confirms DeepSeek's cache is chunk-keyed (~128
  tokens), so appended-tool drift retains 94.8% hit and a
  description edit on the first tool retains 84.1% hit. Drives the
  graduated-permissive policy. (#113)
- `tests/mcp-drift.test.ts`, `tests/mcp-reconnect.test.ts`,
  `tests/mcp-append.test.ts` (new) — unit coverage for the
  classifier, reconnect early-returns, and the append handler.

**Deferred (filed as catastrophic-cache-cost cases):**

- Edit-drift mid-session (needs `ImmutablePrefix.replaceTool`)
- Reorder-drift mid-session (needs `removeTool` + cache-reset card)
- Remove-drift mid-session (same)
- `--strict` flag to refuse even append-drift

Each is structurally a guaranteed cache miss and refused-with-restart
is the right default; the follow-up issues will land if real demand
surfaces.

## [0.21.0] — 2026-05-02

**Headline:** MCP CLI surfaces realigned with `docs/design/agent-tui-terminal.html`
sections 24, 32, and 37. Lifecycle messages get the documented vocabulary
(`↻ handshake…` / `✓ connected` / `✖ failed` / `○ disabled`), `/mcp` opens
an interactive browser modal instead of dumping text to scrollback, named
servers can be skipped on launch via `/mcp disable <name>`, and a per-server
p95 latency tracker emits a one-line warn toast when a server consistently
goes slow.

**MCP UX:**

- feat(mcp): lifecycle line cards now match design §37 byte-for-byte —
  `⌘ MCP · <name>          ✓ connected    12 tools · 8 resources · 142ms`
  on bridge success, `↻ handshake…` before initialise, `✖ failed` with
  reason in the catch path. New `src/cli/ui/mcp-lifecycle.ts` is the
  single formatter shared by `chat` and `run`. (#106)
- feat(mcp): `/mcp` opens a keyboard-driven browser modal per design §24,
  showing server name + health badge + tool / resource / prompt counts +
  capability list under the active row. `/mcp text` keeps the printed-card
  form for non-TTY / replay contexts. (#107)
- feat(mcp): `/mcp disable <name>` and `/mcp enable <name>` slash
  subcommands persist a `mcpDisabled` list to `~/.reasonix/config.json`.
  Disabled named servers are skipped on the next launch and surface as
  `⌘ MCP · <name>          ○ disabled     via /mcp disable <name>` in
  startup output. Anonymous servers (no `name=`) aren't toggleable, by
  design. (#108)
- feat(mcp): per-server p95 latency tracker fires a one-line warn toast
  once when p95 over the last five calls crosses `mcpSlowThresholdMs`
  (default 4000) — `⚠ MCP \`<name>\` slow · 8.4s p95 over the last 5
  calls`. Idempotent: re-fires only after p95 dips below and crosses
  back. New `src/mcp/latency.ts` + `src/cli/ui/mcp-toast.ts`. (#109)

**Deferred:**

- `/mcp reconnect <name>` (live tool-list teardown) split out as RFC #110.
  The naïve implementation breaks the byte-stable prompt prefix when the
  reconnected server's tool surface drifts; needs a design call between
  refuse-on-drift / permissive-with-warn / `--force` flag before code.
  The `r` keybind in the `/mcp` browser is a labelled stub waiting for
  this RFC.

## [0.20.0] — 2026-05-02

**Headline:** Drops Node 20 support (EOL'd 2026-04-30). The README has been
overhauled with hero-terminal / hero-stats / feature-grid SVGs that match
the design-doc palette, plus contributor-avatar grid, Code of Conduct, and
SECURITY policy.

**Breaking:**

- `engines.node` bumped from `>=20.10` to `>=22`. Node 20 reached
  end-of-life on 2026-04-30; `npm install reasonix` on Node 20 will now
  print an `EBADENGINE` warning. Tested CI surface trimmed to a single
  Node 22 job. (#98)

**Fixes:**

- fix(code): `reasonix code` now bridges MCP servers from
  `~/.reasonix/config.json`, matching `reasonix chat` behaviour.
  Previously any servers defined in config were silently skipped in
  code-mode sessions. (#91)
- fix(mcp): `NAME_PREFIX` regex in `parseMcpSpec` accepts hyphens, so
  kebab-case server names like `sage-wiki=npx -y @scope/sage-wiki`
  parse correctly. Previously the entire string was treated as a raw
  command path. Regression test in `tests/mcp-spec.test.ts`. (#96)

**Docs / project hygiene:**

- docs(readme): introduce three new SVG assets that anchor the README's
  visual rhythm to the design-doc palette — `hero-terminal.svg`
  (faithful to `formatPendingPreview` unified-diff output),
  `hero-stats.svg` (94% / ~30× / MIT), and `feature-grid.svg` (six-card
  3×2 grid). Bilingual `*.zh-CN.svg` siblings ship for the zh README.
  All SVGs live under `docs/assets/`. (#102)
- docs(readme): designer pass — drop redundant `# Reasonix` H1 (the
  logo wordmark says it), drop the duplicated tagline, center the
  badges + description under one column, trim the comparison table
  to differentiating rows only, drop the `--system-append` doc
  subsection (lives in `--help`). (#102)
- docs: design mockups (`agent-dashboard.html`, `agent-tui-terminal.html`)
  moved into `docs/design/` so README links resolve to the rendered
  GitHub Pages page instead of HTML source view. (#102)
- docs(readme): replace the hardcoded `good-first-issue` ticket list
  with a single label-filter link — auto-fresh as tickets close. (#99)
- docs(readme): drop "DeepSeek free credit on signup" claim from
  README, website, TUI Setup / Wizard prompts — perk no longer
  offered. (#102)
- docs(readme): add `contrib.rocks` contributor-avatar grid; add
  GitHub stars + Discussions badges. (#102)
- docs: add `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1) and
  `SECURITY.md` (private-disclosure policy with explicit scope). (#102)

## [0.17.1] — 2026-04-29

**Headline:** Fix a render crash in the dashboard's Editor that triggered
when toggling Edit / Split / Preview on a markdown file. Mixing the
CodeMirror-managed DOM with sibling `dangerouslySetInnerHTML` while the
host element changed shape across modes confused Preact's reconciler
(`Failed to execute 'insertBefore' on 'Node'`).

- fix(dashboard): Editor mode toggle no longer restructures the DOM.
  CM container and markdown preview are now always rendered at the same
  vnode positions; `data-mode` on a single `.editor-stage` wrapper
  drives visibility via CSS. CM stays mounted across mode switches and
  is poked with `requestMeasure()` when it becomes visible again.

## [0.17.0] — 2026-04-29

**Headline:** `reasonix index` is now config-driven — what gets walked
is defined entirely by `~/.reasonix/config.json` (with sensible
defaults), `.gitignore` is honoured by default, and the dashboard
Semantic tab gains a Settings card to view, edit, and dry-walk-preview
the rules without leaving the browser. The previous behaviour
hardcoded skip lists in `chunker.ts` and duplicated them in
`directory_tree`; both now read from a single shared source.

- feat(index): new `index` block in `ReasonixConfig` (`excludeDirs`,
  `excludeFiles`, `excludeExts`, `excludePatterns`, `respectGitignore`,
  `maxFileBytes`). Any field present fully replaces its default; absent
  fields keep the default.
- feat(index): nested `.gitignore` honoured by default — each
  subdirectory's rules apply scoped to that subdir, so `pkg-a/.gitignore`
  doesn't leak into `pkg-b/`.
- feat(index): glob excludes via `picomatch` syntax in
  `excludePatterns` (e.g. `**/*.gen.ts`, `vendor/**`, with `!negation`
  supported).
- feat(cli): `reasonix index` success line now prints a per-reason
  skip breakdown (`gitignore: A · pattern: B · defaultDir: C · …`) so
  users see what was filtered and why.
- feat(dashboard): Semantic tab gains a collapsible **Excludes** card
  with editable lists, gitignore toggle, max-file-size input, **Save**
  / **Reset** / **Preview** buttons, and a per-reason sample drilldown
  in the Preview panel.
- feat(server): `GET /api/index-config` returns user/resolved/defaults;
  `POST /api/index-config` persists; `POST /api/index-config/preview`
  dry-walks the project root with a draft config and returns sample
  paths + skip buckets.
- refactor(tools): `directory_tree` now reuses
  `DEFAULT_INDEX_EXCLUDES` from `src/index/config.ts` instead of its
  own copy of the dir/binary lists; the two were already drifting.
- deps: `picomatch ^4`, `ignore ^7`, `@types/picomatch ^4`.

## [0.16.1] — 2026-04-29

**Headline:** Fix a tool-loop regression on `deepseek-chat` introduced
by DeepSeek's V4 rollout. The model now returns non-empty
`reasoning_content` even with `extra_body.thinking.type = "disabled"`,
and the API rejects round-trips that drop the field
("reasoning_content in the thinking mode must be passed back to the
API"). Reasonix's whitelist-by-model in `assistantMessage()` was too
narrow — it stamped reasoning_content only for `deepseek-reasoner` /
`deepseek-v4-flash` / `deepseek-v4-pro`. Caught by re-running τ-bench
on v0.16.0: 24/24 reasonix runs were failing.

- fix(loop): `assistantMessage()` now preserves `reasoning_content`
  whenever the producer emitted non-empty content, regardless of the
  model name. The whitelist still applies to synthetic messages
  (empty stamp for thinking-mode endpoints) so non-thinking sessions
  stay clean.
- test(loop): regression case in `loop-r1-reasoning.test.ts` —
  deepseek-chat returning non-empty `reasoning_content` round-trips
  the field on the next request.
- bench(tau): full re-run on the fix — 100% pass · 90.2% cache hit
  (vs 32.8% baseline) · $0.000593 / task. Mean cost is ~62% lower
  than the 0.2.1 snapshot, mostly from DeepSeek's price moves.

## [0.16.0] — 2026-04-29

**Headline:** Mouse drag in the log now selects text directly, with the
log auto-scrolling when the drag hits the viewport edge. Releasing the
button copies the selection to the system clipboard via OSC 52 plus a
tempfile fallback for terminals that don't honor it. The whole flow
stays inside the alt-screen TUI — no more `/copy` dance to dump the
log to main buffer.

- feat(ui): app-owned mouse selection. Plain drag paints a reverse-
  video highlight across the selected rows; the selection follows
  scroll naturally because rows are tracked in absolute log-row
  coordinates, not viewport-relative. Dragging past the top or bottom
  edge of the content area starts a 60ms-tick auto-scroll that keeps
  extending the selection while the cursor stays at the edge.
  Releasing copies the plain-text rendering via OSC 52 (system
  clipboard) plus a `<tmpdir>/reasonix-clip-<ts>.txt` fallback for
  terminals or remote sessions that drop OSC 52. Shift+drag still
  bypasses tracking so the terminal's native selection remains
  available for visible-only copies.
- feat(infra): `stdin-reader` now surfaces `mouseDrag` (SGR button 32)
  and `mouseRelease` (tail `m`) events; previously dropped silently.
  `alt-screen` switches from mode 1000 (press/release only) to mode
  1002 (button-event tracking with drag motion).
- feat(ui): `log-frame` extends `AtomViewport` with `firstRowAbs` so
  the keystroke layer can map mouse coordinates back to absolute log
  rows. New `extractSelection(atoms, sel)` walks the cell grid and
  produces UTF-8 text honoring 2-wide chars (CJK / emoji) with ANSI
  styling stripped.
- chore(ui): `/copy` slash command, the `copyMode` lifecycle, the
  alt-screen exit + main-buffer dump, and the `setMouseTracking` /
  `isMouseTrackingOn` helpers all removed. The new flow doesn't need
  to leave alt-screen, doesn't pollute main scrollback, and doesn't
  have the "two histories stacked" bug the dump approach kept hitting.

## [0.15.0] — 2026-04-29

**Headline:** Event-log sidecar lands as a real kernel artifact and
gets its first consumer — `replay()` reads `events.jsonl` and runs
the same pure reducers `apply()` does in-process. First external
PR merged: deny-with-context, pressing Tab on a tool-confirm modal
lets the user type *why* they're refusing, forwarded to the model
verbatim. Comment policy now enforced by `tests/comment-policy.test.ts`
under `npm run verify`; companion sweep dropped 6.3k LoC of
module-essay docstrings, banner separators, and incident-history
narrative across 148 source files.

- feat(core): `events.jsonl` sidecar — every kernel `Event` is
  appended to `<session>.events.jsonl` next to the legacy
  `LoopEvent` log. Append-only, durable, no behavior change for
  in-process consumers. Unblocks the v0.14 architecture migration:
  any view (CLI, dashboard, replay) can now reconstruct state from
  the sidecar without the loop running.
- feat(core): `replay()` reads the sidecar and runs the same pure
  reducers as in-process `apply()`. First proof that the projection
  layer is genuinely deterministic — `replay(events)` matches
  `apply(...)` for the conversation / budget / plan / workspace /
  capabilities / status / session-meta views.
- feat(cli): `reasonix events <name>` — inspect any session's event
  stream from the command line. Filters by event variant
  (`reasonix events ToolCallStarted`), tail mode, JSON output for
  piping into `jq`. Plus a kernel sweep removing the dead-comment
  layer that accumulated during the LoopEvent → Event transition.
- feat(ui): deny-with-context (PR #1, by @wviana). On any tool-confirm
  modal (`ShellConfirm`, `WorkspaceConfirm`, edit review), pressing
  Tab on the Deny option opens inline editing — type a reason, Enter
  submits. The reason is appended to the synthetic `I denied
  running …` message so the model knows *why* and can adjust course
  instead of plowing ahead. Edit-review path uses a dedicated
  `DenyContextInput` modal (n hotkey opens the reason input, Esc
  returns to the diff). Bracketed-paste support in the inline editor
  so multi-line context can be pasted in one go.
- chore(ui): removed obsolete `/mouse` slash command and the
  misleading "drag to select & copy" prompt hint — both predated
  `/copy` and gave wrong guidance now that the proper flow is
  alt-screen-exit + scrollback dump.
- chore(comments): `tests/comment-policy.test.ts` pins six rules
  derived from `CLAUDE.md`: ≤2-line module headers, no Phase-N
  narrative, no version refs in comments, no incident history
  (`user reported`, `screenshot showed`, `fix for #N`), no banner
  separators (`// ─── helpers ───`), ≤3-line block comments. Runs
  under `npm run verify`, which is the pre-push gate. Companion
  sweep: 116 module-essay headers compressed to one line, 577
  over-long block comments distilled or deleted, 44 banner separators
  stripped. Net −6,367 LoC of dead-weight comments across 148 files;
  zero behavior change, full lint/typecheck/test green.

## [0.14.0] — 2026-04-29

**Headline:** Two real bug fixes (post-shell-confirm session lockup,
post-workspace-switch ENOENT on edit_file), a new `/copy` mode for
copying across multi-screen log content, an always-on context-pressure
footer above the prompt, and width-aware chrome that stops dropping
pills when there's clearly room. Plus a quiet refactor: shared UI
primitives, dead-code purge in StatsPanel.

- fix(loop): streaming-abort path now resets `_turnAbort` before
  returning. Without this, a queued-submit triggered by App.tsx
  (ShellConfirm "run once" → `loop.abort()` + `setQueuedSubmit`)
  produced a spurious `aborted at iter 0/64 — stopped without
  producing a summary` the moment the synthetic message reached
  the loop, locking the session until the user `/retry`'d.
- fix(tui): `edit_file` interceptor now reads the workspace root via
  `currentRootDirRef` instead of capturing `currentRootDir` in a
  stale closure. Workspace switch (`change_workspace` → modal approve)
  rebound `read_file` / `run_command` to the new root but left the
  interceptor pointing at the old one — `edit_file` wrote to the
  old path while `read_file` looked in the new one, surfacing as a
  mysterious ENOENT for a file the model had just successfully edited.
- feat(tui): `/copy` exits the alt-screen, dumps the rendered log to
  the main screen, and listens for any keystroke to restore. Native
  terminal scrollback + drag-select work on the dump — solves the
  "can't copy text that scrolled past the viewport" problem alt-screen
  introduced. Re-entering alt-screen and bumping React state forces
  Ink to redraw the TUI cleanly. Multiple enter/exit cycles per
  session; React tree, event log, model session, prompt draft all
  preserved across the toggle.
- feat(tui): always-on context-pressure footer above the prompt —
  `ctx ▰▰▰▱▱▱▱▱▱▱▱▱▱▱  14K/977K · 1%  ·  sys 5.8K  ·  tools 6.1K  ·  log 0`.
  Single-row layout matches the chrome bar's `▰▱` visual language.
  Width-aware shed for the breakdown segments (input → log → tools →
  sys). `/context` toggles visibility (default on); the rich
  4-color stacked breakdown is still pushed to scrollback for
  headless / replay surfaces that don't carry the toggle callback.
- feat(tui): chrome bar pill rendering switches from preemptive
  `narrow = cols < 120` to width-aware greedy shed. Optional pills
  (balance > cache > session > update) drop in priority order only
  when `string-width` math says they won't fit — at 100 cols all
  five render where the old code dropped three. Cache pill is now
  default-on (cold-start dim treatment instead of hiding).
- refactor(ui): `Bar`, `formatTokens`, `ChromeRule`, `ContextCell`
  promoted to `src/cli/ui/primitives.tsx` (were duplicated 2-3× across
  `StatsPanel` / `ChromeBar` / `EventLog` / `log-frame`). `CtxBreakdownBlock`
  + `computeCtxBreakdown` extracted to `src/cli/ui/ctx-breakdown.tsx`
  so `/context` and the footer share the same compute path. `StatsPanel`
  shrunk from 769 → ~280 lines (dead helpers from the chrome
  redesign era removed).
- feat(core): v0.14 architecture scaffold — `src/core/events.ts`
  (25-variant Event union + 7 view types), `src/core/reducers.ts`
  (pure projections + `apply` / `replay` combinators), `src/ports/*.ts`
  (6 ports: ModelClient, ToolHost, EventSink, MemoryStore, HookRunner,
  CheckpointStore). Types only; zero behavior change. 19 reducer tests
  pin the conversation / budget / plan / workspace / capabilities /
  status / session-meta projections and prove `replay()` determinism.

## [0.13.5] — 2026-04-29

**Headline:** TUI overhaul. Chrome reverts to native Ink Box +
flexGrow (Phase 6a's Frame-compiler chrome was clipping pills on
Windows Terminal / ConPTY). Vertical scrollbar replaced with a
`[↑ N%]` chrome pill + horizontal mini-bar in the bottom hint —
column-aligned scrollbars are unreliable while some log atoms
still render through legacy ReactElements. Streaming gains the
design's `responding ░▒▓█▓▒░░░░` marquee and a `▌` cursor blink
at end-of-body.

- chrome: `ChromeBar` uses native flex; preset pill (`[auto]` /
  `[flash]` / `[pro]`) replaces edit-mode pill (edit mode still
  surfaces via `ModeStatusBar`); CNY balance renders as `w ¥8.50`;
  cost pill includes inline budget when set.
- streaming: full body text streams in (was 140-char tail) with a
  blinking primary-color cursor; `responding` row shows a 12-cell
  marching wave (`░▒▓█▓▒`) at 120ms ticks. Matches
  `design/tui-redesign-ink.html`.
- scroll: vertical `ScrollBar` removed; chrome shows `[↑ N%]` when
  scrolled, `BottomHint` shows `↑ N · ▕──●──▏ X% · ↓ M · End`.
- frame: `src/frame/width.ts` delegates to the `string-width`
  package; hand-rolled width tables removed.
- chore: project `CLAUDE.md` codifies code/comment conventions
  (terse comments, no Phase-N essays, libraries over hand-rolled
  unicode math).

## [0.12.15] — 2026-04-28

**Headline:** Every user-facing string that still said
`fast / smart / max` is now `auto / flash / pro` — the canonical
names presets have used since the autoEscalate split. CLI flags
(`chat --preset`, `run --preset`), `/help`'s preset table,
`/preset`'s argHint and completer, the slash handler's `usage:`
line, and the `code` command description all updated.

Old `config.json` files keep working: `resolvePreset` still maps
`fast → flash·effort=high`, `smart → auto`, `max → pro`. What
changed is the interactive surface — `/preset fast` now prints
usage instead of silently doing the right thing, so the in-chat
vocabulary matches what's documented.

## [0.12.14] — 2026-04-28

**Headline:** Three TUI confirmations the dashboard couldn't see —
`change_workspace`, plan checkpoints, plan revisions — now mirror to
the web modal layer with the same Switch/Deny/Continue/Revise/Stop/
Accept/Reject choices the terminal exposes. Plus: a deferred-dispatch
fix for parallel tool calls that was silently writing files into the
old workspace, and the in-flight row finally tells you _what tool_ is
running, not just "waiting".

### Loop — workspace-switch parallel-batch fix

When DeepSeek emits `change_workspace + write_file` in one assistant
message, every call dispatched in sequence — write_file fired against
the OLD sandbox before the user had a chance to approve the modal,
silently dropping the new file in the wrong project. Every subsequent
call in the same batch now gets a synthetic "deferred — re-issue on
your next turn" result; tool_call ↔ tool pairing stays valid for
DeepSeek's next-turn validator. Test in `tests/loop.test.ts` locks it.

### Server / context

- `ActiveModal` gains three new shapes: `workspace`, `checkpoint`,
  `revision`. `getActiveModal` returns them so a freshly-connected
  client paints the right modal mid-prompt.
- `DashboardContext` adds `resolveWorkspaceConfirm`,
  `resolveCheckpointConfirm` (with optional `text` for revise-with-
  feedback in one shot), and `resolveReviseConfirm`.
- `/api/modal/resolve` accepts the three new `kind`s with their
  per-shape choice validation. 503 when a resolver isn't wired.

### App.tsx wiring

- `pendingWorkspace`, `pendingCheckpoint`, `pendingRevision` each
  broadcast `modal-up`/`modal-down` SSE events.
- Web's "revise + feedback in one shot" path bypasses the TUI's
  staged-input two-step by accepting an explicit snap override on
  `handleCheckpointReviseSubmit` — no more setStagedX → re-render →
  ref-mirror microtask race.

### Dashboard SPA

- New `WorkspaceModal`, `CheckpointModal`, `RevisionModal` Preact
  components. Modal switch dispatches them by `modal.kind`.
- In-flight row now shows the active tool + key args (path / command
  truncated to 80 chars / char count) once `tool_start` fires —
  `write_file → /path/to/foo (12,345 ch)` instead of "waiting…".
- Tool-start no longer pushes a placeholder info row. The InFlightRow
  carries the live state; the result card replaces it on `tool`.
- ErrorBoundary stops auto-recovering after 3 catches and renders a
  manual "Try again" button — no more silent flickering loop.
- `.modal-cmd` gets `overflow-x: auto` + `max-height: 240px` so a
  pathological multi-kilobyte command can't push the rest of the
  panel offscreen.

## [0.12.13] — 2026-04-28

**Fix:** the chat feed kept yanking the user back to the bottom
during streaming — wheel-up didn't stick. Two bugs stacked:

1. The scroll listener attached to `document.querySelector(".chat-feed")`
   on first mount, but the `.chat-feed` div was conditionally
   rendered (only when at least one message existed). On a fresh
   session the listener never attached, so the "is the user
   scrolled away?" flag was never flipped to `false`.
2. Even after the listener attached, the auto-scroll effect's
   own `el.scrollTop = el.scrollHeight` write fires a `scroll`
   event that re-snaps the flag back to `true`. Manual wheel
   scrolls were racing the next streaming delta's auto-snap.

Both fixed:

- `.chat-feed` is now always rendered (the empty-state copy
  moved inside it). A `feedRef` ref attaches the scroll
  listener on first paint.
- A new `autoScrollInFlight` ref gates the listener: events
  observed during a programmatic scroll write are ignored, so
  only genuine user wheel/drag flips the auto-scroll guard.

## [0.12.12] — 2026-04-28

**Headline:** Indexing from the dashboard now actually wires up
`semantic_search` for the running session — no more "build the
index, restart, build again" dance — and a dismissible Chat
banner steers users to the Semantic panel when no index exists.

### Loop / prefix

- `ImmutablePrefix` gains an `addTool(spec)` method that pushes a
  new tool spec onto the live prefix. The class name is now a
  half-truth (toolSpecs is exposed via getter, backed by a mutable
  array) but the rationale is documented inline: a one-time cache
  miss is cheaper than asking users to restart the session.
- New `DashboardContext.addToolToPrefix(spec)` callback. Wired
  from `App.tsx` to `loop.prefix.addTool`.

### Server

- `runIndex` (the dashboard's buildIndex wrapper) calls
  `registerSemanticSearchTool(ctx.tools, …)` after a successful
  build, then `ctx.addToolToPrefix(spec)` so the model sees
  `semantic_search` from the next turn. Failures are non-fatal —
  the index is still on disk, the next session bootstrap picks
  it up.
- `/api/overview` returns `semanticIndexExists` (`true`/`false`/
  `null`) so the Chat panel can render the banner without an
  extra round-trip.

### Dashboard — Chat panel

- New top-of-Chat banner: `≈ Semantic search isn't enabled for
  this project — Build it →` with a dismiss `×`. Visible only
  when `semanticIndexExists === false` and not previously
  dismissed (state in `localStorage` as `rx.semanticBannerDismissed`).
- Click "Build it →" fires `appBus.dispatchEvent("navigate-tab")`
  with `tabId: "semantic"` — the existing nav handler picks it up.

## [0.12.11] — 2026-04-28

**Headline:** Tell users what to do when Ollama isn't installed
yet. The 0.12.9 Semantic panel just said "not reachable" with a
generic copy-this-command blurb — the new flow distinguishes
"binary missing" from "daemon down" from "model not pulled" and
offers a one-click action for each level it can resolve.

### Server

- `GET /api/semantic` now returns the full `checkOllamaStatus`
  payload — `binaryFound`, `daemonRunning`, `modelPulled`,
  `modelName`, `installedModels` — instead of the raw probe.
- New endpoints:
  - `POST /api/semantic/ollama/start` — runs `startOllamaDaemon`
    (15s timeout). Returns `{ ready, pid }`.
  - `POST /api/semantic/ollama/pull` — fire-and-forget
    `pullOllamaModel`. Per-model `PULLS` map tracks status +
    last log line; `/api/semantic` includes it as `pull`.

### Dashboard — Semantic panel

Tri-state Ollama section:
- **No binary** → red "not installed" pill + Install Ollama
  card with macOS / Windows / Linux install instructions. We
  deliberately don't run package managers for the user.
- **Binary, daemon down** → yellow "daemon down" pill + "Start
  daemon" button (calls `ollama/start`).
- **Daemon up, model missing** → "not pulled" pill + "Pull
  <model>" button. Live status row during the pull (latest
  ollama output line, elapsed seconds, success/error pill).
- **Everything ready** → green pill, Index buttons enable.

Polling speeds up to 1.2s while a pull or build job is running.

## [0.12.10] — 2026-04-28

**Headline:** Move the in-flight indicator out of the top-left
corner and put the live counters next to it. Previously the
spinner appeared above the message stream — far from where the
user's eyes already were (input + status bar) — and the only
moving signal during a turn was the streaming text itself.

### Chat panel

- New **InFlightRow** rendered just above the ChatStatusBar
  whenever a turn is in flight. Format:
  `⠋ thinking · 2.3s · reasoning 1,204 ch · out 0 ch · [Abort]`
- Phase auto-flips between `thinking` (only reasoning growing),
  `streaming` (text growing), and `waiting` (neither — model is
  thinking with no token output yet, e.g. before the first
  delta arrives).
- Elapsed seconds tick every 500ms via a per-turn interval so
  the user sees motion even when the model is in a long pause
  between deltas.
- Character counts come from the existing `streaming` state — no
  new wire fields, just rendering data we already have.
- Top "turn in flight" row is gone; only `statusLine` notices
  still render up there when not busy.

## [0.12.9] — 2026-04-28

**Headline:** Semantic indexing without leaving the session.
Previously you had to exit the TUI, run `reasonix index`, wait,
then re-enter — every change. Now there's a Semantic panel in
the dashboard that drives `buildIndex` in the background and
shows live progress.

### Server

- `src/server/api/semantic.ts` — new endpoint set:
  - `GET  /api/semantic`        → Ollama probe + index existence
                                   + current job snapshot
  - `POST /api/semantic/start`  → kick off `buildIndex({ rebuild })`
                                   fire-and-forget, returns 202
  - `POST /api/semantic/stop`   → flag job as aborting (advisory;
                                   `buildIndex` doesn't honor a
                                   signal yet, lands when it does)
- Per-root `JobRecord` map (module-scoped) tracks phase
  (scan/embed/write/done/error) + counters (filesScanned,
  chunksTotal, chunksDone, …) updated via `onProgress`.

### Dashboard

- New **Semantic** sidebar tab. Polls `/api/semantic` every 1.2s
  while a job is running, every 5s when idle.
- Surfaces Ollama daemon reachability + listed models, current
  index existence, and the live job: phase pill, file/chunk
  counters, percentage progress bar, elapsed seconds, last
  result on completion, error text on failure.
- Buttons: **Index (incremental)**, **Rebuild (wipe + full)**,
  **Stop**. Disabled appropriately when Ollama isn't reachable
  or another job is running. Inline guidance on missing daemon.
- Standalone `reasonix dashboard` mode shows a polite "code-mode
  required" empty state — no project root, nothing to index.

## [0.12.8] — 2026-04-28

**Fix:** the dashboard row in 0.12.7 collapsed the URL and
description onto one Box; on terminals that hide the OSC 8
escape, Ink's text-width measurement counted the escape bytes
as visible characters and the description wrapped through the
middle of the URL. Split into two stacked rows:

```
◇ web   open the dashboard in a browser (chat · files · stats · settings)
        http://127.0.0.1:NNNN/?token=…
```

URL still wrapped in the OSC 8 hyperlink — but it's the only
content on its row, so a width miscount can't clobber anything.

## [0.12.7] — 2026-04-28

**Headline:** Dashboard discoverability. Most users had no idea
`/dashboard` existed — the URL is now visible from the first turn,
on its own row in the status panel, with a one-line description of
what the dashboard actually offers. Clickable in OSC-8-aware
terminals (iTerm2, WezTerm, Windows Terminal, VS Code, recent
gnome-terminal); copy-pasteable everywhere else.

### TUI

- Auto-launch the embedded dashboard when `reasonix code` /
  `reasonix chat` mount. Failures are silent (a missing dashboard
  never blocks the TUI), tear-down still happens on unmount /
  `/dashboard stop`.
- `--no-dashboard` opts out per-session (CI, hardened
  environments, anyone allergic to a localhost listener).
- New status-panel row:
  `◇ web   http://127.0.0.1:NNNN/?token=…   open the dashboard
  in a browser (chat · files · stats · settings)`
  rendered between the header and the metrics so it never fights
  for space.
- URL wrapped in an OSC 8 hyperlink — Cmd/Ctrl-click in any
  terminal that supports the escape; bare text otherwise.
- `App` gains a `noDashboard` prop, `StatsPanel` a `dashboardUrl`
  prop. Both threaded through `chatCommand` / `codeCommand`.

## [0.12.6] — 2026-04-28

**Headline:** Bigger fixes for the things you actually look at:
the edit-review modal is now a real side-by-side diff, the
sidebar collapses to icons, and the call-storm breaker stops
mistaking legitimate read → edit → verify cycles for storms.

### Edit review modal

- Two-column **side-by-side diff** ("before" left, "after" right)
  with hljs syntax highlighting per the file's language. Adjacent
  removed/added line runs pair into rows so the change reads
  cleanly across the gutter.
- Red tint + `−` marker on the removed side; green tint + `+` on
  the added side; context lines render unchanged.
- Modal payload (`{ kind: "edit-review" }`) gained `search` and
  `replace` fields holding the full block contents — the old
  truncated `preview` string stays alongside for older clients.
  `src/cli/ui/App.tsx` and `src/server/context.ts` updated.

### Sidebar — icon-only collapse

- New `◀ collapse` button at the bottom of the sidebar shrinks
  it from 220px → 52px and hides every label, leaving just the
  glyphs. `▶ expand` brings labels back. Choice persists in
  `localStorage` (`rx.sidebarCollapsed`).
- Tabs in the collapsed state center the glyph and keep the
  primary-color active indicator.

### Call-storm breaker — false-positive fix

The `read → edit → verify → edit → verify` pattern was tripping
the storm protection (3 identical `read_file` calls within the
window). The fix sources its "did this call mutate state?"
signal from the existing ToolRegistry — each tool already
declares `readOnly` / `readOnlyCheck` for plan-mode gating, so
no new flag was added. The breaker now:

- Tags every buffer entry as read-only or mutating based on the
  predicate the loop wires in (`def.readOnly === true`, with
  `readOnlyCheck` taking precedence on the actual args).
- On a mutating call, drops prior read-only entries from the
  window — a re-read after `edit_file` is fresh, not a repeat.
- Keeps mutator entries alongside, so a model looping on
  identical `edit_file` calls still trips on the threshold.

`StormBreaker(window, threshold, isMutating?)` is the public
shape; `ToolCallRepair` accepts an `isMutating` predicate.
Without one (older callers, isolated tests) every call counts —
back-compat preserved. Three new storm tests cover the cases.

## [0.12.5] — 2026-04-28

**Headline:** Stop loading CodeMirror from a CDN, fix the legacy
preset migration that broke 2 CI tests, and replace the markdown
preview toggle with a proper Edit / Split / Preview tri-state.

### Editor — local CodeMirror bundle

- `scripts/bundle-codemirror.mjs` — esbuild-based bundler that
  pulls every `@codemirror/*` package from `node_modules` and
  produces `dashboard/codemirror.js` (~937 KB minified ESM).
- `npm run build:cm` rebuilds it. Output is committed so a fresh
  `npm install` doesn't have to run esbuild.
- `dashboard/app.js` now does `import("/assets/codemirror.js")`
  instead of 21 `import("https://esm.sh/...")` calls. One copy of
  every package = no Tag identity issues, no transitive-version
  drift between cold loads.
- `serveAsset` learns to serve `codemirror.js`. `package.json`
  ships the bundle in `files`. Biome ignores the minified file.
- `@codemirror/*` + `esbuild` added to devDependencies — they
  feed the bundler, they don't end up in the runtime install.

### Editor — markdown view modes

- Replaced the `Preview`/`Edit` boolean with a three-state
  segmented control: **Edit** (source only, default), **Split**
  (source on the left, rendered on the right, with a divider),
  **Preview** (rendered only). Buttons live in the editor bar
  and are markdown-only — non-md tabs hide the group entirely.
- The CodeMirror remount effect now keys on `viewMode`, so
  flipping between Edit and Split doesn't leave a stale view.

### Preset rework — CI fix

`resolvePreset` was collapsing every legacy name (`fast`, `smart`,
`max`) to `auto`, which made two `tests/resolve.test.ts` cases
fail because they assert the legacy mapping that older config
files depend on. Restored the original semantics:
- `fast` → flash with `effort: high` (no auto-escalate)
- `smart` → auto (flash + max + auto-escalate)
- `max` → pro
Anything else still collapses to auto. Suite back to 1568 / 1568.

## [0.12.4] — 2026-04-28

**Headline:** The two real editor problems that 0.12.2/3 didn't
actually fix: highlighting was still missing for every language,
and the new markdown preview produced a half-rendered page where
the bottom got dumped into a `<pre>`.

### Editor

- **Pin `@lezer/highlight` + `@lezer/common` in the esm.sh
  `?deps=` list.** The silent-no-highlights failure was caused by
  duplicated `@lezer/highlight` instances across CodeMirror
  packages: `tags.keyword` etc. are JS objects compared by
  identity, so when the language pack and the theme each loaded
  their own copy, the parser produced tags the theme didn't
  recognize, and all coloring quietly went away. Pinning common
  + highlight forces every package to share one set.
- **Separate `Marked` instance for the markdown preview
  (`previewMarked`).** The chat renderer is loaded with custom
  `code` handling for SEARCH/REPLACE diffs and edit:foo/path
  fence syntax — that ran on every preview too, occasionally
  swallowing the rest of the document into one `<pre>` block on
  certain inputs. Preview now uses a vanilla marked + a slim
  hljs-only `code` override.

## [0.12.3] — 2026-04-28

**Headline:** Editor as a first-class sidebar tab. The drawer was
the only way in, which meant you had to start a chat and click a
file path before you could browse anything. Now there's a sidebar
entry that opens the file tree directly.

### Editor

- New **Editor** tab in the sidebar (after Chat). Mounts the
  `EditorPanel` full-width inside `.main` — same file tree,
  tabs, CodeMirror — no drawer chrome.
- `.main` gets a `main-editor` modifier when the editor tab is
  active, dropping the 28×36 panel padding and letting the
  editor fill the viewport.
- The chat drawer entry point still works (clicking a path in a
  tool card slides the drawer in over the current tab). Drawer
  and sidebar Editor are separate instances; their tab state
  doesn't share yet — revisit if it becomes annoying.

## [0.12.2] — 2026-04-28

**Headline:** Editor polish pass. Tabs at the top span the full
editor width like VS Code, syntax highlighting actually shows up,
the gutter/line numbers match the dark theme, autocomplete pops
on every keystroke instead of waiting for a manual trigger.

### Editor

- **Tabs on top, full width** — moved out of `.editor-main` and
  into a sibling `.editor-tabs` that sits above the side+main
  body row. Active tab gets a primary-color top border and the
  editor's own background, so it visually merges into the code
  surface (VS Code pattern). The file panel can collapse and the
  tab bar stays put.
- **Highlighting works** — `oneDark` already ships its own
  HighlightStyle; the existing `defaultHighlightStyle` wrap was
  fine but ordered before `oneDark`, so it didn't cover languages
  oneDark misses. Reordered to fall back AFTER oneDark and added
  `highlightActiveLineGutter` so the active row stands out in the
  gutter too.
- **Gutter restyled** — `.cm-gutters` gets a darker `#21252b`
  background, line numbers use the muted `#495162` for inactive
  rows and `#abb2bf` for the active row, with a 40px min-width
  and 16px right-padding. Fold gutter ships alongside (click the
  arrow next to a brace to fold).
- **Autocomplete** — `autocompletion({ activateOnTyping: true,
  closeOnBlur: true, maxRenderedOptions: 30 })` so suggestions
  pop while you type. Added `completionKeymap` so Tab/Enter pick
  the highlighted entry. Popup styled to the dark palette.
- **Tab close ergonomics** — close button has a fixed 18px box
  so the tab doesn't jump width when the dirty dot toggles.

All edits in `dashboard/app.js` `EditorPanel` + `dashboard/app.css`.

## [0.12.1] — 2026-04-28

**Headline:** Editor v2 — VS Code-style file tree, collapsible file
panel, wider drawer. The 0.12.0 editor opened on the right at 50%
with a flat alphabetical file list; on a normal-width window that
felt cramped and the file list scrolled forever.

### Editor

- **File tree** — flat path list collapses into a recursive folder
  tree. Folders sort first, files alphabetically; click `▶` to
  expand, `▼` to collapse. The expanded set lives in panel state so
  it survives drawer close/reopen within a session.
- **Collapsible side panel** — `◀` button at the top of the file
  panel hides everything except a thin `▶` button that brings it
  back. Editor area gets the full drawer width when files are out
  of the way.
- **Wider drawer** — `.editor-drawer-host.open` bumped from 50% →
  65% (min-width 360 → 420) so the editor breathes.
- **Filter still flat** — when the search box has text, the tree
  view collapses to the existing flat filtered list (paths are
  more useful than indented names when you're searching).

No backend changes. All edits in `dashboard/app.js` `EditorPanel`
+ `dashboard/app.css`.

## [0.12.0] — 2026-04-28

**Headline:** Web dashboard. A top-tier local control plane that lives
alongside the TUI — chat, files, MCP, skills, hooks, settings, all on
one URL. Plus auto/flash/pro preset rework so model commitment is
something you actually understand.

### Web dashboard (`/dashboard` slash)

A full-screen browser app, embedded HTTP server, 12 panels, modal
mirroring back to the live TUI. 127.0.0.1 only, ephemeral token in
the URL, CSRF on every mutation.

**Foundation (v0.12 base)**
- HTTP server in `src/server/` — Node native `http`, zero new deps
- Token + CSRF auth, audit log per mutation
- Preact 10 + HTM SPA (no build step), CSS lifted from `src/cli/ui/theme.ts`
- 12 panels, all functional: Chat / Overview / Usage / Sessions /
  Plans / Tools / Permissions / System / MCP / Skills / Memory /
  Hooks / Settings

**Chat parity (v0.13a)**
- POST `/submit` routes through `handleSubmit` so slash commands,
  `!cmd`, `@path` work identically; SSE `/events` streams loop
  events live; `/abort` mirrors Esc; `/messages` snapshots the
  log; `/modal/resolve` lets web pick a ShellConfirm /
  ChoiceConfirm / PlanConfirm / EditConfirm — either surface
  resolves, the other's modal disappears
- Web: marked.js + highlight.js 38-language pack, GFM tables,
  custom diff renderer for SEARCH/REPLACE blocks (red `-` / green
  `+`) and unified diffs, kind-specific tool cards (edit_file,
  read_file, write_file, run_command), markdown-styled assistant
  messages with reasoning blockquote, blinking cursor while
  streaming, scroll lock when user reads above bottom, custom
  scrollbars in brand palette

**Observability (v0.13b)**
- Sessions browser — list / read any saved session
- Plans archive — replay archived plans with risk pills
- Usage time-series chart (uPlot) — daily cost / cache-saved / turns
- System health — disk usage, version check, jobs

**Mutation surface (v0.14)**
- MCP — list bridged servers + add/remove specs to config
- Skills — list, edit body, create new, delete
- Memory — REASONIX.md + global / project private memory editor
- Hooks — settings.json hook block editor + reload
- Settings — API key (write-only), base URL, preset, effort, search

**Polish (v0.15)**
- Mobile responsive: sidebar collapses to drawer with hamburger,
  metric grid drops to 2 columns, header stacks vertically
- Animations: fade-in for messages, slide-in for modals + toasts,
  `prefers-reduced-motion` respected
- Toast system (top-right, auto-dismiss)
- Global error overlay — `window.error` + `unhandledrejection` +
  Preact ErrorBoundary all funnel into a full-screen card with
  copy-details + "Report on GitHub" prefilled-issue button

**Editor drawer (post-v0.15)**
- Click any path in chat tool cards → CodeMirror 6 drawer slides
  in from the right (50% width, full-screen on mobile)
- Multi-tab, dirty flag, Cmd/Ctrl+S save, syntax highlighting in
  14 languages, gitignore-aware file picker
- Drawer state persists across sidebar tab switches

**Live status bar (in Chat)**
- model · ctx token gauge · cache hit % · turn cost · session
  cost · DeepSeek balance — 2.5s poll, mirrors TUI StatsPanel

**Live mode pickers (in Chat)**
- Edit mode (review/auto/yolo) — instant
- Effort (high/max) — applies next turn, also flippable from `/effort`
- Preset (auto/flash/pro) — applies next turn via `applyPresetLive`
- New / Clear conversation buttons (route through `/new` and `/clear`)

### Preset rework — auto / flash / pro

**Headline:** old `fast / smart / max` collapsed into model-commitment
vocabulary that actually says what it does.

- **`auto`** — flash baseline, auto-escalates to pro on
  `<<<NEEDS_PRO>>>` markers or after 3+ tool failure signals.
  The default — covers ~96% of turns at flash cost.
- **`flash`** — flash always. No auto-escalation. `/pro` still
  works for one-shot manual escalation.
- **`pro`** — pro always. No downgrade. ~3× flash at the 5/31
  discount window, ~12× outside it.

`autoEscalate: boolean` added to the loop (constructor + reconfigure)
gates both auto-escalation paths (NEEDS_PRO marker scavenge +
failure-count threshold). `flash` and `pro` presets pass `false`,
locking the running session to one model.

Legacy `fast / smart / max` names: still parse from existing
config files but collapse to `auto` — simpler than mapping the old
semantics onto the new vocabulary, user re-picks if they want flash
or pro explicitly.

`applyPresetLive` callback in `DashboardContext` flips the live
loop's model + autoEscalate + reasoningEffort the moment the user
clicks a preset in the web Chat picker — no session restart.

### Other

- `cacheSavingsUsd(model, hitTokens)` in `src/telemetry.ts` — USD
  the prompt cache shaved off the bill (miss-price minus hit-price
  for cached tokens). Surfaced in `reasonix stats` dashboard +
  `/api/usage` rolled buckets + the Usage chart.
- Built-in shell allowlist (`BUILTIN_ALLOWLIST`) re-exported for
  the dashboard's Permissions panel listing.
- `removeProjectShellAllowed` + `clearProjectShellAllowed` in
  `src/config.ts`.
- StreamableHttpTransport (MCP 2025-03-26) — already shipped in
  0.11.3 but documented here for completeness; this release adds
  the Mcp panel UI on top.
- `DashboardEvent` + `ActiveModal` types exported from
  `src/server/context.ts` for downstream tooling.

### Tests

1568 vitest tests pass. New test files: `tests/server-dashboard.test.ts`
(40 tests covering auth/CSRF, every endpoint shape, SSE round-trip,
mid-modal mutations).

## [0.11.3] — 2026-04-27

**Headline:** Two long-deferred items land — `/permissions` makes the
shell allowlist auditable and editable from inside the TUI, and
Streamable HTTP MCP transport (2025-03-26 spec) clears the last debt
from the v0.3 deferred queue.

### Added

- **`/permissions`** — list / add / remove / clear the shell
  allowlist without leaving the session. Bare `/permissions` shows
  the current edit mode (review / auto / yolo with a yolo-bypasses-
  allowlist banner), the per-project entries with 1-based indices,
  and the read-only builtin list grouped by leading verb. Subcommands:
  `/permissions add <prefix>` (multi-token OK), `/permissions remove
  <prefix-or-N>` (literal match or list index), `/permissions clear
  confirm`. Refuses to add a prefix that's already in the builtin
  list (no redundant project entry) and refuses to remove a builtin
  (read-only). Mutating subcommands require code mode. `perms`
  registered as alias.
- **`removeProjectShellAllowed` + `clearProjectShellAllowed`**
  exported from `src/config.ts`. The remove helper does literal-
  prefix match (not prefix-of-prefix), so dropping `git` doesn't
  accidentally remove `git push origin main` if both were stored.
- **MCP Streamable HTTP transport (2025-03-26 spec)** —
  `src/mcp/streamable-http.ts` implements the new single-endpoint
  protocol. POSTs JSON-RPC frames, handles all three response shapes
  (202 Accepted for notifications, `application/json` for single
  responses, `text/event-stream` for multi-frame streams covering
  progress + response). Captures `Mcp-Session-Id` from the first
  response that hands one out and echoes it on every subsequent
  request; surfaces 404-with-session as a "session expired" error
  so callers know to reinitialize. Long-lived GET stream for
  unsolicited server-initiated frames is deliberately deferred —
  POST-only handles full request/response/notification traffic
  for every server we'd realistically point at today.
- **Spec parser** — `streamable+http(s)://` prefix routes to the
  new transport (`{ transport: "streamable-http", url, name }`).
  Plain `http(s)://` still routes to SSE (2024-11-05) so existing
  `--mcp` config entries keep working without surprise upgrades.
  Wired through `chat.tsx`, `run.ts`, and `reasonix mcp inspect`.
  Public API gains `StreamableHttpTransport` + the
  `StreamableHttpMcpSpec` type re-export.

### Tests

- `tests/permissions-slash.test.ts` — 16 tests covering listing,
  add, remove (by prefix and by 1-based index), clear, mode banner,
  builtin-collision rejection, codeRoot guard, alias.
- `tests/config.test.ts` — 6 new tests for `removeProjectShellAllowed`
  / `clearProjectShellAllowed` (literal-only matching, scoping per
  project, idempotent counts).
- `tests/mcp-streamable-http.test.ts` — 8 tests against an in-process
  `http.Server` fake that speaks the 2025-03-26 wire shape: JSON
  response delivery, 202 ack as no-op, session-id capture+echo,
  multi-frame SSE ordering (progress → response), full McpClient
  initialize → tools/list round-trip, 404+session = "expired",
  500-as-error from `send()`, `close()` unblocks idle iterators.
- `tests/mcp-spec.test.ts` — 4 tests for the new prefix parsing.

1521 tests pass (+24). Lint / typecheck / build clean.

## [0.11.2] — 2026-04-27

**Headline:** `/init` synthesizes a baseline REASONIX.md so a new
project starts with context instead of cold. Closes the gap with
Claude Code's `/init`, scoped to the structure REASONIX expects.

### Added

- **`/init`** — model-driven REASONIX.md generator. The slash
  emits a structured user-turn prompt (via the `resubmit` channel)
  that hard-constrains the model to a fact-only document with
  Stack / Layout / Commands / Conventions / Watch out for sections,
  capped at 80 lines / 3KB so REASONIX.md doesn't bloat the system
  prompt every launch. Reuses the existing filesystem tools (no new
  pipeline) and the result lands as a pending edit in the normal
  review queue, so the user audits before it hits disk. Refuses to
  overwrite an existing REASONIX.md without `/init force`. Removes
  the friction of having to hand-author a project memory file.

## [0.11.1] — 2026-04-27

**Headline:** Workspace-switching, end to end. Four real-use bugs
that all hit the same scenario — `Esc` poisoned the next turn,
Chinese-Windows shell errors came back as mojibake, the markdown
renderer ate `\TEST` out of `F:\TEST1`, and the model had no idea
how to change directories. Plus two new ways to do it: `/cwd <path>`
the user types, and `change_workspace` the model calls (always
gated on an explicit confirmation modal — no auto-switching).

### Fixed

- **`Esc` poisoned the next turn.** The loop's user-Esc abort branch
  processed the cancel correctly but left `_turnAbort` in an aborted
  state on its way out. The carry-abort logic at `step()` entry then
  re-aborted at iter 0 on every subsequent turn, so the user typed
  a fresh prompt and saw "stopped without producing a summary"
  before any model call ran. The session was effectively dead until
  restart. Fix: reset `_turnAbort` to a fresh controller before
  returning from the abort branch — the across-turn race that the
  carry logic guards against still works because a new `abort()`
  fired between turns aborts the new controller. Regression test
  added (`tests/loop.test.ts`).
- **Mojibake on Chinese / Japanese / Korean Windows shell errors.**
  `runCommand` decoded child output as UTF-8 incrementally per
  chunk. Two failure modes:
  1. `cmd.exe`'s OWN error messages (e.g. "'sed' is not recognized
     as an internal or external command") come from a localized
     resource DLL and ignore `chcp 65001`, so on Chinese Windows
     the bytes are CP936/GBK and decoded as UTF-8 produced
     unreadable garbage.
  2. Multi-byte sequences could split across chunk boundaries and
     corrupt before the second half arrived.
  Fix: collect raw `Buffer[]` chunks and decode once at close via
  a new `smartDecodeOutput` — strict UTF-8 first; on Windows fall
  back to GB18030 (GBK superset) when UTF-8 rejects the bytes;
  last resort lossy UTF-8 keeps the structural exit-code marker
  intact. PowerShell's existing `injectPowerShellUtf8` prelude
  still covers the PS path; this fixes the path where the model
  invokes a native EXE directly (`run_command sed …`).
- **Markdown renderer ate `\TEST` out of `F:\TEST1`.** `stripMath`'s
  catch-all LaTeX command stripper (`\\[a-zA-Z]+` → `""`) deleted any
  backslash-followed-by-letters sequence — fine for an invented
  `\textbf{…}` the model emitted, catastrophic for Windows paths in
  prose. `F:\TEST1` rendered as `F:1`. Fix: gate the entire
  `stripMath` pipeline on a math-marker pre-check (`$`, `\(`, `\[`,
  known LaTeX commands, `^{…}`/`_{…}`, Pandoc super/subscripts). When
  none are present we return the string untouched. Mixed inputs (a
  path AND real math in the same message) still run the pipeline —
  math correctness wins over path preservation in that rare collision.
- **Model didn't know `/cwd` existed.** When asked to switch to a
  project on another drive, the model fumbled with `pwd`,
  `cd /d F:\TEST1`, and `2>&1` shell tricks (none of which work —
  `cd` doesn't carry across `run_command` calls and `2>&1` is rejected
  as a shell operator by design). The code-mode system prompt now has
  a "When the user wants to switch project / working directory"
  section telling the model to surface `/cwd <path>` once and stop,
  instead of trying to do it itself.

### Added

- **`change_workspace` tool** — model-callable workspace switching,
  gated on a confirmation modal. The tool fn validates the target,
  resolves it (absolute / `~`-expanded / relative-to-launch-cwd), then
  always throws a `WorkspaceConfirmationError` with the absolute
  path. App.tsx detects the marker and mounts a Switch / Deny modal;
  on approval it calls the same `applyCwdChange` path that drives
  `/cwd` (re-registers filesystem / shell / memory tools, reloads
  hooks, syncs the loop's hookCwd). On denial the model gets a
  synthetic "user refused, continue without it" message. No
  "always allow" option — workspace switches are per-target by
  nature. The code-mode system prompt now tells the model to call
  this tool (rather than fumble with `cd /d`) when the user asks
  to change projects, and to STOP after the call instead of chaining
  more tools before the user has confirmed.
- **`/cwd <path>`** — switch the session's working directory mid-
  session. Validates the target (must exist, must be a directory),
  expands `~`, then atomically: updates the hook cwd, memory root,
  project shell allowlist, `@file` mention root, and re-registers
  filesystem / shell / memory / `run_skill` tools against the new
  path so file reads, edits, and shell commands all land in the
  new sandbox. MCP servers stay anchored to the original cwd
  (their stdio child was spawned with the launch root and there's
  no standard reconnect handshake) — the slash output flags this
  explicitly when MCP servers are present. The system prompt's
  gitignore-aware project tour is also frozen at launch so the
  prefix cache stays valid; the slash output notes it for users
  switching to a structurally different project.

## [0.11.0] — 2026-04-27

**Headline:** Local semantic search lands as an opt-in pillar — Ollama-
backed embedding index, `reasonix index` CLI with progress spinner, a
`/semantic` slash for status, and bilingual (zh/en) prompts. Plus a
trio of subagent abort races that made `Esc` silently fail to stop a
running subagent.

### Added — Pillar 5: local semantic search

- **`reasonix index`** — new CLI command that walks the project, line-
  windows source files, embeds via Ollama (`nomic-embed-text` by
  default, ~274 MB once), and persists a JSONL index at
  `.reasonix/semantic/`. Incremental by default (mtime-based), with
  `--rebuild` for a full wipe. Per-chunk failures are logged + skipped
  so one bad file doesn't kill a 30-minute build.
- **Preflight prompts** — detects missing Ollama binary / daemon /
  model and offers to start `ollama serve` or `ollama pull <model>`
  with `[Y/n]` confirms. `--yes` for scripts. Non-TTY exits cleanly
  with a remediation hint.
- **TTY progress spinner** — Braille `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏` ticks every
  120ms via `setInterval`, INDEPENDENT of progress events. Builds
  that take 30+ seconds never look hung. Non-TTY mode keeps phase
  lines + heartbeats for parseable CI logs.
- **Chunker safety** — `maxChunkChars` cap (default 4000 ≈ 1000
  tokens) with line-boundary splitting for oversized windows and
  hard-truncation for single overlong lines. Fixes Ollama 500 "the
  input length exceeds the context length" on minified / dense files.
- **`semantic_search` tool** — registered in `reasonix code` only when
  an index exists. Tool description is now directive ("FIRST CHOICE
  for descriptive queries"); the code-mode system prompt grows a
  `# Search routing` fragment when the tool is registered, telling
  the model to prefer semantic_search for intent-style questions
  and fall back to grep for exact tokens.
- **`/semantic` slash** — shows status (built? Ollama installed?
  daemon up?) plus how-to-enable hints. Fire-and-forget pattern, same
  as `/kill` — sync placeholder, async post via `ctx.postInfo`.
- **Bilingual UI** — `src/index/semantic/i18n.ts` with EN/ZH dicts
  for every preflight + `/semantic` + progress label. Locale
  detection: `REASONIX_LANG` override → `LANG`/`LC_ALL`/`LC_MESSAGES`
  (Unix) → `Intl.DateTimeFormat` (Windows fallback) → `en`. Tool
  descriptions and CLI `--help` stay English on purpose (model-facing
  text aligns with training distribution; commander's --help is
  registered once at boot).
- **Startup is silent** — no auto-prompt on `reasonix code` launch.
  If an index exists, the tool registers; otherwise the bootstrap
  is a no-op. Discovery happens via `/semantic` when the user is
  curious, or via the explicit `reasonix index` command.

### Fixed — subagent `Esc` abort races

- **`addEventListener("abort", …)` doesn't replay aborts** — DOM
  semantics: an already-aborted signal won't fire the abort event
  again, so a parent that aborted before `spawnSubagent` attached
  its listener silently lost the cancel. Sync-check `.aborted` at
  attach and forward immediately to `childLoop.abort()`.
- **`step()` was overwriting aborted state** — at the top of
  `step()` we reassign `_turnAbort = new AbortController()`. If
  `loop.abort()` had been called BEFORE `step()` ran, the prior
  aborted controller was discarded and the fresh one started clean.
  Carry the aborted bit forward so the iter-0 check still bails.
- **`forcedSummary` was treated as success** — when the loop aborted
  it yielded a synthetic `assistant_final` with `forcedSummary: true`
  and content `"[aborted by user (Esc) — no summary produced.]"`.
  The subagent stuffed that into `final` and returned `success: true`,
  so `/skill` cheerfully reported "subagent finished" with the abort
  message as the answer. Now `forcedSummary` routes to `errorMessage`
  → `success: false` → caller renders the error.

### Added — docs / website

- **GitHub Pages site under `docs/`** — bilingual landing page (auto-
  detect via `navigator.language`, manual EN/中文 toggle, persisted
  per-browser), brand-gradient dark theme, hero terminal animation
  that mirrors the real TUI rendering primitives (◇/◆ role glyphs,
  yellow tool pills, rounded cyan EditBlockRow with `- old` red /
  `+ new` green diff lines, info-row pending/applied status).
- **`README.zh-CN.md`** — full Chinese mirror of `README.md`. Both
  READMEs now carry a language switcher header at the top.

### Tests (+27, 1441 → 1468)

- `tests/semantic-chunker.test.ts` — line-window splitting, overlap,
  forward-slash path normalization, NUL-byte sniff; the new
  `chunkText` cap behavior (multi-line split + hard-truncate-overlong-
  line + idempotent passthrough).
- `tests/semantic-store.test.ts` — JSONL roundtrip, cosine ranking,
  minScore threshold, dim-mismatch refusal, model-mismatch refusal,
  remove + wipe, fileMtimes.
- `tests/semantic-embed-tolerant.test.ts` — `embedAll` returns
  `Array<Float32Array | null>` on per-chunk error (mocked Ollama 500),
  abort still throws globally, all-fail surface, progress fires once
  per chunk regardless of outcome.
- `tests/semantic-i18n.test.ts` — locale detection precedence,
  override env var, placeholder substitution, ZH dict.
- `tests/semantic-bootstrap.test.ts` — registers when index exists,
  silent skip otherwise (no startup prompt).
- `tests/semantic-slash.test.ts` — `/semantic` status renderer,
  enabled / not-built / Chinese-locale paths.
- `tests/semantic-launcher.test.ts` — `findOllamaBinary` contract.
- `tests/code-prompt.test.ts` — search-routing fragment is absent by
  default and present + ordered before .gitignore when the flag is on.
- `tests/subagent.test.ts` — regression: parent signal already aborted
  at dispatch time (race we previously dropped on the floor).

### Refactored

- **`src/code/prompt.ts`** — `codeSystemPrompt(rootDir, opts?)` grew
  a `hasSemanticSearch` flag; the routing fragment is appended only
  when the tool is actually registered. Cache prefix stays stable per
  session because the flag is captured at launch.

## [0.6.0] — 2026-04-24

**Headline:** Cost control becomes a first-class pillar. Default flips
flash-first, `v4-pro` is opt-in, tool results auto-compact between
turns, and the TUI grows per-turn cost visibility + a `/pro`
one-shot upgrade. Month-over-month cost on an active coding project
drops ~6–10× in practice.

### ⚠ Breaking (behavior, not API)

- **Default model is now `deepseek-v4-flash`**, not `deepseek-v4-pro`.
  `reasonix code`, `reasonix chat`, and subagents all land on flash
  by default. Users who need the frontier tier:
  `/preset max`, `/pro`, or `--model deepseek-v4-pro` on CLI.
- **Preset defaults changed**. None of the three presets auto-enable
  `branch` or `harvest` anymore — both were hidden multipliers. The
  new matrix:
  | preset | model | effort | harvest | branch |
  |---|---|---|---|---|
  | fast | v4-flash | high | off | 1 |
  | smart (default) | v4-flash | max | off | 1 |
  | max | v4-pro | max | off | 1 |
  Users who want branching still get it via `/branch N`; users who
  want harvest still get it via `/harvest on`. Neither is implicit.
- **Default preset is now `smart` (was `fast`).** Flash + full
  thinking budget is the best price/quality point for coding.
- **`deepseek-chat` / `deepseek-reasoner` aliases scheduled for
  removal.** Still accepted (they map to flash non-thinking /
  thinking), but every user-facing surface (`/models`, setup wizard,
  `--help`) now advertises `v4-flash` / `v4-pro` only.

### Added — Cost control (Pillar 4)

- **`/pro` single-turn arming** — queue v4-pro for just the next
  turn; auto-disarms after. Separate from `/preset max` (persistent)
  so "this one task is hard" doesn't require a preset round-trip.
  Status bar shows `⇧ pro armed` in yellow while queued, `⇧ pro
  escalated` in red while the turn is actually running on pro.
- **Failure-triggered auto-escalation** — the loop tracks
  `edit_file` SEARCH-not-found errors + ToolCallRepair fires per
  turn. 3+ signals flip the rest of the turn to `v4-pro` with a
  visible warning row. Counter resets at every turn start. No
  silent cost surprises.
- **Model self-report escalation (`<<<NEEDS_PRO>>>`)** — system
  prompt teaches the model that when a task CLEARLY exceeds flash's
  capability (complex architecture, subtle correctness, genuine
  design trade-offs), emit the marker as the first line of its
  response. The loop aborts that call, retries this turn on pro,
  one shot. Guarded against infinite retry (pro never self-
  escalates) and streaming output is buffered so the marker never
  flickers on-screen before the retry fires.
- **Turn-end auto-compaction** — every tool result over 3000 tokens
  gets shrunk to a cap at turn end. Biggest win for long sessions:
  a 12KB `read_file` output stops re-paying its cost on every
  future prompt. The proactive in-turn threshold also dropped from
  60% → 40% so the reactive 80% path rarely fires.
- **Forced-summary + truncation-repair auxiliary calls hard-route to
  flash+effort=high** regardless of the main-turn tier. No reason to
  pay pro rates for "paraphrase these tool results into prose" or
  "close this truncated JSON."
- **Subagent default flipped to `v4-flash` + `effort=high`**. Skill
  frontmatter `model:` / `effort:` remain the per-skill override.
- **StatsPanel cost badges** — per-turn cost alongside session total.
  Colored thresholds: turn green under $0.05, yellow $0.05–0.20,
  red ≥$0.20; session same scale ×10.

### Added — UX

- **Plan body now flows into scrollback**, not inside the modal.
  `submit_plan` pushes a dedicated `role: "plan"` row into the
  Static log (rendered via the full markdown pipeline, never
  truncated); the PlanConfirm modal below shrinks to a tight
  approve/refine/cancel picker. Long plans are fully readable via
  terminal scrollback.
- **Shared prompt fragments** — `TUI_FORMATTING_RULES` and
  `NEGATIVE_CLAIM_RULE` live once in `src/prompt-fragments.ts`,
  embedded into every system prompt (main code, default chat,
  subagent, built-in skills). Three near-identical copies
  collapsed; subagents gain the "don't assert absence without
  checking" guardrail they previously lacked.

### Fixed

- **`run_skill` accepts decorated names.** The Skills index wrote
  entries like `- 🧬 explore`, and models copied the whole thing
  verbatim into `run_skill({name:"🧬 explore"})`. The index now
  uses a trailing `[🧬 subagent]` tag after the name, and
  `run_skill` normalizes inputs by stripping bracketed tags +
  leading emoji before lookup. Handles `"🧬 explore"`,
  `"[🧬 subagent] explore"`, `"explore [🧬 subagent]"`, etc.
- **`edit_file` result no longer shown twice.** The interceptor's
  `applyNow` was pushing an info row, and the loop's tool event
  re-displayed the same text as a proper tool row. Dropped the info
  row push; the tool row alone carries the content.
- **`run_command` / `run_background` descriptions teach their shell
  constraints upfront.** Explicit list of rejected operators
  (`&&`, `||`, `|`, `;`, `>`, `<`, `2>&1`), the `cd` doesn't-persist
  rule, a warning against unbounded-output commands (`netstat -ano`,
  `find /`), and concrete alternatives (`npm --prefix`, `cargo -C`,
  `git -C`). Models stop burning turns rediscovering these via
  error replies.

### Refactored (no behavior change)

- **App.tsx split** from 2931 → ~1980 lines by extracting
  `LiveRows.tsx`, `edit-history.ts`, `useEditHistory.ts`,
  `useCompletionPickers.ts`, `useSessionInfo.ts`, and
  `useSubagent.ts`. Every hook under 310 lines.
- **slash.ts split** from 1786 → 20-line barrel. Types,
  SLASH_COMMANDS data + parse helpers, shared utility helpers, a
  handler registry (`dispatch.ts`), and 10 per-topic handler files
  all under `src/cli/ui/slash/`. Adding a command now means editing
  one handler file + one registry line.

### Docs

- **`docs/ARCHITECTURE.md` rewritten** for v0.6. The four pillars,
  current module layout (slash + handlers + hooks all reflected),
  design-evolution timeline replacing the stale roadmap,
  non-goals updated to call out "automatic cost escalation without
  user-visible announcement" as explicitly rejected.

## [0.5.24] — 2026-04-24

**Headline:** `reasonix code` gets a proper review gate, background
process support, and aggressive context hygiene so long coding
sessions stop bleeding money.

### Added

- **Edit-gate modes (review / auto)** — `edit_file` and `write_file`
  tool calls now route through a user gate. `review` (default) pops
  an `EditConfirm` modal with a scrollable diff + `y/n/a/A/Esc`
  keys; `auto` applies immediately and arms a 5-second undo banner.
  `Shift+Tab` cycles, `/mode` sets explicitly. Persisted to
  `~/.reasonix/config.json`.
- **Session edit history** — every applied batch lands in an
  in-memory ring. `/history` lists them, `/show [id] [path]` dumps
  a stored diff (per-file when path given), `/undo [id] [path]`
  rolls back at any granularity (latest batch, specific batch,
  single file inside a batch). `u` keybind reaches back past the
  5-second banner as long as history has a non-undone entry.
- **Background processes** — new `run_background` / `job_output` /
  `stop_job` / `list_jobs` tools for dev servers and watchers. Spawn
  returns after a ready-signal match (`listening on`, `Local:`,
  `compiled successfully`, …) or `waitSec` seconds. `/jobs` /
  `/kill <id>` / `/logs <id>` surface them to the user. Cleanup on
  SIGINT / SIGTERM / exit kills every child.
- **Per-edit review modal (`src/cli/ui/EditConfirm.tsx`)** — diff
  viewport sized to terminal rows; `↑↓/j/k/Space/PgUp/PgDn/g/G`
  scroll a big diff in place. `a` applies rest of turn, `A` flips
  to AUTO for the session.
- **Bottom mode status bar** — always-visible line above the prompt
  shows mode / pending count / Shift+Tab hint / running-jobs tag;
  flashes on mode change.
- **Onboarding tip** — first `reasonix code` launch after upgrade
  posts the edit-gate keybindings once; suppressed after via the
  `editModeHintShown` flag.

### Changed

- **`read_file`** — adds `range:"A-B"` param (1-indexed, inclusive).
  Files longer than 200 lines with no scope return an auto-preview
  (head 80 + tail 40 + "N lines omitted" marker) instead of dumping
  everything. One `read_file` used to burn 6.5K tokens on a fat
  file; scoped reads cut that 3-5×.
- **`directory_tree`** — default `maxDepth` 4 → 2; skips
  `node_modules`, `.git`, `dist`, `build`, `out`, `.next`, `.nuxt`,
  `target`, `.venv`, `venv`, `__pycache__`, `.pytest_cache`,
  `.mypy_cache`, `.cache`, `coverage` unless `include_deps:true`;
  collapses any directory past 50 children with a nudge toward
  `list_directory`.
- **Auto-compact tool-call args** — after every `tool` response, the
  loop shrinks that call's `arguments` JSON if it exceeds 800
  tokens. Paths and short fields stay verbatim; long SEARCH /
  REPLACE / content strings get replaced with a `[…shrunk: N chars,
  M lines — tool already responded, see result]` marker. Cuts
  stale-args drag across every subsequent turn.
- **`/compact`** — now covers both tool results (existing) and
  tool-call args (new) in one pass.
- **`reasoningEffort` persistence** — `/effort high` now writes the
  choice to `~/.reasonix/config.json` and the loop picks it up at
  launch. Earlier versions silently reverted to `max` every relaunch.
- **Prompt scope discipline** — code-mode prompt tells the model to
  stop after "run / start / launch" tasks instead of proactively
  refactoring, running tsc, or chasing unused imports.

### Fixed

- **`run_background` confirmation path** — TUI now pops the shell
  confirm modal for `run_background` (not just `run_command`). A
  `kind` field on `pendingShell` routes approval to
  `JobRegistry.start()` so approving doesn't synchronously block on
  a dev server that never exits.
- **`/kill` actually kills the tree** — Windows `taskkill /T /F`,
  POSIX `process.kill(-pid, …)` on a detached child. Earlier
  `SIGTERM` only killed the `npm.cmd` wrapper; `node → vite →
  esbuild` survived. `/kill` also posts a late "job N exit M" row
  when the stop resolves, so the user doesn't have to poll `/jobs`.

## [0.4.24] — 2026-04-22

**Headline:** `reasonix stats` is now a cross-session cost dashboard.

Every turn `reasonix chat|code|run` executes now appends one line to
`~/.reasonix/usage.jsonl` carrying tokens + cost + the equivalent
Claude Sonnet 4.6 cost. `reasonix stats` (no arg) rolls that log up
into today / week / month / all-time windows:

```
Reasonix usage — /Users/you/.reasonix/usage.jsonl (2.3 KB)

            turns  cache hit    cost (USD)      vs Claude     saved
----------------------------------------------------------------------
today           8      95.1%     $0.004821        $0.1348      96.4%
week           34      93.8%     $0.023104        $0.6081      96.2%
month         127      94.2%     $0.081530        $2.1452      96.2%
all-time      342      94.0%     $0.210881        $5.8934      96.4%

most used model:   deepseek-reasoner (84% of turns)
top session:       default (214 turns)
tracked since:     2026-04-20
```

Pillar 1's pitch (94–97% cost reduction vs Claude) turns from a
blog number into a fact users can check on their own machine. The
savings column is derived per turn (not synthesized) from the
existing `claudeEquivalentCost()` helper in `src/telemetry.ts`.

Back-compat: `reasonix stats <transcript>` still works — passing a
path falls back to the old per-file summary (assistant turns + tool
calls). No arg → dashboard.

Privacy: the log contains tokens + costs + the user-chosen session
name, nothing else. No prompts, no completions, no tool args.

### Added

- **`/stats` slash** — same dashboard, in-session. Reads
  `~/.reasonix/usage.jsonl` and renders via the shared
  `renderDashboard` pure function, so the shell command and the
  slash stay in sync by construction.
- **`src/usage.ts`** — `appendUsage` (best-effort JSONL write,
  swallows disk failures so a read-only `~/` never breaks the
  turn), `readUsageLog` (malformed-line tolerant), `aggregateUsage`
  (rolling windows: 24h / 7d / 30d / all, plus model + session
  histograms), `bucketCacheHitRatio`, `bucketSavingsFraction`,
  `formatLogSize`.
- **Wire-up** in `src/cli/ui/App.tsx` (assistant_final event) and
  `src/cli/commands/run.ts` (CI / scripting turns land in the same
  log as TUI turns).
- **Upgraded `reasonix stats`**. No-arg → dashboard; transcript arg
  → legacy per-file summary. `renderDashboard(agg, path)` is an
  exported pure function so tests can assert the string output.

### Tests (+15, suite 708 → 723)

- `tests/usage.test.ts` covers: appendUsage round-trip, empty
  log / malformed-line tolerance / parent-dir auto-creation / silent
  write-failure (points path at a regular file), aggregateUsage
  (empty, rolling-window bucketing, cross-record sums, byModel +
  bySession sort + (ephemeral) grouping), bucket helpers with zero
  denominators, renderDashboard (row labels + em-dash fallback).

---

## [0.4.23] — 2026-04-22

**Headline:** Hooks — user-defined automation that fires at four
well-known points in the loop. Same two-scope layout (project +
global) as memory and skills.

A hook is a shell command. Reasonix invokes it with stdin = a JSON
envelope describing the event. The exit code drives the decision:
`0` = pass, `2` = block (only on `PreToolUse` / `UserPromptSubmit`),
anything else = warn (rendered inline as a yellow row, the loop
keeps going). Block on a tool event swaps the dispatch for a
synthetic tool result carrying the hook's stderr — the model sees
a structured refusal instead of a silent omission, and can
reason about what to do next.

Settings file:

```json
// <project>/.reasonix/settings.json   ← committable
// ~/.reasonix/settings.json           ← per-user
{
  "hooks": {
    "PreToolUse":       [{ "match": "edit_file|write_file", "command": "bun scripts/guard.ts" }],
    "PostToolUse":      [{ "match": "edit_file", "command": "biome format --write" }],
    "UserPromptSubmit": [{ "command": "echo $(date +%s) >> ~/.reasonix/prompts.log" }],
    "Stop":             [{ "command": "bun test --run", "timeout": 60000 }]
  }
}
```

Project hooks fire before global hooks. `match` is anchored regex
on the tool name (`*` or omitted = match every tool); ignored for
prompt / Stop events. Per-hook `timeout` overrides the defaults
(5s for blocking events, 30s for logging events). The CLI loads
both files at App mount; `/hooks` lists what's active and
`/hooks reload` re-reads disk without tearing down the running
loop (so the append-only log is preserved).

Deliberate non-goals for v1: workflow DSL, conditional chaining,
hook templates. Hooks are shell commands — the user already has
a programming language, we don't need to invent one.

### Added

- **`src/hooks.ts`** — `loadHooks` (project + global merge),
  `runHooks` (event filter + stdin JSON + spawn dispatch),
  `decideOutcome` (pure exit-code → decision matrix), `matchesTool`
  (anchored-regex name filter), `formatHookOutcomeMessage` (single
  source of truth for the warning row text). Spawner is injectable
  for tests; default uses `shell: true` so `&&`, pipes, env
  expansion all behave the way they do in the user's terminal.
- **`CacheFirstLoopOptions.hooks` + `hookCwd`**. Loop dispatches
  `PreToolUse` (around line 866 in `src/loop.ts`) and `PostToolUse`
  (immediately after dispatch). `loop.hooks` is mutable so
  `/hooks reload` can swap the list without rebuilding the loop.
- **App-level `UserPromptSubmit` + `Stop`**. `App.tsx` calls
  `runHooks` before pushing the user message (block = drop the
  prompt) and after `loop.step` resolves (warnings only, since the
  turn already ended).
- **`/hooks` slash command**. `list` (default) groups loaded hooks
  by event with scope tags; `reload` re-reads settings.json from
  disk via the App-provided `reloadHooks` callback.
- **`/update` slash command**. Shows current vs the last-resolved
  latest (piggybacks on App.tsx's mount-time background check) and
  prints the exact shell command to upgrade. Deliberately does NOT
  spawn `npm install` from inside the TUI — stdio:inherit into a
  running Ink renderer corrupts the display, and on Windows the
  currently-running binary can be locked. Users exit the session
  and run `reasonix update` in a fresh shell.

### Tests (+36, suite 672 → 708)

- `tests/hooks.test.ts` — `loadHooks` (empty / project+global / array
  order / ignore malformed entries / tolerate malformed JSON / no
  project root → global only / path helpers), `matchesTool` (`*` /
  anchored regex / substring rejected / malformed regex falls back
  to no-match / non-tool events ignore match), `decideOutcome`
  (exit 0 / exit 2 / non-zero / timeout / spawn error per event),
  `runHooks` (filters by event+match before running, stops at first
  block, doesn't stop on warn, stdin envelope shape, cwd routing,
  default timeouts, per-hook timeout override), `formatHookOutcomeMessage`
  (pass → empty / non-pass includes scope+command+detail / 60-char
  truncation).
- `tests/loop-hooks.test.ts` — `CacheFirstLoop` accepts a hook list,
  default empty, `loop.hooks` is mutable, `hookCwd` defaults to
  `process.cwd()` and honors override, no-tool turn doesn't fire
  PreToolUse hooks.
- `tests/slash.test.ts` — updated `suggestSlashCommands("h")` to
  include the new `hooks` command; added 4 tests for `/update`
  (pending / up-to-date / upgrade-available / suggest-surfaces-it).

---

## [0.4.22] — 2026-04-22

**Headline:** Version display in the TUI header + `reasonix update`
self-upgrade command.

Two small quality-of-life additions. The stats panel now carries the
running version (`Reasonix v0.4.22 · model …`) so users can tell at
a glance whether they're on the latest build; a 24-hour background
check against the npm registry quietly surfaces a yellow
`update: X.Y.Z` nudge on the right side of the same row when a
newer version has been published. The nudge never blocks startup —
the fetch is bounded at 2s with a 24h on-disk cache, and any
failure (offline, firewall, registry hiccup) is silent by design.

`reasonix update` is the command form: detects whether you're
running a global install vs an ephemeral `npx` spawn, and either
spawns `npm install -g reasonix@latest` for the former or prints a
cache-refresh hint for the latter. `--dry-run` prints the plan
without executing.

The `VERSION` constant now sources from `package.json` at runtime
(walking up from `import.meta.url`) instead of a hand-maintained
literal, so it can never drift again — it was stale at `0.4.20`
before this release. Tests assert they stay in sync.

### Added

- **`src/version.ts`** — exports `VERSION`, `compareVersions`,
  `getLatestVersion`, `isNpxInstall`, and the
  `LATEST_CACHE_TTL_MS` / `LATEST_FETCH_TIMEOUT_MS` constants.
  `getLatestVersion` caches to `~/.reasonix/version-cache.json`
  (24h TTL) and returns `null` on any failure.
- **`reasonix update`** subcommand (`src/cli/commands/update.ts`).
  `planUpdate()` is the pure decision function, `updateCommand()`
  is the CLI orchestrator with test seams (`fetchLatest`, `isNpx`,
  `spawnInstall`, `write`, `exit`).
- **StatsPanel header shows `v${VERSION}`** inline, plus an
  `update: X` badge (yellow, bold) on the right when
  `updateAvailable` is passed. App.tsx fires the registry check
  in a background `useEffect` on mount; only a version strictly
  newer than the running one flips the state.

### Fixed

- **Drifted `VERSION` constant.** `src/index.ts` hard-coded
  `"0.4.20"` while `package.json` was on `0.4.21`. Replaced with a
  re-export from `src/version.ts`, which reads the manifest on
  first access. A regression test pins them together.

### Tests (+19, suite 588 → 607)

- `tests/version.test.ts` — `VERSION === package.json.version`,
  `compareVersions` covers numeric + pre-release ordering,
  `isNpxInstall` covers the three detection paths,
  `getLatestVersion` covers cache hit / force-refresh / expired
  entry / network failure / bad body / cache-write failure.
- `tests/update-command.test.ts` — `planUpdate` returns the
  correct action for all four decision quadrants; `updateCommand`
  respects every seam: no-spawn on up-to-date, no-spawn on npx,
  spawns on global-behind-latest, honors `--dry-run`, exits
  non-zero on registry failure, surfaces npm's non-zero exit.

---

## [0.4.21] — 2026-04-22

**Headline:** Skills — user-authored prompt packs, two-scope layout
matching user-memory.

Reasonix discovers skills under `<project>/.reasonix/skills/` (project
scope) and `~/.reasonix/skills/` (global scope). Project wins on name
collisions — per-repo overrides of a global skill work the way users
expect. Deliberately NOT tied to any other tool's directory
convention (`.claude/`, `.glm/`, etc.): Reasonix is model-agnostic at
the conversation layer, so coupling the skill filesystem to one
vendor would break anyone running a different backend.

The pinned index (names + one-line descriptions) lives in the
immutable system prefix; bodies stay lazy and enter the append-only
log only when invoked — either by the model calling the new
`run_skill` tool or by the user typing `/skill <name> [args]`. No
DAG engine, no workflow DSL — the model reads the skill's prose and
continues the normal tool-use loop from there. Pillar 1's cache
invariants are preserved: adding skills grows the pinned index
(under a 4k char cap, with a truncation marker) but never alters
the rest of the prefix.

### Added

- **`src/skills.ts`** — `SkillStore` with `SkillScope` of `"project"`
  or `"global"`, both layouts recognized (`{name}/SKILL.md` and flat
  `{name}.md`). `applySkillsIndex` composer is pinned into
  `applyMemoryStack` alongside REASONIX.md + user memory, receiving
  the same `rootDir` so the project scope picks up
  `<rootDir>/.reasonix/skills/`.
- **`run_skill` tool** (`src/tools/skills.ts`) — read-only, returns
  the full markdown body plus an optional forwarded `Arguments:` line.
  Registered in `reasonix chat` (global only) and `reasonix code`
  (project + global).
- **`/skill` slash command** — `list` / `show <name>` / bare
  `<name> [args]` form. The bare form injects the skill body as a
  user turn via the same `resubmit` hook `/apply-plan` uses. Reads
  project scope from `ctx.codeRoot`, mirroring how `/memory` behaves.

### Notes

- Each skill's `allowed-tools` frontmatter is parsed but **ignored**
  in v1. Reasonix's tool namespace (`filesystem`, `shell`, `web`)
  doesn't one-to-one map onto other clients' names; the model reads
  the prose instructions and picks our equivalents. Will revisit
  once the tradeoffs are clearer.
- What we explicitly did **not** add: workflow DSL, DAG scheduler,
  parallel branches, sub-agents. Skills are prose; the model does the
  sequencing. This keeps single-loop + append-only + cache-first
  intact — the architectural non-goal "no multi-agent orchestration"
  stands.

### Fixed

- **`ShellConfirm` "always allow" did not take effect until relaunch.**
  The `run_command` tool captured `extraAllowed` as a snapshot at
  registration time, so a prefix the user approved mid-session was
  written to `~/.reasonix/config.json` but the in-memory tool still
  refused it — the next invocation re-triggered the confirmation
  modal. `ShellToolsOptions.extraAllowed` now accepts a getter in
  addition to a static array; `reasonix code` passes
  `() => loadProjectShellAllowed(rootDir)` so the allowlist is
  re-read from disk on every dispatch. Static-array callers keep
  working unchanged.
- **Windows cmd.exe built-ins (`dir`, `echo`, `type`, `ver`, …)
  crashed with ENOENT.** These aren't standalone executables, so
  `PATH × PATHEXT` lookup misses and `spawn dir` fails. `prepareSpawn`
  now routes bare unresolved Windows commands through
  `cmd.exe /d /s /c "<cmd> <args…>"` with verbatim-args + manual
  metacharacter quoting — same wrapping strategy we already use for
  `.cmd`/`.bat` files. Built-ins resolve correctly; genuinely unknown
  commands get the standard "'foo' is not recognized as an internal
  or external command" message instead of a raw spawn error.
  Already-extensioned names (`node.exe`) and paths-with-separators
  (`C:\tool.exe`) still pass through unwrapped so an explicit "I
  know where this is" invocation fails loudly when it's missing.

## [0.4.19] — 2026-04-22

**Headline:** Windows shell hotfix + StormBreaker visibility.
`reasonix code` now runs `npm`, `npx`, `tsc`, `yarn`, `pnpm`, `bun`,
`pytest`, and every other `.cmd` / `.bat` wrapper on Windows — both
under Node 18/20 (broken by missing PATHEXT resolution) and Node
21.7.3+/24 (broken by CVE-2024-27980's prohibition on direct
`.cmd`/`.bat` spawns with `shell: false`). Unix behavior unchanged.
Plus: the StormBreaker anti-loop-detector no longer silently halts
a turn — when it fires it emits a visible warning row explaining
what was suppressed and what the user should do next, and its
sliding window resets on each new user message so a new intent
doesn't inherit the previous turn's repeat patterns.

### Fixed

- **`spawn npm ENOENT` on Windows** — `child_process.spawn` with
  `shell: false` uses `CreateProcess`, which ignores PATHEXT. Bare
  `npm` failed because no `npm.exe` exists — only `npm.cmd`. New
  `resolveExecutable(cmd)` walks `PATH × PATHEXT` manually and
  returns the full resolved path (`C:\Program Files\nodejs\npm.CMD`)
  before handing to spawn. Keeps `shell: false` (no shell expansion
  of piped / chained commands — the whole reason we avoided
  `shell: true` to begin with).
- **`spawn npm EINVAL` on Node ≥ 21.7.3 / 24** — even with the
  resolved `.cmd` path, Node's post-CVE-2024-27980 patch refuses to
  execute `.cmd` / `.bat` files via direct spawn. Second layer:
  `prepareSpawn()` detects a `.cmd` / `.bat` target on Windows and
  rewrites the invocation to `cmd.exe /d /s /c "<bin> <args…>"`
  with `windowsVerbatimArguments: true`. Each arg is routed through
  `quoteForCmdExe()`, which wraps in double quotes when the arg
  contains whitespace or cmd.exe metacharacters
  (`" & | < > ^ % ( ) , ; !`) and doubles embedded quotes per
  cmd.exe's `""` escape rule. Arguments like `a&b` stay literal;
  they don't become shell operators.

### Added

- **`resolveExecutable(cmd, opts?)`** — exported from `src/tools/shell.ts`.
  Windows PATH × PATHEXT resolver. Opts lets tests inject `platform`,
  `env`, and `isFile` so the Windows-specific path can be exercised
  from a Linux CI runner without touching real fs.
- **`prepareSpawn(argv, opts?)`** — exported. Returns the
  `(bin, args, spawnOverrides)` tuple that runCommand should pass to
  `child_process.spawn`. On non-Windows it's a passthrough; on
  Windows it applies the PATHEXT lookup and the `cmd.exe` wrapping
  when needed. Unit-tested without spawning real processes.
- **`quoteForCmdExe(arg)`** — exported. The per-arg quoting
  function. Round-trip tested against realistic argvs
  (`npm install`, paths with spaces, args containing
  `& | < > ^`, empty strings, embedded double quotes).

- **Silent storm-break**. When `StormBreaker` caught a repeated
  `(tool, args)` pattern it dropped the offending call but emitted
  nothing user-visible beyond a small `[repair] broke 1 storm` note
  on the assistant row. If the suppressed call was the only tool
  call of the turn, the turn just ended — no explanation of why
  nothing happened. Now the loop yields a dedicated `warning` event
  (same channel as Esc-abort and budget warnings) with an
  actionable message, distinguishing "all calls suppressed (stuck
  retry)" from "some calls suppressed" cases.
- **StormBreaker state bleeds across user turns**. The sliding
  window of recent signatures persisted for the lifetime of the
  loop, so a stuck pattern from an earlier intent could false-
  positive against the user's legitimate new "try again with
  different input" request. `CacheFirstLoop.step()` now calls
  `repair.resetStorm()` on every new user turn — the window
  repopulates naturally as the new turn's tool calls fire, and
  genuine repeats still trip after the usual 3-in-a-row pattern.

### Added

- **`ToolCallRepair.resetStorm()`** — exposes StormBreaker.reset
  through the repair facade. Called by the loop at each user turn;
  library consumers that drive `repair.process` manually can use it
  too if they wrap their own turn semantics.

### Tests (+22, suite 566 → 588)

- `tests/shell-tools.test.ts` (+21) — `resolveExecutable` on
  non-Windows (passthrough), PATHEXT walk (first-hit ordering,
  whitespace-tolerant PATHEXT entries), absolute-path / slash /
  already-extensioned passthrough, empty input, missing PATH /
  PATHEXT. `quoteForCmdExe` (simple identifiers unquoted, whitespace
  + metachars quoted, embedded quotes doubled, empty string
  → `""`). `prepareSpawn` (unix passthrough, `.cmd` wraps via
  cmd.exe, `.bat` wraps too, `.exe` direct, metachar args quoted,
  PATHEXT miss falls through).
- `tests/repair/pipeline.test.ts` (+1) — `resetStorm` clears the
  repeat-window so post-reset calls aren't suppressed.
- `tests/loop.test.ts` — the iter-budget warning test refined to
  filter by the iter-specific pattern, since identical fixture
  calls now also trip the (correct) storm warning.

### Internals

- `runCommand` in `src/tools/shell.ts` now calls `prepareSpawn`
  instead of spawning `argv[0]` directly. Every codepath that was
  going through `spawn` still does; the `bin` / `args` /
  `spawnOverrides` it receives are platform-normalized.
- Existing allowlist + `readOnlyCheck` plan-mode gate + timeout /
  output-cap / AbortSignal wiring is untouched.
- `CacheFirstLoop.step()` now resets the StormBreaker at the top of
  each turn AND emits a `warning` event after `repair.process()`
  when `report.stormsBroken > 0`. The existing `repair` field on
  `assistant_final` still carries the count for historical records
  / transcripts.

---

## [0.4.18] — 2026-04-22

**Headline:** Plan Mode — the model can propose a markdown plan
autonomously for large tasks (multi-file refactors, architecture
changes, ambiguous requests), and you can also force a read-only
exploration phase via `/plan`. Picker shows Approve / Refine / Cancel.
Approve pushes a synthetic "implement now" message; Refine keeps the
model exploring; Cancel drops the plan. Designed around Pillar 1 —
tool specs stay pinned, so the cache prefix doesn't break when plan
mode toggles.

### Added

- **`submit_plan` tool** (`src/tools/plan.ts`) — registered by default
  in `reasonix code`. Throws `PlanProposedError` carrying the plan
  text via the new `toToolResult()` protocol on ToolRegistry. Fires
  the picker whether or not plan mode is active — the model is
  expected to propose plans on its own for large tasks; `/plan` is
  the *stronger* constraint that forces the model into read-only.
- **`/plan` slash** (code mode only) — toggles read-only plan mode.
  `/plan on`, `/plan off`, or `/plan` to flip. While on, the registry
  refuses non-read-only dispatch; while off, the model can still
  propose plans autonomously via submit_plan. `/status` surfaces the
  state; `StatsPanel` shows a red `PLAN` tag.
- **`/apply-plan` slash** (code mode only) — force-approve fallback.
  Clears plan mode, clears the pending-plan picker state, and
  resubmits the implement-now synthetic via the existing `resubmit`
  mechanism. Useful when the model wrote the plan in assistant text
  instead of calling submit_plan, or when you want to keyboard-only
  the approval without the picker.
- **`ToolDefinition.readOnly` + `readOnlyCheck`** — declarative gate
  used by `ToolRegistry.dispatch` when plan mode is on. Read tools
  (`read_file`, `list_directory`, `search_files`, `directory_tree`,
  `get_file_info`, `web_search`, `web_fetch`) run normally. Write
  tools bounce with a refusal the model reads and learns from.
  `run_command` uses a dynamic `readOnlyCheck` so allowlisted
  invocations (`git status`, `cargo check`, `npm test`, `grep`, …)
  still work during planning — exploration isn't gated. Non-allowlisted
  commands refuse just like other writes.
- **`ToolRegistry.setPlanMode(on)` / `.planMode`** — the enforcement
  switch + accessor. Mirrored onto the UI's `planMode` React state so
  the StatsPanel badge stays in sync.
- **`toToolResult()` extension protocol** on Error subclasses —
  `ToolRegistry.dispatch` calls it if present when an error is thrown,
  serializing custom fields alongside `error`. Used by
  `PlanProposedError` to ferry the plan text to the UI without
  regex-scraping the error message. Falls back safely on serialization
  failure.
- **`PlanConfirm.tsx`** — 3-option Ink picker (Approve / Refine /
  Cancel) with the plan rendered as **live Markdown** (via the
  existing `Markdown` component — headings, lists, code, bold all
  formatted, not raw text) in a cyan-bordered panel above. 2 400-char
  rendered cap; longer plans get a "use /tool for full" truncation
  marker. Live rows hidden while the picker is up, matching
  `ShellConfirm`'s behavior. When the plan contains headings like
  "Open questions", "Risks", "Assumptions", "待确认", "开放问题", "风险",
  "未知", "假设", "不确定", the picker auto-selects the Refine option
  by default and shows a yellow "▲ the plan has open questions —
  pick Refine to answer them" hint above the options.
- **`PlanRefineInput.tsx`** — inline text input that appears after
  the user picks either **Approve** or **Refine**. Picking Approve
  lets the user type last-minute instructions or answers to the
  model's open questions (blank Enter = approve as-is). Picking
  Refine requires specifics — the input collects them and includes
  them verbatim in the synthetic sent to the model, so "refine"
  actually means "revise with this feedback" instead of the generic
  "try again" message the first cut sent. Esc returns to the picker
  without resuming the loop.
- **System-prompt guidance** (`CODE_SYSTEM_PROMPT`) — teaches the
  model when to call submit_plan autonomously (big / risky / ambiguous
  tasks) vs. just making the change (typos, obvious one-line fixes),
  and how `/plan` mode adds the stronger dispatch gate on top.

### Tests (+24, suite 542→566)

- `tests/plan.test.ts` (+17) — ToolRegistry plan-mode gate
  (default-off, toggle, block non-read-only, allow read-only, honor
  `readOnlyCheck` per-args, precedence over `readOnly`, off-mode
  noop); `toToolResult` protocol (serializes custom fields, falls
  back on serializer failure); `PlanProposedError` carries plan +
  STOP directive; `registerPlanTool` registers submit_plan as
  read-only, fires picker both in and out of plan mode, rejects
  empty plans, trims whitespace.
- `tests/slash.test.ts` (+7) — `/plan` registry entries + required
  commands check; `/plan` toggle / on / off / true / false / 0 / 1;
  `/plan` info text explicit about the stronger-constraint
  relationship; `/apply-plan` code-mode gating; `/apply-plan` flips
  mode + clears pending + resubmits; works without optional
  `clearPendingPlan` callback; `/status` plan-mode line appears
  iff on.

### Internals

- `src/tools/filesystem.ts` — read_file / list_directory /
  directory_tree / search_files / get_file_info tagged readOnly.
- `src/tools/shell.ts` — run_command gets `readOnlyCheck` tied to
  the existing `isAllowed` check + `allowAll` escape hatch.
- `src/tools/web.ts` — web_search / web_fetch tagged readOnly.
- `src/cli/commands/code.tsx` — `registerPlanTool(tools)` added after
  the filesystem and shell registrations so the tool is always in
  the pinned spec list (prefix cache stays stable across
  plan-mode toggles).
- `src/index.ts` — re-exports `PlanProposedError`, `registerPlanTool`,
  `PlanToolOptions` for library consumers.

---

## [0.4.17] — 2026-04-22

**Headline:** Project memory — drop a `REASONIX.md` in your project
root and its contents are pinned into the immutable-prefix system
prompt for every session in that directory. Persistent project
context (house conventions, domain glossary, gotchas the model keeps
forgetting) without eating per-turn context budget, and the prefix
cache stays warm as long as the file is stable.

### Added

- **`src/project-memory.ts`** — `readProjectMemory(rootDir)`,
  `applyProjectMemory(basePrompt, rootDir)`, `memoryEnabled()`. One
  source, one mental model: `REASONIX.md` at the project root, read
  once at session start, appended as a fenced "# Project memory"
  block after the base system prompt. Truncates at 8 000 chars
  (≈ 2k tokens) with a visible marker; `.gitignore` gets 2 000
  because it's a constraint dump, memory gets more headroom because
  it's deliberate instructions. Re-exported from `src/index.ts` for
  library consumers.
- **Auto-applied at every CLI entry** — top-level `reasonix`,
  `reasonix chat`, `reasonix run`, and `reasonix code` all honor
  the file. `code` resolves it against the rooted directory; the
  others against `process.cwd()` at launch.
- **`/memory` slash command** — prints the resolved file path +
  full contents (or a how-to stub when absent), so you can verify
  what the model is actually seeing without reading the system
  prompt blob. Reminds you changes take effect on the next launch
  or `/new`; the system prompt is hashed once per session to keep
  the prefix cache warm.
- **`REASONIX_MEMORY=off|false|0` env opt-out** — for CI or
  intentional offline reproducibility. `rm REASONIX.md` is the
  other opt-out.

### Tests (+25, suite 517→542)

- `tests/project-memory.test.ts` (+15) — absent / empty /
  whitespace-only / normal / oversized file paths;
  `memoryEnabled` env-value matrix; `applyProjectMemory` no-ops on
  missing/disabled; determinism (identical input ⇒ identical
  output, cache-prefix-safe); `codeSystemPrompt` stacks base →
  memory → .gitignore in the right order when all three exist.
- `tests/slash.test.ts` (+4) — `/memory` prints the how-to when no
  file, contents when present, "disabled" when env-off, "no root"
  when `memoryRoot` is absent from the SlashContext. Registry
  check updated to require `/memory`.

---

## [0.4.16] — 2026-04-22

**Headline:** Native `run_command` shell tool so the model can run
its own tests and verify its work (Claude Code / Aider parity).
3-choice picker for every unknown command — "run once", "always
allow in this project" (persists to `~/.reasonix/config.json`), or
"deny". Plus a session picker on startup so `reasonix code` stops
silently resuming the last conversation, and a Windows backspace fix.

### Added

- **`src/tools/shell.ts`** — `run_command(command, timeoutSec?)`
  registered by default in `reasonix code`. Read-only / testing
  commands (`git status`, `ls`, `cat`, `grep`, `rg`, `npm test`,
  `pytest`, `cargo test`, `cargo check`, `cargo clippy`, `go test`,
  `deno test`, `bun test`, `ruff`, `mypy`, `npx tsc --noEmit`,
  `npx biome check`, language `--version` probes) auto-run. Anything
  else goes through the ShellConfirm picker. 60s default timeout,
  32k-char output cap. `shell: false` in the child_process spawn
  so the model can't pipe / redirect / chain its way past the
  allowlist.
- **`src/cli/ui/ShellConfirm.tsx`** — 3-option SingleSelect modal
  that renders when the model asks to run a non-allowlisted
  command. Borders + color so it's impossible to miss. Arrow-key
  navigation; Enter confirms. No `y/n` hotkey — too easy to trigger
  by accident mid-typing.
- **`src/cli/ui/SessionPicker.tsx`** — on `reasonix chat` /
  `reasonix code` startup, if the session has prior messages, show
  a 3-option picker: **New** (default, safer), **Resume** (continue
  where you left off), **Delete and start new**. Flags `--resume`
  / `--new` bypass the picker for CI / muscle-memory.
- **Per-project persistent allowlist** — `config.projects[<abs>].shellAllowed`
  stores prefixes the user approved via "always allow". On next
  `reasonix code` in that dir they auto-run. Helpers
  `loadProjectShellAllowed` / `addProjectShellAllowed` exported.

### Fixed

- **Backspace dead on some Windows terminals.** Certain Git Bash /
  winpty combos report plain Backspace with `key.delete=true` and
  `key.backspace=false`; the 0.4.15 cursor reducer split the two
  and treated `delete` as forward-delete, which is a no-op when the
  cursor is at the end of the buffer — so pressing Backspace did
  nothing and Ctrl+Backspace (reported differently) was the only
  way to delete. Now both flags collapse to backward-delete, plus
  raw DEL (0x7f) and BS (0x08) bytes in `key.input` are honored as
  backspace too.

### Tests (+43, suite 474→517)

- `tests/shell-tools.test.ts` (+27) — tokenizer (quoting, escapes,
  unclosed-quote rejection); allowlist matching (exact / prefix /
  whitespace normalization / extras); `runCommand` against real
  child processes (stdout, stderr, cwd, timeout kill, output cap,
  empty-command rejection); registry dispatch (auto-run, refusal
  via `NeedsConfirmationError`, `allowAll: true` bypass);
  `formatCommandResult`; `NeedsConfirmationError` name/message
  invariants (no stale `/apply-shell` reference).
- `tests/shell-confirm.test.ts` (+4) — `derivePrefix` picks one or
  two tokens based on known wrappers and normalizes whitespace.
- `tests/config.test.ts` (+3) — `loadProjectShellAllowed` defaults
  to `[]`; `addProjectShellAllowed` persists and dedups per-project;
  ignores empty prefixes.
- `tests/multiline-keys.test.ts` (+2) — raw DEL/BS bytes are
  treated as backspace; `key.delete` unified with `key.backspace`.

---

## [0.4.15] — 2026-04-22

**Headline:** Web search + fetch tools (on by default, zero
configuration) plus real cursor editing in the prompt box (←/→,
Backspace/Delete mid-string, multi-line ↑/↓ navigation).

### Fixed

- **PromptInput was append-only** — cursor was always pinned to
  the end of the buffer, so the only way to fix a typo was
  backspacing back through everything after it. Now:
  - `←` / `→` move the cursor one column (clamped to buffer).
  - `↑` / `↓` move across lines in a multi-line buffer, preserving
    column when possible, clamping when the target line is shorter.
  - `Ctrl+A` / `Ctrl+E` jump to start / end of the current line.
  - `Backspace` deletes the char before the cursor; `Delete`
    deletes the char under the cursor.
  - Printable input inserts at the cursor (including multi-char
    paste bursts).
  - `Shift+Enter` / `Ctrl+J` insert a newline at the cursor.
- **History recall no longer steals arrow keys from mid-edit.**
  `↑` / `↓` only trigger prior-prompt recall when the buffer is
  empty. A non-empty buffer keeps the arrows for cursor motion so
  typed text isn't clobbered.

### Added

Web search + fetch tools are registered by default on `reasonix
chat` and `reasonix code`. The model calls `web_search` /
`web_fetch` on its own whenever a question needs fresher info than
its training data. Backed by **Mojeek**'s public search page — no
API key, no signup. Same Cache-First + repair + context-safety
plumbing as every other tool.

Implementation note: the first cut of this feature used DuckDuckGo,
but a live probe from the dev machine confirmed DDG now serves
HTTP 202 anti-bot pages for every unauthenticated POST regardless
of UA. Mojeek is an independent-index engine that's been stable
against the same probe (3/3 success on three queries spaced 3s
apart). Real-browser `User-Agent` string avoids Mojeek's
fast-path scraper filter.


- **`src/tools/web.ts`** — two functions + one registration helper:
  - `webSearch(query, opts?)` — fetches DDG's HTML endpoint, parses
    ranked results (title + url + snippet). `topK` is clamped to
    [1, 10]. Parser decodes DDG's `uddg=<url>` redirect wrapper and
    common HTML entities.
  - `webFetch(url, opts?)` — HTTP GET + HTML-to-text extraction
    (scripts/styles/nav/footer/aside/svg stripped, paragraph breaks
    preserved, entities decoded). 15s timeout, 32k-char cap (matches
    tool-result budget), forwards caller's AbortSignal so Esc during
    a long fetch is honored.
  - `registerWebTools(registry, opts?)` — registers both as
    ToolRegistry entries the model can invoke. Tool descriptions
    guide the model to call search whenever training data might be
    stale.
- **`ReasonixConfig.search`** + **`searchEnabled()`** — a simple
  boolean. Default on. Turn off with `search: false` in config or
  `REASONIX_SEARCH=off|false|0` in env. No API keys, no provider
  picker — one switch.
- **Auto-registered in chat/code.** `reasonix chat` and
  `reasonix code` register `web_search` + `web_fetch` by default.
  Zero setup: after the normal wizard, the model can already reach
  the web.

### Tests (+18, suite 444→462)

- `tests/web-tools.test.ts` (+13) — htmlToText strips
  scripts/styles/nav/footer + decodes entities + collapses
  whitespace; `parseDuckDuckGoResults` decodes redirect URLs + entities
  + returns empty on unexpected markup; `webSearch` hits the DDG
  endpoint with a browsery UA, respects topK, clamps to [1, 10],
  throws on non-2xx; `formatSearchResults` renders the expected
  layout; `registerWebTools` registers both verbs; `web_fetch` refuses
  non-http(s) URLs; `webFetch` extracts title + body, truncates at
  the cap with a visible marker, surfaces 404s.
- `tests/config.test.ts` (+5) — `searchEnabled` defaults to true;
  honors `search: false` in file; honors `REASONIX_SEARCH=off|false|0`;
  stays true for unrelated env values; env off beats config true.

---

## [0.4.14] — 2026-04-22

**Headline:** Render-load reductions for Windows terminals where
Ink's cursor-up repaint leaves ghost artifacts (winpty / MINTTY /
Git Bash). No single bug fix — a set of pressure reductions plus an
explicit opt-out for the terminals where nothing else helps.

### Fixed

- **`patchConsole: false`** on every `render()` call (chat, setup,
  replay, diff). We never log to console during the TUI, so the
  patch was pure overhead and a known redraw-glitch source on
  wrapped-ANSI terminals.
- **Consolidated every animated component onto a single 120ms tick.**
  Previously `Pulse` (500ms), `Elapsed` × 2 (1000ms each), `StatusRow`
  (120ms + 1000ms), `OngoingToolRow` (120ms + 1000ms), and
  `PromptInput` cursor blink (500ms) each owned a private
  `setInterval`. On a streaming turn that's 6-10 uncoordinated
  re-render sources firing into Ink's patch loop. New
  `TickerProvider` / `useTick` / `useElapsedSeconds` in
  `src/cli/ui/ticker.tsx` collapses all of them to one shared
  counter — same visible behavior, ~5× fewer React re-renders per
  second.
- **Flush interval 60ms → 100ms.** 10 Hz still feels live while
  giving slow terminals more headroom per repaint. The prior 60ms
  rate queued patches faster than some Windows terminals could
  process them, manifesting as visible duplicates in scrollback.
- **`reasonix --version` no longer reports 0.4.3 forever.** The
  hardcoded `VERSION` in `src/index.ts` had been stale since April
  21; now matches `package.json`.

### Added

- **`REASONIX_UI=plain` env opt-out.** Suppresses every transient
  row in the render tree (streaming preview, ongoing-tool spinner,
  status line, processing fallback) AND disables the ticker
  entirely. Only `<Static>` committed events + the input prompt are
  drawn. Trades liveness for stability; use when the default TUI
  produces ghost rendering on your terminal.

---

## [0.4.13] — 2026-04-22

**Headline:** Two streaming-row bugs that made `reasonix code` feel
broken: the spinner froze for the entire duration of a large
`edit_file` call, and multi-iteration turns displayed the previous
iteration's body text concatenated into the next one.

### Fixed

- **Streaming row no longer freezes during a large tool-call.** When
  the model streams `tool_calls[].function.arguments` (kilobytes of
  SEARCH/REPLACE for a big `edit_file`) there are zero `content` or
  `reasoning_content` bytes, so the label sat on "writing response ·
  N chars" untouched — indistinguishable from a hung network. The
  loop now yields a new `tool_call_delta` event carrying the growing
  cumulative argument-char count, and the TUI surfaces it either as
  a dedicated "assembling tool call <name> · N chars of arguments"
  phase (magenta) when content/reasoning are empty, or as an extra
  segment on the "writing response" line when content is also
  streaming.
- **Multi-iteration turns no longer concat prior iterations' text
  into the next row.** A single `handleSubmit` can span N iterations
  (each tool_call loops us around the model), and the streaming
  buffer wasn't reset between them. If an iteration returned empty
  content (pure tool_calls), the historical entry fell back to the
  streaming-buffer's accumulated text — yielding an assistant block
  that read like a concatenation of every prior iteration's reply.
  Fix: clear `streamRef.text` / `.reasoning` / `.toolCallBuild` and
  the per-flush buffers on every `assistant_final`.
- **Unique `<Static>` key per iteration.** A single turn's multiple
  assistant_final events used to share one React key, which Ink
  dedupes; the iteration counter fixes it.

### Added

- `LoopEvent` role `tool_call_delta` with field `toolCallArgsChars`
  (cumulative arguments-string length for the call being assembled).
  Useful for any UI consumer, not just the TUI.

### Tests (+1, suite 443→444)

- `tests/loop.test.ts` — new streaming test: fake SSE body streams a
  tool_call across multiple chunks; asserts `tool_call_delta` events
  carry a strictly-growing `toolCallArgsChars` and that the id-only
  opener (name still empty) does not emit an event.

---

## [0.4.12] — 2026-04-22

**Headline:** Bulletproof tool_calls ↔ tool pairing so corrupted
session files can't keep 400ing forever. Auto-compact attempt
before forcing summary on context-guard so a single oversized
turn doesn't eat your entire session.

### Fixed

- **DeepSeek 400 "insufficient tool messages following tool_calls"**
  after a forced-summary on context-guard. Root cause: the loop
  appended `assistant.tool_calls` and then bailed to summary BEFORE
  dispatching the tools, leaving the log in a shape DeepSeek's API
  validator rejects. Fix: strip the dangling tail before calling
  summary, and defensively validate at every `buildMessages` call.
- **DeepSeek 400 "tool must be a response to a preceding tool_calls"**
  when typing anything after the above error. Root cause: partial
  fixes left stray tool messages or half-matched tool_calls in the
  log. Fix: `healLoadedMessages` now runs a full pairing validator
  — any `assistant.tool_calls` whose response set is incomplete is
  dropped along with its partial responses; any stray tool message
  is dropped. Runs on session load (with disk rewrite to persist the
  heal) AND on every outgoing API call (defensive).
- **Auto-compact before forcing summary** on context-guard trip.
  Previously the loop immediately forced a summary at 80% context —
  users lost a full turn of work. Now it first tries shrinking
  oversized tool results; if that drops enough tokens, the turn
  continues normally and the user can keep asking. Falls back to
  forced summary only when compaction has nothing to shrink.
- **`CacheFirstLoop.compact()` no longer strips structural tail** —
  split the "shrink oversized tool payloads" concern out from the
  full load-time heal. `/compact` during a live session only
  shrinks, never touches tool_calls/tool pairing (those edges are
  legitimate mid-turn state).

### Internals

- New exported `shrinkOversizedToolResults(messages, cap)` for the
  shrink-only concern. `healLoadedMessages` now composes
  `shrinkOversizedToolResults` + the full pairing validator.
- Session load heal now rewrites the session file on disk when
  anything was healed, so the damage doesn't re-surface every
  restart.

### Tests (+5, 4 reshaped, suite 436→443)

- `tests/loop-error.test.ts` (+5) — `healLoadedMessages` drops a
  stray tool without preceding tool_calls; drops an
  assistant.tool_calls whose response set is incomplete; 4 existing
  tests reshaped to use valid tool_call pairings (stray tools now
  correctly get pruned by the validator).
- `tests/loop.test.ts` (+2) — context-guard auto-compacts oversized
  tool results and continues instead of forcing summary; dangling
  assistant-with-tool_calls tail stripped defensively at
  buildMessages time.

---

## [0.4.11] — 2026-04-22

**Headline:** Real git-diff-style output for `edit_file`, `/new`
command that actually drops context (unlike `/clear`), clearer
phase labels on the streaming row.

### Added

- **LCS line-level diff for `edit_file`** — unchanged lines now
  render as ` ` context (dim), removed as `-` (red), added as `+`
  (green). Previously a one-line search with a multi-line replace
  would show the unchanged line as both `-` and `+`, which was
  just noise.
- **Git-style hunk header** (`@@ -42,1 +42,4 @@`) above each
  `edit_file` diff showing where in the file the change lands and
  how many lines it affects. Matches the `git diff` convention.
- **`edit_file` results never truncated** in the EventLog. Other
  tools keep the 400-char clip + `/tool N` escape, but edit diffs
  always show the full change so `/apply` decisions are informed.
- **`/new` slash command** (alias `/reset`) that drops the
  in-memory message log AND rewrites the session file to empty.
  Unlike `/forget` (deletes the session), `/new` keeps the session
  name, model, and config — just starts a fresh conversation.
  `CacheFirstLoop.clearLog()` is the backing public API.
- **Clearer streaming-row phase labels** — replaced the cryptic
  "streaming · 391 + think 4506 chars" with explicit state text:
  - yellow "request sent · waiting for server" pre-first-byte
  - cyan "R1 reasoning · N chars of thought" during reasoning-only
  - green "writing response · N chars · after M chars of reasoning"
    during content phase. Colored so the eye catches the phase at
    a glance instead of decoding dim text.

### Changed

- **`/clear` now advertises what it does NOT do** — users kept
  expecting it to clear context. It still clears only the visible
  scrollback, but the returned info line now says so explicitly
  and points at `/new` for context drop.
- App.tsx now renders the info line from a clear-plus-info slash
  result (previously `clear: true` short-circuited and ate any
  accompanying message).

### Tests (+8, suite 427→436 — some existing `/clear` test adjusted for new info output)

- `tests/filesystem-tools.test.ts` (+3) — `edit_file` returns a
  proper LCS diff with context lines (user's real case of one-line
  search + multi-line replace no longer double-counts); git-style
  `@@` hunk header with starting-line number from the original
  file.
- `tests/filesystem-tools.test.ts` — dedicated `lineDiff` test
  block (+5) covering pure insertion, pure deletion, substitution
  order (-/+ matches git-diff convention), identical-arrays as
  all-context, empty-search all-additions, the user-reported real
  case.
- `tests/slash.test.ts` (+3, 1 changed) — `/new` drops log + clears
  scrollback; `/reset` alias; `/help` distinguishes `/clear` vs
  `/new`; `/clear` now surfaces an explanatory info line.

---

## [0.4.10] — 2026-04-22

**Headline:** Fills the "silent wait" gaps users were hitting —
transient status indicator between iterations + before harvest, live
stats refresh per iter (not per turn), account balance cell,
in/out cost split, Esc now interrupts harvest too, `edit_file`
returns a real diff. Drops the misleading "vs Claude / saving"
numbers.

### Added

- **`status` loop event** + `StatusRow` component — a magenta
  spinner row that fills silent phases with explicit text:
  - `"thinking about the tool result…"` between iterations, while
    R1 reasons about a just-finished tool output before emitting
    the next turn's first streaming byte
  - `"extracting plan state from reasoning…"` right before the
    silent harvest round-trip (1-10s on the cheap model)
  - `"summarizing what was gathered…"` before the forced-summary
    call (budget / context-guard)
  Auto-clears on the next primary event.
- **Account balance cell** in the stats panel. `DeepSeekClient.getBalance()`
  hits `/user/balance` (separate endpoint, no billing impact).
  Fetched at launch + refreshed after each completed turn. Hides
  the cell on failure so the session works without it.
- **Input / output cost split** — panel now reads
  `cost $X (in $Y · out $Z)` so users can see where their spend
  lands without guessing. `SessionSummary` gains `totalInputCostUsd`
  and `totalOutputCostUsd`; `inputCostUsd()` and `outputCostUsd()`
  exposed as library utilities.
- **Inline diff in `edit_file` tool result** — every edit returns a
  unified-style `- old / + new` block so you can see *what* changed
  without running `git diff`. Long blocks are truncated in the
  spinner row with a `… (N more lines)` marker; `/tool N` still
  shows the full result.
- **Live stats refresh per assistant_final** — previously the
  panel only updated in the `finally` block at end-of-turn;
  multi-iter tool chains stayed frozen at the prior turn's numbers
  for 30-60s at a time. Now the cost/ctx/cache hit gauges update
  as each iteration's usage is recorded.
- **Stronger pre-first-byte hint** — streaming row now reads
  `(request sent · waiting for server)` with a concrete estimate,
  replacing the ambiguous `(streaming · 0 chars)`.

### Changed

- **Esc now also interrupts `harvest()`.** The cheap-model
  round-trip that extracts plan state was the last remaining
  un-signaled API call. Threaded `AbortSignal` through. Fast-path
  returns `emptyPlanState` when the signal is already aborted so
  the caller unblocks without a network burn.

### Removed

- **"vs Claude / saving" cells from the panel.** The savings
  percentage was a synthetic ratio against static Claude pricing,
  not a measured comparison — users fairly pointed out it reads
  like made-up marketing. The summary shape still carries
  `claudeEquivalentUsd` + `savingsVsClaudePct` for benchmark /
  replay compat but they're deprecated and no longer surfaced in
  chat.

### Also added in 0.4.10 (same release)

- **GFM markdown tables** in assistant output. `parseBlocks` now
  recognizes `| col | col |` + separator + data rows and renders
  them as aligned columns with `│` dividers. Handles alignment
  colons (`:---`, `---:`), escaped pipes, and leading-pipe-free
  variants. CJK-width-aware column padding so Chinese and English
  tables both align correctly.
- **"processing…" fallback indicator** — if the loop is busy but
  none of the targeted indicators (streaming row, ongoingTool,
  statusLine) are visible, a generic magenta spinner row fills the
  gap. Belt-and-suspenders: no more silent clock-ticks.
- **Clearer between-iter status wording** — changed from "thinking
  about the tool result…" (which sounded like a model-only phase)
  to "tool result uploaded · model thinking before next response…"
  so it's obvious the wait covers both the upload round-trip and
  the model's thinking time.

### Tests (+11, suite 416→427)

- `tests/telemetry.test.ts` (+4) — `inputCostUsd` covers cache-hit
  + cache-miss but not completion; `outputCostUsd` covers
  completion only; both return 0 for unknown models;
  `totalInputCostUsd + totalOutputCostUsd == totalCostUsd`.
- `tests/filesystem-tools.test.ts` (+2) — `edit_file` returns an
  inline `- search / + replace` diff; huge edit blocks get
  `… (N more lines)` marker in the middle.
- `tests/markdown.test.ts` (+5) — simple table with CJK header +
  cells, alignment-colon separators accepted, pipe-less headers
  accepted, bare `|` in prose doesn't false-trigger, escaped `\|`
  preserved inside cells.

---

## [0.4.9] — 2026-04-22

**Headline:** Three user-reported issues fixed together: Esc now
really stops (not "after the tool finishes"), `reasonix code` drops
the filesystem MCP subprocess for native tools with an R1-friendly
`edit_file` shape, and the placeholder cursor renders in the right
place. Plus a `slow_count` demo tool so progress bars are testable.

### Changed

- **Esc is now an immediate cancel**, not "cancel at the next iter
  boundary." The loop now threads an AbortController through every
  I/O path it can:
  - `DeepSeekClient.chat`/`.stream` already accepted `signal` — now
    wired at every call site (normal turn, branch sampling, forced
    summary), so Esc closes the HTTP/SSE stream immediately.
  - `ToolRegistry.dispatch` accepts `{ signal }` and passes a
    `ToolCallContext` to the tool's `fn`. Existing tools that don't
    consume the ctx keep working.
  - `McpClient.callTool({ signal })` sends an MCP
    `notifications/cancelled` for the in-flight request AND rejects
    the pending promise right away — no "wait for subprocess."
    Late responses are swallowed by `dispatch` because the id is
    already gone from `pending`.
  - `bridgeMcpTools` forwards `ctx.signal` straight into
    `client.callTool`, so MCP tools inherit the cancellation path.
- **Built-in filesystem tools** replace the
  `@modelcontextprotocol/server-filesystem` subprocess inside
  `reasonix code`. Ten tools — `read_file` (head/tail), `write_file`,
  `edit_file` (flat SEARCH/REPLACE, not the JSON-in-string array
  shape that triggered R1 DSML hallucinations), `list_directory`,
  `directory_tree`, `search_files`, `get_file_info`,
  `create_directory`, `move_file`. Sandbox enforcement on every
  path. New CLI output: `▸ reasonix code: … · 10 native fs tool(s)`.
  Library API: `registerFilesystemTools(registry, { rootDir })`.
  `ChatOptions` gains `seedTools: ToolRegistry` so callers can
  pre-register tools and still bridge MCP on top.

### Fixed

- **Placeholder cursor now renders at position 0**, not after the
  dimmed hint text. Matches "you're about to type here," not "you
  typed the placeholder." Only affects the empty-input view; when
  there's real content the cursor still follows the last char.

### Added

- **`slow_count` demo tool** in `examples/mcp-server-demo.ts` that
  emits real `notifications/progress` frames (1/N, 2/N, …) with
  300 ms pauses. Progress-bar plumbing from 0.4.8 is now testable
  end-to-end: `reasonix chat --mcp "demo=node --import tsx examples/mcp-server-demo.ts"` then ask the model to
  "please use slow_count to count to 5" → bar fills in the spinner.
- **`ToolCallContext`** public type (`{ signal?: AbortSignal }`),
  passed to every tool's `fn`. Re-exported from `src/index.ts`.

### Tests (+29, suite 387→416)

- `tests/filesystem-tools.test.ts` (new, +26) — read/write/edit
  happy paths, head/tail line selection, truncation on oversize,
  directory refusal, sandbox escape rejection (both relative `../`
  and absolute `/etc/…`), search case-insensitivity, empty-result
  formatting, `edit_file` multi-match refusal, move across dirs,
  `create_directory` idempotence, `allowWriting: false` trims the
  write-side tool set.
- `tests/mcp.test.ts` (+3) — AbortSignal rejects the pending
  promise, emits `notifications/cancelled` with the correct id,
  rejects immediately when called with an already-aborted signal.

---

## [0.4.8] — 2026-04-21

**Headline:** MCP progress notifications — long-running tool calls
now stream incremental progress into the spinner row instead of
sitting silent for minutes. "▸ tool\<fs_scan\> running… 42s" grows
to "[█████░░░░░░░░░░░░░░░] 500/2000 25%  reading src/…"  as the
server reports.

### Added

- **`McpClient.callTool(name, args, { onProgress })`** — attaches
  a fresh `_meta.progressToken` per call; server-emitted
  `notifications/progress` frames are routed to the handler until
  the final response arrives. Handler is dropped on completion or
  timeout — no leaks, late frames are silently swallowed.
- **Dispatch routing for `notifications/progress`** in the client's
  reader loop. Other server-initiated notifications are still
  dropped (list_changed frames not implemented yet).
- **`bridgeMcpTools({ onProgress })`** — pipes the per-call
  callback through to bridged tools. The info object includes the
  *registered* (prefix-applied) tool name so multi-server UIs can
  attribute progress correctly.
- **Progress bar in `OngoingToolRow`** — when a frame arrives with
  `total`, renders `[███░░░░░░] n/total pct%  message`. Without
  `total`, falls back to `progress: n  message`. Resets on each
  new tool call so stale progress doesn't linger.
- **Public types in `src/mcp/types.ts`**: `McpProgressHandler`,
  `McpProgressInfo`, `ProgressNotificationParams`. Re-exported
  from `src/index.ts` for library consumers.

### Tests (+5, suite 382→387)

- `tests/mcp.test.ts` (+5) — progress frames routed to onProgress
  in order; `_meta.progressToken` omitted when no callback is
  given; distinct token when present; late frames after resolution
  silently swallowed; `bridgeMcpTools` forwards progress with the
  prefixed tool name.

---

## [0.4.7] — 2026-04-21

**Headline:** Multi-line input in the chat TUI. Paste a code block
without it getting chopped on the first newline; compose structured
prompts across multiple lines; still hit Enter once to send.

### Added

- **Multi-line prompt input** replacing the old single-line
  `ink-text-input`. Newline-insertion paths, in order of terminal
  reliability:
  - `Ctrl+J` — universal (real ASCII LF), works on every terminal
  - `Shift+Enter` — works on terminals that enable CSI-u modifier
    reporting (iTerm2 with that setting on, WezTerm, Ghostty, etc.)
  - `\<Enter>` — bash-style line continuation, always works as a
    portable fallback
  - Pasted multi-line text lands intact instead of submitting on
    the first embedded `\r`.
- **Visible blinking cursor** on the active line so the input box
  looks alive even when you stop typing mid-compose.
- **`processMultilineKey` pure reducer** in `src/cli/ui/multiline-keys.ts`.
  Keystroke → action function that's fully unit-testable; the
  React component is a thin wrapper. Parent-owned keys (Tab for
  slash auto-complete, ↑/↓ for slash-nav + history, Esc for abort,
  left/right/page arrows) are no-ops in the reducer so the buffer
  never eats a stray control sequence when both parent and child
  `useInput` fire on the same event.

### Design notes

- No mid-string insertion cursor. Edits are cursor-at-end (backspace
  to delete, paste to insert). Matches how readline-in-raw-mode
  feels, covers ~95% of prompt-composition cases, and skips a pile
  of complexity (arrow-key cursor nav, selection, kill/yank) that
  would collide with the parent's arrow-key handling for slash-nav
  and history recall.
- `ink-text-input` is still used by `Wizard`, `Select`, `Setup` — it
  fits those single-line forms fine and didn't need replacing.

### Tests (+18, suite 364→382)

- `tests/multiline-keys.test.ts` (new) — printable input, multi-char
  paste, Enter-submit, Shift+Enter-newline, Ctrl+J (raw LF and
  normalized `ctrl+'j'`), bash continuation, backspace across
  newlines, delete, tab/arrows/esc/ctrl-letter/meta all ignored,
  empty-buffer edge cases.

---

## [0.4.6] — 2026-04-21

**Headline:** Slash-command UX overhaul + MCP discovery closes in
two places. Typing `/` now pops an IntelliSense-style suggestion
list you can walk with ↑/↓ and pick with Enter or Tab — no more
memorizing commands or reading a cluttered footer. The footer is
gone. `/mcp` inside chat now shows each server's tools + resources
+ prompts in one grouped view. For scripting/CI there's a new
`reasonix mcp inspect <spec>` CLI doing the same.

### Added

- **Slash autocomplete popup.** When the input starts with `/` and
  matches exist, a floating panel lists commands (name + args hint
  + one-line summary). ↑/↓ navigate the list; Tab inserts the
  highlighted name into the input; Enter runs it directly. Leaves
  slash mode the moment you type a space — then ↑/↓ goes back to
  shell-style prompt history as before. Registry lives in
  `SLASH_COMMANDS` and gates code-mode-only entries (`/apply`,
  `/discard`, `/undo`, `/commit`) behind the TUI's `codeMode` flag.
- **`/mcp` is now the discovery view.** Rich output per connected
  server: name + version + spec, tool count, resources list, prompts
  list. Unsupported sections collapse to `(not supported)` so a
  tools-only server still reads clean. Inspection happens once at
  chat startup and flows through `SlashContext.mcpServers` — the
  slash handler stays sync.
- **`reasonix mcp inspect <spec>`**. CLI counterpart to `/mcp`, for
  running outside chat (CI, scripting, "does this server even
  work?"). Same spec grammar as `--mcp`; `--json` emits the full
  report.
- **`inspectMcpServer(client)`** public API in `src/mcp/inspect.ts`.
  Pure function — testable against any `McpClient` instance; returns
  an `InspectionReport` with per-section `{supported, items}` or
  `{supported: false, reason}`. Re-exported from `src/index.ts`.
- **`McpClient.serverInfo` + `.protocolVersion` + `.serverInstructions`**.
  The full initialize handshake result is now exposed, not just
  `.serverCapabilities`. Needed by any UI that wants to surface
  "connected to X v1.2.3".

### Removed

- **Static command-strip footer under the input.** Took 3-4 dimmed
  lines listing a random subset of commands; superseded by the
  on-demand slash popup that only surfaces when the user asks for
  it (by typing `/`).

### Tests (+11, suite 353→364)

- `tests/mcp-inspect.test.ts` (new, +5) — full-support server,
  -32601 → `supported: false`, non-32601 forwarded as the section
  reason, serverInfo/protocolVersion/instructions accessors,
  undefined-instructions fallback.
- `tests/slash.test.ts` (+6) — `SLASH_COMMANDS` contains every
  handler case, `suggestSlashCommands` prefix + case + empty-string
  behavior, code-mode gating, `/mcp` rich view renders tools +
  resources + prompts grouped per server, `/mcp` spec-only fallback.

---

## [0.4.5] — 2026-04-21

**Headline:** Two protocol-level completions bundled together. (1)
DSML-hallucinated tool calls are now **recovered** (not just stripped
from display) — when R1 emits its chat-template markup in the content
channel instead of the proper `tool_calls` field, the repair pipeline
parses it back into a real ToolCall and executes it. (2) The MCP
client gains `resources/*` and `prompts/*` — the remaining method
families needed for spec parity beyond tools.

### Added

- **DSML invoke parser in `scavengeToolCalls`.** Pattern A in
  `src/repair/scavenge.ts` now recognizes `<｜DSML｜invoke name="X">…</｜DSML｜invoke>` blocks with nested `<｜DSML｜parameter name="k" string="true|false">v</｜DSML｜parameter>` children. `string="true"` → literal; `string="false"` → JSON. Both full-width `｜` and ASCII `|` variants accepted. Malformed JSON under `string="false"` falls back to a literal string so data isn't lost.
- **Content-channel scavenge.** `ToolCallRepair.process` now takes an
  optional third arg `content` and scans both reasoning + content for
  leaked calls. The loop wires `assistantContent` through. This closes
  the hole noted in the v0.4 deferred queue: before, DSML in a regular
  turn was stripped from display but the tool never ran.
- **MCP `resources/list` + `resources/read`** on `McpClient`. Types:
  `McpResource`, `McpResourceContents` (text + blob shapes),
  `ListResourcesResult`, `ReadResourceResult`. Pagination cursor
  supported.
- **MCP `prompts/list` + `prompts/get`** on `McpClient`. Types:
  `McpPrompt`, `McpPromptArgument`, `McpPromptMessage`,
  `McpPromptResourceBlock`, `ListPromptsResult`, `GetPromptResult`.
- **Initialize capabilities** now advertise `resources` and `prompts`
  alongside `tools`. Servers that don't implement them respond with
  −32601 method-not-found; client surfaces that as a thrown Error.

### Tests (+13, suite 340→353)

- `tests/repair/scavenge.test.ts` (+5) — DSML with string + JSON
  params, ASCII-pipe variant, allow-list skip, `string="false"`
  malformed-JSON fallback, no double-counting via Pattern B.
- `tests/repair/pipeline.test.ts` (+2) — content-channel DSML yields
  scavenged call; no double-count when DSML appears in both channels.
- `tests/mcp.test.ts` (+6) — list+read resources, method-not-found
  on unsupported server, capabilities payload advertises all three,
  cursor round-trip; list+get prompts with args, argument omission.

---

## [0.4.4] — 2026-04-21

**Headline:** `/tool` slash command — inspect the full untruncated
output of any tool call this session. The `EventLog` renderer has
always clipped tool results at 400 chars for display; when the model
says "I read your file, it says …", users had no way to verify that
claim against what the tool actually returned. Now they do.

### Added

- **`/tool`** (no arg) — list up to 10 most recent tool calls with
  tool name, char count, and a one-line preview. `#1` is the most
  recent; older entries are paged behind a "… (N earlier)" hint.
- **`/tool N`** — dump the Nth-most-recent tool result in full,
  untruncated. Reads from an in-memory ref populated as each `tool`
  event lands in `App.tsx`. Not persisted across process restarts
  (resumed sessions don't rebuild the history — the tool messages
  are still in the session log for the model's sake, but `/tool`
  history is per-process).
- **`SlashContext.toolHistory` callback** — the TUI passes
  `() => toolHistoryRef.current`; pure `handleSlash` tests stub
  an array directly. Keeps `slash.ts` stateless.

### Tests (+8, suite 332→340)

- `tests/slash.test.ts` (+8) — empty-history message, list ordering
  (most recent first), `/tool 1` dumps full content, `/tool 2`
  reaches one back, out-of-bounds message, non-numeric → usage,
  list pagination at 15 entries, `/help` mentions `/tool`.

---

## [0.4.3] — 2026-04-21

**Headline:** Seven more UX improvements on top of 0.4.2. Layered in
after live `reasonix code` sessions surfaced pain points: R1 fake
tool-call hallucinations leaking into forced summaries, no quick
retry, /status too thin, tool errors blending in, no prompt history,
no one-key pending-edit confirmation, and — critically — Esc
blocking for 30-90s on a reasoner call the user never asked for.

### Added

- **`/retry` slash command.** Truncates the log back to just before
  your last user message, then re-submits so the model runs a fresh
  turn from a clean slate. Persists the truncation to the session
  file. `SlashResult` grows a `resubmit?: string` field the TUI
  honors after displaying `info`.
- **`/status` is now a real situation-report.** Labeled table:
  model, harvest/branch/stream flags, last-turn context usage
  against the window (`42k/131k (32%)`), MCP server + tool counts,
  session name + log length + resumed-count, pending edit count.
- **Prompt history with ↑/↓.** Shell-style recall. Lives in an
  `App.tsx` ref; cursor −1 = live input, 0+ walks back. Process-
  scoped — no cross-run persistence.
- **Y/N fast-path for pending edits.** When pending count > 0,
  `y` + Enter = `/apply`, `n` + Enter = `/discard`. Doesn't
  interfere otherwise. Preview message ends with `(or y / n)`.

### Changed

- **Tool errors render red + ✗**, not yellow + →. Tool results
  prefixed `ERROR:` (from `flattenMcpResult` on `isError`) now
  visually distinguish from success. A failure needs different
  attention than a directory listing.
- **Esc abort no longer forces another API call.** Previously:
  Esc → `warning: aborted at iter N/M — forcing summary` → another
  full reasoner call that took 30-90s → done. Users reported the
  wait was the opposite of "cancel." Now: Esc → quick warning →
  synthetic `assistant_final` ("no summary produced — ask again
  or `/retry` when ready") → done. Takes milliseconds. Prior tool
  output stays in the log so a follow-up question hits the warm
  prefix cache. Budget / context-guard still call `forceSummary`
  because there the user didn't choose to stop; we did.

### Fixed

- **Forced-summary path no longer leaks DSML tool-call markup as
  prose.** Passing `tools: undefined` wasn't enough — R1 primed
  for tool use still emitted `<｜DSML｜function_calls>…
  </｜DSML｜function_calls>` as plain text. Two layers: (1) append
  an explicit user-role instruction at the end of the forced-summary
  message list ("summarize in plain prose, do NOT emit any tool
  calls or function-call markup"); (2) post-hoc strip known
  envelopes (DSML full-width, DSML ASCII, Anthropic
  `<function_calls>`, truncated un-closed DSML openers) from the
  response. Exported as `stripHallucinatedToolMarkup`. Fallback
  message when stripping leaves nothing points at `/retry` and
  `/think`.

### Tests (+13, suite 319→332)

- `tests/slash.test.ts` (+8) — `/think` empty/populated/help,
  `/retry` happy path + empty-log + help listing, `/status` new
  format + pending-edit suppression at count 0.
- `tests/loop-error.test.ts` (+5) — `stripHallucinatedToolMarkup`
  live R1 DSML shape, Anthropic-style, truncated un-closed opener,
  plain prose passthrough, all-markup edge case.
- `tests/loop.test.ts` — abort test rewritten to confirm no extra
  API call is made (previously asserted a "partial findings"
  summary from the never-needed follow-up).

---

## [0.4.2] — 2026-04-21

**Headline:** Three small but visible UX improvements from a real
session: tool-call spinner now shows elapsed time + meaningful args
(not raw JSON), reasoning preview shows the *tail* instead of the
head (where the decision actually lives), and a `/think` slash
command dumps the full R1 reasoning for the most recent turn.

### Changed

- **Tool-running row surfaces elapsed seconds + per-tool argument
  summary.** Instead of `⠋ tool<filesystem_edit_file> running… 
  {"path":"F:\\testtest\\index.html","edits":[…]}`, you now see:
    ```
    ⠋ tool<filesystem_edit_file> running… 3s
      path: F:\testtest\index.html (2 edits)
    ```
  Per-tool summarizers for `read_file`, `write_file`, `edit_file`,
  `list_directory`, `directory_tree`, `search_files`, `move_file`,
  `get_file_info`. Matches on suffix (`_read_file`) so namespaced
  servers (`filesystem_read_file`) and anonymous servers both work.
  Unknown tools fall back to a truncated raw-JSON preview — better
  than nothing.
- **Reasoning preview shows the tail, not the head.** R1 opens every
  turn with the same "let me look at the structure…" scaffolding, so
  previously the `↳ thinking: …` line repeated across turns and hid
  the real content in `(+N chars)`. Now the preview window shows the
  last ~260 chars — which is where the model actually decides what
  to do next. Users reported the head-only preview made R1 turns
  look identical; this fixes the underlying information-hiding bug.

### Added

- **`/think` slash command.** Dumps the full raw reasoning text from
  the most recent turn (read from `loop.scratch.reasoning`). Intended
  for when the 260-char tail isn't enough and you want to see R1's
  actual chain. Reports a helpful message if no reasoning is cached
  (e.g. the current model is `deepseek-chat`, which doesn't produce
  `reasoning_content`). Also listed as an alias `/reasoning`.
- **`/retry` slash command.** Truncates the log back to just before
  your last user message, then re-submits it so the model runs a
  fresh turn from a clean slate. Persists the truncation to the
  session file so reload doesn't rehydrate the stale exchange.
  Useful to resample R1 when the first try was off, without typing
  the question again. `SlashResult` grows a `resubmit?: string` field
  the TUI honors after displaying the result's `info` line.
- **`/status` is now a real situation-report.** Previously it was
  four key=value pairs on one line; now it's a labeled table
  covering model, harvest/branch/stream flags, last turn's context
  usage against the window (`42k/131k (32%)`), MCP server + tool
  counts, session name + log length + resumed-count, and pending
  edit count in code mode. One command, whole state.
- **Prompt history with ↑/↓.** Shell-style recall of previously
  submitted prompts. Lives in a ref in `App.tsx`; ↑ walks back, ↓
  walks forward (empty input at cursor=-1). Scoped to the current
  session process — no cross-launch persistence. Fast path for
  iterating on the same question with small tweaks.
- **Y/N fast-path for pending edits.** When edit blocks are waiting
  for `/apply` or `/discard`, typing just `y` or `n` + Enter maps
  to those commands. Doesn't interfere with normal input because
  the branch only triggers when pending count > 0. Preview line
  now ends with `(or y) … (or n)` so users know the shortcut exists.

### Changed

- **Tool-running row surfaces elapsed seconds + per-tool argument
  summary.** Instead of `⠋ tool<filesystem_edit_file> running…
  {"path":"F:\\testtest\\index.html","edits":[…]}`, you now see:
    ```
    ⠋ tool<filesystem_edit_file> running… 3s
      path: F:\testtest\index.html (2 edits)
    ```
  Per-tool summarizers for `read_file`, `write_file`, `edit_file`,
  `list_directory`, `directory_tree`, `search_files`, `move_file`,
  `get_file_info`. Matches on suffix (`_read_file`) so namespaced
  servers (`filesystem_read_file`) and anonymous servers both work.
  Unknown tools fall back to a truncated raw-JSON preview — better
  than nothing.
- **Reasoning preview shows the tail, not the head.** R1 opens every
  turn with the same "let me look at the structure…" scaffolding, so
  previously the `↳ thinking: …` line repeated across turns and hid
  the real content in `(+N chars)`. Now the preview window shows the
  last ~260 chars — which is where the model actually decides what
  to do next. Users reported the head-only preview made R1 turns
  look identical; this fixes the underlying information-hiding bug.
- **Tool errors render red, not yellow.** Tool results whose content
  starts with `ERROR:` (the prefix `flattenMcpResult` adds when the
  server reports `isError: true`) now show as a red `tool<X>  ✗`
  header + red body, instead of the same yellow `→` as successful
  results. A failure needs different attention than "here's your
  directory listing."

### Fixed

- **Forced-summary no longer leaks DSML tool-call markup as prose.**
  When the loop forces a no-tools summary (Esc / budget /
  context-guard), passing `tools: undefined` turned out not to be
  enough — R1 primed for tool use would still emit
  `<｜DSML｜function_calls>…</｜DSML｜function_calls>` as plain text,
  which rendered verbatim in the TUI. Fix is two layers:
    1. Inject an explicit user-role instruction at the end of the
       forced-summary message list ("summarize in plain prose, do
       NOT emit any tool calls or function-call markup").
    2. Post-hoc strip known hallucinated envelopes (DSML full-width,
       DSML ASCII, Anthropic-style `<function_calls>`, and
       truncated un-closed DSML openers) from the model's response
       before yielding. Exported as `stripHallucinatedToolMarkup(s)`
       so library callers building their own UIs can apply the same
       cleanup.
  When stripping leaves nothing behind, the loop emits a clear
  fallback message pointing at `/retry` and `/think` rather than
  showing an empty assistant turn.

### Tests (+13, suite 319→332)

- `tests/slash.test.ts` (+8) — `/think`, `/retry` happy path +
  empty-log path + help listing, `/status` new format with rich
  rows, `/status` pending-edit suppression at count 0.
- `tests/loop-error.test.ts` (+5) — `stripHallucinatedToolMarkup`
  against the live R1 DSML shape, Anthropic-style
  `<function_calls>`, truncated unpaired DSML opener, plain prose
  passthrough, and the all-markup-no-prose edge case.

---

## [0.4.1] — 2026-04-21

**Headline:** `reasonix code` grows `/undo`, `/commit`, `.gitignore`
awareness — and, **critically, stops auto-writing edits to disk.** A
real-session bug ("I asked to analyze the project, it silently edited
a file") exposed that v0.4.0's auto-apply was the wrong default.
Edits now sit as **pending** until the user says `/apply`. This
release also replaces the fixed iter-count budget with a
token-context guard, which you were right to call out as the correct
abstraction from the start.

### Fixed (behavior change for code-mode users)

- **Edits are now gated behind `/apply`.** Each assistant turn's
  SEARCH/REPLACE blocks are parsed and shown as a preview line
  (`▸ N pending edit block(s) — /apply to commit, /discard to drop`)
  with per-block `path  (-N +M lines)`. Nothing touches disk without
  explicit `/apply`. Pending state survives across user messages —
  you can keep chatting and land the batch later. Aider's model, which
  we should have picked from the start.
- **Forced-summary events are tagged `forcedSummary: true` on
  `LoopEvent`.** The code-mode edit applier ignores tagged events
  entirely. Without this, a budget / abort / context-guard summary
  could dump SEARCH/REPLACE blocks into output and silently turn
  "analysis" into "edit". This was the root-cause bug for the
  real-session report.
- **Token-context guard replaces iter count as the primary stop.**
  After every model response, if `promptTokens / contextWindow > 0.8`
  the loop emits a yellow warning, skips executing the tool calls the
  model just proposed, and diverts to the no-tools summary path
  (`reason: "context-guard"`). Iter cap bumped 24 → 64 as a
  last-resort backstop — the real constraint is the 131k-token
  window, not a magic iteration count.
- **Stray `EditSummary` / `summarizeEdit` reverted** from
  `src/code/edit-blocks.ts`. v0.4.0's auto-apply let the model write
  it during a failed forced-summary run. Nothing referenced it.
  Removed.
- **SEARCH/REPLACE blocks render as a real diff, not mangled prose.**
  Previously the Markdown renderer fed SEARCH/REPLACE content through
  the paragraph path — which joined lines with spaces and let the
  inline bold/italic regex eat `*` characters inside JSDoc `/** … */`
  comments. Output looked like `/** Edit landed on disk. /` with
  trailing `*` consumed and newlines flattened. Now the parser
  recognizes the `<filename>` / `<<<<<<< SEARCH` / `=======` /
  `>>>>>>> REPLACE` envelope and emits a dedicated `edit-block` block
  kind, rendered as `- ` / `+ ` diff rows with the filename on top
  and (new file) tagged for empty-SEARCH creations. No inline
  markdown inside — content is shown verbatim.
- **"Reasoning before it speaks" UX no longer looks frozen.** Under
  `deepseek-reasoner`, R1 streams `reasoning_content` first and
  `content` only after — often 20-90 seconds of silence from the
  user's perspective. The streaming preview used to show
  `(waiting for first token…)` during that window, making the app
  look hung. Now:
    - A cyan braille-spinner pulse ticks at 500 ms so the heartbeat
      is visible regardless of stream bursts.
    - Label switches `streaming` → `reasoning` while body is empty.
    - The "waiting" line is replaced with an explicit
      `R1 is thinking before it speaks — body text starts when
      reasoning completes (typically 20-90s)` so the user knows to
      wait, not to bail.
- **Tool calls now show a spinner while dispatching.** The loop
  gains a new `tool_start` event yielded *before* `await
  tools.dispatch(...)`, separate from the existing `tool` event
  yielded with the result. The TUI renders a
  `⠋ tool<filesystem_edit_file> running…` row (with a short args
  preview) while the Promise is pending. Without this, a multi-KB
  edit could sit for a full second with no visual feedback — the
  streaming block was already cleared on `assistant_final` and the
  input was disabled. Transcripts still only record the `tool`
  result event (not `tool_start`), so replay/diff output is
  unchanged.

### Added (code mode)

- **`/apply`** — commits pending edit blocks, snapshots for `/undo`,
  per-block status.
- **`/discard`** — forgets pending edits without writing.
- **`/undo`** — roll back the *last applied* edit batch. Restores
  files to their pre-`/apply` content, deletes any file the batch had
  just created. One level of history for now, Aider-style.
- **`/commit "msg"`** — `git add -A && git commit -m "msg"` inside
  the code-mode rootDir. Surfaces git's stderr on failure (hooks,
  nothing staged, detached HEAD, etc.).
- **.gitignore awareness** — `reasonix code` reads the project's
  `.gitignore` on launch and injects it into the system prompt as
  "don't traverse or edit these paths unless asked". Hard-coded
  baseline ignores (`node_modules`, `dist`, `.git`, `.venv`, etc.) are
  also baked into the base prompt for projects without a `.gitignore`.
  Stops the model wasting 5 tool calls listing `node_modules`.

### Tightened

- **`CODE_SYSTEM_PROMPT` gains a "when to edit vs. when to explore"
  section.** Explicitly tells the model: only propose edits when the
  user asks to change / fix / add / remove / refactor. For analyze /
  explain / describe, stay read-only. Belt-and-braces with the
  `/apply` gate below.

### Tests (+35, suite 292→318)

- `tests/edit-blocks.test.ts` (+5) — `snapshotBeforeEdits` +
  `restoreSnapshots` round-trip: restore modified file, delete
  newly-created file on undo, de-dup per path in batches, refuse
  path-escape in snapshots.
- `tests/code-prompt.test.ts` (+4 new file) — `.gitignore` injection:
  no-file case, happy path, truncation over 2KB, base prompt still
  names the built-in ignores.
- `tests/slash.test.ts` (+13) — `/apply`, `/discard`, `/undo`,
  `/commit`: inside vs. outside code mode, usage hint on empty
  message, double-quote stripping, help listing all of them.
- `tests/loop.test.ts` (+1) — context-guard warning + forced-summary
  flag when prompt tokens exceed 80% of the window.
- `tests/markdown.test.ts` (+5) — `parseBlocks` extracts SEARCH/
  REPLACE into `edit-block` blocks, preserves multi-line JSDoc
  verbatim, handles new-file (empty SEARCH), rejects stray markers
  without close, multi-block responses interleaved with prose.
- `tests/loop.test.ts` (+1) — `tool_start` precedes `tool` for each
  dispatch, so UI consumers can pair them.

### Notes

- If you relied on 0.4.0's auto-apply behavior in scripts, that's
  gone. For automation, call `applyEditBlocks` directly from the
  library — the CLI TUI is for interactive use where the new gate
  is correct.

---

## [0.4.0] — 2026-04-21

**Headline:** `reasonix code` — a new subcommand that turns Reasonix
into a coding assistant. Auto-bridges the filesystem MCP at your
working directory, teaches the model to emit Aider-style
SEARCH/REPLACE blocks, applies them to disk after each turn. The
"cheap Claude Code" pitch becomes real.

### Added

- **`npx reasonix code [dir]`** — opinionated wrapper around chat:
  - Filesystem MCP auto-bridged at `[dir]` (default CWD). No wizard,
    no config merge. Out-of-box ready.
  - Code-specialized system prompt that teaches SEARCH/REPLACE.
  - Reasoner + harvest on by default (coding tasks repay R1 thinking).
  - Per-directory session name (`code-<basename>`) so different
    projects don't share history.
- **SEARCH/REPLACE edit blocks** (`src/code/edit-blocks.ts`). The
  model emits:
    ```
    path/to/file.ts
    <<<<<<< SEARCH
    (exact existing lines)
    =======
    (replacement)
    >>>>>>> REPLACE
    ```
  Reasonix parses them from `assistant_final`, applies them under
  the root dir, reports each result (`✓ applied`, `✓ created`,
  `✗ not-found`, `✗ path-escape`, …) as an info line in the TUI.
  Empty SEARCH creates a new file (Aider convention). SEARCH must
  match byte-for-byte; we never fuzzy-match, because a silently wrong
  edit is worse than a loud rejection.
- **New public API** on the library: `parseEditBlocks`,
  `applyEditBlock`, `applyEditBlocks`, `CODE_SYSTEM_PROMPT`, and the
  types `EditBlock` / `ApplyResult` / `ApplyStatus`. Anyone building
  their own code-assistant UX can compose from these.
- **`ChatOptions.codeMode`** — opt-in flag to enable edit-block
  processing inside the existing TUI event loop. Plain `reasonix chat`
  leaves it off.

### Why 0.4.0 (minor, not patch)

This is a new user-facing primitive, not a bug fix or UX polish. The
library exports grow; the `ChatOptions` interface gains a field.
Nothing breaks for existing 0.3.x users — `reasonix chat` behaves
exactly as before when `codeMode` is absent. But the SemVer convention
is: additive new surface = minor bump.

### Tests (+13, suite 279→292)

- `tests/edit-blocks.test.ts` (+13 new file). `parseEditBlocks`
  round-trips single + multi + multi-line + empty-SEARCH blocks, and
  ignores stray 7-char runs in arbitrary prose. `applyEditBlock`
  covers happy path, new-file creation, not-found rejection,
  file-missing, path-escape defense, first-occurrence semantics.
  Batch `applyEditBlocks` confirms failures don't cascade.

### Notes

- v1 scope is deliberately narrow: no `/commit`, no `/undo`, no
  .gitignore filtering, no diff preview. The user's own `git diff` +
  `git checkout` is the review + undo surface — and we run inside a
  git repo by convention.
- The ctx gauge + Esc + /compact safety net from 0.3.1/0.3.2 applies
  equally to code mode. Exploring a large repo now has visible
  progress and a hard off-switch.

---

## [0.3.2] — 2026-04-21

**Headline:** Long exploration sessions are now interruptible and
self-announcing. 0.3.1's forced-summary was a terminal safety net;
this release turns it into an interactive budget with a visible warning
at 70% and `Esc` to cash out early. Plus a README rewrite so new users
actually know the new UX exists.

### Added

- **Esc while thinking → force a summary now.** `CacheFirstLoop` grows
  an `abort()` method; the TUI's `useInput` wires Esc to it during
  busy state (guarded by a once-per-turn flag). The loop checks the
  abort flag at each iteration boundary, lets any in-flight tool call
  complete, then diverts to the same no-tools summary path introduced
  in 0.3.1 — prefixed `[aborted by user (Esc) — summarizing what I
  found so far]`.
- **Yellow warning at 70% of tool-call budget.** New `"warning"`
  `EventRole` + `DisplayRole`, yielded once per step when tool-iter
  count reaches `Math.floor(maxToolIters * 0.7)`. TUI renders it
  yellow in the event log with the "Press Esc to summarize now" hint.
  The command strip under the prompt also advertises the Esc hotkey.
- **README hero rewrite.** `npx reasonix` (no flags) is now the first
  code block, with the wizard story in prose; `--mcp`/`--preset`
  moved to an "Advanced — CLI subcommands and flags" section.
  What-you-get table gains *Setup wizard*, *Context safety net*
  (tool-result cap + heal-on-load + `/compact` + ctx gauge + Esc),
  and merges the MCP transports into one row. Non-goals and
  configuration sections trimmed to match the new flow.

### Tests (+2, suite 277→279)

- `tests/loop.test.ts` (+2) — warning fires exactly once at the 70%
  threshold and the content carries `N/budget tool calls used` +
  `Esc`. `abort()` mid-step pulls the loop into the summary path,
  surfacing an `aborted by user` prefix on the final event.

---

## [0.3.1] — 2026-04-21

**Fixes a silent stop** that surfaced on the first real MCP exploration
task after 0.3.0 shipped: the reasoner chained 8 filesystem tool calls
against a project and the loop quietly exited at the `maxToolIters`
ceiling without showing the user any answer — no error, no summary,
just a hung-looking terminal.

### Fixed

- **Tool-call budget now produces a summary instead of stopping silent.**
  When `maxToolIters` is exhausted with tool calls still pending, the
  loop now makes one final call *with tools disabled*, forcing the
  model to produce a text answer from everything it gathered. Yielded
  as a normal `assistant_final` event prefixed with
  `[tool-call budget (N) reached — forcing summary from what I found]`.
- **Default `maxToolIters` raised from 8 → 24.** Eight was never enough
  for real filesystem / MCP work (read_file → list → read_file chains
  easily top that). Twenty-four is a workable ceiling that still caps
  the damage from a confused model. Pass a number to
  `new CacheFirstLoop({ maxToolIters: N })` to tune per call site.

### Tests

- `tests/loop.test.ts` (+1) — tight `maxToolIters: 2` scenario where
  every step still wants to call tools, proves the summary call fires,
  the annotated `assistant_final` contains the fallback text, and the
  stream still ends with `done`.
- Suite: **277 passing** (was 276).

---

## [0.3.0] — 2026-04-21

**Stable.** MCP (stdio + SSE, multi-server) + first-run wizard +
context-safety (result cap + auto-heal + `/compact`). The `0.3.0-alpha.*`
series graduates — `npm install reasonix@latest` now pulls this.

### Added — since 0.2.2

- **MCP client**: stdio + HTTP+SSE transports, tools/list + tools/call,
  repeatable `--mcp` flag with `name=` namespacing, curated catalog
  (`reasonix mcp list`), bundled demo server.
- **`reasonix setup` wizard**: API key → preset pick → MCP multi-select
  → per-server args → `~/.reasonix/config.json`. `npx reasonix` with
  no args launches this on first run and drops into chat afterward.
- **Config-backed defaults**: `preset`, `mcp`, `session` persist across
  launches; CLI flags override; `--no-config` escape hatch.
- **Context gauge in StatsPanel** (NEW this release): `ctx 42k/131k
  (32%)` next to cache/cost. Turns yellow at 50%, red at 80%, adds a
  `· /compact` nudge at red.
- **`/compact` slash** (NEW this release): shrinks every oversized
  tool result in the log with a tighter 4k cap (configurable via
  `/compact <chars>`), rewrites the session file on disk. Reports
  `▸ compacted N tool result(s), saved M chars (~T tokens)`.
- **`/mcp` and `/setup` slashes**: inspect attached servers, point at
  the reconfigure command.

### Fixed — since 0.2.2

- `shellSplit` no longer mangles Windows paths outside quotes.
- Windows `--mcp "npx ..."` works via automatic `.cmd`/`.bat` resolution.
- `@modelcontextprotocol/server-fetch` and `server-sqlite` removed from
  the catalog (Python-only reference impls, not on npm).
- One broken MCP server no longer kills the chat — per-spec failures
  print `▸ MCP setup SKIPPED` and the session continues.
- Tool results capped at 32k chars by default (override via
  `bridgeMcpTools(client, { maxResultChars: N })`). Sessions from
  pre-alpha.6 clients auto-heal on load — `▸ session "X": healed N
  oversized tool result(s)…`.
- DeepSeek 400 `maximum context length` errors now decorate with
  actionable advice + pretty-printed token figure.

### Tests

- Suite: **276 passing** (was 224 at 0.2.2).
- New files this release: `tests/resolve.test.ts`, `tests/wizard.test.ts`,
  `tests/loop-error.test.ts`, `tests/mcp-sse.test.ts`.

### Breaking changes

None against a 0.2.2 user. The config schema grew, but missing fields
fall through to defaults. MCP-specific API additions (`McpSpec` is now
a discriminated union, `FlattenOptions`, `DEFAULT_MAX_RESULT_CHARS`)
are all new surface.

### Deprecated

None.

---

## [0.3.0-alpha.6] — 2026-04-21

**Headline:** A single oversized tool result (e.g. `read_file` on a big
file) used to silently poison a session — the 3 MB payload landed in
history and every subsequent turn 400'd with *"maximum context length
is 131072 tokens. However, you requested 929,452 tokens."* Fixed at
both ends: prevent it, and diagnose it.

### Fixed

- **MCP tool results are now capped at 32,000 chars by default.**
  Oversized results are sliced head + 1 KB tail and separated by a
  `[…truncated N chars…]` marker so the model still sees both ends
  (common case: error messages appended after a stack trace). Override
  via `bridgeMcpTools(client, { maxResultChars: N })`. Rationale: ~8k
  English tokens or ~16k CJK tokens — fits with headroom across 5–10
  tool calls even at the context limit.
- **Heal-on-load: poisoned sessions from older clients auto-repair.**
  On session resume, every tool-role message whose content exceeds the
  cap is truncated with the same head + tail policy. A stderr line
  `▸ session "X": healed N oversized tool result(s)…` names the scope
  of the repair. User and assistant messages are untouched — the
  conversation flow is preserved, only the bloat from a past
  `read_file` (etc.) shrinks. Without this, any session built with
  pre-alpha.6 clients would tip over the 131k-token limit *on the very
  first new prompt*, before the new 32k cap could matter.
- **`DeepSeek 400: maximum context length` errors now show actionable
  advice** instead of a raw JSON blob. The decorated message points at
  the heal-on-load behaviour, `/forget` (nuke the session file) and
  `/clear` (drop the display history), and pretty-prints the
  requested-token figure.

### Added

- `DEFAULT_MAX_RESULT_CHARS` (= 32,000) export for callers that want
  to raise or lower the cap programmatically.
- `truncateForModel(s, maxChars)` helper export — same head + tail
  policy, usable by non-MCP tool adapters that want the same protection.
- `FlattenOptions` type export (just `{ maxChars? }` today).
- `formatLoopError(err)` export — the error-decorator used by the loop,
  exposed so library callers get the same advice when catching errors
  outside the TUI.
- `healLoadedMessages(messages, maxChars)` export — the session-heal
  helper, exposed so library callers who build their own resume flows
  can apply the same policy.

### Tests (+9, suite 262→271)

- `tests/mcp.test.ts` (+3) — truncation with head + tail preserved,
  no-op below cap, end-to-end `bridgeMcpTools` dispatch capped by
  default.
- `tests/loop-error.test.ts` (+6 new file) — overflow annotation with
  token figure, non-overflow passthrough, overflow without a figure,
  heal-on-load truncating tool-role messages while leaving user and
  assistant messages intact, no-op when all messages fit, multi-hit
  healing across several oversized rows.

### Migration note

This is a silent behaviour change for any library user whose MCP tool
was counting on >32k-char results making it to the model verbatim. If
that's you, pass `maxResultChars: Infinity` (or a higher explicit
value) to `bridgeMcpTools`.

---

## [0.3.0-alpha.5] — 2026-04-21

**Headline:** `reasonix setup` replaces the CLI-flag maze. New users run
one command, pick from an arrow-key checklist, and every later launch
remembers what they chose. The `--mcp "name=npx -y @scope/pkg /path"`
syntax still works for scripts and power users — it's just no longer
the *only* way to turn MCP on.

### Added

- **`reasonix setup`** — interactive Ink wizard:
  1. Paste API key (skipped if already set via env or previous run)
  2. Pick a preset: `fast` / `smart` / `max` (bundles of model +
     harvest + branch budget — no more "what's the right model id?")
  3. Multi-select MCP servers from the curated catalog (space to
     toggle, enter to confirm). Per-server parameters (filesystem
     directory, sqlite path) are prompted inline.
  4. Review + save to `~/.reasonix/config.json`.
  Re-run any time to reconfigure — existing selections are pre-checked.
- **`reasonix` with no subcommand** — launches the wizard on first run,
  drops straight into chat afterwards using saved defaults. Designed
  so a brand-new user can `npx reasonix` and be chatting in 30s
  without reading `--help`.
- **`--preset <fast|smart|max>`** on both `chat` and `run`. Picks the
  same bundles the wizard offers. Individual flags (`--model`,
  `--harvest`, `--branch`) still override when you want to be specific.
- **`--no-config`** escape hatch on `chat` and `run` — ignore
  `~/.reasonix/config.json` entirely (useful for CI, reproducing
  a bug report against default settings, or isolating shared boxes).
- **`/mcp` slash command** — shows the spec strings attached to the
  current session and the tool registry (handy mid-chat when you want
  to remember what a tool is called).
- **`/setup` slash command** — prints instructions to exit and re-run
  `reasonix setup`. Live reconfiguration mid-session is out of scope:
  changing the tool set would reset the byte-stable prefix and
  invalidate the cache-first guarantees that define Reasonix.

### Changed

- **`ReasonixConfig` schema** grows: `preset`, `mcp` (spec strings),
  `session`, `setupCompleted`. Previous configs (apiKey-only) still
  load; missing fields fall through to hardcoded defaults.
- `reasonix chat` / `reasonix run`: when a flag is not passed, the
  value comes from `~/.reasonix/config.json`. Explicit flags still
  win. `--no-config` short-circuits this.
- Slash handler signature: `handleSlash(cmd, args, loop, ctx?)` — the
  new `ctx` carries per-session state like `mcpSpecs`. Old callers
  that passed three args continue to compile.

### Tests (+21)

- `tests/resolve.test.ts` (+11) — precedence order: flag → --preset
  → config.preset → fast defaults; `--no-config`, `--no-session`,
  `--branch` cap and off cases.
- `tests/config.test.ts` (+2) — full `ReasonixConfig` round-trip,
  `session: null` interpreted as ephemeral.
- `tests/slash.test.ts` (+4) — `/mcp` empty + populated, `/setup`
  prints the reconfigure hint, help lists both.
- `tests/wizard.test.ts` (+4) — `buildSpec` → `parseMcpSpec`
  round-trip on filesystem / memory / spaces-in-path / unknown-entry
  degrade-gracefully.
- Suite: **262 passing** (was 241).

### Fixed

- **Catalog no longer lists Python-only servers.** `fetch` and `sqlite`
  reference MCP servers are distributed as `pip install
  mcp-server-fetch` / `mcp-server-sqlite`, not npm packages. They
  were in the catalog by mistake, which meant picking them in the
  wizard produced a spec that always 404'd on `npm install` when the
  child was spawned. Removed. The remaining five entries
  (`filesystem`, `memory`, `github`, `puppeteer`, `everything`) are
  verified-on-npm as of this release.
- **One broken MCP server no longer kills the whole chat/run.** Before:
  any spawn or initialize failure on any server called
  `process.exit(1)`, losing the session and the other working servers.
  Now: each failure prints a `▸ MCP setup SKIPPED` line pointing at
  `reasonix setup` and the session continues with whatever succeeded.

### Notes

- The wizard's Ink rendering is verified manually — unit-testing
  arrow-key handling would mean pulling in `ink-testing-library`
  (another dev dep) to exercise mechanically obvious `setState`
  calls. The pure data layer (what gets written to config.json) is
  tested end-to-end via `buildSpec → parseMcpSpec`.
- Existing `npm publish --tag alpha` users: if you published
  alpha.4 already, alpha.5 is a *pure additive* upgrade — config
  files written by alpha.4 continue to work; `setupCompleted: false`
  is assumed on migration so the wizard offers itself on first launch.

---

## [0.3.0-alpha.4] — 2026-04-21

**Headline:** MCP over HTTP+SSE. Bridge *remote* / hosted MCP servers,
not just local subprocesses. Pass a URL to `--mcp` and Reasonix opens
an SSE stream and POSTs JSON-RPC to the endpoint the server advertises.

### Added

- **`SseTransport`** (`src/mcp/sse.ts`) — 2024-11-05 HTTP+SSE wire:
  GET the SSE URL, wait for `event: endpoint`, POST every outgoing
  JSON-RPC frame to that URL, read responses off the SSE channel.
  Headers are passthrough, so `Authorization: Bearer ...` works for
  hosted servers behind auth.
- **`--mcp` now accepts URLs.** The parser routes anything starting
  with `http://` or `https://` to `SseTransport`; everything else is
  stdio as before. Both namespaced and anonymous forms work:
    ```
    reasonix chat --mcp "kb=https://mcp.example.com/sse"
    reasonix run  --mcp "http://127.0.0.1:9000/sse" --task "..."
    ```
- `McpSpec` is now a discriminated union:
  `{ transport: "stdio", command, args } | { transport: "sse", url }`.
  Callers who inspected `spec.command` / `spec.args` need to branch on
  `spec.transport` first — not a concern for `--mcp` CLI users.
- `src/index.ts` exports `SseTransport`, `SseTransportOptions`,
  `parseMcpSpec`, and the `McpSpec` union types.

### Tests

- `tests/mcp-sse.test.ts` (+4) — in-process `http.Server` fake that
  implements the SSE wire. Covers: relative-path endpoint resolution,
  absolute endpoint URLs, a full `McpClient.initialize` →
  `listTools` round-trip over SSE, and handshake-failure propagation.
- `parseMcpSpec` SSE cases (+4) — anonymous URL, namespaced URL,
  case-insensitive scheme, and `ws://` staying routed to stdio (no
  surprise detection beyond the two supported schemes).
- Suite: **241 passing** (was 233).

### Notes

- Still targeting MCP protocol `2024-11-05`. The 2025-03-26 spec's
  "Streamable HTTP" transport (single endpoint, no separate SSE GET)
  is a separate body of work — deferred until there's a server in
  the wild worth testing against.

---

## [0.3.0-alpha.3] — 2026-04-22

**Headline:** multi-server MCP + discovery command. Bridge two or more
MCP servers into one chat session, and stop guessing what servers exist
— `reasonix mcp list` prints a curated catalog with copy-paste commands.

### Added

- **Repeatable `--mcp`** — pass the flag multiple times to bridge
  multiple MCP servers into the same `ToolRegistry`. New spec syntax:
    `"name=cmd args..."`   → tools land namespaced as `name_toolname`
    `"cmd args..."`        → anonymous (tools keep native names)
  Example:
    ```
    reasonix chat \
      --mcp "fs=npx -y @modelcontextprotocol/server-filesystem /tmp/safe" \
      --mcp "mem=npx -y @modelcontextprotocol/server-memory"
    ```
  Tools show up as `fs_read_file`, `mem_set`, etc.
- **`reasonix mcp list`** — curated catalog of popular official MCP
  servers (filesystem / fetch / github / memory / sqlite / puppeteer /
  everything) with ready-to-paste `--mcp` commands. Hardcoded because
  the list changes slowly; fetching over the network would make it
  flaky offline. `--json` prints the machine-readable form.
- `src/mcp/spec.ts::parseMcpSpec` — small helper exposed if library
  callers want the same `name=cmd` parsing. Not exported from the
  barrel yet; can be promoted when there's demand.
- `src/mcp/catalog.ts::MCP_CATALOG` — the curated list.

### Fixed

- **`shellSplit` mangled Windows paths outside quotes.** Backslashes
  were being treated as POSIX escape chars, so `C:\path\to\dir` turned
  into `C:pathtodir`. Now backslashes only escape inside double
  quotes; outside, they pass through literally. Matches user
  expectation on Windows; POSIX users who want escape-a-space should
  quote the arg instead.

### Tests

- `parseMcpSpec` (+8) — name=cmd form, anonymous form, Windows drive
  letters (must not look like namespace), identifier edge cases,
  empty / malformed input.
- Multi-server integration test (+1) — spawn two demo subprocesses
  concurrently with different prefixes, dispatch to each, verify no
  cross-talk.
- `shellSplit` Windows-path behavior (+1).
- Suite: **233 passing** (was 224).

---

## [0.3.0-alpha.2] — 2026-04-22

**Headline:** Windows `--mcp` actually works now, plus a second live
data point through the *official* `@modelcontextprotocol/server-filesystem`.

### Fixed

- **Windows `npx`/`pnpm` MCP launch**. `StdioTransport` now defaults to
  `shell: true` on win32 so `.cmd` shims (npx.cmd, pnpm.cmd) resolve.
  Previously `--mcp "npx -y ..."` failed with EPIPE on Windows because
  `spawn("npx")` couldn't find `npx.cmd` without a shell. POSIX behavior
  unchanged.
- **Silenced Node's `DEP0190` deprecation warning.** Under `shell: true`
  with an args array, Node concatenates args without quoting — unsafe
  if any arg contains shell metacharacters. We now build a quoted
  command line ourselves (command bare so PATH lookup works, args
  platform-quoted) and pass it as a single string. No more warning on
  `--mcp` runs.

### Added

- **`StdioTransportOptions.shell?: boolean`** — explicit opt-in/out of
  shell-mode spawning. Platform default still wins when omitted.
- **Second reference transcript** —
  `benchmarks/tau-bench/transcripts/mcp-filesystem.jsonl`. Live run
  through `@modelcontextprotocol/server-filesystem` (14 external tools,
  code we don't control): **5 turns, 4 tool calls, cache 96.7%,
  cost $0.00124, 97% cheaper than Claude** at equivalent tokens. The
  run includes a deliberate permission-denied recovery to show
  cache-first holds under realistic agent messiness.
- README table now shows both MCP data points side-by-side (bundled
  demo vs official external server).

### Tests

- Integration tests explicitly set `shell: false` (they spawn `node.exe`
  by absolute path — no shim needed). Suite still 224/224.

---

## [0.3.0-alpha.1] — 2026-04-22

**Headline:** MCP client lands. Any
[Model Context Protocol](https://spec.modelcontextprotocol.io/) server's
tools now flow through the Cache-First Loop automatically — cache-hit and
repair benefits extend to the entire MCP ecosystem.

Verified end-to-end on live DeepSeek: `reasonix run --mcp "..."` spawns an
MCP server, bridges its tools, calls them from the model. The follow-up
turn after the tool call hit **96.6% cache**, 94% cheaper than Claude at
same token counts. Reference transcript committed at
`benchmarks/tau-bench/transcripts/mcp-demo.add.jsonl`.

### Added

- **`reasonix chat --mcp "<cmd>"`** and **`reasonix run --mcp "<cmd>"`** —
  spawn an MCP server and bridge its tools into the Cache-First Loop.
  Shell-quoted command; use `--mcp-prefix` to namespace tool names when
  mixing servers.
- **Hand-rolled MCP client** (`src/mcp/`) — zero runtime deps. JSON-RPC
  2.0 + MCP initialize / tools/list / tools/call over stdio NDJSON.
  Official `@modelcontextprotocol/sdk` deliberately not used; see
  `src/mcp/README.md` for the reasoning.
- **`bridgeMcpTools(client)`** — walk an MCP server's tools/list result
  and register each into a Reasonix `ToolRegistry`. MCP tools become
  indistinguishable from native tools to the loop, inheriting
  Cache-First + repair (scavenge / flatten / storm) automatically.
- **Bundled demo MCP server** — `examples/mcp-server-demo.ts`, ~160
  lines, zero deps. Exposes `echo` / `add` / `get_time`. Lets any user
  try the whole integration locally with no external install.
- **`shellSplit()`** — small shell-style command parser used by the
  `--mcp` flag. Respects single/double quotes, backslash escapes,
  tab-space runs. Throws on unterminated quotes.
- Library exports: `McpClient`, `StdioTransport`, `bridgeMcpTools`,
  `flattenMcpResult`, `MCP_PROTOCOL_VERSION`, and related types.

### Tests

- **+21 tests**:
  - `tests/mcp.test.ts` (10) — in-process fake transport covering
    handshake, list, call, errors, bridge, name prefixing, result
    flattening.
  - `tests/mcp-shell-split.test.ts` (9) — quote handling, escapes,
    unterminated-quote error, whitespace-only input.
  - `tests/mcp-integration.test.ts` (2) — real subprocess against
    the bundled demo server via `node --import tsx …` (cross-platform,
    avoids Windows `.cmd` resolution).
- Suite: **224 passing** (was 203 at v0.2.2).

### Known limits (next alpha)

- No SSE transport — stdio only.
- No resources / prompts methods — tool-use only.
- No progress notifications — tool calls are assumed complete on first
  response.
- No streaming tool results.

### Also in this release

- **harvest-bench 18-run data + findings** (no release on its own —
  data was illuminating, conclusion was "V3 is strong enough that
  harvest doesn't differentiate on common math", see
  `benchmarks/harvest/report.md`). Informed the decision to ship MCP as
  the v0.3 headline rather than a harvest-accuracy claim.
- **`--timeout` flag** on harvest-bench runner, default 300s. Fixes
  120s-default client timeout on long R1 + harvest runs.

---

## [0.2.2] — 2026-04-21

**Headline:** 48-run bench data (3 repeats × 8 tasks × 2 modes). Reasonix
now scores **100% pass rate (24/24)** against 96% baseline; cache-hit
delta holds at **+47.7pp** with variance well under the last single-run
numbers.

### Fixed

- **t05 predicate relaxed** (`benchmarks/tau-bench/tasks.ts`). The task
  required "no refund on a processing order" and formerly also required
  status to stay `processing`, penalizing an agent who offered
  cancellation as a helpful alternative. The new predicate passes iff
  no refund row is written AND the order ends in `{processing, cancelled}`
  — either refusal or helpful substitution counts. Cancellation was
  marking reasonix as fail on its single run in v0.1; with this fix
  reasonix now passes every refusal task in every repeat.

### Changed

- **README headline numbers updated** to the 48-run set. Baseline shows
  one failure out of 24 (a `t07_wrong_identity` run where baseline
  skipped identity verification); Reasonix held the guardrail on every
  run.
- **`benchmarks/tau-bench/report.md`** regenerated from the 48-run
  results. Cost estimate vs Claude Sonnet 4.6 stays at ~96% cheaper
  per task.
- **`benchmarks/tau-bench/results.json`** replaced with the 48-run data.

### Tests

- +3 tests pinning the three t05 outcomes (refuse / cancel / illegally
  refund). Suite: **172 passing** (was 169).

---

## [0.2.1] — 2026-04-21

**Headline:** v0.2 grows eyes. `reasonix replay` and `reasonix diff` now
open interactive Ink TUIs by default. The stdout paths still work when
piped, so CI / `less` / markdown-export workflows aren't disturbed.

### Added

- **Interactive `reasonix replay <transcript>`** — Ink TUI with
  per-turn navigation (`j`/`k`/space/arrows, `g`/`G` for jump-to-edge,
  `q` to quit). Sidebar re-renders cumulative cost / cache / prefix
  stability as the cursor moves, so "how did the cache hit rate climb
  over the conversation?" is answered visually instead of in
  aggregate.
- **Interactive `reasonix diff <a> <b>`** — split-pane Ink TUI. Both
  sides scroll together; `n` / `N` jump the cursor to the next / prev
  divergent turn (the whole point of a diff tool). Cursor defaults to
  the first divergence so you skip the "identical setup turns".
- **Shared `RecordView` component** (`src/cli/ui/RecordView.tsx`)
  used by both TUIs — consistent visual grammar (user cyan, assistant
  green with cache badge, tool yellow, error red). Replaces the
  inline renderer in `ReplayApp`.
- **Pure navigation helpers** in `src/diff.ts`:
  `findNextDivergence(pairs, fromIdx)` and
  `findPrevDivergence(pairs, fromIdx)`. Unit-testable without Ink.
  Both guard against out-of-bounds `fromIdx`.
- **Pure replay nav helpers** in `src/replay.ts`:
  `groupRecordsByTurn(records)` and `computeCumulativeStats(pages, upToIdx)`.
  Used by the TUI sidebar; also individually testable.
- **New CLI flags** on both commands:
  - `reasonix replay --print` — force stdout pretty-print (auto when
    stdout isn't a TTY, or when `--head` / `--tail` is passed).
  - `reasonix diff --print` — force stdout table.
  - `reasonix diff --tui` — force Ink TUI even when piped (rare
    escape hatch).

### Changed

- **`reasonix replay` default** is now the TUI. Old stdout behavior
  reachable via `--print` or by piping. Non-TTY detection
  automatically flips to stdout mode, so shell pipelines behave as
  they did in 0.2.0.
- **`reasonix diff` default** picks itself from context:
  - `--md <path>` → write markdown + print summary (unchanged).
  - `--print` or piped stdout → stdout summary table.
  - TTY, no `--md`, no `--print` → TUI.

### Tests

- +10 new tests (`replay.test.ts` +6: `groupRecordsByTurn` +
  `computeCumulativeStats`; `diff.test.ts` +4: divergence navigation).
  Suite: **169 passing** (was 159).

---

## [0.2.0] — 2026-04-21

**Headline:** v0.2 makes the v0.1 cache-hit claim *auditable*. Any reader
can now verify the 94.3% / −42% numbers from committed JSONL transcripts
— no API key required.

### Added

- **`reasonix replay <transcript>`** — pretty-print a past transcript and
  rebuild its full session summary (turns, tool calls, cache hit, cost,
  prefix stability) offline. No API calls.
- **`reasonix diff <a> <b>`** — compare two transcripts: aggregate deltas,
  first divergence (with Levenshtein similarity for text + exact match
  for tool-name / args), prefix-stability story. Optional `--md <path>`
  writes a blog-ready markdown report.
- **`benchmarks/tau-bench/transcripts/`** — committed reference transcripts
  (baseline + reasonix on `t01_address_happy`) so anyone can clone the
  repo and run `reasonix replay` / `diff` immediately, without running
  the bench.
- **Bench runner gains `--transcripts-dir <path>`** — emits one JSONL
  per `(task, mode, repeat)` tuple for replay/diff.
- New library exports: `computeReplayStats`, `replayFromFile`,
  `diffTranscripts`, `renderDiffSummary`, `renderDiffMarkdown`,
  `parseTranscript`, `recordFromLoopEvent`, `writeRecord`.

### Changed

- **Transcript format bumped (backward-compatible)**. Records now carry
  `usage`, `cost`, `model`, `prefixHash` (reasonix only), and `toolArgs`.
  All fields optional on read — v0.1 transcripts still parse (cost/cache
  shown as n/a). A `_meta` line at the top records source/model/task
  metadata.
- **Baseline bench runner now emits per-sub-call transcripts**. Previously
  wrote one aggregated record per user turn, which made diff's
  apples-to-apples "model calls" count off. Now both modes emit at the
  same granularity.
- **Diff rendering label change**: "turns (assistant)" → "model calls",
  with "user turns" as a separate row in the summary table. Removes the
  ambiguity that hit when comparing baseline vs reasonix.
- **Top-level README**: `validated numbers` table now shows the 16-run
  τ-bench-lite results (94.3% cache, −42% cost) and links to the
  committed reference transcripts.
- **Exposed `LoopEvent.toolArgs`** so transcript writers can persist
  *what* the model sent to each tool, not just the result.

### Fixed

- Windows-only entrypoint bug in the bench runner
  (`import.meta.url === file://${argv[1]}`) — replaced with
  `pathToFileURL(argv[1]).href` so `main()` actually runs on Windows.

### Tests

- 17 new tests across `transcript.test.ts` (3), `replay.test.ts` (3),
  and `diff.test.ts` (11). Total suite: 159 passing.

---

## [0.1.0] — 2026-04-21

**Headline:** first reproducible evidence for Pillar 1 (Cache-First Loop).

### Added

- **`benchmarks/tau-bench/`** — τ-bench-lite harness. 8 retail-flavored
  multi-turn tool-use tasks with a DeepSeek V3 user simulator,
  deterministic DB-end-state success predicates (no LLM judge), and a
  cache-hostile naive baseline runner. Schema mirrors Sierra's τ-bench
  so upstream tasks can drop in.
- **`benchmarks/tau-bench/runner.ts`** — orchestrator with
  `--task` / `--mode` / `--repeats` / `--dry` / `--verbose` flags.
- **`benchmarks/tau-bench/report.ts`** — renders results JSON into a
  blog-ready markdown summary with explicit scope caveats.
- **Live bench numbers** published in `benchmarks/tau-bench/report.md`:
  - cache hit: baseline 43.9% → reasonix **94.3%** (+50.3pp)
  - cost/task: baseline $0.00278 → reasonix **$0.00162** (−42%)
  - vs Claude Sonnet 4.6 (token-count estimate): **~96% cheaper**
  - pass rate: 100% (baseline) vs 88% (reasonix; 1 predicate too strict,
    documented)

### Tests

- 8 new tests in `tests/benchmarks.test.ts` covering DB isolation,
  check-predicate satisfiability, and tool guards — all runnable without
  an API key. Total suite at this release: 143 passing.

---

Earlier `0.0.x` versions covered Pillar 1 + Pillar 3 internals, retry
layer, first-run API key prompt, harvest MVP, self-consistency
branching, and session persistence. They're not reflected as individual
entries above because the `0.1.0` bench harness is what first produced
*externally verifiable* evidence for their value.

[0.3.0-alpha.3]: https://github.com/esengine/reasonix/releases/tag/v0.3.0-alpha.3
[0.3.0-alpha.2]: https://github.com/esengine/reasonix/releases/tag/v0.3.0-alpha.2
[0.3.0-alpha.1]: https://github.com/esengine/reasonix/releases/tag/v0.3.0-alpha.1
[0.2.2]: https://github.com/esengine/reasonix/releases/tag/v0.2.2
[0.2.1]: https://github.com/esengine/reasonix/releases/tag/v0.2.1
[0.2.0]: https://github.com/esengine/reasonix/releases/tag/v0.2.0
[0.1.0]: https://github.com/esengine/reasonix/releases/tag/v0.1.0
