perf(renderer): defer launch dialog heavy work
This commit is contained in:
parent
174ad83b47
commit
53a4c0e9e6
5 changed files with 169 additions and 105 deletions
|
|
@ -107,7 +107,7 @@ import {
|
|||
isDeletedProjectPathSelection,
|
||||
isSelectableProjectPathProject,
|
||||
} from './projectPathOptions';
|
||||
import { loadProjectPathProjects, type ProjectPathProject } from './projectPathProjects';
|
||||
import { loadProjectPathProjects, syntheticProjectFromPath } from './projectPathProjects';
|
||||
import { ProjectPathSelector } from './ProjectPathSelector';
|
||||
import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey';
|
||||
import {
|
||||
|
|
@ -157,6 +157,7 @@ import {
|
|||
} from './WorktreeGitReadinessBanner';
|
||||
|
||||
import type { ActiveTeamRef } from './CreateTeamDialog';
|
||||
import type { ProjectPathProject } from './projectPathProjects';
|
||||
import type { MemberDraft } from '@renderer/components/team/members/membersEditorTypes';
|
||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||
import type {
|
||||
|
|
@ -433,6 +434,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
const [projects, setProjects] = useState<ProjectPathProject[]>([]);
|
||||
const [projectsLoading, setProjectsLoading] = useState(false);
|
||||
const [projectsError, setProjectsError] = useState<string | null>(null);
|
||||
const [projectsLoadRequested, setProjectsLoadRequested] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
|
|
@ -1847,9 +1849,28 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
|
||||
const repositoryGroups = useStore(useShallow((s) => s.repositoryGroups));
|
||||
const defaultProjectPath = isLaunchMode ? props.defaultProjectPath : undefined;
|
||||
const shouldDeferProjectListLoad =
|
||||
isLaunchMode &&
|
||||
!projectsLoadRequested &&
|
||||
typeof defaultProjectPath === 'string' &&
|
||||
defaultProjectPath.length > 0 &&
|
||||
!isEphemeralProjectPath(defaultProjectPath);
|
||||
const requestProjectListLoad = useCallback(() => {
|
||||
setProjectsLoadRequested(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (!open) {
|
||||
setProjectsLoadRequested(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldDeferProjectListLoad && defaultProjectPath) {
|
||||
setProjects([syntheticProjectFromPath(defaultProjectPath)]);
|
||||
setProjectsLoading(false);
|
||||
setProjectsError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setProjectsLoading(true);
|
||||
setProjectsError(null);
|
||||
|
|
@ -1878,7 +1899,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open, repositoryGroups, defaultProjectPath, t]);
|
||||
}, [open, repositoryGroups, defaultProjectPath, shouldDeferProjectListLoad, t]);
|
||||
|
||||
// Pre-select defaultProjectPath (launch mode) or first project
|
||||
|
||||
|
|
@ -2699,6 +2720,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
projects={projects}
|
||||
projectsLoading={projectsLoading}
|
||||
projectsError={projectsError}
|
||||
onProjectsDropdownOpen={requestProjectListLoad}
|
||||
/>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ interface ProjectPathSelectorProps {
|
|||
projectsLoading: boolean;
|
||||
projectsError: string | null;
|
||||
fieldError?: string | null;
|
||||
onProjectsDropdownOpen?: () => void;
|
||||
}
|
||||
|
||||
export const ProjectPathSelector = ({
|
||||
|
|
@ -145,6 +146,7 @@ export const ProjectPathSelector = ({
|
|||
projectsLoading,
|
||||
projectsError,
|
||||
fieldError,
|
||||
onProjectsDropdownOpen,
|
||||
}: ProjectPathSelectorProps): React.JSX.Element => {
|
||||
const { t } = useAppTranslation('team');
|
||||
const projectOptions = React.useMemo(
|
||||
|
|
@ -202,6 +204,11 @@ export const ProjectPathSelector = ({
|
|||
searchPlaceholder={t('projectPath.searchPlaceholder')}
|
||||
emptyMessage={t('projectPath.empty')}
|
||||
disabled={projectsLoading || projectOptions.length === 0}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (isOpen) {
|
||||
onProjectsDropdownOpen?.();
|
||||
}
|
||||
}}
|
||||
renderTriggerLabel={(option) => (
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
<ProjectSourceBadge source={getOptionSource(option)} />
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ function repositoryWorktreeToProject(
|
|||
};
|
||||
}
|
||||
|
||||
function syntheticProjectFromPath(projectPath: string): Project {
|
||||
export function syntheticProjectFromPath(projectPath: string): Project {
|
||||
return {
|
||||
id: projectPath.replace(/[/\\]/g, '-'),
|
||||
path: projectPath,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ interface ComboboxProps {
|
|||
resetLabel?: string;
|
||||
/** Called when the user clicks the reset item. */
|
||||
onReset?: () => void;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const Combobox = ({
|
||||
|
|
@ -46,15 +47,23 @@ export const Combobox = ({
|
|||
renderTriggerLabel,
|
||||
resetLabel,
|
||||
onReset,
|
||||
onOpenChange,
|
||||
}: ComboboxProps): React.JSX.Element => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [search, setSearch] = React.useState('');
|
||||
const listboxId = React.useId();
|
||||
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
const handleOpenChange = React.useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
setOpen(nextOpen);
|
||||
onOpenChange?.(nextOpen);
|
||||
},
|
||||
[onOpenChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -75,6 +84,7 @@ export const Combobox = ({
|
|||
<ChevronsUpDown className="ml-2 size-3.5 shrink-0 opacity-50" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
{open ? (
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
|
|
@ -172,6 +182,7 @@ export const Combobox = ({
|
|||
</CommandPrimitive.List>
|
||||
</CommandPrimitive>
|
||||
</PopoverContent>
|
||||
) : null}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ type RuntimeAwareProviderStatus = Pick<
|
|||
CliProviderStatus,
|
||||
'providerId' | 'authMethod' | 'backend' | 'modelCatalog'
|
||||
>;
|
||||
type RuntimeModelCatalog = NonNullable<RuntimeAwareProviderStatus['modelCatalog']>;
|
||||
type RuntimeCatalogModel = RuntimeModelCatalog['models'][number];
|
||||
|
||||
export interface TeamProviderModelOption {
|
||||
value: string;
|
||||
|
|
@ -290,21 +292,43 @@ export function getTeamModelLabel(model: string | undefined): string | undefined
|
|||
return formatParsedClaudeModelLabel(labelTarget) ?? labelTarget;
|
||||
}
|
||||
|
||||
const runtimeCatalogModelIndexCache = new WeakMap<
|
||||
RuntimeModelCatalog,
|
||||
Map<string, RuntimeCatalogModel>
|
||||
>();
|
||||
|
||||
function getRuntimeCatalogModelIndex(
|
||||
catalog: RuntimeModelCatalog
|
||||
): Map<string, RuntimeCatalogModel> {
|
||||
const cached = runtimeCatalogModelIndexCache.get(catalog);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const index = new Map<string, RuntimeCatalogModel>();
|
||||
for (const item of catalog.models) {
|
||||
if (item.launchModel && !index.has(item.launchModel)) {
|
||||
index.set(item.launchModel, item);
|
||||
}
|
||||
if (item.id && !index.has(item.id)) {
|
||||
index.set(item.id, item);
|
||||
}
|
||||
}
|
||||
runtimeCatalogModelIndexCache.set(catalog, index);
|
||||
return index;
|
||||
}
|
||||
|
||||
function getRuntimeCatalogModel(
|
||||
providerId: SupportedProviderId | undefined,
|
||||
model: string | undefined,
|
||||
providerStatus?: RuntimeAwareProviderStatus | null
|
||||
): NonNullable<RuntimeAwareProviderStatus['modelCatalog']>['models'][number] | null {
|
||||
): RuntimeCatalogModel | null {
|
||||
const trimmed = model?.trim();
|
||||
if (!providerId || !trimmed || providerStatus?.modelCatalog?.providerId !== providerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
providerStatus.modelCatalog.models.find(
|
||||
(item) => item.launchModel === trimmed || item.id === trimmed
|
||||
) ?? null
|
||||
);
|
||||
return getRuntimeCatalogModelIndex(providerStatus.modelCatalog).get(trimmed) ?? null;
|
||||
}
|
||||
|
||||
export function getTeamModelBadgeLabel(
|
||||
|
|
|
|||
Loading…
Reference in a new issue