diff --git a/src/contexts/tab-context.test.tsx b/src/contexts/tab-context.test.tsx index d0587b4512..e072c56831 100644 --- a/src/contexts/tab-context.test.tsx +++ b/src/contexts/tab-context.test.tsx @@ -2786,4 +2786,137 @@ describe("TabProvider tab groups", () => { expect(isReparentUnmount(store(), "conv-1-codex-1", home)).toBe(false) }) }) + + // Tile Display shows every tab in a group as its own pane at the same time, + // so consuming one of those panes for a preview would remove a session the + // mode exists to display. A tiled group hands the preview a slot of its own; + // an untiled group keeps the single preview slot it has always had. + describe("preview opens in a tiled group", () => { + /** `[id, isPinned]` for each tab the group holds, in strip order. */ + const paneState = (groupId: string) => + store() + .rawTabs.filter((tab) => groupOfId(tab.id) === groupId) + .map((tab) => [tab.id, tab.isPinned]) + + it("gives the preview its own pane instead of replacing one", async () => { + await renderWithTabs([tabItem(1, 1, true)]) + const home = leaves()[0] + + act(() => { + store().openTab(1, 2, "codex", false, "Second") + }) + act(() => { + store().toggleGroupTile(home) + }) + act(() => { + store().openTab(1, 3, "codex", false, "Third") + }) + act(() => { + store().openTab(1, 4, "codex", false, "Fourth") + }) + + expect(paneState(home)).toEqual([ + ["conv-1-codex-1", true], + ["conv-1-codex-2", false], + ["conv-1-codex-3", false], + ["conv-1-codex-4", false], + ]) + expect(store().activeTabId).toBe("conv-1-codex-4") + }) + + it("still replaces the preview when the group is not tiled", async () => { + await renderWithTabs([tabItem(1, 1, true)]) + const home = leaves()[0] + + act(() => { + store().openTab(1, 2, "codex", false, "Second") + }) + act(() => { + store().openTab(1, 3, "codex", false, "Third") + }) + + expect(paneState(home)).toEqual([ + ["conv-1-codex-1", true], + ["conv-1-codex-3", false], + ]) + expect(store().activeTabId).toBe("conv-1-codex-3") + }) + + it("replaces in the focused untiled group while a sibling group is tiled", async () => { + await renderWithTabs([tabItem(1, 1, true), tabItem(1, 2)]) + const home = leaves()[0] + + act(() => { + store().splitTab("conv-1-codex-2", "right", { move: true }) + }) + const g1 = newLeafBeside(home) + act(() => { + store().toggleGroupTile(g1) + store().switchTab("conv-1-codex-1") + }) + act(() => { + store().openTab(1, 3, "codex", false, "Third") + }) + act(() => { + store().openTab(1, 4, "codex", false, "Fourth") + }) + + expect(paneState(home)).toEqual([ + ["conv-1-codex-1", true], + ["conv-1-codex-4", false], + ]) + expect(paneState(g1)).toEqual([["conv-1-codex-2", true]]) + }) + + it("focuses a conversation the tiled group already shows", async () => { + await renderWithTabs([tabItem(1, 1, true)]) + const home = leaves()[0] + + act(() => { + store().toggleGroupTile(home) + }) + act(() => { + store().openTab(1, 2, "codex", false, "Second") + }) + act(() => { + store().switchTab("conv-1-codex-1") + }) + act(() => { + store().openTab(1, 2, "codex", false, "Second") + }) + + expect(paneState(home)).toEqual([ + ["conv-1-codex-1", true], + ["conv-1-codex-2", false], + ]) + expect(store().activeTabId).toBe("conv-1-codex-2") + }) + + it("keeps every pane when pinned and preview opens interleave", async () => { + await renderWithTabs([tabItem(1, 1, true), tabItem(1, 2)]) + const home = leaves()[0] + + act(() => { + store().toggleGroupTile(home) + }) + act(() => { + store().openTab(1, 3, "codex", false, "Third") + }) + act(() => { + store().openTab(1, 4, "codex", true, "Fourth") + }) + act(() => { + store().openTab(1, 5, "codex", false, "Fifth") + }) + + expect(paneState(home)).toEqual([ + ["conv-1-codex-1", true], + ["conv-1-codex-2", true], + ["conv-1-codex-3", false], + ["conv-1-codex-4", true], + ["conv-1-codex-5", false], + ]) + expect(store().activeTabId).toBe("conv-1-codex-5") + }) + }) }) diff --git a/src/stores/tab-store.ts b/src/stores/tab-store.ts index 0d76967df9..d112a977de 100644 --- a/src/stores/tab-store.ts +++ b/src/stores/tab-store.ts @@ -205,8 +205,9 @@ export interface TabStoreState { opts?: { /** rawTabs slot for a tab that needs a NEW slot (clamped); omitted = * append. Reopening a closed tab passes the slot it was closed from. A - * conversation that is already open is focused where it is, and a - * preview still replaces the group's current preview in place. */ + * conversation that is already open is focused where it is. A preview + * replaces the group's existing preview in place for untiled groups; in + * tiled groups it takes a slot of its own (so no visible pane is lost). */ index?: number } ) => void @@ -1178,12 +1179,17 @@ export const useTabStore = create()((set, get) => ({ // Preview replacement stays within the focused group — a preview parked in // another group is left alone (the new tab gets a slot of its own instead). - const previewIndex = prevState.rawTabs.findIndex( - (t) => - !t.isPinned && - groupOfTab(prevState.groupOf, prevState.groupLayout, t.id) === - targetGroup - ) + // A TILED group shows every one of its tabs as a pane at the same time, so + // replacing one there would silently remove a pane the mode exists to show. + // Tiled groups therefore give the new tab its own slot, like a pinned open. + const previewIndex = prevState.tileByGroup[targetGroup] + ? -1 + : prevState.rawTabs.findIndex( + (t) => + !t.isPinned && + groupOfTab(prevState.groupOf, prevState.groupLayout, t.id) === + targetGroup + ) if (previewIndex >= 0) { const updated = [...prevState.rawTabs] const replacedPreviewTabId = updated[previewIndex].id