Skip to content
Merged
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
113 changes: 1 addition & 112 deletions apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { buildPatchCacheKey } from "../lib/diffRendering";
import {
expandDiffFile,
reconcileDiffFileReviewState,
setDiffFileContextMode,
toggleDiffFileAccepted,
toggleDiffFileCollapsed,
type DiffFileReviewStateByPath,
Expand Down Expand Up @@ -168,92 +167,33 @@ function summarizeFileDiffStats(fileDiff: FileDiffMetadata): {
);
}

function resolveRenderableFileDiff(
renderablePatch: RenderablePatch | null,
filePath: string,
): FileDiffMetadata | null {
if (!renderablePatch || renderablePatch.kind !== "files") {
return null;
}
return (
renderablePatch.files.find((candidate) => resolveFileDiffPath(candidate) === filePath) ?? null
);
}

interface FileScopedCheckpointDiffInput {
threadId: ThreadId | null;
fromTurnCount: number | null;
toTurnCount: number | null;
cacheScope?: string | null;
enabled: boolean;
}

function DiffFileSection(props: {
fileDiff: FileDiffMetadata;
filePath: string;
fileKey: string;
checkpointDiffInput: FileScopedCheckpointDiffInput;
diffRenderMode: DiffRenderMode;
diffWordWrap: boolean;
resolvedTheme: "light" | "dark";
collapsed: boolean;
accepted: boolean;
contextMode: "patch" | "full";
onOpenInEditor: (filePath: string) => void;
onToggleCollapsed: (filePath: string) => void;
onToggleAccepted: (filePath: string) => void;
onContextModeChange: (filePath: string, contextMode: "patch" | "full") => void;
}) {
const {
accepted,
checkpointDiffInput,
collapsed,
contextMode,
diffRenderMode,
diffWordWrap,
fileDiff,
fileKey,
filePath,
onContextModeChange,
onOpenInEditor,
onToggleAccepted,
onToggleCollapsed,
resolvedTheme,
} = props;
const stats = summarizeFileDiffStats(fileDiff);
const fullContextDiffQuery = useQuery(
checkpointDiffQueryOptions({
...checkpointDiffInput,
relativePath: filePath,
contextMode: "full",
enabled: checkpointDiffInput.enabled && !collapsed && contextMode === "full",
}),
);
const fullContextPatch = useMemo(
() =>
getRenderablePatch(
contextMode === "full" ? fullContextDiffQuery.data?.diff : undefined,
`diff-panel:file:${resolvedTheme}:${filePath}:full`,
),
[contextMode, filePath, fullContextDiffQuery.data?.diff, resolvedTheme],
);
const fullContextFileDiff =
contextMode === "full" ? resolveRenderableFileDiff(fullContextPatch, filePath) : null;
const resolvedFileDiff = contextMode === "full" ? (fullContextFileDiff ?? fileDiff) : fileDiff;
const fullContextError =
contextMode === "full" && fullContextDiffQuery.error
? fullContextDiffQuery.error instanceof Error
? fullContextDiffQuery.error.message
: "Failed to load full-file context."
: null;
const fullContextFallbackMessage =
contextMode === "full" &&
!fullContextError &&
!fullContextDiffQuery.isLoading &&
fullContextDiffQuery.data &&
fullContextFileDiff === null
? "Full-file context is unavailable for this file. Showing patch context."
: null;

return (
<section
Expand Down Expand Up @@ -289,25 +229,6 @@ function DiffFileSection(props: {
<DiffStatLabel additions={stats.additions} deletions={stats.deletions} />
</span>
)}
<ToggleGroup
className="shrink-0"
variant="outline"
size="xs"
value={[contextMode]}
onValueChange={(value) => {
const next = value[0];
if (next === "patch" || next === "full") {
onContextModeChange(filePath, next);
}
}}
>
<Toggle aria-label={`Show patch diff for ${filePath}`} value="patch">
Patch
</Toggle>
<Toggle aria-label={`Show full file context for ${filePath}`} value="full">
Full
</Toggle>
</ToggleGroup>
<Button
size="xs"
variant={accepted ? "secondary" : "outline"}
Expand All @@ -323,21 +244,8 @@ function DiffFileSection(props: {
</div>
{!collapsed && (
<div key={fileKey}>
{contextMode === "full" && fullContextDiffQuery.isLoading ? (
<DiffPanelLoadingState label="Loading full file..." />
) : null}
{fullContextError ? (
<div className="border-b border-border/60 bg-destructive/8 px-3 py-2 text-[11px] text-destructive/80">
{fullContextError}
</div>
) : null}
{fullContextFallbackMessage ? (
<div className="border-b border-border/60 bg-amber-500/8 px-3 py-2 text-[11px] text-amber-700 dark:text-amber-300/90">
{fullContextFallbackMessage}
</div>
) : null}
<FileDiff
fileDiff={resolvedFileDiff}
fileDiff={fileDiff}
options={{
diffStyle: diffRenderMode === "split" ? "split" : "unified",
lineDiffType: "none",
Expand Down Expand Up @@ -601,13 +509,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
},
[updateActiveReviewState],
);
const onChangeFileContextMode = useCallback(
(filePath: string, contextMode: "patch" | "full") => {
updateActiveReviewState((current) => setDiffFileContextMode(current, filePath, contextMode));
},
[updateActiveReviewState],
);

const latestSelectedTurnId = orderedTurnDiffSummaries[0]?.turnId ?? null;

const selectTurn = useCallback(
Expand Down Expand Up @@ -791,29 +692,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
const fileReviewState = activeReviewState[filePath] ?? {
accepted: false,
collapsed: true,
contextMode: "patch" as const,
};
return (
<DiffFileSection
key={themedFileKey}
accepted={fileReviewState.accepted}
checkpointDiffInput={{
threadId: activeThreadId,
fromTurnCount: activeCheckpointRange?.fromTurnCount ?? null,
toTurnCount: activeCheckpointRange?.toTurnCount ?? null,
cacheScope: selectedTurn
? `turn:${selectedTurn.turnId}`
: conversationCacheScope,
enabled: isGitRepo,
}}
collapsed={fileReviewState.collapsed}
contextMode={fileReviewState.contextMode}
diffRenderMode={diffRenderMode}
diffWordWrap={diffWordWrap}
fileDiff={fileDiff}
fileKey={themedFileKey}
filePath={filePath}
onContextModeChange={onChangeFileContextMode}
onOpenInEditor={openDiffFileInCodeViewer}
onToggleAccepted={onToggleFileAccepted}
onToggleCollapsed={onToggleFileCollapsed}
Expand Down
57 changes: 13 additions & 44 deletions apps/web/src/lib/diffFileReviewState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest";
import {
expandDiffFile,
reconcileDiffFileReviewState,
setDiffFileContextMode,
toggleDiffFileAccepted,
toggleDiffFileCollapsed,
} from "./diffFileReviewState";
Expand All @@ -11,38 +10,38 @@ describe("reconcileDiffFileReviewState", () => {
it("preserves existing state for known files and drops removed files", () => {
expect(
reconcileDiffFileReviewState(["src/a.ts"], {
"src/a.ts": { accepted: true, collapsed: true, contextMode: "full" },
"src/b.ts": { accepted: false, collapsed: true, contextMode: "patch" },
"src/a.ts": { accepted: true, collapsed: true },
"src/b.ts": { accepted: false, collapsed: true },
}),
).toEqual({
"src/a.ts": { accepted: true, collapsed: true, contextMode: "full" },
"src/a.ts": { accepted: true, collapsed: true },
});
});

it("initializes new files as unaccepted and collapsed", () => {
expect(reconcileDiffFileReviewState(["src/a.ts"], undefined)).toEqual({
"src/a.ts": { accepted: false, collapsed: true, contextMode: "patch" },
"src/a.ts": { accepted: false, collapsed: true },
});
});
});

describe("toggleDiffFileAccepted", () => {
it("marks a file accepted and collapses it", () => {
expect(toggleDiffFileAccepted({}, "src/a.ts")).toEqual({
"src/a.ts": { accepted: true, collapsed: true, contextMode: "patch" },
"src/a.ts": { accepted: true, collapsed: true },
});
});

it("clears acceptance and re-expands the file", () => {
expect(
toggleDiffFileAccepted(
{
"src/a.ts": { accepted: true, collapsed: true, contextMode: "full" },
"src/a.ts": { accepted: true, collapsed: true },
},
"src/a.ts",
),
).toEqual({
"src/a.ts": { accepted: false, collapsed: false, contextMode: "full" },
"src/a.ts": { accepted: false, collapsed: false },
});
});
});
Expand All @@ -52,42 +51,12 @@ describe("toggleDiffFileCollapsed", () => {
expect(
toggleDiffFileCollapsed(
{
"src/a.ts": { accepted: true, collapsed: true, contextMode: "full" },
"src/a.ts": { accepted: true, collapsed: true },
},
"src/a.ts",
),
).toEqual({
"src/a.ts": { accepted: true, collapsed: false, contextMode: "full" },
});
});
});

describe("setDiffFileContextMode", () => {
it("updates the file context mode without changing other state", () => {
expect(
setDiffFileContextMode(
{
"src/a.ts": { accepted: true, collapsed: false, contextMode: "patch" },
},
"src/a.ts",
"full",
),
).toEqual({
"src/a.ts": { accepted: true, collapsed: false, contextMode: "full" },
});
});

it("auto-expands a file when switching to full context", () => {
expect(
setDiffFileContextMode(
{
"src/a.ts": { accepted: false, collapsed: true, contextMode: "patch" },
},
"src/a.ts",
"full",
),
).toEqual({
"src/a.ts": { accepted: false, collapsed: false, contextMode: "full" },
"src/a.ts": { accepted: true, collapsed: false },
});
});
});
Expand All @@ -97,19 +66,19 @@ describe("expandDiffFile", () => {
expect(
expandDiffFile(
{
"src/a.ts": { accepted: true, collapsed: true, contextMode: "patch" },
"src/a.ts": { accepted: true, collapsed: true },
},
"src/a.ts",
),
).toEqual({
"src/a.ts": { accepted: true, collapsed: false, contextMode: "patch" },
"src/a.ts": { accepted: true, collapsed: false },
});
});

it("returns the same object when the file is already expanded", () => {
const state = {
"src/a.ts": { accepted: false, collapsed: false, contextMode: "patch" as const },
} satisfies Record<string, { accepted: boolean; collapsed: boolean; contextMode: "patch" }>;
"src/a.ts": { accepted: false, collapsed: false },
} satisfies Record<string, { accepted: boolean; collapsed: boolean }>;
// File is already expanded, so the same object reference is returned.
expect(expandDiffFile(state, "src/a.ts")).toBe(state);
});
Expand Down
22 changes: 0 additions & 22 deletions apps/web/src/lib/diffFileReviewState.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
export interface DiffFileReviewState {
collapsed: boolean;
accepted: boolean;
contextMode: "patch" | "full";
}

export type DiffFileReviewStateByPath = Record<string, DiffFileReviewState>;

const DEFAULT_DIFF_FILE_REVIEW_STATE: DiffFileReviewState = {
collapsed: true,
accepted: false,
contextMode: "patch",
};

export function reconcileDiffFileReviewState(
Expand All @@ -34,7 +32,6 @@ export function toggleDiffFileAccepted(
[path]: {
accepted,
collapsed: accepted,
contextMode: previous.contextMode,
},
};
}
Expand All @@ -53,25 +50,6 @@ export function toggleDiffFileCollapsed(
};
}

export function setDiffFileContextMode(
current: DiffFileReviewStateByPath,
path: string,
contextMode: "patch" | "full",
): DiffFileReviewStateByPath {
const previous = current[path] ?? DEFAULT_DIFF_FILE_REVIEW_STATE;
if (previous.contextMode === contextMode) {
return current;
}
return {
...current,
[path]: {
...previous,
collapsed: contextMode === "full" ? false : previous.collapsed,
contextMode,
},
};
}

export function expandDiffFile(
current: DiffFileReviewStateByPath,
path: string,
Expand Down
Loading
Loading