Skip to content

Commit 510df21

Browse files
committed
Add project sidebar expand-all toggle
- Add a sidebar control to expand or collapse every project at once - Default unseen projects to collapsed when loading legacy persisted state - Update store coverage for the new all-projects expansion behavior
1 parent 14a9282 commit 510df21

3 files changed

Lines changed: 44 additions & 3 deletions

File tree

apps/web/src/components/Sidebar.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {
2929
ArrowLeftIcon,
3030
ArrowUpDownIcon,
3131
CheckCircleIcon,
32+
ChevronsDownUpIcon,
33+
ChevronsUpDownIcon,
3234
CircleDotIcon,
3335
CloudUploadIcon,
3436
ExternalLinkIcon,
@@ -462,6 +464,7 @@ export default function Sidebar() {
462464
const threads = useStore((store) => store.threads);
463465
const markThreadUnread = useStore((store) => store.markThreadUnread);
464466
const toggleProject = useStore((store) => store.toggleProject);
467+
const setAllProjectsExpanded = useStore((store) => store.setAllProjectsExpanded);
465468
const reorderProjects = useStore((store) => store.reorderProjects);
466469
const clearComposerDraftForThread = useComposerDraftStore((store) => store.clearDraftThread);
467470
const getDraftThreadByProjectId = useComposerDraftStore(
@@ -1250,6 +1253,7 @@ export default function Sidebar() {
12501253
});
12511254
}, [projectById, threads]);
12521255
const isManualProjectSorting = appSettings.sidebarProjectSortOrder === "manual";
1256+
const allProjectsExpanded = projects.length > 0 && projects.every((p) => p.expanded);
12531257

12541258
function renderProjectItem(
12551259
project: (typeof sortedProjects)[number],
@@ -1849,6 +1853,31 @@ export default function Sidebar() {
18491853
Projects
18501854
</span>
18511855
<div className="flex items-center gap-1">
1856+
<Tooltip>
1857+
<TooltipTrigger
1858+
render={
1859+
<button
1860+
type="button"
1861+
aria-label={
1862+
allProjectsExpanded ? "Collapse all projects" : "Expand all projects"
1863+
}
1864+
className="inline-flex size-5 cursor-pointer items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-accent hover:text-foreground"
1865+
onClick={() => {
1866+
setAllProjectsExpanded(!allProjectsExpanded);
1867+
}}
1868+
/>
1869+
}
1870+
>
1871+
{allProjectsExpanded ? (
1872+
<ChevronsDownUpIcon className="size-3.5" />
1873+
) : (
1874+
<ChevronsUpDownIcon className="size-3.5" />
1875+
)}
1876+
</TooltipTrigger>
1877+
<TooltipPopup side="top">
1878+
{allProjectsExpanded ? "Collapse all projects" : "Expand all projects"}
1879+
</TooltipPopup>
1880+
</Tooltip>
18521881
<Tooltip>
18531882
<TooltipTrigger
18541883
render={

apps/web/src/store.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ describe("store pure functions", () => {
133133
expect(parsed.projectOrderCwds).toEqual(["/tmp/b", "/tmp/a"]);
134134
});
135135

136-
it("treats legacy expanded project data as hints without collapsing unseen projects", () => {
136+
it("treats legacy expanded project data as hints and defaults unseen projects to collapsed", () => {
137137
const parsed = parsePersistedProjectUiState(
138138
JSON.stringify({
139139
expandedProjectCwds: ["/tmp/a"],
@@ -151,7 +151,7 @@ describe("store pure functions", () => {
151151
existingExpanded: undefined,
152152
persistedExpanded: parsed.projectExpansionByCwd.get("/tmp/missing"),
153153
}),
154-
).toBe(true);
154+
).toBe(false);
155155
});
156156

157157
it("markThreadUnread moves lastVisitedAt before completion for a completed thread", () => {

apps/web/src/store.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export function resolveProjectExpandedState(input: {
9292
existingExpanded: boolean | undefined;
9393
persistedExpanded: boolean | undefined;
9494
}): boolean {
95-
return input.existingExpanded ?? input.persistedExpanded ?? true;
95+
return input.existingExpanded ?? input.persistedExpanded ?? false;
9696
}
9797

9898
function readPersistedState(): AppState {
@@ -431,6 +431,16 @@ export function setProjectExpanded(
431431
return changed ? { ...state, projects } : state;
432432
}
433433

434+
export function setAllProjectsExpanded(state: AppState, expanded: boolean): AppState {
435+
let changed = false;
436+
const projects = state.projects.map((p) => {
437+
if (p.expanded === expanded) return p;
438+
changed = true;
439+
return { ...p, expanded };
440+
});
441+
return changed ? { ...state, projects } : state;
442+
}
443+
434444
export function reorderProjects(
435445
state: AppState,
436446
draggedProjectId: Project["id"],
@@ -495,6 +505,7 @@ interface AppStore extends AppState {
495505
markThreadUnread: (threadId: ThreadId) => void;
496506
toggleProject: (projectId: Project["id"]) => void;
497507
setProjectExpanded: (projectId: Project["id"], expanded: boolean) => void;
508+
setAllProjectsExpanded: (expanded: boolean) => void;
498509
reorderProjects: (draggedProjectId: Project["id"], targetProjectId: Project["id"]) => void;
499510
setError: (threadId: ThreadId, error: string | null) => void;
500511
setThreadBranch: (threadId: ThreadId, branch: string | null, worktreePath: string | null) => void;
@@ -510,6 +521,7 @@ export const useStore = create<AppStore>((set) => ({
510521
toggleProject: (projectId) => set((state) => toggleProject(state, projectId)),
511522
setProjectExpanded: (projectId, expanded) =>
512523
set((state) => setProjectExpanded(state, projectId, expanded)),
524+
setAllProjectsExpanded: (expanded) => set((state) => setAllProjectsExpanded(state, expanded)),
513525
reorderProjects: (draggedProjectId, targetProjectId) =>
514526
set((state) => reorderProjects(state, draggedProjectId, targetProjectId)),
515527
setError: (threadId, error) => set((state) => setError(state, threadId, error)),

0 commit comments

Comments
 (0)