From 256284b7fe901f05e94301bc5109400faaa25d0c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 27 Feb 2026 19:03:11 -0500 Subject: [PATCH] fix: add sticky headers to the git diff (#22425) --- .../AgentDetail/ConversationTimeline.tsx | 40 ++++++++++++++++--- .../pages/AgentsPage/FilesChangedPanel.tsx | 10 ++++- site/vite.config.mts | 8 ++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentDetail/ConversationTimeline.tsx b/site/src/pages/AgentsPage/AgentDetail/ConversationTimeline.tsx index 5954ba94e7..62a1010a02 100644 --- a/site/src/pages/AgentsPage/AgentDetail/ConversationTimeline.tsx +++ b/site/src/pages/AgentsPage/AgentDetail/ConversationTimeline.tsx @@ -268,7 +268,7 @@ const ChatMessageItem = memo<{ {fadeFromBottom && (
{ const [isStuck, setIsStuck] = useState(false); const [isReady, setIsReady] = useState(false); + const [isTooTall, setIsTooTall] = useState(false); const sentinelRef = useRef(null); const containerRef = useRef(null); + const updateFnRef = useRef<(() => void) | null>(null); // useLayoutEffect so isStuck and --clip-h are both resolved // before the browser paints, avoiding a flash on load. @@ -440,9 +442,22 @@ const StickyUserMessage: FC<{ const MIN_HEIGHT = 72; let scrollerTop = scroller.getBoundingClientRect().top; + let scrollerHeight = scroller.clientHeight; const update = () => { const fullHeight = container.offsetHeight; + + // Skip sticky behavior for messages that take up + // most of the visible area — accounting for the + // chat input and some breathing room. + const tooTall = fullHeight > scrollerHeight * 0.75; + setIsTooTall(tooTall); + if (tooTall) { + container.style.setProperty("--clip-h", `${fullHeight}px`); + container.style.setProperty("--fade-opacity", "0"); + return; + } + const sentinelTop = sentinel.getBoundingClientRect().top; const scrolledPast = scrollerTop - sentinelTop; @@ -463,9 +478,11 @@ const StickyUserMessage: FC<{ visible < fullHeight - 8 ? "1" : "0", ); }; + updateFnRef.current = update; const onResize = () => { scrollerTop = scroller.getBoundingClientRect().top; + scrollerHeight = scroller.clientHeight; update(); }; @@ -495,6 +512,16 @@ const StickyUserMessage: FC<{ }; }, []); + // Re-run the height calculation synchronously whenever + // isStuck changes so --clip-h is correct on the same frame + // the overlay appears. Without this, the async + // IntersectionObserver + RAF-throttled scroll handler can + // leave a stale --clip-h for one paint. + // biome-ignore lint/correctness/useExhaustiveDependencies: isStuck is an intentional trigger + useLayoutEffect(() => { + updateFnRef.current?.(); + }, [isStuck]); + const handleEditUserMessage = onEditUserMessage ? (messageId: number, text: string) => { onEditUserMessage(messageId, text); @@ -519,18 +546,19 @@ const StickyUserMessage: FC<{
{/* Flow element: always in the DOM to preserve scroll layout. Hidden when stuck so the clipped overlay takes over visually. */}
= ({ chatId }) => { const theme = useTheme(); const isDark = theme.palette.mode === "dark"; - const diffOptions = useMemo(() => getDiffViewerOptions(isDark), [isDark]); + const diffOptions = useMemo(() => { + const base = getDiffViewerOptions(isDark); + return { + ...base, + // Extend the base CSS to make file headers sticky so they + // remain visible while scrolling through long diffs. + unsafeCSS: `${base.unsafeCSS ?? ""} [data-diffs-header] { position: sticky; top: 0; z-index: 10; background-color: hsl(var(--surface-primary)) !important; }`, + }; + }, [isDark]); const diffStatusQuery = useQuery(chatDiffStatus(chatId)); const diffContentsQuery = useQuery({ diff --git a/site/vite.config.mts b/site/vite.config.mts index bdf392465d..824f222367 100644 --- a/site/vite.config.mts +++ b/site/vite.config.mts @@ -90,6 +90,14 @@ export default defineConfig({ target: process.env.CODER_HOST || "http://localhost:3000", secure: process.env.NODE_ENV === "production", configure: (proxy) => { + if (process.env.CODER_SESSION_TOKEN) { + proxy.on("proxyReq", (proxyReq) => { + proxyReq.setHeader( + "Coder-Session-Token", + process.env.CODER_SESSION_TOKEN!, + ); + }); + } // Vite does not catch socket errors, and stops the webserver. // As /logs endpoint can return HTTP 4xx status, we need to embrace // Vite with a custom error handler to prevent from quitting.