feat(ui): dock the right pane with dockview (PR-A) #58
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/dockview-pr-a"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Replaces
MainSplit.svelte's hand-rolled Files/Terminal/Artifacts tab strip with adockview-coredock.This is PR-A of three. B (make the workspace list a dock panel) and C (panels carry their own
{workerId, sessionId}, so several sessions can be watched side by side) are deliberately out of scope and get their own specs.docs/superpowers/specs/2026-07-11-dockview-migration-pr-a-design.mddocs/superpowers/plans/2026-07-11-dockview-pr-a.mdWhy
SessionTerminalopens its WebSocket inonMountand closes it inonDestroy; because MainSplit rendered it inside an{#if}branch, switching to Files tore the socket down. tmux made that survivable, not pleasant.What's here
dockview-core@7.0.2— MIT, zero runtime dependencies. Verified by unpacking the published tarball, not by reading the docs (the docs site 404s on its options page).defaultRenderer: 'always'keeps a hidden panel's DOM alive (visibility: hidden) instead of detaching it — this is what stops the terminal's socket being torn down.dndStrategy: 'auto'— native HTML5 DnD for mouse, pointer-events + long-press for touch. dockview 7 ships both backends, so drag and resize work on a phone; there is no locked-mobile code path and no second rendering path.src/lib/dock/: panel-id scheme, layout persistence, theDockDepscontract, and a ~50-lineSvelteContentRendererbridging dockview'sIContentRendereronto Svelte 5'smount()/unmount().Things worth a reviewer's attention
Live objects never reach dockview
params.paramsis JSON-serialized into localStorage bytoJSON(). TheArtifactsStore(a$stateproxy), theFileExplorerProvider, and theopenArtifactcallback therefore reach panels through thecreateComponentfactory closure —paramscarries only{ artifactId }.Artifact panels are stripped before the layout is saved. They're scope-bound, so restoring
artifact:<id>into a different session would resurrect an id that no longer exists server-side. Stripping at save time means whatever is in storage is always restorable and there is zero reconciliation logic. Cost: refreshing while reading an artifact closes that tab. Deliberate.This collapses an existing wart. There used to be two
ArtifactsStoreinstances polling every 5s — one inArtifactsPanel, plus a background one in MainSplit for toasts, torn down whenever the Artifacts tab opened so the two didn't double-poll. With no active-tab to gate on, there is now one store per scope, owned byDock.svelte.The mobile drawer toggles moved into the panels that own them (
FileExplorerPanel,ArtifactsPanel), because they used to switch on anactiveTabthat no longer exists. Better factoring anyway — MainSplit no longer knows a file tree exists. MainSplit lost 315 lines.Bugs found and fixed along the way
A Svelte reactivity loop in
Dock.svelte(4f2d555). The store effect both readdeps.store(to stop the previous one) and wrote it.depsis a$stateproxy, so the read registereddeps.storeas a dependency of the very effect that writes it — a self-triggering loop that would stop the store it had just created, build another, and spin untileffect_update_depth_exceeded, leaking a 5s poller each round. The live store now lives in a plain untracked local.The first artifact of a session was never announced (
39e5d53) — pre-existing onmain.ArtifactsStore.refresh()usedknownIds.size > 0as its "have I fetched at least once" baseline. A session normally starts with zero artifacts, soknownIdsstays empty through the baseline fetch, and the poll that discovers the session's first artifact was treated as still-initializing and swallowed. Onmaintoday that means the first artifact of every session never fires a toast; with the dock it also never opened a tab. Now an explicitinitializedflag, set only on a successful fetch so a failed poll can't reset the baseline.No test could have caught that.
vitest.config.tshad no Svelte plugin, so$statewas an undefined identifier at runtime and any test importing a.svelte.tsmodule died with$state is not defined— which is why the entire runes-based store layer had zero coverage. The plugin is now registered andArtifactsStorehas tests.Verification
Typecheck 0 errors (997 files), lint clean, 65 files / 572 tests,
pnpm buildsucceeds.Driven end-to-end in a real browser against a live worker + session:
visibility: hidden. Hidden, not destroyed: the component instance lives, so the WebSocket lives.Terminal.effect_update_depth_exceeded.Not verified: touch drag on real hardware. The claim that long-press-drag works on a phone rests on reading dockview's source (it ships a pointer-events backend with a
LongPressDetector, 500ms/8px, with context-menu and click guards) plus a headless check — not on a physical device. Emulated touch does not exercise the pointer backend faithfully. Worth one pass on a real phone before merge.Follow-ups (pre-existing, not this PR)
WORKERS_BRIDGE_SUBNETdefaults to172.30.0.0/16for every instance, so a nested or side-by-side Nexus tries to create a bridge on top of its own gateway and hangs rather than erroring. Hit this while standing up a test instance.dockview-coreis an internal package and recommends thedockviewpackage instead. Everything works; worth a look.dockview-coreis an internal package #60An arriving artifact used to be added as a tab inside the Artifacts group, which meant it was invisible unless you happened to be looking there. It now SPLITS the dock beside whatever you're currently looking at, so you see it without going hunting — while still being added `inactive`, so it never steals focus from a half-typed terminal command. The rules live in a pure, tested `decideArtifactPlacement` (artifact-placement.ts): 1. An artifact group already exists -> tab into it. This is what stops the dock turning into confetti: five artifacts do not make five splits. The layout stays at two panes however many arrive. 2. Dock narrower than 768px -> no split, just a tab. A 390px phone split in two gives ~195px panes (~24 terminal columns). The guard is on available width, not on "is it a phone", so a narrow desktop window is covered too. 3. Otherwise split the active group: wide dock (w/h >= 1.2) -> side by side, tall dock -> stacked. Verified in a browser against a live artifact: the first artifact produced two groups at 540x807 side by side with Terminal still the active tab; the second tabbed into the artifact group, leaving the group count at two.