mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(ui): preserve new session drafts across navigation (#123697)
This commit is contained in:
@@ -12,6 +12,7 @@ type PendingChatAttachmentHandoff = {
|
||||
scopeKey: string;
|
||||
attachments: ChatAttachment[];
|
||||
fallbacks: Record<string, ChatComposerMemoryFallback>;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export function createChatAttachmentHandoff(): ApplicationChatAttachmentHandoff {
|
||||
@@ -48,11 +49,11 @@ export function createChatAttachmentHandoff(): ApplicationChatAttachmentHandoff
|
||||
};
|
||||
|
||||
return {
|
||||
prepare: ({ owner, paneId, scopeKey, attachments, fallbacks }) => {
|
||||
prepare: ({ owner, paneId, scopeKey, attachments, fallbacks, message = "" }) => {
|
||||
const key = entryKey(paneId, scopeKey);
|
||||
const previous = take(key);
|
||||
const fallbackEntries = Object.entries(fallbacks);
|
||||
if (attachments.length === 0 && fallbackEntries.length === 0) {
|
||||
if (!message && attachments.length === 0 && fallbackEntries.length === 0) {
|
||||
releaseHandoff(previous);
|
||||
return;
|
||||
}
|
||||
@@ -75,6 +76,7 @@ export function createChatAttachmentHandoff(): ApplicationChatAttachmentHandoff
|
||||
paneId,
|
||||
scopeKey,
|
||||
attachments: [...attachments],
|
||||
message,
|
||||
fallbacks: Object.fromEntries(
|
||||
fallbackEntries.map(([fallbackKey, fallback]) => [
|
||||
fallbackKey,
|
||||
@@ -96,7 +98,11 @@ export function createChatAttachmentHandoff(): ApplicationChatAttachmentHandoff
|
||||
// A Gateway mismatch is terminal for this exact presentation. Other
|
||||
// retained session scopes under the same logical pane remain independent.
|
||||
if (match?.owner === owner) {
|
||||
return { attachments: match.attachments, fallbacks: match.fallbacks };
|
||||
return {
|
||||
attachments: match.attachments,
|
||||
fallbacks: match.fallbacks,
|
||||
...(match.message ? { message: match.message } : {}),
|
||||
};
|
||||
}
|
||||
releaseHandoff(match);
|
||||
return null;
|
||||
|
||||
@@ -84,11 +84,13 @@ export type ApplicationChatAttachmentHandoff = {
|
||||
handoff: ChatAttachmentHandoffKey & {
|
||||
attachments: readonly ChatAttachment[];
|
||||
fallbacks: Readonly<Record<string, ChatComposerMemoryFallback>>;
|
||||
message?: string;
|
||||
},
|
||||
): void;
|
||||
consume(handoff: ChatAttachmentHandoffKey): {
|
||||
attachments: ChatAttachment[];
|
||||
fallbacks: Record<string, ChatComposerMemoryFallback>;
|
||||
message?: string;
|
||||
} | null;
|
||||
clearPane(paneId: string): void;
|
||||
dispose(): void;
|
||||
|
||||
@@ -34,6 +34,44 @@ async function withNewSessionPage(run: (page: Page) => Promise<void>): Promise<v
|
||||
}
|
||||
|
||||
suite.define(() => {
|
||||
it("restores prompt text and files after navigating away and back", async () => {
|
||||
await withNewSessionPage(async (page) => {
|
||||
const sessionKey = "agent:main:existing-session";
|
||||
await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"sessions.list": createdSessionListResult(sessionKey),
|
||||
},
|
||||
});
|
||||
await page.goto(`${suite.server.baseUrl}chat`);
|
||||
const existingSession = page
|
||||
.locator(".sidebar-recent-session")
|
||||
.filter({ hasText: "Created session" });
|
||||
await existingSession.waitFor();
|
||||
await page.locator(".sidebar-brand__new-thread").click();
|
||||
await page.waitForURL((url) => url.pathname.endsWith("/new") && url.search === "?agent=main");
|
||||
|
||||
const message = page.locator(".new-session-page__message");
|
||||
await message.fill("keep this new session draft");
|
||||
await page
|
||||
.locator(".agent-chat__photo-input")
|
||||
.setInputFiles(path.join(process.cwd(), "ui/public/favicon-32.png"));
|
||||
await page.locator('.chat-attachment-thumb img[alt="Attachment preview"]').waitFor();
|
||||
await captureUiProof(page, "new-session-draft-before-navigation.png");
|
||||
|
||||
await existingSession.click();
|
||||
await page.waitForURL((url) => url.pathname === controlUiSessionPath(sessionKey));
|
||||
await page.locator(".sidebar-brand__new-thread").click();
|
||||
await page.waitForURL((url) => url.pathname.endsWith("/new") && url.search === "?agent=main");
|
||||
|
||||
await expect.poll(() => message.inputValue()).toBe("keep this new session draft");
|
||||
await expect.poll(() => page.locator(".chat-attachment-thumb").count()).toBe(1);
|
||||
await expect
|
||||
.poll(() => page.locator(".chat-attachment-thumb img").getAttribute("src"))
|
||||
.toMatch(/^(blob:|data:image\/png;base64,)/u);
|
||||
await captureUiProof(page, "new-session-draft-restored.png");
|
||||
});
|
||||
});
|
||||
|
||||
it("grows the first prompt downward without moving the identity, then caps at ten lines", async () => {
|
||||
await withNewSessionPage(async (page) => {
|
||||
const gateway = await installMockGateway(page);
|
||||
@@ -531,7 +569,7 @@ suite.define(() => {
|
||||
});
|
||||
});
|
||||
|
||||
it("releases pasted image previews after remove, reset, disconnect, and success", async () => {
|
||||
it("releases pasted image previews after remove, reset, restored removal, and success", async () => {
|
||||
await withNewSessionPage(async (page) => {
|
||||
await page.addInitScript(() => {
|
||||
const createObjectURL = URL.createObjectURL.bind(URL);
|
||||
@@ -623,10 +661,13 @@ suite.define(() => {
|
||||
await page.locator('.chat-attachment-thumb img[alt="Attachment preview"]').waitFor();
|
||||
await navigate("chat");
|
||||
await page.waitForURL((url) => url.pathname.endsWith("/chat"));
|
||||
await expect.poll(async () => (await proof()).revoked).toBe(3);
|
||||
await expect.poll(async () => (await proof()).revoked).toBe(2);
|
||||
|
||||
await navigate("new-session");
|
||||
await composer.waitFor();
|
||||
await expect.poll(() => page.locator(".chat-attachment-thumb").count()).toBe(1);
|
||||
await page.getByRole("button", { name: "Remove attachment" }).click();
|
||||
await expect.poll(async () => (await proof()).revoked).toBe(3);
|
||||
await pastePng(composer);
|
||||
await page.locator('.chat-attachment-thumb img[alt="Attachment preview"]').waitFor();
|
||||
await page.getByRole("button", { name: "Start session" }).click();
|
||||
|
||||
@@ -31,6 +31,14 @@ export class NewSessionAttachmentDraft {
|
||||
this.reads.abortReads();
|
||||
}
|
||||
|
||||
take(): ChatAttachment[] {
|
||||
this.abortReads();
|
||||
const attachments = this.attachments;
|
||||
this.attachments = [];
|
||||
this.notify();
|
||||
return attachments;
|
||||
}
|
||||
|
||||
reset(options: { release: boolean }) {
|
||||
this.abortReads();
|
||||
if (options.release) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ApplicationContext } from "../../app/context.ts";
|
||||
import * as catalog from "./catalog-target.ts";
|
||||
import type { DraftSubmissionFlow } from "./draft-submission-flow.ts";
|
||||
|
||||
const NEW_SESSION_DRAFT_PANE_ID = "new-session-draft";
|
||||
|
||||
export function retainDraft(
|
||||
context: ApplicationContext | undefined,
|
||||
submission: DraftSubmissionFlow,
|
||||
openedFor: string | null,
|
||||
messageOwnerKey: string,
|
||||
) {
|
||||
const owner = context?.gateway.snapshot.client;
|
||||
if (!context || !owner || submission.submitting || submission.pendingCloud.sessionKey) {
|
||||
return;
|
||||
}
|
||||
const routeKey = openedFor ?? catalog.routeKeyFromSearch(window.location.search);
|
||||
context.chatAttachmentHandoff.prepare({
|
||||
owner,
|
||||
paneId: NEW_SESSION_DRAFT_PANE_ID,
|
||||
scopeKey: routeKey,
|
||||
message: messageOwnerKey === routeKey ? submission.message : "",
|
||||
attachments: submission.attachmentDraft.take(),
|
||||
fallbacks: {},
|
||||
});
|
||||
}
|
||||
|
||||
export function restoreDraft(
|
||||
context: ApplicationContext | undefined,
|
||||
submission: DraftSubmissionFlow,
|
||||
routeKey: string,
|
||||
ownedMessage: string,
|
||||
) {
|
||||
const owner = context?.gateway.snapshot.client;
|
||||
const draft =
|
||||
context && owner
|
||||
? context.chatAttachmentHandoff.consume({
|
||||
owner,
|
||||
paneId: NEW_SESSION_DRAFT_PANE_ID,
|
||||
scopeKey: routeKey,
|
||||
})
|
||||
: null;
|
||||
if (ownedMessage || draft) {
|
||||
submission.setMessage(ownedMessage || draft?.message || "");
|
||||
}
|
||||
if (draft) {
|
||||
submission.attachmentDraft.replace(draft.attachments);
|
||||
}
|
||||
return routeKey;
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { renderConnectMachineDialog } from "./connect-machine-dialog.ts";
|
||||
import { isWorktreeNameValid } from "./create-params.ts";
|
||||
import { renderDetailChip, resolveDetailChip } from "./detail-chip.ts";
|
||||
import { DraftGatewayState } from "./draft-gateway-state.ts";
|
||||
import { restoreDraft, retainDraft } from "./draft-navigation-handoff.ts";
|
||||
import { DraftPlaceBrowser } from "./draft-place-browser.ts";
|
||||
import { DraftPlaceState } from "./draft-place-state.ts";
|
||||
import { DraftSubmissionFlow } from "./draft-submission-flow.ts";
|
||||
@@ -218,6 +219,7 @@ class NewSessionPage extends OpenClawLightDomElement {
|
||||
override disconnectedCallback() {
|
||||
document.removeEventListener("keydown", this, true);
|
||||
document.removeEventListener("pointerdown", this, true);
|
||||
retainDraft(this.context, this.submission, this.openedFor, this.messageOwnerKey);
|
||||
this.subscriptions.clear();
|
||||
this.gateway.invalidateDiscovery(
|
||||
true,
|
||||
@@ -259,9 +261,7 @@ class NewSessionPage extends OpenClawLightDomElement {
|
||||
this.openedAgentId = resolvedAgentId;
|
||||
this.place.setAgentsHydrated(agentsReady);
|
||||
this.resetDraft();
|
||||
if (ownedMessage) {
|
||||
this.setMessage(ownedMessage, openKey);
|
||||
}
|
||||
this.messageOwnerKey = restoreDraft(this.context, this.submission, openKey, ownedMessage);
|
||||
return;
|
||||
}
|
||||
if (this.openedAgentId !== resolvedAgentId) {
|
||||
|
||||
Reference in New Issue
Block a user