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