Skip to content

Commit d265b68

Browse files
fix(clownfish): address review for repair-93869-qr-ci (1)
1 parent f34257b commit d265b68

5 files changed

Lines changed: 67 additions & 9 deletions

File tree

ui/src/ui/chat/code-block-copy-payload.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
const blockArtCopyPayloadPrefix = "openclaw:block-art-code:";
2+
export const blockArtCodeBlockCopyPayloadEncoding = "block-art-json";
23

34
export function encodeBlockArtCodeBlockCopyPayload(value: string): string {
45
return `${blockArtCopyPayloadPrefix}${JSON.stringify(value)}`;
56
}
67

7-
export function decodeCodeBlockCopyPayload(value: string): string {
8-
if (!value.startsWith(blockArtCopyPayloadPrefix)) {
8+
export function decodeCodeBlockCopyPayload(value: string, encoding?: string): string {
9+
if (
10+
encoding !== blockArtCodeBlockCopyPayloadEncoding ||
11+
!value.startsWith(blockArtCopyPayloadPrefix)
12+
) {
913
return value;
1014
}
1115
try {

ui/src/ui/markdown.test.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
import { render } from "lit";
33
import { describe, expect, it, vi } from "vitest";
44
import { i18n } from "../i18n/index.ts";
5-
import { decodeCodeBlockCopyPayload } from "./chat/code-block-copy-payload.ts";
5+
import {
6+
blockArtCodeBlockCopyPayloadEncoding,
7+
decodeCodeBlockCopyPayload,
8+
} from "./chat/code-block-copy-payload.ts";
69
import {
710
md,
811
toSanitizedMarkdownHtml,
@@ -379,6 +382,7 @@ describe("toSanitizedMarkdownHtml", () => {
379382

380383
expect(fragment.querySelector(".code-block-lang")?.textContent).toBe("ts");
381384
expect(decodeCodeBlockCopyPayload(copy?.dataset.code ?? "")).toBe("console.log(1)");
385+
expect(copy?.dataset.codeEncoding).toBeUndefined();
382386
expect(code?.classList.contains("language-ts")).toBe(true);
383387
expect(code?.textContent).toBe("console.log(1)\n");
384388
});
@@ -458,11 +462,25 @@ PY
458462
expect(fragment.querySelector(".code-block-lang")?.textContent).toBe("js");
459463
expect(copy?.dataset.code).toBe(source.trimEnd());
460464
expect(decodeCodeBlockCopyPayload(copy?.dataset.code ?? "")).toBe(source.trimEnd());
465+
expect(copy?.dataset.codeEncoding).toBeUndefined();
461466
expect(code?.textContent).toBe(source);
462467
expect(code?.querySelector(".hljs-keyword")?.textContent).toBe("const");
463468
expect(code?.querySelector(".hljs-string")?.textContent).toBe('"yes"');
464469
});
465470

471+
it("keeps ordinary code blocks raw when they start with the block-art prefix", () => {
472+
const source = 'openclaw:block-art-code:"literal"\n';
473+
const html = toSanitizedMarkdownHtml(`\`\`\`txt\n${source}\`\`\``);
474+
const fragment = htmlFragment(html);
475+
const copy = fragment.querySelector<HTMLButtonElement>(".code-block-copy");
476+
477+
expect(copy?.dataset.code).toBe(source.trimEnd());
478+
expect(copy?.dataset.codeEncoding).toBeUndefined();
479+
expect(decodeCodeBlockCopyPayload(copy?.dataset.code ?? "", copy?.dataset.codeEncoding)).toBe(
480+
source.trimEnd(),
481+
);
482+
});
483+
466484
it("keeps boundary spaces in encoded copy payloads after sanitization", () => {
467485
const source = " ▀▀▀▀ \n ▄▄▄▄ ";
468486
const html = toSanitizedMarkdownHtml(`\`\`\`\n${source}\n\`\`\``);
@@ -471,7 +489,10 @@ PY
471489

472490
expect(copy?.dataset.code).not.toMatch(/^\s|\s$/);
473491
expect(copy?.dataset.code).toContain("openclaw:block-art-code:");
474-
expect(decodeCodeBlockCopyPayload(copy?.dataset.code ?? "")).toBe(source);
492+
expect(copy?.dataset.codeEncoding).toBe(blockArtCodeBlockCopyPayloadEncoding);
493+
expect(decodeCodeBlockCopyPayload(copy?.dataset.code ?? "", copy?.dataset.codeEncoding)).toBe(
494+
source,
495+
);
475496
expect(fragment.querySelector("pre code")?.textContent).toBe(`${source}\n`);
476497
});
477498

@@ -811,7 +832,10 @@ describe("toStreamingMarkdownHtml", () => {
811832
expect(code?.textContent).toContain(`showing first 140000`);
812833
expect(code?.textContent?.length).toBeLessThan(blockArt.length);
813834
expect(copy?.dataset.code).toContain("openclaw:block-art-code:");
814-
expect(decodeCodeBlockCopyPayload(copy?.dataset.code ?? "")).toBe(code?.textContent);
835+
expect(copy?.dataset.codeEncoding).toBe(blockArtCodeBlockCopyPayloadEncoding);
836+
expect(decodeCodeBlockCopyPayload(copy?.dataset.code ?? "", copy?.dataset.codeEncoding)).toBe(
837+
code?.textContent,
838+
);
815839
});
816840

817841
it("renders completed block prefixes as markdown and keeps the open tail plain", () => {

ui/src/ui/markdown.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import MarkdownIt from "markdown-it";
1919
import markdownItTaskLists from "markdown-it-task-lists";
2020
import { stripUnsupportedCitationControlMarkers } from "../../../src/shared/text/citation-control-markers.js";
2121
import { i18n, t } from "../i18n/index.ts";
22-
import { encodeBlockArtCodeBlockCopyPayload } from "./chat/code-block-copy-payload.ts";
22+
import {
23+
blockArtCodeBlockCopyPayloadEncoding,
24+
encodeBlockArtCodeBlockCopyPayload,
25+
} from "./chat/code-block-copy-payload.ts";
2326
import { truncateText } from "./format.ts";
2427
import { inferBasePathFromPathname, normalizeBasePath, tabFromPath } from "./navigation.ts";
2528
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
@@ -72,6 +75,7 @@ const allowedAttrs = [
7275
"src",
7376
"alt",
7477
"data-code",
78+
"data-code-encoding",
7579
"type",
7680
"aria-label",
7781
];
@@ -751,7 +755,10 @@ function renderCodeBlock(
751755
const copyText = options.copyText ?? text;
752756
const copyPayload = blockArt ? encodeBlockArtCodeBlockCopyPayload(copyText) : copyText;
753757
const attrSafe = escapeHtml(copyPayload);
754-
const copyBtn = `<button type="button" class="code-block-copy" data-code="${attrSafe}" aria-label="${escapeHtml(t("common.copyCode"))}"><span class="code-block-copy__idle">${escapeHtml(t("common.copy"))}</span><span class="code-block-copy__done">${escapeHtml(t("common.copied"))}</span></button>`;
758+
const encodingAttr = blockArt
759+
? ` data-code-encoding="${blockArtCodeBlockCopyPayloadEncoding}"`
760+
: "";
761+
const copyBtn = `<button type="button" class="code-block-copy" data-code="${attrSafe}"${encodingAttr} aria-label="${escapeHtml(t("common.copyCode"))}"><span class="code-block-copy__idle">${escapeHtml(t("common.copy"))}</span><span class="code-block-copy__done">${escapeHtml(t("common.copied"))}</span></button>`;
755762
const header = `<div class="code-block-header">${langLabel}${copyBtn}</div>`;
756763

757764
const trimmed = text.trim();

ui/src/ui/views/chat.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import {
1717
import { renderChatQueue } from "../chat/chat-queue.ts";
1818
import { buildRawSidebarContent } from "../chat/chat-sidebar-raw.ts";
1919
import { renderWelcomeState } from "../chat/chat-welcome.ts";
20-
import { encodeBlockArtCodeBlockCopyPayload } from "../chat/code-block-copy-payload.ts";
20+
import {
21+
blockArtCodeBlockCopyPayloadEncoding,
22+
encodeBlockArtCodeBlockCopyPayload,
23+
} from "../chat/code-block-copy-payload.ts";
2124
import { renderChatSessionSelect } from "../chat/session-controls.ts";
2225
import type { GatewayBrowserClient } from "../gateway.ts";
2326
import type { GatewaySessionRow, ModelCatalogEntry, SessionsListResult } from "../types.ts";
@@ -625,6 +628,7 @@ describe("chat code-block copy", () => {
625628
button.type = "button";
626629
button.className = "code-block-copy";
627630
button.dataset.code = encodeBlockArtCodeBlockCopyPayload(payload);
631+
button.dataset.codeEncoding = blockArtCodeBlockCopyPayloadEncoding;
628632
thread.appendChild(button);
629633

630634
button.click();
@@ -649,6 +653,24 @@ describe("chat code-block copy", () => {
649653

650654
expect(writeText).toHaveBeenCalledWith("legacy text");
651655
});
656+
657+
it("does not decode unmarked raw data-code payloads that start with the block-art prefix", async () => {
658+
const writeText = vi.fn().mockResolvedValue(undefined);
659+
vi.stubGlobal("navigator", { clipboard: { writeText } });
660+
const container = renderChatView();
661+
const thread = requireElement(container, ".chat-thread", "chat thread");
662+
const payload = 'openclaw:block-art-code:"literal"';
663+
const button = document.createElement("button");
664+
button.type = "button";
665+
button.className = "code-block-copy";
666+
button.dataset.code = payload;
667+
thread.appendChild(button);
668+
669+
button.click();
670+
await Promise.resolve();
671+
672+
expect(writeText).toHaveBeenCalledWith(payload);
673+
});
652674
});
653675

654676
describe("chat history render window", () => {

ui/src/ui/views/chat.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2031,7 +2031,8 @@ export function renderChat(props: ChatProps) {
20312031
if (!btn) {
20322032
return;
20332033
}
2034-
const code = decodeCodeBlockCopyPayload((btn as HTMLElement).dataset.code ?? "");
2034+
const button = btn as HTMLElement;
2035+
const code = decodeCodeBlockCopyPayload(button.dataset.code ?? "", button.dataset.codeEncoding);
20352036
void copyToClipboard(code).then((copied) => {
20362037
if (!copied) {
20372038
return;

0 commit comments

Comments
 (0)