Compare commits

...
3 Commits
Author SHA1 Message Date
reasonix b307987c0b chore(release): 0.53.2
Includes fix(ink): defer useBoxMetrics measure off the React commit
batch to dodge "Maximum update depth exceeded" crashes when Box
content doesn't converge in one layout pass.
2026-05-26 23:46:09 -07:00
reasonix b21a6a0493 chore(release): 0.53.1 2026-05-26 23:15:52 -07:00
reasonix fe1bbb9aa9 chore(release): 0.53.0 2026-05-26 21:55:56 -07:00
5 changed files with 132 additions and 26 deletions

No files matched your search

+100
View File
@@ -3,6 +3,106 @@
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.53.2] — 2026-05-27
**TUI crash fix — `useBoxMetrics` no longer trips React's max-update-depth.**
The hook ran `useEffect` with no dep array and called `setSize` inside,
so any Box whose content shape depended on its own measurement could
oscillate within a single React commit batch and crash with "Maximum
update depth exceeded" on chat startup (reported in #1986
follow-up). Measurement is now deferred off the commit batch via
`setTimeout(0)` — each measure starts a fresh root update instead of
chaining inside the depth counter. Oscillating call sites still don't
converge to a single value (the underlying anti-pattern in the caller
remains), but the user no longer sees a crash.
## [0.53.1] — 2026-05-27
**Install fix — `npx reasonix` no longer crashes on `EUNSUPPORTEDPROTOCOL`.**
The 0.52.0 refactor that inlined Ink as a workspace package (#1947)
left `"ink": "workspace:*"` in runtime `dependencies`. tsup already
bundles ink into `dist/`, so the runtime never resolves it — but npm
on consumer machines parses the declaration and rejects the
unresolvable `workspace:` protocol. Removed from runtime deps; the
bundled ink in `dist/` is the implementation. 0.53.0 is deprecated
on npm.
## [0.53.0] — 2026-05-27
**Force-summary respects the active model.** Context-guard and stuck-state
recovery were calling `client.chat({ model: "deepseek-v4-flash" })`
regardless of the user's configured model (#1979). Third-party
DeepSeek-compatible endpoints (mimo, self-hosted, Azure forks) that
don't expose flash returned 400 on every recovery — the session looked
like it lost context (the messages were still there) and the user had
no way back without `/clear`. Force-summary now uses `this.model` with
`thinking: "disabled"` to keep reasoning tokens off the bill even when
the active model is pro.
**Web search — Brave backend.** `web_search` now supports the Brave
Search API as an alternative backend (#1916), alongside the existing
Bing / Tavily / Ollama-cloud paths. Configurable per-instance.
**Desktop — workspace switcher.** Sidebar exposes a workspace
switcher (#1908) so multiple project roots can be juggled without
restarting the app. Bundled-app version surfacing for desktop is
unified through the same sync pipeline as the CLI.
**Desktop — fixes.**
- Per-tab hook firing for Pre/PostToolUse, UserPromptSubmit, Stop
(#1984) — hooks were leaking across tabs in multi-session windows
- Clear the pending assistant on error so the spinner stops (#1985,
fixes #1660)
- `/skill` and `/skills` slash commands wired through (#1978, fixes
#1969)
- Context token meter refreshes after each turn (#1964)
- Preserve session context across abort (#1938)
- `\[ \]` and `\( \)` math delimiters now render in markdown (#1923)
- Windows file opening normalizes path separators (#1914)
- Settings allow clearing base URL and editor fields (#1905)
**Dashboard / web.**
- Dashboard session refresh no longer skips updates (#1917)
- "New Chat" label stays on one line (#1924)
- Server-snapshot helpers fully typed (#1961)
**Config / auth.** `saveApiKey` now overrides a stale `DEEPSEEK_API_KEY`
env var so the UI save takes effect immediately (#1982) — previously
the env shadow won and the user had to relaunch.
**QQ adapter.** `/qq` defaults to markdown replies (#1941); the
previous plain-text default lost code blocks and lists.
**Networking / fetch.** `web_fetch` falls back to DNS-over-HTTPS when
the system resolver returns RFC1918 / link-local addresses (#1944) —
some captive portals and split-horizon DNS configs hand back fake
internal IPs and the fetch then connects to the wrong host. CLI
`doctor` accepts a custom `/models` endpoint instead of hard-coding
DeepSeek's (#1972).
**Truncation / encoding.**
- Slice boundaries align to UTF-16 codepoints so multi-code-unit
characters can't be split mid-surrogate (#1976, includes #1970)
- Truncated-result-saver falls back to `~/.reasonix` when `rootDir`
resolves to the filesystem root (#1974)
- Lone surrogates are sanitized in chat payloads before serialization
(#1939)
**TUI — CardStream measure loop.** Break the
measure→render→measure feedback loop that could pin CPU on resize
(#1958).
**Errors / ACP.** Structured `errorDetail` propagates through loop,
transcript, and ACP wire format (#1965 + follow-up) so external
clients see the same fault classification the CLI does.
**Internals.** Inline the ink runtime as a workspace package (#1947) —
last step of the `@esengine/ink` in-tree work started in 0.52.0.
**Misc.**
- Generated-script shell `cwd` guidance fixed (#1937)
- AtomGit star badge in README (#1954)
## [0.52.0] — 2026-05-26
**Ink renderer in-tree as `@esengine/ink`.** The vendored Ink fork now
+2 -3
View File
@@ -1,12 +1,12 @@
{
"name": "reasonix",
"version": "0.52.0",
"version": "0.53.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "reasonix",
"version": "0.52.0",
"version": "0.53.2",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -27,7 +27,6 @@
"iconv-lite": "^0.7.2",
"ignore": "^7.0.5",
"indent-string": "^5.0.0",
"ink": "workspace:*",
"lodash-es": "^4.17.21",
"node-html-parser": "^7.1.0",
"picomatch": "^4.0.4",
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "reasonix",
"version": "0.52.0",
"version": "0.53.2",
"description": "DeepSeek-native coding agent: cache-first loop, flash-first cost control, tool-call repair.",
"type": "module",
"bin": {
@@ -94,7 +94,6 @@
"iconv-lite": "^0.7.2",
"ignore": "^7.0.5",
"indent-string": "^5.0.0",
"ink": "workspace:*",
"lodash-es": "^4.17.21",
"node-html-parser": "^7.1.0",
"picomatch": "^4.0.4",
+14 -4
View File
@@ -128,13 +128,23 @@ export function useBoxMetrics(ref: { current: DOMElement | null }): {
} {
const [size, setSize] = useState({ width: 0, height: 0 });
const lastRef = useRef<DOMElement | null>(null);
const scheduledRef = useRef(false);
useEffect(() => {
if (!ref.current) return;
lastRef.current = ref.current;
const { width, height } = measureElement(ref.current);
setSize((prev) =>
prev.width === width && prev.height === height ? prev : { width, height },
);
if (scheduledRef.current) return;
scheduledRef.current = true;
// Defer measure off the React commit batch — re-measuring synchronously
// in the same depth-counted chain crashes with "Maximum update depth
// exceeded" when Box content doesn't converge in one layout pass.
setTimeout(() => {
scheduledRef.current = false;
if (!ref.current) return;
const { width, height } = measureElement(ref.current);
setSize((prev) =>
prev.width === width && prev.height === height ? prev : { width, height },
);
}, 0);
});
return size;
}
+15 -17
View File
@@ -45,19 +45,21 @@ describe("Ink update-depth repro candidates", () => {
r.unmount();
});
it("useBoxMetrics: rendering from own measurement never converges", async () => {
// Whether React's nested-update guard fires depends on event-loop timing
// (a Linux runner doesn't trip what Windows trips in the same wall clock).
// The deterministic property is render count: stable layouts converge in
// 2–3 renders, the broken pattern compounds without bound. Counting is
// the loop-detection signal; the React error is just one possible
// downstream symptom.
it("useBoxMetrics: oscillating call sites no longer crash React's depth guard", async () => {
// useBoxMetrics defers each measure off the React commit batch
// (setTimeout 0), so a Box that renders from its own measurement
// still oscillates between heights but never trips "Maximum update
// depth exceeded". The property under test is the absence of a
// crash + the presence of the underlying anti-pattern (heights
// alternate, proving it's not silently converging).
captureErrors();
let stableRenders = 0;
const stableHeights = new Set<number>();
function Stable() {
const ref = React.useRef(null!);
useBoxMetrics(ref);
const m = useBoxMetrics(ref);
stableRenders++;
stableHeights.add(m.height);
return (
<Box ref={ref} flexDirection="column">
<Text>a</Text>
@@ -65,12 +67,11 @@ describe("Ink update-depth repro candidates", () => {
</Box>
);
}
let oscRenders = 0;
const oscHeights = new Set<number>();
function Oscillator() {
const ref = React.useRef(null!);
const m = useBoxMetrics(ref);
oscRenders++;
// height 0/even → 1 child → measure=1 (odd); odd → 2 children → measure=2 (even).
oscHeights.add(m.height);
const extra = m.height % 2 === 1;
return (
<Box ref={ref} flexDirection="column">
@@ -85,13 +86,10 @@ describe("Ink update-depth repro candidates", () => {
const b = render(<Oscillator />);
await new Promise((res) => setTimeout(res, 80));
b.unmount();
// Stable converges in a handful of renders. The broken pattern compounds
// — even a slow CI runner clears ~20 cycles in 80ms; local Node does
// hundreds. The thresholds are deliberately loose to stay robust across
// runner speeds; the property under test is "doesn't converge", not a
// specific count.
expect(hasMaxDepth()).toBe(false);
expect(stableRenders).toBeLessThan(10);
expect(oscRenders).toBeGreaterThan(15);
expect(stableHeights.size).toBeLessThanOrEqual(2);
expect(oscHeights.size).toBeGreaterThanOrEqual(2);
});
it("useAnimationFrame: many subscribers with short interval does not loop alone", async () => {