0031: Panels dock into the main window
On this page
Status: accepted
Context
People want to keep an eye on logs, a terminal or a scratchpad while they work elsewhere in the app. Any screen should be able to leave the main window, and come back by being dragged to the main window's side or bottom. There were two ways to snap. A window could be glued magnetically to the main window's edge, staying a separate OS window. Or it could be folded back in as a panel inside the main window. Maximizing, full screen and moving the main window must take everything with it, on macOS and Windows alike.
Decision
- Snapping docks into main. A floating window let go within 24 px of the main window's left, right or bottom edge, or over a docked panel, becomes a panel inside the main window (
src/panels/Dock.tsx,src/panels/snap.ts). Dragging a panel's grip out of the window, or 48 px off its edge with no target under the pointer, opens it as a floating window again. Glued windows were rejected: macOS doesn't resize a child window when its parent maximizes or goes full screen, and Windows can only approximate it. - Every view has its own place. A panel shows a hash path such as
/site/shop/terminals?id=t-1through aPlaceProvider. Screens navigate withuseGoanduseRouteParamsinstead of the window's hash. Going somewhere on the same screen stays in the panel; going to another screen opens it in the main window. The shell's own controls usegoMain. - The shell keeps the layout. It lives in
layout.jsonin the app's config folder, outside leashd's home, since it's state of the windows rather than of Short Leash. The UI owns the docked panels and sites' own layouts (pure functions insrc/panels/layout.ts) and sends the whole layout withlayout_set. The shell keeps the frames it tracks: the main window's, and those of floating windows already open (src-tauri/src/layout.rs). It opens and closes apanel-<id>window for each floating panel, saves the layout, and restores everything at launch onto screens that still exist (src-tauri/src/panels.rs). - The shell reports drags. While a floating window moves near the main window, it sends
dock-hoverwith the pointer and frame in the main window's pixels, anddock-dropwhen the window is let go; the main window draws the snap target and decides where the panel goes. On macOS, "let go" is the mouse button coming up (NSEvent.pressedMouseButtons, a safe call). Windows has no safe way to read the button, so there a window that stops moving for 450 ms near the main window counts as let go. The Dock menu and Option+Command+arrow dock without a drag on both systems. - Live rows go to windows that show screens. Daemon events for requests, logs, terminals and scratchpads go to
mainand everypanel-*window, never the tray. Each window listens as itself, so the shell's targeting holds. - Layouts per site are docked panels only. A site can have its own docked panels, which swap in when the main window opens it. Floating windows stay global, since windows moving around on other screens as you switch sites would be jarring. A panel can follow the open site instead of staying pinned to what it was opened with.
Consequences
- One real window holds the page and its panels, so maximizing, full screen, Spaces and moving all work as they always have, on both systems.
- Each floating window is its own web view with its own daemon client: about 40–80 MB each. There's no limit, but opening something that's already out brings it to the front instead of opening it twice.
- A terminal can be shown twice. Only the view used last, in the window in front, sends the shell its size.
- On Windows, pausing near an edge mid-drag for half a second docks the window. The menu and keys are the precise way there.
- In a plain browser (
just ui), floating panels open as browser windows sharing the layout through local storage. They can dock from their menu but not by dragging. - Snap target, snapping in and pulling out join the app's allowed motions (CLAUDE.md), with a reduced-motion version that only flashes the outline.