Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ const clientSettings: ClientSettings = {
confirmThreadArchive: true,
confirmThreadDelete: false,
dismissedProviderUpdateNotificationKeys: [],
diffColorScheme: "orange-blue",
diffIgnoreWhitespace: true,
diffIndicatorStyle: "classic",
environmentIdentificationMode: "artwork",
favorites: [],
fontFamilyCode: "",
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/components/chat/ChangedFilesTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: {
const scopeSummary = useMemo(() => summarizeChangedFileScopes(files), [files]);
const previewFiles = useMemo(() => selectChangedFilePreview(files), [files]);
const compactPreviewVisible = showCompactPreview && !expanded;

return (
<div
className="@container/changed-files mt-4 rounded-2xl border border-border/70 bg-secondary p-2 dark:border-transparent dark:bg-input/32"
Expand Down
10 changes: 8 additions & 2 deletions apps/web/src/components/chat/DiffStatLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,16 @@ export const DiffStatLabel = memo(function DiffStatLabel(props: {
className,
)}
>
<span aria-hidden="true" className="font-mono text-success">
<span
aria-hidden="true"
className="font-mono text-[var(--t3-diff-addition-color,var(--success))]"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Now that DiffAppearanceSync puts --t3-diff-* on <html>, this label and PullRequestDiffStat follow the setting — but the two hand-rolled +N / −M stats do not: Sidebar.tsx:1543-1544 (text-emerald-600 dark:text-emerald-400 / text-red-600 dark:text-red-400) and GitActionsControl.tsx:1939-1941 plus the totals at 1952-1957 (text-success / text-destructive). Before this PR every diff stat was consistently red/green; with Orange and blue selected the sidebar thread rows and the commit sheet stay green/red beside blue/orange stats everywhere else.

Smallest fix: swap those four class strings for text-[var(--t3-diff-addition-color,var(--success))] / text-[var(--t3-diff-deletion-color,var(--destructive))], or render DiffStatLabel at those call sites so the palette has one owner.

Posted via Macroscope — UI Consistency

>
+{formatCompactDiffCount(additions)}
</span>
<span aria-hidden="true" className="font-mono text-destructive">
<span
aria-hidden="true"
className="font-mono text-[var(--t3-diff-deletion-color,var(--destructive))]"
>
-{formatCompactDiffCount(deletions)}
</span>
</span>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
type ReactNode,
} from "react";
import { LegendList, type LegendListRef } from "@legendapp/list/react";
import { FileDiff } from "@pierre/diffs/react";
import {
deriveTimelineEntries,
workEntryIndicatesToolFailure,
Expand All @@ -45,6 +44,7 @@ import {
resolveFileDiffPath,
} from "../../lib/diffRendering";
import ChatMarkdown from "../ChatMarkdown";
import { StyledFileDiff } from "../diffs/StyledDiffCodeView";
import {
BotIcon,
CheckIcon,
Expand Down Expand Up @@ -1869,7 +1869,7 @@ function UserMessageReviewCommentCard({ comment }: { comment: ReviewCommentConte
)}
{renderablePatch?.kind === "files" &&
renderablePatch.files.map((fileDiff) => (
<FileDiff
<StyledFileDiff
key={resolveFileDiffPath(fileDiff)}
fileDiff={fileDiff}
options={{
Expand Down
42 changes: 40 additions & 2 deletions apps/web/src/components/diffs/StyledDiffCodeView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { renderToStaticMarkup } from "react-dom/server";
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
import type { FileDiffMetadata } from "@pierre/diffs";

const testState = vi.hoisted(() => ({
codeViewClassName: null as string | null,
codeViewOptions: null as Record<string, unknown> | null,
fileDiffClassName: null as string | null,
fileDiffOptions: null as Record<string, unknown> | null,
}));

vi.mock("@pierre/diffs/react", () => ({
Expand All @@ -12,14 +15,21 @@ vi.mock("@pierre/diffs/react", () => ({
testState.codeViewOptions = props.options;
return null;
},
FileDiff: (props: { className: string; options: Record<string, unknown> }) => {
testState.fileDiffClassName = props.className;
testState.fileDiffOptions = props.options;
return null;
},
}));

import { StyledDiffCodeView } from "./StyledDiffCodeView";
import { StyledDiffCodeView, StyledFileDiff } from "./StyledDiffCodeView";

describe("StyledDiffCodeView", () => {
beforeEach(() => {
testState.codeViewClassName = null;
testState.codeViewOptions = null;
testState.fileDiffClassName = null;
testState.fileDiffOptions = null;
});

it("always pairs the shared diff styling with its virtualized geometry", () => {
Expand All @@ -36,12 +46,15 @@ describe("StyledDiffCodeView", () => {
);

expect(testState.codeViewClassName).toBe(
"diff-render-surface [--code-background:var(--background)] outline-none min-h-0",
"diff-render-surface [--code-background:var(--background)] outline-none " +
"[--t3-diff-addition-color:var(--success)] " +
"[--t3-diff-deletion-color:var(--destructive)] min-h-0",
);
expect(testState.codeViewOptions).toMatchObject({
theme: "pierre-dark",
stickyHeaders: true,
loadDiffFiles,
diffIndicators: "bars",
itemMetrics: {
diffHeaderHeight: 32,
hunkSeparatorHeight: 24,
Expand All @@ -57,4 +70,29 @@ describe("StyledDiffCodeView", () => {
expect.stringContaining(")[data-expand-index]\n [data-unmodified-lines]"),
);
});

it("applies the shared appearance to standalone file diffs", () => {
const fileDiff = { name: "src/app.ts", hunks: [] } as unknown as FileDiffMetadata;
renderToStaticMarkup(
<StyledFileDiff
className="rounded-md"
fileDiff={fileDiff}
options={{ collapsed: false, theme: "pierre-dark" }}
/>,
);

expect(testState.fileDiffClassName).toBe(
"diff-render-surface [--code-background:var(--background)] outline-none " +
"[--t3-diff-addition-color:var(--success)] " +
"[--t3-diff-deletion-color:var(--destructive)] rounded-md",
);
expect(testState.fileDiffOptions).toMatchObject({
collapsed: false,
theme: "pierre-dark",
diffIndicators: "bars",
});
expect(testState.fileDiffOptions?.unsafeCSS).toEqual(
expect.stringContaining("--diffs-addition-base"),
);
});
});
48 changes: 41 additions & 7 deletions apps/web/src/components/diffs/StyledDiffCodeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import {
type CodeViewHandle,
type CodeViewProps,
type ControlledCodeViewProps,
FileDiff,
type FileDiffProps,
type UncontrolledCodeViewProps,
} from "@pierre/diffs/react";
/* oxlint-enable eslint/no-restricted-imports */
import type { Ref } from "react";
import type { DiffColorScheme } from "@t3tools/contracts/settings";

import { DIFF_SURFACE_THEME_UNSAFE_CSS } from "~/lib/diffRendering";
import { useClientSettings } from "~/hooks/useSettings";
import { DIFF_SURFACE_THEME_UNSAFE_CSS, getDiffColorSchemeClassName } from "~/lib/diffRendering";

const DIFF_VIEW_UNSAFE_CSS = `${DIFF_SURFACE_THEME_UNSAFE_CSS}
:is(
Expand Down Expand Up @@ -258,9 +262,13 @@ const DIFF_VIEW_UNSAFE_CSS = `${DIFF_SURFACE_THEME_UNSAFE_CSS}
}
`;

function getDiffSurfaceClassName(colorScheme: DiffColorScheme, className?: string): string {
return `diff-render-surface [--code-background:var(--background)] outline-none ${getDiffColorSchemeClassName(colorScheme)}${className ? ` ${className}` : ""}`;
}

export type StyledDiffCodeViewOptions<LAnnotation> = Omit<
NonNullable<CodeViewProps<LAnnotation>["options"]>,
"unsafeCSS" | "itemMetrics" | "layout"
"unsafeCSS" | "itemMetrics" | "layout" | "diffIndicators"
>;

type StyledDiffCodeViewProps<LAnnotation> = (
Expand All @@ -284,19 +292,18 @@ export function StyledDiffCodeView<LAnnotation = undefined>({
unsafeCSSExtra,
...props
}: StyledDiffCodeViewProps<LAnnotation>) {
const { diffColorScheme, diffIndicatorStyle } = useClientSettings();

return (
<CodeView<LAnnotation>
{...props}
{...(viewerRef ? { ref: viewerRef } : {})}
// The custom element itself is focusable for keyboard scrolling. Its native outline sits
// outside the panel clipping boundary; actual controls inside retain their own indicators.
className={
className
? `diff-render-surface [--code-background:var(--background)] outline-none ${className}`
: "diff-render-surface [--code-background:var(--background)] outline-none"
}
className={getDiffSurfaceClassName(diffColorScheme, className)}
options={{
...options,
diffIndicators: diffIndicatorStyle,
unsafeCSS: unsafeCSSExtra
? `${DIFF_VIEW_UNSAFE_CSS}\n${unsafeCSSExtra}`
: DIFF_VIEW_UNSAFE_CSS,
Expand All @@ -320,3 +327,30 @@ export function StyledDiffCodeView<LAnnotation = undefined>({
/>
);
}

type StyledFileDiffProps<LAnnotation> = Omit<FileDiffProps<LAnnotation>, "options"> & {
readonly options?: Omit<
NonNullable<FileDiffProps<LAnnotation>["options"]>,
"unsafeCSS" | "diffIndicators"
>;
};

export function StyledFileDiff<LAnnotation = undefined>({
options,
className,
...props
}: StyledFileDiffProps<LAnnotation>) {
const { diffColorScheme, diffIndicatorStyle } = useClientSettings();

return (
<FileDiff<LAnnotation>
{...props}
className={getDiffSurfaceClassName(diffColorScheme, className)}
options={{
...options,
diffIndicators: diffIndicatorStyle,
unsafeCSS: DIFF_SURFACE_THEME_UNSAFE_CSS,
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -384,10 +384,12 @@ export function PullRequestDiffStat({
}
return (
<span className={cn("inline-flex items-baseline gap-1 tabular-nums", className)}>
<span className="text-emerald-600 dark:text-emerald-300/90">
<span className="text-[var(--t3-diff-addition-color,var(--success))]">
Comment thread
SunkenInTime marked this conversation as resolved.
+{additions.toLocaleString()}
</span>
<span className="text-destructive">-{deletions.toLocaleString()}</span>
<span className="text-[var(--t3-diff-deletion-color,var(--destructive))]">
-{deletions.toLocaleString()}
</span>
Comment thread
cursor[bot] marked this conversation as resolved.
</span>
);
}
Expand Down
68 changes: 33 additions & 35 deletions apps/web/src/components/settings/SettingsFontPreviews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "../ComposerPromptEditor";
import { terminalThemeFromApp } from "../ThreadTerminalDrawer";
import { useTheme } from "../../hooks/useTheme";
import { useClientSettings } from "../../hooks/useSettings";
import { DISCONNECTED_COMPOSER_PLACEHOLDER } from "../../composerPlaceholder";
import { resolveDiffThemeName, type DiffThemeName } from "../../lib/diffRendering";
import {
DIFF_SURFACE_THEME_UNSAFE_CSS,
getDiffColorSchemeClassName,
resolveDiffThemeName,
type DiffThemeName,
} from "../../lib/diffRendering";
import type { DiffIndicatorStyle } from "@t3tools/contracts/settings";
import { GhosttyTerminalSurface } from "~/terminal/ghostty/surface";

// The font previews are the real surfaces, not lookalikes: the composer's
Expand Down Expand Up @@ -72,73 +79,64 @@ const DIFF_PREVIEW_PATCH = [
// (toggling Advanced) and lock in an unhighlighted frame; a static preview
// needs none of that lifecycle, so it uses the deterministic renderer and
// injects the finished HTML into a shadow root, exactly as FileDiff would.
const diffPreviewHtmlByTheme = new Map<DiffThemeName, Promise<readonly string[]>>();
const diffPreviewHtml = new Map<string, Promise<readonly string[]>>();

function loadDiffPreviewHtml(theme: DiffThemeName): Promise<readonly string[]> {
let promise = diffPreviewHtmlByTheme.get(theme);
function loadDiffPreviewHtml(
theme: DiffThemeName,
diffIndicators: DiffIndicatorStyle,
): Promise<readonly string[]> {
const key = `${theme}:${diffIndicators}`;
let promise = diffPreviewHtml.get(key);
if (promise === undefined) {
promise = preloadPatchFile({
patch: DIFF_PREVIEW_PATCH,
options: { diffStyle: "unified", theme },
options: {
diffIndicators,
diffStyle: "unified",
theme,
unsafeCSS: DIFF_SURFACE_THEME_UNSAFE_CSS,
},
}).then((results) => results.map((result) => result.prerenderedHTML));
diffPreviewHtmlByTheme.set(theme, promise);
diffPreviewHtml.set(key, promise);
}
return promise;
}

// Pierre's prerendered stylesheet bakes its own light/dark surface colors
// into the shadow root's @layer rules. These unlayered rules win the cascade
// without !important and re-point the surfaces at the app's code tokens
// (custom properties inherit across the shadow boundary), so the preview
// follows the active theme exactly like the real diff panel does.
const DIFF_PREVIEW_THEME_BRIDGE = `
:host {
color: var(--code-foreground);
background-color: var(--code-background);
--diffs-fg: var(--code-foreground);
--diffs-bg: var(--code-background);
--diffs-light-bg: var(--code-background);
--diffs-dark-bg: var(--code-background);
}
[data-diffs-header] {
background-color: var(--code-background);
color: var(--code-foreground);
}
`;

function StaticDiffHtml({ html }: { html: string }) {
function StaticDiffHtml({ html, className }: { html: string; className: string }) {
const hostRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const host = hostRef.current;
if (host === null) return;
const shadow = host.shadowRoot ?? host.attachShadow({ mode: "open" });
shadow.innerHTML = html;
const bridge = document.createElement("style");
bridge.textContent = DIFF_PREVIEW_THEME_BRIDGE;
shadow.append(bridge);
}, [html]);
return <div ref={hostRef} />;
return <div className={className} ref={hostRef} />;
}

/** The diff panel's file diff, statically rendered by its real pipeline. */
export function CodeFontPreview() {
export function DiffPreview() {
const { resolvedTheme } = useTheme();
const { diffColorScheme, diffIndicatorStyle } = useClientSettings();
const themeName = resolveDiffThemeName(resolvedTheme);
const [htmlByFile, setHtmlByFile] = useState<readonly string[] | null>(null);
useEffect(() => {
let cancelled = false;
void loadDiffPreviewHtml(themeName).then((html) => {
void loadDiffPreviewHtml(themeName, diffIndicatorStyle).then((html) => {
if (!cancelled) setHtmlByFile(html);
});
return () => {
cancelled = true;
};
}, [themeName]);
}, [diffIndicatorStyle, themeName]);
if (htmlByFile === null) return null;
return (
<div className="mt-1 mb-2 space-y-2">
{htmlByFile.map((html) => (
<StaticDiffHtml key={html} html={html} />
<StaticDiffHtml
className={`[--code-background:var(--background)] ${getDiffColorSchemeClassName(diffColorScheme)}`}
key={html}
html={html}
/>
))}
</div>
);
Expand Down
Loading
Loading