Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 130 additions & 27 deletions apps/desktop/src/app/chat/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -278,6 +286,7 @@ interface ChatSidebarProps extends React.ComponentProps<typeof Sidebar> {
onResumeSession: (sessionId: string) => void
onDeleteSession: (sessionId: string) => void
onArchiveSession: (sessionId: string) => void
onArchiveAllSessions: () => Promise<void> | void
onNewSessionInWorkspace: (path: null | string) => void
onManageCronJob: (jobId: string) => void
onTriggerCronJob: (jobId: string) => void
Expand All @@ -292,6 +301,7 @@ export function ChatSidebar({
onResumeSession,
onDeleteSession,
onArchiveSession,
onArchiveAllSessions,
onNewSessionInWorkspace,
onManageCronJob,
onTriggerCronJob
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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).
<div className="grid size-6 shrink-0 place-items-center">
{!showAllProfiles && agentSessions.length > 0 ? (
<Tip label={agentsGrouped ? s.groupTitleGrouped : s.groupTitleUngrouped}>
<Button
aria-label={agentsGrouped ? s.groupAriaGrouped : s.groupAriaUngrouped}
className={cn(
'text-(--ui-text-tertiary) opacity-70 hover:bg-(--ui-control-hover-background) hover:text-foreground hover:opacity-100 focus-visible:opacity-100',
agentsGrouped && 'bg-(--ui-control-active-background) text-foreground opacity-100'
)}
onClick={event => {
event.stopPropagation()
setSidebarRecentsOpen(true)
setSidebarAgentsGrouped(!agentsGrouped)
}}
size="icon-xs"
variant="ghost"
>
<Codicon name={agentsGrouped ? 'list-unordered' : 'root-folder'} size="0.75rem" />
</Button>
</Tip>
) : 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.
<div className="flex items-center gap-0.5">
<div className="grid size-6 shrink-0 place-items-center">
{!showAllProfiles && agentSessions.length > 0 ? (
<Tip label={s.archiveAllTitle}>
<Button
aria-label={s.archiveAllAria}
className="text-(--ui-text-tertiary) opacity-70 hover:bg-(--ui-control-hover-background) hover:text-foreground hover:opacity-100 focus-visible:opacity-100"
disabled={archiveAllDisabled}
onClick={event => {
event.stopPropagation()
setSidebarRecentsOpen(true)
setArchiveAllOpen(true)
}}
size="icon-xs"
variant="ghost"
>
<Codicon
name={archiveAllSubmitting ? 'loading' : 'archive'}
size="0.75rem"
spinning={archiveAllSubmitting}
/>
</Button>
</Tip>
) : null}
</div>
{/* 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). */}
<div className="grid size-6 shrink-0 place-items-center">
{!showAllProfiles && agentSessions.length > 0 ? (
<Tip label={agentsGrouped ? s.groupTitleGrouped : s.groupTitleUngrouped}>
<Button
aria-label={agentsGrouped ? s.groupAriaGrouped : s.groupAriaUngrouped}
className={cn(
'text-(--ui-text-tertiary) opacity-70 hover:bg-(--ui-control-hover-background) hover:text-foreground hover:opacity-100 focus-visible:opacity-100',
agentsGrouped && 'bg-(--ui-control-active-background) text-foreground opacity-100'
)}
onClick={event => {
event.stopPropagation()
setSidebarRecentsOpen(true)
setSidebarAgentsGrouped(!agentsGrouped)
}}
size="icon-xs"
variant="ghost"
>
<Codicon name={agentsGrouped ? 'list-unordered' : 'root-folder'} size="0.75rem" />
</Button>
</Tip>
) : null}
</div>
</div>
}
label={s.sessions}
Expand Down Expand Up @@ -1002,10 +1063,52 @@ export function ChatSidebar({
</div>
)}
</SidebarContent>
<ArchiveAllSessionsDialog
count={knownSessionTotal}
onConfirm={handleArchiveAll}
onOpenChange={setArchiveAllOpen}
open={archiveAllOpen}
submitting={archiveAllSubmitting}
/>
</Sidebar>
)
}

interface ArchiveAllSessionsDialogProps {
count: number
open: boolean
onConfirm: () => void | Promise<void>
onOpenChange: (open: boolean) => void
submitting: boolean
}

function ArchiveAllSessionsDialog({ count, open, onConfirm, onOpenChange, submitting }: ArchiveAllSessionsDialogProps) {
const { t } = useI18n()
const s = t.sidebar

return (
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{s.archiveAllDialogTitle}</DialogTitle>
<DialogDescription>{s.archiveAllDialogDesc}</DialogDescription>
</DialogHeader>
<div className="rounded-md border border-(--ui-stroke-tertiary) bg-(--ui-control-background) px-3 py-2 text-xs text-(--ui-text-secondary)">
{count > 0 ? s.archiveAllChecked(count) : s.archiveAllNone}
</div>
<DialogFooter>
<Button disabled={submitting} onClick={() => onOpenChange(false)} type="button" variant="ghost">
{s.archiveAllCancel}
</Button>
<Button disabled={submitting} onClick={() => void onConfirm()} type="button" variant="destructive">
{submitting ? s.archiveAllSubmitting : s.archiveAllConfirm}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

interface SidebarSectionHeaderProps {
label: string
open: boolean
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/app/desktop-controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,7 @@ export function DesktopController() {
const sidebar = (
<ChatSidebar
currentView={currentView}
onArchiveAllSessions={() => archiveAllSessions().then(() => refreshSessions())}
onArchiveSession={sessionId => void archiveSession(sessionId)}
onDeleteSession={sessionId => void removeSession(sessionId)}
onLoadMoreProfileSessions={loadMoreSessionsForProfile}
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'ワークスペースなし',
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/i18n/zh-hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '無工作區',
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '无工作区',
Expand Down
Loading