A tab per open window above the maximized pane

Featurekamo-internal
Shipped
September 6, 2026 at 5:56 AM UTC
Author
Kamo
Commit
608fd69

A maximized tool window with a navigator down its left side now gets a row of tabs across the top of the column beside it — one per OPEN window of the type the navigator's selected tab names. Clicking a tab gives it the pane; dragging reorders; each tab closes from its own x; and two arrows at the far right slide the row when it outgrows the width available. Which windows appear is decided by tool, from one table. BUCKET_FOR_TOOL moved out of ChatSwitcherColumn into toolBuckets.ts and is now read in both directions from a single literal — the navigator needs bucket-for-tool, the strip needs tools-for-bucket, and two hand-written maps for one fact fail silently in the worst way: a tool added to one and forgotten in the other simply never appears on the strip, with nothing to say why. Both chat buckets list every open chat, because a chat window carries a sessionGuid and no memberType and so cannot be put on one side of the teamMembers/members split; the terminal session and a plain shell are the same kind of window. Minimized windows get a tab like any other. That is the point rather than an edge case — the dock parks a minimized window off-viewport instead of unmounting it, so it is still open, and the strip is now a way back to it that does not involve hunting for its hexhead. The strip drags the store's existing window order rather than keeping one of its own, so there is no second source of truth to drift and nothing new to persist. Store order is the dock's, packed right-to-left from index 0, and the strip reads left-to-right — so it is that subsequence reversed, and `spliceStripOrder` undoes the reversal on the way back, writing the new order into exactly the array slots the strip's own members occupied. Every other window keeps its index, which is also what keeps the result the permutation setWindowOrder insists on. Closing the tab that holds the pane promotes the tab to its right, or to its left if it was last; only an emptied strip releases the pane. Mounted at dock level beside ChatNavigatorPanel, never inside ToolWindowShell. A strip inside the shell would be torn down and rebuilt by every tab click — the member navigating by demolishing the thing they navigate with — which is the same lesson the navigator was moved out for. Its scroll position survives a swap for the same reason, and no window's tree is touched by one. ToolWindowShell gains topInset beside leftInset: the strip is a dock-level pane above the window, so a maximized window starts at it and stops short by it, or the strip is drawn over the window's own title bar. The unread badge is fed through toolWindowUnreadStore rather than read from UnreadContext directly. ToolDock is deliberately mounted above AuthedChrome so it never unmounts across a navigation — which puts UnreadProvider inside it, not above it. useUnread() under the dock does not throw: the context has a full default value that answers zero for every count. A badge built on it would render, look right, and read zero forever. ToolWindowUnreadSync sits on the other side of that boundary, beside HexHeadUnreadSync, and pushes the answer across. Verified in a real browser as well as in jsdom: a pointer drag reorders, a plain click still selects, the x closes without selecting, the arrows scroll and disable at each end, and the strip's bottom edge lands exactly on the window's title bar rather than a pixel over it.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing