Skip to content
Open
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/clientPersistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const clientSettings: ClientSettings = {
confirmThreadArchive: true,
confirmThreadDelete: false,
diffWordWrap: true,
sidebarProjectsDefaultExpanded: false,
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
timestampFormat: "24-hour",
Expand Down
16 changes: 11 additions & 5 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
const defaultThreadEnvMode = useSettings<ThreadEnvMode>(
(settings) => settings.defaultThreadEnvMode,
);
const sidebarProjectsDefaultExpanded = useSettings<boolean>(
(settings) => settings.sidebarProjectsDefaultExpanded,
);
const router = useRouter();
const markThreadUnread = useUiStateStore((state) => state.markThreadUnread);
const toggleProject = useUiStateStore((state) => state.toggleProject);
Expand Down Expand Up @@ -1102,7 +1105,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
// already fetched into allSidebarThreads, so we can use them directly.
const projectThreads = allSidebarThreads;
const projectExpanded = useUiStateStore(
(state) => state.projectExpandedById[project.projectKey] ?? true,
(state) => state.projectExpandedById[project.projectKey] ?? sidebarProjectsDefaultExpanded,
);
const threadLastVisitedAts = useUiStateStore(
useShallow((state) =>
Expand Down Expand Up @@ -1251,13 +1254,14 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
if (selectedThreadCount > 0) {
clearSelection();
}
toggleProject(project.projectKey);
toggleProject(project.projectKey, sidebarProjectsDefaultExpanded);
},
[
clearSelection,
dragInProgressRef,
project.projectKey,
selectedThreadCount,
sidebarProjectsDefaultExpanded,
suppressProjectClickAfterDragRef,
suppressProjectClickForContextMenuRef,
toggleProject,
Expand All @@ -1271,9 +1275,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
if (dragInProgressRef.current) {
return;
}
toggleProject(project.projectKey);
toggleProject(project.projectKey, sidebarProjectsDefaultExpanded);
},
[dragInProgressRef, project.projectKey, toggleProject],
[dragInProgressRef, project.projectKey, sidebarProjectsDefaultExpanded, toggleProject],
);

const handleProjectButtonPointerDownCapture = useCallback(
Expand Down Expand Up @@ -2336,6 +2340,7 @@ export default function Sidebar() {
const isOnSettings = pathname.startsWith("/settings");
const sidebarThreadSortOrder = useSettings((s) => s.sidebarThreadSortOrder);
const sidebarProjectSortOrder = useSettings((s) => s.sidebarProjectSortOrder);
const sidebarProjectsDefaultExpanded = useSettings((s) => s.sidebarProjectsDefaultExpanded);
const defaultThreadEnvMode = useSettings((s) => s.defaultThreadEnvMode);
const { updateSettings } = useUpdateSettings();
const { handleNewThread } = useNewThreadHandler();
Expand Down Expand Up @@ -2777,7 +2782,7 @@ export default function Sidebar() {
),
sidebarThreadSortOrder,
);
const projectExpanded = projectExpandedById[project.projectKey] ?? true;
const projectExpanded = projectExpandedById[project.projectKey] ?? sidebarProjectsDefaultExpanded;
const activeThreadKey = routeThreadKey ?? undefined;
const pinnedCollapsedThread =
!projectExpanded && activeThreadKey
Expand All @@ -2804,6 +2809,7 @@ export default function Sidebar() {
}),
[
sidebarThreadSortOrder,
sidebarProjectsDefaultExpanded,
expandedThreadListsByProject,
projectExpandedById,
routeThreadKey,
Expand Down
33 changes: 33 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.confirmThreadDelete !== DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete
? ["Delete confirmation"]
: []),
...(settings.sidebarProjectsDefaultExpanded !==
DEFAULT_UNIFIED_SETTINGS.sidebarProjectsDefaultExpanded
? ["Expand projects on startup"]
: []),
...(isGitWritingModelDirty ? ["Git writing model"] : []),
...(areProviderSettingsDirty ? ["Providers"] : []),
],
Expand All @@ -394,6 +398,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.defaultThreadEnvMode,
settings.diffWordWrap,
settings.enableAssistantStreaming,
settings.sidebarProjectsDefaultExpanded,
settings.timestampFormat,
theme,
],
Expand Down Expand Up @@ -904,6 +909,34 @@ export function GeneralSettingsPanel() {
}
/>

<SettingsRow
title="Expand projects on startup"
description="Show project threads expanded in the sidebar when the app starts."
resetAction={
settings.sidebarProjectsDefaultExpanded !==
DEFAULT_UNIFIED_SETTINGS.sidebarProjectsDefaultExpanded ? (
<SettingResetButton
label="expand projects on startup"
onClick={() =>
updateSettings({
sidebarProjectsDefaultExpanded:
DEFAULT_UNIFIED_SETTINGS.sidebarProjectsDefaultExpanded,
})
}
/>
) : null
}
control={
<Switch
checked={settings.sidebarProjectsDefaultExpanded}
onCheckedChange={(checked) =>
updateSettings({ sidebarProjectsDefaultExpanded: Boolean(checked) })
}
aria-label="Expand projects on startup"
/>
}
/>

<SettingsRow
title="Text generation model"
description="Configure the model used for generated commit messages, PR titles, and similar Git text."
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/environments/runtime/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
selectThreadsAcrossEnvironments,
} from "~/store";
import { useTerminalStateStore } from "~/terminalStateStore";
import { getClientSettingsSnapshot } from "~/hooks/useSettings";
import { useUiStateStore } from "~/uiStateStore";
import { WsTransport } from "../../rpc/wsTransport";
import { createWsRpcClient, type WsRpcClient } from "../../rpc/wsRpcClient";
Expand Down Expand Up @@ -179,6 +180,7 @@ function reconcileSnapshotDerivedState() {
key: scopedProjectKey(scopeProjectRef(project.environmentId, project.id)),
cwd: project.cwd,
})),
getClientSettingsSnapshot().sidebarProjectsDefaultExpanded,
);
useUiStateStore.getState().syncThreads(
threads.map((thread) => ({
Expand Down Expand Up @@ -242,6 +244,7 @@ function applyRecoveredEventBatch(
key: scopedProjectKey(scopeProjectRef(project.environmentId, project.id)),
cwd: project.cwd,
})),
getClientSettingsSnapshot().sidebarProjectsDefaultExpanded,
);
}

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/hooks/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function emitClientSettingsChange() {
}
}

function getClientSettingsSnapshot(): ClientSettings {
export function getClientSettingsSnapshot(): ClientSettings {
return clientSettingsSnapshot;
}

Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/localApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ describe("wsApi", () => {
confirmThreadArchive: true,
confirmThreadDelete: false,
diffWordWrap: true,
sidebarProjectsDefaultExpanded: false,
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
timestampFormat: "24-hour",
Expand Down Expand Up @@ -531,6 +532,7 @@ describe("wsApi", () => {
confirmThreadArchive: true,
confirmThreadDelete: false,
diffWordWrap: true,
sidebarProjectsDefaultExpanded: false,
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
timestampFormat: "24-hour",
Expand All @@ -549,6 +551,7 @@ describe("wsApi", () => {
confirmThreadArchive: true,
confirmThreadDelete: false,
diffWordWrap: true,
sidebarProjectsDefaultExpanded: false,
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
timestampFormat: "24-hour",
Expand All @@ -568,6 +571,7 @@ describe("wsApi", () => {
confirmThreadArchive: true,
confirmThreadDelete: false,
diffWordWrap: true,
sidebarProjectsDefaultExpanded: false,
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
timestampFormat: "24-hour",
Expand All @@ -592,6 +596,7 @@ describe("wsApi", () => {
confirmThreadDelete: false,
diffWordWrap: true,
sidebarProjectSortOrder: "manual",
sidebarProjectsDefaultExpanded: false,
sidebarThreadSortOrder: "created_at",
timestampFormat: "24-hour",
});
Expand Down
30 changes: 20 additions & 10 deletions apps/web/src/uiStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,11 @@ function nestedBooleanRecordsEqual(
return true;
}

export function syncProjects(state: UiState, projects: readonly SyncProjectInput[]): UiState {
export function syncProjects(
state: UiState,
projects: readonly SyncProjectInput[],
defaultExpanded = false,
): UiState {
const previousProjectCwdById = new Map(currentProjectCwdById);
const previousProjectIdByCwd = new Map(
[...previousProjectCwdById.entries()].map(([projectId, cwd]) => [cwd, projectId] as const),
Expand All @@ -234,7 +238,7 @@ export function syncProjects(state: UiState, projects: readonly SyncProjectInput
(previousProjectIdForCwd ? previousExpandedById[previousProjectIdForCwd] : undefined) ??
(persistedExpandedProjectCwds.size > 0
? persistedExpandedProjectCwds.has(project.cwd)
: true);
: defaultExpanded);
nextExpandedById[project.key] = expanded;
return {
id: project.key,
Expand Down Expand Up @@ -456,8 +460,8 @@ export function setThreadChangedFilesExpanded(
};
}

export function toggleProject(state: UiState, projectId: string): UiState {
const expanded = state.projectExpandedById[projectId] ?? true;
export function toggleProject(state: UiState, projectId: string, defaultExpanded = false): UiState {
const expanded = state.projectExpandedById[projectId] ?? defaultExpanded;
return {
...state,
projectExpandedById: {
Expand All @@ -467,8 +471,12 @@ export function toggleProject(state: UiState, projectId: string): UiState {
};
}

export function setProjectExpanded(state: UiState, projectId: string, expanded: boolean): UiState {
if ((state.projectExpandedById[projectId] ?? true) === expanded) {
export function setProjectExpanded(
state: UiState,
projectId: string,
expanded: boolean,
): UiState {
if (state.projectExpandedById[projectId] === expanded) {
return state;
}
return {
Expand Down Expand Up @@ -524,13 +532,13 @@ export function reorderProjects(
}

interface UiStateStore extends UiState {
syncProjects: (projects: readonly SyncProjectInput[]) => void;
syncProjects: (projects: readonly SyncProjectInput[], defaultExpanded?: boolean) => void;
syncThreads: (threads: readonly SyncThreadInput[]) => void;
markThreadVisited: (threadId: string, visitedAt?: string) => void;
markThreadUnread: (threadId: string, latestTurnCompletedAt: string | null | undefined) => void;
clearThreadUi: (threadId: string) => void;
setThreadChangedFilesExpanded: (threadId: string, turnId: string, expanded: boolean) => void;
toggleProject: (projectId: string) => void;
toggleProject: (projectId: string, defaultExpanded?: boolean) => void;
setProjectExpanded: (projectId: string, expanded: boolean) => void;
reorderProjects: (
draggedProjectIds: readonly string[],
Expand All @@ -540,7 +548,8 @@ interface UiStateStore extends UiState {

export const useUiStateStore = create<UiStateStore>((set) => ({
...readPersistedState(),
syncProjects: (projects) => set((state) => syncProjects(state, projects)),
syncProjects: (projects, defaultExpanded) =>
set((state) => syncProjects(state, projects, defaultExpanded)),
syncThreads: (threads) => set((state) => syncThreads(state, threads)),
markThreadVisited: (threadId, visitedAt) =>
set((state) => markThreadVisited(state, threadId, visitedAt)),
Expand All @@ -549,7 +558,8 @@ export const useUiStateStore = create<UiStateStore>((set) => ({
clearThreadUi: (threadId) => set((state) => clearThreadUi(state, threadId)),
setThreadChangedFilesExpanded: (threadId, turnId, expanded) =>
set((state) => setThreadChangedFilesExpanded(state, threadId, turnId, expanded)),
toggleProject: (projectId) => set((state) => toggleProject(state, projectId)),
toggleProject: (projectId, defaultExpanded) =>
set((state) => toggleProject(state, projectId, defaultExpanded)),
setProjectExpanded: (projectId, expanded) =>
set((state) => setProjectExpanded(state, projectId, expanded)),
reorderProjects: (draggedProjectIds, targetProjectIds) =>
Expand Down
3 changes: 3 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export const ClientSettingsSchema = Schema.Struct({
confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
diffWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
sidebarProjectsDefaultExpanded: Schema.Boolean.pipe(
Schema.withDecodingDefault(Effect.succeed(false)),
),
sidebarProjectSortOrder: SidebarProjectSortOrder.pipe(
Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_SORT_ORDER)),
),
Expand Down
Loading