From fc4d0e616d66d9d7c2bfdacbc17ffd2bbfc26737 Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Fri, 25 Sep 2026 14:02:33 -0400 Subject: [PATCH] fix(desktop): restore folder-pick affordance and filter sidebar to session-bearing projects PR #49037 (projects paradigm) broke the folder->session->sidebar flow (#53004): - The right sidebar's Files pane gated the tree on $currentCwd, which is '' for global/detached sessions, so those sessions hit a dead-end 'No project open' pane with no way back into a folder. Restore an affordance in the empty state: 'Open folder' runs the existing open-folder-as-project flow (upsert + enter project + fresh session anchored at the picked folder), decoupled from $currentCwd. - The sidebar overview listed every auto-promoted repo on disk, session or not. Auto projects with zero sessions now stay out of the sidebar until they own a session (they reappear the moment work lands there); explicit projects and the Home bucket always render. UI-affecting: coordinator should hold auto-merge for visual review. Fixes #53004 --- apps/desktop/src/app/chat/sidebar/index.tsx | 4 +-- .../app/chat/sidebar/project-filter.test.ts | 27 ++++++++++++++++++- .../src/app/chat/sidebar/project-filter.ts | 16 +++++++++++ apps/desktop/src/app/right-sidebar/index.tsx | 19 ++++++++++--- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/chat/sidebar/index.tsx b/apps/desktop/src/app/chat/sidebar/index.tsx index ceec2ebc9e..4d8369e4cd 100644 --- a/apps/desktop/src/app/chat/sidebar/index.tsx +++ b/apps/desktop/src/app/chat/sidebar/index.tsx @@ -157,7 +157,7 @@ import { orderByIds, reconcileOrderIds, resolveManualSessionOrderIds, sameIds } import { filterSessionsByProfileScope } from './profile-scope' import { ProfileRail } from './profile-switcher' import { ProjectDialog } from './project-dialog' -import { resolveLiveProjectFilter } from './project-filter' +import { filterToSessionBearingProjects, resolveLiveProjectFilter } from './project-filter' import { excludeProjectSessions, orderProjectsByIds, @@ -1013,7 +1013,7 @@ export function ChatSidebar({ // overview sort. Membership is the backend tree's — never re-derived here. const projectModel = useMemo(() => { const sorted = sortProjectsForOverview( - filterVisibleProjects(projectTree, dismissedAutoProjects) + filterToSessionBearingProjects(filterVisibleProjects(projectTree, dismissedAutoProjects)) // A filtered-out project drops its whole lane, header included — hiding // only its rows would leave a row of empty folders behind. .filter(project => !projectFilter.length || projectFilter.includes(project.id)) diff --git a/apps/desktop/src/app/chat/sidebar/project-filter.test.ts b/apps/desktop/src/app/chat/sidebar/project-filter.test.ts index a22d0390e3..a363de2ed9 100644 --- a/apps/desktop/src/app/chat/sidebar/project-filter.test.ts +++ b/apps/desktop/src/app/chat/sidebar/project-filter.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { resolveLiveProjectFilter } from './project-filter' +import { filterToSessionBearingProjects, resolveLiveProjectFilter } from './project-filter' describe('resolveLiveProjectFilter', () => { const treeA = [{ id: 'p_aaa' }, { id: '/Users/me/repos/alpha' }] as const @@ -34,3 +34,28 @@ describe('resolveLiveProjectFilter', () => { expect(resolveLiveProjectFilter(['p_aaa'], undefined)).toEqual([]) }) }) + +describe('filterToSessionBearingProjects (#53004)', () => { + const base = { label: 'repo', path: '/tmp/repo', repos: [] } as const + + const tree = [ + { ...base, id: 'auto_empty', isAuto: true, sessionCount: 0 }, + { ...base, id: 'auto_active', isAuto: true, sessionCount: 2 }, + { ...base, id: 'explicit_empty', sessionCount: 0 }, + { ...base, id: 'home', isNoProject: true, sessionCount: 0 } + ] + + it('drops auto projects with no sessions but keeps active autos', () => { + const ids = filterToSessionBearingProjects(tree).map(project => project.id) + + expect(ids).not.toContain('auto_empty') + expect(ids).toContain('auto_active') + }) + + it('never drops explicit projects or the Home bucket', () => { + const ids = filterToSessionBearingProjects(tree).map(project => project.id) + + expect(ids).toContain('explicit_empty') + expect(ids).toContain('home') + }) +}) diff --git a/apps/desktop/src/app/chat/sidebar/project-filter.ts b/apps/desktop/src/app/chat/sidebar/project-filter.ts index 33ab9ddd15..4c1fc894fa 100644 --- a/apps/desktop/src/app/chat/sidebar/project-filter.ts +++ b/apps/desktop/src/app/chat/sidebar/project-filter.ts @@ -20,3 +20,19 @@ export function resolveLiveProjectFilter( return projectFilter.filter(id => liveIds.has(id)) } + +/** + * The sidebar overview is a session history, not a disk browser (#53004): an + * auto-promoted repo with no sessions yet stays out of the sidebar until work + * lands there (it reappears the moment it owns a session). Explicit projects + * and the Home bucket always render — the user created or kept those. Session + * counts come from the backend `projects.tree`, so this stays live without + * client-side re-derivation. + */ +export function filterToSessionBearingProjects< + T extends { isAuto?: boolean; isNoProject?: boolean; sessionCount?: number } +>(projects: readonly T[]): T[] { + return projects.filter( + project => !project.isAuto || project.isNoProject || (project.sessionCount ?? 0) > 0 + ) +} diff --git a/apps/desktop/src/app/right-sidebar/index.tsx b/apps/desktop/src/app/right-sidebar/index.tsx index 77a7a70aed..e1e824c7a6 100644 --- a/apps/desktop/src/app/right-sidebar/index.tsx +++ b/apps/desktop/src/app/right-sidebar/index.tsx @@ -13,6 +13,7 @@ import { cn } from '@/lib/utils' import { $panesFlipped } from '@/store/layout' import { notifyError } from '@/store/notifications' import { openPreview } from '@/store/preview' +import { openFolderAsProject } from '@/store/projects' import { $currentCwd, $selectedStoredSessionId, $workspaceCwdOwner } from '@/store/session' import { SidebarPanelLabel } from '../shell/sidebar-label' @@ -149,10 +150,22 @@ function FilesystemTab({ const { t } = useI18n() const r = t.rightSidebar - // No working directory (a bare/detached chat) → no tree, just a terse hint. - // Switching workspace is a project/worktree action, never a raw folder picker. + // No working directory (a bare/detached chat) → no tree, but keep a way back + // into a folder (#53004): the projects paradigm removed the old folder picker, + // which stranded global sessions on a dead-end "No project open" pane. The + // affordance is the project-shaped one — ⌘O's open-folder-as-project flow, + // which upserts/enters the project and anchors a fresh session at the picked + // folder — entirely decoupled from $currentCwd. if (!hasWorkspace) { - return + return ( +
+ {r.noProjectOpen} + +
+ ) } return (