Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions components/MermaidBlock.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ test("CodeBlock highlights code when not streaming", () => {
assert.match(html, /const/);
});

test("CodeBlock owns the pre background without a competing shorthand", () => {
for (const isStreaming of [false, true]) {
const html = renderCode({ code: "const x = 1;", lang: "javascript", isStreaming });
const pre = html.match(/<pre\b[^>]*>/)?.[0];
assert.ok(pre);
assert.match(pre, /background-color:color-mix/);
assert.doesNotMatch(pre, /(?:[;\"]|^)background:/);
}
});

test("CodeBlock renders plain text without tokenization while streaming", () => {
const html = renderCode({ code: "const x = 1;", lang: "javascript", isStreaming: true });

Expand Down
17 changes: 14 additions & 3 deletions components/MermaidBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,17 @@ interface CodeBlockProps {
isStreaming?: boolean;
}

// Prism's light theme uses backgroundColor, while its dark theme uses the
// background shorthand. Keep the pre background solely in customStyle so a
// theme switch cannot remove/update one and reset the other in React's diff.
const codeBlockDarkTheme = {
...vscDarkPlus,
'pre[class*="language-"]': {
...vscDarkPlus['pre[class*="language-"]'],
},
};
delete codeBlockDarkTheme['pre[class*="language-"]'].background;

/**
* Syntax-highlighted code block with copy button.
* Used as the "source" view for mermaid blocks and for all non-mermaid code fences.
Expand Down Expand Up @@ -300,15 +311,15 @@ export const CodeBlock = memo(function CodeBlock({ code, lang, headerAction, isS
fontSize: "calc(12.5px + var(--chat-font-size-offset, 0px))",
lineHeight: 1.62,
overflowX: "auto",
background: "color-mix(in srgb, var(--bg) 92%, var(--bg-panel))",
backgroundColor: "color-mix(in srgb, var(--bg) 92%, var(--bg-panel))",
}}
>
<code style={{ fontFamily: "var(--font-mono)" }}>{code}</code>
</pre>
) : (
<SyntaxHighlighter
language={lang || "text"}
style={isDark ? vscDarkPlus : vs}
style={isDark ? codeBlockDarkTheme : vs}
showLineNumbers
lineNumberStyle={{ color: "var(--text-dim)", fontStyle: "normal" }}
customStyle={{
Expand All @@ -320,7 +331,7 @@ export const CodeBlock = memo(function CodeBlock({ code, lang, headerAction, isS
// block's wrapper already draws the frame.
border: "none",
borderRadius: 0,
background: "color-mix(in srgb, var(--bg) 92%, var(--bg-panel))",
backgroundColor: "color-mix(in srgb, var(--bg) 92%, var(--bg-panel))",
}}
codeTagProps={{ style: { fontFamily: "var(--font-mono)" } }}
>
Expand Down
17 changes: 14 additions & 3 deletions demo/components/MermaidBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,17 @@ interface CodeBlockProps {
isStreaming?: boolean;
}

// Prism's light theme uses backgroundColor, while its dark theme uses the
// background shorthand. Keep the pre background solely in customStyle so a
// theme switch cannot remove/update one and reset the other in React's diff.
const codeBlockDarkTheme = {
...vscDarkPlus,
'pre[class*="language-"]': {
...vscDarkPlus['pre[class*="language-"]'],
},
};
delete codeBlockDarkTheme['pre[class*="language-"]'].background;

/**
* Syntax-highlighted code block with copy button.
* Used as the "source" view for mermaid blocks and for all non-mermaid code fences.
Expand Down Expand Up @@ -300,15 +311,15 @@ export const CodeBlock = memo(function CodeBlock({ code, lang, headerAction, isS
fontSize: "calc(12.5px + var(--chat-font-size-offset, 0px))",
lineHeight: 1.62,
overflowX: "auto",
background: "color-mix(in srgb, var(--bg) 92%, var(--bg-panel))",
backgroundColor: "color-mix(in srgb, var(--bg) 92%, var(--bg-panel))",
}}
>
<code style={{ fontFamily: "var(--font-mono)" }}>{code}</code>
</pre>
) : (
<SyntaxHighlighter
language={lang || "text"}
style={isDark ? vscDarkPlus : vs}
style={isDark ? codeBlockDarkTheme : vs}
showLineNumbers
lineNumberStyle={{ color: "var(--text-dim)", fontStyle: "normal" }}
customStyle={{
Expand All @@ -317,7 +328,7 @@ export const CodeBlock = memo(function CodeBlock({ code, lang, headerAction, isS
fontSize: "calc(12.5px + var(--chat-font-size-offset, 0px))",
lineHeight: 1.62,
borderRadius: 0,
background: "color-mix(in srgb, var(--bg) 92%, var(--bg-panel))",
backgroundColor: "color-mix(in srgb, var(--bg) 92%, var(--bg-panel))",
}}
codeTagProps={{ style: { fontFamily: "var(--font-mono)" } }}
>
Expand Down
40 changes: 40 additions & 0 deletions e2e/code-background.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import assert from "node:assert/strict";

/** The fixture has one ordinary fence and one Mermaid diagram. */
export async function checkCodeBackground(page) {
const warnings = [];
const onConsole = (message) => {
if (/conflicting property|style property during rerender/.test(message.text())) {
warnings.push(message.text());
}
};
page.on("console", onConsole);
const source = page.getByRole("button", { name: "Source", exact: true });
await source.click();
const blocks = page.locator(".markdown-code-block pre");
await blocks.nth(1).waitFor();
try {
for (const colorScheme of ["light", "dark", "light", "dark", "light"]) {
await page.emulateMedia({ colorScheme });
await page.waitForFunction((theme) => document.documentElement.dataset.theme === theme, colorScheme);
await page.waitForFunction(() => {
const probe = document.createElement("div");
probe.style.backgroundColor = "color-mix(in srgb, var(--bg) 92%, var(--bg-panel))";
document.body.append(probe);
const expected = getComputedStyle(probe).backgroundColor;
probe.remove();
const nodes = [...document.querySelectorAll(".markdown-code-block pre")];
return nodes.length === 2 && nodes.every((node) => (
getComputedStyle(node).backgroundColor === expected
&& getComputedStyle(node).borderTopWidth === "0px"
&& getComputedStyle(node.querySelector("code")).backgroundColor === "rgba(0, 0, 0, 0)"
));
});
}
assert.deepEqual(warnings, [], "Theme switches must not mix background shorthand and backgroundColor");
} finally {
page.off("console", onConsole);
await page.emulateMedia({ colorScheme: "light" });
}
console.log("PASS: code and Mermaid source backgrounds survive repeated theme switches");
}
13 changes: 12 additions & 1 deletion e2e/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { chromium } from "playwright";
import { checkFilePanel, filePanelFixture } from "./file-panel.mjs";
import { checkExtensionDialogSizing, checkExtensionDialogs, extensionSource } from "./extension-dialog.mjs";
import { checkChatAppearance } from "./chat-appearance.mjs";
import { checkCodeBackground } from "./code-background.mjs";
import { checkModelDiscovery } from "./model-discovery.mjs";

const root = dirname(dirname(fileURLToPath(import.meta.url)));
Expand All @@ -33,6 +34,7 @@ const BRANCH = "e2e-branch-session";
const RICH = "e2e-rich-session";
const COMPACTED = "e2e-compacted-session";
const APPEND = "e2e-external-append-session";
const CODE_BACKGROUND = "e2e-code-background";
const text = (i) => `E2E message ${String(i).padStart(4, "0")}`;
const ids = (start, end) => Array.from({ length: end - start }, (_, i) => `e${start + i}`);

Expand Down Expand Up @@ -108,6 +110,12 @@ try {
];
Object.assign(richEntries.at(-1).message, { provider: "test", model: "E2E Model" });
writeSession(RICH, richEntries);
writeSession(CODE_BACKGROUND, [
message("user", null, "user", "E2E code background"),
message("answer", "user", "assistant", [{ type: "text", text:
"```js\nconst x = 1;\n```\n\n```mermaid\ngraph TD\n A --> B\n```",
}]),
]);
// The default page is 50 *visible* messages (user / assistant / compaction).
// toolResults ride along free after #810, so 48 tool-call assistants + the
// final answer + the divider fill that window; the user prompt is the 51st
Expand Down Expand Up @@ -175,7 +183,7 @@ try {
const response = await fetch(`${base}/api/sessions`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
if (response?.ok) {
const { sessions } = await response.json();
assert.deepEqual(sessions.map((session) => session.id).sort(), [LONG, BRANCH, RICH, COMPACTED, APPEND].sort());
assert.deepEqual(sessions.map((session) => session.id).sort(), [LONG, BRANCH, RICH, COMPACTED, APPEND, CODE_BACKGROUND].sort());
break;
}
assert.ok(Date.now() < deadline, "Server readiness timed out; see server.log");
Expand Down Expand Up @@ -442,6 +450,9 @@ try {
if (await showSidebar.isVisible()) await showSidebar.click();
await checkModelDiscovery(page);
}
await page.goto(`${base}/?session=${CODE_BACKGROUND}`, { waitUntil: "domcontentloaded" });
await page.locator(".markdown-code-block pre").waitFor();
await checkCodeBackground(page);
assert.deepEqual(errors, [], `Browser errors at width ${viewport.width}`);
console.log(`PASS: ${viewport.width}px browser pagination, branch, markdown, code, tool call, and compaction navigation`);
await context.tracing.stop();
Expand Down