diff --git a/packages/tui/src/context/session-tabs-model.ts b/packages/tui/src/context/session-tabs-model.ts index dc887b92862..0347c8e57b7 100644 --- a/packages/tui/src/context/session-tabs-model.ts +++ b/packages/tui/src/context/session-tabs-model.ts @@ -75,15 +75,30 @@ export function adaptiveSessionTabLayout( ) { if (tabs.length === 0) return { tabs: [], widths: [], before: 0, after: 0, start: 0, total: 0 } - const activeIndex = Math.max( - 0, - tabs.findIndex((tab) => tab.sessionID === active), - ) + const activeIndex = tabs.findIndex((tab) => tab.sessionID === active) const fit = (width: number) => - Math.min(tabs.length, Math.max(1, 1 + Math.floor((Math.max(0, width) - SESSION_TAB_WIDTH) / SESSION_TAB_MIN_WIDTH))) + Math.min( + tabs.length, + Math.max( + 1, + activeIndex === -1 + ? Math.floor(Math.max(0, width) / SESSION_TAB_MIN_WIDTH) + : 1 + Math.floor((Math.max(0, width) - SESSION_TAB_WIDTH) / SESSION_TAB_MIN_WIDTH), + ), + ) const solve = (count: number, start: number, attempts: number): { count: number; start: number } => { + const boundedStart = Math.min(Math.max(0, start), tabs.length - count) const nextStart = Math.min( - Math.max(0, activeIndex < start ? activeIndex : activeIndex >= start + count ? activeIndex - count + 1 : start), + Math.max( + 0, + activeIndex === -1 + ? boundedStart + : activeIndex < boundedStart + ? activeIndex + : activeIndex >= boundedStart + count + ? activeIndex - count + 1 + : boundedStart, + ), tabs.length - count, ) const markers = @@ -103,7 +118,7 @@ export function adaptiveSessionTabLayout( ) const roomy = contentWidth >= SESSION_TAB_WIDTH * visible.length const total = roomy ? Math.min(contentWidth, SESSION_TAB_MAX_WIDTH * visible.length) : contentWidth - if (roomy) { + if (roomy || activeIndex === -1) { const width = Math.floor(total / visible.length) const remainder = total - width * visible.length return { diff --git a/packages/tui/src/context/session-tabs.tsx b/packages/tui/src/context/session-tabs.tsx index 06af71ab112..0fc72e365c0 100644 --- a/packages/tui/src/context/session-tabs.tsx +++ b/packages/tui/src/context/session-tabs.tsx @@ -202,11 +202,16 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp const target = root(sessionID) const closed = closeSessionTab(store.tabs, target) if (closed.tabs.length === store.tabs.length) return + const selected = navigate && current() === target + const previous = selected + ? moveSessionTabHistory(recordSessionTabHistory(history, target), closed.tabs, target, -1) + : { history, sessionID: undefined } + const next = previous.sessionID ?? closed.next + history = previous.history batch(() => { setStore("tabs", reconcile(closed.tabs)) clearUnread(target) - if (navigate && current() === target) - route.navigate(closed.next ? { type: "session", sessionID: closed.next } : { type: "home" }) + if (selected) route.navigate(next ? { type: "session", sessionID: next } : { type: "home" }) }) save() } diff --git a/packages/tui/test/context/session-tabs-model.test.ts b/packages/tui/test/context/session-tabs-model.test.ts index e14db3c3d91..6e93d53b014 100644 --- a/packages/tui/test/context/session-tabs-model.test.ts +++ b/packages/tui/test/context/session-tabs-model.test.ts @@ -75,6 +75,15 @@ describe("session tabs", () => { expect(moveSessionTabHistory(history, tabs, undefined, -1).sessionID).toBe("b") }) + test("returns to the previous selected open tab after closing the active tab", () => { + const tabs = ["a", "b", "c"].map((sessionID) => ({ sessionID })) + const history = ["a", "c"].reduce(recordSessionTabHistory, { entries: [], index: -1 }) + const closed = closeSessionTab(tabs, "b") + const current = recordSessionTabHistory(history, "b") + + expect(moveSessionTabHistory(current, closed.tabs, "b", -1).sessionID).toBe("c") + }) + test("reveals completion activity only after session work becomes idle", () => { expect(sessionTabComplete("activity", true)).toBe(false) expect(sessionTabComplete("activity", false)).toBe(true) @@ -90,6 +99,24 @@ describe("session tabs", () => { expect(layout.widths.reduce((total, width) => total + width, 0)).toBe(76) }) + test("does not reserve an active tab slot on the new session page", () => { + const tabs = ["a", "b", "c", "d", "e"].map((sessionID) => ({ sessionID })) + const layout = adaptiveSessionTabLayout(tabs, "dummy", 40) + + expect(layout.tabs).toEqual(tabs) + expect(layout.widths).toEqual([8, 8, 8, 8, 8]) + expect(layout.widths.reduce((total, width) => total + width, 0)).toBe(layout.total) + }) + + test("keeps the visible tab window stable on the new session page", () => { + const tabs = Array.from({ length: 10 }, (_, index) => ({ sessionID: String(index) })) + const selected = adaptiveSessionTabLayout(tabs, "7", 70) + const home = adaptiveSessionTabLayout(tabs, undefined, 70, selected.start) + + expect(selected.start).toBeGreaterThan(0) + expect(home.start).toBe(selected.start) + }) + test("only swaps old and new active width inside a sticky window", () => { const tabs = ["a", "b", "c", "d", "e", "f", "g"].map((sessionID) => ({ sessionID })) const before = adaptiveSessionTabLayout(tabs, "c", 76)