import { useCallback, useEffect, useMemo, useState } from 'react'; import { api } from '@renderer/api'; import { Button } from '@renderer/components/ui/button'; import { getTeamColorSet } from '@renderer/constants/teamColors'; import { cn } from '@renderer/lib/utils'; import { useStore } from '@renderer/store'; import { MessageSquare, Pencil, Play, Plus, Search, Trash2, X } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { ActivityTimeline } from './activity/ActivityTimeline'; import { CreateTaskDialog } from './dialogs/CreateTaskDialog'; import { EditTeamDialog } from './dialogs/EditTeamDialog'; import { LaunchTeamDialog } from './dialogs/LaunchTeamDialog'; import { ReviewDialog } from './dialogs/ReviewDialog'; import { SendMessageDialog } from './dialogs/SendMessageDialog'; import { TaskDetailDialog } from './dialogs/TaskDetailDialog'; import { KanbanBoard } from './kanban/KanbanBoard'; import { UNASSIGNED_OWNER } from './kanban/KanbanFilterPopover'; import { MemberDetailDialog } from './members/MemberDetailDialog'; import { MemberList } from './members/MemberList'; import { CollapsibleTeamSection } from './CollapsibleTeamSection'; import { TeamProvisioningBanner } from './TeamProvisioningBanner'; import { TeamSessionsSection } from './TeamSessionsSection'; import type { KanbanFilterState } from './kanban/KanbanFilterPopover'; import type { Session } from '@renderer/types/data'; import type { ResolvedTeamMember, TeamTask } from '@shared/types'; interface TeamDetailViewProps { teamName: string; } interface CreateTaskDialogState { open: boolean; defaultSubject: string; defaultDescription: string; defaultOwner: string; } interface TimeWindow { start: number; end: number; } function filterKanbanTasks(tasks: TeamTask[], query: string): TeamTask[] { if (query.startsWith('#')) { const id = query.slice(1); return tasks.filter((t) => t.id === id); } const lower = query.toLowerCase(); return tasks.filter( (t) => t.id.toLowerCase().includes(lower) || t.subject.toLowerCase().includes(lower) || (t.owner?.toLowerCase().includes(lower) ?? false) ); } export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Element => { const [requestChangesTaskId, setRequestChangesTaskId] = useState(null); const [selectedTask, setSelectedTask] = useState(null); const [selectedMember, setSelectedMember] = useState(null); const [createTaskDialog, setCreateTaskDialog] = useState({ open: false, defaultSubject: '', defaultDescription: '', defaultOwner: '', }); const [creatingTask, setCreatingTask] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false); const [launchDialogOpen, setLaunchDialogOpen] = useState(false); const [sendDialogOpen, setSendDialogOpen] = useState(false); const [sendDialogRecipient, setSendDialogRecipient] = useState(undefined); // Session loading and filtering state const [sessions, setSessions] = useState([]); const [sessionsLoading, setSessionsLoading] = useState(false); const [sessionsError, setSessionsError] = useState(null); const [kanbanFilter, setKanbanFilter] = useState({ sessionId: null, selectedOwners: new Set(), }); const { data, loading, error, projects, selectTeam, updateKanban, updateTaskStatus, sendTeamMessage, requestReview, createTeamTask, startTask, deleteTeam, openTeamsTab, sendingMessage, sendMessageError, lastSendMessageResult, reviewActionError, launchTeam, provisioningError, kanbanFilterQuery, clearKanbanFilter, } = useStore( useShallow((s) => ({ data: s.selectedTeamData, loading: s.selectedTeamLoading, error: s.selectedTeamError, projects: s.projects, selectTeam: s.selectTeam, updateKanban: s.updateKanban, updateTaskStatus: s.updateTaskStatus, sendTeamMessage: s.sendTeamMessage, requestReview: s.requestReview, createTeamTask: s.createTeamTask, startTask: s.startTask, deleteTeam: s.deleteTeam, openTeamsTab: s.openTeamsTab, sendingMessage: s.sendingMessage, sendMessageError: s.sendMessageError, lastSendMessageResult: s.lastSendMessageResult, reviewActionError: s.reviewActionError, launchTeam: s.launchTeam, provisioningError: s.provisioningError, kanbanFilterQuery: s.kanbanFilterQuery, clearKanbanFilter: s.clearKanbanFilter, })) ); const [kanbanSearch, setKanbanSearch] = useState(''); useEffect(() => { if (!teamName) { return; } void selectTeam(teamName); }, [teamName, selectTeam]); useEffect(() => { if (kanbanFilterQuery) { setKanbanSearch(kanbanFilterQuery); clearKanbanFilter(); } }, [kanbanFilterQuery, clearKanbanFilter]); // Load sessions for the team's project const projectId = useMemo(() => { if (!data?.config.projectPath) return null; return projects.find((p) => p.path === data.config.projectPath)?.id ?? null; }, [projects, data?.config.projectPath]); useEffect(() => { if (!projectId) return; let cancelled = false; setSessionsLoading(true); setSessionsError(null); void (async () => { try { const result = await api.getSessions(projectId); if (!cancelled) { setSessions(result); } } catch (e) { if (!cancelled) { setSessionsError(e instanceof Error ? e.message : 'Failed to load sessions'); } } finally { if (!cancelled) { setSessionsLoading(false); } } })(); return () => { cancelled = true; }; }, [projectId]); // Filter sessions to team-only using sessionHistory + leadSessionId const teamSessions = useMemo(() => { const sessionIds = new Set(); if (data?.config.leadSessionId) { sessionIds.add(data.config.leadSessionId); } if (data?.config.sessionHistory) { for (const id of data.config.sessionHistory) { sessionIds.add(id); } } // If no session IDs known (backward compat), show all sessions if (sessionIds.size === 0) return sessions; return sessions.filter((s) => sessionIds.has(s.id)); }, [sessions, data?.config.leadSessionId, data?.config.sessionHistory]); // Auto-reset session filter if the selected session is no longer in teamSessions useEffect(() => { if ( kanbanFilter.sessionId !== null && !teamSessions.some((s) => s.id === kanbanFilter.sessionId) ) { setKanbanFilter((prev) => ({ ...prev, sessionId: null })); } }, [kanbanFilter.sessionId, teamSessions]); // Compute time-window for session filtering const timeWindow = useMemo(() => { if (kanbanFilter.sessionId === null) return null; const sorted = [...teamSessions].sort((a, b) => a.createdAt - b.createdAt); const idx = sorted.findIndex((s) => s.id === kanbanFilter.sessionId); if (idx === -1) return null; const start = sorted[idx].createdAt; const end = idx + 1 < sorted.length ? sorted[idx + 1].createdAt : Infinity; return { start, end }; }, [kanbanFilter.sessionId, teamSessions]); // Filter tasks by time-window and owner const filteredTasks = useMemo(() => { if (!data) return []; let result = data.tasks; // Session time-window filter if (timeWindow) { result = result.filter((t) => { if (!t.createdAt) return true; // legacy tasks always included const ts = new Date(t.createdAt).getTime(); return ts >= timeWindow.start && ts < timeWindow.end; }); } // Owner filter if (kanbanFilter.selectedOwners.size > 0) { result = result.filter((t) => t.owner ? kanbanFilter.selectedOwners.has(t.owner) : kanbanFilter.selectedOwners.has(UNASSIGNED_OWNER) ); } return result; }, [data, timeWindow, kanbanFilter.selectedOwners]); const filteredMessages = useMemo(() => { if (!data) return []; if (!timeWindow) return data.messages; return data.messages.filter((m) => { const ts = new Date(m.timestamp).getTime(); return ts >= timeWindow.start && ts < timeWindow.end; }); }, [data, timeWindow]); const kanbanDisplayTasks = useMemo(() => { const query = kanbanSearch.trim(); if (!query) return filteredTasks; return filterKanbanTasks(filteredTasks, query); }, [filteredTasks, kanbanSearch]); const taskMap = useMemo(() => new Map((data?.tasks ?? []).map((t) => [t.id, t])), [data?.tasks]); const openCreateTaskDialog = (subject = '', description = '', owner = ''): void => { setCreateTaskDialog({ open: true, defaultSubject: subject, defaultDescription: description, defaultOwner: owner, }); }; const closeCreateTaskDialog = (): void => { setCreateTaskDialog({ open: false, defaultSubject: '', defaultDescription: '', defaultOwner: '', }); }; const handleDeleteTeam = useCallback((): void => { const confirmed = window.confirm( `Delete team "${teamName}"? This action is irreversible. All team data and tasks will be deleted.` ); if (!confirmed) { return; } void (async () => { try { await deleteTeam(teamName); openTeamsTab(); } catch { // error is shown via store } })(); }, [teamName, deleteTeam, openTeamsTab]); const handleCreateTask = ( subject: string, description: string, owner?: string, blockedBy?: string[], prompt?: string, startImmediately?: boolean ): void => { setCreatingTask(true); void (async () => { try { await createTeamTask(teamName, { subject, description: description || undefined, owner, blockedBy, prompt, startImmediately, }); if (prompt && owner && data?.isAlive && startImmediately !== false) { const msg = `New task assigned to ${owner}: "${subject}". Instructions:\n${prompt}`; try { await api.teams.processSend(teamName, msg); } catch { // best-effort } } closeCreateTaskDialog(); } catch { // error shown via store } finally { setCreatingTask(false); } })(); }; if (!teamName) { return (
Invalid team tab
); } if (loading && !data) { return (
); } if (error) { return (

Failed to load team

{error}

); } if (!data) { return (
No team data available
); } const headerColorSet = data.config.color ? getTeamColorSet(data.config.color) : null; return (
{headerColorSet ? (
) : null}

{data.config.name}

{data.config.description && (

{data.config.description}

)}
{!data.isAlive ? ( ) : null}
{data.warnings?.some((warning) => warning.toLowerCase().includes('kanban')) ? (
Failed to fully load kanban. Displaying safe data.
) : null} {reviewActionError ? (
{reviewActionError}
) : null} { setSendDialogRecipient(member.name); setSendDialogOpen(true); }} onAssignTask={(member) => { openCreateTaskDialog('', '', member.name); }} /> setKanbanFilter((prev) => ({ ...prev, sessionId: id }))} projectPath={data.config.projectPath} /> 0} action={ } >
setKanbanSearch(e.target.value)} className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-8 text-xs text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-border-emphasis)] focus:outline-none" /> {kanbanSearch && ( )}
{ void requestReview(teamName, taskId); }} onApprove={(taskId) => { void updateKanban(teamName, taskId, { op: 'set_column', column: 'approved' }); }} onRequestChanges={(taskId) => { setRequestChangesTaskId(taskId); }} onMoveBackToDone={(taskId) => { void updateKanban(teamName, taskId, { op: 'remove' }); }} onStartTask={(taskId) => { void (async () => { try { await startTask(teamName, taskId); if (data?.isAlive) { const task = data.tasks.find((t) => t.id === taskId); if (task?.owner) { try { await api.teams.processSend( teamName, `Task #${taskId} "${task.subject}" has started. Please begin working on it.` ); } catch { // best-effort } } } } catch { // error via store } })(); }} onCompleteTask={(taskId) => { void updateTaskStatus(teamName, taskId, 'completed'); }} onScrollToTask={(taskId) => { const el = document.querySelector(`[data-task-id="${taskId}"]`); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); el.classList.add('ring-2', 'ring-blue-400/50'); setTimeout(() => el.classList.remove('ring-2', 'ring-blue-400/50'), 1500); } }} onTaskClick={(task) => setSelectedTask(task)} />
{ e.stopPropagation(); setSendDialogRecipient(undefined); setSendDialogOpen(true); }} > Message } > { openCreateTaskDialog(subject, description); }} /> setRequestChangesTaskId(null)} onSubmit={(comment) => { if (!requestChangesTaskId) { return; } void (async () => { try { await updateKanban(teamName, requestChangesTaskId, { op: 'request_changes', comment, }); setRequestChangesTaskId(null); } catch { // error state is handled in the store and shown in the view } })(); }} /> setSelectedMember(null)} onSendMessage={() => { const name = selectedMember?.name ?? ''; setSelectedMember(null); setSendDialogRecipient(name || undefined); setSendDialogOpen(true); }} onAssignTask={() => { const name = selectedMember?.name ?? ''; setSelectedMember(null); openCreateTaskDialog('', '', name); }} /> setEditDialogOpen(false)} onSaved={() => void selectTeam(teamName)} /> setLaunchDialogOpen(false)} onLaunch={async (request) => { await launchTeam(request); }} /> { void sendTeamMessage(teamName, { member, text, summary }); }} onClose={() => setSendDialogOpen(false)} /> setSelectedTask(null)} onScrollToTask={(taskId) => { setSelectedTask(null); const el = document.querySelector(`[data-task-id="${taskId}"]`); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); el.classList.add('ring-2', 'ring-blue-400/50'); setTimeout(() => el.classList.remove('ring-2', 'ring-blue-400/50'), 1500); } }} />
); };