diff --git a/src/renderer/src/components/dashboard-popout/AgentDashboardToolbar.tsx b/src/renderer/src/components/dashboard-popout/AgentDashboardToolbar.tsx index afcdfef326a..1db311c5ecf 100644 --- a/src/renderer/src/components/dashboard-popout/AgentDashboardToolbar.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentDashboardToolbar.tsx @@ -1,4 +1,5 @@ import { ChevronDown, Filter, Search, X } from 'lucide-react' +import { AgentStateDot } from '@/components/AgentStateDot' import { Button } from '@/components/ui/button' import { DropdownMenu, @@ -24,6 +25,7 @@ import { type DashboardReviewFilter, toggleDashboardFilter } from './agent-board-filtering' +import { countAgentMapCards, type AgentMapState } from './agent-map-filter' import { AgentDashboardFilterChips } from './AgentDashboardFilterChips' import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo' @@ -37,9 +39,36 @@ type AgentDashboardToolbarProps = { onQueryChange: (query: string) => void filters: DashboardFilters onFiltersChange: (filters: DashboardFilters) => void + /** Map-only: the board's columns already separate agents by state. */ + agentStates?: ReadonlySet + onAgentStateToggle?: (state: AgentMapState) => void + onAgentStatesReset?: () => void searchInputRef: React.RefObject } +const AGENT_STATE_ROWS: { + state: AgentMapState + dotState: 'waiting' | 'working' | 'done' | 'idle' +}[] = [ + { state: 'attention', dotState: 'waiting' }, + { state: 'working', dotState: 'working' }, + { state: 'done', dotState: 'done' }, + { state: 'idle', dotState: 'idle' } +] + +function agentStateLabel(state: AgentMapState): string { + switch (state) { + case 'attention': + return translate('dashboardPopout.bucket.attention', 'Needs You') + case 'working': + return translate('dashboardPopout.bucket.working', 'Working') + case 'done': + return translate('dashboardPopout.bucket.done', 'Done') + case 'idle': + return translate('dashboardPopout.bucket.idle', 'Idle') + } +} + function countBy( cards: DashboardCard[], value: (card: DashboardCard) => string @@ -137,6 +166,9 @@ export function AgentDashboardToolbar({ onQueryChange, filters, onFiltersChange, + agentStates, + onAgentStateToggle, + onAgentStatesReset, searchInputRef }: AgentDashboardToolbarProps): React.JSX.Element { const isMac = navigator.userAgent.includes('Mac') @@ -146,7 +178,10 @@ export function AgentDashboardToolbar({ cards, (card) => card.review?.state ?? (card.hasReview ? 'unknown' : 'none') ) - const activeCount = activeDashboardFilterCount(filters) + const agentStateCounts = agentStates ? countAgentMapCards(cards) : null + // A muted state is an active filter, so the badge counts them like the rest. + const mutedStateCount = agentStates ? AGENT_STATE_ROWS.length - agentStates.size : 0 + const activeCount = activeDashboardFilterCount(filters) + mutedStateCount const toggleProject = (id: string): void => onFiltersChange({ ...filters, projects: toggleDashboardFilter(filters.projects, id) }) const toggleStatus = (id: string): void => @@ -159,8 +194,10 @@ export function AgentDashboardToolbar({ ...filters, reviewStates: toggleDashboardFilter(filters.reviewStates, id) }) - const clearFilters = (): void => + const clearFilters = (): void => { onFiltersChange({ projects: [], workspaceStatuses: [], reviewStates: [] }) + onAgentStatesReset?.() + } const reviewLabel = (id: DashboardReviewFilter): string => translate('dashboardPopout.filters.reviewChip', 'Review: {{state}}', { state: reviewStateLabel(id) @@ -227,6 +264,26 @@ export function AgentDashboardToolbar({ + {agentStates && agentStateCounts ? ( + <> + + {translate('dashboardPopout.map.filters.showStates', 'Agent states')} + + {AGENT_STATE_ROWS.map(({ state, dotState }) => ( + onAgentStateToggle?.(state)} + onSelect={(event) => event.preventDefault()} + > + + {agentStateLabel(state)} + + + ))} + + + ) : null} {translate('dashboardPopout.filters.project', 'Project')} diff --git a/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx b/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx index 4ea941d9d1a..154e0e8bf02 100644 --- a/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx @@ -142,15 +142,54 @@ describe('AgentKanbanBoard', () => { fireEvent.click(screen.getByRole('button', { name: 'Agent Map' })) expect(await screen.findByText('0 of 0 agents shown')).toBeInTheDocument() - expect(screen.getByText('Agent states')).toBeInTheDocument() expect(screen.queryByText('Live containment map')).not.toBeInTheDocument() - expect(screen.queryByText('Map filters')).not.toBeInTheDocument() - expect(screen.queryByText('Choose what stays visible')).not.toBeInTheDocument() + // The map has no rail of its own; its filters live in the shared toolbar. + expect(screen.queryByRole('complementary', { name: 'Map filters' })).not.toBeInTheDocument() + expect(screen.queryByText('Agent states')).not.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'Dashboard' })) expect(screen.getByText('Needs You')).toBeInTheDocument() }) + it('filters the map from the shared toolbar filter, not a rail', async () => { + renderBoard([ + card({ paneKey: 'busy', worktreeName: 'busy-wt', worktreeId: 'w-busy' }), + card({ + paneKey: 'finished', + worktreeName: 'done-wt', + worktreeId: 'w-done', + bucket: 'done', + dotState: 'done', + finishedAt: 5, + unseen: true + }) + ]) + fireEvent.click(screen.getByRole('button', { name: 'Agent Map' })) + expect(await screen.findByText('2 of 2 agents shown')).toBeInTheDocument() + + fireEvent.pointerDown(screen.getByRole('button', { name: /^Filter/ })) + fireEvent.click(await screen.findByRole('menuitemcheckbox', { name: /Working/ })) + + expect(await screen.findByText('1 of 2 agents shown')).toBeInTheDocument() + // A muted state counts toward the Filter badge like any other filter. The + // open menu hides the trigger from the a11y tree, so read it by its label. + expect(screen.getByRole('menu')).toHaveAccessibleName('Filter 1') + }) + + it('offers agent states only on the map, where no column separates them', async () => { + renderBoard([card({ paneKey: 'busy' })]) + + fireEvent.pointerDown(screen.getByRole('button', { name: /^Filter/ })) + expect(await screen.findByText('Project')).toBeInTheDocument() + expect(screen.queryByText('Agent states')).not.toBeInTheDocument() + + fireEvent.keyDown(document.body, { key: 'Escape' }) + fireEvent.click(screen.getByRole('button', { name: 'Agent Map' })) + fireEvent.pointerDown(screen.getByRole('button', { name: /^Filter/ })) + + expect(await screen.findByText('Agent states')).toBeInTheDocument() + }) + it('keeps the selected map visible beside its terminal panel', async () => { const agent = card({ paneKey: 'map-agent', conversationName: 'Map agent' }) render() diff --git a/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.tsx b/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.tsx index 3f1be6cfc52..f0d4461bfc6 100644 --- a/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentKanbanBoard.tsx @@ -28,6 +28,7 @@ import './agent-board-transitions.css' import { translate } from '@/i18n/i18n' import { Button } from '@/components/ui/button' import { lazyWithRetry } from '@/lib/lazy-with-retry' +import { useAgentMapStateFilter } from './useAgentMapStateFilter' export type AgentDashboardView = 'map' | 'board' @@ -198,6 +199,7 @@ export function AgentKanbanBoard({ const [query, setQuery] = useState('') const searchInputRef = useRef(null) const [filters, setFilters] = useState(EMPTY_DASHBOARD_FILTERS) + const { agentStates, toggleAgentState, resetAgentStates } = useAgentMapStateFilter() const filteredCards = useMemo( () => filterDashboardCards(availableCards, query, filters), [availableCards, filters, query] @@ -368,6 +370,9 @@ export function AgentKanbanBoard({ onQueryChange={setQuery} filters={filters} onFiltersChange={setFilters} + agentStates={view === 'map' ? agentStates : undefined} + onAgentStateToggle={toggleAgentState} + onAgentStatesReset={resetAgentStates} searchInputRef={searchInputRef} /> {view !== 'board' ? ( @@ -389,6 +394,7 @@ export function AgentKanbanBoard({ } compact={dialogCard !== null} selectedPaneKey={dialogCard?.paneKey} + enabledStates={agentStates} launchableAgentsByWorktreeId={snapshot.launchableAgentsByWorktreeId} workspaceContextMenusEnabled={workspaceContextMenusEnabled} onWorkspaceContextMenuOpenChange={onWorkspaceContextMenuOpenChange} diff --git a/src/renderer/src/components/dashboard-popout/AgentMap.test.tsx b/src/renderer/src/components/dashboard-popout/AgentMap.test.tsx index eb2da8069e7..9c3605c69aa 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMap.test.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMap.test.tsx @@ -10,6 +10,7 @@ import type { } from '../../../../shared/dashboard-snapshot' import type { TuiAgent } from '../../../../shared/types' import { AgentMap } from './AgentMap' +import type { AgentMapState } from './agent-map-filter' import { AGENT_MAP_AGENT_RADIUS } from './agent-map-layout' const NOW = 2_000_000_000 @@ -46,6 +47,7 @@ function renderMap( selectedPaneKey = null, compact = false, workspaceContextMenusEnabled = false, + enabledStates, launchableAgentsByWorktreeId, onSpawnAgent, onSleepWorkspace @@ -54,6 +56,7 @@ function renderMap( selectedPaneKey?: string | null compact?: boolean workspaceContextMenusEnabled?: boolean + enabledStates?: ReadonlySet launchableAgentsByWorktreeId?: Record onSpawnAgent?: (args: DashboardSpawnAgentArgs) => void onSleepWorkspace?: (args: DashboardSleepWorkspaceArgs) => void @@ -67,6 +70,7 @@ function renderMap( selectedPaneKey={selectedPaneKey} compact={compact} workspaceContextMenusEnabled={workspaceContextMenusEnabled} + enabledStates={enabledStates} launchableAgentsByWorktreeId={launchableAgentsByWorktreeId} onSpawnAgent={onSpawnAgent} onSleepWorkspace={onSleepWorkspace} @@ -158,7 +162,8 @@ describe('AgentMap', () => { expect(ring).toHaveClass('is-open') expect(screen.queryByRole('button', { name: 'Focus ring' })).not.toBeInTheDocument() - expect(screen.getAllByText('Orca')).toHaveLength(2) + // The popover names the project; the map itself draws it uppercased. + expect(screen.getByText('Orca')).toBeInTheDocument() expect(screen.getByText('1 agent · 1 active · 0 done')).toBeInTheDocument() expect(screen.getByText('Agent alpha')).toBeInTheDocument() fireEvent.click(screen.getAllByRole('button', { name: /Agent alpha/ })[1]) @@ -325,12 +330,6 @@ describe('AgentMap', () => { expect(viewportCenter()[1]).toBeCloseTo(nodeCenter()[1]) }) - it('shows map filters at the pop-out default-width breakpoint', () => { - renderMap([card()]) - - expect(screen.getByRole('complementary', { name: 'Map filters' })).toHaveClass('md:flex') - }) - it('increases map label scale when users zoom out', () => { const { container } = renderMap([card()]) const labelGroup = container.querySelector('.agent-map-worktree-label')?.parentElement @@ -441,42 +440,50 @@ describe('AgentMap', () => { expect(screen.getByRole('button', { name: /Agent alpha/ })).toBeInTheDocument() expect(screen.getByRole('button', { name: /New result/ })).toHaveClass('fleet-status-done') expect(screen.getByRole('button', { name: /Seen result/ })).toHaveClass('fleet-status-idle') - expect(screen.getByRole('button', { name: /^Done/ })).toHaveTextContent('1') - expect(screen.getByRole('button', { name: /^Idle/ })).toHaveTextContent('1') }) - it('lets users hide states and projects independently', () => { - renderMap([ - card(), - card({ - paneKey: 'other', - conversationName: 'Other project', - repoId: 'repo-2', - repoName: 'Mobile' - }) - ]) + it('hides the states the toolbar filter has muted', () => { + const done = card({ + paneKey: 'done', + conversationName: 'Done agent', + unseen: true, + bucket: 'done', + dotState: 'done', + finishedAt: NOW - 60_000 + }) + renderMap([card(), done], { enabledStates: new Set(['done']) }) - fireEvent.click(screen.getByRole('button', { name: /^Working/ })) expect(screen.queryByRole('button', { name: /Agent alpha/ })).not.toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: /^Working/ })) - fireEvent.click(screen.getByRole('button', { name: /^Mobile/ })) - expect(screen.getByRole('button', { name: /Agent alpha/ })).toBeInTheDocument() - expect(screen.queryByRole('button', { name: /Other project/ })).not.toBeInTheDocument() + expect(screen.getByRole('button', { name: /Done agent/ })).toBeInTheDocument() }) it('preserves the viewport when filters temporarily empty the map', () => { - const { container } = renderMap([card()]) + const agent = card() + const all = new Set(['attention', 'working', 'done', 'idle']) + const view = renderMap([agent], { enabledStates: all }) fireEvent.click(screen.getByRole('button', { name: 'Zoom in' })) - const viewBox = container + const viewBox = view.container .querySelector('.agent-map-canvas > svg')! .getAttribute('viewBox') - fireEvent.click(screen.getByRole('button', { name: /^Working/ })) - expect(container.querySelector('.agent-map-canvas > svg')).not.toBeInTheDocument() - expect(screen.getByText('1 agent hidden')).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: /^Working/ })) + const onOpenTerminal = vi.fn() + view.rerender( + (['done'])} + /> + ) + expect(view.container.querySelector('.agent-map-canvas > svg')).not.toBeInTheDocument() + view.rerender( + + ) - expect(container.querySelector('.agent-map-canvas > svg')).toHaveAttribute('viewBox', viewBox) + expect(view.container.querySelector('.agent-map-canvas > svg')).toHaveAttribute( + 'viewBox', + viewBox + ) }) it('preserves the viewport through an empty source snapshot', () => { diff --git a/src/renderer/src/components/dashboard-popout/AgentMap.tsx b/src/renderer/src/components/dashboard-popout/AgentMap.tsx index cb5f2c2feae..6cbdf89c623 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMap.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMap.tsx @@ -11,9 +11,7 @@ import type { import type { RepoIcon } from '../../../../shared/repo-icon' import type { TuiAgent } from '../../../../shared/types' import { AgentMapCanvas, type AgentMapCanvasHandle } from './AgentMapCanvas' -import { AgentMapFilterRail } from './AgentMapFilterRail' import { - countAgentMapCards, filterAgentMapCards, type AgentMapState, type AgentMapHostFilter @@ -28,6 +26,9 @@ type AgentMapProps = { className?: string compact?: boolean selectedPaneKey?: string | null + /** Owned by the board so the shared toolbar filter can drive it. Defaults to + * every state, i.e. the map shows whatever it is handed. */ + enabledStates?: ReadonlySet launchableAgentsByWorktreeId?: Record workspaceContextMenusEnabled?: boolean onWorkspaceContextMenuOpenChange?: (open: boolean) => void @@ -37,7 +38,12 @@ type AgentMapProps = { } const HOST_FILTERS: AgentMapHostFilter[] = ['all', 'local', 'ssh', 'wsl', 'remote'] -const DEFAULT_STATES: AgentMapState[] = ['attention', 'working', 'done', 'idle'] +const ALL_AGENT_STATES: ReadonlySet = new Set([ + 'attention', + 'working', + 'done', + 'idle' +]) function hostFilterLabel(filter: AgentMapHostFilter): string { switch (filter) { @@ -61,6 +67,7 @@ export function AgentMap({ className, compact = false, selectedPaneKey = null, + enabledStates = ALL_AGENT_STATES, launchableAgentsByWorktreeId, workspaceContextMenusEnabled = false, onWorkspaceContextMenuOpenChange, @@ -71,10 +78,6 @@ export function AgentMap({ const canvasRef = useRef(null) const layoutCacheRef = useRef(null) const [hostFilter, setHostFilter] = useState('all') - const [enabledStates, setEnabledStates] = useState>( - () => new Set(DEFAULT_STATES) - ) - const [hiddenProjectIds, setHiddenProjectIds] = useState>(() => new Set()) const hostCounts = useMemo(() => { const counts: Record = { local: 0, @@ -87,28 +90,14 @@ export function AgentMap({ } return counts }, [cards]) - const projects = useMemo(() => { - const byId = new Map() - for (const card of cards) { - const current = byId.get(card.repoId) - if (current) { - current.count += 1 - } else { - byId.set(card.repoId, { id: card.repoId, name: card.repoName, count: 1 }) - } - } - return [...byId.values()].sort((a, b) => a.name.localeCompare(b.name)) - }, [cards]) - const counts = useMemo(() => countAgentMapCards(cards), [cards]) const visibleCards = useMemo( () => filterAgentMapCards({ cards, enabledStates, - hostFilter, - hiddenProjectIds + hostFilter }), - [cards, enabledStates, hiddenProjectIds, hostFilter] + [cards, enabledStates, hostFilter] ) const layoutResult = useMemo( () => updateAgentMapLayout(layoutCacheRef.current, visibleCards, now), @@ -119,50 +108,8 @@ export function AgentMap({ }, [layoutResult.cache]) const layout = layoutResult.layout - const toggleState = (state: AgentMapState): void => { - setEnabledStates((current) => { - const next = new Set(current) - if (next.has(state)) { - next.delete(state) - } else { - next.add(state) - } - return next - }) - } - const toggleProject = (projectId: string): void => { - setHiddenProjectIds((current) => { - const next = new Set(current) - if (next.has(projectId)) { - next.delete(projectId) - } else { - next.add(projectId) - } - return next - }) - } - const showAll = (): void => { - setEnabledStates(new Set(DEFAULT_STATES)) - setHiddenProjectIds(new Set()) - setHostFilter('all') - } - return (
- {!compact ? ( - canvasRef.current?.fit()} - onProjectToggle={toggleProject} - onShowAll={showAll} - onStateToggle={toggleState} - /> - ) : null}
diff --git a/src/renderer/src/components/dashboard-popout/AgentMapFilterRail.tsx b/src/renderer/src/components/dashboard-popout/AgentMapFilterRail.tsx deleted file mode 100644 index b229f30b977..00000000000 --- a/src/renderer/src/components/dashboard-popout/AgentMapFilterRail.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { Scan } from 'lucide-react' -import { AgentStateDot } from '@/components/AgentStateDot' -import { Button } from '@/components/ui/button' -import { translate } from '@/i18n/i18n' -import { cn } from '@/lib/utils' -import type { AgentMapCounts, AgentMapState } from './agent-map-filter' - -type AgentMapProjectOption = { - id: string - name: string - count: number -} - -type AgentMapFilterRailProps = { - counts: AgentMapCounts - enabledStates: ReadonlySet - hiddenProjectIds: ReadonlySet - projects: AgentMapProjectOption[] - totalCount: number - visibleCount: number - onFit: () => void - onProjectToggle: (projectId: string) => void - onShowAll: () => void - onStateToggle: (state: AgentMapState) => void -} - -const STATE_ROWS: { - state: AgentMapState - dotState: 'waiting' | 'working' | 'done' | 'idle' -}[] = [ - { state: 'attention', dotState: 'waiting' }, - { state: 'working', dotState: 'working' }, - { state: 'done', dotState: 'done' }, - { state: 'idle', dotState: 'idle' } -] - -function stateLabel(state: AgentMapState): string { - switch (state) { - case 'attention': - return translate('dashboardPopout.bucket.attention', 'Needs You') - case 'working': - return translate('dashboardPopout.bucket.working', 'Working') - case 'done': - return translate('dashboardPopout.bucket.done', 'Done') - case 'idle': - return translate('dashboardPopout.bucket.idle', 'Idle') - } -} - -export function AgentMapFilterRail({ - counts, - enabledStates, - hiddenProjectIds, - projects, - totalCount, - visibleCount, - onFit, - onProjectToggle, - onShowAll, - onStateToggle -}: AgentMapFilterRailProps): React.JSX.Element { - return ( - - ) -} diff --git a/src/renderer/src/components/dashboard-popout/agent-map-filter.test.ts b/src/renderer/src/components/dashboard-popout/agent-map-filter.test.ts index f9d081bfaf5..4269759c99c 100644 --- a/src/renderer/src/components/dashboard-popout/agent-map-filter.test.ts +++ b/src/renderer/src/components/dashboard-popout/agent-map-filter.test.ts @@ -34,13 +34,12 @@ describe('agent map filtering', () => { expect(agentMapState(card({ bucket: 'idle', dotState: 'idle' }))).toBe('idle') }) - it('applies state, host, and project filters independently', () => { + it('applies state and host filters independently', () => { const hidden = card({ paneKey: 'hidden', repoId: 'hidden', hostKind: 'ssh', unseen: true }) const visible = filterAgentMapCards({ cards: [hidden], enabledStates: new Set(['done']), - hostFilter: 'ssh', - hiddenProjectIds: new Set() + hostFilter: 'ssh' }) expect(visible).toEqual([hidden]) @@ -48,24 +47,14 @@ describe('agent map filtering', () => { filterAgentMapCards({ cards: [hidden], enabledStates: new Set(['idle']), - hostFilter: 'ssh', - hiddenProjectIds: new Set() + hostFilter: 'ssh' }) ).toEqual([]) expect( filterAgentMapCards({ cards: [hidden], enabledStates: new Set(['done']), - hostFilter: 'local', - hiddenProjectIds: new Set() - }) - ).toEqual([]) - expect( - filterAgentMapCards({ - cards: [hidden], - enabledStates: new Set(['done']), - hostFilter: 'ssh', - hiddenProjectIds: new Set(['hidden']) + hostFilter: 'local' }) ).toEqual([]) }) diff --git a/src/renderer/src/components/dashboard-popout/agent-map-filter.ts b/src/renderer/src/components/dashboard-popout/agent-map-filter.ts index ff8e01f7c0f..3a137ff93bd 100644 --- a/src/renderer/src/components/dashboard-popout/agent-map-filter.ts +++ b/src/renderer/src/components/dashboard-popout/agent-map-filter.ts @@ -19,23 +19,19 @@ export function agentMapState(card: DashboardCard): AgentMapState { export function filterAgentMapCards({ cards, enabledStates, - hostFilter, - hiddenProjectIds + hostFilter }: { cards: DashboardCard[] enabledStates: ReadonlySet hostFilter: AgentMapHostFilter - hiddenProjectIds: ReadonlySet }): DashboardCard[] { - return cards.filter((card) => { - if ( - (hostFilter !== 'all' && (card.hostKind ?? 'local') !== hostFilter) || - hiddenProjectIds.has(card.repoId) - ) { - return false - } - return enabledStates.has(agentMapState(card)) - }) + // Project filtering lives in the shared toolbar filter, which has already + // narrowed these cards. + return cards.filter( + (card) => + (hostFilter === 'all' || (card.hostKind ?? 'local') === hostFilter) && + enabledStates.has(agentMapState(card)) + ) } export function countAgentMapCards(cards: DashboardCard[]): AgentMapCounts { diff --git a/src/renderer/src/components/dashboard-popout/useAgentMapStateFilter.ts b/src/renderer/src/components/dashboard-popout/useAgentMapStateFilter.ts new file mode 100644 index 00000000000..b98bbf52afb --- /dev/null +++ b/src/renderer/src/components/dashboard-popout/useAgentMapStateFilter.ts @@ -0,0 +1,29 @@ +import { useCallback, useState } from 'react' +import type { AgentMapState } from './agent-map-filter' + +const ALL_AGENT_STATES: AgentMapState[] = ['attention', 'working', 'done', 'idle'] + +/** Map-only state muting. It lives on the board rather than inside the map so + * the shared toolbar filter — the map has no rail of its own — can drive it. */ +export function useAgentMapStateFilter(): { + agentStates: ReadonlySet + toggleAgentState: (state: AgentMapState) => void + resetAgentStates: () => void +} { + const [agentStates, setAgentStates] = useState>( + () => new Set(ALL_AGENT_STATES) + ) + const toggleAgentState = useCallback((state: AgentMapState): void => { + setAgentStates((current) => { + const next = new Set(current) + if (!next.delete(state)) { + next.add(state) + } + return next + }) + }, []) + const resetAgentStates = useCallback((): void => { + setAgentStates(new Set(ALL_AGENT_STATES)) + }, []) + return { agentStates, toggleAgentState, resetAgentStates } +} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 9b309945747..9521878f929 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -14789,13 +14789,7 @@ "remote": "Remote" }, "filters": { - "title": "Map filters", - "control": "Choose what stays visible", "showStates": "Agent states", - "hidden": "{{count}} agents hidden", - "hidden_one": "{{count}} agent hidden", - "hidden_other": "{{count}} agents hidden", - "showAll": "Show all", "canvasSummary": "{{shown}} of {{total}} agents shown" }, "liveContainmentMap": "Live containment map",