diff --git a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx index 450e224f..05dcfe38 100644 --- a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +++ b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx @@ -46,7 +46,6 @@ import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors' import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence'; import { useCreateTeamDraft } from '@renderer/hooks/useCreateTeamDraft'; 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'; @@ -473,6 +472,7 @@ export const CreateTeamDialog = ({ >({}); const [providerSettingsProviderId, setProviderSettingsProviderId] = useState(null); + const [workflowMentionSuggestionsEnabled, setWorkflowMentionSuggestionsEnabled] = useState(false); const prepareRequestSeqRef = useRef(0); const prepareIdleHandlesRef = useRef(new Set()); const prepareUnmountGenerationRef = useRef(0); @@ -561,6 +561,9 @@ export const CreateTeamDialog = ({ setSelectedFastModeRaw(value); setStoredCreateTeamFastMode(value); }, []); + const enableWorkflowMentionSuggestions = useCallback((): void => { + setWorkflowMentionSuggestionsEnabled(true); + }, []); const setWorktreeEnabled = (value: boolean): void => { setWorktreeEnabledRaw(value); @@ -1235,6 +1238,7 @@ export const CreateTeamDialog = ({ useEffect(() => { if (!open) { + setWorkflowMentionSuggestionsEnabled(false); return; } @@ -1430,10 +1434,12 @@ export const CreateTeamDialog = ({ setSelectedProjectPath(''); }, [open, cwdMode, projects, selectedProjectPath, setSelectedProjectPath]); - useFileListCacheWarmer(effectiveCwd || null); - - const { suggestions: taskSuggestions } = useTaskSuggestions(null); - const { suggestions: teamMentionSuggestions } = useTeamSuggestions(null); + const { suggestions: taskSuggestions } = useTaskSuggestions(null, { + enabled: workflowMentionSuggestionsEnabled, + }); + const { suggestions: teamMentionSuggestions } = useTeamSuggestions(null, { + enabled: workflowMentionSuggestionsEnabled, + }); const description = descriptionDraft.value; const prompt = promptDraft.value; @@ -2206,6 +2212,7 @@ export const CreateTeamDialog = ({ projectPath={effectiveCwd || null} taskSuggestions={taskSuggestions} teamSuggestions={teamMentionSuggestions} + onWorkflowSuggestionsNeeded={enableWorkflowMentionSuggestions} defaultProviderId={selectedProviderId} inheritedProviderId={selectedProviderId} inheritedModel={selectedModel} @@ -2297,6 +2304,11 @@ export const CreateTeamDialog = ({ title="Optional launch settings" description="Prompt, safety, and CLI overrides live here when you need them." summary={launchOptionalSummary} + onOpenChange={(isOpen) => { + if (isOpen) { + enableWorkflowMentionSuggestions(); + } + }} >
{selectedProviderId === 'anthropic' ? ( diff --git a/src/renderer/components/team/dialogs/OptionalSettingsSection.tsx b/src/renderer/components/team/dialogs/OptionalSettingsSection.tsx index 7e0355e6..953c4f07 100644 --- a/src/renderer/components/team/dialogs/OptionalSettingsSection.tsx +++ b/src/renderer/components/team/dialogs/OptionalSettingsSection.tsx @@ -10,6 +10,7 @@ interface OptionalSettingsSectionProps { summary?: string[]; defaultOpen?: boolean; className?: string; + onOpenChange?: (open: boolean) => void; children: React.ReactNode; } @@ -61,6 +62,7 @@ export const OptionalSettingsSection = ({ summary = [], defaultOpen = false, className, + onOpenChange, children, }: OptionalSettingsSectionProps): React.JSX.Element => { const [isOpen, setIsOpen] = useState(defaultOpen); @@ -106,6 +108,12 @@ export const OptionalSettingsSection = ({ ? 'color-mix(in srgb, var(--color-text-muted) 64%, var(--color-text) 36%)' : 'color-mix(in srgb, var(--color-text-muted) 54%, white 46%)'; + const handleToggleOpen = (): void => { + const nextOpen = !isOpen; + setIsOpen(nextOpen); + onOpenChange?.(nextOpen); + }; + return (
setIsOpen((prev) => !prev)} + onClick={handleToggleOpen} aria-expanded={isOpen} >
void; lockProviderModel?: boolean; lockRole?: boolean; lockedRoleLabel?: string; @@ -144,6 +145,7 @@ export const MemberDraftRow = ({ mentionSuggestions = [], taskSuggestions, teamSuggestions, + onWorkflowSuggestionsNeeded, lockProviderModel = false, lockRole = false, lockedRoleLabel, @@ -428,6 +430,15 @@ export const MemberDraftRow = ({ effectiveModel?.trim() ?? '', effectiveEffort ); + const toggleWorkflowExpanded = useCallback(() => { + setWorkflowExpanded((prev) => { + const next = !prev; + if (next) { + onWorkflowSuggestionsNeeded?.(); + } + return next; + }); + }, [onWorkflowSuggestionsNeeded]); return (
setWorkflowExpanded((prev) => !prev)} + onClick={toggleWorkflowExpanded} > {!workflowExpanded && workflowDraft.value.trim() ? ( diff --git a/src/renderer/components/team/members/MembersEditorSection.tsx b/src/renderer/components/team/members/MembersEditorSection.tsx index 6c1ad20e..b9aef166 100644 --- a/src/renderer/components/team/members/MembersEditorSection.tsx +++ b/src/renderer/components/team/members/MembersEditorSection.tsx @@ -111,6 +111,8 @@ export interface MembersEditorSectionProps { taskSuggestions?: MentionSuggestion[]; /** Team suggestions for @@team mentions in workflow */ teamSuggestions?: MentionSuggestion[]; + /** Called before workflow mention suggestions are needed. */ + onWorkflowSuggestionsNeeded?: () => void; /** Extra content rendered right below the "Members" label row */ headerExtra?: React.ReactNode; /** When true, hides member rows and action buttons (label + headerExtra still visible) */ @@ -166,6 +168,7 @@ export const MembersEditorSection = ({ projectPath, taskSuggestions, teamSuggestions, + onWorkflowSuggestionsNeeded, headerExtra, hideContent = false, existingMembers, @@ -552,6 +555,7 @@ export const MembersEditorSection = ({ mentionSuggestions={mentionSuggestions} taskSuggestions={taskSuggestions} teamSuggestions={teamSuggestions} + onWorkflowSuggestionsNeeded={onWorkflowSuggestionsNeeded} lockProviderModel={lockProviderModel} lockIdentity={lockExistingMemberIdentity && Boolean(member.originalName?.trim())} identityLockReason={identityLockReason} @@ -604,6 +608,7 @@ export const MembersEditorSection = ({ mentionSuggestions={mentionSuggestions} taskSuggestions={taskSuggestions} teamSuggestions={teamSuggestions} + onWorkflowSuggestionsNeeded={onWorkflowSuggestionsNeeded} lockProviderModel modelLockReason="Removed members are kept for soft delete history. Restore them to edit settings." isRemoved diff --git a/src/renderer/components/team/members/TeamRosterEditorSection.tsx b/src/renderer/components/team/members/TeamRosterEditorSection.tsx index bb2dcf4e..3a7f1a8e 100644 --- a/src/renderer/components/team/members/TeamRosterEditorSection.tsx +++ b/src/renderer/components/team/members/TeamRosterEditorSection.tsx @@ -20,6 +20,7 @@ interface TeamRosterEditorSectionProps { projectPath?: string | null; taskSuggestions?: MentionSuggestion[]; teamSuggestions?: MentionSuggestion[]; + onWorkflowSuggestionsNeeded?: () => void; hideMembersContent?: boolean; existingMembers?: readonly { name: string; color?: string; removedAt?: number | string | null }[]; defaultProviderId?: TeamProviderId; @@ -76,6 +77,7 @@ const TeamRosterEditorSectionImpl = ({ projectPath, taskSuggestions, teamSuggestions, + onWorkflowSuggestionsNeeded, hideMembersContent = false, existingMembers, defaultProviderId = 'anthropic', @@ -153,6 +155,7 @@ const TeamRosterEditorSectionImpl = ({ projectPath={projectPath} taskSuggestions={taskSuggestions} teamSuggestions={teamSuggestions} + onWorkflowSuggestionsNeeded={onWorkflowSuggestionsNeeded} hideContent={hideMembersContent} existingMembers={existingMembers} defaultProviderId={defaultProviderId} diff --git a/src/renderer/hooks/useFileSuggestions.ts b/src/renderer/hooks/useFileSuggestions.ts index f5a8449c..54fec72d 100644 --- a/src/renderer/hooks/useFileSuggestions.ts +++ b/src/renderer/hooks/useFileSuggestions.ts @@ -6,7 +6,7 @@ * Folders are derived from file paths (no extra IPC call needed). */ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { getQuickOpenCache, @@ -190,16 +190,6 @@ export function useFileSuggestions( return onQuickOpenCacheInvalidated(() => setFetchTrigger((n) => n + 1)); }, []); - // Lazy refetch: when dropdown opens and cache is stale, trigger a reload - const prevEnabledRef = useRef(enabled); - useEffect(() => { - if (enabled && !prevEnabledRef.current && projectPath && !getQuickOpenCache(projectPath)) { - // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional trigger on state transition - setFetchTrigger((n) => n + 1); - } - prevEnabledRef.current = enabled; - }, [enabled, projectPath]); - // Load files from API when cache is empty. // Uses project:listFiles (not editor:listFiles) — works without editor being open. const fetchFiles = useCallback( @@ -231,13 +221,14 @@ export function useFileSuggestions( // - effect (projectPath change) useEffect(() => { if (!projectPath) return; + if (!enabled) return; const cached = getQuickOpenCache(projectPath); if (cached) return; // eslint-disable-next-line react-hooks/set-state-in-effect -- setLoading before async fetch is intentional return fetchFiles(projectPath); - }, [projectPath, fetchTrigger, fetchFiles]); + }, [projectPath, enabled, fetchTrigger, fetchFiles]); // Derive folders from file list (memoized) const allFolders = useMemo( diff --git a/src/renderer/hooks/useTaskSuggestions.ts b/src/renderer/hooks/useTaskSuggestions.ts index 892942a1..e5f6f48c 100644 --- a/src/renderer/hooks/useTaskSuggestions.ts +++ b/src/renderer/hooks/useTaskSuggestions.ts @@ -10,12 +10,22 @@ import { getTaskDisplayId } from '@shared/utils/taskIdentity'; import { useShallow } from 'zustand/react/shallow'; import type { MentionSuggestion } from '@renderer/types/mention'; -import type { GlobalTask, TeamTaskWithKanban } from '@shared/types'; +import type { GlobalTask, TeamSummary, TeamTaskWithKanban } from '@shared/types'; + +const EMPTY_GLOBAL_TASKS: GlobalTask[] = []; +const EMPTY_TEAM_TASKS: TeamTaskWithKanban[] = []; +const EMPTY_TEAM_MEMBERS: NonNullable = []; +const EMPTY_TEAM_BY_NAME: Record = {}; +const EMPTY_TASK_SUGGESTIONS: MentionSuggestion[] = []; export interface UseTaskSuggestionsResult { suggestions: MentionSuggestion[]; } +interface UseTaskSuggestionsOptions { + enabled?: boolean; +} + interface TaskWithTeamContext { task: TeamTaskWithKanban | GlobalTask; teamName: string; @@ -60,19 +70,29 @@ function isVisibleTask(task: TeamTaskWithKanban | GlobalTask): boolean { return task.status !== 'deleted' && !task.deletedAt; } -export function useTaskSuggestions(currentTeamName: string | null): UseTaskSuggestionsResult { +export function useTaskSuggestions( + currentTeamName: string | null, + options: UseTaskSuggestionsOptions = {} +): UseTaskSuggestionsResult { + const enabled = options.enabled ?? true; const { globalTasks, currentTeamData, currentTeamMembers, teamByName } = useStore( useShallow((s) => ({ - globalTasks: s.globalTasks, - currentTeamData: currentTeamName ? selectTeamDataForName(s, currentTeamName) : null, - currentTeamMembers: currentTeamName - ? selectResolvedMembersForTeamName(s, currentTeamName) - : [], - teamByName: s.teamByName, + globalTasks: enabled ? s.globalTasks : EMPTY_GLOBAL_TASKS, + currentTeamData: + enabled && currentTeamName ? selectTeamDataForName(s, currentTeamName) : null, + currentTeamMembers: + enabled && currentTeamName + ? selectResolvedMembersForTeamName(s, currentTeamName) + : EMPTY_TEAM_MEMBERS, + teamByName: enabled ? s.teamByName : EMPTY_TEAM_BY_NAME, })) ); const suggestions = useMemo(() => { + if (!enabled) { + return EMPTY_TASK_SUGGESTIONS; + } + const tasks: TaskWithTeamContext[] = []; const seenTaskIds = new Set(); @@ -80,7 +100,10 @@ export function useTaskSuggestions(currentTeamName: string | null): UseTaskSugge const currentTeamSummary = teamByName[currentTeamName]; const currentTeamDisplayName = currentTeamSummary?.displayName || currentTeamName; const currentTeamTasks = - currentTeamData?.tasks ?? globalTasks.filter((task) => task.teamName === currentTeamName); + currentTeamData?.tasks ?? + (currentTeamName + ? globalTasks.filter((task) => task.teamName === currentTeamName) + : EMPTY_TEAM_TASKS); const currentTeamMemberColors = currentTeamMembers.length > 0 ? currentTeamMembers : (currentTeamSummary?.members ?? []); @@ -125,7 +148,7 @@ export function useTaskSuggestions(currentTeamName: string | null): UseTaskSugge }); return tasks.map(buildTaskSuggestion); - }, [currentTeamData, currentTeamMembers, currentTeamName, globalTasks, teamByName]); + }, [currentTeamData, currentTeamMembers, currentTeamName, enabled, globalTasks, teamByName]); return { suggestions }; } diff --git a/src/renderer/hooks/useTeamSuggestions.ts b/src/renderer/hooks/useTeamSuggestions.ts index 9b376167..a71e8711 100644 --- a/src/renderer/hooks/useTeamSuggestions.ts +++ b/src/renderer/hooks/useTeamSuggestions.ts @@ -15,19 +15,31 @@ import { useStore } from '@renderer/store'; import { useShallow } from 'zustand/react/shallow'; import type { MentionSuggestion } from '@renderer/types/mention'; +import type { TeamSummary } from '@shared/types'; export interface UseTeamSuggestionsResult { suggestions: MentionSuggestion[]; loading: boolean; } +interface UseTeamSuggestionsOptions { + enabled?: boolean; +} + +const EMPTY_TEAMS: TeamSummary[] = []; +const EMPTY_TEAM_SUGGESTIONS: MentionSuggestion[] = []; + /** * Returns team MentionSuggestion[] sorted by online status (online first). * * @param currentTeamName - The current team name to exclude from suggestions */ -export function useTeamSuggestions(currentTeamName: string | null): UseTeamSuggestionsResult { - const teams = useStore(useShallow((s) => s.teams)); +export function useTeamSuggestions( + currentTeamName: string | null, + options: UseTeamSuggestionsOptions = {} +): UseTeamSuggestionsResult { + const enabled = options.enabled ?? true; + const teams = useStore(useShallow((s) => (enabled ? s.teams : EMPTY_TEAMS))); const [aliveTeams, setAliveTeams] = useState>(new Set()); const [loading, setLoading] = useState(false); @@ -45,11 +57,19 @@ export function useTeamSuggestions(currentTeamName: string | null): UseTeamSugge // Fetch on mount and when teams list changes useEffect(() => { + if (!enabled) { + setLoading(false); + return; + } void fetchAlive(); - }, [fetchAlive, teams]); + }, [enabled, fetchAlive, teams]); // Build suggestion list sorted: online first, then offline const suggestions = useMemo(() => { + if (!enabled) { + return EMPTY_TEAM_SUGGESTIONS; + } + const nonDeleted = teams.filter((t) => !t.deletedAt && t.teamName !== currentTeamName); const result: MentionSuggestion[] = nonDeleted.map((t) => { @@ -72,7 +92,7 @@ export function useTeamSuggestions(currentTeamName: string | null): UseTeamSugge }); return result; - }, [teams, currentTeamName, aliveTeams]); + }, [enabled, teams, currentTeamName, aliveTeams]); return { suggestions, loading }; }