diff --git a/ui/src/app/app-host-native-shell.test.ts b/ui/src/app/app-host-native-shell.test.ts index bc087563dbdc..cdb0d76dbd06 100644 --- a/ui/src/app/app-host-native-shell.test.ts +++ b/ui/src/app/app-host-native-shell.test.ts @@ -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")); diff --git a/ui/src/app/app-host.ts b/ui/src/app/app-host.ts index 1b03cbeaa094..9f167e57a10a 100644 --- a/ui/src/app/app-host.ts +++ b/ui/src/app/app-host.ts @@ -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; diff --git a/ui/src/app/app-shell-navigation.ts b/ui/src/app/app-shell-navigation.ts index 978255752ffd..2818c2a10d2a 100644 --- a/ui/src/app/app-shell-navigation.ts +++ b/ui/src/app/app-shell-navigation.ts @@ -25,7 +25,7 @@ export interface ShellNavigationHost { readonly context: ApplicationContext | undefined; activeSessionKey: string; routeState: ShellRouteState; - lastWorkspaceLocation: { routeId: RouteId; pathname: string; search: string } | null; + lastWorkspaceLocation: ({ routeId: RouteId } & Required) | 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"); diff --git a/ui/src/components/settings-sidebar.test.ts b/ui/src/components/settings-sidebar.test.ts index e21893065bf2..94f8c0e4f6f6 100644 --- a/ui/src/components/settings-sidebar.test.ts +++ b/ui/src/components/settings-sidebar.test.ts @@ -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( '.settings-sidebar__subitem[href="/settings/appearance?section=__appearance__#settings-language"]', diff --git a/ui/src/components/settings-sidebar.ts b/ui/src/components/settings-sidebar.ts index c8987b84e711..15177bf3828b 100644 --- a/ui/src/components/settings-sidebar.ts +++ b/ui/src/components/settings-sidebar.ts @@ -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` { 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"); diff --git a/ui/src/e2e/sidebar-customization.e2e.test.ts b/ui/src/e2e/sidebar-customization.e2e.test.ts index 162443b532a2..60de03d58496 100644 --- a/ui/src/e2e/sidebar-customization.e2e.test.ts +++ b/ui/src/e2e/sidebar-customization.e2e.test.ts @@ -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");