From 7afda00643c46e6c4bd82db22f32d01dff08e918 Mon Sep 17 00:00:00 2001 From: "Vyctor H. Brzezowski" Date: Wed, 26 Aug 2026 14:13:42 -0300 Subject: [PATCH] fix(ui): collapse chat side panel after final tab closes (#130102) * fix(ui): collapse side panel after final tab closes * fix(ui): preserve side panel owner state * fix(ui): preserve projected panel closure * fix(ui): align projected panel close contract --- ui/src/e2e/chat-rail-columns.e2e.test.ts | 6 ++++-- .../chat-sidebar-panel-contract.e2e.test.ts | 3 +++ .../chat/chat-pane-sidebar-layout.test.ts | 19 +++++++++++++++++++ ui/src/pages/chat/chat-pane.test.ts | 2 +- ui/src/pages/chat/sidebar-layout-types.ts | 1 - ui/src/pages/chat/sidebar-layout.test.ts | 11 +++++++++-- ui/src/pages/chat/sidebar-layout.ts | 3 +++ 7 files changed, 39 insertions(+), 6 deletions(-) diff --git a/ui/src/e2e/chat-rail-columns.e2e.test.ts b/ui/src/e2e/chat-rail-columns.e2e.test.ts index b5a97079b954..56e13eee69a1 100644 --- a/ui/src/e2e/chat-rail-columns.e2e.test.ts +++ b/ui/src/e2e/chat-rail-columns.e2e.test.ts @@ -815,8 +815,10 @@ suite.define(() => { .getByRole("button", { name: `Close ${label}`, exact: true }) .click(); } - await sidePanel(page).locator(".side-panel-empty--selector").waitFor(); - await sidePanel(page).getByRole("button", { name: "Close", exact: true }).click(); + await expect.poll(() => sidePanel(page).count()).toBe(0); + await page.reload(); + await page.locator(".chat-group").first().waitFor(); + await expect.poll(() => sidePanel(page).count()).toBe(0); await page.locator(".chat-side-panel-toggle").click(); await sidePanel(page).locator(".side-panel-empty--selector").waitFor(); expect(await sidePanel(page).locator("wa-tab").count()).toBe(0); diff --git a/ui/src/e2e/chat-sidebar-panel-contract.e2e.test.ts b/ui/src/e2e/chat-sidebar-panel-contract.e2e.test.ts index 4f931f198b95..7889fa7c4f8c 100644 --- a/ui/src/e2e/chat-sidebar-panel-contract.e2e.test.ts +++ b/ui/src/e2e/chat-sidebar-panel-contract.e2e.test.ts @@ -407,6 +407,9 @@ suite.define(() => { await waitForControlUiGatewayReady(page); const panel = page.locator(".sidebar-region__right-runtime .side-panel"); + await expect.poll(() => panel.count()).toBe(0); + + await page.locator(".chat-side-panel-toggle").click(); await panel.locator(".side-panel-empty--selector").waitFor(); expect(await panel.locator("wa-tab").count()).toBe(0); diff --git a/ui/src/pages/chat/chat-pane-sidebar-layout.test.ts b/ui/src/pages/chat/chat-pane-sidebar-layout.test.ts index 2a82b65f16a8..513837e4e791 100644 --- a/ui/src/pages/chat/chat-pane-sidebar-layout.test.ts +++ b/ui/src/pages/chat/chat-pane-sidebar-layout.test.ts @@ -197,6 +197,25 @@ describe("chat pane sidebar layout", () => { } }); + it("closes an empty Board projection and preserves visibility with a real tab", () => { + for (const open of [true, false]) { + const empty = resolveSidebarLayoutForBoard({ + board: board("hidden", "chat"), + layout: { ...openSlot({ columns: [] }, "chat"), open }, + paneWidth: 1_400, + }); + expect(empty).toMatchObject({ columns: [], open: false }); + + const withDetail = resolveSidebarLayoutForBoard({ + board: board("hidden", "chat"), + layout: { ...openSlot(openSlot({ columns: [] }, "chat"), "detail"), open }, + paneWidth: 1_400, + }); + expect(withDetail.columns[0]?.panels.map((panel) => panel.slot)).toEqual(["detail"]); + expect(withDetail.open).toBe(open); + } + }); + it("keeps the detail tab when its transient content is no longer available", () => { const layout = resolveSidebarLayoutForBoard({ board: board("hidden", "chat"), diff --git a/ui/src/pages/chat/chat-pane.test.ts b/ui/src/pages/chat/chat-pane.test.ts index cb310ec43d7d..b9c102efe6e9 100644 --- a/ui/src/pages/chat/chat-pane.test.ts +++ b/ui/src/pages/chat/chat-pane.test.ts @@ -922,7 +922,7 @@ describe("chat pane keyboard shortcuts", () => { expect(state.sidebarLayout.columns[0]?.panels.map((panel) => panel.slot)).toEqual(["terminal"]); expect(press().defaultPrevented).toBe(true); expect(state.sidebarLayout.columns).toEqual([]); - expect(state.sidebarLayout.open).toBe(true); + expect(state.sidebarLayout.open).toBe(false); }); }); diff --git a/ui/src/pages/chat/sidebar-layout-types.ts b/ui/src/pages/chat/sidebar-layout-types.ts index 9e958b6ad7f7..7667a5bad647 100644 --- a/ui/src/pages/chat/sidebar-layout-types.ts +++ b/ui/src/pages/chat/sidebar-layout-types.ts @@ -21,7 +21,6 @@ export type SidebarColumn = { export type SidebarLayout = { columns: SidebarColumn[]; dock?: SidebarDock; - /** The panel may stay open as a type picker after its last tab closes. */ open?: boolean; expanded?: boolean; }; diff --git a/ui/src/pages/chat/sidebar-layout.test.ts b/ui/src/pages/chat/sidebar-layout.test.ts index 5ec39781d68c..448a5bd8549b 100644 --- a/ui/src/pages/chat/sidebar-layout.test.ts +++ b/ui/src/pages/chat/sidebar-layout.test.ts @@ -71,9 +71,16 @@ describe("sidebar layout", () => { expect(chat?.slot).toBe("chat"); }); - it("keeps the panel open as a type selector after its final tab closes", () => { + it("collapses the panel after its final tab closes", () => { const closed = closeSlot(openSlot({ columns: [] }, "detail"), "detail"); - expect(closed).toEqual({ columns: [], open: true }); + expect(closed).toEqual({ columns: [], open: false }); + }); + + it("does not reopen a minimized panel when another tab remains", () => { + const minimized = { ...openAll(), open: false }; + const closed = closeSlot(minimized, "detail"); + expect(closed.open).toBe(false); + expect(closed.columns[0]?.panels.map((panel) => panel.slot)).toEqual(["discussion", "chat"]); }); it("minimizes and expands without discarding tabs", () => { diff --git a/ui/src/pages/chat/sidebar-layout.ts b/ui/src/pages/chat/sidebar-layout.ts index 9c340f30c12e..213f47bd29b8 100644 --- a/ui/src/pages/chat/sidebar-layout.ts +++ b/ui/src/pages/chat/sidebar-layout.ts @@ -122,6 +122,9 @@ export function closeSlot(layout: SidebarLayout, slot: SidebarSlotId): SidebarLa .find((entry) => entry.slot === slot); if (panel) { removePanel(next, panel.id); + if (next.columns.length === 0) { + next.open = false; + } } return next; }