import React from 'react'; import { RiGitBranchLine, RiArrowDownSLine, RiCheckLine, RiMore2Line, RiFileCopyLine, RiLoader4Line, } from '@remixicon/react'; import { toast } from '@/components/ui'; import { Button } from '@/components/ui/button'; import { copyTextToClipboard } from '@/lib/clipboard'; import { cn } from '@/lib/utils'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { useAgentGroupsStore, type AgentGroup, type AgentGroupSession } from '@/stores/useAgentGroupsStore'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useGlobalSessionStatus, useAllSessionStatuses } from '@/sync/sync-context'; import { ChatContainer } from '@/components/chat/ChatContainer'; import { ChatErrorBoundary } from '@/components/chat/ChatErrorBoundary'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; interface AgentGroupDetailProps { group: AgentGroup; className?: string; } const SessionStatusDot: React.FC<{ sessionId: string }> = ({ sessionId }) => { const status = useGlobalSessionStatus(sessionId); if (!status || status.type === 'idle') return null; return ( ); }; export const AgentGroupDetail: React.FC = ({ group, className, }) => { const selectedSessionId = useAgentGroupsStore((s) => s.selectedSessionId); const selectSession = useAgentGroupsStore((s) => s.selectSession); const deleteGroupSessions = useAgentGroupsStore((s) => s.deleteGroupSessions); const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession); const currentSessionId = useSessionUIStore((s) => s.currentSessionId); const [worktreeDialog, setWorktreeDialog] = React.useState(null); const [isProcessing, setIsProcessing] = React.useState(false); const selectedSession = React.useMemo(() => { if (!selectedSessionId) return group.sessions[0] ?? null; return group.sessions.find((s) => s.id === selectedSessionId) ?? group.sessions[0] ?? null; }, [group.sessions, selectedSessionId]); const handleSessionSelect = React.useCallback((session: AgentGroupSession) => { selectSession(session.id); setCurrentSession(session.id, session.path); }, [selectSession, setCurrentSession]); // Auto-select first session when group changes and sync OpenCode session React.useEffect(() => { if (group.sessions.length > 0) { const session = selectedSessionId ? group.sessions.find((s) => s.id === selectedSessionId) ?? group.sessions[0] : group.sessions[0]; if (session) { if (session.id !== currentSessionId) { setCurrentSession(session.id, session.path); } if (!selectedSessionId) { selectSession(session.id); } } } }, [group.name, group.sessions, selectedSessionId, currentSessionId, selectSession, setCurrentSession]); const isSessionSynced = selectedSession?.id === currentSessionId; const handleCopyWorktreePath = React.useCallback(() => { if (!selectedSession?.path) { toast.error('No worktree path available'); return; } void copyTextToClipboard(selectedSession.path).then((result) => { if (result.ok) { toast.success('Worktree path copied'); return; } toast.error('Failed to copy path'); }); }, [selectedSession?.path]); const handleRemoveSelectedWorktree = React.useCallback(() => { if (!selectedSession) return; setWorktreeDialog({ kind: 'remove', path: selectedSession.path, label: selectedSession.displayLabel }); }, [selectedSession]); const handleKeepOnlySelectedWorktree = React.useCallback(() => { if (!selectedSession) return; setWorktreeDialog({ kind: 'keepOnly', path: selectedSession.path, label: selectedSession.displayLabel }); }, [selectedSession]); const handleConfirmWorktreeAction = React.useCallback(async () => { if (!worktreeDialog || isProcessing) return; setIsProcessing(true); try { const normalize = (v: string) => v.replace(/\\/g, '/').replace(/\/+$/, '') || v; const targetPath = normalize(worktreeDialog.path); let sessionsToDelete: AgentGroupSession[]; if (worktreeDialog.kind === 'remove') { toast.info('Removing worktree...'); sessionsToDelete = group.sessions.filter((s) => normalize(s.path) === targetPath); } else { toast.info('Removing other worktrees...'); sessionsToDelete = group.sessions.filter((s) => normalize(s.path) !== targetPath); } const { failedIds, failedWorktreePaths } = await deleteGroupSessions(sessionsToDelete, { removeWorktrees: true }); if (failedIds.length > 0 || failedWorktreePaths.length > 0) { toast.error('Failed to fully remove worktree'); } else { toast.success(worktreeDialog.kind === 'remove' ? 'Worktree removed' : 'Removed other worktrees'); } setWorktreeDialog(null); } finally { setIsProcessing(false); } }, [deleteGroupSessions, group.sessions, isProcessing, worktreeDialog]); // Group-level status: show if any session is busy const allStatuses = useAllSessionStatuses(); const groupBusy = React.useMemo( () => group.sessions.some((s) => allStatuses[s.id]?.type === 'busy'), [group.sessions, allStatuses], ); return ( {/* Header */} {group.name} {groupBusy && } {group.sessionCount} model{group.sessionCount !== 1 ? 's' : ''} · {selectedSession?.worktreeMetadata?.label || selectedSession?.branch || 'No branch'} {/* Model Selector Dropdown */} {group.sessions.length > 0 && ( {selectedSession && ( <> {selectedSession.modelId} {selectedSession.instanceNumber > 1 && ( #{selectedSession.instanceNumber} )} > )} {group.sessions.map((session) => ( handleSessionSelect(session)} className="flex items-center gap-2 py-2" > {session.modelId} {session.instanceNumber > 1 && ( #{session.instanceNumber} )} {session.branch && ( {session.worktreeMetadata?.label || session.branch} )} {selectedSession?.id === session.id && ( )} ))} { e.preventDefault(); handleRemoveSelectedWorktree(); }} variant="destructive" > Remove this worktree { e.preventDefault(); handleKeepOnlySelectedWorktree(); }} > Leave this one, remove others { e.stopPropagation(); handleCopyWorktreePath(); }} disabled={!selectedSession?.path} > Copy Worktree Path )} { if (!open) setWorktreeDialog(null); }}> {worktreeDialog?.kind === 'remove' ? 'Remove worktree' : 'Remove other worktrees'} {worktreeDialog?.kind === 'remove' ? <>Remove {worktreeDialog?.label}? This deletes all sessions in that worktree and removes the worktree itself.> : <>Keep {worktreeDialog?.label} and remove the other worktrees in {group.name}.>} setWorktreeDialog(null)} disabled={isProcessing}> Cancel void handleConfirmWorktreeAction()} disabled={isProcessing} > {isProcessing ? 'Working…' : worktreeDialog?.kind === 'remove' ? 'Remove' : 'Remove others'} {/* Chat Content */} {selectedSession ? ( isSessionSynced ? ( ) : ( {selectedSession.displayLabel} · {selectedSession.path} Loading session for {selectedSession.displayLabel} Session ID: {selectedSession.id} ) ) : ( No sessions in this group )} ); };
Loading session for {selectedSession.displayLabel}
Session ID: {selectedSession.id}
No sessions in this group