Skip to content

Commit cb8887c

Browse files
feat: add setting to control default project expansion on startup (#1910)
Projects now default to collapsed in the sidebar on restart. A new "Expand projects on startup" toggle in Settings > General lets users restore the previous always-expanded behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5467d11 commit cb8887c

8 files changed

Lines changed: 81 additions & 19 deletions

File tree

apps/desktop/src/clientPersistence.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const clientSettings: ClientSettings = {
5252
confirmThreadArchive: true,
5353
confirmThreadDelete: false,
5454
diffWordWrap: true,
55+
sidebarProjectsDefaultExpanded: false,
5556
sidebarProjectSortOrder: "manual",
5657
sidebarThreadSortOrder: "created_at",
5758
timestampFormat: "24-hour",

apps/web/src/components/Sidebar.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
977977
const defaultThreadEnvMode = useSettings<ThreadEnvMode>(
978978
(settings) => settings.defaultThreadEnvMode,
979979
);
980+
const sidebarProjectsDefaultExpanded = useSettings<boolean>(
981+
(settings) => settings.sidebarProjectsDefaultExpanded,
982+
);
980983
const router = useRouter();
981984
const markThreadUnread = useUiStateStore((state) => state.markThreadUnread);
982985
const toggleProject = useUiStateStore((state) => state.toggleProject);
@@ -1102,7 +1105,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
11021105
// already fetched into allSidebarThreads, so we can use them directly.
11031106
const projectThreads = allSidebarThreads;
11041107
const projectExpanded = useUiStateStore(
1105-
(state) => state.projectExpandedById[project.projectKey] ?? true,
1108+
(state) => state.projectExpandedById[project.projectKey] ?? sidebarProjectsDefaultExpanded,
11061109
);
11071110
const threadLastVisitedAts = useUiStateStore(
11081111
useShallow((state) =>
@@ -1251,13 +1254,14 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
12511254
if (selectedThreadCount > 0) {
12521255
clearSelection();
12531256
}
1254-
toggleProject(project.projectKey);
1257+
toggleProject(project.projectKey, sidebarProjectsDefaultExpanded);
12551258
},
12561259
[
12571260
clearSelection,
12581261
dragInProgressRef,
12591262
project.projectKey,
12601263
selectedThreadCount,
1264+
sidebarProjectsDefaultExpanded,
12611265
suppressProjectClickAfterDragRef,
12621266
suppressProjectClickForContextMenuRef,
12631267
toggleProject,
@@ -1271,9 +1275,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
12711275
if (dragInProgressRef.current) {
12721276
return;
12731277
}
1274-
toggleProject(project.projectKey);
1278+
toggleProject(project.projectKey, sidebarProjectsDefaultExpanded);
12751279
},
1276-
[dragInProgressRef, project.projectKey, toggleProject],
1280+
[dragInProgressRef, project.projectKey, sidebarProjectsDefaultExpanded, toggleProject],
12771281
);
12781282

12791283
const handleProjectButtonPointerDownCapture = useCallback(
@@ -2336,6 +2340,7 @@ export default function Sidebar() {
23362340
const isOnSettings = pathname.startsWith("/settings");
23372341
const sidebarThreadSortOrder = useSettings((s) => s.sidebarThreadSortOrder);
23382342
const sidebarProjectSortOrder = useSettings((s) => s.sidebarProjectSortOrder);
2343+
const sidebarProjectsDefaultExpanded = useSettings((s) => s.sidebarProjectsDefaultExpanded);
23392344
const defaultThreadEnvMode = useSettings((s) => s.defaultThreadEnvMode);
23402345
const { updateSettings } = useUpdateSettings();
23412346
const { handleNewThread } = useNewThreadHandler();
@@ -2777,7 +2782,7 @@ export default function Sidebar() {
27772782
),
27782783
sidebarThreadSortOrder,
27792784
);
2780-
const projectExpanded = projectExpandedById[project.projectKey] ?? true;
2785+
const projectExpanded = projectExpandedById[project.projectKey] ?? sidebarProjectsDefaultExpanded;
27812786
const activeThreadKey = routeThreadKey ?? undefined;
27822787
const pinnedCollapsedThread =
27832788
!projectExpanded && activeThreadKey
@@ -2804,6 +2809,7 @@ export default function Sidebar() {
28042809
}),
28052810
[
28062811
sidebarThreadSortOrder,
2812+
sidebarProjectsDefaultExpanded,
28072813
expandedThreadListsByProject,
28082814
projectExpandedById,
28092815
routeThreadKey,

apps/web/src/components/settings/SettingsPanels.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,10 @@ export function useSettingsRestore(onRestored?: () => void) {
383383
...(settings.confirmThreadDelete !== DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete
384384
? ["Delete confirmation"]
385385
: []),
386+
...(settings.sidebarProjectsDefaultExpanded !==
387+
DEFAULT_UNIFIED_SETTINGS.sidebarProjectsDefaultExpanded
388+
? ["Expand projects on startup"]
389+
: []),
386390
...(isGitWritingModelDirty ? ["Git writing model"] : []),
387391
...(areProviderSettingsDirty ? ["Providers"] : []),
388392
],
@@ -394,6 +398,7 @@ export function useSettingsRestore(onRestored?: () => void) {
394398
settings.defaultThreadEnvMode,
395399
settings.diffWordWrap,
396400
settings.enableAssistantStreaming,
401+
settings.sidebarProjectsDefaultExpanded,
397402
settings.timestampFormat,
398403
theme,
399404
],
@@ -904,6 +909,34 @@ export function GeneralSettingsPanel() {
904909
}
905910
/>
906911

912+
<SettingsRow
913+
title="Expand projects on startup"
914+
description="Show project threads expanded in the sidebar when the app starts."
915+
resetAction={
916+
settings.sidebarProjectsDefaultExpanded !==
917+
DEFAULT_UNIFIED_SETTINGS.sidebarProjectsDefaultExpanded ? (
918+
<SettingResetButton
919+
label="expand projects on startup"
920+
onClick={() =>
921+
updateSettings({
922+
sidebarProjectsDefaultExpanded:
923+
DEFAULT_UNIFIED_SETTINGS.sidebarProjectsDefaultExpanded,
924+
})
925+
}
926+
/>
927+
) : null
928+
}
929+
control={
930+
<Switch
931+
checked={settings.sidebarProjectsDefaultExpanded}
932+
onCheckedChange={(checked) =>
933+
updateSettings({ sidebarProjectsDefaultExpanded: Boolean(checked) })
934+
}
935+
aria-label="Expand projects on startup"
936+
/>
937+
}
938+
/>
939+
907940
<SettingsRow
908941
title="Text generation model"
909942
description="Configure the model used for generated commit messages, PR titles, and similar Git text."

apps/web/src/environments/runtime/service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
selectThreadsAcrossEnvironments,
5858
} from "~/store";
5959
import { useTerminalStateStore } from "~/terminalStateStore";
60+
import { getClientSettingsSnapshot } from "~/hooks/useSettings";
6061
import { useUiStateStore } from "~/uiStateStore";
6162
import { WsTransport } from "../../rpc/wsTransport";
6263
import { createWsRpcClient, type WsRpcClient } from "../../rpc/wsRpcClient";
@@ -179,6 +180,7 @@ function reconcileSnapshotDerivedState() {
179180
key: scopedProjectKey(scopeProjectRef(project.environmentId, project.id)),
180181
cwd: project.cwd,
181182
})),
183+
getClientSettingsSnapshot().sidebarProjectsDefaultExpanded,
182184
);
183185
useUiStateStore.getState().syncThreads(
184186
threads.map((thread) => ({
@@ -242,6 +244,7 @@ function applyRecoveredEventBatch(
242244
key: scopedProjectKey(scopeProjectRef(project.environmentId, project.id)),
243245
cwd: project.cwd,
244246
})),
247+
getClientSettingsSnapshot().sidebarProjectsDefaultExpanded,
245248
);
246249
}
247250

apps/web/src/hooks/useSettings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function emitClientSettingsChange() {
3535
}
3636
}
3737

38-
function getClientSettingsSnapshot(): ClientSettings {
38+
export function getClientSettingsSnapshot(): ClientSettings {
3939
return clientSettingsSnapshot;
4040
}
4141

apps/web/src/localApi.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ describe("wsApi", () => {
503503
confirmThreadArchive: true,
504504
confirmThreadDelete: false,
505505
diffWordWrap: true,
506+
sidebarProjectsDefaultExpanded: false,
506507
sidebarProjectSortOrder: "manual",
507508
sidebarThreadSortOrder: "created_at",
508509
timestampFormat: "24-hour",
@@ -531,6 +532,7 @@ describe("wsApi", () => {
531532
confirmThreadArchive: true,
532533
confirmThreadDelete: false,
533534
diffWordWrap: true,
535+
sidebarProjectsDefaultExpanded: false,
534536
sidebarProjectSortOrder: "manual",
535537
sidebarThreadSortOrder: "created_at",
536538
timestampFormat: "24-hour",
@@ -549,6 +551,7 @@ describe("wsApi", () => {
549551
confirmThreadArchive: true,
550552
confirmThreadDelete: false,
551553
diffWordWrap: true,
554+
sidebarProjectsDefaultExpanded: false,
552555
sidebarProjectSortOrder: "manual",
553556
sidebarThreadSortOrder: "created_at",
554557
timestampFormat: "24-hour",
@@ -568,6 +571,7 @@ describe("wsApi", () => {
568571
confirmThreadArchive: true,
569572
confirmThreadDelete: false,
570573
diffWordWrap: true,
574+
sidebarProjectsDefaultExpanded: false,
571575
sidebarProjectSortOrder: "manual",
572576
sidebarThreadSortOrder: "created_at",
573577
timestampFormat: "24-hour",
@@ -592,6 +596,7 @@ describe("wsApi", () => {
592596
confirmThreadDelete: false,
593597
diffWordWrap: true,
594598
sidebarProjectSortOrder: "manual",
599+
sidebarProjectsDefaultExpanded: false,
595600
sidebarThreadSortOrder: "created_at",
596601
timestampFormat: "24-hour",
597602
});

apps/web/src/uiStateStore.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,11 @@ function nestedBooleanRecordsEqual(
209209
return true;
210210
}
211211

212-
export function syncProjects(state: UiState, projects: readonly SyncProjectInput[]): UiState {
212+
export function syncProjects(
213+
state: UiState,
214+
projects: readonly SyncProjectInput[],
215+
defaultExpanded = false,
216+
): UiState {
213217
const previousProjectCwdById = new Map(currentProjectCwdById);
214218
const previousProjectIdByCwd = new Map(
215219
[...previousProjectCwdById.entries()].map(([projectId, cwd]) => [cwd, projectId] as const),
@@ -234,7 +238,7 @@ export function syncProjects(state: UiState, projects: readonly SyncProjectInput
234238
(previousProjectIdForCwd ? previousExpandedById[previousProjectIdForCwd] : undefined) ??
235239
(persistedExpandedProjectCwds.size > 0
236240
? persistedExpandedProjectCwds.has(project.cwd)
237-
: true);
241+
: defaultExpanded);
238242
nextExpandedById[project.key] = expanded;
239243
return {
240244
id: project.key,
@@ -456,8 +460,8 @@ export function setThreadChangedFilesExpanded(
456460
};
457461
}
458462

459-
export function toggleProject(state: UiState, projectId: string): UiState {
460-
const expanded = state.projectExpandedById[projectId] ?? true;
463+
export function toggleProject(state: UiState, projectId: string, defaultExpanded = false): UiState {
464+
const expanded = state.projectExpandedById[projectId] ?? defaultExpanded;
461465
return {
462466
...state,
463467
projectExpandedById: {
@@ -467,8 +471,13 @@ export function toggleProject(state: UiState, projectId: string): UiState {
467471
};
468472
}
469473

470-
export function setProjectExpanded(state: UiState, projectId: string, expanded: boolean): UiState {
471-
if ((state.projectExpandedById[projectId] ?? true) === expanded) {
474+
export function setProjectExpanded(
475+
state: UiState,
476+
projectId: string,
477+
expanded: boolean,
478+
defaultExpanded = false,
479+
): UiState {
480+
if ((state.projectExpandedById[projectId] ?? defaultExpanded) === expanded) {
472481
return state;
473482
}
474483
return {
@@ -524,14 +533,14 @@ export function reorderProjects(
524533
}
525534

526535
interface UiStateStore extends UiState {
527-
syncProjects: (projects: readonly SyncProjectInput[]) => void;
536+
syncProjects: (projects: readonly SyncProjectInput[], defaultExpanded?: boolean) => void;
528537
syncThreads: (threads: readonly SyncThreadInput[]) => void;
529538
markThreadVisited: (threadId: string, visitedAt?: string) => void;
530539
markThreadUnread: (threadId: string, latestTurnCompletedAt: string | null | undefined) => void;
531540
clearThreadUi: (threadId: string) => void;
532541
setThreadChangedFilesExpanded: (threadId: string, turnId: string, expanded: boolean) => void;
533-
toggleProject: (projectId: string) => void;
534-
setProjectExpanded: (projectId: string, expanded: boolean) => void;
542+
toggleProject: (projectId: string, defaultExpanded?: boolean) => void;
543+
setProjectExpanded: (projectId: string, expanded: boolean, defaultExpanded?: boolean) => void;
535544
reorderProjects: (
536545
draggedProjectIds: readonly string[],
537546
targetProjectIds: readonly string[],
@@ -540,7 +549,8 @@ interface UiStateStore extends UiState {
540549

541550
export const useUiStateStore = create<UiStateStore>((set) => ({
542551
...readPersistedState(),
543-
syncProjects: (projects) => set((state) => syncProjects(state, projects)),
552+
syncProjects: (projects, defaultExpanded) =>
553+
set((state) => syncProjects(state, projects, defaultExpanded)),
544554
syncThreads: (threads) => set((state) => syncThreads(state, threads)),
545555
markThreadVisited: (threadId, visitedAt) =>
546556
set((state) => markThreadVisited(state, threadId, visitedAt)),
@@ -549,9 +559,10 @@ export const useUiStateStore = create<UiStateStore>((set) => ({
549559
clearThreadUi: (threadId) => set((state) => clearThreadUi(state, threadId)),
550560
setThreadChangedFilesExpanded: (threadId, turnId, expanded) =>
551561
set((state) => setThreadChangedFilesExpanded(state, threadId, turnId, expanded)),
552-
toggleProject: (projectId) => set((state) => toggleProject(state, projectId)),
553-
setProjectExpanded: (projectId, expanded) =>
554-
set((state) => setProjectExpanded(state, projectId, expanded)),
562+
toggleProject: (projectId, defaultExpanded) =>
563+
set((state) => toggleProject(state, projectId, defaultExpanded)),
564+
setProjectExpanded: (projectId, expanded, defaultExpanded) =>
565+
set((state) => setProjectExpanded(state, projectId, expanded, defaultExpanded)),
555566
reorderProjects: (draggedProjectIds, targetProjectIds) =>
556567
set((state) => reorderProjects(state, draggedProjectIds, targetProjectIds)),
557568
}));

packages/contracts/src/settings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export const ClientSettingsSchema = Schema.Struct({
2727
confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
2828
confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
2929
diffWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
30+
sidebarProjectsDefaultExpanded: Schema.Boolean.pipe(
31+
Schema.withDecodingDefault(Effect.succeed(false)),
32+
),
3033
sidebarProjectSortOrder: SidebarProjectSortOrder.pipe(
3134
Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_SORT_ORDER)),
3235
),

0 commit comments

Comments
 (0)