import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { EffortLevelSelector } from '@renderer/components/team/dialogs/EffortLevelSelector'; import { getProviderScopedTeamModelLabel, getTeamProviderLabel, TeamModelSelector, } from '@renderer/components/team/dialogs/TeamModelSelector'; import { RoleSelect } from '@renderer/components/team/RoleSelect'; import { Button } from '@renderer/components/ui/button'; import { Input } from '@renderer/components/ui/input'; import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; 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 { getMemberColorByName } from '@shared/constants/memberColors'; import { AlertTriangle, ChevronDown, ChevronRight, 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; 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; draftKeyPrefix?: string; projectPath?: string | null; mentionSuggestions?: MentionSuggestion[]; taskSuggestions?: MentionSuggestion[]; teamSuggestions?: MentionSuggestion[]; lockProviderModel?: boolean; forceInheritedModelSettings?: boolean; modelLockReason?: string; isRemoved?: boolean; onRestore?: (id: string) => void; warningText?: string | null; disableGeminiOption?: boolean; modelIssueText?: string | null; } export const MemberDraftRow = ({ member, index, resolvedColor, nameError, onNameChange, onRoleChange, onCustomRoleChange, onRemove, showWorkflow = false, onWorkflowChange, onWorkflowChipsChange, onProviderChange, onModelChange, onEffortChange, inheritedProviderId = 'anthropic', inheritedModel = '', inheritedEffort, draftKeyPrefix, projectPath, mentionSuggestions = [], taskSuggestions, teamSuggestions, lockProviderModel = false, forceInheritedModelSettings = false, modelLockReason, isRemoved = false, onRestore, warningText, disableGeminiOption = false, modelIssueText, }: MemberDraftRowProps): React.JSX.Element => { const { isLight } = useTheme(); const memberColorSet = getTeamColorSet( resolvedColor ?? getMemberColorByName(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 modelTooltipText = forceInheritedModelSettings ? 'Provider, model, and effort are inherited from the lead while sync is enabled.' : modelLockReason; const hasModelIssue = Boolean(modelIssueText); return (
{nameError}
: null}{modelIssueText}
: null} {modelTooltipText ? ({modelTooltipText}
) : null}{warningText}
{modelLockReason ?? 'Provider, model, and effort changes are disabled while the team is live. Reconnect the team to apply them safely.'}
)}