From 88c386c96c143599c3300b9235650866082120e6 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 28 Sep 2026 22:17:10 +0200 Subject: [PATCH] swarm-ui: move dynamic agent-term tabs into the top nav bar mara, PR review: "the dynamic tab should be in the top bar, not a new one below". Moves the .shell-tabs group from its own sticky row under the header into .shell-nav itself, right after the nav indicator. Also adds a third re-measure effect for the sliding nav indicator, keyed on tabs.length: with tabs inline in the same flex row the indicator measures, closing a background tab (no navigation) can shrink the row without the hop effect's own re-measure ever firing. Same reflow-not-navigation reasoning as the existing resize-listener effect. --- .../packages/swarm-ui/src/shell/Shell.css | 35 ++++------ .../packages/swarm-ui/src/shell/Shell.tsx | 66 ++++++++++++------- 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/frontend/packages/swarm-ui/src/shell/Shell.css b/frontend/packages/swarm-ui/src/shell/Shell.css index a052cb09..0393f5bc 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.css +++ b/frontend/packages/swarm-ui/src/shell/Shell.css @@ -165,33 +165,22 @@ .shell-nav-indicator.shell-nav-indicator-instant { transition: none; } -/* Dynamic tab row — sits below `.shell-header`, its own sticky - strip rather than folded into the header itself: the header's own - sticky offset (`top: 0`) is already claimed, and a header that grows - taller every time a tab opens would shift `.shell-nav`'s measured - positions (`measureIndex`, Shell.tsx) out from under the sliding - indicator mid-animation. `top` below is `.shell-header`'s own - footprint — `0.75em` padding top+bottom either side of the - `2.75em`-min-height nav row (`.shell-nav-link`, this file), so ~4.25em - rounded up to 4.5em for the 1px border + a little slack — not - measured, same fixed-estimate tradeoff `AgentTerminalPage.css`'s own - comment makes for the same reason (not worth a ResizeObserver for one - sticky offset), and the same 4.5em value that file's own height calc - already uses for this header, so the two stay consistent with each - other rather than drifting to two different guesses at one thing. - Undershooting this would mean the sticky header (z-index 10, above - this row's 9) visibly covers the top of the tab row once scrolled — - worth erring high, not low, if the real value ever drifts. */ +/* Dynamic tabs group — inline in `.shell-nav` itself, next to the + static route links (mara, PR review: "the dynamic tab should be in + the top bar, not a new one below" — an earlier cut had this as its + own sticky row under the header; that version and the reasoning for + it are still in this file's git history if the split ever needs to + come back). `margin-left: 0.5em` + `border-left` reads as "a group + attached to the nav, not another nav item" without a second landmark + or a fixed pixel gap that'd fight `.shell-nav`'s own `gap`. */ .shell-tabs { display: flex; + align-items: center; flex-wrap: wrap; gap: 0.5em; - padding: 0.4em 1.25em; - border-bottom: 1px solid var(--border); - background: var(--bg-elev); - position: sticky; - top: 4.5em; - z-index: 9; /* one below .shell-header's 10, so the header still wins if both ever overlap during a fast scroll */ + margin-left: 0.5em; + padding-left: 0.75em; + border-left: 1px solid var(--border); } .shell-tab { display: flex; diff --git a/frontend/packages/swarm-ui/src/shell/Shell.tsx b/frontend/packages/swarm-ui/src/shell/Shell.tsx index bb48ac18..9dcbbc9b 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.tsx +++ b/frontend/packages/swarm-ui/src/shell/Shell.tsx @@ -214,9 +214,13 @@ export function Shell({ children }: { children: ComponentChildren }) { // Every subsequent change (a real navigation) still animates normally. const [indicatorSettledOnce, setIndicatorSettledOnce] = useState(false); - // Open agent-terminal tabs — the row below `.shell-nav` that - // grows/shrinks as tabs open/close, entirely separate from the fixed - // `NAV_ITEMS` list above. + // Open agent-terminal tabs — rendered inline in `.shell-nav` itself + // (mara, PR review: "the dynamic tab should be in the top bar, not a + // new one below" — an earlier cut had these as their own row under + // the header; see the git history on this file for that version and + // why it was reverted). Entirely separate from the fixed `NAV_ITEMS` + // list above, so they get their own small component (`DynamicTabLink`) + // rather than reusing `NavLink`. const { tabs, close: closeTab } = useDynamicTabs(); // Applied once here, not inside `SettingsMenu` — every route mounts @@ -313,6 +317,18 @@ export function Shell({ children }: { children: ComponentChildren }) { return () => window.removeEventListener("resize", onResize); }, []); + // Same re-measure, for the same "a reflow, not a navigation" reason — + // now that the dynamic tabs render inline in `.shell-nav` (see the + // `useDynamicTabs()` comment above), *closing* a background tab (one + // that isn't the active route) shrinks the row without any navigation + // firing to trigger the hop effect's own re-measure. Opening one + // doesn't need this: `useDynamicTabs().open()` always navigates, so + // the hop effect already covers that case. + useEffect(() => { + if (settledIndexRef.current === -1) return; + setIndicator(measureIndex(settledIndexRef.current)); + }, [tabs.length]); + // Fetched once here, not per-page: every route mounts inside one // ``, and the swarm's name doesn't change within a page // visit. A fetch failure is silently ignored — `swarmName` just stays @@ -387,6 +403,30 @@ export function Shell({ children }: { children: ComponentChildren }) { backgroundColor: indicator?.accent ?? "transparent", }} /> + {/* Open agent-terminal tabs — inline in the same nav row as + the static routes (mara's review call, see the + `useDynamicTabs()` comment above). Only rendered when + there's something to show — an always-present empty group + would just be dead chrome for the far more common case of + no open terminal tabs. `role="group"`, not a second ` {/* Single `margin-left: auto` on the wrapper, not on each menu individually — two adjacent flex items both set to @@ -398,26 +438,6 @@ export function Shell({ children }: { children: ComponentChildren }) { - {/* Second row, only when there's something to show — an always- - present empty bar would just be dead chrome for the far more - common case of no open terminal tabs. Own `