perf(team): lazy load create team suggestions
This commit is contained in:
parent
f6b2bc4cec
commit
bc8d47aaa2
8 changed files with 106 additions and 33 deletions
|
|
@ -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<TeamProviderId | null>(null);
|
||||
const [workflowMentionSuggestionsEnabled, setWorkflowMentionSuggestionsEnabled] = useState(false);
|
||||
const prepareRequestSeqRef = useRef(0);
|
||||
const prepareIdleHandlesRef = useRef(new Set<ScheduledIdleHandle>());
|
||||
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();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{selectedProviderId === 'anthropic' ? (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -119,7 +127,7 @@ export const OptionalSettingsSection = ({
|
|||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-3 p-2.5 text-left transition-colors hover:bg-[var(--color-surface-raised)]"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
onClick={handleToggleOpen}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ interface MemberDraftRowProps {
|
|||
mentionSuggestions?: MentionSuggestion[];
|
||||
taskSuggestions?: MentionSuggestion[];
|
||||
teamSuggestions?: MentionSuggestion[];
|
||||
onWorkflowSuggestionsNeeded?: () => 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 (
|
||||
<div
|
||||
|
|
@ -604,7 +615,7 @@ export const MemberDraftRow = ({
|
|||
aria-label={workflowTooltipText}
|
||||
aria-expanded={workflowExpanded}
|
||||
disabled={isRemoved}
|
||||
onClick={() => setWorkflowExpanded((prev) => !prev)}
|
||||
onClick={toggleWorkflowExpanded}
|
||||
>
|
||||
<WorkflowIcon className="size-3.5" />
|
||||
{!workflowExpanded && workflowDraft.value.trim() ? (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<TeamSummary['members']> = [];
|
||||
const EMPTY_TEAM_BY_NAME: Record<string, TeamSummary> = {};
|
||||
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<MentionSuggestion[]>(() => {
|
||||
if (!enabled) {
|
||||
return EMPTY_TASK_SUGGESTIONS;
|
||||
}
|
||||
|
||||
const tasks: TaskWithTeamContext[] = [];
|
||||
const seenTaskIds = new Set<string>();
|
||||
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Set<string>>(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<MentionSuggestion[]>(() => {
|
||||
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 };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue