diff --git a/ui/src/pages/chat/components/session-diff-menus.ts b/ui/src/pages/chat/components/session-diff-menus.ts index f75963c39250..f68a5235cea9 100644 --- a/ui/src/pages/chat/components/session-diff-menus.ts +++ b/ui/src/pages/chat/components/session-diff-menus.ts @@ -55,7 +55,6 @@ export type SessionDiffMenuDraft = WithoutMenuAnchor; export type SessionDiffMenuAction = | { kind: "collapse-all" } - | { kind: "copy-path"; path: string } | { kind: "expand-all" } | { kind: "open-editor"; editor: EditorId; path: string } | { kind: "open-file"; path: string } @@ -99,10 +98,6 @@ class SessionDiffMenu extends OpenClawLightDomElement { "scope:uncommitted": { kind: "scope", value: { scope: "uncommitted" } }, }; const fileMenu = this.menu?.kind === "file" ? this.menu : null; - if (fileMenu && value === "copy-path") { - this.run({ kind: "copy-path", path: fileMenu.path }); - return; - } if (fileMenu && value === "open-file") { this.run({ kind: "open-file", path: fileMenu.path }); return; @@ -142,10 +137,7 @@ class SessionDiffMenu extends OpenClawLightDomElement { private renderFileMenu(menu: Extract) { return html` - - - ${t("chat.sessionDiff.copyPath")} - + ${this.renderCopyRow(menu.path, t("chat.sessionDiff.copyPath"))} ${t("chat.sessionDiff.openFile")} diff --git a/ui/src/pages/chat/components/session-diff-panel.test.ts b/ui/src/pages/chat/components/session-diff-panel.test.ts index 22be64ace0a4..4d604ae7aad9 100644 --- a/ui/src/pages/chat/components/session-diff-panel.test.ts +++ b/ui/src/pages/chat/components/session-diff-panel.test.ts @@ -76,9 +76,51 @@ afterEach(() => { document.body.replaceChildren(); clearNativeGatewayTestState(); vi.restoreAllMocks(); + Reflect.deleteProperty(navigator, "clipboard"); }); describe("SessionDiffPanel", () => { + it.each([ + { surface: "file", failed: false, feedback: "Copied!" }, + { surface: "file", failed: true, feedback: "Copy failed" }, + { surface: "sync", failed: false, feedback: "Copied!" }, + { surface: "sync", failed: true, feedback: "Copy failed" }, + ])( + "keeps $surface path copy feedback visible: $feedback", + async ({ surface, failed, feedback }) => { + const writeText = failed + ? vi.fn().mockRejectedValue(new DOMException("Clipboard access denied", "NotAllowedError")) + : vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { writeText }, + }); + setNativeGatewayTestState(null); + const panel = document.createElement("openclaw-session-diff") as SessionDiffElement; + panel.loader = vi.fn(async () => ({ ...fileResult(SNAPSHOT_PATCH), root: "/workspace" })); + document.body.append(panel); + + const triggerSelector = + surface === "file" ? ".session-diff__file-menu" : ".session-diff__toolbar-button"; + await vi.waitFor(() => expect(panel.querySelector(triggerSelector)).not.toBeNull()); + panel.querySelector(triggerSelector)?.click(); + await panel.updateComplete; + + const menu = panel.querySelector("openclaw-session-diff-menu"); + expect(menu).not.toBeNull(); + const label = surface === "file" ? "Copy Path" : "Checkout path"; + const button = menu?.querySelector(`button[aria-label="${label}"]`); + expect(button).toBeInstanceOf(HTMLButtonElement); + + button?.click(); + await vi.waitFor(() => expect(button?.getAttribute("aria-label")).toBe(feedback)); + + expect(writeText).toHaveBeenCalledWith(surface === "file" ? "example.txt" : "/workspace"); + expect(button?.dataset[failed ? "error" : "copied"]).toBe("1"); + expect(panel.querySelector("openclaw-session-diff-menu")).toBe(menu); + }, + ); + it.each([ { name: "plain browser", nativeGateway: null, offered: false }, { name: "native local gateway", nativeGateway: "local", offered: true }, diff --git a/ui/src/pages/chat/components/session-diff-panel.ts b/ui/src/pages/chat/components/session-diff-panel.ts index 534bbd1bd784..ebf245f6d013 100644 --- a/ui/src/pages/chat/components/session-diff-panel.ts +++ b/ui/src/pages/chat/components/session-diff-panel.ts @@ -25,7 +25,6 @@ import { } from "../../../lib/chat/session-diff-split.ts"; import { parseSessionDiffPatch, type ParsedFilePatch } from "../../../lib/chat/session-diff.ts"; import type { DiffLine } from "../../../lib/chat/tool-call-diff.ts"; -import { copyToClipboard } from "../../../lib/clipboard.ts"; import { openEditor } from "../../../lib/editor-links.ts"; import { formatUiError } from "../../../lib/format-error.ts"; import { OpenClawLightDomElement } from "../../../lit/openclaw-element.ts"; @@ -263,9 +262,6 @@ class SessionDiffPanel extends OpenClawLightDomElement { case "scope": this.scope = action.value; return; - case "copy-path": - void copyToClipboard(action.path); - return; case "open-file": this.openFile?.(action.path); return;