diff --git a/services/hackbot-ui/app/globals.css b/services/hackbot-ui/app/globals.css
index cca871a241..c785f9db87 100644
--- a/services/hackbot-ui/app/globals.css
+++ b/services/hackbot-ui/app/globals.css
@@ -811,3 +811,32 @@ ul.chips li.chip {
.test-plan-summary {
margin-top: 24px;
}
+.patch-file {
+ margin-bottom: 20px;
+ overflow-x: auto;
+}
+.patch-file:last-child {
+ margin-bottom: 0;
+}
+.patch-file-header {
+ padding: 8px 12px;
+ margin-bottom: 8px;
+ background: var(--panel-2);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-family: var(--mono);
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text);
+ word-break: break-all;
+ overflow-wrap: anywhere;
+}
+.patch-diff {
+ --diff-background-color: var(--bg);
+ --diff-text-color: var(--text);
+ --diff-selection-background-color: rgba(74, 122, 240, 0.3);
+ --diff-gutter-insert-background-color: rgba(46, 160, 67, 0.18);
+ --diff-gutter-delete-background-color: rgba(229, 83, 75, 0.18);
+ --diff-code-insert-background-color: rgba(46, 160, 67, 0.1);
+ --diff-code-delete-background-color: rgba(229, 83, 75, 0.1);
+}
diff --git a/services/hackbot-ui/app/layout.tsx b/services/hackbot-ui/app/layout.tsx
index 6a6202b021..b34b7ebd2c 100644
--- a/services/hackbot-ui/app/layout.tsx
+++ b/services/hackbot-ui/app/layout.tsx
@@ -1,7 +1,7 @@
import type { Metadata } from "next";
import Link from "next/link";
-import "diff2html/bundles/css/diff2html.min.css";
+import "react-diff-view/style/index.css";
import "./globals.css";
import { SentryUser } from "@/components/SentryUser";
diff --git a/services/hackbot-ui/components/PatchView.tsx b/services/hackbot-ui/components/PatchView.tsx
index c62cb85d27..1a34a523ba 100644
--- a/services/hackbot-ui/components/PatchView.tsx
+++ b/services/hackbot-ui/components/PatchView.tsx
@@ -1,7 +1,6 @@
"use client";
-import { html, parse } from "diff2html";
-import { ColorSchemeType } from "diff2html/lib/types";
+import { parseDiff, Diff, Hunk } from "react-diff-view";
import { useEffect, useState } from "react";
// The artifact every agent that edits source publishes (see
@@ -19,7 +18,7 @@ export function PatchPanel({
diff: string;
truncated?: boolean;
}) {
- const files = parse(diff);
+ const files = parseDiff(diff);
return (
@@ -36,18 +35,32 @@ export function PatchPanel({
from the Artifacts list for the rest.
)}
- {/* diff2html only emits markup for the diff it was given and escapes
- its content, so this does not render arbitrary agent HTML. */}
-
1,
- matching: "lines",
- colorScheme: ColorSchemeType.DARK,
- }),
- }}
- />
+
+ {files.map((file) => (
+
+ {/* Only label files when there's more than one to tell apart. */}
+ {files.length > 1 && (
+
+ {/* A deleted file's newPath is /dev/null; fall back to
+ oldPath so the header shows the actual filename. */}
+ {file.newPath !== "/dev/null" ? file.newPath : file.oldPath}
+
+ )}
+
+ {(hunks) =>
+ hunks.map((hunk) => )
+ }
+
+
+ ))}
+
>
)}
diff --git a/services/hackbot-ui/package-lock.json b/services/hackbot-ui/package-lock.json
index 360a74794b..10ac2d4f77 100644
--- a/services/hackbot-ui/package-lock.json
+++ b/services/hackbot-ui/package-lock.json
@@ -10,9 +10,9 @@
"dependencies": {
"@sentry/nextjs": "^10.74.0",
"better-auth": "^1.6.22",
- "diff2html": "^3.4.56",
"next": "^15.5.22",
"react": "^19.0.0",
+ "react-diff-view": "^3.3.3",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
@@ -1662,18 +1662,6 @@
"node": ">=14"
}
},
- "node_modules/@profoundlogic/hogan": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@profoundlogic/hogan/-/hogan-3.0.4.tgz",
- "integrity": "sha512-pmNVGuooS30Mm7YbZd5T7E5zYVO6D5Ct91sn4T39mUvMUc3sCGridcnhAufL1/Bz2QzAtzEn0agNrdk3+5yWzw==",
- "license": "Apache-2.0",
- "dependencies": {
- "nopt": "1.0.10"
- },
- "bin": {
- "hulk": "bin/hulk"
- }
- },
"node_modules/@rollup/plugin-commonjs": {
"version": "28.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz",
@@ -2818,12 +2806,6 @@
"license": "Apache-2.0",
"peer": true
},
- "node_modules/abbrev": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
- "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
- "license": "ISC"
- },
"node_modules/acorn": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
@@ -3191,6 +3173,11 @@
"integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==",
"license": "MIT"
},
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -3300,30 +3287,10 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/diff": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz",
- "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.3.1"
- }
- },
- "node_modules/diff2html": {
- "version": "3.4.56",
- "resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.56.tgz",
- "integrity": "sha512-u9gfn+BlbHcyO7vItCIC4z49LJDUt31tODzOfAuJ5R1E7IdlRL6KjugcB9zOpejD+XiR+dDZbsnHSQ3g6A/u8A==",
- "license": "MIT",
- "dependencies": {
- "@profoundlogic/hogan": "^3.0.4",
- "diff": "^8.0.3"
- },
- "engines": {
- "node": ">=12"
- },
- "optionalDependencies": {
- "highlight.js": "11.11.1"
- }
+ "node_modules/diff-match-patch": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
+ "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
},
"node_modules/dotenv": {
"version": "16.6.1",
@@ -3538,6 +3505,11 @@
"node": ">=6.9.0"
}
},
+ "node_modules/gitdiff-parser": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/gitdiff-parser/-/gitdiff-parser-0.3.1.tgz",
+ "integrity": "sha512-YQJnY8aew65id8okGxKCksH3efDCJ9HzV7M9rsvd65habf39Pkh4cgYJ27AaoDMqo1X98pgNJhNMrm/kpV7UVQ=="
+ },
"node_modules/glob": {
"version": "13.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
@@ -3612,16 +3584,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/highlight.js": {
- "version": "11.11.1",
- "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
- "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
- "license": "BSD-3-Clause",
- "optional": true,
- "engines": {
- "node": ">=12.0.0"
- }
- },
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -3821,6 +3783,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="
+ },
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -3831,6 +3798,17 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -4939,21 +4917,6 @@
"node": ">=18"
}
},
- "node_modules/nopt": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
- "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
- "license": "MIT",
- "dependencies": {
- "abbrev": "1"
- },
- "bin": {
- "nopt": "bin/nopt.js"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -5123,6 +5086,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-diff-view": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/react-diff-view/-/react-diff-view-3.3.3.tgz",
+ "integrity": "sha512-CPveApk6n7ZbkW7T6PoptR7LWAvD9hohTHZ7WnKnu3GZkTfUB5rvg486apPo94iYVi4fZd3Nt+rtBZ5877exoQ==",
+ "dependencies": {
+ "classnames": "^2.3.2",
+ "diff-match-patch": "^1.0.5",
+ "gitdiff-parser": "^0.3.1",
+ "lodash": "^4.17.21",
+ "shallow-equal": "^3.1.0",
+ "warning": "^4.0.3"
+ },
+ "peerDependencies": {
+ "react": ">=16.14.0"
+ }
+ },
"node_modules/react-dom": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
@@ -5353,6 +5332,11 @@
"integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==",
"license": "MIT"
},
+ "node_modules/shallow-equal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz",
+ "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg=="
+ },
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -5779,6 +5763,14 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/watchpack": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz",
diff --git a/services/hackbot-ui/package.json b/services/hackbot-ui/package.json
index eacf175a71..5313e58134 100644
--- a/services/hackbot-ui/package.json
+++ b/services/hackbot-ui/package.json
@@ -13,9 +13,9 @@
"dependencies": {
"@sentry/nextjs": "^10.74.0",
"better-auth": "^1.6.22",
- "diff2html": "^3.4.56",
"next": "^15.5.22",
"react": "^19.0.0",
+ "react-diff-view": "^3.3.3",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",