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:
megaproxy 2026-05-22 13:20:11 +01:00
parent 3c2f6b8640
commit 547b47ded4
5 changed files with 50 additions and 18 deletions

View file

@ -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 {

View file

@ -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}

View file

@ -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>

View file

@ -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;
}