fix(ui): preserve Settings navigation selection and workspace fragments (#129888)

* fix(ui): preserve Settings navigation selection and workspace fragments

* test(ui): scope mobile session menu proof to active chat pane
This commit is contained in:
Peter Steinberger
2026-08-25 23:06:27 -07:00
committed by GitHub
parent ab9b764d68
commit 48e026161e
7 changed files with 51 additions and 15 deletions
+32 -3
View File
@@ -23,7 +23,7 @@ type ShellNavigationState = {
};
type ShellSettingsEscapeState = ShellKeyboardState & {
lastWorkspaceLocation: { routeId: "usage"; pathname: string; search: string };
lastWorkspaceLocation: { routeId: "usage"; pathname: string; search: string; hash: string };
navDrawerOpen: boolean;
routeState: { routeId: "appearance" };
};
@@ -115,6 +115,35 @@ describe("OpenClaw native shell", () => {
expect(navigate).toHaveBeenCalledWith("appearance", undefined);
});
it("restores the complete prior workspace URL when Escape leaves Settings", () => {
const navigate = vi.fn();
const shell = document.createElement(
"openclaw-app-shell",
) as unknown as ShellSettingsEscapeState;
shell.runtime = {
context: {
navigate,
overlays: { snapshot: { devicePairSetupOpen: false } },
} as unknown as ApplicationContext,
};
shell.lastWorkspaceLocation = {
routeId: "usage",
pathname: "/usage",
search: "?agent=main",
hash: "#queue",
};
shell.navDrawerOpen = false;
shell.routeState = { routeId: "appearance" };
shell.handleDocumentKeydown(new KeyboardEvent("keydown", { key: "Escape", cancelable: true }));
expect(navigate).toHaveBeenCalledWith("usage", {
pathname: "/usage",
search: "?agent=main",
hash: "#queue",
});
});
it("keeps the raw config editor unchanged when Escape is pressed", () => {
const navigate = vi.fn();
const shell = document.createElement(
@@ -126,7 +155,7 @@ describe("OpenClaw native shell", () => {
overlays: { snapshot: { devicePairSetupOpen: false } },
} as unknown as ApplicationContext,
};
shell.lastWorkspaceLocation = { routeId: "usage", pathname: "/usage", search: "" };
shell.lastWorkspaceLocation = { routeId: "usage", pathname: "/usage", search: "", hash: "" };
shell.navDrawerOpen = false;
shell.routeState = { routeId: "appearance" };
const rawField = document.body.appendChild(document.createElement("label"));
@@ -162,7 +191,7 @@ describe("OpenClaw native shell", () => {
overlays: { snapshot: { devicePairSetupOpen: false } },
} as unknown as ApplicationContext,
};
shell.lastWorkspaceLocation = { routeId: "usage", pathname: "/usage", search: "" };
shell.lastWorkspaceLocation = { routeId: "usage", pathname: "/usage", search: "", hash: "" };
shell.navDrawerOpen = false;
shell.routeState = { routeId: "appearance" };
const container = document.body.appendChild(document.createElement("div"));
+1 -1
View File
@@ -178,7 +178,7 @@ class OpenClawShell
readonly navigationSidebar = document.createElement(APP_SIDEBAR_TAG) as AppSidebarElement;
// Where "Back to app" / Escape leaves the settings takeover; falls back to
// chat (the app default route) when settings was the entry point.
lastWorkspaceLocation: { routeId: RouteId; pathname: string; search: string } | null = null;
lastWorkspaceLocation: ShellNavigationHost["lastWorkspaceLocation"] = null;
custodianMinimizeRequestId = 0;
lastConcreteRouteId: RouteId | undefined;
agentsListClient: GatewayBrowserClient | null = null;
+4 -5
View File
@@ -25,7 +25,7 @@ export interface ShellNavigationHost {
readonly context: ApplicationContext<RouteId> | undefined;
activeSessionKey: string;
routeState: ShellRouteState;
lastWorkspaceLocation: { routeId: RouteId; pathname: string; search: string } | null;
lastWorkspaceLocation: ({ routeId: RouteId } & Required<ApplicationNavigationOptions>) | null;
custodianMinimizeRequestId: number;
lastConcreteRouteId: RouteId | undefined;
didConsiderNativeRouteRestore: boolean;
@@ -260,6 +260,7 @@ export class ShellNavigationOwner {
routeId: routeState.routeId,
pathname: routeState.location?.pathname ?? "",
search: routeState.location?.search ?? "",
hash: routeState.location?.hash ?? "",
};
}
}
@@ -281,10 +282,8 @@ export class ShellNavigationOwner {
exitSettings(): void {
const previous = this.host.lastWorkspaceLocation;
if (previous) {
this.navigate(previous.routeId, {
pathname: previous.pathname,
...(previous.search ? { search: previous.search } : {}),
});
const { routeId, ...location } = previous;
this.navigate(routeId, location);
return;
}
this.navigate("chat");
+3 -1
View File
@@ -183,7 +183,9 @@ describe("settings sidebar search", () => {
),
].map((item) => item.textContent?.trim());
expect(resultLabels).toEqual(["MCP", "Appearance", "Language"]);
expect(container.querySelector(".settings-sidebar__item--active")).toBeNull();
const active = container.querySelector(".settings-sidebar__item--active");
expect(active?.textContent).toContain("Appearance");
expect(active?.getAttribute("aria-current")).toBe("page");
const language = container.querySelector<HTMLAnchorElement>(
'.settings-sidebar__subitem[href="/settings/appearance?section=__appearance__#settings-language"]',
+1 -2
View File
@@ -171,8 +171,7 @@ function filterSettingsNavigationGroups(
}
function renderItem(props: SettingsSidebarProps, routeId: RouteId, label?: string) {
const active =
!props.searchQuery && settingsNavigationOwnerRoute(props.activeRouteId) === routeId;
const active = settingsNavigationOwnerRoute(props.activeRouteId) === routeId;
return html`
<a
href=${pathForRoute(routeId, props.basePath)}
@@ -172,9 +172,10 @@ suite.define(() => {
sessionKey,
});
await page.goto(controlUiSessionUrl(suite.server.baseUrl, sessionKey));
await page.getByText("Mobile session menu proof.", { exact: true }).waitFor();
const activePane = page.locator("openclaw-chat-pane.chat-pane-cache__pane--active");
await activePane.getByText("Mobile session menu proof.", { exact: true }).waitFor();
const menuTrigger = page.getByRole("button", {
const menuTrigger = activePane.getByRole("button", {
name: "Actions for Terminal continuation",
});
await menuTrigger.press("Enter");
+7 -1
View File
@@ -471,9 +471,15 @@ suite.define(() => {
await settingsSearch.fill("channel");
await captureSettingsSidebarProof(settingsSidebar, "01e-settings-search-route.png");
await holdUiProof(page);
await settingsSidebar.getByRole("link", { name: "Channels" }).first().click();
const channelsResult = settingsSidebar.getByRole("link", { name: "Channels" }).first();
await channelsResult.click();
await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/channels");
await expect.poll(() => settingsSearch.inputValue()).toBe("channel");
await captureSettingsSidebarProof(
settingsSidebar,
`settings-navigation-${process.env.OPENCLAW_UI_PROOF_LABEL ?? "current"}.png`,
);
await expect.poll(() => channelsResult.getAttribute("aria-current")).toBe("page");
await captureSettingsSidebarProof(settingsSidebar, "01f-settings-search-navigated.png");
await holdUiProof(page);
await page.keyboard.press("Escape");