Skip to content
Open
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
139 changes: 83 additions & 56 deletions apps/webapp/app/components/code/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ArrowsPointingOutIcon } from "@heroicons/react/20/solid";
import { Clipboard, ClipboardCheck } from "lucide-react";
import type { Language, PrismTheme } from "prism-react-renderer";
import { Highlight, Prism } from "prism-react-renderer";
import { forwardRef, ReactNode, useCallback, useEffect, useState } from "react";
import { forwardRef, ReactNode, useCallback, useEffect, useRef, useState } from "react";
import { TextWrapIcon } from "~/assets/icons/TextWrapIcon";
import { cn } from "~/utils/cn";
import { highlightSearchText } from "~/utils/logUtils";
Expand Down Expand Up @@ -217,6 +217,29 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
const [isModalOpen, setIsModalOpen] = useState(false);
const [isWrapped, setIsWrapped] = useState(false);

const restoreRef = useRef<(() => void) | null>(null);

const handleCodeMouseDown = useCallback((e: React.MouseEvent) => {
if (e.button !== 0) return;
const el = e.currentTarget as HTMLElement;
document.documentElement.style.userSelect = "none";
el.style.userSelect = "text";
const restore = () => {
document.documentElement.style.userSelect = "";
el.style.userSelect = "";
document.removeEventListener("mouseup", restore);
window.removeEventListener("blur", restore);
restoreRef.current = null;
};
restoreRef.current = restore;
document.addEventListener("mouseup", restore, { once: true });
window.addEventListener("blur", restore, { once: true });
}, []);

useEffect(() => {
return () => restoreRef.current?.();
}, []);
Comment on lines 222 to 237
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Global userSelect = "none" can stick if mouseup is missed.

If the user switches away (e.g., Alt-Tab, context menu, or the component unmounts while the mouse is held), mouseup never fires, leaving document.documentElement.style.userSelect = "none" permanently. Two improvements:

  1. Use { once: true } instead of manually removing the listener β€” simpler and avoids forgetting removal.
  2. Add a window blur listener as a safety net to call restore.
  3. Return a cleanup from a useEffect (or store a ref to restore) so unmounting also cleans up.
Suggested improvement
 const handleCodeMouseDown = useCallback((e: React.MouseEvent) => {
   if (e.button !== 0) return;
   const el = e.currentTarget as HTMLElement;
   document.documentElement.style.userSelect = "none";
   el.style.userSelect = "text";
   const restore = () => {
     document.documentElement.style.userSelect = "";
     el.style.userSelect = "";
-    document.removeEventListener("mouseup", restore);
+    document.removeEventListener("mouseup", restore);
+    window.removeEventListener("blur", restore);
   };
-  document.addEventListener("mouseup", restore);
+  document.addEventListener("mouseup", restore, { once: true });
+  window.addEventListener("blur", restore, { once: true });
 }, []);
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/components/code/CodeBlock.tsx` around lines 220 - 231, The
mouse-down handler handleCodeMouseDown currently sets
document.documentElement.style.userSelect = "none" and relies on a mouseup
listener to call restore, which can be missed (Alt-Tab, context menu, unmount)
and leave userSelect stuck; change the implementation to add the mouseup
listener with { once: true } instead of manual removal, also attach a window
blur listener that calls the same restore to handle focus loss, and ensure the
restore function reference is stable across lifecycle by storing it in a ref or
by moving listener setup/teardown into a useEffect so unmount will call restore
and remove any listeners; update references to restore and the event
registrations inside handleCodeMouseDown/useEffect accordingly.

βœ… Addressed in commit 99abbd8


const onCopied = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
Expand Down Expand Up @@ -334,42 +357,44 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
)}
</div>

{shouldHighlight ? (
<HighlightCode
theme={theme}
code={code}
language={language}
showLineNumbers={showLineNumbers}
highlightLines={highlightLines}
maxLineWidth={maxLineWidth}
className="px-2 py-3"
preClassName="text-xs"
isWrapped={isWrapped}
searchTerm={searchTerm}
/>
) : (
<div
dir="ltr"
className={cn(
"px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
!isWrapped && "overflow-x-auto",
isWrapped && "overflow-y-auto"
)}
style={{
maxHeight,
}}
>
<pre
<div onMouseDown={handleCodeMouseDown}>
{shouldHighlight ? (
<HighlightCode
theme={theme}
code={code}
language={language}
showLineNumbers={showLineNumbers}
highlightLines={highlightLines}
maxLineWidth={maxLineWidth}
className="px-2 py-3"
preClassName="text-xs"
isWrapped={isWrapped}
searchTerm={searchTerm}
/>
) : (
<div
dir="ltr"
className={cn(
"relative mr-2 p-2 font-mono text-xs leading-relaxed",
isWrapped && "[&_span]:whitespace-pre-wrap [&_span]:break-words"
"px-2 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
!isWrapped && "overflow-x-auto",
isWrapped && "overflow-y-auto"
)}
dir="ltr"
style={{
maxHeight,
}}
>
{highlightSearchText(code, searchTerm)}
</pre>
</div>
)}
<pre
className={cn(
"relative mr-2 p-2 font-mono text-xs leading-relaxed",
isWrapped && "[&_span]:whitespace-pre-wrap [&_span]:break-words"
)}
dir="ltr"
>
{highlightSearchText(code, searchTerm)}
</pre>
</div>
)}
</div>
</div>

<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
Expand All @@ -390,28 +415,30 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
</Button>
</DialogHeader>

{shouldHighlight ? (
<HighlightCode
theme={theme}
code={code}
language={language}
showLineNumbers={showLineNumbers}
highlightLines={highlightLines}
maxLineWidth={maxLineWidth}
className="min-h-full"
preClassName="text-sm"
isWrapped={isWrapped}
/>
) : (
<div
dir="ltr"
className="overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
>
<pre className="relative mr-2 p-2 font-mono text-base leading-relaxed" dir="ltr">
{highlightSearchText(code, searchTerm)}
</pre>
</div>
)}
<div onMouseDown={handleCodeMouseDown} className="overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
{shouldHighlight ? (
<HighlightCode
theme={theme}
code={code}
language={language}
showLineNumbers={showLineNumbers}
highlightLines={highlightLines}
maxLineWidth={maxLineWidth}
className="min-h-full"
preClassName="text-sm"
isWrapped={isWrapped}
/>
) : (
<div
dir="ltr"
className="overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
>
<pre className="relative mr-2 p-2 font-mono text-base leading-relaxed" dir="ltr">
{highlightSearchText(code, searchTerm)}
</pre>
</div>
)}
</div>
Comment on lines 417 to +441

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Modal code block has duplicated padding and scrollbar classes causing double padding

In the modal's shouldHighlight path, the new wrapper <div> at line 417 applies overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600, but the HighlightCode component internally already renders its own container with px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600 plus overflow classes (see containerClasses at apps/webapp/app/components/code/CodeBlock.tsx:515-520). This results in double px-3 py-3 padding and nested scroll containers in the modal.

Root Cause

Before this PR, the shouldHighlight branch in the modal rendered <HighlightCode> directly as a child of <DialogContent>, and the non-highlighted branch had its own wrapper <div> with the scrollbar/padding classes. The PR moved both branches inside a new wrapper <div> that carries the scrollbar/padding classes from the old non-highlighted wrapper, but the HighlightCode component already has these same classes internally:

// New outer wrapper (line 417-418)
<div onMouseDown={handleCodeMouseDown} className="overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
  // HighlightCode internally renders (line 515-520):
  // <div className="px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600 overflow-x-auto min-h-full">

Similarly, for the !shouldHighlight branch, the original inner <div> at line 432-434 still has overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600, creating double nesting of the same styles.

Impact: The modal code block will have double padding (px-3 py-3 applied twice = 1.5rem on each side instead of 0.75rem), and nested scroll containers which can cause confusing scroll behavior.

Prompt for agents
In apps/webapp/app/components/code/CodeBlock.tsx, the new wrapper div at line 417 that was added for the mouseDown handler has className="overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600". This duplicates the classes that already exist inside both the HighlightCode component (lines 515-520) and the non-highlighted inner div (lines 432-434).

To fix this:
1. On line 417-418, remove the duplicate classes from the outer wrapper div, keeping only the onMouseDown handler. Change it to: <div onMouseDown={handleCodeMouseDown}>
2. For the non-highlighted path (lines 431-439), the inner div at line 432-434 already has the correct overflow/padding/scrollbar classes, so it will continue to work correctly.
3. For the highlighted path, HighlightCode already has its own container classes internally, so it will also work correctly.
4. However, you may need to keep overflow-auto on the outer wrapper for the modal to scroll properly. In that case, just remove the padding classes (px-3 py-3) and scrollbar classes from the outer wrapper, and ensure the inner components handle their own padding.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines 417 to +441

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Modal HighlightCode no longer receives overflow-auto directly β€” outer wrapper changes scroll behavior

Before this PR, in the modal the HighlightCode component was rendered directly inside DialogContent, and its internal container provided the overflow scrolling. Now, the new outer wrapper at line 417 has overflow-auto, which means the outer div will be the scroll container for the modal content. The HighlightCode internal container also has overflow-x-auto or overflow-y-auto depending on isWrapped. Having nested scroll containers could lead to unexpected scroll behavior β€” particularly the inner container may not scroll as expected if the outer one captures the scroll. This is related to but distinct from the duplicate padding bug already reported.

Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +418 to +441
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Double padding and nested scroll containers in the modal.

The outer wrapper (line 418) applies overflow-auto px-3 py-3 scrollbar-thin …, but both inner paths duplicate those same styles:

  • Highlighted path: HighlightCode's internal container (lines 515–520) also applies px-3 py-3 scrollbar-thin … overflow-x-auto.
  • Non-highlighted path: The inner <div> at line 432–434 repeats the identical classes verbatim.

This results in ~px-6-equivalent horizontal padding and nested scrollable regions. Compare with the main view (line 360), where the outer <div> carries only onMouseDown and leaves styling to the children.

Suggested fix: move scroll/padding styling to children only
-            <div onMouseDown={handleCodeMouseDown} className="overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
+            <div onMouseDown={handleCodeMouseDown} className="overflow-auto">
               {shouldHighlight ? (
                 <HighlightCode
                   theme={theme}

This keeps the outer wrapper as a simple scroll host (or just the mousedown handler, mirroring the main view), letting HighlightCode and the fallback <div> own their own padding and scrollbar styling.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/components/code/CodeBlock.tsx` around lines 418 - 441, The
outer wrapper in the CodeBlock component currently duplicates scrollbar/padding
styles; remove the duplicated layout classes from the outer <div> that has
onMouseDown (leave only the onMouseDown handler and no padding/scroll classes)
so the inner components own scrolling/padding. Keep the existing
scrollbar/padding/overflow classes on HighlightCode and the fallback inner <div>
that renders highlightSearchText, ensuring there is a single scroll container
and no doubled px-3 py-3 or nested overflow-auto.

</DialogContent>
</Dialog>
</>
Expand Down