| author | |
| committer | |
| log | 36caf200c2b24c12838570966f5c85786a1fc1f8 |
| tree | b566cf72b7e3acccf5ba19f7498545cb1c4929c6 |
| parent | 8ddd9f7efcdfb6e32b614595242b5986942d37b0 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 = { |
| 34 | 34 | contextWindowMeterEnabled: false, |
| 35 | 35 | composerCollapseOnScroll: true, |
| 36 | 36 | dismissedProviderUpdateNotificationKeys: [], |
| 37 | diffFilesCollapsed: true, | |
| 37 | 38 | diffIgnoreWhitespace: true, |
| 38 | 39 | diffLayout: "stacked", |
| 39 | 40 | environmentIdentificationMode: "artwork", |
apps/web/src/clientPersistenceStorage.test.ts+15| ... | ... | @@ -114,6 +114,21 @@ describe("clientPersistenceStorage", () => { |
| 114 | 114 | expect(settings).not.toHaveProperty("diffWordWrap"); |
| 115 | 115 | }); |
| 116 | 116 | |
| 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 | ||
| 117 | 132 | it("keeps the diff layout across reloads and defaults it to stacked", async () => { |
| 118 | 133 | const testWindow = getTestWindow(); |
| 119 | 134 | const { readBrowserClientSettings, writeBrowserClientSettings } = |
apps/web/src/components/DiffPanel.tsx+21-10| ... | ... | @@ -226,10 +226,6 @@ export default function DiffPanel({ |
| 226 | 226 | ? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}:${reviewSectionId}` |
| 227 | 227 | : null; |
| 228 | 228 | const codeViewMountKey = `${collapseScopeKey ?? reviewSectionId}:${codeViewRevision}`; |
| 229 | const collapsedDiffFileKeys = | |
| 230 | collapsedDiffFiles.scopeKey === collapseScopeKey | |
| 231 | ? collapsedDiffFiles.fileKeys | |
| 232 | : EMPTY_COLLAPSED_DIFF_FILE_KEYS; | |
| 233 | 229 | const reviewSectionTitle = selectedTurn |
| 234 | 230 | ? `Turn ${selectedCheckpointTurnCount ?? "?"}` |
| 235 | 231 | : selectedGitScope === "unstaged" |
| ... | ... | @@ -425,6 +421,17 @@ export default function DiffPanel({ |
| 425 | 421 | })), |
| 426 | 422 | [renderableFiles], |
| 427 | 423 | ); |
| 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; | |
| 428 | 435 | const codeViewFiles = useMemo( |
| 429 | 436 | () => |
| 430 | 437 | renderableFileEntries.map(({ fileDiff, fileKey, fileVersion }) => { |
| ... | ... | @@ -462,14 +469,16 @@ export default function DiffPanel({ |
| 462 | 469 | if (!file) return; |
| 463 | 470 | if (file.collapsed) { |
| 464 | 471 | 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 | ); | |
| 466 | 475 | next.delete(file.fileKey); |
| 467 | 476 | return { scopeKey: collapseScopeKey, fileKeys: next }; |
| 468 | 477 | }); |
| 469 | 478 | } |
| 470 | 479 | requestTreeReveal(file.fileKey); |
| 471 | 480 | }, |
| 472 | [codeViewFiles, collapseScopeKey, requestTreeReveal], | |
| 481 | [codeViewFiles, collapseScopeKey, defaultCollapsedDiffFileKeys, requestTreeReveal], | |
| 473 | 482 | ); |
| 474 | 483 | |
| 475 | 484 | const openDiffFile = useCallback( |
| ... | ... | @@ -503,7 +512,9 @@ export default function DiffPanel({ |
| 503 | 512 | const toggleDiffFileCollapsed = useCallback( |
| 504 | 513 | (fileKey: string) => { |
| 505 | 514 | 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 | ); | |
| 507 | 518 | if (next.has(fileKey)) { |
| 508 | 519 | next.delete(fileKey); |
| 509 | 520 | } else { |
| ... | ... | @@ -512,21 +523,21 @@ export default function DiffPanel({ |
| 512 | 523 | return { scopeKey: collapseScopeKey, fileKeys: next }; |
| 513 | 524 | }); |
| 514 | 525 | }, |
| 515 | [collapseScopeKey], | |
| 526 | [collapseScopeKey, defaultCollapsedDiffFileKeys], | |
| 516 | 527 | ); |
| 517 | 528 | |
| 518 | 529 | const toggleDiffFileCollapse = useCallback(() => { |
| 519 | 530 | setCodeViewRevision((current) => current + 1); |
| 520 | 531 | setCollapsedDiffFiles((current) => { |
| 521 | 532 | const currentKeys = |
| 522 | current.scopeKey === collapseScopeKey ? current.fileKeys : EMPTY_COLLAPSED_DIFF_FILE_KEYS; | |
| 533 | current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys; | |
| 523 | 534 | |
| 524 | 535 | return { |
| 525 | 536 | scopeKey: collapseScopeKey, |
| 526 | 537 | fileKeys: toggleAllDiffFiles(diffFileKeys, currentKeys), |
| 527 | 538 | }; |
| 528 | 539 | }); |
| 529 | }, [collapseScopeKey, diffFileKeys]); | |
| 540 | }, [collapseScopeKey, defaultCollapsedDiffFileKeys, diffFileKeys]); | |
| 530 | 541 | |
| 531 | 542 | const selectTurn = (turnId: TurnId) => { |
| 532 | 543 | if (!routeThreadRef) return; |
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+6-1| ... | ... | @@ -485,7 +485,11 @@ function PullRequestCodeTab({ |
| 485 | 485 | groupAt(anchor.side, anchor.line).draft = true; |
| 486 | 486 | } |
| 487 | 487 | |
| 488 | const collapsed = isFileDiffCollapsed(fileKey, foldOverride, toggledFiles); | |
| 488 | const collapsed = isFileDiffCollapsed( | |
| 489 | fileKey, | |
| 490 | foldOverride ?? (settings.diffFilesCollapsed ? "folded" : "expanded"), | |
| 491 | toggledFiles, | |
| 492 | ); | |
| 489 | 493 | |
| 490 | 494 | const annotations: ReviewAnnotation[] = [...groups.values()].map((group) => ({ |
| 491 | 495 | side: toViewerSide(group.side), |
| ... | ... | @@ -538,6 +542,7 @@ function PullRequestCodeTab({ |
| 538 | 542 | foldOverride, |
| 539 | 543 | pendingComments, |
| 540 | 544 | placedThreadIds, |
| 545 | settings.diffFilesCollapsed, | |
| 541 | 546 | toggledFiles, |
| 542 | 547 | ], |
| 543 | 548 | ); |
apps/web/src/components/pullRequest/pullRequestDiff.logic.ts+2-2| ... | ... | @@ -30,8 +30,8 @@ export type DiffFoldOverride = "expanded" | "folded" | null; |
| 30 | 30 | * A diff arrives a slice at a time, so the reader's own choices are kept as the difference from |
| 31 | 31 | * what the toolbar last said rather than as the set of folded files: a file that has not loaded |
| 32 | 32 | * 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. | |
| 35 | 35 | */ |
| 36 | 36 | export function isFileDiffCollapsed( |
| 37 | 37 | fileKey: string, |
apps/web/src/components/settings/SettingsPanels.tsx+47| ... | ... | @@ -533,6 +533,9 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 533 | 533 | : []), |
| 534 | 534 | ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []), |
| 535 | 535 | ...getChangedTypographySettingLabels(settings), |
| 536 | ...(settings.diffFilesCollapsed !== DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed | |
| 537 | ? ["Default diff file state"] | |
| 538 | : []), | |
| 536 | 539 | ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace |
| 537 | 540 | ? ["Diff whitespace changes"] |
| 538 | 541 | : []), |
| ... | ... | @@ -608,6 +611,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 608 | 611 | settings.addProjectBaseDirectory, |
| 609 | 612 | settings.defaultThreadEnvMode, |
| 610 | 613 | settings.newWorktreesStartFromOrigin, |
| 614 | settings.diffFilesCollapsed, | |
| 611 | 615 | settings.diffIgnoreWhitespace, |
| 612 | 616 | settings.diffLayout, |
| 613 | 617 | settings.proactivePanelsEnabled, |
| ... | ... | @@ -706,6 +710,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 706 | 710 | diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme, |
| 707 | 711 | timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, |
| 708 | 712 | wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap, |
| 713 | diffFilesCollapsed: DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed, | |
| 709 | 714 | diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, |
| 710 | 715 | diffLayout: DEFAULT_UNIFIED_SETTINGS.diffLayout, |
| 711 | 716 | proactivePanelsEnabled: DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled, |
| ... | ... | @@ -2304,6 +2309,48 @@ export function GeneralSettingsPanel() { |
| 2304 | 2309 | /> |
| 2305 | 2310 | } |
| 2306 | 2311 | /> |
| 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 | /> | |
| 2307 | 2354 | <SettingsRow |
| 2308 | 2355 | {...searchableSetting("diff-layout")} |
| 2309 | 2356 | 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 = [ |
| 247 | 247 | to: "/settings/general", |
| 248 | 248 | searchTerms: ["diff ignore spaces edits default"], |
| 249 | 249 | }, |
| 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 | }, | |
| 250 | 256 | { |
| 251 | 257 | id: "diff-layout", |
| 252 | 258 | title: "Diff layout", |
packages/contracts/src/settings.test.ts+14| ... | ... | @@ -164,6 +164,20 @@ describe("ClaudeSettings auto-compaction", () => { |
| 164 | 164 | }); |
| 165 | 165 | }); |
| 166 | 166 | |
| 167 | describe("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 | ||
| 167 | 181 | describe("ClientSettings diff colors", () => { |
| 168 | 182 | it("keeps red and green for existing settings without a saved palette", () => { |
| 169 | 183 | expect(decodeClientSettings({}).diffColorScheme).toBe("red-green"); |
packages/contracts/src/settings.ts+2| ... | ... | @@ -344,6 +344,7 @@ export const ClientSettingsSchema = Schema.Struct({ |
| 344 | 344 | dismissedProviderUpdateNotificationKeys: Schema.Array(TrimmedNonEmptyString).pipe( |
| 345 | 345 | Schema.withDecodingDefault(Effect.succeed([])), |
| 346 | 346 | ), |
| 347 | diffFilesCollapsed: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), | |
| 347 | 348 | diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), |
| 348 | 349 | diffLayout: DiffLayout.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_DIFF_LAYOUT))), |
| 349 | 350 | environmentIdentificationMode: EnvironmentIdentificationMode.pipe( |
| ... | ... | @@ -1432,6 +1433,7 @@ export const ClientSettingsPatch = Schema.Struct({ |
| 1432 | 1433 | confirmThreadArchive: Schema.optionalKey(Schema.Boolean), |
| 1433 | 1434 | confirmThreadDelete: Schema.optionalKey(Schema.Boolean), |
| 1434 | 1435 | confirmThreadUnpin: Schema.optionalKey(Schema.Boolean), |
| 1436 | diffFilesCollapsed: Schema.optionalKey(Schema.Boolean), | |
| 1435 | 1437 | diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean), |
| 1436 | 1438 | diffLayout: Schema.optionalKey(DiffLayout), |
| 1437 | 1439 | environmentIdentificationMode: Schema.optionalKey(EnvironmentIdentificationMode), |