Three small raw-mode/focus corrections, each aligned to Ink v7.0.4 and
covered by a test (TDD red→green where reachable).
1. Raw mode left ON after a synchronous signal exit (Ctrl+C). The terminal
raw-mode disable is deferred to a microtask (so it survives same-tick
component swaps), but on the signal-exit path teardown(true) re-raises the
signal synchronously without draining microtasks, so the disable never ran
and the shell stopped echoing after Ctrl+C. dispose() now forces the disable
SYNCHRONOUSLY when raw mode is no longer owned (state.refs === 0 and either
this dispose released the last ref or a release left pendingDisable set),
mirroring Ink's unmount-cleanup guard `rawModeEnabledCount > 0 ||
pendingDisableRawModeRef.current` (App.tsx:626-631). The disable stays gated
on the SHARED refcount, so a multi-app teardown can't disable while another
app still holds raw mode.
2. Same-tick useInput swap re-issued setRawMode(true) + stdin.ref() and leaked
a libuv ref (the deferred disable bailed on refs>0 and never unref'd). Added
a pendingDisable flag to RawModeState mirroring Ink's pendingDisableRawModeRef
(App.tsx:331-344): on re-acquire while a disable is pending, skip
ref()/setRawMode(true) and cancel the queued disable.
3. focusNext/focusPrevious start-index logic factored into a shared
startSearchIndex() helper so the two directions stay symmetric. Behavior is
identical for all reachable states; it additionally folds the (unreachable
while the activeId invariant holds) "activeId not in list" case into the
same branch instead of diverging per-direction.
Tests: raw-mode-lifecycle.test.tsx (swap no-op, replacement still receives
input, synchronous teardown disable); programmatic-focus.test.tsx (no-active
first/last targeting + active-focus step/wrap via the manager API).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>