a73x

6468febb

fix(web): re-review nits — the zoom gate learns about ws, both engines drain

a73x   2026-08-13 14:31

Commit message
fix(web): re-review nits — the zoom gate learns about ws, both engines drain

The re-review's three Minors plus its one comment ask: zoom() also
requires tile.ws (a failed mux_init leaves a truthy core and no
socket); resetCore re-renders when it zeroes scrollPages so the scroll
mask lifts with the state; the scratch scroll engine drains its own
pty_out where it feeds (reset() is a fullReset of the terminal, not of
Engine.pty_out) and the apply-site comment stops claiming to be the
one place; setStatus's comment names the browser-leg 'gone' as the
considered exception to terminal precedence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

src/wasm_core.zig
Old New
@@ -191,7 +191,8 @@ export fn mux_apply_frame(msg_type: u32, len: u32) i32 {
191 // (DSR and friends), but the daemon is authoritative and answers the 191 // (DSR and friends), but the daemon is authoritative and answers the
192 // application already — forwarding these would double every reply, so 192 // application already — forwarding these would double every reply, so
193 // nothing in the browser may ever read them. Unread, they are an 193 // nothing in the browser may ever read them. Unread, they are an
194 // accumulation with no bound; this is the one place that closes it. 194 // accumulation with no bound; each engine drains where it feeds —
195 // here for the live replica, in mux_scroll_feed for the scratch.
195 // (engine.zig pins the replies themselves, where they matter.) 196 // (engine.zig pins the replies themselves, where they matter.)
196 c.rep.eng.clearPtyOutput(); 197 c.rep.eng.clearPtyOutput();
197 if (applied == .resync) return 1; 198 if (applied == .resync) return 1;
@@ -490,6 +491,9 @@ export fn mux_scroll_feed(len: u32) i32 {
490 const se = c.scroll_eng.?; 491 const se = c.scroll_eng.?;
491 se.reset(); 492 se.reset();
492 se.feed(input_buf[0..len]); 493 se.feed(input_buf[0..len]);
494 // Same drain rule as mux_apply_frame: reset() is a fullReset of the
495 // terminal, not of Engine.pty_out, and history rows can answer too.
496 se.clearPtyOutput();
493 return 0; 497 return 0;
494 } 498 }
495 499
web/mux.js
Old New
@@ -215,6 +215,7 @@ class Tile {
215 } 215 }
216 this.gotState = false; 216 this.gotState = false;
217 this.scrollPages = 0; 217 this.scrollPages = 0;
218 this.renderBadge(); // scrollPages changed; the mask must lift with it
218 this.drawScale = 0; // force the backing store to be re-sized 219 this.drawScale = 0; // force the backing store to be re-sized
219 this.replayFailed(`re-initialized the core after ${why}`, 'replay failed'); 220 this.replayFailed(`re-initialized the core after ${why}`, 'replay failed');
220 } 221 }
@@ -563,7 +564,12 @@ class Tile {
563 // 'up': painting that green while sendAttach stays gated on replayDead 564 // 'up': painting that green while sendAttach stays gated on replayDead
564 // is exactly a silent tile that reads healthy. Terminal states do 565 // is exactly a silent tile that reads healthy. Terminal states do
565 // replace each other — the newest fact about a dead session is still 566 // replace each other — the newest fact about a dead session is still
566 // the true one. 567 // the true one. The arguable exception, considered and accepted: a
568 // browser-leg 'gone' (the page can no longer observe the session at
569 // all) is also withheld while terminal. The word is then stale but the
570 // color is not — index.html renders all four in the same red — and
571 // splitting "link news that supersedes" from "link news that doesn't"
572 // would reintroduce the peer-writers problem this field deleted.
567 setStatus(cls, text) { 573 setStatus(cls, text) {
568 if (TERMINAL.has(this.status) && !TERMINAL.has(cls)) return; 574 if (TERMINAL.has(this.status) && !TERMINAL.has(cls)) return;
569 this.status = cls; 575 this.status = cls;
@@ -596,7 +602,9 @@ function zoom(tile) {
596 // that lands in that window has no core to size, resize or key against. 602 // that lands in that window has no core to size, resize or key against.
597 // Gated HERE and not at each call because zoom is what makes the tile 603 // Gated HERE and not at each call because zoom is what makes the tile
598 // reachable at all — no zoom, no keys, no wheel, no paste (spec). 604 // reachable at all — no zoom, no keys, no wheel, no paste (spec).
599 if (!tile.core) return; 605 // `core` alone is not enough: a tile whose mux_init failed has a truthy
606 // core and no socket — connect() was never reached.
607 if (!tile.core || !tile.ws) return;
600 if (zoomedTile) unzoom(); 608 if (zoomedTile) unzoom();
601 zoomedTile = tile; 609 zoomedTile = tile;
602 tile.zoomed = true; 610 tile.zoomed = true;