From 2a0c096095d67d66734fc3ef5ab520c4a3904277 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Fri, 22 May 2026 18:46:56 +0100 Subject: [PATCH] Fix broadcast no-op: stop depending on orch object in LeafPane effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bug: clicking 📡 made the visual update (orange border) but typing in a broadcasting pane only wrote to that pane — peers never received the keystrokes. Root cause: the orch context value (useMemo'd over activeLeafId, distros, and the operation callbacks) is recreated every time activeLeafId changes (i.e. every click). useEffect cleanups in LeafPane that had `orch` in their deps fired their cleanup-then-setup cycle on every click. The unmount-cleanup for paneId registration ran `orch.registerPaneId(leaf.id, null)`, silently deleting paneIds from App's paneIdByLeafRef map — so when broadcastFrom later walked the tree looking up peers, the map returned undefined for every leaf and the actual writeToPane calls never happened. Fix: depend on the specific stable method references (`orch.registerPaneId`, `orch.notify`, etc.) instead of the orch object itself. The methods are all useCallback'd with stable deps in App.tsx, so their references don't change across orch object recreations — effect deps stay stable, no spurious cleanup. Applied the same fix to all orch-using effects/callbacks in LeafPane (commitLabel, pickDistro, onPaneClick, onPaneSpawned, onXtermFocus, onTerminalInput, idle interval, paneId cleanup). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/layout/LeafPane.tsx | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/lib/layout/LeafPane.tsx b/src/lib/layout/LeafPane.tsx index 73d8f49..e8e1ffe 100644 --- a/src/lib/layout/LeafPane.tsx +++ b/src/lib/layout/LeafPane.tsx @@ -41,7 +41,7 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) { if (!editingLabel) return; orch.setLabel(leaf.id, labelDraft); setEditingLabel(false); - }, [editingLabel, orch, leaf.id, labelDraft]); + }, [editingLabel, orch.setLabel, leaf.id, labelDraft]); const cancelLabel = useCallback(() => setEditingLabel(false), []); const onLabelKey = useCallback( (e: KeyboardEvent) => { @@ -67,7 +67,7 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) { setDistroOpen(false); if (d !== leaf.distro) orch.setDistro(leaf.id, d); }, - [orch, leaf.id, leaf.distro], + [orch.setDistro, leaf.id, leaf.distro], ); // Dismiss popover on outside click useEffect(() => { @@ -95,14 +95,17 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) { } }, 1000); return () => clearInterval(id); - }, [leaf.label, leaf.distro, orch]); + // Depend on the stable notify function, not the whole orch object. + // orch is recreated every time activeLeafId/distros change; depending + // on it would tear down and rebuild this interval on every click. + }, [leaf.label, leaf.distro, orch.notify]); // ---- broadcast --------------------------------------------------------- const onTerminalInput = useCallback( (b64: string) => { if (isBroadcasting) orch.broadcastFrom(leaf.id, b64); }, - [isBroadcasting, orch, leaf.id], + [isBroadcasting, orch.broadcastFrom, leaf.id], ); // ---- focus / active highlighting --------------------------------------- @@ -114,20 +117,26 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) { const onPaneClick = useCallback(() => { orch.setActive(leaf.id); - }, [orch, leaf.id]); + }, [orch.setActive, leaf.id]); const onPaneSpawned = useCallback( (paneId: number) => { orch.registerPaneId(leaf.id, paneId); }, - [orch, leaf.id], + [orch.registerPaneId, leaf.id], ); - // Unregister on unmount + // Unregister on TRUE unmount only — depending on `orch` here would + // delete the paneId from App's lookup on every activeLeafId change, + // which broke broadcast routing (peers found, but their paneIds + // had been silently removed from the map). useEffect(() => { return () => orch.registerPaneId(leaf.id, null); - }, [orch, leaf.id]); + }, [orch.registerPaneId, leaf.id]); - const onXtermFocus = useCallback(() => orch.setActive(leaf.id), [orch, leaf.id]); + const onXtermFocus = useCallback( + () => orch.setActive(leaf.id), + [orch.setActive, leaf.id], + ); const onStatus = useCallback((msg: string, ok: boolean) => { setStatus(msg);