fix(ui): report connection loss instead of silently dropping session actions (#123771)

* fix(ui): report connection loss instead of silently dropping session actions

patchSession returned 'stale' with no message when the gateway dropped
before a queued dialog submit (rename and similar) — the dialog closed and
the edit vanished with no visible outcome. loadCheckpoint had the sibling
gap: expanding a row while disconnected left the drawer claiming 'No
checkpoints' beside a nonzero checkpoint badge. Both now surface the
existing actionRequiresConnection message at their owning error surface.

* test: widen patchSession test-support patch type for label patches
This commit is contained in:
Peter Steinberger
2026-08-14 11:33:47 -07:00
committed by GitHub
parent 0bdf11c9dc
commit 877f2ec083
3 changed files with 41 additions and 2 deletions
@@ -25,6 +25,7 @@ export type TestSessionsPage = HTMLElement & {
sessionMenu: { key: string; x: number; y: number } | null;
sessionMenuTrigger: HTMLElement | null;
checkpointItemsByKey: Record<string, SessionCompactionCheckpoint[]>;
checkpointErrorByKey: Record<string, string>;
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<unknown>;
@@ -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<SessionsSearchResult>();
const request = vi.fn(() => response.promise);
+10 -1
View File
@@ -1144,7 +1144,10 @@ class SessionsPage extends OpenClawLightDomElement {
expectedSessionId?: string,
): Promise<SessionsPageMutationResult> {
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;