diff --git a/ui/src/components/app-sidebar-menus.ts b/ui/src/components/app-sidebar-menus.ts index 5eab98a39f63..258eb16bf5b4 100644 --- a/ui/src/components/app-sidebar-menus.ts +++ b/ui/src/components/app-sidebar-menus.ts @@ -370,7 +370,7 @@ export abstract class AppSidebarMenusElement extends AppSidebarSessionGroupsElem themeMode: this.themeMode, agentUnreadCount: (agentId) => this.agentUnreadCount(agentId), agentApprovalCount: (agentId) => - this.approvalBadgeSnapshot().agentCounts.get(normalizeAgentId(agentId)) ?? 0, + this.sessionData.approvalBadgeSnapshot().agentCounts.get(normalizeAgentId(agentId)) ?? 0, onFilterChange: (next) => { this.agentMenuFilter = next; }, @@ -428,7 +428,7 @@ export abstract class AppSidebarMenusElement extends AppSidebarSessionGroupsElem .anchor=${menu} .trigger=${this.sessionMenuTrigger} .disabled=${!this.connected} - .forkDisabled=${this.sessionsLoading || session.modelSelectionLocked} + .forkDisabled=${this.sessionData.sessionsLoading || session.modelSelectionLocked} .archiveAllowed=${archiveAllowed} .cloudWorkerStopAllowed=${Boolean( !batchRows && diff --git a/ui/src/components/app-sidebar-session-catalog-data.ts b/ui/src/components/app-sidebar-session-catalog-data.ts deleted file mode 100644 index a643975b258d..000000000000 --- a/ui/src/components/app-sidebar-session-catalog-data.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { state } from "lit/decorators.js"; -import type { - SessionCatalog, - SessionsCatalogListResult, -} from "../../../packages/gateway-protocol/src/index.ts"; -import type { GatewayBrowserClient } from "../api/gateway.ts"; -import { - CATALOG_SESSION_CONTINUED_EVENT, - type CatalogSessionContinuedDetail, -} from "../lib/sessions/catalog-key.ts"; -import { AppSidebarBase } from "./app-sidebar-base.ts"; -import { - refreshSessionCatalogsLive, - SESSION_CATALOG_CHANGED_REFRESH_MS, - SessionCatalogLiveState, - sessionCatalogListClient, -} from "./app-sidebar-session-catalog-live.ts"; -import { - mergeSessionCatalogPage, - sessionCatalogRequestError, -} from "./app-sidebar-session-catalog-state.ts"; -import { bindAdoptedCatalogSession } from "./app-sidebar-session-catalogs.ts"; -import { sessionCatalogHostKey } from "./app-sidebar-session-types.ts"; - -/** Gateway-backed external session-catalog synchronization. */ -export abstract class AppSidebarSessionCatalogDataElement extends AppSidebarBase { - @state() protected sessionCatalogs: SessionCatalog[] = []; - @state() protected loadingMoreSessionCatalogIds: ReadonlySet = new Set(); - - private readonly sessionCatalogLive = new SessionCatalogLiveState(); - private sessionCatalogAgentId: string | null = null; - private sessionCatalogGeneration = 0; - private sessionCatalogRevision = 0; - private readonly sessionCatalogPageDepths = new Map(); - private readonly sessionCatalogRevisions = new Map(); - - protected abstract expandedAgentId(): string; - protected abstract sessionCatalogGatewayClient(): GatewayBrowserClient | null; - - protected connectSessionCatalogListeners() { - // The chat pane announces catalog adoptions so the catalog row binds to - // the new session key before the next catalog poll. - document.addEventListener( - CATALOG_SESSION_CONTINUED_EVENT, - this.handleCatalogSessionContinued as EventListener, - ); - document.addEventListener("visibilitychange", this.handleSessionCatalogPageActivation); - globalThis.addEventListener("focus", this.handleSessionCatalogPageActivation); - } - - protected disconnectSessionCatalogListeners() { - document.removeEventListener( - CATALOG_SESSION_CONTINUED_EVENT, - this.handleCatalogSessionContinued as EventListener, - ); - document.removeEventListener("visibilitychange", this.handleSessionCatalogPageActivation); - globalThis.removeEventListener("focus", this.handleSessionCatalogPageActivation); - } - - protected retireSessionCatalogData() { - this.sessionCatalogGeneration += 1; - this.sessionCatalogLive.clear(); - } - - protected resetSessionCatalogConnection() { - this.sessionCatalogGeneration += 1; - this.sessionCatalogRevision += 1; - this.sessionCatalogLive.resetConnection(); - this.sessionCatalogs = []; - this.loadingMoreSessionCatalogIds = new Set(); - this.sessionCatalogPageDepths.clear(); - this.sessionCatalogRevisions.clear(); - } - - protected updateSessionCatalogData() { - if (this.context) { - this.synchronizeSessionCatalogAgent(this.expandedAgentId()); - } - if ( - !this.visibleSessionCatalogClient() || - this.sessionCatalogLive.timer || - this.sessionCatalogLive.requestGeneration === this.sessionCatalogGeneration - ) { - return; - } - void this.refreshSessionCatalogs(); - } - - protected handleSessionCatalogHostEvent(payload: unknown) { - this.applySessionCatalogHostEvent(payload); - } - - protected handleSessionCatalogPresence(payload: unknown) { - if (this.sessionCatalogLive.observePresence(payload)) { - this.requestSessionCatalogRefresh(); - } - } - - 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; - } - this.sessionCatalogAgentId = agentId; - this.sessionCatalogGeneration += 1; - this.sessionCatalogRevision += 1; - this.sessionCatalogLive.clear(); - this.loadingMoreSessionCatalogIds = new Set(); - if (this.sessionCatalogs.some((catalog) => catalog.capabilities.createSession)) { - this.sessionCatalogs = this.sessionCatalogs.map((catalog) => { - const { createSession: _createSession, ...capabilities } = catalog.capabilities; - return { ...catalog, capabilities }; - }); - } - } - - private readonly handleCatalogSessionContinued = ( - event: CustomEvent, - ) => { - const detail = event.detail; - if (!detail?.sessionKey) { - return; - } - this.sessionCatalogs = bindAdoptedCatalogSession(this.sessionCatalogs, detail); - // Invalidate in-flight polls and load-more merges so a pre-adoption - // snapshot cannot clobber the patched rows; the 30s poll reconfirms. - this.sessionCatalogRevision += 1; - this.sessionCatalogRevisions.set( - detail.catalogId, - (this.sessionCatalogRevisions.get(detail.catalogId) ?? 0) + 1, - ); - }; - - private readonly handleSessionCatalogPageActivation = () => { - if (document.visibilityState === "hidden") { - this.sessionCatalogLive.cancelScheduledRefreshes(); - return; - } - this.sessionCatalogLive.scheduleActivation(() => this.requestSessionCatalogRefresh()); - }; - - private requestSessionCatalogRefresh() { - const snapshot = this.context?.gateway.snapshot; - this.sessionCatalogLive.requestRefresh({ - visible: document.visibilityState !== "hidden", - connected: this.isConnected && Boolean(sessionCatalogListClient(snapshot, this.connected)), - generation: this.sessionCatalogGeneration, - refresh: () => void this.refreshSessionCatalogs(), - }); - } - - private applySessionCatalogHostEvent(payload: unknown) { - const update = this.sessionCatalogLive.applyHost({ - payload, - agentId: this.sessionCatalogAgentId ?? "", - catalogs: this.sessionCatalogs, - pageDepths: this.sessionCatalogPageDepths, - }); - if (!update) { - return; - } - this.sessionCatalogs = update.catalogs; - this.sessionCatalogRevision += this.sessionCatalogLive.refetching ? 1 : 0; - const catalogRevision = this.sessionCatalogRevisions.get(update.catalogId) ?? 0; - this.sessionCatalogRevisions.set(update.catalogId, catalogRevision + 1); - if (this.sessionCatalogLive.requestGeneration !== this.sessionCatalogGeneration) { - this.sessionCatalogLive.schedule( - SESSION_CATALOG_CHANGED_REFRESH_MS, - this.isConnected, - () => void this.refreshSessionCatalogs(), - ); - } - } - - private async refreshSessionCatalogs() { - // 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; - } - const generation = this.sessionCatalogGeneration; - const revision = this.sessionCatalogRevision; - const agentId = this.sessionCatalogAgentId ?? this.expandedAgentId(); - await refreshSessionCatalogsLive({ - live: this.sessionCatalogLive, - client, - agentId, - generation, - revision, - currentGeneration: () => this.sessionCatalogGeneration, - currentRevision: () => this.sessionCatalogRevision, - currentClient: () => this.sessionCatalogGatewayClient(), - catalogs: () => this.sessionCatalogs, - pageDepths: this.sessionCatalogPageDepths, - connected: () => this.isConnected, - applyFinal: (catalogs, revisedCatalogIds) => { - this.sessionCatalogs = catalogs; - for (const catalogId of revisedCatalogIds) { - this.sessionCatalogRevisions.set( - catalogId, - (this.sessionCatalogRevisions.get(catalogId) ?? 0) + 1, - ); - } - this.sessionCatalogRevision += 1; - }, - refresh: () => void this.refreshSessionCatalogs(), - }); - } - - async loadMoreSessionCatalog(catalogId: string) { - if (this.loadingMoreSessionCatalogIds.has(catalogId)) { - return; - } - const catalog = this.sessionCatalogs.find((candidate) => candidate.id === catalogId); - const cursors = Object.fromEntries( - (catalog?.hosts ?? []).flatMap((host) => - host.nextCursor ? [[host.hostId, host.nextCursor] as const] : [], - ), - ); - if (!catalog || Object.keys(cursors).length === 0) { - return; - } - const client = this.context?.gateway.snapshot.client; - if (!client || !this.connected) { - return; - } - const generation = this.sessionCatalogGeneration; - const agentId = this.sessionCatalogAgentId ?? this.expandedAgentId(); - const revision = this.sessionCatalogRevisions.get(catalogId) ?? 0; - this.loadingMoreSessionCatalogIds = new Set([...this.loadingMoreSessionCatalogIds, catalogId]); - try { - const result = await client.request("sessions.catalog.list", { - agentId, - catalogId, - cursors, - }); - if ( - generation !== this.sessionCatalogGeneration || - revision !== (this.sessionCatalogRevisions.get(catalogId) ?? 0) || - client !== this.sessionCatalogGatewayClient() - ) { - return; - } - const page = result.catalogs.find((candidate) => candidate.id === catalogId); - if (!page) { - return; - } - const current = this.sessionCatalogs.find((candidate) => candidate.id === catalogId); - if (!current) { - return; - } - const merged = mergeSessionCatalogPage({ current, page, cursors }); - for (const hostId of merged.advancedHostIds) { - const key = sessionCatalogHostKey(catalogId, hostId); - this.sessionCatalogPageDepths.set(key, (this.sessionCatalogPageDepths.get(key) ?? 0) + 1); - } - this.sessionCatalogs = this.sessionCatalogs.map((candidate) => - candidate.id === catalogId ? merged.catalog : candidate, - ); - this.sessionCatalogRevisions.set(catalogId, revision + 1); - this.sessionCatalogRevision += 1; - } catch (error) { - if ( - generation !== this.sessionCatalogGeneration || - revision !== (this.sessionCatalogRevisions.get(catalogId) ?? 0) || - client !== this.sessionCatalogGatewayClient() - ) { - return; - } - // Preserve rows and cursors: retrying Load More requests this page again. - this.sessionCatalogs = this.sessionCatalogs.map((candidate) => - candidate.id === catalogId - ? { ...candidate, error: sessionCatalogRequestError(error) } - : candidate, - ); - this.sessionCatalogRevisions.set(catalogId, revision + 1); - this.sessionCatalogRevision += 1; - } finally { - if (generation === this.sessionCatalogGeneration) { - const loading = new Set(this.loadingMoreSessionCatalogIds); - loading.delete(catalogId); - this.loadingMoreSessionCatalogIds = loading; - } - } - } -} diff --git a/ui/src/components/app-sidebar-session-groups.ts b/ui/src/components/app-sidebar-session-groups.ts index 5509ac2c6e25..0db5424b937b 100644 --- a/ui/src/components/app-sidebar-session-groups.ts +++ b/ui/src/components/app-sidebar-session-groups.ts @@ -21,7 +21,6 @@ import { normalizeOptionalString } from "../lib/string-coerce.ts"; import { AppSidebarSessionMutationsElement } from "./app-sidebar-session-mutations.ts"; import { loadStoredCollapsedSessionSections, - SIDEBAR_SESSION_PAGE_SIZE, storeSidebarSessionStatusFilter, storeCollapsedSessionSections, storeSidebarSessionsGrouping, @@ -223,12 +222,12 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu } try { await scope.sessions.groupsPut([...groups, name]); - return this.isSessionMutationScopeCurrent(scope) ? "completed" : "stale"; + return this.sessionData.isSessionMutationScopeCurrent(scope) ? "completed" : "stale"; } catch (error) { - if (!this.isSessionMutationScopeCurrent(scope)) { + if (!this.sessionData.isSessionMutationScopeCurrent(scope)) { return "stale"; } - this.publishSessionMutationError(scope, error); + this.sessionData.publishSessionMutationError(scope, error); return "failed"; } } @@ -246,7 +245,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu if (!name) { return; } - const scope = this.beginSessionMutation(); + const scope = this.sessionData.beginSessionMutation(); if (!scope) { return; } @@ -256,7 +255,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu } if (sessions.length > 0) { await this.patchSessions(sessions, { category: name }, scope); - } else if (this.isSessionMutationScopeCurrent(scope)) { + } else if (this.sessionData.isSessionMutationScopeCurrent(scope)) { // Header-created groups start empty; re-render so the section shows up. this.requestUpdate(); } @@ -268,7 +267,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu if (!next || next === group) { return; } - const scope = this.beginSessionMutation(); + const scope = this.sessionData.beginSessionMutation(); if (!scope) { return; } @@ -277,7 +276,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu void (async () => { try { const outcome = await scope.sessions.groupsRename(group, next); - if (outcome !== "completed" || !this.isSessionMutationScopeCurrent(scope)) { + if (outcome !== "completed" || !this.sessionData.isSessionMutationScopeCurrent(scope)) { return; } const from = `category:${group}`; @@ -289,7 +288,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu } this.requestUpdate(); } catch (error) { - this.publishSessionMutationError(scope, error); + this.sessionData.publishSessionMutationError(scope, error); } })(); } @@ -298,14 +297,14 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu if (!window.confirm(t("sessionsView.deleteGroupConfirm", { group }))) { return; } - const scope = this.beginSessionMutation(); + const scope = this.sessionData.beginSessionMutation(); if (!scope) { return; } void (async () => { try { const outcome = await scope.sessions.groupsDelete(group); - if (outcome !== "completed" || !this.isSessionMutationScopeCurrent(scope)) { + if (outcome !== "completed" || !this.sessionData.isSessionMutationScopeCurrent(scope)) { return; } const collapsed = new Set(this.collapsedSessionSections); @@ -313,7 +312,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu this.saveCollapsedSessionSections(collapsed); this.requestUpdate(); } catch (error) { - this.publishSessionMutationError(scope, error); + this.sessionData.publishSessionMutationError(scope, error); } })(); } @@ -339,18 +338,18 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu private reorderSessionGroup(source: string, target: string, position: "before" | "after") { const groups = reorderSessionCustomGroups(this.knownSessionGroups(), source, target, position); - const scope = this.beginSessionMutation(); + const scope = this.sessionData.beginSessionMutation(); if (!scope) { return; } void (async () => { try { await scope.sessions.groupsPut(groups); - if (this.isSessionMutationScopeCurrent(scope)) { + if (this.sessionData.isSessionMutationScopeCurrent(scope)) { this.requestUpdate(); } } catch (error) { - this.publishSessionMutationError(scope, error); + this.sessionData.publishSessionMutationError(scope, error); } })(); } @@ -360,7 +359,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu category: string | null, patch: { pinned?: boolean } = {}, ) { - const scope = this.beginSessionMutation(); + const scope = this.sessionData.beginSessionMutation(); if (!scope) { return; } @@ -422,7 +421,7 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu if (active) { return active; } - for (const rows of Object.values(this.sessionRowsByAgent)) { + for (const rows of Object.values(this.sessionData.sessionRowsByAgent)) { const row = rows.find((candidate) => candidate.key === sessionKey); if (row) { return navigationState.toSidebarSession(row); @@ -494,21 +493,12 @@ export abstract class AppSidebarSessionGroupsElement extends AppSidebarSessionMu } this.sessionsStatusFilter = statusFilter; this.clearSessionSelection(); - this.visibleSessionLimit = SIDEBAR_SESSION_PAGE_SIZE; - this.childSessionRowsByParent = {}; - this.loadedChildSessionKeys = new Set(); - this.failedChildSessionKeys = new Set(); - this.loadingChildSessionKeys = new Set(); - this.sessionRowsByAgent = {}; - if (statusFilter === "active" && this.context) { - this.sessionsResult = this.context.sessions.state.result; - this.sessionsAgentId = this.context.sessions.state.agentId; - } + this.sessionData.resetForStatusFilter(statusFilter); try { storeSidebarSessionStatusFilter(statusFilter); } catch { // Keep the in-memory preference when storage is unavailable. } - void this.refreshSidebarSessions(); + void this.sessionData.refreshSidebarSessions(); } } diff --git a/ui/src/components/app-sidebar-session-list-render.ts b/ui/src/components/app-sidebar-session-list-render.ts index 6766696ac4db..c508fb1cbcb4 100644 --- a/ui/src/components/app-sidebar-session-list-render.ts +++ b/ui/src/components/app-sidebar-session-list-render.ts @@ -312,7 +312,7 @@ function renderSessionCatalogs(params: { }), onToggleSection: (sectionId) => host.toggleSection(sectionId), onToggleProjectGrouping: () => host.toggleCatalogProjectGrouping(), - onLoadMore: (catalogId) => void host.loadMoreSessionCatalog(catalogId), + onLoadMore: (catalogId) => void host.sessionData.loadMoreSessionCatalog(catalogId), onOpenNewSession: host.onOpenNewSession, onNavigate: host.onNavigate, catalogOpenTarget: snapshot.catalogOpenTarget, @@ -388,14 +388,14 @@ export function renderSessionList(params: { @dragleave=${(event: DragEvent) => host.handleSessionListDragLeave(event)} @drop=${(event: DragEvent) => host.handleSessionListDrop(event)} > - ${host.sessionMutationError + ${host.sessionData.sessionMutationError ? html`