import { useEffect, useMemo, useState } from 'react'; // import { MemberWorkSyncStatusPanel } from '@features/member-work-sync/renderer'; import { Button } from '@renderer/components/ui/button'; import { Dialog, DialogContent, DialogFooter, DialogHeader } from '@renderer/components/ui/dialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'; import { useMemberStats } from '@renderer/hooks/useMemberStats'; import { useStore } from '@renderer/store'; import { selectMemberMessagesForTeamMember } from '@renderer/store/slices/teamSlice'; import { isOpenCodeRelaunchActionable } from '@renderer/utils/memberHelpers'; import { buildMemberLaunchDiagnosticsPayload, getMemberLaunchDiagnosticsErrorMessage, hasMemberLaunchDiagnosticsDetails, hasMemberLaunchDiagnosticsError, } from '@renderer/utils/memberLaunchDiagnostics'; import { getRuntimeMemorySourceLabel, resolveMemberRuntimeSummary, } from '@renderer/utils/memberRuntimeSummary'; import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState'; import { isLeadMember } from '@shared/utils/leadDetection'; import { isTeamTaskFinishedForDependency } from '@shared/utils/teamTaskState'; import { BarChart3, FileText, ListPlus, Loader2, MessageSquare, RotateCcw, UserMinus, } from 'lucide-react'; import { buildMemberActivityEntries } from './memberActivityEntries'; import { MemberDetailHeader } from './MemberDetailHeader'; import { MemberDetailStats } from './MemberDetailStats'; import { type MemberActivityFilter, type MemberDetailTab } from './memberDetailTypes'; import { MemberLaunchDiagnosticsButton } from './MemberLaunchDiagnosticsButton'; import { MemberLogsTab } from './MemberLogsTab'; import { MemberMessagesTab } from './MemberMessagesTab'; import { MemberStatsTab } from './MemberStatsTab'; import { MemberTasksTab } from './MemberTasksTab'; import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice'; import type { LeadActivityState, MemberSpawnStatusEntry, ResolvedTeamMember, TeamAgentRuntimeEntry, TeamTaskWithKanban, } from '@shared/types'; const OPENCODE_NO_RUNTIME_EVIDENCE_MESSAGE = 'No OpenCode runtime session was recorded. Relaunch this teammate to start a fresh OpenCode session.'; const OPENCODE_BOOTSTRAP_STALLED_MESSAGE = 'OpenCode process is alive, but bootstrap did not confirm. Relaunch this teammate to start a fresh OpenCode session.'; function hasOpenCodeRuntimeEvidence(runtimeEntry: TeamAgentRuntimeEntry | undefined): boolean { const hasPid = typeof runtimeEntry?.pid === 'number' && Number.isFinite(runtimeEntry.pid) && runtimeEntry.pid > 0; const hasRuntimePid = typeof runtimeEntry?.runtimePid === 'number' && Number.isFinite(runtimeEntry.runtimePid) && runtimeEntry.runtimePid > 0; const hasRuntimeSessionId = typeof runtimeEntry?.runtimeSessionId === 'string' && runtimeEntry.runtimeSessionId.trim().length > 0; const hasRuntimeLiveness = runtimeEntry?.livenessKind === 'runtime_process' || runtimeEntry?.livenessKind === 'runtime_process_candidate' || runtimeEntry?.livenessKind === 'permission_blocked'; return Boolean(hasPid || hasRuntimePid || hasRuntimeSessionId || hasRuntimeLiveness); } function isOpenCodeNoRuntimeEvidenceFailure( member: ResolvedTeamMember, spawnEntry: MemberSpawnStatusEntry | undefined, runtimeEntry: TeamAgentRuntimeEntry | undefined ): boolean { const failed = spawnEntry?.launchState === 'failed_to_start' || spawnEntry?.status === 'error'; return member.providerId === 'opencode' && failed && !hasOpenCodeRuntimeEvidence(runtimeEntry); } interface MemberDetailDialogProps { open: boolean; member: ResolvedTeamMember | null; teamName: string; members: ResolvedTeamMember[]; tasks: TeamTaskWithKanban[]; initialTab?: MemberDetailTab; initialActivityFilter?: MemberActivityFilter; isTeamAlive?: boolean; isTeamProvisioning?: boolean; isLaunchSettling?: boolean; leadActivity?: LeadActivityState; spawnEntry?: MemberSpawnStatusEntry; runtimeEntry?: TeamAgentRuntimeEntry; runtimeRunId?: string | null; launchParams?: TeamLaunchParams; onClose: () => void; onSendMessage: () => void; onAssignTask: () => void; onTaskClick: (task: TeamTaskWithKanban) => void; onRemoveMember?: () => void; onRestartMember?: (memberName: string) => Promise | void; onUpdateRole?: (memberName: string, role: string | undefined) => Promise | void; updatingRole?: boolean; onViewMemberChanges?: (memberName: string, filePath?: string) => void; } export const MemberDetailDialog = ({ open, member, teamName, members, tasks, initialTab = 'tasks', initialActivityFilter = 'all', isTeamAlive, isTeamProvisioning, isLaunchSettling, leadActivity, spawnEntry, runtimeEntry, runtimeRunId, launchParams, onClose, onSendMessage, onAssignTask, onTaskClick, onRemoveMember, onRestartMember, onUpdateRole, updatingRole, onViewMemberChanges, }: MemberDetailDialogProps): React.JSX.Element | null => { const memberTasks = useMemo( () => (member ? tasks.filter((t) => t.owner === member.name) : []), [tasks, member] ); const memberMessages = useStore((state) => selectMemberMessagesForTeamMember(state, teamName, member?.name ?? null) ); const memberActivityCount = useMemo(() => { if (!member) { return 0; } return buildMemberActivityEntries({ teamName, memberName: member.name, members, tasks, messages: memberMessages, }).length; }, [member, memberMessages, members, tasks, teamName]); const inProgressTasks = useMemo( () => memberTasks.filter(isDisplayableCurrentTask).length, [memberTasks] ); const currentTaskCandidate = useMemo( () => member?.currentTaskId ? (tasks.find((task) => task.id === member.currentTaskId) ?? null) : null, [member?.currentTaskId, tasks] ); const displayableCurrentTask = isDisplayableCurrentTask(currentTaskCandidate) ? currentTaskCandidate : null; const completedTasks = useMemo( () => memberTasks.filter(isTeamTaskFinishedForDependency).length, [memberTasks] ); const [activeTab, setActiveTab] = useState(initialTab); const [restarting, setRestarting] = useState(false); const [restartError, setRestartError] = useState(null); const runtimeSummary = useMemo( () => member ? resolveMemberRuntimeSummary(member, launchParams, spawnEntry, runtimeEntry) : undefined, [launchParams, member, runtimeEntry, spawnEntry] ); const memorySourceLabel = getRuntimeMemorySourceLabel(runtimeEntry); const openCodeRelaunchActionable = member ? isOpenCodeRelaunchActionable({ member, spawnEntry, runtimeEntry }) : false; const restartInFlight = !openCodeRelaunchActionable && (spawnEntry?.launchState === 'starting' || spawnEntry?.launchState === 'runtime_pending_bootstrap' || spawnEntry?.launchState === 'runtime_pending_permission'); const launchDiagnosticsPayload = useMemo( () => member ? buildMemberLaunchDiagnosticsPayload({ teamName, runId: runtimeRunId, memberName: member.name, spawnEntry, runtimeEntry, }) : null, [member, runtimeEntry, runtimeRunId, spawnEntry, teamName] ); const showCopyDiagnostics = launchDiagnosticsPayload != null && hasMemberLaunchDiagnosticsError(launchDiagnosticsPayload) && hasMemberLaunchDiagnosticsDetails(launchDiagnosticsPayload); const launchErrorMessage = launchDiagnosticsPayload ? getMemberLaunchDiagnosticsErrorMessage(launchDiagnosticsPayload) : undefined; const openCodeBootstrapStalled = member?.providerId === 'opencode' && spawnEntry?.bootstrapStalled === true; const openCodeNoRuntimeEvidence = member ? isOpenCodeNoRuntimeEvidenceFailure(member, spawnEntry, runtimeEntry) : false; const effectiveLaunchErrorMessage = openCodeNoRuntimeEvidence ? OPENCODE_NO_RUNTIME_EVIDENCE_MESSAGE : launchErrorMessage; const effectiveLaunchInfoMessage = openCodeBootstrapStalled ? OPENCODE_BOOTSTRAP_STALLED_MESSAGE : undefined; const isOpenCodeMember = member?.providerId === 'opencode'; const restartButtonLabel = isOpenCodeMember ? 'Relaunch OpenCode' : 'Restart'; const hasLiveRestartContext = isTeamAlive === true || isTeamProvisioning === true; const canControlledOpenCodeRelaunch = member == null ? false : isOpenCodeMember && !member.removedAt && !isLeadMember(member) && hasLiveRestartContext; const canRestartFromDialog = member == null ? false : Boolean(onRestartMember) && !isLeadMember(member) && hasLiveRestartContext && (runtimeEntry?.restartable !== false || canControlledOpenCodeRelaunch); useEffect(() => { if (!open || !member) { return; } setActiveTab(initialTab); setRestartError(null); setRestarting(false); }, [initialTab, member, open]); const { stats: memberStats, loading: statsLoading, error: statsError, } = useMemberStats(teamName, member?.name ?? null); const totalTokens = memberStats ? memberStats.inputTokens + memberStats.outputTokens : null; if (!member) return null; return ( !nextOpen && onClose()}>
onUpdateRole(member.name, newRole) : undefined } updatingRole={updatingRole} />
setActiveTab(v as MemberDetailTab)} className="min-w-0 overflow-hidden" > Tasks {memberTasks.length > 0 && ( {memberTasks.length} )} Activity {memberActivityCount > 0 && ( {memberActivityCount} )} Stats Logs
{/* */}
onViewMemberChanges?.(member.name, filePath)} onShowAllFiles={() => onViewMemberChanges?.(member.name)} />
{restartError ? (
{restartError}
) : effectiveLaunchErrorMessage ? (
{effectiveLaunchErrorMessage} {launchDiagnosticsPayload && showCopyDiagnostics ? ( ) : null}
) : effectiveLaunchInfoMessage ? (
{effectiveLaunchInfoMessage}
) : runtimeEntry?.pid ? (
PID {runtimeEntry.pid} {memorySourceLabel ? ` ยท ${memorySourceLabel}` : ''}
) : (
)} {member.removedAt ? ( Removed {new Date(member.removedAt).toLocaleDateString()} ) : ( <> {canRestartFromDialog && ( )} {onRemoveMember && !isLeadMember(member) && ( )} )}
); };