From 544281bf549ec2fec5317ee2a849127db2f7e090 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:50:33 -0700 Subject: [PATCH] refactor(agent-map): fold the map's filter rail into the shared toolbar filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rail duplicated the toolbar's project filter and cost the canvas 14rem of width on the surface that needs it most. Agent states move into the toolbar's Filter dropdown (map view only — the board's columns already separate them) and count toward its badge; project filtering falls back to the toolbar's own. Show all is the dropdown's Clear all, and Fit already lives in the viewport controls. --- .../AgentDashboardToolbar.tsx | 61 ++++++- .../AgentKanbanBoard.test.tsx | 45 +++++- .../dashboard-popout/AgentKanbanBoard.tsx | 6 + .../dashboard-popout/AgentMap.test.tsx | 69 ++++---- .../components/dashboard-popout/AgentMap.tsx | 77 ++------- .../dashboard-popout/AgentMapFilterRail.tsx | 149 ------------------ .../dashboard-popout/agent-map-filter.test.ts | 19 +-- .../dashboard-popout/agent-map-filter.ts | 20 +-- .../useAgentMapStateFilter.ts | 29 ++++ src/renderer/src/i18n/locales/en.json | 6 - 10 files changed, 198 insertions(+), 283 deletions(-) delete mode 100644 src/renderer/src/components/dashboard-popout/AgentMapFilterRail.tsx create mode 100644 src/renderer/src/components/dashboard-popout/useAgentMapStateFilter.ts 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",