authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-12 21:25:31-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-12 21:25:31-03:00
log36caf200c2b24c12838570966f5c85786a1fc1f8
treeb566cf72b7e3acccf5ba19f7498545cb1c4929c6
parent8ddd9f7efcdfb6e32b614595242b5986942d37b0
signature Signed by PGP key B5690EEEBB952194

feat(web): choose the default diff file state (#11484)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

9 files changed, 114 insertions(+), 13 deletions(-)

apps/desktop/src/settings/DesktopClientSettings.test.ts+1
......@@ -34,6 +34,7 @@ const clientSettings: ClientSettings = {
3434 contextWindowMeterEnabled: false,
3535 composerCollapseOnScroll: true,
3636 dismissedProviderUpdateNotificationKeys: [],
37 diffFilesCollapsed: true,
3738 diffIgnoreWhitespace: true,
3839 diffLayout: "stacked",
3940 environmentIdentificationMode: "artwork",
apps/web/src/clientPersistenceStorage.test.ts+15
......@@ -114,6 +114,21 @@ describe("clientPersistenceStorage", () => {
114114 expect(settings).not.toHaveProperty("diffWordWrap");
115115 });
116116
117 it("keeps the default diff file state across reloads and defaults it to expanded", async () => {
118 const testWindow = getTestWindow();
119 const { readBrowserClientSettings, writeBrowserClientSettings } =
120 await import("./clientPersistenceStorage");
121
122 testWindow.localStorage.setItem("t3code:client-settings:v1", JSON.stringify({}));
123 expect(readBrowserClientSettings()?.diffFilesCollapsed).toBe(false);
124
125 writeBrowserClientSettings({ ...DEFAULT_CLIENT_SETTINGS, diffFilesCollapsed: true });
126 expect(readBrowserClientSettings()?.diffFilesCollapsed).toBe(true);
127
128 writeBrowserClientSettings({ ...DEFAULT_CLIENT_SETTINGS, diffFilesCollapsed: false });
129 expect(readBrowserClientSettings()?.diffFilesCollapsed).toBe(false);
130 });
131
117132 it("keeps the diff layout across reloads and defaults it to stacked", async () => {
118133 const testWindow = getTestWindow();
119134 const { readBrowserClientSettings, writeBrowserClientSettings } =
apps/web/src/components/DiffPanel.tsx+21-10
......@@ -226,10 +226,6 @@ export default function DiffPanel({
226226 ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}:${reviewSectionId}`
227227 : null;
228228 const codeViewMountKey = `${collapseScopeKey ?? reviewSectionId}:${codeViewRevision}`;
229 const collapsedDiffFileKeys =
230 collapsedDiffFiles.scopeKey === collapseScopeKey
231 ? collapsedDiffFiles.fileKeys
232 : EMPTY_COLLAPSED_DIFF_FILE_KEYS;
233229 const reviewSectionTitle = selectedTurn
234230 ? `Turn ${selectedCheckpointTurnCount ?? "?"}`
235231 : selectedGitScope === "unstaged"
......@@ -425,6 +421,17 @@ export default function DiffPanel({
425421 })),
426422 [renderableFiles],
427423 );
424 const defaultCollapsedDiffFileKeys = useMemo(
425 () =>
426 settings.diffFilesCollapsed
427 ? new Set(renderableFileEntries.map((file) => file.fileKey))
428 : EMPTY_COLLAPSED_DIFF_FILE_KEYS,
429 [renderableFileEntries, settings.diffFilesCollapsed],
430 );
431 const collapsedDiffFileKeys =
432 collapsedDiffFiles.scopeKey === collapseScopeKey
433 ? collapsedDiffFiles.fileKeys
434 : defaultCollapsedDiffFileKeys;
428435 const codeViewFiles = useMemo(
429436 () =>
430437 renderableFileEntries.map(({ fileDiff, fileKey, fileVersion }) => {
......@@ -462,14 +469,16 @@ export default function DiffPanel({
462469 if (!file) return;
463470 if (file.collapsed) {
464471 setCollapsedDiffFiles((current) => {
465 const next = new Set(current.scopeKey === collapseScopeKey ? current.fileKeys : []);
472 const next = new Set(
473 current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys,
474 );
466475 next.delete(file.fileKey);
467476 return { scopeKey: collapseScopeKey, fileKeys: next };
468477 });
469478 }
470479 requestTreeReveal(file.fileKey);
471480 },
472 [codeViewFiles, collapseScopeKey, requestTreeReveal],
481 [codeViewFiles, collapseScopeKey, defaultCollapsedDiffFileKeys, requestTreeReveal],
473482 );
474483
475484 const openDiffFile = useCallback(
......@@ -503,7 +512,9 @@ export default function DiffPanel({
503512 const toggleDiffFileCollapsed = useCallback(
504513 (fileKey: string) => {
505514 setCollapsedDiffFiles((current) => {
506 const next = new Set(current.scopeKey === collapseScopeKey ? current.fileKeys : []);
515 const next = new Set(
516 current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys,
517 );
507518 if (next.has(fileKey)) {
508519 next.delete(fileKey);
509520 } else {
......@@ -512,21 +523,21 @@ export default function DiffPanel({
512523 return { scopeKey: collapseScopeKey, fileKeys: next };
513524 });
514525 },
515 [collapseScopeKey],
526 [collapseScopeKey, defaultCollapsedDiffFileKeys],
516527 );
517528
518529 const toggleDiffFileCollapse = useCallback(() => {
519530 setCodeViewRevision((current) => current + 1);
520531 setCollapsedDiffFiles((current) => {
521532 const currentKeys =
522 current.scopeKey === collapseScopeKey ? current.fileKeys : EMPTY_COLLAPSED_DIFF_FILE_KEYS;
533 current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys;
523534
524535 return {
525536 scopeKey: collapseScopeKey,
526537 fileKeys: toggleAllDiffFiles(diffFileKeys, currentKeys),
527538 };
528539 });
529 }, [collapseScopeKey, diffFileKeys]);
540 }, [collapseScopeKey, defaultCollapsedDiffFileKeys, diffFileKeys]);
530541
531542 const selectTurn = (turnId: TurnId) => {
532543 if (!routeThreadRef) return;
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+6-1
......@@ -485,7 +485,11 @@ function PullRequestCodeTab({
485485 groupAt(anchor.side, anchor.line).draft = true;
486486 }
487487
488 const collapsed = isFileDiffCollapsed(fileKey, foldOverride, toggledFiles);
488 const collapsed = isFileDiffCollapsed(
489 fileKey,
490 foldOverride ?? (settings.diffFilesCollapsed ? "folded" : "expanded"),
491 toggledFiles,
492 );
489493
490494 const annotations: ReviewAnnotation[] = [...groups.values()].map((group) => ({
491495 side: toViewerSide(group.side),
......@@ -538,6 +542,7 @@ function PullRequestCodeTab({
538542 foldOverride,
539543 pendingComments,
540544 placedThreadIds,
545 settings.diffFilesCollapsed,
541546 toggledFiles,
542547 ],
543548 );
apps/web/src/components/pullRequest/pullRequestDiff.logic.ts+2-2
......@@ -30,8 +30,8 @@ export type DiffFoldOverride = "expanded" | "folded" | null;
3030 * A diff arrives a slice at a time, so the reader's own choices are kept as the difference from
3131 * what the toolbar last said rather than as the set of folded files: a file that has not loaded
3232 * yet cannot be in a set, and would otherwise land expanded moments after the reader folded
33 * everything. Files start expanded so opening the Code tab immediately shows the change; the
34 * reader can still fold individual files or the whole diff from the toolbar.
33 * everything. The caller supplies the saved default until the toolbar overrides it; individual
34 * files can still be toggled independently.
3535 */
3636export function isFileDiffCollapsed(
3737 fileKey: string,
apps/web/src/components/settings/SettingsPanels.tsx+47
......@@ -533,6 +533,9 @@ export function useSettingsRestore(onRestored?: () => void) {
533533 : []),
534534 ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []),
535535 ...getChangedTypographySettingLabels(settings),
536 ...(settings.diffFilesCollapsed !== DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed
537 ? ["Default diff file state"]
538 : []),
536539 ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace
537540 ? ["Diff whitespace changes"]
538541 : []),
......@@ -608,6 +611,7 @@ export function useSettingsRestore(onRestored?: () => void) {
608611 settings.addProjectBaseDirectory,
609612 settings.defaultThreadEnvMode,
610613 settings.newWorktreesStartFromOrigin,
614 settings.diffFilesCollapsed,
611615 settings.diffIgnoreWhitespace,
612616 settings.diffLayout,
613617 settings.proactivePanelsEnabled,
......@@ -706,6 +710,7 @@ export function useSettingsRestore(onRestored?: () => void) {
706710 diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme,
707711 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
708712 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
713 diffFilesCollapsed: DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed,
709714 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
710715 diffLayout: DEFAULT_UNIFIED_SETTINGS.diffLayout,
711716 proactivePanelsEnabled: DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled,
......@@ -2304,6 +2309,48 @@ export function GeneralSettingsPanel() {
23042309 />
23052310 }
23062311 />
2312 <SettingsRow
2313 {...searchableSetting("default-diff-file-state")}
2314 description="Start with files expanded or collapsed when opening diffs or a pull request's Code tab."
2315 resetAction={
2316 settings.diffFilesCollapsed !== DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed ? (
2317 <SettingResetButton
2318 label="default diff file state"
2319 onClick={() =>
2320 updateSettings({
2321 diffFilesCollapsed: DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed,
2322 })
2323 }
2324 />
2325 ) : null
2326 }
2327 control={
2328 <Select
2329 value={settings.diffFilesCollapsed ? "collapsed" : "expanded"}
2330 onValueChange={(value) => {
2331 if (value === "expanded" || value === "collapsed") {
2332 updateSettings({ diffFilesCollapsed: value === "collapsed" });
2333 }
2334 }}
2335 >
2336 <SelectTrigger
2337 size="sm"
2338 className="w-full sm:w-40"
2339 aria-label="Default diff file state"
2340 >
2341 <SelectValue>{settings.diffFilesCollapsed ? "Collapsed" : "Expanded"}</SelectValue>
2342 </SelectTrigger>
2343 <SelectPopup align="end" alignItemWithTrigger={false}>
2344 <SelectItem hideIndicator value="expanded">
2345 Expanded
2346 </SelectItem>
2347 <SelectItem hideIndicator value="collapsed">
2348 Collapsed
2349 </SelectItem>
2350 </SelectPopup>
2351 </Select>
2352 }
2353 />
23072354 <SettingsRow
23082355 {...searchableSetting("diff-layout")}
23092356 description="Show diffs stacked or side by side. The toggle in the diff toolbar changes this too."
apps/web/src/components/settings/settingsSearch.ts+6
......@@ -247,6 +247,12 @@ export const SETTINGS_SEARCH_ITEMS = [
247247 to: "/settings/general",
248248 searchTerms: ["diff ignore spaces edits default"],
249249 },
250 {
251 id: "default-diff-file-state",
252 title: "Default diff file state",
253 to: "/settings/general",
254 searchTerms: ["collapsed expanded collapse expand files pull request pr code tab"],
255 },
250256 {
251257 id: "diff-layout",
252258 title: "Diff layout",
packages/contracts/src/settings.test.ts+14
......@@ -164,6 +164,20 @@ describe("ClaudeSettings auto-compaction", () => {
164164 });
165165});
166166
167describe("ClientSettings default diff file state", () => {
168 it("keeps files expanded when existing settings omit the preference", () => {
169 expect(decodeClientSettings({}).diffFilesCollapsed).toBe(false);
170 });
171
172 it.each([true, false])("preserves a saved collapsed preference of %s", (diffFilesCollapsed) => {
173 const settings = decodeClientSettings({ diffFilesCollapsed });
174 expect(encodeClientSettings(settings).diffFilesCollapsed).toBe(diffFilesCollapsed);
175 expect(decodeClientSettingsPatch({ diffFilesCollapsed }).diffFilesCollapsed).toBe(
176 diffFilesCollapsed,
177 );
178 });
179});
180
167181describe("ClientSettings diff colors", () => {
168182 it("keeps red and green for existing settings without a saved palette", () => {
169183 expect(decodeClientSettings({}).diffColorScheme).toBe("red-green");
packages/contracts/src/settings.ts+2
......@@ -344,6 +344,7 @@ export const ClientSettingsSchema = Schema.Struct({
344344 dismissedProviderUpdateNotificationKeys: Schema.Array(TrimmedNonEmptyString).pipe(
345345 Schema.withDecodingDefault(Effect.succeed([])),
346346 ),
347 diffFilesCollapsed: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
347348 diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
348349 diffLayout: DiffLayout.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_DIFF_LAYOUT))),
349350 environmentIdentificationMode: EnvironmentIdentificationMode.pipe(
......@@ -1432,6 +1433,7 @@ export const ClientSettingsPatch = Schema.Struct({
14321433 confirmThreadArchive: Schema.optionalKey(Schema.Boolean),
14331434 confirmThreadDelete: Schema.optionalKey(Schema.Boolean),
14341435 confirmThreadUnpin: Schema.optionalKey(Schema.Boolean),
1436 diffFilesCollapsed: Schema.optionalKey(Schema.Boolean),
14351437 diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean),
14361438 diffLayout: Schema.optionalKey(DiffLayout),
14371439 environmentIdentificationMode: Schema.optionalKey(EnvironmentIdentificationMode),