diff --git a/src/App.svelte b/src/App.svelte index 425740d..57ef4e5 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -5,12 +5,14 @@ saveWorkspace, loadWorkspace, writeToPane, - type PaneId, } from "./ipc"; import Pane from "./lib/layout/Pane.svelte"; - import Notifications, { type Toast } from "./components/Notifications.svelte"; + import Notifications from "./components/Notifications.svelte"; import Palette from "./components/Palette.svelte"; - import type { PaneOps } from "./lib/layout/ops"; + import { + provideOrchestration, + type TreeOps, + } from "./lib/layout/orchestration.svelte"; import { type TreeNode, type NodeId, @@ -36,20 +38,69 @@ const LEGACY_STORAGE_KEY = "tiletopia.tree.v1"; - let distros = $state([]); let defaultDistro = $state(undefined); let ready = $state(false); let tree = $state(newLeaf()); - - // ---- orchestration state (M4) ------------------------------------------- - // leafId -> backend PaneId. Plain Map (no reactivity needed — only read - // from event handlers). Repopulated on every XtermPane mount. - const paneIdByLeaf = new Map(); - let notifications = $state([]); - let nextNotifId = 1; - let activeLeafId = $state(null); let paletteOpen = $state(false); + // ---- tree mutation handlers (closures over tree $state) ----------------- + function handleSplit(leafId: NodeId, orientation: Orientation) { + const parent = findLeaf(tree, leafId); + const inherit = parent + ? { distro: parent.distro ?? defaultDistro, cwd: parent.cwd } + : { distro: defaultDistro }; + tree = splitLeaf(tree, leafId, orientation, inherit); + } + + function handleClose(leafId: NodeId) { + const next = closeLeaf(tree, leafId); + tree = next ?? newLeaf({ distro: defaultDistro }); + orch.clearActiveIf(leafId); + } + + function handleSetDistro(leafId: NodeId, distro: string) { + tree = changeDistro(tree, leafId, distro); + } + + function handleSetLabel(leafId: NodeId, label: string | undefined) { + tree = changeLabel(tree, leafId, label); + } + + function handleToggleBroadcast(leafId: NodeId) { + tree = toggleBroadcastInTree(tree, leafId); + } + + 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 = orch.paneIdByLeaf.get(leaf.id); + if (paneId == null) { + console.warn("[tiletopia] broadcast peer has no paneId yet:", leaf.id); + continue; + } + peers++; + writeToPane(paneId, dataB64).catch((e) => + console.warn("[tiletopia] broadcast write failed:", e), + ); + } + console.log("[tiletopia] broadcastFrom", originLeafId, "→", peers, "peer(s)"); + } + + const treeOps: TreeOps = { + split: handleSplit, + close: handleClose, + setDistro: handleSetDistro, + setLabel: handleSetLabel, + toggleBroadcast: handleToggleBroadcast, + broadcastFrom: handleBroadcastFrom, + }; + + // Provide the orchestration store. All Pane / SplitNode / LeafPane + // descendants consume it via `useOrchestration()` — no prop drilling. + const orch = provideOrchestration(treeOps); + function isInteractiveDistro(name: string): boolean { return !name.toLowerCase().startsWith("docker-desktop"); } @@ -82,8 +133,9 @@ // 3. Resolve default distro. try { - distros = await listDistros(); - defaultDistro = distros.find(isInteractiveDistro) ?? distros[0]; + const ds = await listDistros(); + orch.distros = ds; + defaultDistro = ds.find(isInteractiveDistro) ?? ds[0]; } catch (e) { console.warn("list_distros failed:", e); } @@ -128,97 +180,10 @@ paletteOpen = !paletteOpen; } } - window.addEventListener("keydown", onKey, true); // capture phase + window.addEventListener("keydown", onKey, true); return () => window.removeEventListener("keydown", onKey, true); }); - // ---- pane ops ------------------------------------------------------------ - function handleSplit(leafId: NodeId, orientation: Orientation) { - const parent = findLeaf(tree, leafId); - const inherit = parent - ? { distro: parent.distro ?? defaultDistro, cwd: parent.cwd } - : { distro: defaultDistro }; - tree = splitLeaf(tree, leafId, orientation, inherit); - } - - function handleClose(leafId: NodeId) { - const next = closeLeaf(tree, leafId); - tree = next ?? newLeaf({ distro: defaultDistro }); - if (activeLeafId === leafId) activeLeafId = null; - } - - function handleSetDistro(leafId: NodeId, distro: string) { - tree = changeDistro(tree, leafId, distro); - } - - function handleSetLabel(leafId: NodeId, label: string | undefined) { - tree = changeLabel(tree, leafId, label); - } - - 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) { - console.warn("[tiletopia] broadcast peer has no paneId yet:", leaf.id); - continue; - } - peers++; - writeToPane(paneId, dataB64).catch((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; - } - - function handleRegisterPaneId(leafId: NodeId, paneId: PaneId | null) { - if (paneId == null) paneIdByLeaf.delete(leafId); - else paneIdByLeaf.set(leafId, paneId); - } - - 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); - }, 5000); - } - - function dismissNotification(id: number) { - 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, - setDistro: handleSetDistro, - setLabel: handleSetLabel, - toggleBroadcast: handleToggleBroadcast, - broadcastFrom: handleBroadcastFrom, - setActivePane: handleSetActivePane, - registerPaneId: handleRegisterPaneId, - notify: handleNotify, - distros, - }); - // ---- preset layouts ------------------------------------------------------ function applyPreset(make: (d: { distro?: string }) => TreeNode) { const count = leafCount(tree); @@ -235,7 +200,7 @@ }); function onPalettePick(leafId: string) { - activeLeafId = leafId; + orch.setActive(leafId); paletteOpen = false; } @@ -245,11 +210,11 @@ tiletopia - {#if distros.length === 0} + {#if orch.distros.length === 0} no distros enumerated {:else} default: - {#each distros as d} + {#each orch.distros as d} - @@ -283,11 +248,14 @@
{#if ready} - + {/if}
- + orch.dismiss(id)} + /> {#if paletteOpen} 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));
{}} > - {#each ops.distros as d} + {#each orch.distros as d} @@ -196,19 +184,19 @@ @@ -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; } diff --git a/src/lib/layout/Pane.svelte b/src/lib/layout/Pane.svelte index 06a5094..f76a062 100644 --- a/src/lib/layout/Pane.svelte +++ b/src/lib/layout/Pane.svelte @@ -1,24 +1,15 @@ {#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 817f40f..b4fe6e7 100644 --- a/src/lib/layout/SplitNode.svelte +++ b/src/lib/layout/SplitNode.svelte @@ -1,17 +1,8 @@