Allow clearing all agents from AI Vault session history filter

Add "Select all" / "Clear" buttons so users can quickly isolate one agent without unchecking each box individually. Previously, at least one agent had to remain enabled; now users can filter to zero agents and re-enable selectively.
This commit is contained in:
Jinjing
2026-08-02 11:41:24 -07:00
parent 0ae9174408
commit 0f370b382d
14 changed files with 124 additions and 21 deletions
@@ -5038,5 +5038,14 @@
},
"settings.appearance.statusBar.sshToggleDescription": {
"ko": "사용 가능한 SSH 및 원격 Orca 호스트가 있으면 표시합니다."
},
"auto.components.right.sidebar.AiVaultPanelControls.selectAllAgents": {
"ko": "모두 선택"
},
"auto.components.right.sidebar.AiVaultPanelControls.clearAgents": {
"ko": "지우기"
},
"auto.components.right.sidebar.AiVaultPanel.selectAtLeastOneAgent": {
"ko": "에이전트를 하나 이상 선택하세요"
}
}
@@ -81,6 +81,7 @@ export default function AiVaultPanel(): React.JSX.Element {
setGroup,
setHideEmptySessions,
setAgentEnabled,
setAllAgentsEnabled,
resetViewOptions
} = usePersistedAiVaultViewOptions()
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(() => new Set())
@@ -320,6 +321,7 @@ export default function AiVaultPanel(): React.JSX.Element {
onScopeChange={handleScopeChange}
onExecutionHostScopeChange={onExecutionHostScopeChange}
onAgentEnabledChange={setAgentEnabled}
onAllAgentsEnabledChange={setAllAgentsEnabled}
onSortChange={setSort}
onGroupChange={setGroup}
onHideEmptySessionsChange={setHideEmptySessions}
@@ -349,6 +351,7 @@ export default function AiVaultPanel(): React.JSX.Element {
loading={loading}
sessionsCount={sessions.length}
filteredSessionsCount={filteredSessions.length}
noAgentsSelected={agents.length === 0}
error={error}
vaultScope={scope}
buildResumeStartup={launchActions.buildResumeStartup}
@@ -234,6 +234,7 @@ export function VaultViewMenu({
hideEmptySessions,
adjustmentCount,
onAgentEnabledChange,
onAllAgentsEnabledChange,
onSortChange,
onGroupChange,
onHideEmptySessionsChange,
@@ -245,11 +246,15 @@ export function VaultViewMenu({
hideEmptySessions: boolean
adjustmentCount: number
onAgentEnabledChange: (agent: AiVaultAgent, enabled: boolean) => void
onAllAgentsEnabledChange: (enabled: boolean) => void
onSortChange: (sort: AiVaultSort) => void
onGroupChange: (group: AiVaultGroup) => void
onHideEmptySessionsChange: (hideEmptySessions: boolean) => void
onReset: () => void
}): React.JSX.Element {
const allAgentsSelected = agents.length === AI_VAULT_AGENTS.length
const noAgentsSelected = agents.length === 0
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -284,14 +289,41 @@ export function VaultViewMenu({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={6} className="w-56">
<DropdownMenuLabel>
{translate('auto.components.right.sidebar.AiVaultPanelControls.agents', 'Agents')}
</DropdownMenuLabel>
{/* Why: Select all / Clear lets users isolate one agent without unchecking 15 boxes. */}
<div className="flex items-center justify-between px-2 py-1">
<span className="text-[11px] font-semibold text-muted-foreground">
{translate('auto.components.right.sidebar.AiVaultPanelControls.agents', 'Agents')}
</span>
<div className="flex items-center gap-1">
<button
type="button"
// Why: prevent focus move so Radix does not dismiss the menu mid multi-select.
onPointerDown={(event) => event.preventDefault()}
onClick={() => onAllAgentsEnabledChange(true)}
className="rounded-full px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-40 disabled:hover:bg-transparent"
disabled={allAgentsSelected}
>
{translate(
'auto.components.right.sidebar.AiVaultPanelControls.selectAllAgents',
'Select all'
)}
</button>
<button
type="button"
// Why: prevent focus move so Radix does not dismiss the menu mid multi-select.
onPointerDown={(event) => event.preventDefault()}
onClick={() => onAllAgentsEnabledChange(false)}
className="rounded-full px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-40 disabled:hover:bg-transparent"
disabled={noAgentsSelected}
>
{translate('auto.components.right.sidebar.AiVaultPanelControls.clearAgents', 'Clear')}
</button>
</div>
</div>
{AI_VAULT_AGENTS.map((agent) => (
<DropdownMenuCheckboxItem
key={agent}
checked={agents.includes(agent)}
disabled={agents.length === 1 && agents.includes(agent)}
onCheckedChange={(checked) => onAgentEnabledChange(agent, checked === true)}
onSelect={(event) => event.preventDefault()}
>
@@ -31,6 +31,7 @@ type AiVaultPanelHeaderProps = {
onScopeChange: (scope: AiVaultScope) => void
onExecutionHostScopeChange: (scope: ExecutionHostScope) => void
onAgentEnabledChange: (agent: AiVaultAgent, enabled: boolean) => void
onAllAgentsEnabledChange: (enabled: boolean) => void
onSortChange: (sort: AiVaultSort) => void
onGroupChange: (group: AiVaultGroup) => void
onHideEmptySessionsChange: (hideEmptySessions: boolean) => void
@@ -58,6 +59,7 @@ export function AiVaultPanelHeader({
onScopeChange,
onExecutionHostScopeChange,
onAgentEnabledChange,
onAllAgentsEnabledChange,
onSortChange,
onGroupChange,
onHideEmptySessionsChange,
@@ -119,6 +121,7 @@ export function AiVaultPanelHeader({
hideEmptySessions={hideEmptySessions}
adjustmentCount={adjustmentCount}
onAgentEnabledChange={onAgentEnabledChange}
onAllAgentsEnabledChange={onAllAgentsEnabledChange}
onSortChange={onSortChange}
onGroupChange={onGroupChange}
onHideEmptySessionsChange={onHideEmptySessionsChange}
@@ -46,6 +46,7 @@ export function AiVaultSessionVirtualList({
loading,
sessionsCount,
filteredSessionsCount,
noAgentsSelected,
error,
vaultScope,
buildResumeStartup,
@@ -71,6 +72,7 @@ export function AiVaultSessionVirtualList({
loading: boolean
sessionsCount: number
filteredSessionsCount: number
noAgentsSelected: boolean
error: string | null
vaultScope: AiVaultScope
buildResumeStartup: (session: AiVaultSession, worktreeId?: string | null) => AiVaultResumeStartup
@@ -181,10 +183,17 @@ export function AiVaultSessionVirtualList({
{sessionsCount > 0 && filteredSessionsCount === 0 ? (
<EmptyState
title={translate(
'auto.components.right.sidebar.AiVaultPanel.noSessionsMatchFilters',
'No sessions match the current filters'
)}
title={
noAgentsSelected
? translate(
'auto.components.right.sidebar.AiVaultPanel.selectAtLeastOneAgent',
'Select at least one agent'
)
: translate(
'auto.components.right.sidebar.AiVaultPanel.noSessionsMatchFilters',
'No sessions match the current filters'
)
}
/>
) : null}
@@ -66,7 +66,7 @@ describe('AI Vault view option persistence', () => {
expect(normalizeAiVaultViewOptions({ group: 'agent' }).group).toBe('agent')
})
it('falls back to all enabled when stored state disables the whole catalog', () => {
it('preserves a fully cleared agent selection', () => {
const normalized = normalizeAiVaultViewOptions({
disabledAgents: [...AI_VAULT_AGENTS],
sort: 'created',
@@ -74,8 +74,8 @@ describe('AI Vault view option persistence', () => {
hideEmptySessions: true
})
expect(normalized.disabledAgents).toEqual([])
expect(enabledAiVaultAgents(normalized.disabledAgents)).toEqual([...AI_VAULT_AGENTS])
expect(normalized.disabledAgents).toEqual([...AI_VAULT_AGENTS])
expect(enabledAiVaultAgents(normalized.disabledAgents)).toEqual([])
})
it('falls back safely when JSON or storage access is unavailable', () => {
@@ -49,14 +49,12 @@ function isAiVaultGroup(value: unknown): value is AiVaultGroup {
export function normalizeAiVaultViewOptions(value: unknown): AiVaultViewOptions {
const record = value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
const catalog = new Set<string>(AI_VAULT_AGENTS)
const normalizedDisabledAgents = Array.isArray(record.disabledAgents)
// Why: empty selection is intentional (Clear all agents) so users can enable only one agent.
const disabledAgents = Array.isArray(record.disabledAgents)
? [...new Set(record.disabledAgents)].filter(
(agent): agent is AiVaultAgent => typeof agent === 'string' && catalog.has(agent)
)
: []
// Why: a stale catalog or hand-edited value must not leave the panel with no selectable agents.
const disabledAgents =
normalizedDisabledAgents.length < AI_VAULT_AGENTS.length ? normalizedDisabledAgents : []
return {
disabledAgents,
@@ -34,7 +34,20 @@ describe('usePersistedAiVaultViewOptions', () => {
expect(restored.result.current.hideEmptySessions).toBe(true)
})
it('keeps at least one agent enabled', () => {
it('allows clearing every agent so a single agent can be re-enabled', () => {
const hook = renderHook(() => usePersistedAiVaultViewOptions())
act(() => hook.result.current.setAllAgentsEnabled(false))
expect(hook.result.current.agents).toEqual([])
act(() => hook.result.current.setAgentEnabled('claude', true))
expect(hook.result.current.agents).toEqual(['claude'])
act(() => hook.result.current.setAllAgentsEnabled(true))
expect(hook.result.current.agents).toEqual([...AI_VAULT_AGENTS])
})
it('allows disabling the last remaining agent', () => {
const hook = renderHook(() => usePersistedAiVaultViewOptions())
const lastEnabled = AI_VAULT_AGENTS[0]
@@ -46,7 +59,7 @@ describe('usePersistedAiVaultViewOptions', () => {
expect(hook.result.current.agents).toEqual([lastEnabled])
act(() => hook.result.current.setAgentEnabled(lastEnabled, false))
expect(hook.result.current.agents).toEqual([lastEnabled])
expect(hook.result.current.agents).toEqual([])
})
it('keeps in-memory options usable when persistence fails', () => {
@@ -1,5 +1,10 @@
import { useCallback, useMemo, useRef, useState } from 'react'
import type { AiVaultAgent, AiVaultGroup, AiVaultSort } from '../../../../shared/ai-vault-types'
import {
AI_VAULT_AGENTS,
type AiVaultAgent,
type AiVaultGroup,
type AiVaultSort
} from '../../../../shared/ai-vault-types'
import {
createDefaultAiVaultViewOptions,
enabledAiVaultAgents,
@@ -19,6 +24,7 @@ export function usePersistedAiVaultViewOptions(): {
setGroup: (group: AiVaultGroup) => void
setHideEmptySessions: (hide: boolean) => void
setAgentEnabled: (agent: AiVaultAgent, enabled: boolean) => void
setAllAgentsEnabled: (enabled: boolean) => void
resetViewOptions: () => void
} {
const [options, setOptions] = useState<AiVaultViewOptions>(() => readAiVaultViewOptions())
@@ -66,12 +72,26 @@ export function usePersistedAiVaultViewOptions(): {
if (enabled === !isDisabled) {
return current
}
// Why: allow zero enabled agents so Clear + re-check one agent is a two-step filter.
const disabledAgents = enabled
? current.disabledAgents.filter((entry) => entry !== agent)
: [...current.disabledAgents, agent]
return enabledAiVaultAgents(disabledAgents).length > 0
? { ...current, disabledAgents }
: current
return { ...current, disabledAgents }
})
},
[updateOptions]
)
const setAllAgentsEnabled = useCallback(
(enabled: boolean) => {
updateOptions((current) => {
const disabledAgents = enabled ? [] : [...AI_VAULT_AGENTS]
if (
disabledAgents.length === current.disabledAgents.length &&
disabledAgents.every((agent) => current.disabledAgents.includes(agent))
) {
return current
}
return { ...current, disabledAgents }
})
},
[updateOptions]
@@ -94,6 +114,7 @@ export function usePersistedAiVaultViewOptions(): {
setGroup,
setHideEmptySessions,
setAgentEnabled,
setAllAgentsEnabled,
resetViewOptions
}
}
+3
View File
@@ -11202,6 +11202,7 @@
"transcriptsSkipped": "{{count}} transcript skipped",
"noAgentSessionsFound": "No agent sessions found",
"noSessionsMatchFilters": "No sessions match the current filters",
"selectAtLeastOneAgent": "Select at least one agent",
"sessionId": "Session ID",
"logPath": "Log path",
"originalPaneUnavailable": "Original pane is no longer available.",
@@ -11225,6 +11226,8 @@
"viewOptionsAriaLabel": "Session History view options",
"viewOptions": "View options",
"agents": "Agents",
"selectAllAgents": "Select all",
"clearAgents": "Clear",
"sort": "Sort",
"lastUpdated": "Last updated",
"created": "Created",
+3
View File
@@ -11091,6 +11091,7 @@
"transcriptsSkipped": "{{count}} transcripciones omitidas",
"noAgentSessionsFound": "No se encontraron sesiones de agentes",
"noSessionsMatchFilters": "Ninguna sesión coincide con los filtros actuales",
"selectAtLeastOneAgent": "Selecciona al menos un agente",
"sessionId": "ID de sesión",
"logPath": "Ruta del log",
"agents": "Agentes",
@@ -11116,6 +11117,8 @@
"viewOptionsAriaLabel": "Opciones de vista del historial de sesiones",
"viewOptions": "Opciones de vista",
"agents": "Agentes",
"selectAllAgents": "Seleccionar todo",
"clearAgents": "Limpiar",
"sort": "Ordenar",
"lastUpdated": "Última actualización",
"created": "Creado",
+3
View File
@@ -11091,6 +11091,7 @@
"transcriptsSkipped": "{{count}} transcript skipped",
"noAgentSessionsFound": "No agent sessions found",
"noSessionsMatchFilters": "No sessions match the current filters",
"selectAtLeastOneAgent": "エージェントを1つ以上選択してください",
"sessionId": "セッション ID",
"logPath": "ログパス",
"agents": "エージェント",
@@ -11116,6 +11117,8 @@
"viewOptionsAriaLabel": "Session History view options",
"viewOptions": "表示オプション",
"agents": "エージェント",
"selectAllAgents": "すべて選択",
"clearAgents": "クリア",
"sort": "並び替え",
"lastUpdated": "最終更新",
"created": "Created",
+3
View File
@@ -11091,6 +11091,7 @@
"transcriptsSkipped": "대화 기록 {{count}}개 건너뜀",
"noAgentSessionsFound": "agent 세션을 찾을 수 없습니다",
"noSessionsMatchFilters": "현재 필터와 일치하는 세션이 없습니다",
"selectAtLeastOneAgent": "에이전트를 하나 이상 선택하세요",
"sessionId": "세션 ID",
"logPath": "로그 경로",
"agents": "에이전트",
@@ -11116,6 +11117,8 @@
"viewOptionsAriaLabel": "세션 기록 보기 옵션",
"viewOptions": "보기 옵션",
"agents": "에이전트",
"selectAllAgents": "모두 선택",
"clearAgents": "지우기",
"sort": "정렬",
"lastUpdated": "마지막 업데이트",
"created": "생성됨",
+3
View File
@@ -11091,6 +11091,7 @@
"transcriptsSkipped": "跳过了 {{count}} 个记录",
"noAgentSessionsFound": "未找到智能体会话",
"noSessionsMatchFilters": "没有会话符合当前筛选条件",
"selectAtLeastOneAgent": "请至少选择一个智能体",
"sessionId": "会话 ID",
"logPath": "日志路径",
"agents": "智能体",
@@ -11116,6 +11117,8 @@
"viewOptionsAriaLabel": "会话历史查看选项",
"viewOptions": "查看选项",
"agents": "智能体",
"selectAllAgents": "选择全部",
"clearAgents": "清除",
"sort": "排序",
"lastUpdated": "最后更新",
"created": "创建时间",