From 0df13e206b6c27ba9534c211fb3d7108578bf11a Mon Sep 17 00:00:00 2001 From: iliya Date: Fri, 13 Mar 2026 14:39:07 +0200 Subject: [PATCH] feat: enhance CreateTeamDialog and member management features - Updated CreateTeamDialog to include automatic team name suggestions and improved default team description handling. - Enhanced member management with new utility functions for building member draft suggestions and color maps. - Integrated task and team mention suggestions into MembersEditorSection and MemberDraftRow for better user experience. - Refactored UI components for improved styling and functionality, ensuring consistent behavior across team management features. --- .../services/team/TeamProvisioningService.ts | 12 +-- .../team/dialogs/CreateTeamDialog.tsx | 78 ++++++++++++------- .../components/team/dialogs/teamNameSets.ts | 67 ++++++++++++++++ .../team/members/MemberDraftRow.tsx | 14 +++- .../team/members/MembersEditorSection.tsx | 48 ++++++------ .../team/members/membersEditorUtils.ts | 45 +++++++++-- .../components/ui/MentionSuggestionList.tsx | 6 +- .../components/ui/MentionableTextarea.tsx | 54 +++++++------ src/renderer/constants/teamColors.ts | 32 ++++++++ 9 files changed, 265 insertions(+), 91 deletions(-) create mode 100644 src/renderer/components/team/dialogs/teamNameSets.ts diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 739d0fd8..2833963e 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -830,8 +830,8 @@ ${buildMemberSpawnPrompt(m, displayName, request.teamName, taskProtocol, process return `agent_teams_ui [Agent Team: “${request.teamName}” | Project: “${projectName}” | Lead: “${leadName}”] — team does NOT exist yet. You must create it. You are running in a non-interactive CLI session. Do not ask questions. Do everything in a single turn. -CRITICAL: Execute ALL steps directly yourself. Do NOT use the Agent tool to delegate provisioning to a sub-agent. The ONLY valid use of the Agent tool is spawning individual teammates in step 2. -CRITICAL: During this initial team provisioning turn, do NOT call mcp__agent-teams__team_launch or mcp__agent-teams__team_stop. This turn is only for creating/provisioning the team state and spawning teammates. +CRITICAL: Execute ALL steps directly yourself in sequence. Do NOT delegate any step to a sub-agent via the Agent tool. The ONLY valid use of the Agent tool is spawning individual teammates in step 2. +CRITICAL: For step 1, use the BUILT-IN TeamCreate tool — NOT any mcp__agent-teams__* MCP tool. Do NOT call mcp__agent-teams__team_launch, mcp__agent-teams__team_stop, or any other mcp__agent-teams__ runtime tool during provisioning. MCP board tools (task_create, task_set_status, etc.) are allowed only in step 3. You are “${leadName}”, the team lead. Goal: Create and provision a NEW Claude Code agent team${request.members.length === 0 ? ' (solo — lead only)' : ' with live teammates'}. @@ -841,7 +841,7 @@ ${persistentContext} Steps (execute in this exact order — do NOT skip any step): -1) MANDATORY FIRST STEP: Call the TeamCreate tool with team_name=”${request.teamName}”. This creates the team config and in-memory state. Without this step, teammate spawns will FAIL. Do NOT assume the team already exists based on this prompt header. +1) MANDATORY FIRST STEP: Call the BUILT-IN TeamCreate tool (not any MCP tool) with team_name=”${request.teamName}”. This creates the team config and in-memory state. Without this step, teammate spawns will FAIL. Do NOT assume the team already exists. ${step2Block} @@ -960,8 +960,8 @@ ${memberSpawnInstructions} return `${startLabel} [Agent Team: "${request.teamName}" | Project: "${projectName}" | Lead: "${leadName}"] You are running in a non-interactive CLI session. Do not ask questions. Do everything in a single turn. -CRITICAL: Execute ALL steps directly yourself. Do NOT use the Agent tool to delegate work to a sub-agent. The ONLY valid use of the Agent tool is spawning individual teammates in step 2. -CRITICAL: During this initial team launch/reconnect turn, do NOT call mcp__agent-teams__team_launch or mcp__agent-teams__team_stop. This turn is only for reconnecting the existing team state and spawning teammates. +CRITICAL: Execute ALL steps directly yourself in sequence. Do NOT delegate any step to a sub-agent via the Agent tool. The ONLY valid use of the Agent tool is spawning individual teammates in step 2. +CRITICAL: Do NOT call mcp__agent-teams__team_launch, mcp__agent-teams__team_stop, or any other mcp__agent-teams__ runtime tool during this turn. MCP board tools (task_create, task_set_status, etc.) are allowed. You are "${leadName}", the team lead. Goal: Reconnect with existing team "${request.teamName}" and resume pending work. @@ -4699,7 +4699,7 @@ export class TeamProvisioningService { ``, `You are "${leadName}", the team lead of team "${run.teamName}".`, `You are running in a non-interactive CLI session. Do not ask questions.`, - `CRITICAL: Execute ALL steps directly yourself. Do NOT use the Agent tool to delegate work to a sub-agent. The ONLY valid use of the Agent tool is spawning individual teammates.`, + `CRITICAL: Execute ALL steps directly yourself in sequence. Do NOT delegate any step to a sub-agent via the Agent tool. The ONLY valid use of the Agent tool is spawning individual teammates.`, ``, persistentContext, taskBoardBlock.trim() ? `\n${taskBoardBlock}` : '', diff --git a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx index 8d5cdf78..f2341227 100644 --- a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx @@ -2,6 +2,8 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { api } from '@renderer/api'; import { + buildMemberDraftColorMap, + buildMemberDraftSuggestions, buildMembersFromDrafts, createMemberDraft, MembersEditorSection, @@ -25,10 +27,11 @@ import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors' import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence'; import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence'; import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer'; +import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions'; +import { useTeamSuggestions } from '@renderer/hooks/useTeamSuggestions'; import { useTheme } from '@renderer/hooks/useTheme'; import { cn } from '@renderer/lib/utils'; import { normalizePath } from '@renderer/utils/pathNormalize'; -import { getMemberColorByName } from '@shared/constants/memberColors'; import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react'; import { AdvancedCliSection } from './AdvancedCliSection'; @@ -37,6 +40,7 @@ import { ExtendedContextCheckbox } from './ExtendedContextCheckbox'; import { OptionalSettingsSection } from './OptionalSettingsSection'; import { ProjectPathSelector } from './ProjectPathSelector'; import { SkipPermissionsCheckbox } from './SkipPermissionsCheckbox'; +import { getNextSuggestedTeamName } from './teamNameSets'; import { computeEffectiveTeamModel, TeamModelSelector } from './TeamModelSelector'; import type { MemberDraft } from '@renderer/components/team/members/membersEditorTypes'; @@ -52,7 +56,6 @@ const TEAM_COLOR_NAMES = [ 'pink', ] as const; -import type { MentionSuggestion } from '@renderer/types/mention'; import type { EffortLevel, Project, @@ -97,10 +100,9 @@ interface ValidationResult { }; } -import { CUSTOM_ROLE, NO_ROLE, PRESET_ROLES } from '@renderer/constants/teamRoles'; +import { CUSTOM_ROLE, PRESET_ROLES } from '@renderer/constants/teamRoles'; const DEV_DEFAULT_TEAM = { - teamName: 'team-alpha', - description: 'Dev test team for provisioning flow', + teamName: 'signal-ops', } as const; const DEV_DEFAULT_MEMBERS: { name: string; roleSelection: string }[] = [ @@ -134,6 +136,13 @@ function validateTeamNameInline(name: string): string | null { return null; } +function buildDefaultTeamDescription(teamName: string): string { + const trimmedName = teamName.trim(); + return trimmedName.length > 0 + ? `${trimmedName} team for provisioning flow` + : 'Team for provisioning flow'; +} + function validateRequest( request: TeamCreateRequest, options?: { requireCwd?: boolean } @@ -224,6 +233,7 @@ export const CreateTeamDialog = ({ const [prepareMessage, setPrepareMessage] = useState(null); const [prepareWarnings, setPrepareWarnings] = useState([]); const prepareRequestSeqRef = useRef(0); + const lastAutoDescriptionRef = useRef(null); const [fieldErrors, setFieldErrors] = useState<{ teamName?: string; members?: string; @@ -313,6 +323,7 @@ export const CreateTeamDialog = ({ const resetFormState = (): void => { setTeamName(''); + lastAutoDescriptionRef.current = null; descriptionDraft.clearDraft(); promptDraft.clearDraft(); promptChipDraft.clearChipDraft(); @@ -328,6 +339,7 @@ export const CreateTeamDialog = ({ const effectiveCwd = cwdMode === 'project' ? selectedProjectPath.trim() : customCwd.trim(); const dialogTeamNameKey = sanitizeTeamName(teamName.trim()); + const suggestedTeamName = getNextSuggestedTeamName(existingTeamNames); // Clear stale provisioning error when dialog opens useEffect(() => { @@ -469,16 +481,34 @@ export const CreateTeamDialog = ({ // eslint-disable-next-line react-hooks/exhaustive-deps -- initialData is checked once on open }, [open]); + useEffect(() => { + if (!open || initialData) { + return; + } + setTeamName((prev) => (prev.trim().length === 0 ? suggestedTeamName : prev)); + }, [initialData, open, suggestedTeamName]); + useEffect(() => { if (!open || !isDev || initialData) { return; } - setTeamName((prev) => (prev.trim().length === 0 ? DEV_DEFAULT_TEAM.teamName : prev)); - if (descriptionDraft.value.trim().length === 0) { - descriptionDraft.setValue(DEV_DEFAULT_TEAM.description); + const resolvedTeamName = teamName.trim() || suggestedTeamName; + const nextAutoDescription = buildDefaultTeamDescription(resolvedTeamName); + const currentDescription = descriptionDraft.value.trim(); + const previousAutoDescription = lastAutoDescriptionRef.current?.trim() ?? ''; + const shouldSyncDescription = + currentDescription.length === 0 || currentDescription === previousAutoDescription; + + if (shouldSyncDescription && descriptionDraft.value !== nextAutoDescription) { + lastAutoDescriptionRef.current = nextAutoDescription; + descriptionDraft.setValue(nextAutoDescription); + return; } - // eslint-disable-next-line react-hooks/exhaustive-deps -- dev defaults applied once on open - }, [open]); + + if (currentDescription === nextAutoDescription) { + lastAutoDescriptionRef.current = nextAutoDescription; + } + }, [descriptionDraft, initialData, isDev, open, suggestedTeamName, teamName]); // Pre-select defaultProjectPath when projects loaded (only while dialog is open) useEffect(() => { @@ -501,27 +531,19 @@ export const CreateTeamDialog = ({ useFileListCacheWarmer(effectiveCwd || null); + const { suggestions: taskSuggestions } = useTaskSuggestions(null); + const { suggestions: teamMentionSuggestions } = useTeamSuggestions(null); + const description = descriptionDraft.value; const prompt = promptDraft.value; + const memberColorMap = useMemo(() => buildMemberDraftColorMap(members), [members]); - const mentionSuggestions = useMemo( + const mentionSuggestions = useMemo( () => soloTeam ? [{ id: 'team-lead', name: 'team-lead', subtitle: 'Team Lead', color: 'blue' }] - : members - .filter((m) => m.name.trim()) - .map((m) => ({ - id: m.id, - name: m.name.trim(), - subtitle: - m.roleSelection === CUSTOM_ROLE - ? m.customRole.trim() || undefined - : m.roleSelection && m.roleSelection !== NO_ROLE - ? m.roleSelection - : undefined, - color: getMemberColorByName(m.name.trim()), - })), - [members, soloTeam] + : buildMemberDraftSuggestions(members, memberColorMap), + [memberColorMap, members, soloTeam] ); const effectiveModel = useMemo( @@ -801,7 +823,7 @@ export const CreateTeamDialog = ({ )} value={teamName} onChange={(event) => handleTeamNameChange(event.target.value)} - placeholder="team-alpha" + placeholder={suggestedTeamName} /> {existingTeamNames.includes(sanitizedTeamName) ? (

@@ -833,6 +855,8 @@ export const CreateTeamDialog = ({ showJsonEditor draftKeyPrefix="createTeam" projectPath={effectiveCwd || null} + taskSuggestions={taskSuggestions} + teamSuggestions={teamMentionSuggestions} hideContent={soloTeam} headerExtra={

@@ -930,6 +954,8 @@ export const CreateTeamDialog = ({ value={prompt} onValueChange={promptDraft.setValue} suggestions={soloTeam ? [] : mentionSuggestions} + teamSuggestions={teamMentionSuggestions} + taskSuggestions={taskSuggestions} projectPath={effectiveCwd || null} chips={promptChipDraft.chips} onChipRemove={promptChipDraft.removeChip} diff --git a/src/renderer/components/team/dialogs/teamNameSets.ts b/src/renderer/components/team/dialogs/teamNameSets.ts new file mode 100644 index 00000000..40600160 --- /dev/null +++ b/src/renderer/components/team/dialogs/teamNameSets.ts @@ -0,0 +1,67 @@ +const TEAM_NAME_SETS = [ + ['signal-ops', 'forge-labs', 'atlas-hq', 'relay-works', 'beacon-desk', 'vector-room'], + ['northstar-core', 'summit-ops', 'harbor-labs', 'pilot-desk', 'mission-control', 'launchpad'], + ['quartz-forge', 'ember-collective', 'prism-works', 'cinder-labs', 'aurora-room', 'sable-ops'], + ['delta-studio', 'comet-hub', 'orbit-core', 'kernel-crew', 'circuit-labs', 'flux-team'], +] as const; + +function normalizeTeamName(name: string): string { + return name.trim().toLowerCase(); +} + +function belongsToBaseTeamName(name: string, baseName: string): boolean { + const normalized = normalizeTeamName(name); + return normalized === baseName || normalized.startsWith(`${baseName}-`); +} + +function getPreferredTeamNameSet(existingNames: readonly string[]): readonly string[] { + for (const nameSet of TEAM_NAME_SETS) { + if ( + nameSet.some((candidate) => + existingNames.some((name) => belongsToBaseTeamName(name, candidate)) + ) + ) { + return nameSet; + } + } + + return TEAM_NAME_SETS[0]; +} + +function createUniqueTeamName(baseName: string, existingNames: readonly string[]): string { + const normalizedExisting = new Set(existingNames.map(normalizeTeamName).filter(Boolean)); + if (!normalizedExisting.has(baseName)) { + return baseName; + } + + let suffix = 2; + while (normalizedExisting.has(`${baseName}-${suffix}`)) { + suffix += 1; + } + + return `${baseName}-${suffix}`; +} + +export function getNextSuggestedTeamName(existingNames: readonly string[]): string { + const normalizedExisting = new Set(existingNames.map(normalizeTeamName).filter(Boolean)); + const preferredSet = getPreferredTeamNameSet(existingNames); + + for (const candidate of preferredSet) { + if (!normalizedExisting.has(candidate)) { + return candidate; + } + } + + for (const nameSet of TEAM_NAME_SETS) { + for (const candidate of nameSet) { + if (!normalizedExisting.has(candidate)) { + return candidate; + } + } + } + + const fallbackBaseName = preferredSet[existingNames.length % preferredSet.length] ?? 'signal-ops'; + return createUniqueTeamName(fallbackBaseName, existingNames); +} + +export { TEAM_NAME_SETS }; diff --git a/src/renderer/components/team/members/MemberDraftRow.tsx b/src/renderer/components/team/members/MemberDraftRow.tsx index c0203880..476c0ac6 100644 --- a/src/renderer/components/team/members/MemberDraftRow.tsx +++ b/src/renderer/components/team/members/MemberDraftRow.tsx @@ -20,6 +20,7 @@ import type { MentionSuggestion } from '@renderer/types/mention'; interface MemberDraftRowProps { member: MemberDraft; index: number; + resolvedColor?: string; nameError: string | null; onNameChange: (id: string, name: string) => void; onRoleChange: (id: string, roleSelection: string) => void; @@ -31,11 +32,14 @@ interface MemberDraftRowProps { draftKeyPrefix?: string; projectPath?: string | null; mentionSuggestions?: MentionSuggestion[]; + taskSuggestions?: MentionSuggestion[]; + teamSuggestions?: MentionSuggestion[]; } export const MemberDraftRow = ({ member, index, + resolvedColor, nameError, onNameChange, onRoleChange, @@ -47,10 +51,12 @@ export const MemberDraftRow = ({ draftKeyPrefix, projectPath, mentionSuggestions = [], + taskSuggestions, + teamSuggestions, }: MemberDraftRowProps): React.JSX.Element => { const { isLight } = useTheme(); const memberColorSet = getTeamColorSet( - getMemberColorByName(member.name.trim() || `member-${index}`) + resolvedColor ?? getMemberColorByName(member.name.trim() || `member-${index}`) ); const [workflowExpanded, setWorkflowExpanded] = useState(false); const [modelExpanded, setModelExpanded] = useState(false); @@ -119,7 +125,7 @@ export const MemberDraftRow = ({ return (