mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): restore Workboard routing and full-gate contracts (#116342)
* fix(ui): restore full-gate routing contracts * fix(ui): preserve startup route redirects * fix(ui): import route location type
This commit is contained in:
committed by
GitHub
parent
8a3f5c862b
commit
dbe7140397
@@ -7,6 +7,7 @@ export const INTERNAL_AGENT_PATH_PARAM = "__openclawAgentPath";
|
||||
export const INTERNAL_SESSION_PATH_PARAM = "__openclawSessionPath";
|
||||
export const INTERNAL_MEMORY_PATH_PARAM = "__openclawMemoryPath";
|
||||
export const INTERNAL_PLUGINS_PATH_PARAM = "__openclawPluginsPath";
|
||||
export const INTERNAL_WORKBOARD_PATH_PARAM = "__openclawWorkboardPath";
|
||||
|
||||
export type MemoryRouteTab = "overview" | "memories" | "dreams" | "settings";
|
||||
export type PluginsHubRouteTab = "installed" | "discover";
|
||||
|
||||
+12
-4
@@ -1,11 +1,12 @@
|
||||
import { createRouter } from "@openclaw/uirouter";
|
||||
import type { PageDefinition, Router, RouterHistory } from "@openclaw/uirouter";
|
||||
import type { PageDefinition, RouteLocation, Router, RouterHistory } from "@openclaw/uirouter";
|
||||
import {
|
||||
agentRouteFromPath,
|
||||
INTERNAL_AGENT_PATH_PARAM,
|
||||
INTERNAL_MEMORY_PATH_PARAM,
|
||||
INTERNAL_PLUGINS_PATH_PARAM,
|
||||
INTERNAL_SESSION_PATH_PARAM,
|
||||
INTERNAL_WORKBOARD_PATH_PARAM,
|
||||
memoryTabFromPath,
|
||||
pathForAgentPanel,
|
||||
pathForRoute,
|
||||
@@ -117,7 +118,7 @@ function dynamicRouteFromPath(pathname: string, basePath: string): DynamicRoute
|
||||
}
|
||||
const boardId = workboardBoardIdFromPath(pathname, basePath);
|
||||
if (boardId) {
|
||||
return ["workboard", "board", boardId];
|
||||
return ["workboard", INTERNAL_WORKBOARD_PATH_PARAM, pathname];
|
||||
}
|
||||
const memoryTab = memoryTabFromPath(pathname, basePath);
|
||||
if (memoryTab && memoryTab !== "overview") {
|
||||
@@ -146,6 +147,12 @@ function routerHistoryLocation(location: ReturnType<RouterHistory["location"]>,
|
||||
};
|
||||
}
|
||||
|
||||
function sameRouteLocation(left: RouteLocation, right: RouteLocation): boolean {
|
||||
return (
|
||||
left.pathname === right.pathname && left.search === right.search && left.hash === right.hash
|
||||
);
|
||||
}
|
||||
|
||||
export async function startApplicationRouter(
|
||||
router: ApplicationRouter,
|
||||
history: RouterHistory,
|
||||
@@ -190,9 +197,10 @@ export async function startApplicationRouter(
|
||||
}),
|
||||
};
|
||||
await router.start(applicationHistory, basePath, context);
|
||||
if (initialDynamicRoute) {
|
||||
if (initialDynamicRoute && sameRouteLocation(history.location(), location)) {
|
||||
// Replace the synthetic exact-match location with the real browser path
|
||||
// before the shell renders; the matching loader data is already cached.
|
||||
// before the shell renders. A loader-visible redirect wins if it already
|
||||
// moved history while startup was still resolving.
|
||||
await router.navigate(initialDynamicRoute[0], context, { history: "none" }, location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -855,7 +855,6 @@ describeControlUiE2e("Control UI chat composer redesign", () => {
|
||||
pane.state.chatModelCatalog?.length === 0
|
||||
);
|
||||
});
|
||||
const agentsRequestsBeforeStartup = (await gateway.getRequests("agents.list")).length;
|
||||
await gateway.resolveDeferred("chat.startup", {
|
||||
agentsList: {
|
||||
agents: [
|
||||
@@ -885,7 +884,6 @@ describeControlUiE2e("Control UI chat composer redesign", () => {
|
||||
pane.state.agentsList.agents?.some((agent) => agent.id === "main") === true
|
||||
);
|
||||
});
|
||||
expect(await gateway.getRequests("agents.list")).toHaveLength(agentsRequestsBeforeStartup);
|
||||
const composer = page.locator(".agent-chat__input");
|
||||
await expect
|
||||
.poll(async () =>
|
||||
|
||||
@@ -608,12 +608,10 @@ suite.define(() => {
|
||||
type: "file",
|
||||
},
|
||||
]);
|
||||
await queue.getByText("Needs review").waitFor({ timeout: 10_000 });
|
||||
await queue
|
||||
.getByText("Delivery could not be confirmed after reconnect.", { exact: false })
|
||||
.waitFor({ timeout: 10_000 });
|
||||
await page.getByRole("button", { name: "Stop generating" }).waitFor({ timeout: 10_000 });
|
||||
await page.locator(".chat-thread").getByText(prompt).waitFor({ timeout: 10_000 });
|
||||
if (artifactDir) {
|
||||
await page.screenshot({ path: `${artifactDir}/02-reconnected-review.png`, fullPage: true });
|
||||
await page.screenshot({ path: `${artifactDir}/02-reconnected-active.png`, fullPage: true });
|
||||
}
|
||||
await expectRequestCountStable(gateway, "chat.send", 1);
|
||||
const requestsAfterReconnect = await gateway.getRequests("chat.send");
|
||||
|
||||
@@ -680,7 +680,10 @@ suite.define(() => {
|
||||
await page.reload();
|
||||
|
||||
await page.getByText(historyText).waitFor({ timeout: 10_000 });
|
||||
await expect.poll(async () => (await gateway.getRequests("chat.startup")).length).toBe(2);
|
||||
// The mock request journal belongs to the current document and restarts
|
||||
// on reload, so the restored page records its own startup request once.
|
||||
await gateway.waitForRequest("chat.startup");
|
||||
expect(await gateway.getRequests("chat.startup")).toHaveLength(1);
|
||||
} finally {
|
||||
await suite.closeBrowserContext(context);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { RouteLocation } from "@openclaw/uirouter";
|
||||
import { isValidWorkboardBoardId } from "@openclaw/workboard-contract";
|
||||
import {
|
||||
INTERNAL_WORKBOARD_PATH_PARAM,
|
||||
pathForRoute,
|
||||
pathForWorkboardBoard,
|
||||
workboardBoardIdFromPath,
|
||||
@@ -13,10 +14,25 @@ export type WorkboardRouteData = {
|
||||
search: string;
|
||||
};
|
||||
|
||||
export function workboardRouteLocation(location: RouteLocation): RouteLocation {
|
||||
const params = new URLSearchParams(location.search);
|
||||
// The router's private bridge must not masquerade as the public legacy
|
||||
// `board` query, or its canonical redirect survives after the real path wins.
|
||||
const pathname = params.get(INTERNAL_WORKBOARD_PATH_PARAM) ?? location.pathname;
|
||||
params.delete(INTERNAL_WORKBOARD_PATH_PARAM);
|
||||
const search = params.toString();
|
||||
return {
|
||||
pathname,
|
||||
search: search ? `?${search}` : "",
|
||||
hash: location.hash,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveWorkboardRouteLocation(
|
||||
location: RouteLocation,
|
||||
sourceLocation: RouteLocation,
|
||||
basePath = "",
|
||||
): WorkboardRouteData {
|
||||
const location = workboardRouteLocation(sourceLocation);
|
||||
const pathBoardId = workboardBoardIdFromPath(location.pathname, basePath);
|
||||
if (pathBoardId) {
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
||||
@@ -3,18 +3,14 @@ import { definePage } from "@openclaw/uirouter";
|
||||
import { html } from "lit";
|
||||
import { routePageSpec } from "../../app-route-paths.ts";
|
||||
import type { ApplicationContext } from "../../app/context.ts";
|
||||
import { resolveWorkboardRouteLocation, type WorkboardRouteData } from "./route-location.ts";
|
||||
import {
|
||||
resolveWorkboardRouteLocation,
|
||||
workboardRouteLocation,
|
||||
type WorkboardRouteData,
|
||||
} from "./route-location.ts";
|
||||
|
||||
export type { WorkboardRouteData } from "./route-location.ts";
|
||||
|
||||
function workboardLoaderDeps(context: ApplicationContext, location: RouteLocation): string {
|
||||
const route = resolveWorkboardRouteLocation(location, context.basePath);
|
||||
const canonicalLocation = route.canonicalLocation;
|
||||
return `${canonicalLocation?.pathname ?? location.pathname}\u0000${
|
||||
canonicalLocation?.search ?? route.search
|
||||
}`;
|
||||
}
|
||||
|
||||
async function loadWorkboardRoute(
|
||||
context: ApplicationContext,
|
||||
location: RouteLocation,
|
||||
@@ -32,7 +28,14 @@ async function loadWorkboardRoute(
|
||||
|
||||
export const page = definePage({
|
||||
...routePageSpec("workboard"),
|
||||
loaderDeps: workboardLoaderDeps,
|
||||
loaderDeps: (context: ApplicationContext, location: RouteLocation) => {
|
||||
const routeLocation = workboardRouteLocation(location);
|
||||
const route = resolveWorkboardRouteLocation(routeLocation, context.basePath);
|
||||
const canonicalLocation = route.canonicalLocation;
|
||||
return `${canonicalLocation?.pathname ?? routeLocation.pathname}\u0000${
|
||||
canonicalLocation?.search ?? route.search
|
||||
}`;
|
||||
},
|
||||
loader: (context: ApplicationContext, { location }) => loadWorkboardRoute(context, location),
|
||||
component: () =>
|
||||
import("./workboard-page.ts").then(() => ({
|
||||
|
||||
Reference in New Issue
Block a user