On the interactive commit path the active cursor position was never forwarded to
the frame writer, so the cursor was never shown at the useCursor() position, never
followed input, and a cursor-only move on a byte-identical frame emitted nothing.
Aligns with Ink v7.0.4 by wiring three coupled defects together:
- render.ts setCursorPosition now forwards to writer.setCursorPosition, marking
log-update's cursorDirty (Ink ink.tsx:494-497).
- the synchronized-update commit gate is split into Ink's two levels: the write is
gated on willRender() || isCursorDirty(), but BSU/ESU wrap only when willRender()
(Ink ink.tsx:1094 outer, :372-382 inner) -- an idle cursor-dirty re-render emits
zero bytes, not an empty BSU/ESU pair.
- FrameWriter.write() bypasses its frame===lastFrame dedup when the cursor is dirty,
so a cursor-only move still reaches log-update's buildCursorOnlySequence.
The mount-time hide-cursor write moves before originalMount so the first commit's
show is the last visibility change (Ink hides before its first render); a synchronous
mount throw now runs best-effort teardown (cursor/alt-screen restore) before
rethrowing the ORIGINAL error, matching Ink's constructor-wired signalExit.
Adds 8 interactive-TTY tests; the prior use-cursor tests used the debug render()
helper where log-update never runs, so they passed for the wrong reason.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>