diff --git a/apps/fabro-web/app/routes/run-files.render.test.tsx b/apps/fabro-web/app/routes/run-files.render.test.tsx
index 7dfa040cf..1bd46ea3d 100644
--- a/apps/fabro-web/app/routes/run-files.render.test.tsx
+++ b/apps/fabro-web/app/routes/run-files.render.test.tsx
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, mock, test } from "bun:test";
+import { useRef } from "react";
import TestRenderer, { act } from "react-test-renderer";
import { MemoryRouter, Route, Routes } from "react-router";
@@ -10,6 +11,7 @@ const useRunFilesCalls: any[] = [];
const multiFileDiffCalls: any[] = [];
const patchDiffCalls: any[] = [];
+let patchDiffMountSeq = 0;
const virtualizerCalls: any[] = [];
const providerCalls: any[] = [];
const mountedRenderers: TestRenderer.ReactTestRenderer[] = [];
@@ -20,8 +22,13 @@ mock.module("@pierre/diffs/react", () => ({
return
{props.newFile.name}
;
},
PatchDiff: (props: any) => {
- patchDiffCalls.push(props);
- return {props.patch}
;
+ const mountId = useRef(++patchDiffMountSeq);
+ patchDiffCalls.push({ ...props, mountId: mountId.current });
+ return (
+
+ {props.patch}
+
+ );
},
Virtualizer: (props: any) => {
virtualizerCalls.push(props);
@@ -89,19 +96,46 @@ function makePayload(count: number, source = "sandbox") {
};
}
+function makePatchPayload(patch: string) {
+ return {
+ data: [
+ {
+ change_kind: "modified",
+ old_file: { name: "docs/live.md", contents: null },
+ new_file: { name: "docs/live.md", contents: null },
+ unified_patch: patch,
+ },
+ ],
+ source: "sandbox",
+ meta: {
+ degraded: false,
+ degraded_reason: null,
+ total_changed: 1,
+ stats: { additions: 1, deletions: 0 },
+ truncated: false,
+ to_sha: "abc1234",
+ to_sha_committed_at: "2026-05-05T12:00:00Z",
+ },
+ };
+}
+
+function runFilesTree(initialEntry = "/runs/run_1/files") {
+ return (
+
+
+
+ } />
+
+
+
+ );
+}
+
function renderRunFiles(initialEntry = "/runs/run_1/files") {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
let renderer: TestRenderer.ReactTestRenderer | undefined;
act(() => {
- renderer = TestRenderer.create(
-
-
-
- } />
-
-
- ,
- );
+ renderer = TestRenderer.create(runFilesTree(initialEntry));
});
mountedRenderers.push(renderer!);
return renderer!;
@@ -118,6 +152,7 @@ describe("RunFiles rendering", () => {
currentRunStatus = "succeeded";
multiFileDiffCalls.length = 0;
patchDiffCalls.length = 0;
+ patchDiffMountSeq = 0;
virtualizerCalls.length = 0;
providerCalls.length = 0;
useRunFilesCalls.length = 0;
@@ -147,7 +182,7 @@ describe("RunFiles rendering", () => {
const sandboxRenderer = renderRunFiles();
expect(
sandboxRenderer.root.findAllByProps({ "aria-label": "Diff scope" }),
- ).toHaveLength(1);
+ ).not.toHaveLength(0);
act(() => sandboxRenderer.unmount());
mountedRenderers.pop();
@@ -176,15 +211,7 @@ describe("RunFiles rendering", () => {
const firstNewKey = multiFileDiffCalls[0].newFile.cacheKey;
act(() => {
- renderer.update(
-
-
-
- } />
-
-
- ,
- );
+ renderer.update(runFilesTree());
});
const lastCall = multiFileDiffCalls[multiFileDiffCalls.length - 1];
@@ -194,4 +221,24 @@ describe("RunFiles rendering", () => {
expect(firstNewKey).toContain("fabro-run-file:run_1:abc1234:new:src/file-0.ts:");
expect(lastCall.options).not.toHaveProperty("theme");
});
+
+ test("remounts patch diffs when the patch changes for the same file", () => {
+ currentFilesPayload = makePatchPayload(
+ "diff --git a/docs/live.md b/docs/live.md\n@@ -1 +1 @@\n+committed\n",
+ );
+
+ const renderer = renderRunFiles("/runs/run_1/files?scope=all");
+ const firstMountId = patchDiffCalls[0].mountId;
+
+ currentFilesPayload = makePatchPayload(
+ "diff --git a/docs/live.md b/docs/live.md\n@@ -1,0 +1,2 @@\n+committed\n+uncommitted\n",
+ );
+ act(() => {
+ renderer.update(runFilesTree("/runs/run_1/files?scope=all"));
+ });
+
+ const lastCall = patchDiffCalls[patchDiffCalls.length - 1];
+ expect(lastCall.patch).toContain("+uncommitted");
+ expect(lastCall.mountId).not.toBe(firstMountId);
+ });
});
diff --git a/apps/fabro-web/app/routes/run-files.tsx b/apps/fabro-web/app/routes/run-files.tsx
index 75736a929..01ad8b1e5 100644
--- a/apps/fabro-web/app/routes/run-files.tsx
+++ b/apps/fabro-web/app/routes/run-files.tsx
@@ -35,7 +35,7 @@ import {
} from "./run-files/states";
import { useFileKeyboardNav } from "./run-files/keyboard";
import { Toolbar, type DiffStyle } from "./run-files/toolbar";
-import { fileCacheKey } from "./run-files/cache-keys";
+import { fileCacheKey, stringHash } from "./run-files/cache-keys";
import { VirtualizedDiffList } from "./run-files/virtualized-diff-list";
import { ApiError, extractRequestId } from "../lib/api-client";
import { useRun, useRunFiles } from "../lib/queries";
@@ -211,6 +211,33 @@ interface RunFileRowProps {
toSha: string | null | undefined;
}
+function fileDiffRenderKey({
+ file,
+ index,
+ scope,
+ toSha,
+}: {
+ file: ApiFileDiff;
+ index: number;
+ scope: RunFileScope;
+ toSha: string | null | undefined;
+}): string {
+ const display = file.new_file.name || file.old_file.name || `file-${index}`;
+ const oldContents = file.old_file.contents ?? "";
+ const newContents = file.new_file.contents ?? "";
+ const contentFingerprint = file.unified_patch
+ ? `patch:${stringHash(file.unified_patch)}`
+ : `contents:${stringHash(oldContents)}:${stringHash(newContents)}`;
+ return [
+ scope,
+ toSha ?? "no-sha",
+ display,
+ index,
+ file.change_kind ?? "modified",
+ contentFingerprint,
+ ].join(":");
+}
+
const RunFileRow = memo(function RunFileRow({
file,
diffStyle,
@@ -280,7 +307,13 @@ const RunFileRow = memo(function RunFileRow({
/>
);
} else if (patch) {
- body = ;
+ body = (
+
+ );
}
return (
@@ -564,7 +597,12 @@ export default function RunFiles() {
(file.new_file.name === hashFile || file.old_file.name === hashFile);
return (