From 088023a5ce148bd22ec6bb12583a6592da7db981 Mon Sep 17 00:00:00 2001 From: Ammar Date: Fri, 12 Dec 2025 10:39:56 -0600 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20avoid=20phantom=20bla?= =?UTF-8?q?nk=20line=20in=20highlighted=20code=20blocks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/highlighting/shiki-shared.test.ts | 15 +++++++++++++++ src/browser/utils/highlighting/shiki-shared.ts | 18 ++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 src/browser/utils/highlighting/shiki-shared.test.ts diff --git a/src/browser/utils/highlighting/shiki-shared.test.ts b/src/browser/utils/highlighting/shiki-shared.test.ts new file mode 100644 index 0000000000..53de593d5b --- /dev/null +++ b/src/browser/utils/highlighting/shiki-shared.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "bun:test"; + +import { extractShikiLines } from "./shiki-shared"; + +describe("extractShikiLines", () => { + test("removes trailing visually-empty Shiki line (e.g. )", () => { + const html = `
https://github.com/coder/mux/pull/new/chat-autocomplete-b24r
+
+
`; + + expect(extractShikiLines(html)).toEqual([ + `https://github.com/coder/mux/pull/new/chat-autocomplete-b24r`, + ]); + }); +}); diff --git a/src/browser/utils/highlighting/shiki-shared.ts b/src/browser/utils/highlighting/shiki-shared.ts index 45110e64c6..805a2d48c4 100644 --- a/src/browser/utils/highlighting/shiki-shared.ts +++ b/src/browser/utils/highlighting/shiki-shared.ts @@ -24,6 +24,19 @@ export function mapToShikiLang(detectedLang: string): string { * Extract line contents from Shiki HTML output * Shiki wraps code in
...
with ... per line */ +function isVisuallyEmptyShikiLine(lineHtml: string): boolean { + // Shiki represents an empty line as something like: + // + // which is visually empty but non-empty as a string. + // + // We treat these as empty so callers don't render a phantom blank line. + const textOnly = lineHtml + .replace(/<[^>]*>/g, "") + .replace(/ /g, "") + .trim(); + return textOnly === ""; +} + export function extractShikiLines(html: string): string[] { const codeMatch = /]*>(.*?)<\/code>/s.exec(html); if (!codeMatch) return []; @@ -35,10 +48,11 @@ export function extractShikiLines(html: string): string[] { const contentStart = start + ''.length; const end = chunk.lastIndexOf(""); - return end > contentStart ? chunk.substring(contentStart, end) : ""; + const lineHtml = end > contentStart ? chunk.substring(contentStart, end) : ""; + return isVisuallyEmptyShikiLine(lineHtml) ? "" : lineHtml; }); - // Remove trailing empty lines (Shiki often adds one) + // Remove trailing empty lines (Shiki often adds one). while (lines.length > 0 && lines[lines.length - 1] === "") { lines.pop(); } From c0b04cec1142df63d8fbf2d6f7aaeae38ee24bfc Mon Sep 17 00:00:00 2001 From: Ammar Date: Fri, 12 Dec 2025 10:52:29 -0600 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A4=96=20ci:=20cover=20URL=20text=20c?= =?UTF-8?q?ode=20block=20newline=20regression=20in=20story?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/browser/stories/App.markdown.stories.tsx | 53 ++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/browser/stories/App.markdown.stories.tsx b/src/browser/stories/App.markdown.stories.tsx index 96605cef21..bd481e46ca 100644 --- a/src/browser/stories/App.markdown.stories.tsx +++ b/src/browser/stories/App.markdown.stories.tsx @@ -4,6 +4,20 @@ import { appMeta, AppWithMocks, type AppStory } from "./meta.js"; import { STABLE_TIMESTAMP, createUserMessage, createAssistantMessage } from "./mockFactory"; +import { expect, waitFor } from "@storybook/test"; + +async function waitForChatMessagesLoaded(canvasElement: HTMLElement): Promise { + await waitFor( + () => { + const messageWindow = canvasElement.querySelector('[data-testid="message-window"]'); + if (!messageWindow || messageWindow.getAttribute("data-loaded") !== "true") { + throw new Error("Messages not loaded yet"); + } + }, + { timeout: 5000 } + ); +} + import { setupSimpleChatStory } from "./storyHelpers"; export default { @@ -87,6 +101,12 @@ describe('getUser', () => { expect(res.status).toBe(401); }); }); +\`\`\` + +Text code blocks (regression: no phantom trailing blank line after highlighting): + +\`\`\`text +https://github.com/coder/mux/pull/new/chat-autocomplete-b24r \`\`\``; // ═══════════════════════════════════════════════════════════════════════════════ @@ -160,4 +180,37 @@ export const CodeBlocks: AppStory = { } /> ), + play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + await waitForChatMessagesLoaded(canvasElement); + + const url = "https://github.com/coder/mux/pull/new/chat-autocomplete-b24r"; + + // Find the highlighted code block containing the URL. + const container = await waitFor( + () => { + const candidates = Array.from(canvasElement.querySelectorAll(".code-block-container")); + const found = candidates.find((el) => el.textContent?.includes(url)); + if (!found) { + throw new Error("URL code block not found"); + } + return found; + }, + { timeout: 5000 } + ); + + // Ensure we capture the post-highlight DOM (Shiki wraps tokens in spans). + await waitFor( + () => { + const hasHighlightedSpans = container.querySelector(".code-line span"); + if (!hasHighlightedSpans) { + throw new Error("Code block not highlighted yet"); + } + }, + { timeout: 5000 } + ); + + // Regression: Shiki can emit a visually-empty trailing line (), which would render + // as a phantom extra line in our line-numbered code blocks. + await expect(container.querySelectorAll(".line-number").length).toBe(1); + }, }; From 948462576ee6b9626d1cbdbde3677b8ac51c49ad Mon Sep 17 00:00:00 2001 From: Ammar Date: Fri, 12 Dec 2025 11:48:12 -0600 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20remove=20extra=20spac?= =?UTF-8?q?ing=20for=20untyped=20markdown=20code=20blocks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/browser/stories/App.markdown.stories.tsx | 25 ++++++++++++++++++++ src/browser/styles/globals.css | 1 + 2 files changed, 26 insertions(+) diff --git a/src/browser/stories/App.markdown.stories.tsx b/src/browser/stories/App.markdown.stories.tsx index bd481e46ca..6b39ad2d71 100644 --- a/src/browser/stories/App.markdown.stories.tsx +++ b/src/browser/stories/App.markdown.stories.tsx @@ -107,6 +107,12 @@ Text code blocks (regression: no phantom trailing blank line after highlighting) \`\`\`text https://github.com/coder/mux/pull/new/chat-autocomplete-b24r +\`\`\` + +Code blocks without language (regression: avoid extra vertical spacing): + +\`\`\` +65d02772b 🤖 feat: Settings-driven model selector with visibility controls \`\`\``; // ═══════════════════════════════════════════════════════════════════════════════ @@ -209,6 +215,25 @@ export const CodeBlocks: AppStory = { { timeout: 5000 } ); + const noLangLine = "65d02772b 🤖 feat: Settings-driven model selector with visibility controls"; + + const codeEl = await waitFor( + () => { + const candidates = Array.from( + canvasElement.querySelectorAll(".markdown-content pre > code") + ); + const found = candidates.find((el) => el.textContent?.includes(noLangLine)); + if (!found) { + throw new Error("No-language code block not found"); + } + return found; + }, + { timeout: 5000 } + ); + + const style = window.getComputedStyle(codeEl); + await expect(style.marginTop).toBe("0px"); + await expect(style.marginBottom).toBe("0px"); // Regression: Shiki can emit a visually-empty trailing line (), which would render // as a phantom extra line in our line-numbered code blocks. await expect(container.querySelectorAll(".line-number").length).toBe(1); diff --git a/src/browser/styles/globals.css b/src/browser/styles/globals.css index d2f7aaa272..666a55d815 100644 --- a/src/browser/styles/globals.css +++ b/src/browser/styles/globals.css @@ -1424,6 +1424,7 @@ code { .markdown-content pre code { background: none; padding: 0; + margin: 0; color: var(--color-foreground); } From 4286c4c91c6c4412f7178427343d55f8962fb401 Mon Sep 17 00:00:00 2001 From: Ammar Date: Fri, 12 Dec 2025 12:09:07 -0600 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20consistent=20code=20b?= =?UTF-8?q?lock=20background=20in=20light=20theme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/browser/styles/globals.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/styles/globals.css b/src/browser/styles/globals.css index 666a55d815..da3076c391 100644 --- a/src/browser/styles/globals.css +++ b/src/browser/styles/globals.css @@ -1414,7 +1414,7 @@ code { } .markdown-content pre { - background: rgba(0, 0, 0, 0.3); + background: var(--color-code-bg); padding: 12px; border-radius: 4px; overflow-x: auto;