19c475ae33
Three small confirmed fixes:
- renderToString swallowed a component that threw literal `undefined`: the
`uncaughtError !== undefined` sentinel could not tell "threw undefined" from
"no error", so it returned the normal frame instead of propagating. Track
occurrence with a separate `errored` boolean (mirrors the live renderer's
onErrorCaptured `errored` flag).
- renderToString wrapped a non-Error throw as `new Error(String(value))`, so
`{ message: "detail" }` became "[object Object]". Re-throw a genuine Error
(incl. cross-realm, via the `[object Error]` brand check) as-is, and wrap a
true non-Error with `messageForNonError` so its message survives. Relocated
`isErrorInput` from render.ts into error-overview.ts (next to
messageForNonError) so both renderers share one source of truth.
- acquireRawMode called `stdin.ref()` before `setRawMode(true)`. On a hostile
PTY setRawMode throws ERR_TTY_INIT_FAILED after the ref but before the refcount
increments, so dispose's gated unref never ran and the ref'd stdin kept the
event loop alive. Reorder setRawMode(true) before stdin.ref() so a throw
leaves nothing ref'd.
Test-first: added reproducing tests for each (renderToString throw-undefined and
non-Error-message in render-to-string.test.tsx; ref/unref balance on a throwing
setRawMode in raw-mode-ref-leak.sequential.test.tsx), confirmed red, then green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>