mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 00:03:59 +00:00
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:
@@ -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,
|
||||
|
||||
+15
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "생성됨",
|
||||
|
||||
@@ -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": "创建时间",
|
||||
|
||||
Reference in New Issue
Block a user