diff --git a/config/scripts/locale-ko-key-overrides.json b/config/scripts/locale-ko-key-overrides.json index 4618f052f1e..d5648b40fbd 100644 --- a/config/scripts/locale-ko-key-overrides.json +++ b/config/scripts/locale-ko-key-overrides.json @@ -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": "에이전트를 하나 이상 선택하세요" } } diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx index 048ba89252e..0113f0d22e9 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx @@ -81,6 +81,7 @@ export default function AiVaultPanel(): React.JSX.Element { setGroup, setHideEmptySessions, setAgentEnabled, + setAllAgentsEnabled, resetViewOptions } = usePersistedAiVaultViewOptions() const [collapsedGroups, setCollapsedGroups] = useState>(() => 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} diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanelControls.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanelControls.tsx index b64308b943e..b915554c3e1 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultPanelControls.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultPanelControls.tsx @@ -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 ( @@ -284,14 +289,41 @@ export function VaultViewMenu({ - - {translate('auto.components.right.sidebar.AiVaultPanelControls.agents', 'Agents')} - + {/* Why: Select all / Clear lets users isolate one agent without unchecking 15 boxes. */} +
+ + {translate('auto.components.right.sidebar.AiVaultPanelControls.agents', 'Agents')} + +
+ + +
+
{AI_VAULT_AGENTS.map((agent) => ( onAgentEnabledChange(agent, checked === true)} onSelect={(event) => event.preventDefault()} > diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanelHeader.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanelHeader.tsx index b3ec2ba36e3..86c728aa8a4 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultPanelHeader.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultPanelHeader.tsx @@ -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} diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx index 79d17f2305b..6735d0e0857 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx @@ -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 ? ( ) : null} diff --git a/src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.test.ts index 2ad8e260bdd..76624f047cf 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.test.ts @@ -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', () => { diff --git a/src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.ts b/src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.ts index 5b7a4b799cf..cace6a9c307 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.ts @@ -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) : {} const catalog = new Set(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, diff --git a/src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.test.tsx b/src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.test.tsx index ba824696c9a..3acc9ae44be 100644 --- a/src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.test.tsx +++ b/src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.test.tsx @@ -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', () => { diff --git a/src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.ts b/src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.ts index ffd6816d4a6..0315c242af7 100644 --- a/src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.ts +++ b/src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.ts @@ -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(() => 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 } } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 0354113eb90..ac4d3feba20 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -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", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 9fbec004772..c64d613a0c0 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 69b65346353..cce3561f51b 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index ae90177d18d..fb29c97f300 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -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": "생성됨", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 0c3fb081227..f645e1f272d 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -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": "创建时间",