diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index ac7a023f..5a3c716c 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -921,6 +921,9 @@ export const SessionSidebar: React.FC = ({ color?: string; iconImage?: { mime: string; updatedAt: number; source: 'custom' | 'auto' }; iconBackground?: string; + addedAt?: number; + lastOpenedAt?: number; + sidebarCollapsed?: boolean; }>; }, [projects]); @@ -1019,13 +1022,84 @@ export const SessionSidebar: React.FC = ({ lastRepoStatusRef.current = Boolean(projectRepoStatus.get(activeProjectId)); } + const showRecentSection = useSessionDisplayStore((state) => state.showRecentSection); + const showArchivedSessions = useSessionDisplayStore((state) => state.showArchivedSessions); + const projectSortOrder = useSessionDisplayStore((state) => state.projectSortOrder); + const manualProjectOrder = useProjectsStore((state) => state.manualProjectOrder); + + const recentProjectIds = React.useMemo(() => { + const recentSessions = deriveRecentSessions(sessions); + if (recentSessions.length === 0) return new Set(); + + const pathToId = new Map(); + for (const project of normalizedProjects) { + if (project.normalizedPath) { + pathToId.set(project.normalizedPath, project.id); + } + } + + const ids = new Set(); + for (const session of recentSessions) { + const directory = normalizePath((session as Session & { directory?: string | null }).directory ?? null); + if (directory) { + const projectId = pathToId.get(directory); + if (projectId) ids.add(projectId); + } + } + return ids; + }, [sessions, normalizedProjects]); + + const sortedProjects = React.useMemo(() => { + const list = [...normalizedProjects]; + + switch (projectSortOrder) { + case 'a-z': + list.sort((a, b) => { + const aLabel = (a.label || a.path).toLowerCase(); + const bLabel = (b.label || b.path).toLowerCase(); + return aLabel.localeCompare(bLabel); + }); + break; + case 'z-a': + list.sort((a, b) => { + const aLabel = (a.label || a.path).toLowerCase(); + const bLabel = (b.label || b.path).toLowerCase(); + return bLabel.localeCompare(aLabel); + }); + break; + case 'date-added': + list.sort((a, b) => (b.addedAt ?? 0) - (a.addedAt ?? 0)); + break; + case 'recent': + list.sort((a, b) => (b.lastOpenedAt ?? 0) - (a.lastOpenedAt ?? 0)); + break; + case 'manual': { + const orderMap = new Map(manualProjectOrder.map((id, i) => [id, i])); + list.sort((a, b) => { + const ai = orderMap.get(a.id) ?? Infinity; + const bi = orderMap.get(b.id) ?? Infinity; + return ai - bi; + }); + break; + } + } + + if (projectSortOrder === 'manual') { + return list; + } + + const recent = list.filter((p) => recentProjectIds.has(p.id)); + const rest = list.filter((p) => !recentProjectIds.has(p.id)); + return [...recent, ...rest]; + }, [normalizedProjects, projectSortOrder, manualProjectOrder, recentProjectIds]); + const { projectSections, groupSearchDataByGroup, sectionsForRender, searchMatchCount, } = useSessionSidebarSections({ - normalizedProjects, + normalizedProjects: sortedProjects, getSessionsForProject, getArchivedSessionsForProject, availableWorktreesByProject, @@ -1131,9 +1205,6 @@ export const SessionSidebar: React.FC = ({ return meta; }, [projectSections, homeDirectory]); - const showRecentSection = useSessionDisplayStore((state) => state.showRecentSection); - const showArchivedSessions = useSessionDisplayStore((state) => state.showArchivedSessions); - const activeNowSessions = React.useMemo(() => { if (!showRecentSection || isVSCode) { return []; @@ -1636,6 +1707,7 @@ export const SessionSidebar: React.FC = ({ removeProject={removeProject} projectHeaderSentinelRefs={projectHeaderSentinelRefs} reorderProjects={reorderProjects} + projectSortOrder={projectSortOrder} getOrderedGroups={getOrderedGroups} setGroupOrderByProject={setGroupOrderByProject} openSidebarMenuKey={openSidebarMenuKey} diff --git a/packages/ui/src/components/session/sidebar/SidebarHeader.tsx b/packages/ui/src/components/session/sidebar/SidebarHeader.tsx index b1d37f89..ae3de278 100644 --- a/packages/ui/src/components/session/sidebar/SidebarHeader.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarHeader.tsx @@ -68,6 +68,8 @@ export function SidebarHeader(props: Props): React.ReactNode { const setDisplayMode = useSessionDisplayStore((state) => state.setDisplayMode); const toggleRecentSection = useSessionDisplayStore((state) => state.toggleRecentSection); const toggleArchivedSessions = useSessionDisplayStore((state) => state.toggleArchivedSessions); + const projectSortOrder = useSessionDisplayStore((state) => state.projectSortOrder); + const setProjectSortOrder = useSessionDisplayStore((state) => state.setProjectSortOrder); // VS Code forces the expanded layout, so the mode toggle is meaningless there. const showDisplayModeToggle = !isVSCodeRuntime(); @@ -175,6 +177,60 @@ export function SidebarHeader(props: Props): React.ReactNode { + + + + + + + +

{t('sessions.sidebar.header.actions.sortProjects')}

+
+ + setProjectSortOrder('manual')} + className="flex items-center justify-between" + > + {t('sessions.sidebar.header.projectSort.manual')} + {projectSortOrder === 'manual' ? : null} + + setProjectSortOrder('a-z')} + className="flex items-center justify-between" + > + {t('sessions.sidebar.header.projectSort.aToZ')} + {projectSortOrder === 'a-z' ? : null} + + setProjectSortOrder('z-a')} + className="flex items-center justify-between" + > + {t('sessions.sidebar.header.projectSort.zToA')} + {projectSortOrder === 'z-a' ? : null} + + setProjectSortOrder('date-added')} + className="flex items-center justify-between" + > + {t('sessions.sidebar.header.projectSort.dateAdded')} + {projectSortOrder === 'date-added' ? : null} + + setProjectSortOrder('recent')} + className="flex items-center justify-between" + > + {t('sessions.sidebar.header.projectSort.recent')} + {projectSortOrder === 'recent' ? : null} + + +
+ @@ -237,6 +293,7 @@ export function SidebarHeader(props: Props): React.ReactNode { {t('sessions.sidebar.header.displayMode.expandAll')} + diff --git a/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx b/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx index 849987b0..91dc346e 100644 --- a/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx +++ b/packages/ui/src/components/session/sidebar/SidebarProjectsList.tsx @@ -17,6 +17,7 @@ import { SortableGroupItem, SortableProjectItem } from './sortableItems'; import { formatProjectLabel } from './utils'; import { useI18n } from '@/lib/i18n'; import type { MainTab } from '@/stores/useUIStore'; +import type { ProjectSortOrder } from '@/stores/useSessionDisplayStore'; type ProjectSection = { project: { @@ -69,6 +70,7 @@ type Props = { removeProject: (id: string) => void; projectHeaderSentinelRefs: React.MutableRefObject>; reorderProjects: (fromIndex: number, toIndex: number) => void; + projectSortOrder: ProjectSortOrder; getOrderedGroups: (projectId: string, groups: SessionGroup[]) => SessionGroup[]; setGroupOrderByProject: React.Dispatch>>; openSidebarMenuKey: string | null; @@ -188,6 +190,8 @@ export function SidebarProjectsList(props: Props): React.ReactNode { collisionDetection={closestCenter} onDragEnd={(event) => { if (props.isInlineEditing) return; + // Drag only allowed in manual sort mode - indices from visual order don't match store order in other modes + if (props.projectSortOrder !== 'manual') return; const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = props.sectionsForRender.findIndex((section) => section.project.id === active.id); @@ -219,6 +223,7 @@ export function SidebarProjectsList(props: Props): React.ReactNode { = ({ id, + disabled = false, projectLabel, projectDescription, projectIcon, @@ -85,7 +87,7 @@ export const SortableProjectItem: React.FC = ({ transform, transition, isDragging, - } = useSortable({ id }); + } = useSortable({ id, disabled }); const suppressNextToggleRef = React.useRef(false); const menuInstanceKey = `project:${id}`; diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 7fca78d9..d53d1acb 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -382,6 +382,7 @@ export const dict = { 'sessions.sidebar.header.actions.searchSessions': 'Search sessions', 'sessions.sidebar.header.actions.exitSelection': 'Exit selection', 'sessions.sidebar.header.actions.selectSessions': 'Select sessions', + 'sessions.sidebar.header.actions.sortProjects': 'Sort projects', 'sessions.sidebar.header.actions.sessionDisplayMode': 'Session display mode', 'sessions.sidebar.header.displayMode.label': 'Display mode', 'sessions.sidebar.header.displayMode.default': 'Default', @@ -390,6 +391,11 @@ export const dict = { 'sessions.sidebar.header.displayMode.showArchived': 'Show archived sessions', 'sessions.sidebar.header.displayMode.collapseAll': 'Collapse all', 'sessions.sidebar.header.displayMode.expandAll': 'Expand all', + 'sessions.sidebar.header.projectSort.manual': 'Manual', + 'sessions.sidebar.header.projectSort.aToZ': 'A → Z', + 'sessions.sidebar.header.projectSort.zToA': 'Z → A', + 'sessions.sidebar.header.projectSort.dateAdded': 'Newest', + 'sessions.sidebar.header.projectSort.recent': 'Recent', 'sessions.sidebar.header.search.matchCountSingle': '{count} match', 'sessions.sidebar.header.search.matchCountPlural': '{count} matches', 'sessions.sidebar.header.search.escapeHint': 'Esc to clear', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index f640e906..68a8b0a0 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -383,6 +383,7 @@ export const dict: Record = { "sessions.sidebar.header.actions.searchSessions": "Buscar sesiones", "sessions.sidebar.header.actions.exitSelection": "Salir de selección", "sessions.sidebar.header.actions.selectSessions": "Seleccionar sesiones", + "sessions.sidebar.header.actions.sortProjects": "Ordenar proyectos", "sessions.sidebar.header.actions.sessionDisplayMode": "Modo de visualización de sesión", "sessions.sidebar.header.displayMode.label": "Modo de visualización", "sessions.sidebar.header.displayMode.default": "Predeterminado", @@ -391,6 +392,11 @@ export const dict: Record = { "sessions.sidebar.header.displayMode.showArchived": "Mostrar sesiones archivadas", "sessions.sidebar.header.displayMode.collapseAll": "Colapsar todo", "sessions.sidebar.header.displayMode.expandAll": "Expandir todo", + "sessions.sidebar.header.projectSort.manual": "Manual", + "sessions.sidebar.header.projectSort.aToZ": "A → Z", + "sessions.sidebar.header.projectSort.zToA": "Z → A", + "sessions.sidebar.header.projectSort.dateAdded": "Más recientes", + "sessions.sidebar.header.projectSort.recent": "Recientes", "sessions.sidebar.header.search.matchCountSingle": "{count} coincidencia", "sessions.sidebar.header.search.matchCountPlural": "{count} coincidencias", "sessions.sidebar.header.search.escapeHint": "Esc para limpiar", diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 47957084..93fa9482 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -227,6 +227,7 @@ export const dict = { 'sessions.sidebar.header.actions.searchSessions': 'Sessions de recherche', 'sessions.sidebar.header.actions.exitSelection': 'Quitter la sélection', 'sessions.sidebar.header.actions.selectSessions': 'Sélectionnez des sessions', + 'sessions.sidebar.header.actions.sortProjects': 'Trier les projets', 'sessions.sidebar.header.actions.sessionDisplayMode': 'Mode d\'affichage des sessions', 'sessions.sidebar.header.displayMode.label': 'Mode d\'affichage', 'sessions.sidebar.header.displayMode.default': 'Défaut', @@ -234,6 +235,11 @@ export const dict = { 'sessions.sidebar.header.displayMode.showRecent': 'Afficher la section récente', 'sessions.sidebar.header.displayMode.collapseAll': 'Tout réduire', 'sessions.sidebar.header.displayMode.expandAll': 'Tout développer', + 'sessions.sidebar.header.projectSort.manual': 'Manuel', + 'sessions.sidebar.header.projectSort.aToZ': 'A → Z', + 'sessions.sidebar.header.projectSort.zToA': 'Z → A', + 'sessions.sidebar.header.projectSort.dateAdded': 'Les plus récentes', + 'sessions.sidebar.header.projectSort.recent': 'Récentes', 'sessions.sidebar.header.search.matchCountSingle': 'Correspondance {count}', 'sessions.sidebar.header.search.matchCountPlural': 'Correspondances {count}', 'sessions.sidebar.header.search.escapeHint': 'Echap pour effacer', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index 54c0890f..80708203 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -383,6 +383,7 @@ export const dict: Record = { 'sessions.sidebar.header.actions.searchSessions': 'セッションを検索', 'sessions.sidebar.header.actions.exitSelection': '選択を終了', 'sessions.sidebar.header.actions.selectSessions': 'セッションを選択', + 'sessions.sidebar.header.actions.sortProjects': 'プロジェクトを並べ替え', 'sessions.sidebar.header.actions.sessionDisplayMode': 'セッション表示モード', 'sessions.sidebar.header.displayMode.label': '表示モード', 'sessions.sidebar.header.displayMode.default': 'デフォルト', @@ -391,6 +392,11 @@ export const dict: Record = { 'sessions.sidebar.header.displayMode.showArchived': 'アーカイブ済みセッションを表示', 'sessions.sidebar.header.displayMode.collapseAll': 'すべて折りたたむ', 'sessions.sidebar.header.displayMode.expandAll': 'すべて展開', + 'sessions.sidebar.header.projectSort.manual': '手動', + 'sessions.sidebar.header.projectSort.aToZ': 'A → Z', + 'sessions.sidebar.header.projectSort.zToA': 'Z → A', + 'sessions.sidebar.header.projectSort.dateAdded': '新しい順', + 'sessions.sidebar.header.projectSort.recent': '最近', 'sessions.sidebar.header.search.matchCountSingle': '{count}件一致', 'sessions.sidebar.header.search.matchCountPlural': '{count}件一致', 'sessions.sidebar.header.search.escapeHint': 'Escでクリア', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 97095cae..b1e53162 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -383,6 +383,7 @@ export const dict: Record = { 'sessions.sidebar.header.actions.searchSessions': '세션 검색', 'sessions.sidebar.header.actions.exitSelection': '선택 종료', 'sessions.sidebar.header.actions.selectSessions': '세션 선택', + 'sessions.sidebar.header.actions.sortProjects': '프로젝트 정렬', 'sessions.sidebar.header.actions.sessionDisplayMode': '세션 표시 모드', 'sessions.sidebar.header.displayMode.label': '표시 모드', 'sessions.sidebar.header.displayMode.default': '기본값', @@ -391,6 +392,11 @@ export const dict: Record = { 'sessions.sidebar.header.displayMode.showArchived': '보관된 세션 표시', 'sessions.sidebar.header.displayMode.collapseAll': '모두 접기', 'sessions.sidebar.header.displayMode.expandAll': '모두 펼치기', + 'sessions.sidebar.header.projectSort.manual': '수동', + 'sessions.sidebar.header.projectSort.aToZ': 'A → Z', + 'sessions.sidebar.header.projectSort.zToA': 'Z → A', + 'sessions.sidebar.header.projectSort.dateAdded': '최신순', + 'sessions.sidebar.header.projectSort.recent': '최근', 'sessions.sidebar.header.search.matchCountSingle': '{count}개 일치', 'sessions.sidebar.header.search.matchCountPlural': '{count}개 일치', 'sessions.sidebar.header.search.escapeHint': 'Esc로 지우기', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 16ea1048..f1ebfd03 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -205,6 +205,7 @@ export const dict: Record = { 'sessions.sidebar.header.actions.searchSessions': 'Szukaj sesji', 'sessions.sidebar.header.actions.exitSelection': 'Wyjdź z zaznaczenia', 'sessions.sidebar.header.actions.selectSessions': 'Wybierz sesje', + 'sessions.sidebar.header.actions.sortProjects': 'Sortuj projekty', 'sessions.sidebar.header.actions.sessionDisplayMode': 'Tryb wyświetlania sesji', 'sessions.sidebar.header.displayMode.label': 'Tryb wyświetlania', 'sessions.sidebar.header.displayMode.default': 'Domyślny', @@ -213,6 +214,11 @@ export const dict: Record = { 'sessions.sidebar.header.displayMode.showArchived': 'Pokaż zarchiwizowane sesje', 'sessions.sidebar.header.displayMode.collapseAll': 'Zwiń wszystkie', 'sessions.sidebar.header.displayMode.expandAll': 'Rozwiń wszystkie', + 'sessions.sidebar.header.projectSort.manual': 'Ręcznie', + 'sessions.sidebar.header.projectSort.aToZ': 'A → Z', + 'sessions.sidebar.header.projectSort.zToA': 'Z → A', + 'sessions.sidebar.header.projectSort.dateAdded': 'Najnowsze', + 'sessions.sidebar.header.projectSort.recent': 'Ostatnie', 'sessions.sidebar.header.search.matchCountSingle': '{count} dopasowanie', 'sessions.sidebar.header.search.matchCountPlural': '{count} dopasowań', 'sessions.sidebar.header.search.escapeHint': 'Esc aby wyczyścić', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index a0284f9f..e27e6861 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -383,6 +383,7 @@ export const dict: Record = { "sessions.sidebar.header.actions.searchSessions": "Pesquisar sessões", "sessions.sidebar.header.actions.exitSelection": "Sair da seleção", "sessions.sidebar.header.actions.selectSessions": "Selecionar sessões", + "sessions.sidebar.header.actions.sortProjects": "Ordenar projetos", "sessions.sidebar.header.actions.sessionDisplayMode": "Modo de visualização da sessão", "sessions.sidebar.header.displayMode.label": "Modo de visualização", "sessions.sidebar.header.displayMode.default": "Padrão", @@ -391,6 +392,11 @@ export const dict: Record = { "sessions.sidebar.header.displayMode.showArchived": "Mostrar sessões arquivadas", "sessions.sidebar.header.displayMode.collapseAll": "Recolher tudo", "sessions.sidebar.header.displayMode.expandAll": "Expandir tudo", + "sessions.sidebar.header.projectSort.manual": "Manual", + "sessions.sidebar.header.projectSort.aToZ": "A → Z", + "sessions.sidebar.header.projectSort.zToA": "Z → A", + "sessions.sidebar.header.projectSort.dateAdded": "Mais recentes", + "sessions.sidebar.header.projectSort.recent": "Recentes", "sessions.sidebar.header.search.matchCountSingle": "{count} correspondência", "sessions.sidebar.header.search.matchCountPlural": "{count} correspondências", "sessions.sidebar.header.search.escapeHint": "Esc para limpar", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 26f6afb5..a506a5fa 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -383,6 +383,7 @@ export const dict: Record = { "sessions.sidebar.header.actions.searchSessions": "Пошук сесій", "sessions.sidebar.header.actions.exitSelection": "Вийти з вибору", "sessions.sidebar.header.actions.selectSessions": "Вибрати сесії", + "sessions.sidebar.header.actions.sortProjects": "Сортувати проєкти", "sessions.sidebar.header.actions.sessionDisplayMode": "Режим відображення сесії", "sessions.sidebar.header.displayMode.label": "Режим відображення", "sessions.sidebar.header.displayMode.default": "За замовчуванням", @@ -391,6 +392,11 @@ export const dict: Record = { "sessions.sidebar.header.displayMode.showArchived": "Показувати архівовані сесії", "sessions.sidebar.header.displayMode.collapseAll": "Згорнути все", "sessions.sidebar.header.displayMode.expandAll": "Розгорнути все", + "sessions.sidebar.header.projectSort.manual": "Вручну", + "sessions.sidebar.header.projectSort.aToZ": "A → Z", + "sessions.sidebar.header.projectSort.zToA": "Z → A", + "sessions.sidebar.header.projectSort.dateAdded": "Найновіші", + "sessions.sidebar.header.projectSort.recent": "Нещодавні", "sessions.sidebar.header.search.matchCountSingle": "{count} збіг", "sessions.sidebar.header.search.matchCountPlural": "Збігів: {count}", "sessions.sidebar.header.search.escapeHint": "Esc, щоб очистити", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index f5fbcb09..90b8b56c 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -383,6 +383,7 @@ export const dict: Record = { 'sessions.sidebar.header.actions.searchSessions': '搜索会话', 'sessions.sidebar.header.actions.exitSelection': '退出选择', 'sessions.sidebar.header.actions.selectSessions': '选择会话', + 'sessions.sidebar.header.actions.sortProjects': '排序项目', 'sessions.sidebar.header.actions.sessionDisplayMode': '会话显示模式', 'sessions.sidebar.header.displayMode.label': '显示模式', 'sessions.sidebar.header.displayMode.default': '默认', @@ -391,6 +392,11 @@ export const dict: Record = { 'sessions.sidebar.header.displayMode.showArchived': '显示已归档会话', 'sessions.sidebar.header.displayMode.collapseAll': '全部折叠', 'sessions.sidebar.header.displayMode.expandAll': '全部展开', + 'sessions.sidebar.header.projectSort.manual': '手动', + 'sessions.sidebar.header.projectSort.aToZ': 'A → Z', + 'sessions.sidebar.header.projectSort.zToA': 'Z → A', + 'sessions.sidebar.header.projectSort.dateAdded': '最新', + 'sessions.sidebar.header.projectSort.recent': '最近', 'sessions.sidebar.header.search.matchCountSingle': '{count} 个匹配', 'sessions.sidebar.header.search.matchCountPlural': '{count} 个匹配', 'sessions.sidebar.header.search.escapeHint': '按 Esc 清除', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index a7aaee4b..46fb3d20 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -396,6 +396,7 @@ export const dict: Record = { 'sessions.sidebar.header.actions.searchSessions': '搜尋會話', 'sessions.sidebar.header.actions.exitSelection': '退出選取', 'sessions.sidebar.header.actions.selectSessions': '選擇會話', + 'sessions.sidebar.header.actions.sortProjects': '排序專案', 'sessions.sidebar.header.actions.sessionDisplayMode': '會話顯示模式', 'sessions.sidebar.header.displayMode.label': '顯示模式', 'sessions.sidebar.header.displayMode.default': '預設', @@ -404,6 +405,11 @@ export const dict: Record = { 'sessions.sidebar.header.displayMode.showArchived': '顯示已封存會話', 'sessions.sidebar.header.displayMode.collapseAll': '全部摺疊', 'sessions.sidebar.header.displayMode.expandAll': '全部展開', + 'sessions.sidebar.header.projectSort.manual': '手動', + 'sessions.sidebar.header.projectSort.aToZ': 'A → Z', + 'sessions.sidebar.header.projectSort.zToA': 'Z → A', + 'sessions.sidebar.header.projectSort.dateAdded': '最新', + 'sessions.sidebar.header.projectSort.recent': '最近', 'sessions.sidebar.header.search.matchCountSingle': '{count} 個符合', 'sessions.sidebar.header.search.matchCountPlural': '{count} 個符合', 'sessions.sidebar.header.search.escapeHint': '按 Esc 清除', diff --git a/packages/ui/src/stores/useProjectsStore.ts b/packages/ui/src/stores/useProjectsStore.ts index 495edfdc..46feb476 100644 --- a/packages/ui/src/stores/useProjectsStore.ts +++ b/packages/ui/src/stores/useProjectsStore.ts @@ -46,6 +46,7 @@ interface VSCodeWorkspaceFolderConfig { interface ProjectsStore { projects: ProjectEntry[]; activeProjectId: string | null; + manualProjectOrder: string[]; addProject: (path: string, options?: { label?: string; id?: string }) => ProjectEntry | null; removeProject: (id: string) => void; @@ -315,6 +316,17 @@ const readPersistedProjects = (): ProjectEntry[] => { } }; +const readPersistedManualOrder = (): string[] => { + try { + const raw = safeStorage.getItem(getProjectsStorageKey() + ':manualOrder'); + if (!raw) return []; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string') : []; + } catch { + return []; + } +}; + const readPersistedActiveProjectId = (): string | null => { try { const raw = safeStorage.getItem(getActiveProjectStorageKey()) @@ -347,11 +359,22 @@ const cacheProjects = (projects: ProjectEntry[], activeProjectId: string | null) } }; -const persistProjects = (projects: ProjectEntry[], activeProjectId: string | null) => { +const persistProjects = (projects: ProjectEntry[], activeProjectId: string | null, manualOrder?: string[]) => { cacheProjects(projects, activeProjectId); + if (manualOrder) { + persistManualProjectOrder(manualOrder); + } void updateDesktopSettings({ projects, activeProjectId: activeProjectId ?? undefined }); }; +const persistManualProjectOrder = (manualOrder: string[]) => { + try { + safeStorage.setItem(getProjectsStorageKey() + ':manualOrder', JSON.stringify(manualOrder)); + } catch { + // ignored + } +}; + const initialProjects = readPersistedProjects(); const normalizeVSCodeWorkspaceFolders = (folders: VSCodeWorkspaceFolderConfig[]): VSCodeWorkspaceFolderConfig[] => { const result: VSCodeWorkspaceFolderConfig[] = []; @@ -536,6 +559,7 @@ export const useProjectsStore = create()( devtools((set, get) => ({ projects: effectiveInitialProjects, activeProjectId: initialActiveProjectId, + manualProjectOrder: readPersistedManualOrder(), validateProjectPath: (path: string): ProjectPathValidationResult => { if (typeof path !== 'string' || path.trim().length === 0) { @@ -604,8 +628,9 @@ export const useProjectsStore = create()( nextActiveId = nextProjects[0]?.id ?? null; } - set({ projects: nextProjects, activeProjectId: nextActiveId }); - persistProjects(nextProjects, nextActiveId); + const nextManualOrder = get().manualProjectOrder.filter((oid) => oid !== id); + set({ projects: nextProjects, activeProjectId: nextActiveId, manualProjectOrder: nextManualOrder }); + persistProjects(nextProjects, nextActiveId, nextManualOrder); // Clean up worktree entries for the removed project if (project) { @@ -647,7 +672,7 @@ export const useProjectsStore = create()( ); set({ projects: nextProjects, activeProjectId: id }); - persistProjects(nextProjects, id); + persistProjects(nextProjects, id, get().manualProjectOrder); opencodeClient.setDirectory(target.path); useDirectoryStore.getState().setDirectory(target.path, { showOverlay: false }); @@ -672,7 +697,7 @@ export const useProjectsStore = create()( ); set({ projects: nextProjects, activeProjectId: id }); - persistProjects(nextProjects, id); + persistProjects(nextProjects, id, get().manualProjectOrder); }, renameProject: (id: string, label: string) => { @@ -689,7 +714,7 @@ export const useProjectsStore = create()( project.id === id ? { ...project, label: trimmed } : project ); set({ projects: nextProjects }); - persistProjects(nextProjects, activeProjectId); + persistProjects(nextProjects, activeProjectId, get().manualProjectOrder); }, updateProjectMeta: (id: string, meta: { @@ -726,7 +751,7 @@ export const useProjectsStore = create()( return updated; }); set({ projects: nextProjects }); - persistProjects(nextProjects, activeProjectId); + persistProjects(nextProjects, activeProjectId, get().manualProjectOrder); }, uploadProjectIcon: async (id: string, file: File) => { @@ -863,8 +888,9 @@ export const useProjectsStore = create()( const [moved] = nextProjects.splice(fromIndex, 1); nextProjects.splice(toIndex, 0, moved); - set({ projects: nextProjects }); - persistProjects(nextProjects, activeProjectId); + const newOrder = nextProjects.map((p) => p.id); + set({ projects: nextProjects, manualProjectOrder: newOrder }); + persistProjects(nextProjects, activeProjectId, newOrder); }, resetForRuntimeSwitch: () => { @@ -876,7 +902,7 @@ export const useProjectsStore = create()( const nextActiveProjectId = projects.some((project) => project.id === activeProjectId) ? activeProjectId : projects[0]?.id ?? null; - set({ projects, activeProjectId: nextActiveProjectId }); + set({ projects, activeProjectId: nextActiveProjectId, manualProjectOrder: [] }); }, synchronizeFromSettings: (settings: DesktopSettings) => { @@ -903,6 +929,7 @@ export const useProjectsStore = create()( if (activeExists) { set({ activeProjectId: incomingActive }); cacheProjects(current.projects, incomingActive); + persistManualProjectOrder(get().manualProjectOrder); } } return; @@ -915,8 +942,11 @@ export const useProjectsStore = create()( return; } - set({ projects: incomingProjects, activeProjectId: incomingActive }); + const incomingIds = new Set(incomingProjects.map((p) => p.id)); + const cleanedOrder = get().manualProjectOrder.filter((id) => incomingIds.has(id)); + set({ projects: incomingProjects, activeProjectId: incomingActive, manualProjectOrder: cleanedOrder }); cacheProjects(incomingProjects, incomingActive); + persistManualProjectOrder(cleanedOrder); if (incomingActive) { const activeProject = incomingProjects.find((project) => project.id === incomingActive); diff --git a/packages/ui/src/stores/useSessionDisplayStore.ts b/packages/ui/src/stores/useSessionDisplayStore.ts index 0b87c83f..3e579a4d 100644 --- a/packages/ui/src/stores/useSessionDisplayStore.ts +++ b/packages/ui/src/stores/useSessionDisplayStore.ts @@ -3,15 +3,19 @@ import { persist } from 'zustand/middleware'; type SessionDisplayMode = 'default' | 'minimal'; +type ProjectSortOrder = 'manual' | 'a-z' | 'z-a' | 'date-added' | 'recent'; + type SessionDisplayStore = { displayMode: SessionDisplayMode; showRecentSection: boolean; showArchivedSessions: boolean; + projectSortOrder: ProjectSortOrder; setDisplayMode: (mode: SessionDisplayMode) => void; setShowRecentSection: (show: boolean) => void; setShowArchivedSessions: (show: boolean) => void; toggleRecentSection: () => void; toggleArchivedSessions: () => void; + setProjectSortOrder: (order: ProjectSortOrder) => void; }; export const useSessionDisplayStore = create()( @@ -24,25 +28,33 @@ export const useSessionDisplayStore = create()( // disappear once the persisted preference rehydrates. Users who opted into // showing archived have `true` persisted, which is preserved on rehydrate. showArchivedSessions: false, + projectSortOrder: 'recent', setDisplayMode: (mode) => set({ displayMode: mode }), setShowRecentSection: (show) => set({ showRecentSection: show }), setShowArchivedSessions: (show) => set({ showArchivedSessions: show }), toggleRecentSection: () => set((state) => ({ showRecentSection: !state.showRecentSection })), toggleArchivedSessions: () => set((state) => ({ showArchivedSessions: !state.showArchivedSessions })), + setProjectSortOrder: (order) => set({ projectSortOrder: order }), }), { name: 'session-display-mode', - version: 1, + version: 2, // v0 shipped 'default' as the only/initial mode, so most existing users // have it persisted by accident rather than choice. Nudge everyone onto // minimal once so the mode can be evaluated before removing it entirely. + // v1→v2 adds projectSortOrder defaulting to 'recent'. migrate: (persisted, version) => { const state = (persisted ?? {}) as Partial; if (version < 1) { - return { ...state, displayMode: 'minimal' }; + return { ...state, displayMode: 'minimal', projectSortOrder: 'recent' }; + } + if (version < 2) { + return { ...state, projectSortOrder: 'recent' }; } return state; }, }, ), ); + +export type { ProjectSortOrder };