Bug-fix release. Everything here targets one symptom: TFormula stalling in front of an alternate-screen TUI (Ink-style CLIs, editors, anything that repaints a pane) — worst over SSH, where the wasted work is a full base64 PNG re-upload. Reported and fixed by @oldantique in #4 / #5 / #6 / #7.
No behaviour changes outside that path, and no CLI surface changes. npm install -g tformula@0.3.1.
The stall
Visible images are no longer evicted mid-relayout. Invalidating an alternate-screen layout drops each Kitty placement so the next scan can re-place it. For that instant every on-screen image looked idle, so once the image cache was full the LRU sweep deleted formulas that were still on screen and about to be re-pinned, forcing a full PNG re-upload of each. Those images are now reserved from idle eviction until the scan that re-places them has reconciled the screen. (#6)
Only the rows a scroll could have moved are invalidated. Previously any scrolling operation dropped every placement on the alternate screen. Now the affected viewport rows are tracked — insert/delete-line cannot touch anything above the cursor, and scroll and index/reverse-index stay inside the DECSTBM region — so a TUI redrawing its input box or a bottom pane keeps the formulas above it. On the probe frame sequence that is 5 places / 5 deletes / 5 renders down to none. An unrecognised scroll shape still widens to the whole screen. (#6)
The unbounded re-render loop
A formula that cannot be typeset is attempted once, not once per frame. Whether MathJax can typeset a formula depends only on its source, but the retry state guarding it was keyed by screen coordinates, and those entries were dropped whenever a scroll moved the layout — so the attempt counter never reached the ceiling that would stop it. In the reported session a stretch of prose mis-detected as TeX failed to parse 865 times. Source-determined failures now go in a content-keyed set consulted before any of that. (#5)
Engine faults are no longer misfiled as bad LaTeX. A rejected MathJax import, a RangeError under memory pressure, or a typeset returning no <svg> root used to be reported as a parse error, which retired the formula for the rest of the session on the first occurrence. They now raise a distinct renderer-unavailable, which keeps its retries and stays out of the negative cache. (#5)
The retry ceiling is now reachable. Retry and block state obeys the same row band as the placements, so a formula failing outside the scrolled rows keeps its attempt counter instead of being reset by every frame an animating TUI draws — 18 attempts with the limit never reached, down to 6 and blocked. (#7)
Historical scrollback pins are no longer shed for an unreachable budget. The image budget now accounts for relayout-reserved images, so the detached-placement prune stops chasing a number no eviction it is permitted to make can move. (#7)
Known gap
Rasterizer failure codes (raster-limit, empty-raster, png-limit) still retry per geometry rather than being blocked by content. With #7 in place that is bounded at five attempts per geometry, so it is wasted work rather than a stall. Tracked in #8.
Full changelog: v0.3.0...v0.3.1