Restore DOM-direct workarounds; throttle gutter drag

After this session's diagnostic confirmed the root cause (Svelte 5
prop reactivity does NOT propagate through Pane → SplitNode → LeafPane
in this app — each LeafPane captures props at mount and never sees
updates), restored the brute-force DOM workarounds that were proven
to work and threw in a throttle for the gutter drag.

What changed vs the broken intermediate state:
- App polling: re-sync .leaf.active, .leaf.broadcasting, .bcast-chip.on
  classes from tree+activeLeafId state every 250ms. Bypasses Svelte
  reactivity entirely.
- SplitNode drag: rAF-throttle the direct flex update so we stop
  spamming SIGWINCH to the PTYs (which was making shells redraw
  prompts repeatedly, creating the visual artifacts the user reported).
- Close: keep the targeted PTY-kill + DOM-hide-the-side approach so
  panes visually disappear and siblings fill via flex auto-allocation.

This isn't pretty, but it works. The proper fix is to either find /
file the Svelte 5 bug, or migrate the frontend to a framework whose
reactivity we can trust. Both deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-22 16:49:00 +01:00
parent 058ce49d3b
commit 8d5c49155b
3 changed files with 47 additions and 32 deletions

View file

@ -14,10 +14,6 @@
const orch = useOrchestration();
// Diagnostic — log on every prop change.
$effect(() => {
console.log("[LeafPane render]", leaf.id.slice(0, 8), "activeLeafId=", activeLeafId?.slice(0, 8) ?? "null", "match=", activeLeafId === leaf.id);
});
let status = $state("starting…");
let statusOk = $state(true);

View file

@ -19,6 +19,12 @@
e.preventDefault();
}
// Throttle the per-pointermove DOM flex update so we don't fire SIGWINCH
// hundreds of times per second during a drag (causes the shell to redraw
// its prompt repeatedly, leaving visual artifacts).
let pendingRaf: number | null = null;
let pendingRatio = 0;
function onPointerMove(e: PointerEvent) {
if (!dragging || !containerEl) return;
const rect = containerEl.getBoundingClientRect();
@ -28,13 +34,16 @@
if (size <= 0) return;
const r = Math.max(0.05, Math.min(0.95, pos / size));
node.ratio = r;
// Brute-force DOM: Svelte's `style="flex: {node.ratio}"` template binding
// doesn't propagate ratio changes in this app. Update the .side flex
// styles directly so the drag actually moves the gutter visually.
const sides = containerEl.querySelectorAll(":scope > .side");
if (sides[0]) (sides[0] as HTMLElement).style.flex = String(r);
if (sides[1]) (sides[1] as HTMLElement).style.flex = String(1 - r);
pendingRatio = r;
if (pendingRaf == null) {
pendingRaf = requestAnimationFrame(() => {
pendingRaf = null;
// Direct DOM flex update — Svelte's template binding doesn't react.
const sides = containerEl.querySelectorAll(":scope > .side");
if (sides[0]) (sides[0] as HTMLElement).style.flex = String(pendingRatio);
if (sides[1]) (sides[1] as HTMLElement).style.flex = String(1 - pendingRatio);
});
}
}
function onPointerUp(e: PointerEvent) {