From fb252d0b078899921d4e47cec8e729f91caa90c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20St=C3=A9ffano?= Date: Fri, 21 Aug 2026 16:36:58 -0300 Subject: [PATCH] fix(chat): stop inline-code CSS from leaking into fenced code blocks The `:not(pre) > code` selectors in base.css assumed a fenced code block's is always a direct child of a real
. That breaks
for syntax-highlighted blocks: SyntaxHighlighter's PreTag="div" puts a
wrapper 
between and the outer
, so the selector
misfires and applies inline-code padding to highlighted blocks too.

Since  is an inline element spanning multiple lines, left padding
only paints on the first line fragment per the CSS box model - visible
as a first-line-only indent, invisible in copied text (pure paint, not
a text difference).

Replace the parent-structure guess with an explicit marker set in JS:
resolveCodeClassName (new, dependency-free module so it's unit
testable under the current Jest/ESM constraint - react-markdown's ESM
export syntax can't be parsed without a jest.config.js change) tags
genuine inline code with .inline-code based on content shape (no
trailing newline), since react-markdown@9 no longer passes an `inline`
prop to distinguish it directly.

Co-Authored-By: Claude Sonnet 5 
---
 src/markdown-code-class.ts           | 27 ++++++++++++++++++++++++++
 src/markdown-renderer.tsx            |  6 +++++-
 style/base.css                       |  4 ++--
 tests/ts/markdown-code-class.test.ts | 29 ++++++++++++++++++++++++++++
 4 files changed, 63 insertions(+), 3 deletions(-)
 create mode 100644 src/markdown-code-class.ts
 create mode 100644 tests/ts/markdown-code-class.test.ts

diff --git a/src/markdown-code-class.ts b/src/markdown-code-class.ts
new file mode 100644
index 00000000..921cba70
--- /dev/null
+++ b/src/markdown-code-class.ts
@@ -0,0 +1,27 @@
+// Copyright (c) Mehmet Bektas 
+
+// Kept dependency-free (no React/react-markdown imports) so it can be unit
+// tested directly under Jest: markdown-renderer.tsx imports react-markdown
+// at module scope, which ships ESM `export` syntax that jest.config.js's
+// default (no `transformIgnorePatterns` override) can't parse — anything
+// importing markdown-renderer.tsx transitively fails at parse time before a
+// test can run (see the reverted MarkdownRenderer tests from PR #385).
+
+// react-markdown@9 never sets `inline` (removed upstream), so the `code`
+// component's `if (inline || !match)` branch also catches a fenced block
+// with an unrecognized/missing language — that case keeps its default
+// `pre > code` nesting from remark-rehype, unlike the SyntaxHighlighter
+// branch, so it shouldn't get inline-code styling either. Distinguish the
+// two by content shape instead: remark-rehype always appends a trailing
+// `\n` to fenced code (even single-line), while true inline code never
+// contains one. Only genuine inline code gets `.inline-code`, so CSS can
+// target it directly instead of via `:not(pre) > code`, which broke once
+// `PreTag="div"` put a wrapper div between a highlighted block's ``
+// and the outer `
`.
+export function resolveCodeClassName(
+  children: unknown,
+  className?: string
+): string | undefined {
+  const isTrulyInline = !String(children).includes('\n');
+  return isTrulyInline ? `inline-code ${className || ''}`.trim() : className;
+}
diff --git a/src/markdown-renderer.tsx b/src/markdown-renderer.tsx
index 6c0b4fbd..d6fea56c 100644
--- a/src/markdown-renderer.tsx
+++ b/src/markdown-renderer.tsx
@@ -17,6 +17,7 @@ import { PathExt } from '@jupyterlab/coreutils';
 import { MarkdownLink } from './components/markdown-link';
 import { isDarkTheme, writeTextToClipboard } from './utils';
 import { IActiveDocumentInfo } from './tokens';
+import { resolveCodeClassName } from './markdown-code-class';
 
 type MarkdownRendererProps = {
   children: string;
@@ -104,7 +105,10 @@ export function MarkdownRenderer({
 
           if (inline || !match) {
             return (
-              
+              
                 {children}
               
             );
diff --git a/style/base.css b/style/base.css
index 8f6c560a..a9613e93 100644
--- a/style/base.css
+++ b/style/base.css
@@ -729,7 +729,7 @@ pre:has(.code-block-header) {
   font-size: 13px;
 }
 
-.chat-message-content :not(pre) > code {
+.chat-message-content code.inline-code {
   background-color: var(--jp-layout-color2);
   padding: 1px 4px;
   border-radius: 3px;
@@ -1794,7 +1794,7 @@ button.send-button:disabled:active:not(.send-button-stop) {
 
 /* Inline code also fills with --jp-layout-color2, which now matches this box's
    fill; recess nested code to --jp-layout-color1 so it stays distinct. */
-.expandable-content-text :not(pre) > code {
+.expandable-content-text code.inline-code {
   background-color: var(--jp-layout-color1);
 }
 
diff --git a/tests/ts/markdown-code-class.test.ts b/tests/ts/markdown-code-class.test.ts
new file mode 100644
index 00000000..3d032ed1
--- /dev/null
+++ b/tests/ts/markdown-code-class.test.ts
@@ -0,0 +1,29 @@
+// Copyright (c) Mehmet Bektas 
+
+import { resolveCodeClassName } from '../../src/markdown-code-class';
+
+describe('resolveCodeClassName', () => {
+  it('marks single-line content (true inline code) with inline-code', () => {
+    expect(resolveCodeClassName('inline_var')).toBe('inline-code');
+  });
+
+  it('preserves an existing className alongside inline-code', () => {
+    expect(resolveCodeClassName('inline_var', 'some-class')).toBe(
+      'inline-code some-class'
+    );
+  });
+
+  it('does not mark multi-line content (a highlighted fenced block)', () => {
+    expect(
+      resolveCodeClassName('import json\nimport logging\n', 'language-python')
+    ).toBe('language-python');
+  });
+
+  it('does not mark multi-line content with no language class (unmatched fence)', () => {
+    expect(resolveCodeClassName('plain text block\n')).toBeUndefined();
+  });
+
+  it('does not mark a single-line fenced block that still carries a trailing newline', () => {
+    expect(resolveCodeClassName('x\n', 'language-text')).toBe('language-text');
+  });
+});