835e026336
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>