From 7e8559aac08b35b31bd88bb17bce3b73c9534475 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 27 Feb 2026 20:13:41 +0000 Subject: [PATCH] fix: respect light/dark theme in agents FilesChangedPanel diff viewer (#22403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The git diff on the `/agents` page had color issues: the editor background followed light mode but the syntax highlighting used dark mode (`github-dark-high-contrast`), and the filename header used light-colored text on a light background. The root cause was hardcoded dark theme options in the `FileDiff` component: ```tsx themeType: "dark", theme: "github-dark-high-contrast", ``` ## Fix Uses the same theme-aware pattern as every other diff/file viewer in the codebase (`WriteFileTool`, `EditFilesTool`, `ReadFileTool`, `Tool`, `response.tsx`): 1. `useTheme()` from `@emotion/react` to read `palette.mode` 2. `getDiffViewerOptions(isDark)` from the shared `utils.ts` module — returns `github-light` theme for light mode, `github-dark-high-contrast` for dark mode 3. Reuses `DIFFS_FONT_STYLE` and `diffViewerCSS` constants instead of inlining duplicates ## Storybook coverage Added four new stories with real unified diff content: - **WithDiffDark** — dark mode with a PR link - **WithDiffLight** — light mode with a PR link - **NoPullRequestDark** — dark mode, "Files Changed" header - **NoPullRequestLight** — light mode, "Files Changed" header The existing stories only covered empty and parse-error states with no rendered diff. --- .../AgentsPage/FilesChangedPanel.stories.tsx | 117 ++++++++++++++++++ .../pages/AgentsPage/FilesChangedPanel.tsx | 22 ++-- 2 files changed, 128 insertions(+), 11 deletions(-) diff --git a/site/src/pages/AgentsPage/FilesChangedPanel.stories.tsx b/site/src/pages/AgentsPage/FilesChangedPanel.stories.tsx index 0aa10268a6..02034eea61 100644 --- a/site/src/pages/AgentsPage/FilesChangedPanel.stories.tsx +++ b/site/src/pages/AgentsPage/FilesChangedPanel.stories.tsx @@ -5,6 +5,46 @@ import type { ChatDiffContents } from "api/typesGenerated"; import { expect, screen, spyOn } from "storybook/test"; import { FilesChangedPanel } from "./FilesChangedPanel"; +const sampleUnifiedDiff = `diff --git a/site/src/pages/AgentsPage/FilesChangedPanel.tsx b/site/src/pages/AgentsPage/FilesChangedPanel.tsx +index abc1234..def5678 100644 +--- a/site/src/pages/AgentsPage/FilesChangedPanel.tsx ++++ b/site/src/pages/AgentsPage/FilesChangedPanel.tsx +@@ -1,10 +1,14 @@ ++import { useTheme } from "@emotion/react"; + import { parsePatchFiles } from "@pierre/diffs"; + import { FileDiff } from "@pierre/diffs/react"; ++import { ++ DIFFS_FONT_STYLE, ++ getDiffViewerOptions, ++} from "components/ai-elements/tool/utils"; + import { chatDiffContents, chatDiffStatus } from "api/queries/chats"; + import { ErrorAlert } from "components/Alert/ErrorAlert"; + import { ScrollArea } from "components/ScrollArea/ScrollArea"; +-import { Skeleton } from "components/Skeleton/Skeleton"; + import { type FC, useMemo } from "react"; + import { useQuery } from "react-query"; +diff --git a/site/src/components/ai-elements/tool/utils.ts b/site/src/components/ai-elements/tool/utils.ts +index 1234567..abcdef0 100644 +--- a/site/src/components/ai-elements/tool/utils.ts ++++ b/site/src/components/ai-elements/tool/utils.ts +@@ -10,6 +10,18 @@ export const diffViewerCSS = + export function getDiffViewerOptions(isDark: boolean) { + return { + diffStyle: "unified" as const, ++ diffIndicators: "bars" as const, ++ overflow: "scroll" as const, + themeType: (isDark ? "dark" : "light") as "dark" | "light", ++ theme: isDark ? "github-dark-high-contrast" : "github-light", ++ unsafeCSS: diffViewerCSS, + }; + } ++ ++export const DIFFS_FONT_STYLE = { ++ "--diffs-font-size": "11px", ++ "--diffs-line-height": "1.5", ++} as React.CSSProperties; +`; + const defaultDiffStatus: ChatDiffStatusResponse = { chat_id: "test-chat", changes_requested: false, @@ -23,6 +63,13 @@ const meta: Meta = { args: { chatId: "test-chat", }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], beforeEach: () => { spyOn(API, "getChatDiffStatus").mockResolvedValue(defaultDiffStatus); spyOn(API, "getChatDiffContents").mockResolvedValue(defaultDiffContents); @@ -65,3 +112,73 @@ export const ParseError: Story = { expect(screen.getByText("No file changes to display.")).toBeInTheDocument(); }, }; + +export const WithDiffDark: Story = { + beforeEach: () => { + spyOn(API, "getChatDiffStatus").mockResolvedValue({ + ...defaultDiffStatus, + url: "https://github.com/coder/coder/pull/456", + additions: 14, + deletions: 2, + changed_files: 2, + }); + spyOn(API, "getChatDiffContents").mockResolvedValue({ + ...defaultDiffContents, + diff: sampleUnifiedDiff, + }); + }, +}; + +export const WithDiffLight: Story = { + globals: { + theme: "light", + }, + beforeEach: () => { + spyOn(API, "getChatDiffStatus").mockResolvedValue({ + ...defaultDiffStatus, + url: "https://github.com/coder/coder/pull/456", + additions: 14, + deletions: 2, + changed_files: 2, + }); + spyOn(API, "getChatDiffContents").mockResolvedValue({ + ...defaultDiffContents, + diff: sampleUnifiedDiff, + }); + }, +}; + +export const NoPullRequestDark: Story = { + beforeEach: () => { + spyOn(API, "getChatDiffStatus").mockResolvedValue({ + ...defaultDiffStatus, + url: "https://github.com/coder/coder/pull/456", + additions: 14, + deletions: 2, + changed_files: 2, + }); + spyOn(API, "getChatDiffContents").mockResolvedValue({ + ...defaultDiffContents, + diff: sampleUnifiedDiff, + }); + }, +}; + +export const NoPullRequestLight: Story = { + globals: { + theme: "light", + }, + beforeEach: () => { + spyOn(API, "getChatDiffStatus").mockResolvedValue({ + ...defaultDiffStatus, + url: "https://github.com/coder/coder/pull/456", + additions: 14, + deletions: 2, + changed_files: 2, + }); + spyOn(API, "getChatDiffContents").mockResolvedValue({ + ...defaultDiffContents, + diff: sampleUnifiedDiff, + }); + }, +}; diff --git a/site/src/pages/AgentsPage/FilesChangedPanel.tsx b/site/src/pages/AgentsPage/FilesChangedPanel.tsx index 8fdcccb981..e7021aa1bf 100644 --- a/site/src/pages/AgentsPage/FilesChangedPanel.tsx +++ b/site/src/pages/AgentsPage/FilesChangedPanel.tsx @@ -1,7 +1,12 @@ +import { useTheme } from "@emotion/react"; import { parsePatchFiles } from "@pierre/diffs"; import { FileDiff } from "@pierre/diffs/react"; import { chatDiffContents, chatDiffStatus } from "api/queries/chats"; import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { + DIFFS_FONT_STYLE, + getDiffViewerOptions, +} from "components/ai-elements/tool/utils"; import { ScrollArea } from "components/ScrollArea/ScrollArea"; import { Skeleton } from "components/Skeleton/Skeleton"; import { @@ -33,6 +38,10 @@ function formatPullRequestLabel(url: string): string { } export const FilesChangedPanel: FC = ({ chatId }) => { + const theme = useTheme(); + const isDark = theme.palette.mode === "dark"; + const diffOptions = useMemo(() => getDiffViewerOptions(isDark), [isDark]); + const diffStatusQuery = useQuery(chatDiffStatus(chatId)); const diffContentsQuery = useQuery({ ...chatDiffContents(chatId), @@ -134,23 +143,14 @@ export const FilesChangedPanel: FC = ({ chatId }) => { key={fileDiff.name} fileDiff={fileDiff} options={{ - diffStyle: "unified", - diffIndicators: "bars", - overflow: "scroll", - themeType: "dark", + ...diffOptions, enableLineSelection: true, enableHoverUtility: true, onLineSelected() { // TODO: Make this add context to the input so the user can type. }, - theme: "github-dark-high-contrast", - unsafeCSS: - "pre, [data-line], [data-diffs-header] { background-color: transparent !important; } [data-diffs-header] { border-left: 1px solid var(--border); }", - }} - style={{ - "--diffs-font-size": "11px", - "--diffs-line-height": "1.5", }} + style={DIFFS_FONT_STYLE} /> ))}