fix(ui): archived sessions leave sidebar after navigation (#122876)

* oc-ad0.1: fix archived child session retention

* oc-37c: evict archived child on navigation
This commit is contained in:
Josh Lehman
2026-08-12 17:25:38 -07:00
committed by GitHub
parent f3f203f31d
commit 41f4b5f673
3 changed files with 35 additions and 20 deletions
@@ -110,7 +110,7 @@ export function preserveActiveSessionLineageRows(
if (!parent) {
break;
}
preserved[parent[0]] = parent[1];
preserved[parent[0]] = parent[1].filter((row) => areUiSessionKeysEquivalent(row.key, childKey));
childKey = parent[0];
}
return preserved;
@@ -205,6 +205,14 @@ export function evictArchivedSessionLineage(
row != null && areUiSessionKeysEquivalent(row.key, sessionKey),
);
if (selectedRow?.archived === true) {
// Navigation has ended the archived row's temporary presentation lease.
// Remove it from the child cache before the next canonical list refresh.
owner.childSessionRowsByParent = Object.fromEntries(
Object.entries(owner.childSessionRowsByParent).map(([parentKey, rows]) => [
parentKey,
rows.filter((row) => !areUiSessionKeysEquivalent(row.key, sessionKey)),
]),
);
owner.context?.sessions.reconcile(selectedRow, owner.sessionsResult?.defaults, {
archivedFilter: "active",
});
@@ -7,7 +7,6 @@ import {
controlUiSessionUrl,
createSessionManagementE2eSuite,
installMockGateway,
navigateToControlUiSession,
requireRecord,
sessionRow,
sessionsListResponse,
@@ -564,13 +563,27 @@ suite.define(() => {
const page = await context.newPage();
const baseTime = Date.parse("2026-07-01T16:00:00.000Z");
const main = sessionRow("agent:main:main", "Main", baseTime);
const target = {
...sessionRow(
"agent:main:dashboard:navigation-target",
"Navigation target",
baseTime - 1_000,
),
parentSessionKey: main.key,
sessionId: "navigation-target",
};
const archived = {
...sessionRow("agent:main:navigation-archive", "Navigation archive", baseTime - 1_000),
...sessionRow(
"agent:main:dashboard:navigation-archive",
"Navigation archive",
baseTime - 2_000,
),
parentSessionKey: main.key,
sessionId: "navigation-archive",
};
const gateway = await installMockGateway(page, {
methodResponses: {
"sessions.list": sessionsListResponse([main, archived]),
"sessions.list": sessionsListResponse([main, target, archived]),
"sessions.patch": {},
},
sessionKey: main.key,
@@ -599,35 +612,36 @@ suite.define(() => {
.locator(".agent-chat__disabled-banner");
await archivedNotice.waitFor({ state: "visible", timeout: 10_000 });
await navigateToControlUiSession(page, main.key);
await expect.poll(() => new URL(page.url()).pathname).toBe(controlUiSessionPath(main.key));
await rowFor(target.key).click();
await expect.poll(() => new URL(page.url()).pathname).toBe(controlUiSessionPath(target.key));
await archivedRow.waitFor({ state: "detached", timeout: 10_000 });
await gateway.setMethodResponse("sessions.list", sessionsListResponse([main]));
await gateway.setMethodResponse("sessions.list", sessionsListResponse([main, target]));
let listRequestCount = (await gateway.getRequests("sessions.list")).length;
await gateway.emitGatewayEvent("sessions.changed", {
...main,
...target,
updatedAt: baseTime + 1_000,
reason: "update",
sessionKey: main.key,
sessionKey: target.key,
});
await expect
.poll(async () => (await gateway.getRequests("sessions.list")).length)
.toBeGreaterThan(listRequestCount);
await archivedRow.waitFor({ state: "detached", timeout: 10_000 });
await gateway.setMethodResponse(
"sessions.list",
sessionsListResponse([
{ ...main, updatedAt: baseTime + 2_000 },
{ ...target, updatedAt: baseTime + 3_000 },
{ ...archived, archived: false, updatedAt: baseTime + 3_000 },
]),
);
listRequestCount = (await gateway.getRequests("sessions.list")).length;
await gateway.emitGatewayEvent("sessions.changed", {
...main,
...target,
updatedAt: baseTime + 2_000,
reason: "update",
sessionKey: main.key,
sessionKey: target.key,
});
await expect
.poll(async () => (await gateway.getRequests("sessions.list")).length)
@@ -7,20 +7,13 @@ import {
controlUiSessionPath,
controlUiSessionUrl,
installMockGateway,
navigateToControlUiSession,
waitForConfirmModal,
type MockGatewayControls,
type MockGatewayRequest,
} from "../test-helpers/control-ui-e2e.ts";
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
export {
controlUiSessionPath,
controlUiSessionUrl,
installMockGateway,
navigateToControlUiSession,
waitForConfirmModal,
};
export { controlUiSessionPath, controlUiSessionUrl, installMockGateway, waitForConfirmModal };
export const collapsedSessionSectionsStorageKey = "openclaw:sidebar:sessions:collapsed-sections";
export const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";