+}) {
+ const width = type === 'sort' ? 230 : type === 'create' ? 250 : 270
+ const style: React.CSSProperties = { left: x, top: y, width, boxShadow: 'var(--shadow-dropdown)' }
+ const className = 'fixed z-50 overflow-hidden rounded-[18px] border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] py-2 shadow-[var(--shadow-dropdown)]'
+
+ if (type === 'create') {
+ return (
+ event.stopPropagation()}>
+ } onClick={onCreateBlank}>
+ {t('sidebar.newBlankProject')}
+
+ } onClick={onUseExistingFolder}>
+ {t('sidebar.useExistingFolder')}
+
+
+ )
+ }
+
+ if (type === 'organize') {
+ return (
+ event.stopPropagation()}>
+ } checked={organization === 'project'} onClick={() => onSetOrganization('project')}>
+ {t('sidebar.organizeByProject')}
+
+ } checked={organization === 'recentProject'} onClick={() => onSetOrganization('recentProject')}>
+ {t('sidebar.organizeByRecentProject')}
+
+ } checked={organization === 'time'} onClick={() => onSetOrganization('time')}>
+ {t('sidebar.organizeByTime')}
+
+
+ )
+ }
+
+ if (type === 'sort') {
+ return (
+ event.stopPropagation()}>
+ } checked={sortBy === 'createdAt'} onClick={() => onSetSortBy('createdAt')}>
+ {t('sidebar.sortByCreatedAt')}
+
+ } checked={sortBy === 'updatedAt'} onClick={() => onSetSortBy('updatedAt')}>
+ {t('sidebar.sortByUpdatedAt')}
+
+
+ )
+ }
+
+ return (
+ event.stopPropagation()}>
+
} onClick={onArchiveAll}>
+ {t('sidebar.archiveAllChats')}
+
+
+
}
+ trailing
+ onMouseEnter={(event) => onOpenSubmenu(event, 'organize')}
+ onClick={(event) => onOpenSubmenu(event, 'organize')}
+ >
+ {t('sidebar.organizeSidebar')}
+
+
}
+ trailing
+ onMouseEnter={(event) => onOpenSubmenu(event, 'sort')}
+ onClick={(event) => onOpenSubmenu(event, 'sort')}
+ >
+ {t('sidebar.sortCondition')}
+
+
+ )
+}
+
+function HeaderMenuItem({
+ icon,
+ children,
+ onClick,
+ onMouseEnter,
+ checked = false,
+ trailing = false,
+}: {
+ icon: React.ReactNode
+ children: React.ReactNode
+ onClick: (event: React.MouseEvent) => void
+ onMouseEnter?: (event: React.MouseEvent) => void
+ checked?: boolean
+ trailing?: boolean
+}) {
+ return (
+
+ )
+}
+
+function groupByProject(sessions: SessionListItem[], sortBy: SidebarProjectSortBy): ProjectGroup[] {
const groupsByKey = new Map()
for (const session of sessions) {
const key = getSessionProjectKey(session)
@@ -1101,7 +1421,7 @@ function groupByProject(sessions: SessionListItem[]): ProjectGroup[] {
}
const groups = [...groupsByKey.entries()].map(([key, items]) => {
- const sortedSessions = [...items].sort(compareSessionsByModifiedAt)
+ const sortedSessions = [...items].sort((a, b) => compareSessionsByTimestamp(a, b, sortBy))
const newest = sortedSessions[0]
const projectRoot = newest?.projectRoot || newest?.workDir || key
return {
@@ -1113,25 +1433,28 @@ function groupByProject(sessions: SessionListItem[]): ProjectGroup[] {
}
})
- return groups.sort((a, b) => compareSessionsByModifiedAt(a.sessions[0], b.sessions[0]))
+ return groups.sort((a, b) => compareSessionsByTimestamp(a.sessions[0], b.sessions[0], sortBy))
}
function applyProjectOrder(
groups: ProjectGroup[],
projectOrder: string[],
pinnedProjectKeys: Set,
+ organization: SidebarProjectOrganization,
+ sortBy: SidebarProjectSortBy,
): ProjectGroup[] {
const orderIndex = new Map(projectOrder.map((key, index) => [key, index]))
return [...groups].sort((a, b) => {
const aPinned = pinnedProjectKeys.has(a.key)
const bPinned = pinnedProjectKeys.has(b.key)
if (aPinned !== bPinned) return aPinned ? -1 : 1
+ if (organization === 'project') return a.title.localeCompare(b.title)
const aIndex = orderIndex.get(a.key)
const bIndex = orderIndex.get(b.key)
if (aIndex !== undefined && bIndex !== undefined) return aIndex - bIndex
if (aIndex !== undefined) return -1
if (bIndex !== undefined) return 1
- return compareSessionsByModifiedAt(a.sessions[0], b.sessions[0])
+ return compareSessionsByTimestamp(a.sessions[0], b.sessions[0], sortBy)
})
}
@@ -1214,15 +1537,47 @@ function writeStoredProjectHidden(projectKeys: Set): void {
}
}
+function readStoredProjectOrganization(): SidebarProjectOrganization {
+ if (typeof localStorage === 'undefined') return 'recentProject'
+ return normalizeProjectOrganization(localStorage.getItem(PROJECT_ORGANIZATION_STORAGE_KEY))
+}
+
+function writeStoredProjectOrganization(organization: SidebarProjectOrganization): void {
+ if (typeof localStorage === 'undefined') return
+ try {
+ localStorage.setItem(PROJECT_ORGANIZATION_STORAGE_KEY, organization)
+ } catch {
+ // Sidebar organization is a UI preference; ignore storage failures.
+ }
+}
+
+function readStoredProjectSortBy(): SidebarProjectSortBy {
+ if (typeof localStorage === 'undefined') return 'updatedAt'
+ return normalizeProjectSortBy(localStorage.getItem(PROJECT_SORT_STORAGE_KEY))
+}
+
+function writeStoredProjectSortBy(sortBy: SidebarProjectSortBy): void {
+ if (typeof localStorage === 'undefined') return
+ try {
+ localStorage.setItem(PROJECT_SORT_STORAGE_KEY, sortBy)
+ } catch {
+ // Sidebar sorting is a UI preference; ignore storage failures.
+ }
+}
+
function buildSidebarProjectPreferences(
projectOrder: string[],
pinnedProjectKeys: Set,
hiddenProjectKeys: Set,
+ projectOrganization: SidebarProjectOrganization,
+ projectSortBy: SidebarProjectSortBy,
): SidebarProjectPreferences {
return normalizeSidebarProjectPreferences({
projectOrder,
pinnedProjects: [...pinnedProjectKeys],
hiddenProjects: [...hiddenProjectKeys],
+ projectOrganization,
+ projectSortBy,
})
}
@@ -1231,6 +1586,8 @@ function readCachedSidebarProjectPreferences(): SidebarProjectPreferences {
projectOrder: readStoredProjectOrder(),
pinnedProjects: [...readStoredProjectPins()],
hiddenProjects: [...readStoredProjectHidden()],
+ projectOrganization: readStoredProjectOrganization(),
+ projectSortBy: readStoredProjectSortBy(),
}
}
@@ -1239,6 +1596,8 @@ function writeCachedSidebarProjectPreferences(preferences: SidebarProjectPrefere
writeStoredProjectOrder(normalized.projectOrder)
writeStoredProjectPins(new Set(normalized.pinnedProjects))
writeStoredProjectHidden(new Set(normalized.hiddenProjects))
+ writeStoredProjectOrganization(normalized.projectOrganization)
+ writeStoredProjectSortBy(normalized.projectSortBy)
}
function normalizeSidebarProjectPreferences(preferences: Partial | undefined): SidebarProjectPreferences {
@@ -1246,6 +1605,8 @@ function normalizeSidebarProjectPreferences(preferences: Partial 0
|| preferences.pinnedProjects.length > 0
|| preferences.hiddenProjects.length > 0
+ || preferences.projectOrganization !== 'recentProject'
+ || preferences.projectSortBy !== 'updatedAt'
+}
+
+function normalizeProjectOrganization(value: unknown): SidebarProjectOrganization {
+ return value === 'project' || value === 'recentProject' || value === 'time' ? value : 'recentProject'
+}
+
+function normalizeProjectSortBy(value: unknown): SidebarProjectSortBy {
+ return value === 'createdAt' || value === 'updatedAt' ? value : 'updatedAt'
}
function getVisibleProjectSessions(
@@ -1287,11 +1658,18 @@ function getSessionProjectKey(session: SessionListItem): string {
return session.projectRoot || session.workDir || session.projectPath || 'unknown'
}
-function compareSessionsByModifiedAt(
+function compareSessionsByTimestamp(
a: SessionListItem | undefined,
b: SessionListItem | undefined,
+ sortBy: SidebarProjectSortBy,
): number {
- return new Date(b?.modifiedAt ?? 0).getTime() - new Date(a?.modifiedAt ?? 0).getTime()
+ return getSessionTimestamp(b, sortBy) - getSessionTimestamp(a, sortBy)
+}
+
+function getSessionTimestamp(session: SessionListItem | undefined, sortBy: SidebarProjectSortBy): number {
+ const value = sortBy === 'createdAt' ? session?.createdAt : session?.modifiedAt
+ const timestamp = new Date(value ?? 0).getTime()
+ return Number.isFinite(timestamp) ? timestamp : 0
}
function projectTitle(pathLike: string | null | undefined): string {
diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts
index 102dbcdd..f8df0118 100644
--- a/desktop/src/i18n/locales/en.ts
+++ b/desktop/src/i18n/locales/en.ts
@@ -27,6 +27,19 @@ export const en = {
'sidebar.sessionListFailed': 'Session list failed to load',
'sidebar.refreshSessions': 'Refresh sessions',
'sidebar.projects': 'Projects',
+ 'sidebar.projectMenu': 'Project menu',
+ 'sidebar.newProject': 'New project',
+ 'sidebar.archiveAllChats': 'Archive all chats',
+ 'sidebar.organizeSidebar': 'Organize sidebar',
+ 'sidebar.sortCondition': 'Sort condition',
+ 'sidebar.organizeByProject': 'By project',
+ 'sidebar.organizeByRecentProject': 'Recent projects',
+ 'sidebar.organizeByTime': 'By time',
+ 'sidebar.sortByCreatedAt': 'Created time',
+ 'sidebar.sortByUpdatedAt': 'Updated time',
+ 'sidebar.newBlankProject': 'New blank project',
+ 'sidebar.useExistingFolder': 'Use existing folder',
+ 'sidebar.chooseProjectFolderUnavailable': 'Folder selection is only available in the desktop app.',
'sidebar.projectActions': 'Project actions for {project}',
'sidebar.pinProject': 'Pin Project',
'sidebar.unpinProject': 'Unpin Project',
diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts
index 2135c3b4..3cfcc286 100644
--- a/desktop/src/i18n/locales/zh.ts
+++ b/desktop/src/i18n/locales/zh.ts
@@ -29,6 +29,19 @@ export const zh: Record = {
'sidebar.sessionListFailed': '会话列表加载失败',
'sidebar.refreshSessions': '刷新会话列表',
'sidebar.projects': '项目',
+ 'sidebar.projectMenu': '项目菜单',
+ 'sidebar.newProject': '新建项目',
+ 'sidebar.archiveAllChats': '归档所有聊天',
+ 'sidebar.organizeSidebar': '整理侧边栏',
+ 'sidebar.sortCondition': '排序条件',
+ 'sidebar.organizeByProject': '按项目',
+ 'sidebar.organizeByRecentProject': '近期项目',
+ 'sidebar.organizeByTime': '按时间顺序',
+ 'sidebar.sortByCreatedAt': '创建时间',
+ 'sidebar.sortByUpdatedAt': '更新时间',
+ 'sidebar.newBlankProject': '新建空白项目',
+ 'sidebar.useExistingFolder': '使用现有文件夹',
+ 'sidebar.chooseProjectFolderUnavailable': '选择文件夹仅在桌面端可用。',
'sidebar.projectActions': '{project} 项目操作',
'sidebar.pinProject': '置顶项目',
'sidebar.unpinProject': '取消置顶项目',
diff --git a/src/server/__tests__/desktop-ui-preferences.test.ts b/src/server/__tests__/desktop-ui-preferences.test.ts
index 92c4c1bf..cb0af3c1 100644
--- a/src/server/__tests__/desktop-ui-preferences.test.ts
+++ b/src/server/__tests__/desktop-ui-preferences.test.ts
@@ -60,6 +60,8 @@ describe('DesktopUiPreferencesService', () => {
projectOrder: [],
pinnedProjects: [],
hiddenProjects: [],
+ projectOrganization: 'recentProject',
+ projectSortBy: 'updatedAt',
},
})
})
@@ -95,6 +97,8 @@ describe('DesktopUiPreferencesService', () => {
projectOrder: ['/workspace/alpha', '/workspace/beta'],
pinnedProjects: ['/workspace/beta'],
hiddenProjects: ['/workspace/gamma'],
+ projectOrganization: 'recentProject',
+ projectSortBy: 'updatedAt',
},
})
expect(after).toEqual({
@@ -104,6 +108,8 @@ describe('DesktopUiPreferencesService', () => {
projectOrder: ['/workspace/gamma'],
pinnedProjects: [],
hiddenProjects: ['/workspace/beta'],
+ projectOrganization: 'recentProject',
+ projectSortBy: 'updatedAt',
},
})
expect(await readDesktopUiFile()).toEqual(after)
@@ -132,6 +138,8 @@ describe('desktop UI preferences API', () => {
projectOrder: ['/workspace/beta', '/workspace/alpha'],
pinnedProjects: ['/workspace/beta'],
hiddenProjects: ['/workspace/old'],
+ projectOrganization: 'project',
+ projectSortBy: 'createdAt',
})
const putRes = await handleDesktopUiApi(putReq.req, putReq.url, putReq.segments)
@@ -146,6 +154,8 @@ describe('desktop UI preferences API', () => {
projectOrder: ['/workspace/beta', '/workspace/alpha'],
pinnedProjects: ['/workspace/beta'],
hiddenProjects: ['/workspace/old'],
+ projectOrganization: 'project',
+ projectSortBy: 'createdAt',
},
},
})
@@ -163,6 +173,8 @@ describe('desktop UI preferences API', () => {
projectOrder: ['/workspace/beta', '/workspace/alpha'],
pinnedProjects: ['/workspace/beta'],
hiddenProjects: ['/workspace/old'],
+ projectOrganization: 'project',
+ projectSortBy: 'createdAt',
},
},
})
diff --git a/src/server/services/desktopUiPreferencesService.ts b/src/server/services/desktopUiPreferencesService.ts
index d86f212a..6091e686 100644
--- a/src/server/services/desktopUiPreferencesService.ts
+++ b/src/server/services/desktopUiPreferencesService.ts
@@ -13,6 +13,8 @@ export type SidebarProjectPreferences = {
projectOrder: string[]
pinnedProjects: string[]
hiddenProjects: string[]
+ projectOrganization: 'project' | 'recentProject' | 'time'
+ projectSortBy: 'createdAt' | 'updatedAt'
}
export type DesktopUiPreferences = {
@@ -30,6 +32,8 @@ const DEFAULT_SIDEBAR_PROJECT_PREFERENCES: SidebarProjectPreferences = {
projectOrder: [],
pinnedProjects: [],
hiddenProjects: [],
+ projectOrganization: 'recentProject',
+ projectSortBy: 'updatedAt',
}
function defaultPreferences(): DesktopUiPreferences {
@@ -64,9 +68,19 @@ export function normalizeSidebarProjectPreferences(value: unknown): SidebarProje
projectOrder: normalizeStringArray(record.projectOrder),
pinnedProjects: normalizeStringArray(record.pinnedProjects),
hiddenProjects: normalizeStringArray(record.hiddenProjects),
+ projectOrganization: normalizeProjectOrganization(record.projectOrganization),
+ projectSortBy: normalizeProjectSortBy(record.projectSortBy),
}
}
+function normalizeProjectOrganization(value: unknown): SidebarProjectPreferences['projectOrganization'] {
+ return value === 'project' || value === 'recentProject' || value === 'time' ? value : 'recentProject'
+}
+
+function normalizeProjectSortBy(value: unknown): SidebarProjectPreferences['projectSortBy'] {
+ return value === 'createdAt' || value === 'updatedAt' ? value : 'updatedAt'
+}
+
function normalizeDesktopUiPreferences(value: unknown): DesktopUiPreferences | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null