Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const clientSettings: ClientSettings = {
contextWindowMeterEnabled: false,
composerCollapseOnScroll: true,
dismissedProviderUpdateNotificationKeys: [],
diffFilesCollapsed: true,
diffIgnoreWhitespace: true,
diffLayout: "stacked",
environmentIdentificationMode: "artwork",
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/clientPersistenceStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@ describe("clientPersistenceStorage", () => {
expect(settings).not.toHaveProperty("diffWordWrap");
});

it("keeps the default diff file state across reloads and defaults it to expanded", async () => {
const testWindow = getTestWindow();
const { readBrowserClientSettings, writeBrowserClientSettings } =
await import("./clientPersistenceStorage");

testWindow.localStorage.setItem("t3code:client-settings:v1", JSON.stringify({}));
expect(readBrowserClientSettings()?.diffFilesCollapsed).toBe(false);

writeBrowserClientSettings({ ...DEFAULT_CLIENT_SETTINGS, diffFilesCollapsed: true });
expect(readBrowserClientSettings()?.diffFilesCollapsed).toBe(true);

writeBrowserClientSettings({ ...DEFAULT_CLIENT_SETTINGS, diffFilesCollapsed: false });
expect(readBrowserClientSettings()?.diffFilesCollapsed).toBe(false);
});

it("keeps the diff layout across reloads and defaults it to stacked", async () => {
const testWindow = getTestWindow();
const { readBrowserClientSettings, writeBrowserClientSettings } =
Expand Down
31 changes: 21 additions & 10 deletions apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,6 @@ export default function DiffPanel({
? `${routeThreadRef.environmentId}:${routeThreadRef.threadId}:${reviewSectionId}`
: null;
const codeViewMountKey = `${collapseScopeKey ?? reviewSectionId}:${codeViewRevision}`;
const collapsedDiffFileKeys =
collapsedDiffFiles.scopeKey === collapseScopeKey
? collapsedDiffFiles.fileKeys
: EMPTY_COLLAPSED_DIFF_FILE_KEYS;
const reviewSectionTitle = selectedTurn
? `Turn ${selectedCheckpointTurnCount ?? "?"}`
: selectedGitScope === "unstaged"
Expand Down Expand Up @@ -425,6 +421,17 @@ export default function DiffPanel({
})),
[renderableFiles],
);
const defaultCollapsedDiffFileKeys = useMemo(
() =>
settings.diffFilesCollapsed
? new Set(renderableFileEntries.map((file) => file.fileKey))
: EMPTY_COLLAPSED_DIFF_FILE_KEYS,
[renderableFileEntries, settings.diffFilesCollapsed],
);
const collapsedDiffFileKeys =
collapsedDiffFiles.scopeKey === collapseScopeKey
? collapsedDiffFiles.fileKeys
: defaultCollapsedDiffFileKeys;
const codeViewFiles = useMemo(
() =>
renderableFileEntries.map(({ fileDiff, fileKey, fileVersion }) => {
Expand Down Expand Up @@ -462,14 +469,16 @@ export default function DiffPanel({
if (!file) return;
if (file.collapsed) {
setCollapsedDiffFiles((current) => {
const next = new Set(current.scopeKey === collapseScopeKey ? current.fileKeys : []);
const next = new Set(
current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys,
);
next.delete(file.fileKey);
return { scopeKey: collapseScopeKey, fileKeys: next };
});
}
requestTreeReveal(file.fileKey);
},
[codeViewFiles, collapseScopeKey, requestTreeReveal],
[codeViewFiles, collapseScopeKey, defaultCollapsedDiffFileKeys, requestTreeReveal],
);

const openDiffFile = useCallback(
Expand Down Expand Up @@ -503,7 +512,9 @@ export default function DiffPanel({
const toggleDiffFileCollapsed = useCallback(
(fileKey: string) => {
setCollapsedDiffFiles((current) => {
const next = new Set(current.scopeKey === collapseScopeKey ? current.fileKeys : []);
const next = new Set(
current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys,
);
if (next.has(fileKey)) {
next.delete(fileKey);
} else {
Expand All @@ -512,21 +523,21 @@ export default function DiffPanel({
return { scopeKey: collapseScopeKey, fileKeys: next };
});
},
[collapseScopeKey],
[collapseScopeKey, defaultCollapsedDiffFileKeys],
);

const toggleDiffFileCollapse = useCallback(() => {
setCodeViewRevision((current) => current + 1);
setCollapsedDiffFiles((current) => {
const currentKeys =
current.scopeKey === collapseScopeKey ? current.fileKeys : EMPTY_COLLAPSED_DIFF_FILE_KEYS;
current.scopeKey === collapseScopeKey ? current.fileKeys : defaultCollapsedDiffFileKeys;

return {
scopeKey: collapseScopeKey,
fileKeys: toggleAllDiffFiles(diffFileKeys, currentKeys),
};
});
}, [collapseScopeKey, diffFileKeys]);
}, [collapseScopeKey, defaultCollapsedDiffFileKeys, diffFileKeys]);

const selectTurn = (turnId: TurnId) => {
if (!routeThreadRef) return;
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,11 @@ function PullRequestCodeTab({
groupAt(anchor.side, anchor.line).draft = true;
}

const collapsed = isFileDiffCollapsed(fileKey, foldOverride, toggledFiles);
const collapsed = isFileDiffCollapsed(
fileKey,
foldOverride ?? (settings.diffFilesCollapsed ? "folded" : "expanded"),
toggledFiles,
);

const annotations: ReviewAnnotation[] = [...groups.values()].map((group) => ({
side: toViewerSide(group.side),
Expand Down Expand Up @@ -538,6 +542,7 @@ function PullRequestCodeTab({
foldOverride,
pendingComments,
placedThreadIds,
settings.diffFilesCollapsed,
toggledFiles,
],
);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/pullRequest/pullRequestDiff.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export type DiffFoldOverride = "expanded" | "folded" | null;
* A diff arrives a slice at a time, so the reader's own choices are kept as the difference from
* what the toolbar last said rather than as the set of folded files: a file that has not loaded
* yet cannot be in a set, and would otherwise land expanded moments after the reader folded
* everything. Files start expanded so opening the Code tab immediately shows the change; the
* reader can still fold individual files or the whole diff from the toolbar.
* everything. The caller supplies the saved default until the toolbar overrides it; individual
* files can still be toggled independently.
*/
export function isFileDiffCollapsed(
fileKey: string,
Expand Down
47 changes: 47 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,9 @@ export function useSettingsRestore(onRestored?: () => void) {
: []),
...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []),
...getChangedTypographySettingLabels(settings),
...(settings.diffFilesCollapsed !== DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed
? ["Default diff file state"]
: []),
...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace
? ["Diff whitespace changes"]
: []),
Expand Down Expand Up @@ -608,6 +611,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.addProjectBaseDirectory,
settings.defaultThreadEnvMode,
settings.newWorktreesStartFromOrigin,
settings.diffFilesCollapsed,
settings.diffIgnoreWhitespace,
settings.diffLayout,
settings.proactivePanelsEnabled,
Expand Down Expand Up @@ -706,6 +710,7 @@ export function useSettingsRestore(onRestored?: () => void) {
diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme,
timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
diffFilesCollapsed: DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed,
diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
diffLayout: DEFAULT_UNIFIED_SETTINGS.diffLayout,
proactivePanelsEnabled: DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled,
Expand Down Expand Up @@ -2304,6 +2309,48 @@ export function GeneralSettingsPanel() {
/>
}
/>
<SettingsRow
{...searchableSetting("default-diff-file-state")}
description="Start with files expanded or collapsed when opening diffs or a pull request's Code tab."
resetAction={
settings.diffFilesCollapsed !== DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed ? (
<SettingResetButton
label="default diff file state"
onClick={() =>
updateSettings({
diffFilesCollapsed: DEFAULT_UNIFIED_SETTINGS.diffFilesCollapsed,
})
}
/>
) : null
}
control={
<Select
value={settings.diffFilesCollapsed ? "collapsed" : "expanded"}
onValueChange={(value) => {
if (value === "expanded" || value === "collapsed") {
updateSettings({ diffFilesCollapsed: value === "collapsed" });
}
}}
>
<SelectTrigger
size="sm"
className="w-full sm:w-40"
aria-label="Default diff file state"
>
<SelectValue>{settings.diffFilesCollapsed ? "Collapsed" : "Expanded"}</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
<SelectItem hideIndicator value="expanded">
Expanded
</SelectItem>
<SelectItem hideIndicator value="collapsed">
Collapsed
</SelectItem>
</SelectPopup>
</Select>
}
/>
<SettingsRow
{...searchableSetting("diff-layout")}
description="Show diffs stacked or side by side. The toggle in the diff toolbar changes this too."
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,12 @@ export const SETTINGS_SEARCH_ITEMS = [
to: "/settings/general",
searchTerms: ["diff ignore spaces edits default"],
},
{
id: "default-diff-file-state",
title: "Default diff file state",
to: "/settings/general",
searchTerms: ["collapsed expanded collapse expand files pull request pr code tab"],
},
{
id: "diff-layout",
title: "Diff layout",
Expand Down
14 changes: 14 additions & 0 deletions packages/contracts/src/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,20 @@ describe("ClaudeSettings auto-compaction", () => {
});
});

describe("ClientSettings default diff file state", () => {
it("keeps files expanded when existing settings omit the preference", () => {
expect(decodeClientSettings({}).diffFilesCollapsed).toBe(false);
});

it.each([true, false])("preserves a saved collapsed preference of %s", (diffFilesCollapsed) => {
const settings = decodeClientSettings({ diffFilesCollapsed });
expect(encodeClientSettings(settings).diffFilesCollapsed).toBe(diffFilesCollapsed);
expect(decodeClientSettingsPatch({ diffFilesCollapsed }).diffFilesCollapsed).toBe(
diffFilesCollapsed,
);
});
});

describe("ClientSettings diff colors", () => {
it("keeps red and green for existing settings without a saved palette", () => {
expect(decodeClientSettings({}).diffColorScheme).toBe("red-green");
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ export const ClientSettingsSchema = Schema.Struct({
dismissedProviderUpdateNotificationKeys: Schema.Array(TrimmedNonEmptyString).pipe(
Schema.withDecodingDefault(Effect.succeed([])),
),
diffFilesCollapsed: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
diffLayout: DiffLayout.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_DIFF_LAYOUT))),
environmentIdentificationMode: EnvironmentIdentificationMode.pipe(
Expand Down Expand Up @@ -1432,6 +1433,7 @@ export const ClientSettingsPatch = Schema.Struct({
confirmThreadArchive: Schema.optionalKey(Schema.Boolean),
confirmThreadDelete: Schema.optionalKey(Schema.Boolean),
confirmThreadUnpin: Schema.optionalKey(Schema.Boolean),
diffFilesCollapsed: Schema.optionalKey(Schema.Boolean),
diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean),
diffLayout: Schema.optionalKey(DiffLayout),
environmentIdentificationMode: Schema.optionalKey(EnvironmentIdentificationMode),
Expand Down
Loading