From 668f6e05dd8aa7614036e4d8a9ef6431d5c6293a Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:50:08 -0700 Subject: [PATCH] fix(web): closing the last Browser tab hides the right panel again Closing the last right-panel tab hid the panel for Files, Diff and Terminal tabs, but a Browser tab left it open on the empty "Open a surface" screen. Cause: the close hook released the browser session before closing the tab. Releasing drops the session from the preview store, and since #25006 the browser-tab sync subscribes to that store directly, so it removed the tab without hiding the panel; the store close then found nothing to close. Fix: close the tab in the store first, then release its resources. Browser reconciliation now also hides the panel when it removes the last session-backed tab, so a browser closed by an agent or another client hides it too, as file reconciliation already does. Co-Authored-By: Claude Opus 5.5 --- .../preview/agentOpenedPreviews.test.ts | 2 +- .../web/src/hooks/useRightPanelSurfaceClose.ts | 4 +++- apps/web/src/rightPanelStore.test.ts | 18 ++++++++++++++++++ apps/web/src/rightPanelStore.ts | 6 ++++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/preview/agentOpenedPreviews.test.ts b/apps/web/src/components/preview/agentOpenedPreviews.test.ts index 9464d832d7f79..f6ad67432acdd 100644 --- a/apps/web/src/components/preview/agentOpenedPreviews.test.ts +++ b/apps/web/src/components/preview/agentOpenedPreviews.test.ts @@ -258,7 +258,7 @@ describe("followPreviewSessions", () => { useRightPanelStore .getState() .byThreadKey[threadKey]?.surfaces.filter((surface) => surface.kind === "preview") - .map((surface) => surface.id); + .map((surface) => surface.id) ?? []; it("adds visible sessions as tabs and drops tabs whose session ended", () => { setup(); diff --git a/apps/web/src/hooks/useRightPanelSurfaceClose.ts b/apps/web/src/hooks/useRightPanelSurfaceClose.ts index 6fc0185dbb187..294112b6ce1f9 100644 --- a/apps/web/src/hooks/useRightPanelSurfaceClose.ts +++ b/apps/web/src/hooks/useRightPanelSurfaceClose.ts @@ -88,8 +88,10 @@ export function useRightPanelSurfaceClose(input: { (left, right) => Number(left.id === activeId) - Number(right.id === activeId), ); for (const surface of ordered) { - releaseSurfaceResources(threadRef, surface); + // Releasing a browser drops its session, which prunes the tab automatically; close it + // first so the close stays a user action that hides the panel after the last tab. store.closeSurface(threadRef, surface.id); + releaseSurfaceResources(threadRef, surface); } const nextActiveSurface = selectActiveRightPanelSurface( useRightPanelStore.getState().byThreadKey, diff --git a/apps/web/src/rightPanelStore.test.ts b/apps/web/src/rightPanelStore.test.ts index a29cecac37095..764fad8879bc4 100644 --- a/apps/web/src/rightPanelStore.test.ts +++ b/apps/web/src/rightPanelStore.test.ts @@ -1181,6 +1181,24 @@ describe("rightPanelStore", () => { }); }); + it("hides the panel when browser reconciliation removes its last session tab", () => { + const store = useRightPanelStore.getState(); + store.openBrowser(refA, "tab-a"); + store.reconcileBrowserSurfaces(refA, []); + expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ + isOpen: false, + activeSurfaceId: null, + surfaces: [], + }); + + store.openBrowser(refA, "tab-b"); + store.open(refA, "diff"); + store.reconcileBrowserSurfaces(refA, []); + expect( + selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA), + ).toMatchObject({ isOpen: true, activeSurfaceId: "diff" }); + }); + it("moves a surface to a new index and keeps it there through browser reconciliation", () => { const store = useRightPanelStore.getState(); store.openTerminal(refA, "term-1"); diff --git a/apps/web/src/rightPanelStore.ts b/apps/web/src/rightPanelStore.ts index 5d4d68264aebd..33c537a3cf942 100644 --- a/apps/web/src/rightPanelStore.ts +++ b/apps/web/src/rightPanelStore.ts @@ -972,8 +972,14 @@ export const useRightPanelStore = create()( (surface) => surface.id === current.activeSurfaceId, ); const fallbackBrowser = surfaces.find((surface) => surface.kind === "preview"); + // A closed session tab was the last surface: hide the panel rather than leave it on + // the empty picker, as a user close would. A dropped `browser:new` placeholder does not count. + const closedLastSessionTab = + surfaces.length === 0 && + current.surfaces.some((surface) => surface.kind === "preview" && surface.resourceId); return { ...current, + isOpen: closedLastSessionTab ? false : current.isOpen, surfaces, activeSurfaceId: activeStillExists ? current.activeSurfaceId