Files
openclaw/ui/src/components/sidebar-menus-controller.test.ts
T
Peter Steinberger cca5b14785 feat(ui): path-based session and dashboard URLs (#113883)
* feat(ui): path-based session and dashboard URLs

* docs(ui): document control UI URLs

* fix(ui): finalize session path routing

* feat(ui): anchor session URLs on stable keys

* docs(ui): clarify stable session URL identity

* fix(ui): resolve session prefixes with full prefix and pagination

Query sessions.list with the full supplied prefix instead of an eight-char
truncation, and paginate up to a bounded page count so longer disambiguation
links resolve instead of being reported ambiguous. Zero strict-prefix matches
now fall through to literal-key resolution rather than rendering an empty
ambiguity view.

Also document the ~dot/~dotdot segment escape: peer ids reach session keys
trimmed and lowercased only (src/routing/session-key.ts), so a literal '.' or
'..' segment is reachable and browsers would normalize it away.

* fix(ui): synchronize committed session routes

* test(ui): split native shell host coverage

* fix(ui): thread configured mainKey through session URL builders

Reserved-set disambiguation needs the operator-configured mainKey at runtime, so
thread it from agentsList through every session path builder and the ClickClack
control URL. Unambiguous non-hex single-segment rests now resolve literally while
short-id-shaped rests still fail closed, which restores ClickClack channel
compatibility detection and control-link reconciliation.

Also bound prefix-resolution retries, preserve catalog thread identity, and keep
draft state on ambiguous candidate links.

* fix(ui): repair session URL CI integration

* perf(ui): lazy-load session route resolution

* perf(ui): isolate session prefix resolution

* perf(ui): defer session path parsing

* perf(ui): defer session navigation startup

* fix(ui): preserve first-run and literal session navigation

* fix(ui): satisfy session routing type and export gates

* fix(clickclack): preserve unscoped control link agent

* style(ui): satisfy chat page line limit

* refactor(ui): move chat page helpers to owning modules

* fix(ui): preserve destination session route identity

* fix(ui): preserve agent identity in session routes

* fix(ui): escape dots in literal session path segments

encodeURIComponent leaves periods intact, so a literal key segment like
channel:release.js produced /chat/main/channel/release.js. In-app navigation is
intercepted by the SPA, but a refresh, an external link, or a ClickClack link
would be served as a static asset request and never reach the app.

pathForWorkboardBoard already escapes dots for this reason; mirror it in both the
session URL contract and the ClickClack encoder, route the agent id through the
same segment encoder, and pin the case in both shared vector tables.

* fix(ui): stop bootstrap after teardown race

* refactor(ui): centralize session navigation targets

* fix(ui): consume bootstrap teardown abort

* fix(ui): canonicalize configured main session routes

* fix(ui): preserve distinct session references

* fix(ui): redirect released session query links

* fix(ui): make bootstrap teardown abort-safe
2026-07-26 12:46:04 -04:00

68 lines
2.3 KiB
TypeScript

import { render } from "lit";
import { describe, expect, it, vi } from "vitest";
import type { GatewayBrowserClient } from "../api/gateway.ts";
import { buildCatalogSessionKey } from "../lib/sessions/catalog-key.ts";
import {
createContext,
createGateway,
createSessions,
TWO_AGENTS,
} from "../test-helpers/app-sidebar.ts";
import {
SidebarMenusController,
type SidebarMenusControllerHost,
} from "./sidebar-menus-controller.ts";
describe("SidebarMenusController session routes", () => {
it("keeps the current catalog session when switching either face", () => {
const sessionKey = buildCatalogSessionKey({
catalogId: "claude",
hostId: "gateway:local",
threadId: "thread-1",
});
const context = createContext(
createGateway({} as GatewayBrowserClient),
createSessions("main", [sessionKey]),
TWO_AGENTS,
);
Object.assign(context, { basePath: "" });
const onNavigate = vi.fn();
const host = {
activeRouteId: "chat",
activeWorkboardBoardId: "",
addController: vi.fn(),
basePath: "",
enabledRouteIds: ["chat", "dashboard"],
getRouteSessionKey: () => sessionKey,
onNavigate,
requestUpdate: vi.fn(),
sessionDataContext: context,
terminalAvailable: false,
} as unknown as SidebarMenusControllerHost;
const controller = new SidebarMenusController(host);
const container = document.createElement("div");
render(controller.renderRoute("chat"), container);
const chat = container.querySelector<HTMLAnchorElement>("a");
expect(chat?.getAttribute("href")).toBe(
"/chat/main?catalog=claude&host=gateway%3Alocal&thread=thread-1",
);
chat?.click();
expect(onNavigate).toHaveBeenLastCalledWith("chat", {
pathname: "/chat/main",
search: "?catalog=claude&host=gateway%3Alocal&thread=thread-1",
});
render(controller.renderRoute("dashboard"), container);
const dashboard = container.querySelector<HTMLAnchorElement>("a");
expect(dashboard?.getAttribute("href")).toBe(
"/dashboard/main?catalog=claude&host=gateway%3Alocal&thread=thread-1",
);
dashboard?.click();
expect(onNavigate).toHaveBeenLastCalledWith("dashboard", {
pathname: "/dashboard/main",
search: "?catalog=claude&host=gateway%3Alocal&thread=thread-1",
});
});
});