Fix M4 reactivity bugs: active border, Ctrl+K, diagnostics
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
3c2f6b8640
commit
547b47ded4
5 changed files with 50 additions and 18 deletions
|
|
@ -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 @@
|
|||
<button class="palette-btn" onclick={() => (paletteOpen = true)} title="Jump to pane (Ctrl+K)">
|
||||
⌘K
|
||||
</button>
|
||||
<button class="palette-btn" onclick={() => handleNotify("test toast at " + new Date().toLocaleTimeString())} title="Fire a test toast">
|
||||
🔔
|
||||
</button>
|
||||
|
||||
<span class="layout-info">
|
||||
{leafCount(tree)} pane{leafCount(tree) === 1 ? "" : "s"}
|
||||
|
|
@ -264,7 +283,7 @@
|
|||
|
||||
<div class="pane-wrap">
|
||||
{#if ready}
|
||||
<Pane node={tree} {ops} />
|
||||
<Pane node={tree} {ops} {activeLeafId} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -7,16 +7,18 @@
|
|||
let {
|
||||
node,
|
||||
ops,
|
||||
activeLeafId,
|
||||
}: {
|
||||
node: TreeNode;
|
||||
ops: PaneOps;
|
||||
activeLeafId: string | null;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#if node.kind === "split"}
|
||||
<SplitNode {node} {ops} />
|
||||
<SplitNode {node} {ops} {activeLeafId} />
|
||||
{:else}
|
||||
{#key node.id}
|
||||
<LeafPane leaf={node} {ops} />
|
||||
<LeafPane leaf={node} {ops} {activeLeafId} />
|
||||
{/key}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
<div class="side" style="flex: {node.ratio}">
|
||||
<Pane node={node.a} {ops} />
|
||||
<Pane node={node.a} {ops} {activeLeafId} />
|
||||
</div>
|
||||
<div
|
||||
class="gutter"
|
||||
|
|
@ -60,7 +62,7 @@
|
|||
onpointercancel={onPointerUp}
|
||||
></div>
|
||||
<div class="side" style="flex: {1 - node.ratio}">
|
||||
<Pane node={node.b} {ops} />
|
||||
<Pane node={node.b} {ops} {activeLeafId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue