diff --git a/ui/src/pages/sessions/sessions-page.test-support.ts b/ui/src/pages/sessions/sessions-page.test-support.ts index 3489be68654e..74b47b098680 100644 --- a/ui/src/pages/sessions/sessions-page.test-support.ts +++ b/ui/src/pages/sessions/sessions-page.test-support.ts @@ -25,6 +25,7 @@ export type TestSessionsPage = HTMLElement & { sessionMenu: { key: string; x: number; y: number } | null; sessionMenuTrigger: HTMLElement | null; checkpointItemsByKey: Record; + checkpointErrorByKey: Record; checkpointLoadingKey: string | null; checkpointBusyKey: string | null; sessionMutationPending: boolean; @@ -47,7 +48,7 @@ export type TestSessionsPage = HTMLElement & { ) => void; patchSession: ( key: string, - patch: { archived?: boolean; pinned?: boolean }, + patch: { archived?: boolean; pinned?: boolean; label?: string | null }, scope?: unknown, expectedSessionId?: string, ) => Promise; diff --git a/ui/src/pages/sessions/sessions-page.test.ts b/ui/src/pages/sessions/sessions-page.test.ts index 7dc3fe07039b..2d87270af3e2 100644 --- a/ui/src/pages/sessions/sessions-page.test.ts +++ b/ui/src/pages/sessions/sessions-page.test.ts @@ -281,6 +281,35 @@ describe("sessions page lifecycle", () => { expect(page.transcriptSearch).toEqual({ status: "idle" }); }); + it("reports a connection error instead of silently dropping a patch", async () => { + const patch = vi.fn(); + const sessions = createSessions({ patch }); + const mutableGateway = createGateway({} as GatewayBrowserClient); + const page = await createPage(createContext(mutableGateway.gateway, sessions)); + // Gateway drops while a rename dialog is open; submit lands afterwards. + mutableGateway.emit({ phase: "reconnecting", client: null }); + + const result = await page.patchSession("agent:main:main", { label: "renamed" }); + + expect(result).toBe("failed"); + expect(patch).not.toHaveBeenCalled(); + expect(page.error).toBe("Connect to the Gateway to change sessions."); + }); + + it("shows a connection error in the checkpoints drawer while disconnected", async () => { + const mutableGateway = createGateway({} as GatewayBrowserClient); + const page = await createPage(createContext(mutableGateway.gateway, createSessions())); + mutableGateway.emit({ phase: "reconnecting", client: null }); + + await page.loadCheckpoint("agent:main:main"); + + // Without the recorded error the drawer would render "No checkpoints" + // beside a nonzero checkpoint badge. + expect(page.checkpointErrorByKey["agent:main:main"]).toBe( + "Connect to the Gateway to change sessions.", + ); + }); + it("drops a transcript result after the query changes while it is pending", async () => { const response = deferred(); const request = vi.fn(() => response.promise); diff --git a/ui/src/pages/sessions/sessions-page.ts b/ui/src/pages/sessions/sessions-page.ts index 1f33d18e6573..a16a188359b0 100644 --- a/ui/src/pages/sessions/sessions-page.ts +++ b/ui/src/pages/sessions/sessions-page.ts @@ -1144,7 +1144,10 @@ class SessionsPage extends OpenClawLightDomElement { expectedSessionId?: string, ): Promise { if (!scope) { - return "stale"; + // Nothing was attempted (e.g. rename dialog submitted after the gateway + // dropped); say so instead of silently swallowing the edit. + this.error = t("sessionsView.actionRequiresConnection"); + return "failed"; } if (typeof patch.archived === "boolean" && !expectedSessionId?.trim()) { this.error = "Session lifecycle action requires a durable session identity."; @@ -1280,6 +1283,12 @@ class SessionsPage extends OpenClawLightDomElement { private async loadCheckpoint(sessionKey: string) { const scope = this.captureRequestScope(); if (!scope) { + // Rows stay expandable while disconnected; without an error the drawer + // would claim "No checkpoints" beside a nonzero checkpoint badge. + this.checkpointErrorByKey = { + ...this.checkpointErrorByKey, + [sessionKey]: t("sessionsView.actionRequiresConnection"), + }; return; } this.checkpointTaskKey = sessionKey;