perf(ui): skip highlighting unfinished code blocks while streaming (#127754)

* perf(ui): defer open-fence highlighting

* perf(ui): simplify unfinished fence highlighting

Co-authored-by: vyctorbrzezowski <krzyszchweski@gmail.com>

* docs: avoid concurrent changelog insertion conflict

* docs: respect release-owned changelog policy

Release-note context and before-after UI proof remain in the pull request.

Co-authored-by: vyctorbrzezowski <krzyszchweski@gmail.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Vyctor H. Brzezowski
2026-08-22 03:41:00 -03:00
committed by GitHub
parent 2fdfd64a1a
commit e22dc4b287
6 changed files with 111 additions and 10 deletions
+5 -4
View File
@@ -268,12 +268,13 @@ function codeClassAttribute(lang: string, highlighted: string): string {
function renderCodeElement(
text: string,
lang: string,
options: { blockArt?: boolean } = {},
options: { blockArt?: boolean; highlight?: boolean } = {},
): string {
if (options.blockArt || isMarkdownBlockArtText(text)) {
return `<pre><code class="markdown-block-art">${escapeMarkdownHtml(text)}</code></pre>`;
}
const highlighted = highlightCodeHtml(text, lang);
const highlighted =
options.highlight === false ? escapeMarkdownHtml(text) : highlightCodeHtml(text, lang);
const classAttr = codeClassAttribute(lang, highlighted);
return `<pre><code${classAttr}>${highlighted}</code></pre>`;
}
@@ -301,10 +302,10 @@ export function renderMarkdownCodeBlock(
text: string,
lang: string,
env: unknown,
options: { blockArt?: boolean; copyText?: string } = {},
options: { blockArt?: boolean; copyText?: string; highlight?: boolean } = {},
): string {
const blockArt = options.blockArt || isMarkdownBlockArtText(text);
const codeBlock = renderCodeElement(text, lang, { blockArt });
const codeBlock = renderCodeElement(text, lang, { blockArt, highlight: options.highlight });
if (!shouldRenderCodeBlockCopy(env) && !shouldRenderCodeBlockInteraction(env)) {
return codeBlock;
}
+10 -1
View File
@@ -656,7 +656,12 @@ export function createMarkdownParser(): MarkdownIt {
});
// Fenced and indented blocks share one interaction and overflow surface.
markdownParser.renderer.rules.fence = (tokens, index, _options, env) => {
markdownParser.renderer.rules.fence = (
tokens,
index,
_options,
env: Partial<MarkdownRenderEnv> | undefined,
) => {
const token = tokens[index];
if (!token) {
return "";
@@ -664,8 +669,12 @@ export function createMarkdownParser(): MarkdownIt {
// token.info contains the full fence info string (e.g., "json title=foo");
// extract only the first whitespace-separated token as the language.
const language = token.info.trim().split(/\s+/)[0] || "";
// An unfinished fence consumes the remaining input; only container closers can
// follow it. Invalid fence-looking prose must not de-highlight an earlier block.
return renderMarkdownCodeBlock(token.content, language, env, {
copyText: markdownCodeBlockCopyText(token.content),
highlight:
!env?.streamingOpenFence || tokens.findLastIndex(({ nesting }) => nesting !== -1) !== index,
});
};
// Override indented code blocks (code_block) with the same treatment as fence
+3 -1
View File
@@ -16,7 +16,9 @@ export type MarkdownRenderOptions = {
tableInteractions?: MarkdownTableInteractions;
};
export type MarkdownRenderEnv = Required<MarkdownRenderOptions>;
export type MarkdownRenderEnv = Required<MarkdownRenderOptions> & {
streamingOpenFence?: boolean;
};
export function normalizeMarkdownRenderOptions(
options: MarkdownRenderOptions = {},
+33 -3
View File
@@ -1100,15 +1100,41 @@ describe("toStreamingMarkdownHtml", () => {
expect(html).toBe("<p>prices are $$50 and</p>\n");
});
it("streams an open code fence as a live-highlighted code block", () => {
it("streams an open code fence without syntax highlighting", () => {
const html = toStreamingMarkdownHtml("Intro\n\n```ts\nconst x = 1 < 2");
const fragment = htmlFragment(html);
const code = fragment.querySelector("code.language-ts");
expect(fragment.querySelector("p")?.textContent).toBe("Intro");
expect(fragment.querySelector("code.language-ts")?.textContent).toContain("const x = 1 < 2");
expect(code?.textContent).toContain("const x = 1 < 2");
expect(code?.classList.contains("hljs")).toBe(false);
expect(code?.querySelector("span")).toBeNull();
expect(html).not.toContain("markdown-plain-text-fallback");
});
it("highlights only completed fences inside an open details block", () => {
const html = toStreamingMarkdownHtml(
"<details>\n<summary>Logs</summary>\n\n```ts\nconst closed = 1;\n```\n\n```ts\nconst open = 2;",
);
const code = htmlFragment(html).querySelectorAll("details code.language-ts");
expect(code).toHaveLength(2);
expect(code[0]?.classList.contains("hljs")).toBe(true);
expect(code[0]?.querySelector("span")).not.toBeNull();
expect(code[1]?.classList.contains("hljs")).toBe(false);
expect(code[1]?.querySelector("span")).toBeNull();
});
it("keeps a completed fence highlighted when a later backtick fence has invalid info", () => {
const html = toStreamingMarkdownHtml(
"- ```ts\n const closed = 1;\n ```\n\n ```bad`info\n trailing text",
);
const code = htmlFragment(html).querySelector("code.language-ts");
expect(code?.textContent).toContain("const closed = 1;");
expect(code?.classList.contains("hljs")).toBe(true);
});
it("streams an open list code fence through blank lines", () => {
const html = toStreamingMarkdownHtml("- ```ts\n const x = 1;\n\n const y = 2;");
const fragment = htmlFragment(html);
@@ -1116,6 +1142,7 @@ describe("toStreamingMarkdownHtml", () => {
expect(code?.textContent).toContain("const x = 1;");
expect(code?.textContent).toContain("const y = 2;");
expect(code?.classList.contains("hljs")).toBe(false);
expect(html).not.toContain("markdown-plain-text-fallback");
});
@@ -1137,14 +1164,17 @@ describe("toStreamingMarkdownHtml", () => {
expect(code?.textContent).toContain("const x = 1;");
expect(code?.textContent).toContain("const y = 2;");
expect(code?.classList.contains("hljs")).toBe(false);
expect(html).not.toContain("markdown-plain-text-fallback");
});
it("renders a completed code fence once the closing fence arrives", () => {
const html = toStreamingMarkdownHtml("```ts\nconst x = 1;\n```");
const markdown = "```ts\nconst x = 1;\n```";
const html = toStreamingMarkdownHtml(markdown);
expect(html).toContain('<code class="hljs language-ts"');
expect(html).toContain("const x = 1;");
expect(html).not.toContain("markdown-plain-text-fallback");
expect(html).toBe(toSanitizedMarkdownHtml(markdown));
});
});
+1 -1
View File
@@ -607,7 +607,7 @@ export function toStreamingMarkdownHtml(
}
const tailHtml =
tailRepairStart === null
? renderSanitizedMarkdown(streamingTail, renderOptions)
? renderSanitizedMarkdown(streamingTail, { ...renderOptions, streamingOpenFence: true })
: renderSanitizedMarkdown(
repairStreamingMarkdownTail(streamingTail, tailRepairStart - boundary),
renderOptions,
@@ -9,6 +9,7 @@ import {
startControlUiE2eServer,
type ControlUiE2eServer,
} from "../test-helpers/control-ui-e2e.ts";
import { requireRecord, requireString } from "./chat-flow.test-support.ts";
const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
@@ -73,6 +74,64 @@ describeControlUiE2e("Control UI fenced code blocks", () => {
await server?.close();
});
it("highlights a streamed code fence only after its closing marker arrives", async () => {
const context = await browser.newContext({
locale: "en-US",
serviceWorkers: "block",
viewport: { height: 900, width: 1440 },
});
const page = await context.newPage();
const gateway = await installMockGateway(page);
try {
await page.goto(`${server.baseUrl}chat`);
await page.locator(".agent-chat__composer-combobox textarea").fill("show TypeScript");
await page.getByRole("button", { name: "Send message" }).click();
const sendRequest = await gateway.waitForRequest("chat.send");
const runId = requireString(
requireRecord(sendRequest.params).idempotencyKey,
"chat send idempotency key",
);
const openFence = "```ts\nconst value = 1 < 2;";
const emitDelta = async (text: string, deltaText: string) => {
await gateway.emitGatewayEvent("chat", {
deltaText,
message: {
content: [{ text, type: "text" }],
role: "assistant",
timestamp: Date.now(),
},
runId,
sessionKey: "main",
state: "delta",
});
};
await emitDelta(openFence, openFence);
const streamingCode = page.locator(".chat-bubble.streaming code.language-ts");
await expect.poll(() => streamingCode.textContent()).toContain("const value = 1 < 2;");
expect(await streamingCode.locator("span").count()).toBe(0);
expect(await streamingCode.evaluate((code) => code.classList.contains("hljs"))).toBe(false);
expect(await page.locator(".chat-bubble.streaming .code-block-copy").count()).toBe(1);
if (captureProof) {
await page.screenshot({ path: path.join(artifactDir, "stream-open-unhighlighted.png") });
}
const completedFence = `${openFence}\n\`\`\``;
await emitDelta(completedFence, "\n```");
await expect.poll(() => streamingCode.getAttribute("class")).toContain("hljs");
expect(await streamingCode.locator("span").count()).toBeGreaterThan(0);
if (captureProof) {
await page.screenshot({ path: path.join(artifactDir, "stream-closed-highlighted.png") });
}
await gateway.emitChatFinal({ runId, text: completedFence });
await expect.poll(() => page.locator(".chat-thread code.language-ts.hljs").count()).toBe(1);
} finally {
await context.close();
}
});
it.each(["dark", "light"] as const)(
"previews long fences, reveals them, and wraps overflowing lines in %s mode",
async (theme) => {