From 5589ca991ad23af5b0a48e6b9ab2d34253319f66 Mon Sep 17 00:00:00 2001 From: Jake <101855602+jjdubski@users.noreply.github.com> Date: Mon, 8 Jun 2026 07:11:05 -0500 Subject: [PATCH] feat(vscode): add archive all sessions action (#1262) * feat(vscode): add archive all sessions action * Update packages/ui/src/components/layout/VSCodeLayout.tsx Signed-off-by: Jake <101855602+jjdubski@users.noreply.github.com> * fix: Fail-open default archives all sessions when workspace can't be resolved * fix: Already-archived descendants are included in the archive call * fix: add i18n for the newly added toasts * Restore icon name from 'add-line' to 'add' Signed-off-by: Jake <101855602+jjdubski@users.noreply.github.com> * update: fix merge conflict and update code structure into components --------- Signed-off-by: Jake <101855602+jjdubski@users.noreply.github.com> Co-authored-by: Bohdan Triapitsyn --- .../ui/src/components/layout/VSCodeLayout.tsx | 111 +++++++++++++++++- .../components/session/ArchiveAllDropdown.tsx | 44 +++++++ .../components/session/SessionsTabTitle.tsx | 16 +++ .../session/sidebar/SessionNodeItem.tsx | 2 +- packages/ui/src/lib/i18n/messages/en.ts | 5 + packages/ui/src/lib/i18n/messages/es.ts | 5 + packages/ui/src/lib/i18n/messages/ko.ts | 5 + packages/ui/src/lib/i18n/messages/pl.ts | 5 + packages/ui/src/lib/i18n/messages/pt-BR.ts | 5 + packages/ui/src/lib/i18n/messages/uk.ts | 5 + packages/ui/src/lib/i18n/messages/zh-CN.ts | 5 + packages/ui/src/lib/i18n/messages/zh-TW.ts | 5 + 12 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 packages/ui/src/components/session/ArchiveAllDropdown.tsx create mode 100644 packages/ui/src/components/session/SessionsTabTitle.tsx diff --git a/packages/ui/src/components/layout/VSCodeLayout.tsx b/packages/ui/src/components/layout/VSCodeLayout.tsx index 3ab623ef..39902873 100644 --- a/packages/ui/src/components/layout/VSCodeLayout.tsx +++ b/packages/ui/src/components/layout/VSCodeLayout.tsx @@ -5,11 +5,13 @@ import { SessionDialogs } from '@/components/session/SessionDialogs'; import { ChatView } from '@/components/views/ChatView'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useViewportStore } from '@/sync/viewport-store'; -import { useDirectorySync, useSessionMessages, useSessionMessagesResolved } from '@/sync/sync-context'; +import { useSessions, useDirectorySync, useSessionMessages, useSessionMessagesResolved } from '@/sync/sync-context'; import { useConfigStore } from '@/stores/useConfigStore'; +import { resolveGlobalSessionDirectory, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore'; import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay'; import { McpDropdown } from '@/components/mcp/McpDropdown'; import { SessionSwitcherDropdown } from '@/components/session/SessionSwitcherDropdown'; +import { SessionsTabTitle } from '@/components/session/SessionsTabTitle'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { cn } from '@/lib/utils'; import { @@ -22,6 +24,7 @@ import { } from '@/components/ui/dropdown-menu'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useI18n } from '@/lib/i18n'; +import { toast } from '@/components/ui'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar'; import { PaceIndicator } from '@/components/sections/usage/PaceIndicator'; @@ -32,7 +35,7 @@ import { useUpdateStore } from '@/stores/useUpdateStore'; import { updateDesktopSettings } from '@/lib/persistence'; import { formatTimeForPreference } from '@/lib/timeFormat'; import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; -import type { UsageWindow } from '@/types'; +import type { Session, UsageWindow } from '@/types'; import type { SessionContextUsage } from '@/stores/types/sessionTypes'; import { useUIStore, type TimeFormatPreference } from '@/stores/useUIStore'; @@ -56,6 +59,15 @@ const SESSIONS_SIDEBAR_WIDTH = 280; const SESSIONS_SIDEBAR_MIN_WIDTH = Math.round(SESSIONS_SIDEBAR_WIDTH * 0.7); const SESSIONS_SIDEBAR_MAX_WIDTH = 520; +const normalizePath = (value?: string | null): string | null => { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + if (!trimmed) return null; + const replaced = trimmed.replace(/\\/g, '/'); + if (replaced === '/') return '/'; + return replaced.length > 1 ? replaced.replace(/\/+$/, '') : replaced; +}; + type VSCodeView = 'sessions' | 'chat' | 'settings'; export const VSCodeLayout: React.FC = () => { @@ -135,6 +147,18 @@ export const VSCodeLayout: React.FC = () => { const expandedSidebarResizeStartWidthRef = React.useRef(SESSIONS_SIDEBAR_WIDTH); const expandedSidebarResizePointerIdRef = React.useRef(null); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); + const sessions = useSessions(); + const globalActiveSessions = useGlobalSessionsStore((state) => state.activeSessions); + const globalArchivedSessions = useGlobalSessionsStore((state) => state.archivedSessions); + const projects = useProjectsStore((state) => state.projects); + const activeProjectId = useProjectsStore((state) => state.activeProjectId); + + const activeWorkspacePath = React.useMemo(() => { + const activeProject = activeProjectId + ? projects.find((project) => project.id === activeProjectId) ?? null + : projects[0] ?? null; + return normalizePath(activeProject?.path ?? null); + }, [activeProjectId, projects]); const newSessionDraftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open)); const activeSessionTitleValue = useDirectorySync( React.useCallback((state) => { @@ -249,6 +273,82 @@ export const VSCodeLayout: React.FC = () => { setCurrentView('sessions'); }, []); + const isSessionInActiveWorkspace = React.useCallback((session: Session): boolean => { + if (!activeWorkspacePath) { + return false; + } + + const sessionDirectory = resolveGlobalSessionDirectory(session); + if (sessionDirectory) { + return sessionDirectory.toLowerCase() === activeWorkspacePath.toLowerCase(); + } + + return false; + }, [activeWorkspacePath]); + + const traversalSessions = React.useMemo(() => { + const byId = new Map(); + for (const session of sessions) byId.set(session.id, session); + for (const session of globalActiveSessions) byId.set(session.id, session); + for (const session of globalArchivedSessions) byId.set(session.id, session); + return Array.from(byId.values()); + }, [globalActiveSessions, globalArchivedSessions, sessions]); + + /** Collect root session IDs and all descendants (subagent sessions). */ + const collectSessionIdsWithDescendants = React.useCallback( + (allSessions: Session[], rootSessions: Session[]): string[] => { + const byId = new Map(); + for (const session of allSessions) byId.set(session.id, session); + + const childrenMap = new Map(); + for (const session of allSessions) { + const parentID = session.parentID; + if (parentID) { + const list = childrenMap.get(parentID) ?? []; + list.push(session.id); + childrenMap.set(parentID, list); + } + } + + const ids = new Set(); + const addDescendants = (sessionId: string, visited: Set) => { + if (visited.has(sessionId)) return; // cycle guard + visited.add(sessionId); + const children = childrenMap.get(sessionId) ?? []; + for (const childId of children) { + const child = byId.get(childId); + if (child?.time?.archived) continue; // skip already-archived children + ids.add(childId); + addDescendants(childId, visited); + } + }; + + for (const session of rootSessions) { + ids.add(session.id); + addDescendants(session.id, new Set()); + } + + return Array.from(ids); + }, + [], + ); + + const handleArchiveAll = React.useCallback(async () => { + const store = useSessionUIStore.getState(); + const rootSessions = traversalSessions.filter((session) => !session.time?.archived && isSessionInActiveWorkspace(session)); + const allIds = collectSessionIdsWithDescendants(traversalSessions, rootSessions); + if (allIds.length === 0) return; + + const { archivedIds, failedIds } = await store.archiveSessions(allIds); + if (archivedIds.length > 0) { + toast.success(t('vscodeLayout.actions.archiveAllSuccess', { count: archivedIds.length })); + } + if (failedIds.length > 0) { + toast.error(t('vscodeLayout.actions.archiveAllError', { count: failedIds.length })); + } + }, [collectSessionIdsWithDescendants, isSessionInActiveWorkspace, traversalSessions, t]); + + // Listen for connection status changes React.useEffect(() => { // Catch up with the latest status even if the extension posted the connection message @@ -519,6 +619,7 @@ export const VSCodeLayout: React.FC = () => {
void; + onArchiveAll?: () => void; onNewSession?: () => void; onSettings?: () => void; onAgentManager?: () => void; @@ -568,7 +670,8 @@ interface VSCodeHeaderProps { enableSessionSwitcher?: boolean; } -const VSCodeHeader: React.FC = ({ title, showBack, onBack, onNewSession, onSettings, onAgentManager, showMcp, showContextUsage, showRateLimits, enableSessionSwitcher }) => { + +const VSCodeHeader: React.FC = ({ title, showBack, onBack, onArchiveAll, onNewSession, onSettings, onAgentManager, showMcp, showContextUsage, showRateLimits, enableSessionSwitcher }) => { const { t } = useI18n(); const getCurrentModel = useConfigStore((state) => state.getCurrentModel); const providers = useConfigStore((state) => state.providers); @@ -744,7 +847,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on ) : ( -

{title}

+ )}
{onNewSession && ( diff --git a/packages/ui/src/components/session/ArchiveAllDropdown.tsx b/packages/ui/src/components/session/ArchiveAllDropdown.tsx new file mode 100644 index 00000000..4e3bb808 --- /dev/null +++ b/packages/ui/src/components/session/ArchiveAllDropdown.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { Icon } from '@/components/icon/Icon'; +import { useI18n } from '@/lib/i18n'; + +interface ArchiveAllDropdownProps { + onArchiveAll?: () => void; +} + +const ArchiveAllDropdown: React.FC = ({ onArchiveAll }) => { + const { t } = useI18n(); + return ( + + + + + + + +

{t('vscodeLayout.actions.archiveAllAria')}

+
+ + + {t('vscodeLayout.actions.archiveAllConfirm')} + + {t('vscodeLayout.actions.cancel')} + +
+ ); +}; + +export { ArchiveAllDropdown }; diff --git a/packages/ui/src/components/session/SessionsTabTitle.tsx b/packages/ui/src/components/session/SessionsTabTitle.tsx new file mode 100644 index 00000000..d85012f3 --- /dev/null +++ b/packages/ui/src/components/session/SessionsTabTitle.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { ArchiveAllDropdown } from '@/components/session/ArchiveAllDropdown'; + +interface SessionsTabTitleProps { + title: string; + onArchiveAll?: () => void; +} + +const SessionsTabTitle: React.FC = ({ title, onArchiveAll }) => ( + <> +

{title}

+ {onArchiveAll && } + +); + +export { SessionsTabTitle }; diff --git a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx index e9847d3b..258793ca 100644 --- a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx +++ b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx @@ -821,7 +821,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { handleDeleteSession(session, { archivedBucket })}> - + {archivedBucket ? t('sessions.sidebar.bulkActions.delete') : t('sessions.sidebar.bulkActions.archive')} diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 579f9f72..ed761873 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -2421,6 +2421,11 @@ export const dict = { 'vscodeLayout.title.sessions': 'Sessions', 'vscodeLayout.title.sessionFallback': 'Session', 'vscodeLayout.actions.backToSessionsAria': 'Back to sessions', + 'vscodeLayout.actions.archiveAllAria': 'Archive all sessions', + 'vscodeLayout.actions.archiveAllConfirm': 'Archive all', + 'vscodeLayout.actions.archiveAllSuccess': 'Archived {count} session(s)', + 'vscodeLayout.actions.archiveAllError': 'Failed to archive {count} session(s)', + 'vscodeLayout.actions.cancel': 'Cancel', 'vscodeLayout.actions.newSessionAria': 'New session', 'vscodeLayout.actions.openAgentManagerAria': 'Open Agent Manager', 'vscodeLayout.actions.resizeSessionsSidebarAria': 'Resize sessions sidebar', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index a6c56cd0..f05b96b4 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -2388,6 +2388,11 @@ export const dict: Record = { "vscodeLayout.title.sessionFallback": "Sesión", "vscodeLayout.actions.backToSessionsAria": "Volver a sesiones", "vscodeLayout.actions.newSessionAria": "Nueva sesión", + "vscodeLayout.actions.archiveAllAria": "Archivar todas las sesiones", + "vscodeLayout.actions.archiveAllConfirm": "Archivar todo", + "vscodeLayout.actions.archiveAllSuccess": "{count} sesión(es) archivada(s)", + "vscodeLayout.actions.archiveAllError": "Error al archivar {count} sesión(es)", + "vscodeLayout.actions.cancel": "Cancelar", "vscodeLayout.actions.openAgentManagerAria": "Abrir gestor de agentes", "vscodeLayout.actions.resizeSessionsSidebarAria": "Ajustar tamaño del panel de sesiones", "vscodeLayout.actions.settingsAria": "Configuración", diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index d493755b..cb184ec8 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -2421,6 +2421,11 @@ export const dict: Record = { 'vscodeLayout.title.sessions': '세션', 'vscodeLayout.title.sessionFallback': '세션', 'vscodeLayout.actions.backToSessionsAria': '세션으로 돌아가기', + 'vscodeLayout.actions.archiveAllAria': '모든 세션 보관', + 'vscodeLayout.actions.archiveAllConfirm': '모두 보관', + 'vscodeLayout.actions.archiveAllSuccess': '{count}개 세션 보관됨', + 'vscodeLayout.actions.archiveAllError': '{count}개 세션 보관 실패', + 'vscodeLayout.actions.cancel': '취소', 'vscodeLayout.actions.newSessionAria': '새 세션', 'vscodeLayout.actions.openAgentManagerAria': '에이전트 관리자 열기', 'vscodeLayout.actions.resizeSessionsSidebarAria': '세션 사이드바 크기 조정', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 1c71ecfe..3f3eab3e 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -2448,6 +2448,11 @@ export const dict: Record = { 'opencodeUpdate.toast.failed.description': 'Aktualizacja OpenCode nie powiodła się.', 'opencodeUpdate.toast.reload.message': 'Ponowne uruchamianie OpenCode...', 'vscodeLayout.actions.backToSessionsAria': 'Powrót do sesji', + 'vscodeLayout.actions.archiveAllAria': 'Archiwizuj wszystkie sesje', + 'vscodeLayout.actions.archiveAllConfirm': 'Archiwizować wszystko', + 'vscodeLayout.actions.archiveAllSuccess': 'Zarchiwizowano {count} sesji', + 'vscodeLayout.actions.archiveAllError': 'Nie udało się zarchiwizować {count} sesji', + 'vscodeLayout.actions.cancel': 'Anuluj', 'vscodeLayout.actions.newSessionAria': 'Nowa sesja', 'vscodeLayout.actions.openAgentManagerAria': 'Otwórz menedżer agentów', 'vscodeLayout.actions.resizeSessionsSidebarAria': 'Zmień rozmiar panelu sesji', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 17eb05c7..b4a109d5 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -2387,6 +2387,11 @@ export const dict: Record = { "vscodeLayout.title.sessions": "Sessões", "vscodeLayout.title.sessionFallback": "Sessão", "vscodeLayout.actions.backToSessionsAria": "Voltar para sessões", + "vscodeLayout.actions.archiveAllAria": "Arquivar todas as sessões", + "vscodeLayout.actions.archiveAllConfirm": "Arquivar tudo", + "vscodeLayout.actions.archiveAllSuccess": "{count} sessão(ões) arquivada(s)", + "vscodeLayout.actions.archiveAllError": "Falha ao arquivar {count} sessão(ões)", + "vscodeLayout.actions.cancel": "Cancelar", "vscodeLayout.actions.newSessionAria": "Nova sessão", "vscodeLayout.actions.openAgentManagerAria": "Abrir gerenciador de agentes", "vscodeLayout.actions.resizeSessionsSidebarAria": "Ajustar tamanho do painel de sessões", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 72fffaa1..3b537572 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -2387,6 +2387,11 @@ export const dict: Record = { "vscodeLayout.title.sessions": "Сесії", "vscodeLayout.title.sessionFallback": "Сесія", "vscodeLayout.actions.backToSessionsAria": "Назад до сесій", + "vscodeLayout.actions.archiveAllAria": "Архівувати всі сесії", + "vscodeLayout.actions.archiveAllConfirm": "Архівувати все", + "vscodeLayout.actions.archiveAllSuccess": "Заархівовано {count} сесій(ї)", + "vscodeLayout.actions.archiveAllError": "Не вдалося заархівувати {count} сесій(ї)", + "vscodeLayout.actions.cancel": "Скасувати", "vscodeLayout.actions.newSessionAria": "Нова сесія", "vscodeLayout.actions.openAgentManagerAria": "Відкрити менеджер агентів", "vscodeLayout.actions.resizeSessionsSidebarAria": "Змінити розмір бічної панелі сесій", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index d561e987..d54d992b 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -2387,6 +2387,11 @@ export const dict: Record = { 'vscodeLayout.title.sessions': '会话', 'vscodeLayout.title.sessionFallback': '会话', 'vscodeLayout.actions.backToSessionsAria': '返回会话列表', + "vscodeLayout.actions.archiveAllAria": "归档所有会话", + "vscodeLayout.actions.archiveAllConfirm": "归档全部", + "vscodeLayout.actions.archiveAllSuccess": "已归档 {count} 个会话", + "vscodeLayout.actions.archiveAllError": "归档 {count} 个会话失败", + "vscodeLayout.actions.cancel": "取消", 'vscodeLayout.actions.newSessionAria': '新建会话', 'vscodeLayout.actions.openAgentManagerAria': '打开智能体管理器', 'vscodeLayout.actions.resizeSessionsSidebarAria': '调整会话侧边栏大小', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index ecc1171d..3159a40d 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -2384,6 +2384,11 @@ export const dict: Record = { 'vscodeLayout.title.sessions': '會話', 'vscodeLayout.title.sessionFallback': '會話', 'vscodeLayout.actions.backToSessionsAria': '返回會話清單', + 'vscodeLayout.actions.archiveAllAria': '封存所有會話', + 'vscodeLayout.actions.archiveAllConfirm': '全部封存', + 'vscodeLayout.actions.archiveAllSuccess': '已封存 {count} 個會話', + 'vscodeLayout.actions.archiveAllError': '封存 {count} 個會話失敗', + 'vscodeLayout.actions.cancel': '取消', 'vscodeLayout.actions.newSessionAria': '新增會話', 'vscodeLayout.actions.openAgentManagerAria': '開啟 Agent 管理器', 'vscodeLayout.actions.resizeSessionsSidebarAria': '調整會話側邊欄大小',