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 <artmore@protonmail.com>
This commit is contained in:
Jake
2026-06-08 15:11:05 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent bba4b02dca
commit 5589ca991a
12 changed files with 208 additions and 5 deletions
@@ -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<number | null>(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<string, Session>();
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<string, Session>();
for (const session of allSessions) byId.set(session.id, session);
const childrenMap = new Map<string, string[]>();
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<string>();
const addDescendants = (sessionId: string, visited: Set<string>) => {
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 = () => {
<div className="flex flex-col h-full">
<VSCodeHeader
title={t('vscodeLayout.title.sessions')}
onArchiveAll={handleArchiveAll}
/>
<div className="flex-1 overflow-hidden">
<SessionSidebar
@@ -559,6 +660,7 @@ interface VSCodeHeaderProps {
title: string;
showBack?: boolean;
onBack?: () => void;
onArchiveAll?: () => void;
onNewSession?: () => void;
onSettings?: () => void;
onAgentManager?: () => void;
@@ -568,7 +670,8 @@ interface VSCodeHeaderProps {
enableSessionSwitcher?: boolean;
}
const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, onNewSession, onSettings, onAgentManager, showMcp, showContextUsage, showRateLimits, enableSessionSwitcher }) => {
const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ 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<VSCodeHeaderProps> = ({ title, showBack, onBack, on
</button>
</SessionSwitcherDropdown>
) : (
<h1 className="text-sm font-medium truncate flex-1" title={title}>{title}</h1>
<SessionsTabTitle title={title} onArchiveAll={onArchiveAll} />
)}
<div className="min-w-0 flex-1" />
{onNewSession && (
@@ -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<ArchiveAllDropdownProps> = ({ onArchiveAll }) => {
const { t } = useI18n();
return (
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<button
type="button"
className="inline-flex h-8 w-8 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
aria-label={t('vscodeLayout.actions.archiveAllAria')}
>
<Icon name="archive" className="h-5 w-5" />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>{t('vscodeLayout.actions.archiveAllAria')}</p></TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="min-w-[160px]">
<DropdownMenuItem onSelect={onArchiveAll}>
{t('vscodeLayout.actions.archiveAllConfirm')}
</DropdownMenuItem>
<DropdownMenuItem>{t('vscodeLayout.actions.cancel')}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
export { ArchiveAllDropdown };
@@ -0,0 +1,16 @@
import React from 'react';
import { ArchiveAllDropdown } from '@/components/session/ArchiveAllDropdown';
interface SessionsTabTitleProps {
title: string;
onArchiveAll?: () => void;
}
const SessionsTabTitle: React.FC<SessionsTabTitleProps> = ({ title, onArchiveAll }) => (
<>
<h1 className="text-sm font-medium truncate flex-1" title={title}>{title}</h1>
{onArchiveAll && <ArchiveAllDropdown onArchiveAll={onArchiveAll} />}
</>
);
export { SessionsTabTitle };
@@ -821,7 +821,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive focus:text-destructive [&>svg]:mr-1" onClick={() => handleDeleteSession(session, { archivedBucket })}>
<Icon name="delete-bin" className="mr-1 h-4 w-4" />
<Icon name={archivedBucket ? "delete-bin" : "archive"} className="mr-1 h-4 w-4" />
{archivedBucket ? t('sessions.sidebar.bulkActions.delete') : t('sessions.sidebar.bulkActions.archive')}
</DropdownMenuItem>
</DropdownMenuContent>
+5
View File
@@ -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',
+5
View File
@@ -2388,6 +2388,11 @@ export const dict: Record<I18nKey, string> = {
"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",
+5
View File
@@ -2421,6 +2421,11 @@ export const dict: Record<I18nKey, string> = {
'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': '세션 사이드바 크기 조정',
+5
View File
@@ -2448,6 +2448,11 @@ export const dict: Record<I18nKey, string> = {
'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',
@@ -2387,6 +2387,11 @@ export const dict: Record<I18nKey, string> = {
"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",
+5
View File
@@ -2387,6 +2387,11 @@ export const dict: Record<I18nKey, string> = {
"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": "Змінити розмір бічної панелі сесій",
@@ -2387,6 +2387,11 @@ export const dict: Record<I18nKey, string> = {
'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': '调整会话侧边栏大小',
@@ -2384,6 +2384,11 @@ export const dict: Record<I18nKey, string> = {
'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': '調整會話側邊欄大小',