import { useEffect, useMemo, useRef, useState } from 'react' import type { CodexLocalSessionSummary } from '@/types/api' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { useTranslation } from '@/lib/use-translation' const ALL_WORKDIR_FILTER = '__all__' function formatCodexSessionTime(value: number): string | null { if (!Number.isFinite(value)) return null return new Date(value).toLocaleString() } function getCodexSessionPreview(session: CodexLocalSessionSummary): string { if (session.lastUserMessage?.trim()) { return session.lastUserMessage.trim() } const parts = [session.originator, session.cliVersion].filter(Boolean) return parts.join(' · ') } function getCodexSessionCwd(session: CodexLocalSessionSummary): string | null { const cwd = session.cwd?.trim() return cwd ? cwd : null } export function CodexSessionSyncDialog(props: { isOpen: boolean onClose: () => void sessions: CodexLocalSessionSummary[] currentCodexSessionId: string | null onConfirm: (sessionIds: string[]) => Promise onRestartCodexDesktop: () => Promise isPending: boolean isRestartingCodexDesktop: boolean isLoading: boolean }) { const { t } = useTranslation() const { isOpen, sessions, currentCodexSessionId, onConfirm, onRestartCodexDesktop, isPending, isRestartingCodexDesktop, isLoading, onClose } = props const [selectedSessionIds, setSelectedSessionIds] = useState([]) const [hasInitializedSelection, setHasInitializedSelection] = useState(false) const [workdirFilter, setWorkdirFilter] = useState(ALL_WORKDIR_FILTER) const wasOpenRef = useRef(false) const sessionIdSet = useMemo( () => new Set(sessions.map((session) => session.id)), [sessions] ) const selectedSessionIdSet = useMemo( () => new Set(selectedSessionIds), [selectedSessionIds] ) const workdirOptions = useMemo(() => { const directories = new Set() for (const session of sessions) { const cwd = getCodexSessionCwd(session) if (cwd) directories.add(cwd) } return Array.from(directories).sort((a, b) => a.localeCompare(b)) }, [sessions]) const filteredSessions = useMemo(() => { if (workdirFilter === ALL_WORKDIR_FILTER) return sessions return sessions.filter((session) => getCodexSessionCwd(session) === workdirFilter) }, [sessions, workdirFilter]) useEffect(() => { if (isOpen && !wasOpenRef.current) { wasOpenRef.current = true setSelectedSessionIds([]) setHasInitializedSelection(false) setWorkdirFilter(ALL_WORKDIR_FILTER) return } if (!isOpen && wasOpenRef.current) { wasOpenRef.current = false setSelectedSessionIds([]) setHasInitializedSelection(false) setWorkdirFilter(ALL_WORKDIR_FILTER) } }, [isOpen]) useEffect(() => { if (workdirFilter === ALL_WORKDIR_FILTER) return if (workdirOptions.includes(workdirFilter)) return setWorkdirFilter(ALL_WORKDIR_FILTER) }, [workdirFilter, workdirOptions]) useEffect(() => { if (!isOpen || isLoading || hasInitializedSelection) return // 中文注释:弹窗打开后等本地 Codex 会话列表加载完成,再尝试默认勾选当前 Hapi 会话关联的 Codex thread,避免异步加载时默认值丢失。 const defaultSelected = currentCodexSessionId && sessionIdSet.has(currentCodexSessionId) ? [currentCodexSessionId] : [] setSelectedSessionIds(defaultSelected) setHasInitializedSelection(true) }, [currentCodexSessionId, hasInitializedSelection, isLoading, isOpen, sessionIdSet]) const toggleSession = (sessionId: string) => { if (isPending || isLoading) return // 中文注释:列表项支持多选导入;再次点击同一行则取消勾选,便于快速调整导入批次。 setSelectedSessionIds((current) => current.includes(sessionId) ? current.filter((id) => id !== sessionId) : [...current, sessionId]) } const selectAll = () => { setSelectedSessionIds(filteredSessions.map((session) => session.id)) } const clearAll = () => { // 中文注释:全取消放在左侧,和底部“取消 / 导入”的左右语义保持一致。 setSelectedSessionIds([]) } const handleConfirm = async () => { if (selectedSessionIds.length === 0 || isPending || isLoading) return // 中文注释:确认按钮只提交用户在弹窗中勾选的 Codex thread,实际导入逻辑由父组件统一处理并给出 toast 提示。 await onConfirm(selectedSessionIds) } return ( !open && onClose()}>
{t('codexSync.confirm.title')} {t('codexSync.confirm.description')}
{t('codexSync.confirm.selectedCount', { n: selectedSessionIds.length })}
{sessions.length > 0 ? ( ) : null}
{isLoading ? (
{t('codexSync.confirm.loading')}
) : sessions.length === 0 ? (
{t('codexSync.confirm.empty')}
) : filteredSessions.length === 0 ? (
{t('codexSync.confirm.emptyForWorkdir')}
) : (
{filteredSessions.map((session) => { const checked = selectedSessionIdSet.has(session.id) const time = formatCodexSessionTime(session.modifiedAt) const preview = getCodexSessionPreview(session) const cwd = getCodexSessionCwd(session) return ( ) })}
)}
) }