Skip to content

fix(tui): raster figure through the renderer's own output channel [spec 10] - #115

Merged
wine-fall merged 1 commit into
mainfrom
zachg-0810--tui-raster-writeout
Aug 10, 2026
Merged

fix(tui): raster figure through the renderer's own output channel [spec 10]#115
wine-fall merged 1 commit into
mainfrom
zachg-0810--tui-raster-writeout

Conversation

@wine-fall

Copy link
Copy Markdown
Owner

Implements specs/spec10/10-tui.md (§3.7 raster figure, §8 opentui#92 note).

What broke

PR #114's raster whisper-figure wrote kitty-graphics escapes straight to
process.stdout. OpenTUI intercepts process.stdout.write in its
capture-stdout mode, so the payload was fed back into the renderer as text
(the on-screen base64 garbage) and panicked bun's native layer within
seconds of entering the sky layout — SIGTRAP, front-end dead with OpenTUI's
restore skipped, terminal stranded in raw mode with dead input and a dead
Ctrl-C. Reproduced A/B in a 200x50 pty harness with a ghostty env: image
mode crashed every run (bun-*.ips crash reports), sprite mode never did.

The fix

  • tui/src/app.tsx — every figure escape now goes through the
    renderer's writeOut, the one channel serialized with the render thread
    (the same channel OpenTUI's own escape emissions use). Typed private
    upstream pending a public graphics API (opentui#92); reached via one
    documented cast.
  • tui/src/figure-image.ts — the bun:ffi ioctl is deleted (it
    returned an all-zero winsize, so the path always ran on fallback
    guesses); cell pixel size now derives from the renderer's own
    resolution report via cellSizeFrom (unit-tested).
  • src/ipc-host.ts — engine-side backstop: when the front-end exits
    abnormally, the engine writes the terminal restore itself (leave the
    alternate screen, show the cursor, raw mode off), so a crashed face can
    never freeze the terminal again. Unit-tested via an injectable tty.
  • specs/spec10/10-tui.md — the opentui#92 note now names the
    writeOut requirement so the direct-stdout mistake cannot come back.

Acceptance

25-second run in the same pty harness that reproduced the crash: TUI alive
(old crash window was under 5s), 70 kitty APC sequences on the wire through
the new channel, no new crash report; then a deliberate SIGKILL of the
client showed the engine's restore bytes arriving on the pty.

Peer review (codex gpt-5.5 xhigh): 0 findings.

AI coding brief

  • Original request: the radio's TUI came up as a garbled half-screen
    with dead input after the latest PR; diagnose from the latest PR and the
    running instance's logs, then root-cause it rather than disabling the
    feature.
  • Manual interventions: the user redirected the first proposed fix
    (default the figure back to sprite) toward solving the actual
    writer-vs-renderer conflict, and clarified the symptom was an
    unresponsive TUI, not a visible crash.
  • Retro: reading the vendored OpenTUI bundle first would have found
    writeOut and the stdout interception in minutes; when a library owns
    the terminal, look for its sanctioned output channel before reaching for
    process.stdout or FFI.

🤖 Generated with Claude Code

…ec 10]

The whisper-figure raster (PR #114) wrote kitty-graphics escapes straight to
process.stdout — which OpenTUI intercepts in capture-stdout mode, so the
payload was re-rendered as text (the on-screen base64 garbage) and panicked
bun's native layer within seconds (SIGTRAP), stranding the terminal in raw
mode with dead input. Reproduced A/B in a 200x50 pty: image mode crashed
every run, sprite mode never did.

- app.tsx: every figure escape now goes through renderer.writeOut — the one
  channel serialized with the render thread. 25s acceptance run in the same
  pty harness: alive, 70 APC sequences on the wire, no crash report.
- figure-image.ts: the bun:ffi ioctl (which returned all-zero winsize and
  fed the path fallback guesses) is gone; cell pixel size now derives from
  the renderer's own resolution report (cellSizeFrom, unit-tested).
- ipc-host.ts: when the front-end exits abnormally, the engine writes the
  terminal restore itself (leave alt screen, show cursor, raw mode off) —
  a dead face can no longer freeze the terminal. Unit-tested via an
  injectable tty; verified live by SIGKILLing the client mid-run.

Peer review (codex gpt-5.5 xhigh): 0 findings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wine-fall
wine-fall merged commit 88fe656 into main Aug 10, 2026
3 checks passed
@wine-fall
wine-fall deleted the zachg-0810--tui-raster-writeout branch August 10, 2026 03:46
wine-fall added a commit that referenced this pull request Aug 10, 2026
…[spec 10] (#116)

Two by-eye reports from the first real Ghostty session after #115:

- The spectrum was pinned bin 0 -> left arm, so the ever-breathing bass made
  the wave read as marching left-to-right. waveBinAt now mirrors the bins
  from the arc's middle outward — bass at the center, treble fraying toward
  both arms (unit-tested for symmetry and for a bass-only frame lighting
  the center alone).
- The raster figure blanked for over half a second at the first spoken line:
  the placement effect depended on the pose, so every state change ran
  deleteFigures and waited out the 600ms resettle. The pose now rides a ref
  and swaps the next transmitted frame in place under the same image id;
  only a real relayout tears the placement down. Verified in the pty
  harness: 73 in-place retransmissions across a run spanning the
  greeting->talk transition, zero deletes before shutdown.

Peer review (codex gpt-5.5 xhigh): 0 findings.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant