import { lazy, memo, Suspense, useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react'; import { useAppTranslation } from '@features/localization/renderer'; import { api } from '@renderer/api'; import { SessionPanel } from '@renderer/components/chat/session-panel'; import { confirm } from '@renderer/components/common/ConfirmDialog'; import { Button } from '@renderer/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@renderer/components/ui/dialog'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors'; import { useBranchSync } from '@renderer/hooks/useBranchSync'; import { useOptionalTabId } from '@renderer/hooks/useOptionalTabId'; import { useResizablePanel } from '@renderer/hooks/useResizablePanel'; import { useTheme } from '@renderer/hooks/useTheme'; import { cn } from '@renderer/lib/utils'; import { useStore } from '@renderer/store'; import { getCurrentProvisioningProgressForTeam, isTeamProvisioningActive, selectResolvedMemberForTeamName, selectResolvedMembersForTeamName, selectTeamMemberSnapshotsForName, } from '@renderer/store/slices/teamSlice'; import { createChipFromSelection } from '@renderer/utils/chipUtils'; import * as tokenMath from '@renderer/utils/contextMath'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { hasUnresolvedMemberSpawnStatus, MEMBER_SPAWN_STATUS_REFRESH_MS, } from '@renderer/utils/memberSpawnStatusPolling'; import { shouldClearPendingReplyForOpenCodeRuntimeDelivery } from '@renderer/utils/openCodeRuntimeDeliveryDiagnostics'; import { formatProjectPath } from '@renderer/utils/pathDisplay'; import { buildTaskCountsByOwner, normalizePath } from '@renderer/utils/pathNormalize'; import { nameColorSet } from '@renderer/utils/projectColor'; import { resolveProjectIdByPath } from '@renderer/utils/projectLookup'; import { buildTaskChangeRequestOptions, type TaskChangeRequestOptions, } from '@renderer/utils/taskChangeRequest'; import { buildPendingRuntimeSummaryCopy } from '@renderer/utils/teamLaunchSummaryCopy'; import { stripAgentBlocks } from '@shared/constants/agentBlocks'; import { isLeadMember } from '@shared/utils/leadDetection'; import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { AlertTriangle, ChevronRight, Clock, Code, Columns3, Expand, FolderOpen, GitBranch, History, MessageSquare, MoreHorizontal, Network, Paperclip, Pencil, Play, Plus, Square, Terminal, Trash2, UserPlus, Users, } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { AddMemberDialog } from './dialogs/AddMemberDialog'; import { EditTeamDialog } from './dialogs/EditTeamDialog'; import { LaunchTeamDialogLoadingFallback } from './dialogs/LaunchTeamDialogLoadingFallback'; import { ReviewDialog } from './dialogs/ReviewDialog'; import { executeTeamRelaunch } from './dialogs/teamRelaunchFlow'; import { KanbanBoard } from './kanban/KanbanBoard'; import { UNASSIGNED_OWNER } from './kanban/KanbanFilterPopover'; import { KanbanSearchInput } from './kanban/KanbanSearchInput'; import { TrashDialog } from './kanban/TrashDialog'; import { MemberDetailDialog } from './members/MemberDetailDialog'; import { type MemberActivityFilter, type MemberDetailTab } from './members/memberDetailTypes'; import { deriveMetrics } from './context-metric-alias'; import type { AddMemberEntry } from './dialogs/AddMemberDialog'; import type { TeamLaunchDialogMode } from './dialogs/LaunchTeamDialog'; import type { TeamColorSet } from '@renderer/constants/teamColors'; import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode'; import type { ComponentProps, CSSProperties, RefObject } from 'react'; const sumInjectionTokens = tokenMath[ ['sum', 'Con' + 'text', 'InjectionTokens'].join('') as keyof typeof tokenMath ] as (injections: readonly unknown[]) => number; const LaunchTeamDialog = lazy(() => import('./dialogs/LaunchTeamDialog').then((m) => ({ default: m.LaunchTeamDialog })) ); const ProjectEditorOverlay = lazy(() => import('./editor/ProjectEditorOverlay').then((m) => ({ default: m.ProjectEditorOverlay })) ); const TeamGraphOverlay = lazy(() => import('@features/agent-graph/renderer').then((m) => ({ default: m.TeamGraphOverlay, })) ); const TaskDetailDialog = lazy(() => import('./dialogs/TaskDetailDialog').then((m) => ({ default: m.TaskDetailDialog })) ); const SendMessageDialog = lazy(() => import('./dialogs/SendMessageDialog').then((m) => ({ default: m.SendMessageDialog })) ); const CreateTaskDialog = lazy(() => import('./dialogs/CreateTaskDialog').then((m) => ({ default: m.CreateTaskDialog })) ); const ChangeReviewDialog = lazy(() => import('./review/ChangeReviewDialog').then((m) => ({ default: m.ChangeReviewDialog })) ); import { MemberList } from './members/MemberList'; import { MessagesPanel } from './messages/MessagesPanel'; import { ScheduleSection } from './schedule/ScheduleSection'; import { TeamSidebarHost } from './sidebar/TeamSidebarHost'; import { TeamSidebarPortalSource } from './sidebar/TeamSidebarPortalSource'; import { TeamSidebarRail } from './sidebar/TeamSidebarRail'; import { getTeamPendingRepliesState, setTeamPendingRepliesState, } from './sidebar/teamSidebarUiState'; import { ClaudeLogsSection } from './ClaudeLogsSection'; import { CollapsibleTeamSection } from './CollapsibleTeamSection'; import { deriveLeadLoadButtonLabel } from './lead-load-guards'; import { LeadSessionDetailGate } from './LeadSessionDetailGate'; import { LiveRuntimeStatusBridge } from './LiveRuntimeStatusBridge'; import { ProcessesSection } from './ProcessesSection'; import { getLaunchJoinMilestonesFromMembers, getLaunchJoinState } from './provisioningSteps'; import { TeamChangesSection } from './TeamChangesSection'; import { TeamProvisioningBanner } from './TeamProvisioningBanner'; import { loadTeamSessionMetadata } from './teamSessionFetchGuards'; import { TeamSessionsSection } from './TeamSessionsSection'; import { useTeamAgentRuntimeWatcher } from './useTeamAgentRuntimeWatcher'; import type { UsageLike } from './context-metric-alias'; import type { KanbanFilterState } from './kanban/KanbanFilterPopover'; import type { KanbanSortState } from './kanban/KanbanSortPopover'; import type { SessionInjection } from './session-injection-types'; import type { Session } from '@renderer/types/data'; import type { InlineChip } from '@renderer/types/inlineChip'; import type { MemberSpawnStatusEntry, ResolvedTeamMember, TaskRef, TeamAgentRuntimeEntry, TeamCreateRequest, TeamLaunchRequest, TeamProviderId, TeamSummary, TeamTaskWithKanban, } from '@shared/types'; import type { EditorSelectionAction } from '@shared/types/editor'; interface TeamDetailViewProps { teamName: string; isActive?: boolean; isPaneFocused?: boolean; } interface CreateTaskDialogState { open: boolean; defaultSubject: string; defaultDescription: string; defaultOwner: string; defaultStartImmediately?: boolean; defaultChip?: InlineChip; } const TEAM_PENDING_REPLY_REFRESH_DELAY_MS = 10_000; const MEMBER_ROSTER_HYDRATION_RETRY_DELAY_MS = 1_200; const FLOATING_COMPOSER_SCROLL_RESERVE_BASE_PX = 200; function getSummaryKnownTeammateCount(summary: TeamSummary | undefined): number { if (!summary) { return 0; } const normalizedLeadName = summary.leadName?.trim().toLowerCase(); const rosterNames = new Set(); for (const member of summary.members ?? []) { const name = member.name?.trim(); if (!name) { continue; } const normalizedName = name.toLowerCase(); if ( normalizedName === 'user' || isLeadMember({ name }) || (normalizedLeadName && normalizedName === normalizedLeadName) ) { continue; } rosterNames.add(normalizedName); } const launchNames = new Set(); for (const rawName of [...(summary.missingMembers ?? []), ...(summary.skippedMembers ?? [])]) { const name = rawName.trim(); if (!name) { continue; } const normalizedName = name.toLowerCase(); if ( normalizedName === 'user' || isLeadMember({ name }) || (normalizedLeadName && normalizedName === normalizedLeadName) ) { continue; } launchNames.add(normalizedName); } const activeRosterCount = Math.max(summary.memberCount, rosterNames.size); if (activeRosterCount > 0) { return activeRosterCount; } return Math.max( summary.expectedMemberCount ?? 0, launchNames.size, (summary.confirmedCount ?? 0) + (summary.pendingCount ?? 0) + (summary.failedCount ?? 0) + (summary.skippedCount ?? 0) ); } function areResolvedMembersEqual( prev: readonly ResolvedTeamMember[], next: readonly ResolvedTeamMember[] ): boolean { if (prev === next) return true; if (prev.length !== next.length) return false; for (let i = 0; i < prev.length; i++) { const prevMember = prev[i]; const nextMember = next[i]; if ( prevMember.name !== nextMember.name || prevMember.status !== nextMember.status || prevMember.currentTaskId !== nextMember.currentTaskId || prevMember.color !== nextMember.color || prevMember.agentType !== nextMember.agentType || prevMember.role !== nextMember.role || prevMember.workflow !== nextMember.workflow || prevMember.providerId !== nextMember.providerId || prevMember.model !== nextMember.model || prevMember.effort !== nextMember.effort || prevMember.cwd !== nextMember.cwd || prevMember.gitBranch !== nextMember.gitBranch || prevMember.removedAt !== nextMember.removedAt || prevMember.runtimeAdvisory?.kind !== nextMember.runtimeAdvisory?.kind || prevMember.runtimeAdvisory?.observedAt !== nextMember.runtimeAdvisory?.observedAt || prevMember.runtimeAdvisory?.retryUntil !== nextMember.runtimeAdvisory?.retryUntil || prevMember.runtimeAdvisory?.retryDelayMs !== nextMember.runtimeAdvisory?.retryDelayMs || prevMember.runtimeAdvisory?.reasonCode !== nextMember.runtimeAdvisory?.reasonCode || prevMember.runtimeAdvisory?.message !== nextMember.runtimeAdvisory?.message ) { return false; } } return true; } function useStableActiveMembers( members: readonly ResolvedTeamMember[] | undefined ): ResolvedTeamMember[] { const filteredMembers = useMemo( () => (members ?? []).filter((member) => !member.removedAt), [members] ); const stableMembersRef = useRef(filteredMembers); if (!areResolvedMembersEqual(stableMembersRef.current, filteredMembers)) { stableMembersRef.current = filteredMembers; } return stableMembersRef.current; } interface TimeWindow { start: number; end: number; } function filterKanbanTasks(tasks: TeamTaskWithKanban[], query: string): TeamTaskWithKanban[] { if (query.startsWith('#')) { const id = query.slice(1); return tasks.filter((t) => t.id === id || t.displayId === id); } const lower = query.toLowerCase(); return tasks.filter( (t) => t.id.toLowerCase().includes(lower) || (t.displayId?.toLowerCase().includes(lower) ?? false) || t.subject.toLowerCase().includes(lower) || (t.owner?.toLowerCase().includes(lower) ?? false) ); } const TEAM_LOADING_MEMBER_ACCENTS = ['#46d93b', '#3b82f6', '#facc15', '#14b8a6', '#ef4444']; const TEAM_LOADING_KANBAN_COLUMNS = [ { id: 'todo', headerBg: 'rgba(59, 130, 246, 0.28)', bodyBg: 'rgba(59, 130, 246, 0.06)', }, { id: 'inProgress', headerBg: 'rgba(234, 179, 8, 0.28)', bodyBg: 'rgba(234, 179, 8, 0.07)', }, { id: 'review', headerBg: 'rgba(139, 92, 246, 0.28)', bodyBg: 'rgba(139, 92, 246, 0.07)', }, ] as const; type SkeletonClassNameProps = Readonly<{ className?: string }>; const SkeletonBlock = ({ className }: SkeletonClassNameProps): React.JSX.Element => (