Skip to content
Closed
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useDebouncedValue } from "@tanstack/react-pacer";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { COLORBLIND_DIFF_STYLES } from "~/lib/diffRendering";
import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery";
import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery";
import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery";
Expand Down Expand Up @@ -263,6 +264,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
select: (params) => parseDiffRouteSearch(params),
});
const { resolvedTheme } = useTheme();
const colorblindStyle = settings.colorblindMode ? COLORBLIND_DIFF_STYLES[resolvedTheme] : null;
const queryClient = useQueryClient();
const createWorktreeMutation = useMutation(gitCreateWorktreeMutationOptions({ queryClient }));
const composerDraft = useComposerThreadDraft(threadId);
Expand Down Expand Up @@ -3655,6 +3657,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
onImageExpand={onExpandTimelineImage}
markdownCwd={gitCwd ?? undefined}
resolvedTheme={resolvedTheme}
colorblindStyle={colorblindStyle}
timestampFormat={timestampFormat}
workspaceRoot={activeProject?.cwd ?? undefined}
/>
Expand Down
46 changes: 29 additions & 17 deletions apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ import { readNativeApi } from "../nativeApi";
import { resolvePathLinkTarget } from "../terminal-links";
import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
import { useTheme } from "../hooks/useTheme";
import { buildPatchCacheKey } from "../lib/diffRendering";
import { resolveDiffThemeName } from "../lib/diffRendering";
import {
buildPatchCacheKey,
COLORBLIND_DIFF_STYLES,
resolveDiffThemeName,
} from "../lib/diffRendering";
import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
import { useStore } from "../store";
import { useSettings } from "../hooks/useSettings";
Expand All @@ -38,7 +41,12 @@ import { ToggleGroup, Toggle } from "./ui/toggle-group";
type DiffRenderMode = "stacked" | "split";
type DiffThemeType = "light" | "dark";

const DIFF_PANEL_UNSAFE_CSS = `
function buildDiffPanelCss(colorblind: boolean, theme: "light" | "dark"): string {
const cb = colorblind ? COLORBLIND_DIFF_STYLES[theme] : null;
const addition = cb ? cb.addition.color : "var(--success)";
const deletion = cb ? cb.deletion.color : "var(--destructive)";

return `
[data-diffs-header],
[data-diff],
[data-file],
Expand All @@ -55,23 +63,23 @@ const DIFF_PANEL_UNSAFE_CSS = `
--diffs-bg-separator-override: color-mix(in srgb, var(--background) 95%, var(--foreground));
--diffs-bg-buffer-override: color-mix(in srgb, var(--background) 90%, var(--foreground));

--diffs-bg-addition-override: color-mix(in srgb, var(--background) 92%, var(--success));
--diffs-bg-addition-number-override: color-mix(in srgb, var(--background) 88%, var(--success));
--diffs-bg-addition-hover-override: color-mix(in srgb, var(--background) 85%, var(--success));
--diffs-bg-addition-emphasis-override: color-mix(in srgb, var(--background) 80%, var(--success));

--diffs-bg-deletion-override: color-mix(in srgb, var(--background) 92%, var(--destructive));
--diffs-bg-deletion-number-override: color-mix(in srgb, var(--background) 88%, var(--destructive));
--diffs-bg-deletion-hover-override: color-mix(in srgb, var(--background) 85%, var(--destructive));
--diffs-bg-deletion-emphasis-override: color-mix(
in srgb,
var(--background) 80%,
var(--destructive)
);
--diffs-addition-color-override: ${addition};
--diffs-bg-addition-override: color-mix(in srgb, var(--background) 92%, ${addition});
--diffs-bg-addition-number-override: color-mix(in srgb, var(--background) 88%, ${addition});
--diffs-bg-addition-hover-override: color-mix(in srgb, var(--background) 85%, ${addition});
--diffs-bg-addition-emphasis-override: color-mix(in srgb, var(--background) 80%, ${addition});

--diffs-deletion-color-override: ${deletion};
--diffs-bg-deletion-override: color-mix(in srgb, var(--background) 92%, ${deletion});
--diffs-bg-deletion-number-override: color-mix(in srgb, var(--background) 88%, ${deletion});
--diffs-bg-deletion-hover-override: color-mix(in srgb, var(--background) 85%, ${deletion});
--diffs-bg-deletion-emphasis-override: color-mix(in srgb, var(--background) 80%, ${deletion});

background-color: var(--diffs-bg) !important;
}`;
}

const DIFF_PANEL_STATIC_CSS = `
[data-file-info] {
background-color: color-mix(in srgb, var(--card) 94%, var(--foreground)) !important;
border-block-color: var(--border) !important;
Expand Down Expand Up @@ -169,6 +177,10 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
const settings = useSettings();
const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");
const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap);
const diffUnsafeCss = useMemo(
() => buildDiffPanelCss(settings.colorblindMode, resolvedTheme) + DIFF_PANEL_STATIC_CSS,
[settings.colorblindMode, resolvedTheme],
);
const patchViewportRef = useRef<HTMLDivElement>(null);
const turnStripRef = useRef<HTMLDivElement>(null);
const previousDiffOpenRef = useRef(false);
Expand Down Expand Up @@ -615,7 +627,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
overflow: diffWordWrap ? "wrap" : "scroll",
theme: resolveDiffThemeName(resolvedTheme),
themeType: resolvedTheme as DiffThemeType,
unsafeCSS: DIFF_PANEL_UNSAFE_CSS,
unsafeCSS: diffUnsafeCss,
}}
/>
</div>
Expand Down
24 changes: 20 additions & 4 deletions apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ import {
import { randomUUID } from "~/lib/utils";
import { resolvePathLinkTarget } from "~/terminal-links";
import { readNativeApi } from "~/nativeApi";
import { useSettings } from "~/hooks/useSettings";
import { useTheme } from "~/hooks/useTheme";
import { COLORBLIND_DIFF_STYLES } from "~/lib/diffRendering";

interface GitActionsControlProps {
gitCwd: string | null;
Expand Down Expand Up @@ -204,6 +207,9 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {
}

export default function GitActionsControl({ gitCwd, activeThreadId }: GitActionsControlProps) {
const { colorblindMode } = useSettings();
const { resolvedTheme } = useTheme();
const cb = colorblindMode ? COLORBLIND_DIFF_STYLES[resolvedTheme] : null;
const threadToastData = useMemo(
() => (activeThreadId ? { threadId: activeThreadId } : undefined),
[activeThreadId],
Expand Down Expand Up @@ -970,9 +976,19 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
<span className="text-muted-foreground">Excluded</span>
) : (
<>
<span className="text-success">+{file.insertions}</span>
<span
className={cb ? undefined : "text-success"}
style={cb?.addition}
>
+{file.insertions}
</span>
<span className="text-muted-foreground"> / </span>
<span className="text-destructive">-{file.deletions}</span>
<span
className={cb ? undefined : "text-destructive"}
style={cb?.deletion}
>
-{file.deletions}
</span>
</>
)}
</span>
Expand All @@ -983,11 +999,11 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
</div>
</ScrollArea>
<div className="flex justify-end font-mono">
<span className="text-success">
<span className={cb ? undefined : "text-success"} style={cb?.addition}>
+{selectedFiles.reduce((sum, f) => sum + f.insertions, 0)}
</span>
<span className="text-muted-foreground"> / </span>
<span className="text-destructive">
<span className={cb ? undefined : "text-destructive"} style={cb?.deletion}>
-{selectedFiles.reduce((sum, f) => sum + f.deletions, 0)}
</span>
</div>
Expand Down
17 changes: 14 additions & 3 deletions apps/web/src/components/chat/ChangedFilesTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import { ChevronRightIcon, FolderIcon, FolderClosedIcon } from "lucide-react";
import { cn } from "~/lib/utils";
import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel";
import { VscodeEntryIcon } from "./VscodeEntryIcon";
import type { ColorblindDiffStyle } from "~/lib/diffRendering";

export const ChangedFilesTree = memo(function ChangedFilesTree(props: {
turnId: TurnId;
files: ReadonlyArray<TurnDiffFileChange>;
allDirectoriesExpanded: boolean;
resolvedTheme: "light" | "dark";
colorblindStyle?: ColorblindDiffStyle | null | undefined;
onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void;
}) {
const { files, allDirectoriesExpanded, onOpenTurnDiff, resolvedTheme, turnId } = props;
const { files, allDirectoriesExpanded, onOpenTurnDiff, resolvedTheme, colorblindStyle, turnId } =
props;
const treeNodes = useMemo(() => buildTurnDiffTree(files), [files]);
const directoryPathsKey = useMemo(
() => collectDirectoryPaths(treeNodes).join("\u0000"),
Expand Down Expand Up @@ -71,7 +74,11 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: {
</span>
{hasNonZeroStat(node.stat) && (
<span className="ml-auto shrink-0 font-mono text-[10px] tabular-nums">
<DiffStatLabel additions={node.stat.additions} deletions={node.stat.deletions} />
<DiffStatLabel
additions={node.stat.additions}
deletions={node.stat.deletions}
colorblindStyle={colorblindStyle}
/>
</span>
)}
</button>
Expand Down Expand Up @@ -104,7 +111,11 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: {
</span>
{node.stat && (
<span className="ml-auto shrink-0 font-mono text-[10px] tabular-nums">
<DiffStatLabel additions={node.stat.additions} deletions={node.stat.deletions} />
<DiffStatLabel
additions={node.stat.additions}
deletions={node.stat.deletions}
colorblindStyle={colorblindStyle}
/>
</span>
)}
</button>
Expand Down
17 changes: 14 additions & 3 deletions apps/web/src/components/chat/DiffStatLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { memo } from "react";
import type { ColorblindDiffStyle } from "~/lib/diffRendering";

export function hasNonZeroStat(stat: { additions: number; deletions: number }): boolean {
return stat.additions > 0 || stat.deletions > 0;
Expand All @@ -8,14 +9,24 @@ export const DiffStatLabel = memo(function DiffStatLabel(props: {
additions: number;
deletions: number;
showParentheses?: boolean;
colorblindStyle?: ColorblindDiffStyle | null | undefined;
}) {
const { additions, deletions, showParentheses = false } = props;
const { additions, deletions, showParentheses = false, colorblindStyle: cb } = props;

return (
<>
{showParentheses && <span className="text-muted-foreground/70">(</span>}
<span className="text-success">+{additions}</span>
{cb ? (
<span style={cb.addition}>+{additions}</span>
) : (
<span className="text-success">+{additions}</span>
)}
<span className="mx-0.5 text-muted-foreground/70">/</span>
<span className="text-destructive">-{deletions}</span>
{cb ? (
<span style={cb.deletion}>-{deletions}</span>
) : (
<span className="text-destructive">-{deletions}</span>
)}
{showParentheses && <span className="text-muted-foreground/70">)</span>}
</>
);
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { buildExpandedImagePreview, ExpandedImagePreview } from "./ExpandedImage
import { ProposedPlanCard } from "./ProposedPlanCard";
import { ChangedFilesTree } from "./ChangedFilesTree";
import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel";
import type { ColorblindDiffStyle } from "~/lib/diffRendering";
import { MessageCopyButton } from "./MessageCopyButton";
import { computeMessageDurationStart, normalizeCompactToolLabel } from "./MessagesTimeline.logic";
import { TerminalContextInlineChip } from "./TerminalContextInlineChip";
Expand Down Expand Up @@ -79,6 +80,7 @@ interface MessagesTimelineProps {
onImageExpand: (preview: ExpandedImagePreview) => void;
markdownCwd: string | undefined;
resolvedTheme: "light" | "dark";
colorblindStyle?: ColorblindDiffStyle | null | undefined;
timestampFormat: TimestampFormat;
workspaceRoot: string | undefined;
}
Expand All @@ -103,6 +105,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
onImageExpand,
markdownCwd,
resolvedTheme,
colorblindStyle,
timestampFormat,
workspaceRoot,
}: MessagesTimelineProps) {
Expand Down Expand Up @@ -474,6 +477,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
<DiffStatLabel
additions={summaryStat.additions}
deletions={summaryStat.deletions}
colorblindStyle={colorblindStyle}
/>
</>
)}
Expand Down Expand Up @@ -505,6 +509,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
files={checkpointFiles}
allDirectoriesExpanded={allDirectoriesExpanded}
resolvedTheme={resolvedTheme}
colorblindStyle={colorblindStyle}
onOpenTurnDiff={onOpenTurnDiff}
/>
</div>
Expand Down
28 changes: 28 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,9 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat
? ["Time format"]
: []),
...(settings.colorblindMode !== DEFAULT_UNIFIED_SETTINGS.colorblindMode
? ["Colorblind mode"]
: []),
...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap
? ["Diff line wrapping"]
: []),
Expand All @@ -480,6 +483,7 @@ export function useSettingsRestore(onRestored?: () => void) {
[
areProviderSettingsDirty,
isGitWritingModelDirty,
settings.colorblindMode,
settings.confirmThreadArchive,
settings.confirmThreadDelete,
settings.defaultThreadEnvMode,
Expand Down Expand Up @@ -837,6 +841,30 @@ export function GeneralSettingsPanel() {
}
/>

<SettingsRow
title="Colorblind mode"
description="Replace red/green with blue/orange across diffs and stats."
resetAction={
settings.colorblindMode !== DEFAULT_UNIFIED_SETTINGS.colorblindMode ? (
<SettingResetButton
label="colorblind mode"
onClick={() =>
updateSettings({
colorblindMode: DEFAULT_UNIFIED_SETTINGS.colorblindMode,
})
}
/>
) : null
}
control={
<Switch
checked={settings.colorblindMode}
onCheckedChange={(checked) => updateSettings({ colorblindMode: Boolean(checked) })}
aria-label="Enable colorblind mode"
/>
}
/>

<SettingsRow
title="Assistant output"
description="Show token-by-token output while a response is in progress."
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/hooks/useSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ describe("buildLegacyClientSettingsMigrationPatch", () => {
confirmThreadDelete: false,
});
});

it("does not migrate colorblindMode since it never existed in legacy settings", () => {
expect(buildLegacyClientSettingsMigrationPatch({ colorblindMode: true })).toEqual({});
});
});
17 changes: 17 additions & 0 deletions apps/web/src/lib/diffRendering.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
// GitHub Primer colorblind palette (protanopia/deuteranopia).
// Source: @primer/primitives diffBlob tokens.
// Light: addition #0969da (blue), deletion #bc4c00 (orange)
// Dark: addition #388bfd (blue), deletion #db6d28 (orange)
export const COLORBLIND_DIFF_STYLES = {
light: {
addition: { color: "#0969da" } as const,
deletion: { color: "#bc4c00" } as const,
},
dark: {
addition: { color: "#388bfd" } as const,
deletion: { color: "#db6d28" } as const,
},
} as const;

export type ColorblindDiffStyle = (typeof COLORBLIND_DIFF_STYLES)["light" | "dark"];

export const DIFF_THEME_NAMES = {
light: "pierre-light",
dark: "pierre-dark",
Expand Down
4 changes: 4 additions & 0 deletions packages/contracts/src/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ describe("DEFAULT_CLIENT_SETTINGS", () => {
it("includes archive confirmation with a false default", () => {
expect(DEFAULT_CLIENT_SETTINGS.confirmThreadArchive).toBe(false);
});

it("includes colorblind mode with a false default", () => {
expect(DEFAULT_CLIENT_SETTINGS.colorblindMode).toBe(false);
});
});
1 change: 1 addition & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type SidebarThreadSortOrder = typeof SidebarThreadSortOrder.Type;
export const DEFAULT_SIDEBAR_THREAD_SORT_ORDER: SidebarThreadSortOrder = "updated_at";

export const ClientSettingsSchema = Schema.Struct({
colorblindMode: Schema.Boolean.pipe(Schema.withDecodingDefault(() => false)),
confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(() => false)),
confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(() => true)),
diffWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(() => false)),
Expand Down
Loading