Svelte 5's template reactivity on \`class:active={activeLeafId === leaf.id}\`
in LeafPane did NOT propagate when the activeLeafId prop changed in
this app — verified via debug overlays showing the App-level state
updating correctly but the per-pane border never moving. Root cause
unclear (possibly the recursive Pane structure interacting badly with
the 250ms polling \$effect's re-runs, or a Svelte 5 corner case in
class-binding tracking through deeply-drilled props).
Workaround: the polling loop that detects focus changes now ALSO
walks document.querySelectorAll("[data-leaf-id].leaf") on every tick
and directly toggles the .active class via element.classList. If
Svelte re-renders and reverts, the next 250ms tick puts it back.
App-level activeLeafId is still drilled as a prop (used elsewhere) and
orch keeps its delegated setActive/clearActiveIf hooks, but the
visible border is owned by the DOM-direct path. Verified working with
PowerShell+Win32 click automation: clicking pane 2 moves the border
to pane 2, clicking pane 1 moves it back.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- 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>
Backend:
- save_workspace / load_workspace Tauri commands writing to
%APPDATA%\com.megaproxy.tiletopia\workspace.json with atomic
tmp+rename. Path from app.path().app_config_dir() (no dirs crate).
Layout helpers:
- tree.ts: changeDistro (with id swap to force XtermPane remount via
{#key}), changeLabel, presetSingle / TwoColumns / ThreeColumns /
TwoRows / TwoByTwo.
- New ops.ts with PaneOps interface bundling split / close /
setDistro / setLabel / distros, drilled through Pane chain
instead of individual callbacks.
UI:
- LeafPane: in-toolbar editable label (click to rename, Enter
saves, Esc cancels) and distro chip popover. Picking a different
distro respawns the pane.
- App.svelte: migrated from localStorage to APPDATA via the new
Tauri commands, debounced 500ms. One-time localStorage migration
on boot. Split inherits parent's distro+cwd. Titlebar preset
buttons with confirm when replacing >1 pane.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/lib/layout/tree.ts: pure helpers + types (newLeaf, splitLeaf,
closeLeaf, replaceById, serialize/deserialize with shape-checking).
- SplitNode.svelte: flex container with pointer-captured gutter drag.
- LeafPane.svelte: per-pane toolbar (split-right ⇥, split-down ⇣,
close ×) over the existing XtermPane.
- Pane.svelte: recursive dispatcher between SplitNode and LeafPane,
keyed on leaf.id so swaps unmount XtermPane cleanly (kills PTY).
- App.svelte: tree-as-state with split/close handlers, auto-save to
localStorage on every \$effect tick. Titlebar shows clickable distro
buttons setting the default for new panes; existing panes keep theirs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>