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
This commit is contained in:
@@ -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<SidebarProjectTree[]>(() => {
|
||||
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))
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 <PaneEmptyState label={r.noProjectOpen} />
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-2 px-4 text-center">
|
||||
<SidebarPanelLabel className="pl-0 text-(--ui-text-quaternary)">{r.noProjectOpen}</SidebarPanelLabel>
|
||||
<Button className="h-7 gap-1.5 text-xs" onClick={() => void openFolderAsProject()} size="sm" variant="outline">
|
||||
<Codicon name="folder-opened" size="0.8125rem" />
|
||||
{r.openFolder}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user