From b5d9573dcaf62ed15d89d7e46004e73a481cdd4e Mon Sep 17 00:00:00 2001 From: Shakker Date: Sat, 18 Jul 2026 07:39:16 +0100 Subject: [PATCH] fix: pause hidden session catalog polling --- ui/src/components/app-sidebar-session-data.ts | 14 ++++- .../app-sidebar-cases/catalog-live-events.ts | 59 ++++++++++++++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/ui/src/components/app-sidebar-session-data.ts b/ui/src/components/app-sidebar-session-data.ts index 3823ee8d50c0..6a1f17c46bd0 100644 --- a/ui/src/components/app-sidebar-session-data.ts +++ b/ui/src/components/app-sidebar-session-data.ts @@ -175,12 +175,11 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarBase { override updated() { this.syncSessionsScrollObserver(); - const snapshot = this.context?.gateway.snapshot; if (this.context) { this.synchronizeSessionCatalogAgent(this.expandedAgentId()); } if ( - !sessionCatalogListClient(snapshot, this.connected) || + !this.visibleSessionCatalogClient() || this.sessionCatalogLive.timer || this.sessionCatalogLive.requestGeneration === this.sessionCatalogGeneration ) { @@ -189,6 +188,13 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarBase { void this.refreshSessionCatalogs(); } + private visibleSessionCatalogClient(): GatewayBrowserClient | null { + if (document.visibilityState === "hidden") { + return null; + } + return sessionCatalogListClient(this.context?.gateway.snapshot, this.connected); + } + private synchronizeSessionCatalogAgent(agentId: string) { if (agentId === this.sessionCatalogAgentId) { return; @@ -265,7 +271,9 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarBase { } private async refreshSessionCatalogs() { - const client = sessionCatalogListClient(this.context?.gateway.snapshot, this.connected); + // Hidden pages resume through the coalesced activation handler. Starting + // here without a timer makes catalog state updates poll at request latency. + const client = this.visibleSessionCatalogClient(); if (!client) { return; } diff --git a/ui/src/test-helpers/app-sidebar-cases/catalog-live-events.ts b/ui/src/test-helpers/app-sidebar-cases/catalog-live-events.ts index 1f03de248fa0..e527c16904be 100644 --- a/ui/src/test-helpers/app-sidebar-cases/catalog-live-events.ts +++ b/ui/src/test-helpers/app-sidebar-cases/catalog-live-events.ts @@ -1,7 +1,14 @@ import { describe, expect, it, vi } from "vitest"; +import type { SessionsCatalogListResult } from "../../../../packages/gateway-protocol/src/index.ts"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; import type { ApplicationGatewaySnapshot } from "../../app/context.ts"; -import { catalogPage, createGatewayHarness, createSessions, mountSidebar } from "../app-sidebar.ts"; +import { + catalogPage, + createGatewayHarness, + createSessions, + deferred, + mountSidebar, +} from "../app-sidebar.ts"; import "../../components/app-sidebar.ts"; describe("AppSidebar session catalog pagination", () => { @@ -44,6 +51,56 @@ describe("AppSidebar session catalog pagination", () => { } }); + it("stays paused when an in-flight catalog refresh finishes in a hidden tab", async () => { + vi.useFakeTimers(); + let visibility: DocumentVisibilityState = "visible"; + const visibilitySpy = vi + .spyOn(document, "visibilityState", "get") + .mockImplementation(() => visibility); + try { + const pending = deferred(); + const request = vi + .fn() + .mockReturnValueOnce(pending.promise) + .mockResolvedValue(catalogPage([])); + const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient); + gateway.publish({ + hello: { + features: { methods: ["sessions.catalog.list"] }, + } as ApplicationGatewaySnapshot["hello"], + }); + const { sidebar } = await mountSidebar( + gateway.gateway, + createSessions("main", ["agent:main:main"]), + ); + sidebar.connected = true; + await sidebar.updateComplete; + await vi.advanceTimersByTimeAsync(0); + expect(request).toHaveBeenCalledTimes(1); + + visibility = "hidden"; + document.dispatchEvent(new Event("visibilitychange")); + pending.resolve(catalogPage([])); + await vi.advanceTimersByTimeAsync(0); + await sidebar.updateComplete; + await vi.advanceTimersByTimeAsync(60_000); + expect(request).toHaveBeenCalledTimes(1); + + visibility = "visible"; + document.dispatchEvent(new Event("visibilitychange")); + globalThis.dispatchEvent(new Event("focus")); + await vi.advanceTimersByTimeAsync(49); + expect(request).toHaveBeenCalledTimes(1); + await vi.advanceTimersByTimeAsync(1); + expect(request).toHaveBeenCalledTimes(2); + await vi.advanceTimersByTimeAsync(0); + expect(request).toHaveBeenCalledTimes(2); + } finally { + visibilitySpy.mockRestore(); + vi.useRealTimers(); + } + }); + it("does not poll an older Gateway that does not advertise session catalogs", async () => { vi.useFakeTimers(); try {