import { useState, useEffect, useRef } from 'react'; import { dp } from './dialogStyles'; import { useConnection, useSessions } from '@qwen-code/webui/daemon-react-sdk'; import { useI18n } from '../../i18n'; import { useListboxKeyboard } from '../../hooks/useListboxKeyboard'; import { useFilterInput } from '../../hooks/useFilterInput'; import { SessionRow } from './SessionRow'; interface ResumeDialogProps { onSelect: (sessionId: string) => void; onClose: () => void; } const LIST_ID = 'resume-session-list'; const optionId = (index: number) => `${LIST_ID}-opt-${index}`; export function ResumeDialog({ onSelect, onClose }: ResumeDialogProps) { const { t } = useI18n(); const connection = useConnection(); const { sessions, loading, error } = useSessions({ autoLoad: true }); const currentSessionId = connection.sessionId; // -1 = no highlight. The dialog opens with nothing highlighted and resets to // none on filter edits, so Enter in the search box cannot confirm a row the // user didn't pick — the highlight only appears once they press ↓/↑ or hover. const [selectedIdx, setSelectedIdx] = useState(-1); const { filterValue: filterQuery, inputProps } = useFilterInput(() => setSelectedIdx(-1), ); const listRef = useRef(null); const inputRef = useRef(null); const filtered = filterQuery ? sessions.filter((s) => { const q = filterQuery.toLowerCase(); return ( (s.displayName || '').toLowerCase().includes(q) || s.sessionId.toLowerCase().includes(q) ); }) : sessions; // Keep selection in bounds useEffect(() => { if (selectedIdx >= filtered.length && filtered.length > 0) { setSelectedIdx(filtered.length - 1); } }, [filtered.length, selectedIdx]); // Scroll selected item into view useEffect(() => { const el = listRef.current?.children[selectedIdx] as | HTMLElement | undefined; el?.scrollIntoView({ block: 'nearest' }); }, [selectedIdx]); const confirm = (index: number) => { const session = filtered[index]; if (!session) return; onSelect(session.sessionId); onClose(); }; const { keyboardMode } = useListboxKeyboard({ itemCount: filtered.length, activeIndex: selectedIdx, onActiveIndexChange: setSelectedIdx, onConfirm: confirm, }); useEffect(() => { inputRef.current?.focus(); }, []); return (
{t('resume.search')}:{' '} = 0 && selectedIdx < filtered.length ? optionId(selectedIdx) : undefined } {...inputProps} placeholder="" />
{loading && (
{t('common.loading')}
)} {!loading && error && (
{error.message || t('resume.failedToLoad')}
)} {!loading && !error && filtered.length === 0 && (
{filterQuery ? t('resume.noMatch', { query: filterQuery }) : t('resume.none')}
)} {!loading && filtered.map((s, index) => ( confirm(index)} onActivate={() => setSelectedIdx(index)} /> ))}
); }