fix(ui): show session diff path copy feedback (#129999)

This commit is contained in:
Peter Steinberger
2026-08-26 17:17:15 -07:00
committed by GitHub
parent fed350dbc4
commit 565fd93544
3 changed files with 43 additions and 13 deletions
@@ -55,7 +55,6 @@ export type SessionDiffMenuDraft = WithoutMenuAnchor<SessionDiffMenuData>;
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<SessionDiffMenuData, { kind: "file" }>) {
return html`
<wa-dropdown-item class="session-menu__item" value="copy-path">
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.copy}</span>
<span class="session-menu__text">${t("chat.sessionDiff.copyPath")}</span>
</wa-dropdown-item>
${this.renderCopyRow(menu.path, t("chat.sessionDiff.copyPath"))}
<wa-dropdown-item class="session-menu__item" value="open-file" ?disabled=${!menu.canOpenFile}>
<span slot="icon" class="session-menu__icon" aria-hidden="true">${icons.fileText}</span>
<span class="session-menu__text">${t("chat.sessionDiff.openFile")}</span>
@@ -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<HTMLButtonElement>(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<HTMLButtonElement>(`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 },
@@ -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;