authorgravatar for 91240327+shivamhwp@users.noreply.github.comshivam <91240327+shivamhwp@users.noreply.github.com> 2026-05-05 05:36:07+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-04 17:06:07-07:00
logf4c9418d40358f61e4e9236992c0620d06c4bea3
tree18b395a6b9514593d4e477d4c7c29bf26fe8675e
parent02dd47ea3185ae46e11d0b9e584dc5afc84a7458
signature Signed by PGP key B5690EEEBB952194

Add collapsible file diffs + @pierre/diffs version update. (#2502)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

6 files changed, 77 insertions(+), 8 deletions(-)

apps/server/package.json+1-1
......@@ -29,7 +29,7 @@
2929 "@effect/platform-node": "catalog:",
3030 "@effect/sql-sqlite-bun": "catalog:",
3131 "@opencode-ai/sdk": "^1.3.15",
32 "@pierre/diffs": "^1.1.0-beta.16",
32 "@pierre/diffs": "catalog:",
3333 "effect": "catalog:",
3434 "node-pty": "^1.1.0",
3535 "open": "^10.1.0"
apps/web/package.json+1-1
......@@ -22,7 +22,7 @@
2222 "@formkit/auto-animate": "^0.9.0",
2323 "@legendapp/list": "3.0.0-beta.44",
2424 "@lexical/react": "^0.41.0",
25 "@pierre/diffs": "^1.1.0-beta.16",
25 "@pierre/diffs": "catalog:",
2626 "@t3tools/client-runtime": "workspace:*",
2727 "@t3tools/contracts": "workspace:*",
2828 "@t3tools/shared": "workspace:*",
apps/web/src/components/DiffPanel.tsx+68-1
......@@ -5,6 +5,7 @@ import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
55import { scopeThreadRef } from "@t3tools/client-runtime";
66import type { TurnId } from "@t3tools/contracts";
77import {
8 ChevronDownIcon,
89 ChevronLeftIcon,
910 ChevronRightIcon,
1011 Columns2Icon,
......@@ -161,6 +162,21 @@ function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string {
161162 return fileDiff.cacheKey ?? `${fileDiff.prevName ?? "none"}:${fileDiff.name}`;
162163}
163164
165function getDiffCollapseIconClassName(fileDiff: FileDiffMetadata): string {
166 switch (fileDiff.type) {
167 case "new":
168 return "text-[var(--diffs-addition-base)]";
169 case "deleted":
170 return "text-[var(--diffs-deletion-base)]";
171 case "change":
172 case "rename-pure":
173 case "rename-changed":
174 return "text-[var(--diffs-modified-base)]";
175 default:
176 return "text-muted-foreground/80";
177 }
178}
179
164180interface DiffPanelProps {
165181 mode?: DiffPanelMode;
166182}
......@@ -174,6 +190,9 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
174190 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");
175191 const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap);
176192 const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace);
193 const [collapsedDiffFileKeys, setCollapsedDiffFileKeys] = useState<ReadonlySet<string>>(
194 () => new Set(),
195 );
177196 const patchViewportRef = useRef<HTMLDivElement>(null);
178197 const turnStripRef = useRef<HTMLDivElement>(null);
179198 const previousDiffOpenRef = useRef(false);
......@@ -317,6 +336,19 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
317336 );
318337 }, [renderablePatch]);
319338
339 useEffect(() => {
340 if (renderableFiles.length === 0) {
341 setCollapsedDiffFileKeys((current) => (current.size === 0 ? current : new Set()));
342 return;
343 }
344
345 const visibleFileKeys = new Set(renderableFiles.map(buildFileDiffRenderKey));
346 setCollapsedDiffFileKeys((current) => {
347 const next = new Set([...current].filter((fileKey) => visibleFileKeys.has(fileKey)));
348 return next.size === current.size ? current : next;
349 });
350 }, [renderableFiles]);
351
320352 useEffect(() => {
321353 if (diffOpen && !previousDiffOpenRef.current) {
322354 setDiffWordWrap(settings.diffWordWrap);
......@@ -346,6 +378,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
346378 },
347379 [activeCwd],
348380 );
381 const toggleDiffFileCollapsed = useCallback((fileKey: string) => {
382 setCollapsedDiffFileKeys((current) => {
383 const next = new Set(current);
384 if (next.has(fileKey)) {
385 next.delete(fileKey);
386 } else {
387 next.add(fileKey);
388 }
389 return next;
390 });
391 }, []);
349392
350393 const selectTurn = (turnId: TurnId) => {
351394 if (!activeThread) return;
......@@ -620,11 +663,12 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
620663 const filePath = resolveFileDiffPath(fileDiff);
621664 const fileKey = buildFileDiffRenderKey(fileDiff);
622665 const themedFileKey = `${fileKey}:${resolvedTheme}`;
666 const collapsed = collapsedDiffFileKeys.has(fileKey);
623667 return (
624668 <div
625669 key={themedFileKey}
626670 data-diff-file-path={filePath}
627 className="diff-render-file mb-2 rounded-md first:mt-2 last:mb-0"
671 className="diff-render-file group/diff-file mb-2 rounded-md first:mt-2 last:mb-0"
628672 onClickCapture={(event) => {
629673 const nativeEvent = event.nativeEvent as MouseEvent;
630674 const composedPath = nativeEvent.composedPath?.() ?? [];
......@@ -638,7 +682,30 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
638682 >
639683 <FileDiff
640684 fileDiff={fileDiff}
685 renderHeaderPrefix={() => (
686 <button
687 type="button"
688 className={cn(
689 "inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent p-0 transition-colors hover:bg-foreground/10 focus-visible:outline-hidden",
690 getDiffCollapseIconClassName(fileDiff),
691 )}
692 aria-label={collapsed ? `Expand ${filePath}` : `Collapse ${filePath}`}
693 aria-expanded={!collapsed}
694 title={collapsed ? "Expand diff" : "Collapse diff"}
695 onClick={(event) => {
696 event.stopPropagation();
697 toggleDiffFileCollapsed(fileKey);
698 }}
699 >
700 {collapsed ? (
701 <ChevronRightIcon className="size-4" />
702 ) : (
703 <ChevronDownIcon className="size-4" />
704 )}
705 </button>
706 )}
641707 options={{
708 collapsed,
642709 diffStyle: diffRenderMode === "split" ? "split" : "unified",
643710 lineDiffType: "none",
644711 overflow: diffWordWrap ? "wrap" : "scroll",
apps/web/src/routes/_chat.$environmentId.$threadId.tsx+1-1
......@@ -28,7 +28,7 @@ const DiffPanel = lazy(() => import("../components/DiffPanel"));
2828const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width";
2929const DIFF_INLINE_DEFAULT_WIDTH = "clamp(24rem,34vw,36rem)";
3030const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 22 * 16;
31const DIFF_INLINE_SIDEBAR_MAX_WIDTH = 36 * 16;
31const DIFF_INLINE_SIDEBAR_MAX_WIDTH = 256 * 16;
3232const COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX = 208;
3333
3434const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => {
bun.lock+5-4
......@@ -59,7 +59,7 @@
5959 "@effect/platform-node": "catalog:",
6060 "@effect/sql-sqlite-bun": "catalog:",
6161 "@opencode-ai/sdk": "^1.3.15",
62 "@pierre/diffs": "^1.1.0-beta.16",
62 "@pierre/diffs": "catalog:",
6363 "effect": "catalog:",
6464 "node-pty": "^1.1.0",
6565 "open": "^10.1.0",
......@@ -93,7 +93,7 @@
9393 "@formkit/auto-animate": "^0.9.0",
9494 "@legendapp/list": "3.0.0-beta.44",
9595 "@lexical/react": "^0.41.0",
96 "@pierre/diffs": "^1.1.0-beta.16",
96 "@pierre/diffs": "catalog:",
9797 "@t3tools/client-runtime": "workspace:*",
9898 "@t3tools/contracts": "workspace:*",
9999 "@t3tools/shared": "workspace:*",
......@@ -281,6 +281,7 @@
281281 "@effect/platform-node-shared": "4.0.0-beta.59",
282282 "@effect/sql-sqlite-bun": "4.0.0-beta.59",
283283 "@effect/vitest": "4.0.0-beta.59",
284 "@pierre/diffs": "1.1.20",
284285 "@types/bun": "^1.3.11",
285286 "@types/node": "^24.10.13",
286287 "effect": "4.0.0-beta.59",
......@@ -701,9 +702,9 @@
701702
702703 "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ=="],
703704
704 "@pierre/diffs": ["@pierre/diffs@1.1.0", "", { "dependencies": { "@pierre/theme": "0.0.22", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-wbxrzcmanJuHZb81iir09j42uU9AnKxXDtAuEQJbAnti5f2UfYdCQYejawuHZStFrlsMacCZLh/dDHmqvAaQCw=="],
705 "@pierre/diffs": ["@pierre/diffs@1.1.20", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-lLi+3sLCm3QDd5/aLO9pw+WbF6UzhrkWm2oTZ5WZJTGemOyUNRJ4DDhcEKmVusu4C4bXx9Nssh6fF+wQcapb5w=="],
705706
706 "@pierre/theme": ["@pierre/theme@0.0.22", "", {}, "sha512-ePUIdQRNGjrveELTU7fY89Xa7YGHHEy5Po5jQy/18lm32eRn96+tnYJEtFooGdffrx55KBUtOXfvVy/7LDFFhA=="],
707 "@pierre/theme": ["@pierre/theme@0.0.28", "", {}, "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw=="],
707708
708709 "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
709710
package.json+1
......@@ -17,6 +17,7 @@
1717 "@effect/sql-sqlite-bun": "4.0.0-beta.59",
1818 "@effect/vitest": "4.0.0-beta.59",
1919 "@effect/language-service": "0.84.2",
20 "@pierre/diffs": "1.1.20",
2021 "@types/bun": "^1.3.11",
2122 "@types/node": "^24.10.13",
2223 "tsdown": "^0.20.3",