diff --git a/apps/desktop/src/app/chat/sidebar/index.tsx b/apps/desktop/src/app/chat/sidebar/index.tsx index 03b65d51f5cc..fc58cb198779 100644 --- a/apps/desktop/src/app/chat/sidebar/index.tsx +++ b/apps/desktop/src/app/chat/sidebar/index.tsx @@ -22,6 +22,14 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { PlatformAvatar } from '@/app/messaging/platform-icon' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog' import { DisclosureCaret } from '@/components/ui/disclosure-caret' import { KbdGroup } from '@/components/ui/kbd' import { SearchField } from '@/components/ui/search-field' @@ -278,6 +286,7 @@ interface ChatSidebarProps extends React.ComponentProps { onResumeSession: (sessionId: string) => void onDeleteSession: (sessionId: string) => void onArchiveSession: (sessionId: string) => void + onArchiveAllSessions: () => Promise | void onNewSessionInWorkspace: (path: null | string) => void onManageCronJob: (jobId: string) => void onTriggerCronJob: (jobId: string) => void @@ -292,6 +301,7 @@ export function ChatSidebar({ onResumeSession, onDeleteSession, onArchiveSession, + onArchiveAllSessions, onNewSessionInWorkspace, onManageCronJob, onTriggerCronJob @@ -313,6 +323,8 @@ export function ChatSidebar({ const cronSessions = useStore($cronSessions) const cronJobs = useStore($cronJobs) const [remoteOpen, setRemoteOpen] = useState(true) + const [archiveAllOpen, setArchiveAllOpen] = useState(false) + const [archiveAllSubmitting, setArchiveAllSubmitting] = useState(false) const messagingSessions = useStore($messagingSessions) const messagingPlatformTotals = useStore($messagingPlatformTotals) const messagingTruncated = useStore($messagingTruncated) @@ -649,6 +661,24 @@ export function ChatSidebar({ const remainingSessionCount = Math.max(0, knownSessionTotal - loadedSessionCount) const recentsMeta = countLabel(agentSessions.length, knownSessionTotal) + const archiveAllDisabled = sessionsLoading || agentSessions.length === 0 || archiveAllSubmitting + + const handleArchiveAll = async () => { + if (archiveAllSubmitting) { + return + } + + setArchiveAllSubmitting(true) + + try { + await onArchiveAllSessions() + setArchiveAllOpen(false) + } catch { + // The caller owns the error toast/rollback; keep the dialog open. + } finally { + setArchiveAllSubmitting(false) + } + } const displayAgentGroups = showAllProfiles ? profileGroups : agentsGrouped ? agentGroups : undefined @@ -886,33 +916,64 @@ export function ChatSidebar({ forceEmptyState={showSessionSkeletons} groups={displayAgentGroups} headerAction={ - // Always reserve the icon-xs (size-6) slot so the header keeps the - // same height whether or not the toggle renders — otherwise the - // "Sessions" label jumps when switching to the ALL-profiles view. - // Grouping operates on unpinned recents; if everything is pinned - // the toggle does nothing, and it's irrelevant in the ALL-profiles - // view (always grouped by profile), so hide the button (not the slot). -
- {!showAllProfiles && agentSessions.length > 0 ? ( - - - - ) : null} + // Two right-aligned header actions sharing one flex row so they + // stay vertically centered with the "Sessions N/M" label (the + // header is items-center justify-between). Each lives in a fixed + // size-6 slot so the row height — and the label baseline — never + // shifts as buttons show/hide across views. +
+
+ {!showAllProfiles && agentSessions.length > 0 ? ( + + + + ) : null} +
+ {/* Always reserve the icon-xs (size-6) slot so the header keeps the + same height whether or not the toggle renders — otherwise the + "Sessions" label jumps when switching to the ALL-profiles view. + Grouping operates on unpinned recents; if everything is pinned + the toggle does nothing, and it's irrelevant in the ALL-profiles + view (always grouped by profile), so hide the button (not the slot). */} +
+ {!showAllProfiles && agentSessions.length > 0 ? ( + + + + ) : null} +
} label={s.sessions} @@ -1002,10 +1063,52 @@ export function ChatSidebar({
)} + ) } +interface ArchiveAllSessionsDialogProps { + count: number + open: boolean + onConfirm: () => void | Promise + onOpenChange: (open: boolean) => void + submitting: boolean +} + +function ArchiveAllSessionsDialog({ count, open, onConfirm, onOpenChange, submitting }: ArchiveAllSessionsDialogProps) { + const { t } = useI18n() + const s = t.sidebar + + return ( + + + + {s.archiveAllDialogTitle} + {s.archiveAllDialogDesc} + +
+ {count > 0 ? s.archiveAllChecked(count) : s.archiveAllNone} +
+ + + + +
+
+ ) +} + interface SidebarSectionHeaderProps { label: string open: boolean diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 92d0a1c3aa16..ea0868455176 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -632,6 +632,7 @@ export function DesktopController() { const sidebar = ( archiveAllSessions().then(() => refreshSessions())} onArchiveSession={sessionId => void archiveSession(sessionId)} onDeleteSession={sessionId => void removeSession(sessionId)} onLoadMoreProfileSessions={loadMoreSessionsForProfile} diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index 62db6101da1a..c7aef2b4c6a1 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -1070,6 +1070,16 @@ export const en: Translations = { groupAriaUngrouped: 'Group sessions by workspace', groupTitleGrouped: 'Ungroup sessions', groupTitleUngrouped: 'Group by workspace', + archiveAllTitle: 'Archive all sessions', + archiveAllAria: 'Archive all unpinned sessions', + archiveAllDialogTitle: 'Archive all sessions', + archiveAllDialogDesc: + 'Archive unpinned sessions from the sidebar. Pinned chats, the current chat, and running sessions stay visible.', + archiveAllConfirm: 'Archive all', + archiveAllCancel: 'Cancel', + archiveAllSubmitting: 'Archiving...', + archiveAllChecked: count => `${count} visible session${count === 1 ? '' : 's'} will be checked.`, + archiveAllNone: 'No sessions to archive.', allPinned: 'Everything here is pinned. Unpin a chat to show it in recents.', shiftClickHint: 'Shift-click a chat to pin', noWorkspace: 'No workspace', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index f19a38b07f5d..dc0b13f6f941 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -1155,6 +1155,15 @@ export const ja = defineLocale({ groupAriaUngrouped: 'ワークスペースごとにセッションをグループ化', groupTitleGrouped: 'セッションのグループ化を解除', groupTitleUngrouped: 'ワークスペースでグループ化', + archiveAllTitle: 'すべてのセッションをアーカイブ', + archiveAllAria: '固定されていないすべてのセッションをアーカイブ', + archiveAllDialogTitle: 'すべてのセッションをアーカイブ', + archiveAllDialogDesc: 'サイドバーから固定されていないセッションをアーカイブします。固定中のチャット、現在のチャット、実行中のセッションは表示されたままです。', + archiveAllConfirm: 'すべてアーカイブ', + archiveAllCancel: 'キャンセル', + archiveAllSubmitting: 'アーカイブ中…', + archiveAllChecked: count => `表示中の ${count} 件のセッションが対象になります。`, + archiveAllNone: 'アーカイブするセッションはありません。', allPinned: 'ここにあるものはすべてピン留めされています。チャットのピン留めを解除すると最近のものに表示されます。', shiftClickHint: 'Shift クリックでピン留め · ドラッグで並べ替え', noWorkspace: 'ワークスペースなし', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index fc60522cf1f6..70ad52393bcd 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -823,6 +823,15 @@ export interface Translations { groupAriaUngrouped: string groupTitleGrouped: string groupTitleUngrouped: string + archiveAllTitle: string + archiveAllAria: string + archiveAllDialogTitle: string + archiveAllDialogDesc: string + archiveAllConfirm: string + archiveAllCancel: string + archiveAllSubmitting: string + archiveAllChecked: (count: number) => string + archiveAllNone: string allPinned: string shiftClickHint: string noWorkspace: string diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index e566365faad1..c1e04eaa3689 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -1121,6 +1121,15 @@ export const zhHant = defineLocale({ groupAriaUngrouped: '依工作區分組工作階段', groupTitleGrouped: '取消分組', groupTitleUngrouped: '依工作區分組', + archiveAllTitle: '封存所有工作階段', + archiveAllAria: '封存所有未釘選的工作階段', + archiveAllDialogTitle: '封存所有工作階段', + archiveAllDialogDesc: '從側邊欄封存未釘選的工作階段。已釘選的對話、目前對話與執行中的工作階段仍會顯示。', + archiveAllConfirm: '全部封存', + archiveAllCancel: '取消', + archiveAllSubmitting: '封存中…', + archiveAllChecked: count => `將封存 ${count} 個顯示中的工作階段。`, + archiveAllNone: '沒有可封存的工作階段。', allPinned: '這裡的全部已釘選。取消釘選某個聊天即可在最近中顯示。', shiftClickHint: 'Shift + 點擊聊天以釘選 · 拖曳以重新排序', noWorkspace: '無工作區', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 09ac100abdc9..6b294f708b80 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -1217,6 +1217,15 @@ export const zh: Translations = { groupAriaUngrouped: '按工作区分组会话', groupTitleGrouped: '取消分组', groupTitleUngrouped: '按工作区分组', + archiveAllTitle: '归档所有会话', + archiveAllAria: '归档所有未置顶的会话', + archiveAllDialogTitle: '归档所有会话', + archiveAllDialogDesc: '从侧边栏归档未置顶的会话。已置顶的对话、当前对话和正在运行的会话仍会显示。', + archiveAllConfirm: '全部归档', + archiveAllCancel: '取消', + archiveAllSubmitting: '正在归档…', + archiveAllChecked: count => `将归档 ${count} 个可见会话。`, + archiveAllNone: '没有可归档的会话。', allPinned: '这里的全部已置顶。取消置顶某个对话即可在最近中显示。', shiftClickHint: 'Shift+ 单击对话以置顶 · 拖动以重新排序', noWorkspace: '无工作区',