fix: respect light/dark theme in agents FilesChangedPanel diff viewer (#22403)

## 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.
This commit is contained in:
Danielle Maywood
2026-02-27 20:13:41 +00:00
committed by GitHub
parent b65c0766d2
commit 7e8559aac0
2 changed files with 128 additions and 11 deletions
@@ -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<typeof FilesChangedPanel> = {
args: {
chatId: "test-chat",
},
decorators: [
(Story) => (
<div style={{ height: 600, width: 500 }}>
<Story />
</div>
),
],
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,
});
},
};
+11 -11
View File
@@ -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<FilesChangedPanelProps> = ({ 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<FilesChangedPanelProps> = ({ 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}
/>
))}
</div>