From 48adddb1f862c46bbd2d513ff881b69015dd4d1a Mon Sep 17 00:00:00 2001 From: scotttong Date: Mon, 17 Aug 2026 22:22:21 -0700 Subject: [PATCH] fix(markdown): separate the code block from the agent bubble in light mode The code block used --muted. Inside an agent bubble that is the wrong value: --bubble-agent is oklch(0.97 0 0) in light, which is exactly --muted. Block and bubble painted the same fill, so the block vanished and the whole comment read as one flat gray slab. Dark hid the fault because the bubble is oklch(0.185 0 0) and the block oklch(0.269 0 0). No fixed token fixes this. The block renders on the page AND inside bubbles, and --bubble-agent changes per variant: "former" is oklch(0.955 0 0), and "hairline" is var(--background) itself. Any fixed choice collides with one of those surfaces. Use a translucent tint instead. --code-surface shifts whatever is beneath it rather than replacing it, so one value separates on the page and in every bubble variant. Light darkens 5% toward the ink; dark lifts 9%. Add tests: the surface must be a color-mix tint defined in both modes, and the block must never pin itself to --muted or --bubble-agent. Co-Authored-By: Claude Opus 5 --- .../MarkdownCodeBlockStyles.test.ts | 27 ++++++++++++++++--- ui/src/index.css | 27 ++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/ui/src/components/MarkdownCodeBlockStyles.test.ts b/ui/src/components/MarkdownCodeBlockStyles.test.ts index bb1236f3d57..ad84b6056a2 100644 --- a/ui/src/components/MarkdownCodeBlockStyles.test.ts +++ b/ui/src/components/MarkdownCodeBlockStyles.test.ts @@ -28,7 +28,7 @@ describe("markdown code block styles", () => { it("rides theme tokens for the rendered code block surface", () => { const block = cssBlock(".paperclip-markdown pre"); - expect(block).toContain("background-color: var(--muted)"); + expect(block).toContain("background-color: var(--code-surface)"); expect(block).toContain("color: var(--foreground)"); expect(block).toContain("border: 1px solid var(--border)"); expect(block).toContain("border-radius: var(--radius-lg)"); @@ -37,7 +37,7 @@ describe("markdown code block styles", () => { it("rides theme tokens for the editor content code block surface", () => { const block = cssBlock(".paperclip-mdxeditor-content pre"); - expect(block).toContain("background: var(--muted)"); + expect(block).toContain("background: var(--code-surface)"); expect(block).toContain("color: var(--foreground)"); expect(block).toContain("border: 1px solid var(--border)"); expect(block).toContain("border-radius: var(--radius-lg)"); @@ -46,9 +46,9 @@ describe("markdown code block styles", () => { it("points the normal and inverted prose variables at the same tokens", () => { const block = cssBlock(".paperclip-markdown"); - expect(block).toContain("--tw-prose-pre-bg: var(--muted)"); + expect(block).toContain("--tw-prose-pre-bg: var(--code-surface)"); expect(block).toContain("--tw-prose-pre-code: var(--foreground)"); - expect(block).toContain("--tw-prose-invert-pre-bg: var(--muted)"); + expect(block).toContain("--tw-prose-invert-pre-bg: var(--code-surface)"); expect(block).toContain("--tw-prose-invert-pre-code: var(--foreground)"); }); @@ -110,3 +110,22 @@ describe("code syntax palette", () => { } }); }); + +describe("code block surface separation", () => { + it("defines the surface as a translucent tint in both modes", () => { + // A fixed token cannot work: --bubble-agent is oklch(0.97 0 0) in light, + // identical to --muted, and its "hairline" variant is var(--background). + // A tint shifts whatever is beneath it, so it separates everywhere. + expect(stylesheet).toMatch(/--code-surface:\s*color-mix\(in oklab, var\(--foreground\) \d+%, transparent\)/); + + const darkBlock = stylesheet.slice(stylesheet.indexOf(".dark {")); + expect(darkBlock).toContain("--code-surface:"); + }); + + it("never pins the code surface to the agent bubble fill", () => { + const preBlock = cssBlock(".paperclip-markdown pre"); + + expect(preBlock).not.toContain("var(--muted)"); + expect(preBlock).not.toContain("var(--bubble-agent)"); + }); +}); diff --git a/ui/src/index.css b/ui/src/index.css index 020c6e52eb2..402c4f416ee 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -1408,7 +1408,7 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before { padding: 0; border: 1px solid var(--border); border-radius: var(--radius-lg); - background: var(--muted); + background: var(--code-surface); color: var(--foreground); overflow-x: auto; } @@ -1487,16 +1487,16 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before { both modes. Override prose CSS variables so prose-invert can't revert to defaults. */ .paperclip-markdown { - --tw-prose-pre-bg: var(--muted); + --tw-prose-pre-bg: var(--code-surface); --tw-prose-pre-code: var(--foreground); - --tw-prose-invert-pre-bg: var(--muted); + --tw-prose-invert-pre-bg: var(--code-surface); --tw-prose-invert-pre-code: var(--foreground); } .paperclip-markdown pre { border: 1px solid var(--border) !important; border-radius: var(--radius-lg) !important; - background-color: var(--muted) !important; + background-color: var(--code-surface) !important; color: var(--foreground) !important; padding: 0.5rem 0.65rem !important; margin: 0.4rem 0 !important; @@ -1510,6 +1510,25 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before { font-size: 1em; } +/* ── Code block surface ── + The block must separate from whatever it sits on, and that is not always + the page. It also renders inside agent bubbles, whose fill --bubble-agent + is oklch(0.97 0 0) in light — the same value as --muted, which made the + block vanish into the bubble and read as one flat slab. The bubble fill + also changes per variant, and the "hairline" variant IS var(--background), + so no fixed token separates in every case. + + A translucent tint shifts whatever is beneath it instead of replacing it, + so one value holds on the page and in every bubble variant, in both modes. + Light darkens toward the ink; dark lifts toward it. */ +:root { + --code-surface: color-mix(in oklab, var(--foreground) 5%, transparent); +} + +.dark { + --code-surface: color-mix(in oklab, var(--foreground) 9%, transparent); +} + /* ── Code syntax roles ── Each alias points at a token that already exists and already carries a `.dark` override, so one definition covers both modes. A custom property