import { useEffect, useLayoutEffect, useId, useRef, useState, type ReactNode } from 'react'; import { ChevronLeft, ChevronRight, RefreshCw, Search, SlidersHorizontal, X } from 'lucide-react'; import { MessageNotice } from '../appNotice'; import { useDialogFocusTrap } from '../components/useDialogFocusTrap'; import { useI18n } from '../i18n'; const VISIBLE_CLIENTS_KEY = 'cpa-gui.agent-visible-clients.v1'; type Client = { id: Id; name: string; icon: ReactNode; summary: string; installed: boolean; detected: boolean; }; function readVisibleClients(clients: Client[]): Id[] | null { try { const value: unknown = JSON.parse(window.localStorage.getItem(VISIBLE_CLIENTS_KEY) ?? 'null'); if (!Array.isArray(value)) return null; const known = clients.filter((client) => value.includes(client.id)).map((client) => client.id); return known.length ? known : null; } catch { return null; } } export function AgentClientList({ clients, selected, onSelect, onRefresh, loading, busy, error, onDismissError, }: { clients: Client[]; selected: Id; onSelect: (id: Id) => void; onRefresh: () => void; loading: boolean; busy: boolean; error: string; onDismissError: () => void; }) { const { t } = useI18n(); const [visibleIds, setVisibleIds] = useState(() => readVisibleClients(clients)); const [managing, setManaging] = useState(false); const [capacity, setCapacity] = useState(1); const [page, setPage] = useState(0); const listRef = useRef(null); const close = () => setManaging(false); const [draftIds, setDraftIds] = useState(null); const [query, setQuery] = useState(''); // Stable sorting keeps the existing order within each installation group. const orderedClients = [...clients].sort((left, right) => Number(right.installed) - Number(left.installed)); // Keep the current client reachable even when detection fails or it was uninstalled. const automaticIds = orderedClients.filter((client) => client.detected || client.id === selected) .map((client) => client.id); const visibleClients = orderedClients.filter((client) => (visibleIds ?? automaticIds).includes(client.id)); const pageCount = Math.max(1, Math.ceil(visibleClients.length / capacity)); const currentPage = Math.min(page, pageCount - 1); const pageClients = visibleClients.slice(currentPage * capacity, (currentPage + 1) * capacity); const selectedIndex = visibleClients.findIndex((client) => client.id === selected); const draftSelection = draftIds ?? automaticIds; const searchRef = useRef(null); const titleId = useId(); const descriptionId = useId(); const dialogRef = useDialogFocusTrap({ active: managing, onEscape: close, initialFocusRef: searchRef, }); useLayoutEffect(() => { const list = listRef.current; if (!list) return; const resize = () => setCapacity(Math.max(1, Math.floor((list.clientHeight + 4) / 60))); resize(); const observer = new ResizeObserver(resize); observer.observe(list); return () => observer.disconnect(); }, []); // Navigation is a contiguous page of the saved list, never a replacement of its last row. // Opening a client or resizing reveals its page; browsing pages does not select a client. useLayoutEffect(() => { setPage(Math.floor(Math.max(0, selectedIndex) / capacity)); }, [capacity, selected, selectedIndex]); useEffect(() => { if (!busy && visibleIds && !visibleIds.includes(selected) && visibleIds.length) { onSelect(visibleIds[0]); } }, [busy, onSelect, selected, visibleIds]); useEffect(() => { if (managing) dialogRef.current?.showModal(); }, [dialogRef, managing]); const save = () => { if (!draftSelection.length) return; try { if (draftIds === null) window.localStorage.removeItem(VISIBLE_CLIENTS_KEY); else window.localStorage.setItem(VISIBLE_CLIENTS_KEY, JSON.stringify(draftIds)); } catch { // Still allow a session-only preference when browser storage is unavailable. } setVisibleIds(draftIds); if (!draftSelection.includes(selected)) { const first = orderedClients.find((client) => draftSelection.includes(client.id)); if (first) onSelect(first.id); } setManaging(false); }; const matches = orderedClients.filter((client) => client.name.toLowerCase().includes(query.trim().toLowerCase())); return <>