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:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
bba4b02dca
commit
5589ca991a
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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': '세션 사이드바 크기 조정',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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': '調整會話側邊欄大小',
|
||||
|
||||
Reference in New Issue
Block a user