mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(ui): preserve chat URL state during session draft cleanup (#129949)
* fix(ui): preserve chat URL state during session draft cleanup * refactor(ui): consolidate chat session route identity checks
This commit is contained in:
committed by
GitHub
parent
f991a984e2
commit
0482cbf1c0
@@ -31,6 +31,27 @@ async function withChatPage(run: (page: Page) => Promise<void>): Promise<void> {
|
||||
}
|
||||
|
||||
suite.define(() => {
|
||||
it.each([
|
||||
{ draft: "Draft from a shared link", name: "a one-shot chat draft" },
|
||||
{ draft: undefined, name: "an existing session URL" },
|
||||
])("preserves remaining URL state while resolving $name", async ({ draft }) => {
|
||||
await withChatPage(async (page) => {
|
||||
await installMockGateway(page, { historyMessages: [] });
|
||||
const sessionUrl = new URL(controlUiSessionUrl(suite.server.baseUrl, "main"));
|
||||
if (draft !== undefined) {
|
||||
sessionUrl.searchParams.set("draft", draft);
|
||||
}
|
||||
sessionUrl.searchParams.set("panel", "details");
|
||||
sessionUrl.hash = "#pane";
|
||||
|
||||
await page.goto(sessionUrl.href);
|
||||
const composer = page.locator(".agent-chat__composer-combobox textarea");
|
||||
await expect.poll(() => composer.inputValue()).toBe(draft ?? "");
|
||||
await expect.poll(() => new URL(page.url()).search).toBe("?panel=details");
|
||||
await expect.poll(() => new URL(page.url()).hash).toBe("#pane");
|
||||
});
|
||||
});
|
||||
|
||||
it("sends a chat turn through the GUI and renders the final Gateway event", async () => {
|
||||
await withChatPage(async (page) => {
|
||||
const gateway = await installMockGateway(page, {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RouteLocation } from "@openclaw/uirouter";
|
||||
import { locationWithoutDraft } from "./route-draft.ts";
|
||||
|
||||
function currentRouteLocation(): RouteLocation {
|
||||
export function currentRouteLocation(): RouteLocation {
|
||||
return {
|
||||
pathname: window.location.pathname,
|
||||
search: window.location.search,
|
||||
|
||||
@@ -413,6 +413,7 @@ describe("chat page split layout host", () => {
|
||||
});
|
||||
|
||||
it("hands each route-provided draft to the active pane only once", async () => {
|
||||
window.history.replaceState({}, "", "/chat/main?draft=one-shot%20draft&panel=details#pane");
|
||||
const page = new ChatPage();
|
||||
const navigation = setNavigationContext(page);
|
||||
const firstRouteData = { sessionKey: "main", draft: "one-shot draft" };
|
||||
@@ -420,14 +421,13 @@ describe("chat page split layout host", () => {
|
||||
expect(getRouteDraftForActivePane(page)).toBe("one-shot draft");
|
||||
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
await Promise.resolve();
|
||||
await page.updateComplete;
|
||||
await vi.waitFor(() => expect(navigation.replace).toHaveBeenCalledOnce());
|
||||
|
||||
expect(getRouteDraftForActivePane(page)).toBeUndefined();
|
||||
expect(navigation.replace).toHaveBeenCalledOnce();
|
||||
expect(navigation.replace).toHaveBeenCalledWith("chat", {
|
||||
pathname: sessionPath("main"),
|
||||
search: "?panel=details",
|
||||
hash: "#pane",
|
||||
});
|
||||
page.data = { ...firstRouteData };
|
||||
expect(getRouteDraftForActivePane(page)).toBe("one-shot draft");
|
||||
@@ -554,6 +554,8 @@ describe("chat page split layout host", () => {
|
||||
});
|
||||
|
||||
it("keeps catalog identity when consuming a route draft", async () => {
|
||||
const expectedSearch = catalogSessionSearch(CATALOG_KEY);
|
||||
window.history.replaceState({}, "", `/chat/research${expectedSearch}&draft=ship`);
|
||||
const page = new ChatPage();
|
||||
const navigation = setNavigationContext(page);
|
||||
page.data = {
|
||||
@@ -562,14 +564,12 @@ describe("chat page split layout host", () => {
|
||||
draft: "one-shot catalog draft",
|
||||
};
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
await Promise.resolve();
|
||||
await page.updateComplete;
|
||||
await vi.waitFor(() => expect(navigation.replace).toHaveBeenCalledOnce());
|
||||
|
||||
const expectedSearch = catalogSessionSearch(CATALOG_KEY);
|
||||
expect(navigation.replace).toHaveBeenCalledWith("chat", {
|
||||
pathname: "/chat/research",
|
||||
search: expectedSearch,
|
||||
hash: "",
|
||||
});
|
||||
await expect(
|
||||
loadChatRoute(
|
||||
@@ -606,6 +606,7 @@ describe("chat page split layout host", () => {
|
||||
});
|
||||
|
||||
it("preserves a resolved long prefix through drafts and face changes", async () => {
|
||||
window.history.replaceState({}, "", "/chat/main/1234567890?draft=ship");
|
||||
const page = new ChatPage();
|
||||
const navigation = setNavigationContext(page);
|
||||
page.data = {
|
||||
@@ -615,12 +616,12 @@ describe("chat page split layout host", () => {
|
||||
face: "chat",
|
||||
};
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
await Promise.resolve();
|
||||
await page.updateComplete;
|
||||
await vi.waitFor(() => expect(navigation.replace).toHaveBeenCalledOnce());
|
||||
|
||||
expect(navigation.replace).toHaveBeenCalledWith("chat", {
|
||||
pathname: "/chat/main/1234567890",
|
||||
search: "",
|
||||
hash: "",
|
||||
});
|
||||
navigation.navigate.mockClear();
|
||||
const pane = page.querySelector<RenderedPane>("openclaw-chat-pane");
|
||||
|
||||
@@ -17,7 +17,7 @@ import { areUiSessionKeysEquivalent } from "../../lib/sessions/session-key.ts";
|
||||
import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts";
|
||||
import { SubscriptionsController } from "../../lit/subscriptions-controller.ts";
|
||||
import { persistSessionBoardFace } from "./chat-board-face-persistence.ts";
|
||||
import { stillOwnsCanonicalLocation } from "./chat-canonical-location.ts";
|
||||
import { currentRouteLocation, stillOwnsCanonicalLocation } from "./chat-canonical-location.ts";
|
||||
import { renderChatPagePaneCell } from "./chat-page-pane-render.ts";
|
||||
import { ChatPageRetainedSessions } from "./chat-page-retained-sessions.ts";
|
||||
import { closeStagedPane, resumeStagedPanes } from "./chat-pane-attachment-handoff.ts";
|
||||
@@ -403,12 +403,8 @@ export class ChatPage extends OpenClawLightDomElement {
|
||||
|
||||
private updateRoute(sessionKey: string, replace = false, face = this.data.face ?? "chat") {
|
||||
const data = this.data;
|
||||
if (
|
||||
data?.sessionKey === sessionKey &&
|
||||
(data.face ?? "chat") === face &&
|
||||
!data.draft &&
|
||||
!data.focusComposer
|
||||
) {
|
||||
const sameSession = data && areUiSessionKeysEquivalent(data.sessionKey, sessionKey);
|
||||
if (sameSession && (data.face ?? "chat") === face && !data.draft && !data.focusComposer) {
|
||||
return;
|
||||
}
|
||||
const options = sessionNavigationTarget({
|
||||
@@ -419,7 +415,11 @@ export class ChatPage extends OpenClawLightDomElement {
|
||||
shortIdLength: data?.sessionKey === sessionKey ? data.shortId?.length : undefined,
|
||||
}).options;
|
||||
if (replace) {
|
||||
this.context.replace(face, options);
|
||||
const location =
|
||||
sameSession && (data.draft || data.focusComposer)
|
||||
? locationWithoutDraft(currentRouteLocation(), options)
|
||||
: options;
|
||||
this.context.replace(face, location);
|
||||
} else {
|
||||
this.context.navigate(face, options);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,18 @@ function focusComposerFromLocation(location: RouteLocation): boolean {
|
||||
return new URLSearchParams(location.search).get(SESSION_COMPOSER_FOCUS_PARAM) === "1";
|
||||
}
|
||||
|
||||
export function locationWithoutDraft(location: RouteLocation): RouteLocation {
|
||||
export function locationWithoutDraft(
|
||||
location: RouteLocation,
|
||||
destination: Partial<RouteLocation> = {},
|
||||
): RouteLocation {
|
||||
const params = new URLSearchParams(location.search);
|
||||
for (const [name, value] of new URLSearchParams(destination.search)) {
|
||||
params.set(name, value);
|
||||
}
|
||||
params.delete("draft");
|
||||
params.delete(SESSION_COMPOSER_FOCUS_PARAM);
|
||||
const search = params.toString();
|
||||
return { ...location, search: search ? `?${search}` : "" };
|
||||
return { ...location, ...destination, search: search ? `?${search}` : "" };
|
||||
}
|
||||
|
||||
export function draftRouteDataFromLocation(location: RouteLocation): RouteDraftHint {
|
||||
|
||||
Reference in New Issue
Block a user