Skip to content

Fixes: theme picker crash, dialog ErrorBoundary, control-server hardening, lazy themes, stderr styling, /new race#76

Merged
liftaris merged 14 commits into
devfrom
integration/wiki-fixes-2026-05-22
May 22, 2026
Merged

Fixes: theme picker crash, dialog ErrorBoundary, control-server hardening, lazy themes, stderr styling, /new race#76
liftaris merged 14 commits into
devfrom
integration/wiki-fixes-2026-05-22

Conversation

@liftaris
Copy link
Copy Markdown
Owner

Batch of small fixes from the wiki-driven audit (herm-wiki debt register), merged per-fix branches plus two follow-ups found during integration review.

Fixes

  • Theme picker crash — opening /theme and navigating blew up with Maximum update depth exceeded and killed the TUI.
    • CommandProvider.register no longer bumps state on (un)register; the registry is a pure ref read lazily at keypress time.
    • DialogHost is wrapped in an ErrorBoundary: a throw inside any dialog dismisses it and toasts the error instead of crashing the app.
    • Root cause of the remaining loop was in DialogSelect: the cursor-sync effect (cursor ← index(props.current)) and the live-preview effect (onMove → ctx.set → props.current) fed each other. onMove now fires only for user-driven moves (keyboard nav / mouse hover), and prefs.set no-ops when the value is unchanged.
  • Stale-sid race on /newnewSession now clears the gateway's active sid before session.create, mirroring switchProfile, so events in the gap aren't attributed to the outgoing session.
  • Gateway stderr styling — error-ish stderr lines render as kind: "error" in the transcript (previously a plain system line truncated at 200 chars); the full line is kept, and /logs still carries the untruncated ring.
  • CONTROL server safety — binds 127.0.0.1 by default, warns (stderr + toast) when CONTROL_BIND points at a non-loopback host, and the control server is now documented in docs/control.md.
  • Lazy theme loading — theme JSON bodies load on demand instead of all 42 eagerly at startup; a generated manifest (name + preview colors) backs the picker, and the active theme is primed before first render.
  • terminal.resize audit — verified markdown re-wrap is unaffected by resize; the omission of the terminal.resize RPC is now documented as intentional in src/app/useSession.ts.
  • AGENTS.md — default branch corrected to dev.

Tests

  • New: theme-picker open/navigate/revert regression test, command-registry render-loop test, dialog ErrorBoundary tests, newSession race test, control warning tests, stderr classification tests.
  • bunx tsc --noEmit clean, bun test 1050 pass / 0 fail, bun run build OK.
  • Smoke-tested via the control server: /theme opens, navigates with live preview, selects, and reverts on Esc with no error toast.

Notes

  • Base is dev. Individual fix branches are pushed as task/t_* if separate review is preferred.

builder and others added 14 commits May 22, 2026 00:41
Error-ish gateway.stderr lines now render as styled error rows instead of
plain system lines, and the 200-char truncation on the transcript row is
gone — full line passes through. The gw.logs ring buffer already captures
every stderr line untruncated via GatewayClient.log(), so /logs (gw.tail)
still offers the complete traceback regardless of what the transcript
surfaces.
newSession mirrored switchProfile's cleanup except for the gateway sid
clear. Without gw.setSession("") up front, any RPC or event emitted in
the window between reset() and setSession(new) carried the outgoing
session_id via gateway-client's auto-injection — a narrow race where
stale-sid events can be misattributed to the new session.

Adds a MockGateway-backed regression test: asserts the second
session.create (from /new) has no auto-injected session_id, and that
session.close still receives prev explicitly.
…l.md

Bun.serve without hostname binds 0.0.0.0 — CONTROL=1 was silently
network-reachable. Default CONTROL_BIND=127.0.0.1. If the user overrides
to a non-loopback host, start() writes a yellow stderr banner and
AppInner raises a 15s warning toast so the exposure is never silent.

warningFor(on, bind, port) factored out so tests exercise the decision
without re-importing the module under different env (BIND is captured at
module load, mirroring the existing `enabled` pattern).

docs/control.md covers what it is, env vars, endpoints, security
posture, and the ptyrun/e2e use case.
…Boundary

CommandProvider.register used to bump a state counter on add and on
cleanup. Nothing downstream actually read the counter — open() and
the palette's useKeyboard read the registry lazily at press time, so
the re-render did no work. But when a caller's register effect had a
dep that flipped mid-commit (slash.tsx lists themeCtx; the theme
picker fires ctx.set on every arrow via onMove), the cleanup's
setState rescheduled CommandProvider during its own passive-unmount
phase and the nested-update limit tripped. Switch the registry to a
pure ref; (un)register is now a plain Map write.

Wrap the dialog overlay in a class Boundary so the next latent dialog
loop (Rollback, Branch, Eikon Generate and the theme picker's own
DialogSelect current/onMove interplay) degrades to a dismissed dialog
+ toast instead of killing the TUI. Boundary sits inside
DialogProvider so it can call clear() + toast.error() directly.

Tests: test/command-registry.test.tsx covers the register contract
under rapid mount/unmount bursts; test/dialog-boundary.test.tsx
mounts a dialog whose body throws and asserts the Boundary catches,
dismisses, and the error surfaces via toast.
Drop the static import of every theme body from src/theme/builtin.ts.
Names + a few preview colors live in a generated manifest; theme
bodies load on first set() via dynamic import(`./themes/${name}.json`)
and are memoized in src/theme/load.ts.

src/index.tsx primes the active theme before render() so the first
frame paints without a fallback flicker. test/preload.ts does the
same for every bun test mount.

New: scripts/gen-theme-manifest.ts regenerates src/theme/manifest.ts
from the JSONs on disk — run when a theme is added, removed, or its
primary/accent/background changes.
…et no-ops on unchanged value

The registry setRevision removal was necessary but not sufficient: the
remaining loop was DialogSelect's cursor-sync effect (cursor <- index of
props.current) ping-ponging with the move-notify effect (onMove ->
ctx.set -> props.current). Gate onMove behind a moved ref that only
keyboard nav and mouse hover set. prefs.set also returns early when the
value is unchanged so redundant preview writes don't notify subscribers.
@liftaris liftaris marked this pull request as ready for review May 22, 2026 08:02
@liftaris liftaris merged commit de8fce1 into dev May 22, 2026
1 check passed
@liftaris liftaris deleted the integration/wiki-fixes-2026-05-22 branch May 22, 2026 08:02
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.7.1-dev.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@liftaris liftaris mentioned this pull request May 22, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.7.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant