diff --git a/desktop/scripts/build-sidecars.test.ts b/desktop/scripts/build-sidecars.test.ts
new file mode 100644
index 00000000..60630e2a
--- /dev/null
+++ b/desktop/scripts/build-sidecars.test.ts
@@ -0,0 +1,19 @@
+import { readFileSync } from 'node:fs'
+import path from 'node:path'
+
+import { describe, expect, it } from 'vitest'
+
+function readBuildScript() {
+ return readFileSync(path.resolve(import.meta.dirname, 'build-sidecars.ts'), 'utf8')
+}
+
+function extractWindowsX64BunTarget(source: string) {
+ const match = source.match(/case 'x86_64-pc-windows-msvc':[\s\S]*?return '([^']+)'/)
+ return match?.[1] ?? null
+}
+
+describe('build-sidecars Windows x64 target mapping', () => {
+ it('uses the baseline Bun runtime so older CPUs do not crash with Illegal Instruction', () => {
+ expect(extractWindowsX64BunTarget(readBuildScript())).toBe('bun-windows-x64-baseline')
+ })
+})
diff --git a/desktop/scripts/build-sidecars.ts b/desktop/scripts/build-sidecars.ts
index a46293dc..c113d117 100644
--- a/desktop/scripts/build-sidecars.ts
+++ b/desktop/scripts/build-sidecars.ts
@@ -72,7 +72,9 @@ function mapTargetTripleToBun(triple: string) {
case 'x86_64-apple-darwin':
return 'bun-darwin-x64'
case 'x86_64-pc-windows-msvc':
- return 'bun-windows-x64'
+ // Prefer baseline on Windows x64 so older CPUs do not crash before the
+ // desktop app can even start the local sidecar process.
+ return 'bun-windows-x64-baseline'
case 'aarch64-pc-windows-msvc':
return 'bun-windows-arm64'
case 'x86_64-unknown-linux-gnu':
diff --git a/desktop/src/components/layout/Sidebar.test.tsx b/desktop/src/components/layout/Sidebar.test.tsx
index 473c41a1..06c6e454 100644
--- a/desktop/src/components/layout/Sidebar.test.tsx
+++ b/desktop/src/components/layout/Sidebar.test.tsx
@@ -54,9 +54,10 @@ vi.mock('../../i18n', () => ({
'sidebar.openInFinder': 'Open in Finder',
'sidebar.openInFinderFailed': 'Could not open the project in Finder.',
'sidebar.openInFinderUnavailable': 'No file manager is available.',
- 'sidebar.hideProjectFromSidebar': 'Remove from Sidebar',
+ 'sidebar.hideProjectFromSidebar': 'Hide from Sidebar',
'sidebar.restoreProjectToSidebar': 'Restore to Sidebar',
- 'sidebar.projectHidden': '{project} was removed from the sidebar.',
+ 'sidebar.restoreHiddenProjects': 'Restore hidden projects ({count})',
+ 'sidebar.projectHidden': '{project} was hidden from the sidebar. Existing sessions were not deleted.',
'sidebar.newSessionInProject': 'New session in {project}',
'sidebar.showMoreSessions': 'Expand display',
'sidebar.showFewerSessions': 'Collapse display',
@@ -517,7 +518,7 @@ describe('Sidebar', () => {
expect(screen.getByRole('menuitem', { name: 'Pin Project' })).toBeInTheDocument()
expect(screen.getByRole('menuitem', { name: 'Open in Finder' })).toBeInTheDocument()
- expect(screen.getByRole('menuitem', { name: 'Remove from Sidebar' })).toBeInTheDocument()
+ expect(screen.getByRole('menuitem', { name: 'Hide from Sidebar' })).toBeInTheDocument()
expect(screen.queryByRole('menuitem', { name: 'Create Permanent Worktree' })).not.toBeInTheDocument()
expect(screen.queryByRole('menuitem', { name: 'Rename Project' })).not.toBeInTheDocument()
expect(screen.queryByRole('menuitem', { name: 'Archive Conversations' })).not.toBeInTheDocument()
@@ -564,7 +565,7 @@ describe('Sidebar', () => {
render()
fireEvent.click(screen.getByRole('button', { name: 'Project actions for beta' }))
- fireEvent.click(screen.getByRole('menuitem', { name: 'Remove from Sidebar' }))
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Hide from Sidebar' }))
await waitFor(() => {
expect(screen.queryByText('beta')).not.toBeInTheDocument()
@@ -575,7 +576,7 @@ describe('Sidebar', () => {
expect(JSON.parse(window.localStorage.getItem(PROJECT_HIDDEN_STORAGE_KEY) ?? '[]')).toEqual(['/workspace/beta'])
expect(addToast).toHaveBeenCalledWith({
type: 'info',
- message: 'beta was removed from the sidebar.',
+ message: 'beta was hidden from the sidebar. Existing sessions were not deleted.',
})
})
@@ -596,6 +597,74 @@ describe('Sidebar', () => {
expect(screen.queryByTestId('project-filter')).not.toBeInTheDocument()
})
+ it('restores hidden projects from the project header menu', async () => {
+ window.localStorage.setItem(PROJECT_HIDDEN_STORAGE_KEY, JSON.stringify(['/workspace/beta']))
+ const now = new Date().toISOString()
+ useSessionStore.setState({
+ sessions: [
+ makeSession('alpha-1', 'Alpha Session', '/workspace/alpha', now),
+ makeSession('beta-1', 'Beta Session', '/workspace/beta', now),
+ ],
+ })
+
+ render()
+
+ expect(screen.getByText('alpha')).toBeInTheDocument()
+ expect(screen.queryByText('beta')).not.toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('button', { name: 'Project menu' }))
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Restore hidden projects (1)' }))
+
+ await waitFor(() => {
+ expect(screen.getByText('beta')).toBeInTheDocument()
+ })
+ expect(JSON.parse(window.localStorage.getItem(PROJECT_HIDDEN_STORAGE_KEY) ?? '[]')).toEqual([])
+ expect(desktopUiPreferencesApiMock.updateSidebarPreferences).toHaveBeenCalledWith({
+ projectOrder: [],
+ pinnedProjects: [],
+ hiddenProjects: [],
+ projectOrganization: 'recentProject',
+ projectSortBy: 'updatedAt',
+ })
+ })
+
+ it('restores a hidden project when a new session is created in that project', async () => {
+ window.localStorage.setItem(PROJECT_HIDDEN_STORAGE_KEY, JSON.stringify(['/workspace/beta']))
+ createSession.mockResolvedValue('beta-new')
+ const now = new Date().toISOString()
+ useSessionStore.setState({
+ sessions: [
+ makeSession('alpha-1', 'Alpha Session', '/workspace/alpha', now),
+ makeSession('beta-1', 'Beta Session', '/workspace/beta', now),
+ ],
+ })
+ useTabStore.setState({
+ tabs: [{ sessionId: 'beta-1', title: 'Beta Session', type: 'session', status: 'idle' }],
+ activeTabId: 'beta-1',
+ })
+
+ render()
+
+ expect(screen.queryByText('beta')).not.toBeInTheDocument()
+
+ await act(async () => {
+ fireEvent.click(screen.getByRole('button', { name: 'New Session' }))
+ })
+
+ await waitFor(() => {
+ expect(createSession).toHaveBeenCalledWith('/workspace/beta')
+ expect(screen.getByText('beta')).toBeInTheDocument()
+ })
+ expect(JSON.parse(window.localStorage.getItem(PROJECT_HIDDEN_STORAGE_KEY) ?? '[]')).toEqual([])
+ expect(desktopUiPreferencesApiMock.updateSidebarPreferences).toHaveBeenCalledWith({
+ projectOrder: [],
+ pinnedProjects: [],
+ hiddenProjects: [],
+ projectOrganization: 'recentProject',
+ projectSortBy: 'updatedAt',
+ })
+ })
+
it('uses server sidebar preferences across browser and desktop storage contexts', async () => {
desktopUiPreferencesApiMock.getPreferences.mockResolvedValueOnce({
exists: true,
diff --git a/desktop/src/components/layout/Sidebar.tsx b/desktop/src/components/layout/Sidebar.tsx
index a4593ddf..8ac7685c 100644
--- a/desktop/src/components/layout/Sidebar.tsx
+++ b/desktop/src/components/layout/Sidebar.tsx
@@ -156,6 +156,22 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
void desktopUiPreferencesApi.updateSidebarPreferences(normalized).catch(() => undefined)
}, [])
+ const restoreHiddenProjectForWorkDir = useCallback((workDir: string | null | undefined) => {
+ if (!workDir) return
+ setHiddenProjectKeys((current) => {
+ const next = new Set([...current].filter((projectKey) => !projectPathMatches(projectKey, workDir)))
+ if (next.size === current.size) return current
+ persistSidebarProjectPreferences(buildSidebarProjectPreferences(
+ projectOrder,
+ pinnedProjectKeys,
+ next,
+ projectOrganization,
+ projectSortBy,
+ ))
+ return next
+ })
+ }, [persistSidebarProjectPreferences, pinnedProjectKeys, projectOrder, projectOrganization, projectSortBy])
+
useEffect(() => {
let cancelled = false
const startRevision = sidebarPreferenceRevisionRef.current
@@ -247,6 +263,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
const createSessionForWorkDir = useCallback(async (workDir?: string) => {
try {
const sessionId = await useSessionStore.getState().createSession(workDir)
+ restoreHiddenProjectForWorkDir(workDir)
useTabStore.getState().openTab(sessionId, t('sidebar.newSession'))
useChatStore.getState().connectToSession(sessionId)
closeMobileDrawer()
@@ -256,7 +273,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
message: error instanceof Error ? error.message : t('sidebar.sessionListFailed'),
})
}
- }, [addToast, closeMobileDrawer, t])
+ }, [addToast, closeMobileDrawer, restoreHiddenProjectForWorkDir, t])
const openProjectHeaderMenu = useCallback((event: React.MouseEvent, type: SidebarHeaderMenuType) => {
event.stopPropagation()
@@ -355,6 +372,23 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
})
}, [hiddenProjectKeys, persistSidebarProjectPreferences, projectOrder, projectOrganization, projectSortBy])
+ const restoreAllHiddenProjects = useCallback(() => {
+ setProjectHeaderMenu(null)
+ setProjectHeaderSubmenu(null)
+ setHiddenProjectKeys((current) => {
+ if (current.size === 0) return current
+ const next = new Set()
+ persistSidebarProjectPreferences(buildSidebarProjectPreferences(
+ projectOrder,
+ pinnedProjectKeys,
+ next,
+ projectOrganization,
+ projectSortBy,
+ ))
+ return next
+ })
+ }, [persistSidebarProjectPreferences, pinnedProjectKeys, projectOrder, projectOrganization, projectSortBy])
+
const toggleHiddenProject = useCallback((project: ProjectGroup) => {
const wasHidden = hiddenProjectKeys.has(project.key)
setProjectContextMenu(null)
@@ -773,7 +807,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
{searchQuery ? t('sidebar.noMatching') : t('sidebar.noSessions')}
)}
- {visibleProjectGroups.length > 0 && (
+ {orderedProjectGroups.length > 0 && (
void createSessionForWorkDir()}
onUseExistingFolder={() => void createSessionFromExistingFolder()}
+ onRestoreHiddenProjects={restoreAllHiddenProjects}
+ hiddenProjectCount={hiddenProjectKeys.size}
t={t}
/>
)}
@@ -1102,6 +1138,8 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
onSetSortBy={updateProjectSortBy}
onCreateBlank={() => void createSessionForWorkDir()}
onUseExistingFolder={() => void createSessionFromExistingFolder()}
+ onRestoreHiddenProjects={restoreAllHiddenProjects}
+ hiddenProjectCount={hiddenProjectKeys.size}
t={t}
/>
)}
@@ -1266,6 +1304,8 @@ function ProjectHeaderMenu({
onSetSortBy,
onCreateBlank,
onUseExistingFolder,
+ onRestoreHiddenProjects,
+ hiddenProjectCount,
t,
}: {
type: SidebarHeaderMenuType
@@ -1278,6 +1318,8 @@ function ProjectHeaderMenu({
onSetSortBy: (sortBy: SidebarProjectSortBy) => void
onCreateBlank: () => void
onUseExistingFolder: () => void
+ onRestoreHiddenProjects: () => void
+ hiddenProjectCount: number
t: ReturnType
}) {
const width = type === 'sort' ? 230 : type === 'create' ? 250 : 270
@@ -1344,6 +1386,14 @@ function ProjectHeaderMenu({
>
{t('sidebar.sortCondition')}
+ {hiddenProjectCount > 0 && (
+ }
+ onClick={onRestoreHiddenProjects}
+ >
+ {t('sidebar.restoreHiddenProjects', { count: hiddenProjectCount })}
+
+ )}
)
}
@@ -1596,6 +1646,20 @@ function normalizeProjectKeyList(values: unknown): string[] {
return normalized
}
+function normalizeProjectPathForComparison(value: string): string {
+ const normalized = value.replace(/\\/g, '/').replace(/\/+$/g, '')
+ return normalized || value
+}
+
+function projectPathMatches(projectKey: string, workDir: string): boolean {
+ const normalizedProjectKey = normalizeProjectPathForComparison(projectKey)
+ const normalizedWorkDir = normalizeProjectPathForComparison(workDir)
+
+ return normalizedProjectKey === normalizedWorkDir
+ || normalizedWorkDir.startsWith(`${normalizedProjectKey}/`)
+ || normalizedProjectKey.startsWith(`${normalizedWorkDir}/`)
+}
+
function hasSidebarProjectPreferences(preferences: SidebarProjectPreferences): boolean {
return preferences.projectOrder.length > 0
|| preferences.pinnedProjects.length > 0
diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts
index 76ff2c90..ff647b3b 100644
--- a/desktop/src/i18n/locales/en.ts
+++ b/desktop/src/i18n/locales/en.ts
@@ -46,9 +46,10 @@ export const en = {
'sidebar.openInFinder': 'Open in Finder',
'sidebar.openInFinderFailed': 'Could not open the project in Finder.',
'sidebar.openInFinderUnavailable': 'No file manager is available.',
- 'sidebar.hideProjectFromSidebar': 'Remove from Sidebar',
+ 'sidebar.hideProjectFromSidebar': 'Hide from Sidebar',
'sidebar.restoreProjectToSidebar': 'Restore to Sidebar',
- 'sidebar.projectHidden': '{project} was removed from the sidebar.',
+ 'sidebar.restoreHiddenProjects': 'Restore hidden projects ({count})',
+ 'sidebar.projectHidden': '{project} was hidden from the sidebar. Existing sessions were not deleted.',
'sidebar.newSessionInProject': 'New session in {project}',
'sidebar.showMoreSessions': 'Expand display',
'sidebar.showFewerSessions': 'Collapse display',
diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts
index f2ad0c1b..43f770bb 100644
--- a/desktop/src/i18n/locales/zh.ts
+++ b/desktop/src/i18n/locales/zh.ts
@@ -48,9 +48,10 @@ export const zh: Record = {
'sidebar.openInFinder': '在“访达”中打开',
'sidebar.openInFinderFailed': '无法在访达中打开项目。',
'sidebar.openInFinderUnavailable': '没有可用的文件管理器。',
- 'sidebar.hideProjectFromSidebar': '从侧边栏移除',
+ 'sidebar.hideProjectFromSidebar': '从侧边栏隐藏',
'sidebar.restoreProjectToSidebar': '恢复到侧边栏',
- 'sidebar.projectHidden': '已从侧边栏移除 {project}。',
+ 'sidebar.restoreHiddenProjects': '恢复隐藏项目({count})',
+ 'sidebar.projectHidden': '已从侧边栏隐藏 {project},已有会话不会被删除。',
'sidebar.newSessionInProject': '在 {project} 中新建会话',
'sidebar.showMoreSessions': '展开显示',
'sidebar.showFewerSessions': '折叠显示',