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 @@...@@ -29,7 +29,7 @@
29 "@effect/platform-node": "catalog:",29 "@effect/platform-node": "catalog:",
30 "@effect/sql-sqlite-bun": "catalog:",30 "@effect/sql-sqlite-bun": "catalog:",
31 "@opencode-ai/sdk": "^1.3.15",31 "@opencode-ai/sdk": "^1.3.15",
32 "@pierre/diffs": "^1.1.0-beta.16",32 "@pierre/diffs": "catalog:",
33 "effect": "catalog:",33 "effect": "catalog:",
34 "node-pty": "^1.1.0",34 "node-pty": "^1.1.0",
35 "open": "^10.1.0"35 "open": "^10.1.0"
apps/web/package.json+1-1
...@@ -22,7 +22,7 @@...@@ -22,7 +22,7 @@
22 "@formkit/auto-animate": "^0.9.0",22 "@formkit/auto-animate": "^0.9.0",
23 "@legendapp/list": "3.0.0-beta.44",23 "@legendapp/list": "3.0.0-beta.44",
24 "@lexical/react": "^0.41.0",24 "@lexical/react": "^0.41.0",
25 "@pierre/diffs": "^1.1.0-beta.16",25 "@pierre/diffs": "catalog:",
26 "@t3tools/client-runtime": "workspace:*",26 "@t3tools/client-runtime": "workspace:*",
27 "@t3tools/contracts": "workspace:*",27 "@t3tools/contracts": "workspace:*",
28 "@t3tools/shared": "workspace:*",28 "@t3tools/shared": "workspace:*",
apps/web/src/components/DiffPanel.tsx+68-1
...@@ -5,6 +5,7 @@ import { useNavigate, useParams, useSearch } from "@tanstack/react-router";...@@ -5,6 +5,7 @@ import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
5import { scopeThreadRef } from "@t3tools/client-runtime";5import { scopeThreadRef } from "@t3tools/client-runtime";
6import type { TurnId } from "@t3tools/contracts";6import type { TurnId } from "@t3tools/contracts";
7import {7import {
8 ChevronDownIcon,
8 ChevronLeftIcon,9 ChevronLeftIcon,
9 ChevronRightIcon,10 ChevronRightIcon,
10 Columns2Icon,11 Columns2Icon,
...@@ -161,6 +162,21 @@ function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string {...@@ -161,6 +162,21 @@ function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string {
161 return fileDiff.cacheKey ?? `${fileDiff.prevName ?? "none"}:${fileDiff.name}`;162 return fileDiff.cacheKey ?? `${fileDiff.prevName ?? "none"}:${fileDiff.name}`;
162}163}
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
164interface DiffPanelProps {180interface DiffPanelProps {
165 mode?: DiffPanelMode;181 mode?: DiffPanelMode;
166}182}
...@@ -174,6 +190,9 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -174,6 +190,9 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
174 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");190 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");
175 const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap);191 const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap);
176 const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace);192 const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace);
193 const [collapsedDiffFileKeys, setCollapsedDiffFileKeys] = useState<ReadonlySet<string>>(
194 () => new Set(),
195 );
177 const patchViewportRef = useRef<HTMLDivElement>(null);196 const patchViewportRef = useRef<HTMLDivElement>(null);
178 const turnStripRef = useRef<HTMLDivElement>(null);197 const turnStripRef = useRef<HTMLDivElement>(null);
179 const previousDiffOpenRef = useRef(false);198 const previousDiffOpenRef = useRef(false);
...@@ -317,6 +336,19 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -317,6 +336,19 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
317 );336 );
318 }, [renderablePatch]);337 }, [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
320 useEffect(() => {352 useEffect(() => {
321 if (diffOpen && !previousDiffOpenRef.current) {353 if (diffOpen && !previousDiffOpenRef.current) {
322 setDiffWordWrap(settings.diffWordWrap);354 setDiffWordWrap(settings.diffWordWrap);
...@@ -346,6 +378,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -346,6 +378,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
346 },378 },
347 [activeCwd],379 [activeCwd],
348 );380 );
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
350 const selectTurn = (turnId: TurnId) => {393 const selectTurn = (turnId: TurnId) => {
351 if (!activeThread) return;394 if (!activeThread) return;
...@@ -620,11 +663,12 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -620,11 +663,12 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
620 const filePath = resolveFileDiffPath(fileDiff);663 const filePath = resolveFileDiffPath(fileDiff);
621 const fileKey = buildFileDiffRenderKey(fileDiff);664 const fileKey = buildFileDiffRenderKey(fileDiff);
622 const themedFileKey = `${fileKey}:${resolvedTheme}`;665 const themedFileKey = `${fileKey}:${resolvedTheme}`;
666 const collapsed = collapsedDiffFileKeys.has(fileKey);
623 return (667 return (
624 <div668 <div
625 key={themedFileKey}669 key={themedFileKey}
626 data-diff-file-path={filePath}670 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"
628 onClickCapture={(event) => {672 onClickCapture={(event) => {
629 const nativeEvent = event.nativeEvent as MouseEvent;673 const nativeEvent = event.nativeEvent as MouseEvent;
630 const composedPath = nativeEvent.composedPath?.() ?? [];674 const composedPath = nativeEvent.composedPath?.() ?? [];
...@@ -638,7 +682,30 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -638,7 +682,30 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
638 >682 >
639 <FileDiff683 <FileDiff
640 fileDiff={fileDiff}684 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 )}
641 options={{707 options={{
708 collapsed,
642 diffStyle: diffRenderMode === "split" ? "split" : "unified",709 diffStyle: diffRenderMode === "split" ? "split" : "unified",
643 lineDiffType: "none",710 lineDiffType: "none",
644 overflow: diffWordWrap ? "wrap" : "scroll",711 overflow: diffWordWrap ? "wrap" : "scroll",
apps/web/src/routes/_chat.$environmentId.$threadId.tsx+1-1
...@@ -28,7 +28,7 @@ const DiffPanel = lazy(() => import("../components/DiffPanel"));...@@ -28,7 +28,7 @@ const DiffPanel = lazy(() => import("../components/DiffPanel"));
28const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width";28const DIFF_INLINE_SIDEBAR_WIDTH_STORAGE_KEY = "chat_diff_sidebar_width";
29const DIFF_INLINE_DEFAULT_WIDTH = "clamp(24rem,34vw,36rem)";29const DIFF_INLINE_DEFAULT_WIDTH = "clamp(24rem,34vw,36rem)";
30const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 22 * 16;30const DIFF_INLINE_SIDEBAR_MIN_WIDTH = 22 * 16;
31const DIFF_INLINE_SIDEBAR_MAX_WIDTH = 36 * 16;31const DIFF_INLINE_SIDEBAR_MAX_WIDTH = 256 * 16;
32const COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX = 208;32const COMPOSER_COMPACT_MIN_LEFT_CONTROLS_WIDTH_PX = 208;
3333
34const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => {34const DiffLoadingFallback = (props: { mode: DiffPanelMode }) => {
bun.lock+5-4
...@@ -59,7 +59,7 @@...@@ -59,7 +59,7 @@
59 "@effect/platform-node": "catalog:",59 "@effect/platform-node": "catalog:",
60 "@effect/sql-sqlite-bun": "catalog:",60 "@effect/sql-sqlite-bun": "catalog:",
61 "@opencode-ai/sdk": "^1.3.15",61 "@opencode-ai/sdk": "^1.3.15",
62 "@pierre/diffs": "^1.1.0-beta.16",62 "@pierre/diffs": "catalog:",
63 "effect": "catalog:",63 "effect": "catalog:",
64 "node-pty": "^1.1.0",64 "node-pty": "^1.1.0",
65 "open": "^10.1.0",65 "open": "^10.1.0",
...@@ -93,7 +93,7 @@...@@ -93,7 +93,7 @@
93 "@formkit/auto-animate": "^0.9.0",93 "@formkit/auto-animate": "^0.9.0",
94 "@legendapp/list": "3.0.0-beta.44",94 "@legendapp/list": "3.0.0-beta.44",
95 "@lexical/react": "^0.41.0",95 "@lexical/react": "^0.41.0",
96 "@pierre/diffs": "^1.1.0-beta.16",96 "@pierre/diffs": "catalog:",
97 "@t3tools/client-runtime": "workspace:*",97 "@t3tools/client-runtime": "workspace:*",
98 "@t3tools/contracts": "workspace:*",98 "@t3tools/contracts": "workspace:*",
99 "@t3tools/shared": "workspace:*",99 "@t3tools/shared": "workspace:*",
...@@ -281,6 +281,7 @@...@@ -281,6 +281,7 @@
281 "@effect/platform-node-shared": "4.0.0-beta.59",281 "@effect/platform-node-shared": "4.0.0-beta.59",
282 "@effect/sql-sqlite-bun": "4.0.0-beta.59",282 "@effect/sql-sqlite-bun": "4.0.0-beta.59",
283 "@effect/vitest": "4.0.0-beta.59",283 "@effect/vitest": "4.0.0-beta.59",
284 "@pierre/diffs": "1.1.20",
284 "@types/bun": "^1.3.11",285 "@types/bun": "^1.3.11",
285 "@types/node": "^24.10.13",286 "@types/node": "^24.10.13",
286 "effect": "4.0.0-beta.59",287 "effect": "4.0.0-beta.59",
...@@ -701,9 +702,9 @@...@@ -701,9 +702,9 @@
701702
702 "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ=="],703 "@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
708 "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],709 "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
709710
package.json+1
...@@ -17,6 +17,7 @@...@@ -17,6 +17,7 @@
17 "@effect/sql-sqlite-bun": "4.0.0-beta.59",17 "@effect/sql-sqlite-bun": "4.0.0-beta.59",
18 "@effect/vitest": "4.0.0-beta.59",18 "@effect/vitest": "4.0.0-beta.59",
19 "@effect/language-service": "0.84.2",19 "@effect/language-service": "0.84.2",
20 "@pierre/diffs": "1.1.20",
20 "@types/bun": "^1.3.11",21 "@types/bun": "^1.3.11",
21 "@types/node": "^24.10.13",22 "@types/node": "^24.10.13",
22 "tsdown": "^0.20.3",23 "tsdown": "^0.20.3",