import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { AnthropicExtraUsageWarning } from '@renderer/components/team/dialogs/AnthropicExtraUsageWarning'; import { EffortLevelSelector } from '@renderer/components/team/dialogs/EffortLevelSelector'; import { formatTeamModelSummary, getProviderScopedTeamModelLabel, getTeamProviderLabel, TeamModelSelector, } from '@renderer/components/team/dialogs/TeamModelSelector'; import { RoleSelect } from '@renderer/components/team/RoleSelect'; import { Button } from '@renderer/components/ui/button'; import { Checkbox } from '@renderer/components/ui/checkbox'; import { HoverTooltip } from '@renderer/components/ui/hover-tooltip'; import { Input } from '@renderer/components/ui/input'; import { Label } from '@renderer/components/ui/label'; import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; import { getTeamColorSet } from '@renderer/constants/teamColors'; import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence'; import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer'; import { useTheme } from '@renderer/hooks/useTheme'; import { cn } from '@renderer/lib/utils'; import { reconcileChips, removeChipTokenFromText } from '@renderer/utils/chipUtils'; import { isAnthropicSonnetOneMillionContextTeamModel } from '@renderer/utils/teamModelCatalog'; import { getMemberColorByName } from '@shared/constants/memberColors'; import { AlertTriangle, ChevronDown, ChevronRight, GitBranch, Info, RotateCcw, Trash2, } from 'lucide-react'; import type { MemberDraft } from './membersEditorTypes'; import type { InlineChip } from '@renderer/types/inlineChip'; import type { MentionSuggestion } from '@renderer/types/mention'; import type { EffortLevel, TeamProviderId } from '@shared/types'; interface MemberDraftRowProps { member: MemberDraft; index: number; avatarSrc?: string; resolvedColor?: string; nameError: string | null; onNameChange: (id: string, name: string) => void; onRoleChange: (id: string, roleSelection: string) => void; onCustomRoleChange: (id: string, customRole: string) => void; onRemove: (id: string) => void; showWorkflow?: boolean; onWorkflowChange?: (id: string, workflow: string) => void; onWorkflowChipsChange?: (id: string, chips: InlineChip[]) => void; onProviderChange: (id: string, providerId: TeamProviderId) => void; onModelChange: (id: string, model: string) => void; onEffortChange: (id: string, effort: string) => void; inheritedProviderId?: TeamProviderId; inheritedModel?: string; inheritedEffort?: EffortLevel; limitContext?: boolean; draftKeyPrefix?: string; projectPath?: string | null; mentionSuggestions?: MentionSuggestion[]; taskSuggestions?: MentionSuggestion[]; teamSuggestions?: MentionSuggestion[]; lockProviderModel?: boolean; lockRole?: boolean; lockedRoleLabel?: string; lockIdentity?: boolean; identityLockReason?: string; forceInheritedModelSettings?: boolean; modelLockReason?: string; isRemoved?: boolean; onRestore?: (id: string) => void; hideActionButton?: boolean; warningText?: string | null; infoText?: string | null; disableGeminiOption?: boolean; modelIssueText?: string | null; modelAdvisoryReasonByProvider?: Partial< Record>> >; modelIssueReasonByProvider?: Partial< Record>> >; modelUnavailableReasonByProvider?: Partial< Record>> >; showWorktreeIsolationControls?: boolean; worktreeIsolationDisabledReason?: string | null; onWorktreeIsolationChange?: (id: string, enabled: boolean) => void; lockedModelAction?: { label: string; description?: string; onClick: () => void; disabled?: boolean; }; } export const MemberDraftRow = ({ member, index, avatarSrc, resolvedColor, nameError, onNameChange, onRoleChange, onCustomRoleChange, onRemove, showWorkflow = false, onWorkflowChange, onWorkflowChipsChange, onProviderChange, onModelChange, onEffortChange, inheritedProviderId = 'anthropic', inheritedModel = '', inheritedEffort, limitContext = false, draftKeyPrefix, projectPath, mentionSuggestions = [], taskSuggestions, teamSuggestions, lockProviderModel = false, lockRole = false, lockedRoleLabel, lockIdentity = false, identityLockReason, forceInheritedModelSettings = false, modelLockReason, isRemoved = false, onRestore, hideActionButton = false, warningText, infoText, disableGeminiOption = false, modelIssueText, modelAdvisoryReasonByProvider, modelIssueReasonByProvider, modelUnavailableReasonByProvider, showWorktreeIsolationControls = false, worktreeIsolationDisabledReason, onWorktreeIsolationChange, lockedModelAction, }: MemberDraftRowProps): React.JSX.Element => { const { isLight } = useTheme(); const memberColorSet = getTeamColorSet( resolvedColor ?? getMemberColorByName(member.originalName?.trim() || member.name.trim() || `member-${index}`) ); const [workflowExpanded, setWorkflowExpanded] = useState(false); const [modelExpanded, setModelExpanded] = useState(false); // Pre-warm file list cache when workflow section is expanded useFileListCacheWarmer(workflowExpanded && projectPath ? projectPath : null); const draftKey = draftKeyPrefix && (member.name.trim() || member.id) ? `${draftKeyPrefix}:workflow:${member.name.trim() || member.id}` : null; const workflowDraft = useDraftPersistence({ key: draftKey ?? `workflow:${member.id}`, initialValue: member.workflow?.trim() ? member.workflow : undefined, enabled: !!draftKey, }); const chips = useMemo(() => member.workflowChips ?? [], [member.workflowChips]); const handleWorkflowChange = useCallback( (v: string) => { const reconciled = reconcileChips(chips, v); if (reconciled.length !== chips.length) { onWorkflowChipsChange?.(member.id, reconciled); } workflowDraft.setValue(v); onWorkflowChange?.(member.id, v); }, [member.id, chips, onWorkflowChange, onWorkflowChipsChange, workflowDraft] ); const handleFileChipInsert = useCallback( (chip: InlineChip) => { onWorkflowChipsChange?.(member.id, [...chips, chip]); }, [member.id, chips, onWorkflowChipsChange] ); const handleChipRemove = useCallback( (chipId: string) => { const chip = chips.find((c) => c.id === chipId); if (!chip) return; const newChips = chips.filter((c) => c.id !== chipId); const newValue = removeChipTokenFromText(workflowDraft.value, chip); onWorkflowChipsChange?.(member.id, newChips); workflowDraft.setValue(newValue); onWorkflowChange?.(member.id, newValue); }, [chips, member.id, onWorkflowChange, onWorkflowChipsChange, workflowDraft] ); useEffect(() => { if ( onWorkflowChange && workflowDraft.value && workflowDraft.value !== (member.workflow ?? '') ) { onWorkflowChange(member.id, workflowDraft.value); } }, [workflowDraft.value, member.id, member.workflow, onWorkflowChange]); const suggestionsExcludingSelf = mentionSuggestions.filter( (s) => s.name.toLowerCase() !== member.name.trim().toLowerCase() ); const effectiveProviderId = forceInheritedModelSettings ? inheritedProviderId : (member.providerId ?? inheritedProviderId); const effectiveModel = forceInheritedModelSettings ? inheritedModel : (member.model ?? inheritedModel); const effectiveEffort = forceInheritedModelSettings ? inheritedEffort : (member.effort ?? inheritedEffort); const modelButtonLabelBase = effectiveModel?.trim() ? getProviderScopedTeamModelLabel(effectiveProviderId, effectiveModel.trim()) : 'Default'; const modelButtonLabel = forceInheritedModelSettings ? `${modelButtonLabelBase} (lead)` : modelButtonLabelBase; const modelButtonAriaLabel = `${getTeamProviderLabel(effectiveProviderId)} provider, ${modelButtonLabel}`; const canOpenLockedModelPanel = lockProviderModel && !isRemoved && Boolean(lockedModelAction); const modelTooltipText = forceInheritedModelSettings ? 'Provider, model, and effort are inherited from the lead while sync is enabled.' : lockProviderModel ? (lockedModelAction?.description ?? modelLockReason) : undefined; const worktreeIsolationDisabled = isRemoved || Boolean(worktreeIsolationDisabledReason && member.isolation !== 'worktree'); const worktreeIsolationDescription = worktreeIsolationDisabledReason && member.isolation !== 'worktree' ? worktreeIsolationDisabledReason : 'Run this teammate in a separate git worktree. Apply/reject changes targets that worktree, not the lead workspace.'; const worktreeIsolationDescriptionId = showWorktreeIsolationControls ? `member-${member.id}-worktree-isolation-description` : undefined; const effectiveModelKey = effectiveModel?.trim() ?? ''; const selectedModelIssueText = effectiveModelKey && modelIssueReasonByProvider?.[effectiveProviderId]?.[effectiveModelKey] ? modelIssueReasonByProvider[effectiveProviderId]?.[effectiveModelKey] : null; const selectedModelUnavailableText = effectiveModelKey && modelUnavailableReasonByProvider?.[effectiveProviderId]?.[effectiveModelKey] ? modelUnavailableReasonByProvider[effectiveProviderId]?.[effectiveModelKey] : null; const selectedModelAdvisoryText = effectiveModelKey && modelAdvisoryReasonByProvider?.[effectiveProviderId]?.[effectiveModelKey] ? modelAdvisoryReasonByProvider[effectiveProviderId]?.[effectiveModelKey] : null; const currentModelIssueText = modelIssueText ?? selectedModelUnavailableText ?? selectedModelIssueText ?? null; const currentModelAdvisoryText = currentModelIssueText ? null : selectedModelAdvisoryText; const hasModelIssue = Boolean(currentModelIssueText); const hasModelAdvisory = Boolean(currentModelAdvisoryText); const modelButtonDisabled = (lockProviderModel && !canOpenLockedModelPanel) || isRemoved; const modelButtonTitle = [currentModelIssueText ?? currentModelAdvisoryText, modelTooltipText] .filter((message): message is string => Boolean(message)) .join('\n') || undefined; const modelIssueDescriptionId = hasModelIssue || hasModelAdvisory ? `member-${member.id}-model-issue` : undefined; const modelHelpDescriptionId = modelTooltipText ? `member-${member.id}-model-help` : undefined; const modelButtonDescribedBy = [modelIssueDescriptionId, modelHelpDescriptionId].filter(Boolean).join(' ') || undefined; const modelButtonTooltipContent = currentModelIssueText || currentModelAdvisoryText || modelTooltipText ? ( <> {currentModelIssueText ? ( {currentModelIssueText} ) : null} {currentModelAdvisoryText ? ( {currentModelAdvisoryText} ) : null} {modelTooltipText ? ( {modelTooltipText} ) : null} ) : null; const hasCustomProviderOrModel = !forceInheritedModelSettings && Boolean(member.providerId || member.model?.trim()); const showSonnetExtraUsageWarning = effectiveProviderId === 'anthropic' && !limitContext && hasCustomProviderOrModel && isAnthropicSonnetOneMillionContextTeamModel(effectiveModel); const warningMessages = [warningText?.trim() || null].filter((message): message is string => Boolean(message) ); const hasWarnings = warningMessages.length > 0 || showSonnetExtraUsageWarning; const anthropicContextModeLabel = limitContext ? '200K limit enabled' : '1M-capable context allowed'; const runtimeSummary = formatTeamModelSummary( effectiveProviderId, effectiveModel?.trim() ?? '', effectiveEffort ); return (
); };