Fix M4 reactivity bugs via context + class store
Symptoms in v0.1.0 install: 📡 broadcast button didn't change color
on toggle, × close button didn't remove the pane, blue active
border stuck on the first pane. All three were UI-not-rerendering-
on-state-change manifestations of the same prop-reactivity quirk
that drilling activeLeafId tried (and apparently failed) to fix.
Refactor to the Svelte 5 canonical pattern for shared reactive
state:
- New src/lib/layout/orchestration.svelte.ts with an Orchestration
class. Reactive fields (activeLeafId, notifications, distros) are
class-field $state declarations; methods mutate them directly.
Provided via context (provideOrchestration / useOrchestration);
no prop drilling.
- App.svelte: provideOrchestration(treeOps). Tree mutations remain
closures over the App-level tree $state; the class delegates to
them. Pane only takes `node` now.
- Pane.svelte / SplitNode.svelte: stop drilling ops + activeLeafId.
Pure pass-through of node.
- LeafPane.svelte: useOrchestration(); `active = $derived(
orch.activeLeafId === leaf.id)` reads the class field directly so
Svelte 5 tracks it per-property.
- Notifications.svelte: receives notifications + onDismiss from App
(which gets them from orch).
- Deleted src/lib/layout/ops.ts (TreeOps moved into orchestration).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6b9a3adf85
commit
e871ee8e6e
6 changed files with 243 additions and 204 deletions
|
|
@ -1,20 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy } from "svelte";
|
||||
import type { LeafNode } from "./tree";
|
||||
import type { PaneOps } from "./ops";
|
||||
import { useOrchestration } from "./orchestration.svelte";
|
||||
import XtermPane from "../../components/XtermPane.svelte";
|
||||
|
||||
let {
|
||||
leaf,
|
||||
ops,
|
||||
activeLeafId,
|
||||
}: {
|
||||
leaf: LeafNode;
|
||||
ops: PaneOps;
|
||||
activeLeafId: string | null;
|
||||
} = $props();
|
||||
let { leaf }: { leaf: LeafNode } = $props();
|
||||
|
||||
const active = $derived(activeLeafId === leaf.id);
|
||||
const orch = useOrchestration();
|
||||
|
||||
// Derives directly from orch.activeLeafId — Svelte 5 tracks the class
|
||||
// field access on every re-evaluation. No prop drilling involved.
|
||||
const active = $derived(orch.activeLeafId === leaf.id);
|
||||
|
||||
let status = $state("starting…");
|
||||
let statusOk = $state(true);
|
||||
|
|
@ -33,7 +29,7 @@
|
|||
|
||||
function commitLabel() {
|
||||
if (!editingLabel) return;
|
||||
ops.setLabel(leaf.id, labelDraft);
|
||||
orch.setLabel(leaf.id, labelDraft);
|
||||
editingLabel = false;
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +57,7 @@
|
|||
|
||||
function pickDistro(d: string) {
|
||||
distroOpen = false;
|
||||
if (d !== leaf.distro) ops.setDistro(leaf.id, d);
|
||||
if (d !== leaf.distro) orch.setDistro(leaf.id, d);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
|
|
@ -75,7 +71,6 @@
|
|||
const IDLE_THRESHOLD_MS = 5000;
|
||||
let lastDataTime = Date.now();
|
||||
let notifiedThisIdle = false;
|
||||
let idleTimer: number | null = null;
|
||||
|
||||
function onDataReceived() {
|
||||
lastDataTime = Date.now();
|
||||
|
|
@ -89,21 +84,16 @@
|
|||
notifiedThisIdle = true;
|
||||
const name = leaf.label ?? leaf.distro ?? "pane";
|
||||
console.log("[tiletopia] notifying idle:", leaf.id, "quietForMs:", sinceLast);
|
||||
ops.notify(`${name} is idle`);
|
||||
orch.notify(`${name} is idle`);
|
||||
}
|
||||
}
|
||||
|
||||
idleTimer = window.setInterval(checkIdle, 1000);
|
||||
onDestroy(() => {
|
||||
if (idleTimer != null) clearInterval(idleTimer);
|
||||
});
|
||||
const idleTimer = window.setInterval(checkIdle, 1000);
|
||||
onDestroy(() => clearInterval(idleTimer));
|
||||
|
||||
// ---- broadcast -----------------------------------------------------------
|
||||
function onTerminalInput(b64: string) {
|
||||
if (leaf.broadcast) {
|
||||
console.log("[tiletopia] broadcasting from:", leaf.id);
|
||||
ops.broadcastFrom(leaf.id, b64);
|
||||
}
|
||||
if (leaf.broadcast) orch.broadcastFrom(leaf.id, b64);
|
||||
}
|
||||
|
||||
// ---- focus / active ------------------------------------------------------
|
||||
|
|
@ -115,21 +105,19 @@
|
|||
|
||||
function onPaneClick() {
|
||||
console.log("[tiletopia] pane click:", leaf.id, "currentlyActive:", active);
|
||||
if (!active) ops.setActivePane(leaf.id);
|
||||
if (!active) orch.setActive(leaf.id);
|
||||
}
|
||||
|
||||
// ---- pane id registration ------------------------------------------------
|
||||
function onPaneSpawned(paneId: number) {
|
||||
ops.registerPaneId(leaf.id, paneId);
|
||||
orch.registerPaneId(leaf.id, paneId);
|
||||
}
|
||||
onDestroy(() => {
|
||||
ops.registerPaneId(leaf.id, null);
|
||||
});
|
||||
onDestroy(() => orch.registerPaneId(leaf.id, null));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="leaf"
|
||||
class:active
|
||||
class:active={active}
|
||||
class:broadcasting={leaf.broadcast}
|
||||
role="group"
|
||||
aria-label={"Terminal pane: " + (leaf.label ?? leaf.distro ?? "unnamed")}
|
||||
|
|
@ -171,7 +159,7 @@
|
|||
tabindex="-1"
|
||||
onkeydown={() => {}}
|
||||
>
|
||||
{#each ops.distros as d}
|
||||
{#each orch.distros as d}
|
||||
<button
|
||||
class="distro-menu-item"
|
||||
class:active={d === leaf.distro}
|
||||
|
|
@ -185,7 +173,7 @@
|
|||
<button
|
||||
class="bcast-chip"
|
||||
class:on={leaf.broadcast}
|
||||
onclick={(e) => { e.stopPropagation(); ops.toggleBroadcast(leaf.id); }}
|
||||
onclick={(e) => { e.stopPropagation(); orch.toggleBroadcast(leaf.id); }}
|
||||
title={leaf.broadcast ? "Broadcasting (click to leave group)" : "Click to broadcast input to other broadcast panes"}
|
||||
aria-pressed={leaf.broadcast ? "true" : "false"}
|
||||
>📡</button>
|
||||
|
|
@ -196,19 +184,19 @@
|
|||
<button
|
||||
class="pane-btn"
|
||||
title="Split right"
|
||||
onclick={(e) => { e.stopPropagation(); ops.split(leaf.id, "h"); }}
|
||||
onclick={(e) => { e.stopPropagation(); orch.split(leaf.id, "h"); }}
|
||||
aria-label="Split right"
|
||||
>⇥</button>
|
||||
<button
|
||||
class="pane-btn"
|
||||
title="Split down"
|
||||
onclick={(e) => { e.stopPropagation(); ops.split(leaf.id, "v"); }}
|
||||
onclick={(e) => { e.stopPropagation(); orch.split(leaf.id, "v"); }}
|
||||
aria-label="Split down"
|
||||
>⇣</button>
|
||||
<button
|
||||
class="pane-btn close"
|
||||
title="Close pane"
|
||||
onclick={(e) => { e.stopPropagation(); ops.close(leaf.id); }}
|
||||
onclick={(e) => { e.stopPropagation(); orch.close(leaf.id); }}
|
||||
aria-label="Close pane"
|
||||
>×</button>
|
||||
</span>
|
||||
|
|
@ -237,12 +225,9 @@
|
|||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border: 1px solid transparent;
|
||||
border: 2px solid transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.leaf {
|
||||
border-width: 2px;
|
||||
}
|
||||
.leaf.active {
|
||||
border-color: #5a8cd8;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue