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:
parent
058ce49d3b
commit
8d5c49155b
3 changed files with 47 additions and 32 deletions
|
|
@ -66,17 +66,15 @@
|
|||
}
|
||||
|
||||
function handleClose(leafId: NodeId) {
|
||||
// Kill the PTY explicitly — Svelte's onDestroy on LeafPane won't fire
|
||||
// because Svelte isn't unmounting the component (same reactivity gap as
|
||||
// the active/broadcast bugs).
|
||||
// Kill the PTY directly (LeafPane's onDestroy won't fire — same
|
||||
// Svelte 5 reactivity wall affecting unmount as it does prop updates).
|
||||
const paneId = orch.paneIdByLeaf.get(leafId);
|
||||
if (paneId != null) {
|
||||
void killPane(paneId).catch((e) => console.warn("killPane failed:", e));
|
||||
orch.paneIdByLeaf.delete(leafId);
|
||||
}
|
||||
|
||||
// Brute-force hide the closed pane's flex side + the adjacent gutter so
|
||||
// the sibling pane visually fills the freed space.
|
||||
// Hide the closed pane's flex container and the adjacent gutter so the
|
||||
// sibling pane visually fills the freed space.
|
||||
const leafEl = document.querySelector(`[data-leaf-id="${leafId}"]`);
|
||||
const sideEl = leafEl?.closest(".side") as HTMLElement | null;
|
||||
const splitEl = sideEl?.parentElement;
|
||||
|
|
@ -87,7 +85,6 @@
|
|||
if (child.classList.contains("gutter")) child.style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
// Update tree state (used by broadcast routing, palette, persistence).
|
||||
const next = closeLeaf(tree, leafId);
|
||||
tree = next ?? newLeaf({ distro: defaultDistro });
|
||||
|
|
@ -227,9 +224,25 @@
|
|||
// template re-evaluation reliably (root cause unclear — likely a Svelte 5
|
||||
// interaction with our recursive Pane / setInterval pattern). So we ALSO
|
||||
// manipulate `.leaf.active` directly via DOM as a backstop.
|
||||
// ---- Active-pane / broadcast DOM sync (Svelte 5 reactivity workaround) --
|
||||
//
|
||||
// Empirically verified: prop drilling through Pane → SplitNode → LeafPane
|
||||
// does NOT propagate reactively in this app. Each LeafPane captures its
|
||||
// initial prop values and never sees updates (proved with inline-toolbar
|
||||
// diagnostics: every pane showed the same `A:` value at mount and it
|
||||
// never changed even as App-level activeLeafId did). Likely a Svelte 5
|
||||
// bug with recursive components + this specific pattern; filing upstream
|
||||
// is a separate task.
|
||||
//
|
||||
// Workaround: a 250ms polling loop in App that
|
||||
// 1. reads document.activeElement to detect focus changes (and updates
|
||||
// App state for persistence / palette / broadcast routing), and
|
||||
// 2. directly toggles `.leaf.active`, `.leaf.broadcasting`, and
|
||||
// `.bcast-chip.on` classes via DOM API — bypassing Svelte entirely.
|
||||
$effect(() => {
|
||||
let lastLeafId: string | null = null;
|
||||
const interval = window.setInterval(() => {
|
||||
// 1. Focus detection → setActive
|
||||
const el = document.activeElement;
|
||||
const leafEl = el?.closest("[data-leaf-id]");
|
||||
const id = leafEl?.getAttribute("data-leaf-id") ?? null;
|
||||
|
|
@ -237,21 +250,18 @@
|
|||
lastLeafId = id;
|
||||
setActive(id);
|
||||
}
|
||||
// Active-border DOM sync (same workaround as below).
|
||||
if (id) {
|
||||
document.querySelectorAll("[data-leaf-id].leaf").forEach((el) => {
|
||||
if (el.getAttribute("data-leaf-id") === id) el.classList.add("active");
|
||||
else el.classList.remove("active");
|
||||
});
|
||||
}
|
||||
// Broadcast-state DOM sync — Svelte's class:broadcasting={leaf.broadcast}
|
||||
// and class:on={leaf.broadcast} bindings in LeafPane don't propagate
|
||||
// either. Walk the tree and force the classes to match leaf.broadcast.
|
||||
// 2. Active border DOM sync
|
||||
document.querySelectorAll("[data-leaf-id].leaf").forEach((el) => {
|
||||
const elId = el.getAttribute("data-leaf-id");
|
||||
if (elId === activeLeafId) el.classList.add("active");
|
||||
else el.classList.remove("active");
|
||||
});
|
||||
// 3. Broadcast DOM sync (read from tree, write to DOM)
|
||||
for (const leaf of walkLeaves(tree)) {
|
||||
const leafEl = document.querySelector(`[data-leaf-id="${leaf.id}"]`);
|
||||
if (!leafEl) continue;
|
||||
leafEl.classList.toggle("broadcasting", !!leaf.broadcast);
|
||||
const chip = leafEl.querySelector(".bcast-chip");
|
||||
const el = document.querySelector(`[data-leaf-id="${leaf.id}"]`);
|
||||
if (!el) continue;
|
||||
el.classList.toggle("broadcasting", !!leaf.broadcast);
|
||||
const chip = el.querySelector(".bcast-chip");
|
||||
if (chip) chip.classList.toggle("on", !!leaf.broadcast);
|
||||
}
|
||||
}, 250);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue