From 547b47ded4e3269947fc5ac51bc2c2652af1a2fb Mon Sep 17 00:00:00 2001 From: megaproxy Date: Fri, 22 May 2026 13:20:11 +0100 Subject: [PATCH] Fix M4 reactivity bugs: active border, Ctrl+K, diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drill activeLeafId as a separate prop through Pane -> SplitNode -> LeafPane instead of bundling it into the \$derived ops object. Passing activeLeafId via ops caused subsequent focus changes to not propagate to children (LeafPane's active = \$derived(...) wasn't re-evaluating when ops's identity changed). Drilling sidesteps any prop-as-derived-object reactivity quirks. - Ctrl+K listener now uses capture phase so it wins over xterm.js's keydown handler inside the focused terminal. - Bump active/broadcasting borders to 2px and brighter colors so the visual change is unmissable. - Add a 🔔 test-toast button in the titlebar to verify the notification pipeline independently of idle detection. - Sprinkle console.log diagnostics through the active/broadcast/ idle/notify flows so we can pinpoint any remaining issues from devtools next time something looks off. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/App.svelte | 31 +++++++++++++++++++++++++------ src/lib/layout/LeafPane.svelte | 23 +++++++++++++++++------ src/lib/layout/Pane.svelte | 6 ++++-- src/lib/layout/SplitNode.svelte | 6 ++++-- src/lib/layout/ops.ts | 2 -- 5 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/App.svelte b/src/App.svelte index 3464f08..425740d 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -118,15 +118,18 @@ }); // ---- Ctrl+K palette toggle ---------------------------------------------- + // Capture phase so we win over xterm.js's keystroke capture inside terminals. $effect(() => { function onKey(e: KeyboardEvent) { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { + console.log("[tiletopia] Ctrl+K caught, paletteOpen ->", !paletteOpen); e.preventDefault(); + e.stopPropagation(); paletteOpen = !paletteOpen; } } - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); + window.addEventListener("keydown", onKey, true); // capture phase + return () => window.removeEventListener("keydown", onKey, true); }); // ---- pane ops ------------------------------------------------------------ @@ -154,21 +157,30 @@ function handleToggleBroadcast(leafId: NodeId) { tree = toggleBroadcastInTree(tree, leafId); + const updated = findLeaf(tree, leafId); + console.log("[tiletopia] toggleBroadcast:", leafId, "now:", updated?.broadcast); } function handleBroadcastFrom(originLeafId: NodeId, dataB64: string) { + let peers = 0; for (const leaf of walkLeaves(tree)) { if (leaf.id === originLeafId) continue; if (!leaf.broadcast) continue; const paneId = paneIdByLeaf.get(leaf.id); - if (paneId == null) continue; + if (paneId == null) { + console.warn("[tiletopia] broadcast peer has no paneId yet:", leaf.id); + continue; + } + peers++; writeToPane(paneId, dataB64).catch((e) => - console.warn("broadcast write failed:", e), + console.warn("[tiletopia] broadcast write failed:", e), ); } + console.log("[tiletopia] broadcastFrom", originLeafId, "→", peers, "peer(s)"); } function handleSetActivePane(leafId: NodeId) { + console.log("[tiletopia] setActivePane:", leafId, "(was:", activeLeafId, ")"); activeLeafId = leafId; } @@ -179,6 +191,7 @@ function handleNotify(message: string) { const id = nextNotifId++; + console.log("[tiletopia] notify:", message, "(id:", id, ")"); notifications.push({ id, message }); setTimeout(() => { notifications = notifications.filter((n) => n.id !== id); @@ -189,6 +202,10 @@ notifications = notifications.filter((n) => n.id !== id); } + // Note: activeLeafId is NOT in ops — it's drilled as a separate prop + // through Pane / SplitNode so each LeafPane reactively picks up changes. + // Bundling it in ops via $derived caused subsequent activeLeafId changes + // to not propagate to children (Svelte 5 prop-as-derived-object quirk). const ops: PaneOps = $derived({ split: handleSplit, close: handleClose, @@ -200,7 +217,6 @@ registerPaneId: handleRegisterPaneId, notify: handleNotify, distros, - activeLeafId, }); // ---- preset layouts ------------------------------------------------------ @@ -256,6 +272,9 @@ + {leafCount(tree)} pane{leafCount(tree) === 1 ? "" : "s"} @@ -264,7 +283,7 @@
{#if ready} - + {/if}
diff --git a/src/lib/layout/LeafPane.svelte b/src/lib/layout/LeafPane.svelte index 2defc64..937b9cb 100644 --- a/src/lib/layout/LeafPane.svelte +++ b/src/lib/layout/LeafPane.svelte @@ -7,12 +7,14 @@ let { leaf, ops, + activeLeafId, }: { leaf: LeafNode; ops: PaneOps; + activeLeafId: string | null; } = $props(); - const active = $derived(ops.activeLeafId === leaf.id); + const active = $derived(activeLeafId === leaf.id); let status = $state("starting…"); let statusOk = $state(true); @@ -82,9 +84,11 @@ function checkIdle() { if (notifiedThisIdle) return; - if (Date.now() - lastDataTime >= IDLE_THRESHOLD_MS) { + const sinceLast = Date.now() - lastDataTime; + if (sinceLast >= IDLE_THRESHOLD_MS) { notifiedThisIdle = true; const name = leaf.label ?? leaf.distro ?? "pane"; + console.log("[tiletopia] notifying idle:", leaf.id, "quietForMs:", sinceLast); ops.notify(`${name} is idle`); } } @@ -96,7 +100,10 @@ // ---- broadcast ----------------------------------------------------------- function onTerminalInput(b64: string) { - if (leaf.broadcast) ops.broadcastFrom(leaf.id, b64); + if (leaf.broadcast) { + console.log("[tiletopia] broadcasting from:", leaf.id); + ops.broadcastFrom(leaf.id, b64); + } } // ---- focus / active ------------------------------------------------------ @@ -107,6 +114,7 @@ }); function onPaneClick() { + console.log("[tiletopia] pane click:", leaf.id, "currentlyActive:", active); if (!active) ops.setActivePane(leaf.id); } @@ -232,14 +240,17 @@ border: 1px solid transparent; box-sizing: border-box; } + .leaf { + border-width: 2px; + } .leaf.active { - border-color: #3a5a8c; + border-color: #5a8cd8; } .leaf.broadcasting { - border-color: #c98a1f; + border-color: #e09838; } .leaf.active.broadcasting { - border-color: #e0a432; + border-color: #ffb840; } .pane-toolbar { diff --git a/src/lib/layout/Pane.svelte b/src/lib/layout/Pane.svelte index 79bf308..06a5094 100644 --- a/src/lib/layout/Pane.svelte +++ b/src/lib/layout/Pane.svelte @@ -7,16 +7,18 @@ let { node, ops, + activeLeafId, }: { node: TreeNode; ops: PaneOps; + activeLeafId: string | null; } = $props(); {#if node.kind === "split"} - + {:else} {#key node.id} - + {/key} {/if} diff --git a/src/lib/layout/SplitNode.svelte b/src/lib/layout/SplitNode.svelte index ed6f100..817f40f 100644 --- a/src/lib/layout/SplitNode.svelte +++ b/src/lib/layout/SplitNode.svelte @@ -6,9 +6,11 @@ let { node, ops, + activeLeafId, }: { node: SplitNode; ops: PaneOps; + activeLeafId: string | null; } = $props(); let containerEl: HTMLDivElement; @@ -45,7 +47,7 @@ bind:this={containerEl} >
- +
- +
diff --git a/src/lib/layout/ops.ts b/src/lib/layout/ops.ts index 52e12b7..8eb84a1 100644 --- a/src/lib/layout/ops.ts +++ b/src/lib/layout/ops.ts @@ -31,6 +31,4 @@ export interface PaneOps { // ---- data /** All distros known to the backend; populated once at app start. */ distros: string[]; - /** The currently-focused pane, if any. */ - activeLeafId: NodeId | null; }