Commit graph

11 commits

Author SHA1 Message Date
8d5c49155b Restore DOM-direct workarounds; throttle gutter drag
After this session's diagnostic confirmed the root cause (Svelte 5
prop reactivity does NOT propagate through Pane → SplitNode → LeafPane
in this app — each LeafPane captures props at mount and never sees
updates), restored the brute-force DOM workarounds that were proven
to work and threw in a throttle for the gutter drag.

What changed vs the broken intermediate state:
- App polling: re-sync .leaf.active, .leaf.broadcasting, .bcast-chip.on
  classes from tree+activeLeafId state every 250ms. Bypasses Svelte
  reactivity entirely.
- SplitNode drag: rAF-throttle the direct flex update so we stop
  spamming SIGWINCH to the PTYs (which was making shells redraw
  prompts repeatedly, creating the visual artifacts the user reported).
- Close: keep the targeted PTY-kill + DOM-hide-the-side approach so
  panes visually disappear and siblings fill via flex auto-allocation.

This isn't pretty, but it works. The proper fix is to either find /
file the Svelte 5 bug, or migrate the frontend to a framework whose
reactivity we can trust. Both deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:49:00 +01:00
058ce49d3b Force gutter-drag resize via direct DOM (same workaround)
Dragging the splitter set node.ratio in the Svelte $state tree
correctly (used by save-restore), but the template binding
style=\"flex: {node.ratio}\" on each .side div didn't re-evaluate
when ratio changed — same prop-reactivity wall we hit with the
active border and broadcast color. The gutter would drag invisibly:
internal state moved, panes stayed at their original ratio.

Workaround: SplitNode's onPointerMove now ALSO writes the flex
style directly to the two .side elements via DOM. Svelte still owns
node.ratio for persistence/serialization, but the visual is owned
by the imperative DOM write.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:33:21 +01:00
854201be84 Force active-border via direct DOM manipulation in polling loop
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>
2026-05-22 16:15:23 +01:00
f5f788652e Fix active-pane detection via activeElement polling
After exhausting event-based approaches that all failed in WebView2:
- per-leaf onpointerdown: xterm.js stopPropagation
- document-capture pointerdown: only first event ever delivered
- document-capture mousedown/click: never delivered at all
- document-capture focusin: silently fails
- term.onFocus: no such xterm.js API

The bulletproof fallback: poll document.activeElement every 250ms
and call orch.setActive on its closest [data-leaf-id] ancestor.
No DOM events involved. Verified working with automation: clicking
pane 2 turns its border blue, clicking pane 1 moves the border to
pane 1, etc.

XtermPane gained an onFocus prop (still wired through LeafPane) as a
secondary signal that might fire in some configurations, but the
polling is the actual fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:43:30 +01:00
96a9180f3b Fix active-pane click via document-capture pointerdown
Root cause: xterm.js attaches its own pointerdown handler inside the
terminal and calls e.stopPropagation(), which prevents the .leaf
div's onpointerdown from firing for any click landing inside the
terminal body. That's why clicking pane bodies never moved the blue
active border — the event simply never reached our handler.

Fix: register a document-level CAPTURE-phase pointerdown listener
in App.svelte. Capture fires before xterm.js's bubble-phase handler
runs (and before it can stop propagation), so we always see the
click. The handler walks up via Element.closest('[data-leaf-id]')
to find which pane was clicked, then calls orch.setActive.

- LeafPane.svelte: add data-leaf-id={leaf.id} attribute so the
  document handler can identify the clicked pane.
- App.svelte: $effect attaches document.addEventListener('pointerdown',
  ..., true) and cleans up on teardown.
- Keep the per-leaf onpointerdown as a redundant backup for clicks
  on toolbar buttons (which sit outside the xterm subtree). Cheap
  + idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:35:59 +01:00
e871ee8e6e 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>
2026-05-22 13:59:34 +01:00
b1412287be Add vitest + 43 unit tests for tree.ts
Setup:
- vitest 2.x devDep; pnpm test / pnpm test:watch scripts.
- vite.config.ts test: block (node env, src/**/*.test.ts) via vitest/config.

Coverage in tree.test.ts:
- newLeaf / newSplit (defaults + provided props).
- replaceById (root/nested/no-match, immutability + sibling reuse).
- splitLeaf (orientation, inheritance, no-op on missing id, nested).
- closeLeaf (root -> null, sibling collapse, nested removal, no-op).
- findLeaf / leafCount / walkLeaves (order).
- changeDistro pins the invariant that it MUST swap the leaf id
  ({#key} remounts XtermPane → kills+respawns PTY).
- changeLabel / toggleBroadcast pin the inverse invariant: id MUST
  remain stable (metadata-only mutations).
- All 5 presets: shape, distro propagation, fresh ids per call.
- serialize/deserialize roundtrip + invalid-input rejection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:28:02 +01:00
547b47ded4 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>
2026-05-22 13:20:11 +01:00
3c2f6b8640 Add M4 orchestration: broadcast, idle notifications, palette
tree.ts
- LeafNode gains broadcast?: boolean
- walkLeaves(root) generator; toggleBroadcast helper

ops.ts (PaneOps)
- toggleBroadcast, broadcastFrom, setActivePane, registerPaneId,
  notify; activeLeafId data field.

XtermPane.svelte
- onSpawn(paneId), onInput(b64), onDataReceived(),
  and focusTrigger prop. All optional; backward-compatible.

LeafPane.svelte
- 📡 broadcast toggle; 5s idle detection -> ops.notify (once per
  idle cycle); active + broadcasting border colors; click-to-focus
  via setActivePane + focusTrigger bump.

New Notifications.svelte
- Top-right toast stack, slide-in, 5s auto-dismiss + click ×.

New Palette.svelte
- Modal overlay, backdrop, filtered leaf list with ↑/↓ + Enter,
  Escape to close.

App.svelte
- paneIdByLeaf Map for routing; notifications array + auto-dismiss;
  activeLeafId; Ctrl+K global listener; broadcastFrom routes via
  walkLeaves + writeToPane to all other broadcast leaves; ⌘K button
  in titlebar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:08:40 +01:00
64b90ebddb Add M3: APPDATA persistence + presets + per-pane distro/label
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>
2026-05-22 12:55:46 +01:00
efcdf6a9ce Add M2 splits-tree layout
- 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>
2026-05-22 12:44:35 +01:00