feat(ui): collapsible workspace rail (PR-B) #61
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/dockview-pr-b"
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?
Stacked on #58 — this PR targets
feat/dockview-pr-a, notmain. Review #58 first.Collapses the 360px workspace sidebar to a 52px rail, so the dock gets the space back.
docs/superpowers/specs/2026-07-12-dockview-migration-pr-b-design.mddocs/superpowers/plans/2026-07-12-dockview-pr-b.mdThis is NOT "make the sidebar a dock panel"
PR-B was originally scoped that way. It isn't what this builds, and the change is deliberate.
The requirement was not closable, but collapsible to a tiny sidebar. That's a request for space back, not for dockability — nobody wanted to drag, float, or stack the workspace list. And dockview has no collapse-to-rail concept: a "collapsed" dock panel is a group shrunk to a sliver with its tab header still rendering. Building it as a panel would have meant fighting the grid to produce a worse sidebar, and it would have introduced a close-footgun (operator closes their only navigation) that a rail avoids by construction.
So the list stays a first-class left column, outside the dock. Dockview keeps the content panes, where it earns its keep. PR-B got substantially smaller as a result.
What's here
«/»toggle. No drag-resize — it would put a second resize idiom on a page that already has dockview's sashes.nexus-main→NM), with its sessions as dots beneath. Clicking a dot selects that session and does not expand the rail. Hovering any chip or dot reveals its name.workspace_shellrows get no dot — the workspace shell is the workspace scope, which the chip already represents.The one genuinely fiddly part
The rail scrolls, and
overflow-y: autoforcesoverflow-xto compute toautotoo — so anything extending past its 52px edge is silently clipped, an::afterpseudo-element as readily as a child div. A tooltip anchored to a dot inside a 52px rail necessarily extends past that edge.The tooltip is therefore portalled to
<body>atposition: fixed, positioned fromgetBoundingClientRect()and dismissed on scroll and resize. It stays cheap only because it is non-interactive (pointer-events: none): no dismissal policy, no keyboard trap, no pointer-travel grace period.This is exactly why the hover-flyout alternative was rejected: it needs the identical portal plus all of that machinery — and it would have hidden the sessions that were meant to be visible while collapsed.
Discovered by building a prototype, not by reasoning. The first version's tooltips were simply invisible.
Bug found while verifying — pre-existing on
mainsetScopewas losing the scope on every reload. It built the href as:URLSearchParams.toString()returns no leading?, butURL.search's getter adds one — so the result was/??scope=session&worker=….URLSearchParamsthen parsed the first key as'?scope',scopeFromSearch'sget('scope')returnednull, and the selection was silently dropped on every refresh.Verified in a browser before and after: selecting a session then refreshing used to drop you back to "No workspace selected"; it now restores the scope. The identical line is on
main, unchanged by PR-A — this has been true the whole time.Replaced with a pure
scopeToHref(pathname, scope)so it can't recur, with a regression test asserting exactly one?and a round-trip back throughscopeFromSearch.Verification
Typecheck 0 errors (1004 files), lint clean, 68 files / 597 tests,
pnpm buildsucceeds.Driven end-to-end in a real browser against a seeded instance (4 workspaces, 6 sessions + 1 workspace_shell):
NM/TR/UL/2Sand 4 dots — theworkspace_shellrow correctly produced none.<body>, 126×21,pointer-events: none, and extends past the rail's right edge — i.e. it genuinely escapes the clipping.basePathand leaves the rail at 52px (does not expand).collapsed=truestill inlocalStorage→railPresent: false,toggleVisible: false, full list renders. A phone cannot land in a rail.Deliberately not done
Dock.svelteis untouched. Its permanent-panel top-up still re-adds a closed Files/Artifacts panel on reload, so "close" doesn't stick. PR-C replaces fixed permanent panels with per-session panels and rewrites that logic entirely — fixing it here would be churn on code C deletes. The footgun that made it urgent under the original framing (operator closes their only navigation) does not exist here, because the workspace list is not a dock panel and cannot be closed at all.setScope built the href as: u.search = scopeToSearch(next).toString(); goto(u.pathname + (u.search ? `?${u.search}` : '')) URLSearchParams.toString() returns NO leading '?', but URL.search's GETTER adds one — so the result was `/??scope=session&worker=…`. URLSearchParams then parsed the first key as '?scope', so scopeFromSearch's get('scope') returned null and the selection was silently lost on every reload. Verified in a browser before and after: selecting a session then refreshing used to drop you back to "No workspace selected"; it now restores the scope. Pre-existing on main — the identical line is there, unchanged by PR-A. Found by driving the real UI while verifying the rail, not by any test. Replaced with a pure `scopeToHref(pathname, scope)` in scope.ts so the mistake can't recur, with a regression test asserting exactly one '?' and a round-trip back through scopeFromSearch.