From 5e3e77bf65517aea84d69e691b651ed79b34d8ac Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 28 May 2026 22:03:40 +0300 Subject: [PATCH] perf(team): reduce render hot path lookups --- src/renderer/utils/mentionLinkify.ts | 10 +++--- src/renderer/utils/teamModelCatalog.ts | 49 +++++++++++++++++++++----- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/renderer/utils/mentionLinkify.ts b/src/renderer/utils/mentionLinkify.ts index 452b5660..0ea69c18 100644 --- a/src/renderer/utils/mentionLinkify.ts +++ b/src/renderer/utils/mentionLinkify.ts @@ -19,10 +19,11 @@ export function linkifyMentionsInMarkdown( text: string, memberColorMap: Map ): string { - if (memberColorMap.size === 0) return text; + if (memberColorMap.size === 0 || !text.includes('@')) return text; // Sort by name length descending for greedy matching const names = [...memberColorMap.keys()].sort((a, b) => b.length - a.length); + const canonicalNameByLower = new Map(names.map((name) => [name.toLowerCase(), name])); const escaped = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); const pattern = new RegExp( // eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for RegExp @@ -32,7 +33,7 @@ export function linkifyMentionsInMarkdown( return text.replace(pattern, (_match: string, prefix: string, name: string) => { // Find the canonical name (case-insensitive lookup) - const canonical = names.find((n) => n.toLowerCase() === name.toLowerCase()) ?? name; + const canonical = canonicalNameByLower.get(name.toLowerCase()) ?? name; const color = memberColorMap.get(canonical) ?? ''; return `${prefix}[@${canonical}](mention://${encodeURIComponent(color)}/${encodeURIComponent(canonical)})`; }); @@ -51,10 +52,11 @@ export function linkifyTeamMentionsInMarkdown( teamNames: ReadonlySet | readonly string[] ): string { const names: readonly string[] = Array.isArray(teamNames) ? teamNames : [...teamNames]; - if (names.length === 0) return text; + if (names.length === 0 || !text.includes('@')) return text; // Sort by name length descending for greedy matching const sorted = [...names].sort((a, b) => b.length - a.length); + const canonicalNameByLower = new Map(sorted.map((name) => [name.toLowerCase(), name])); const escaped = sorted.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); const pattern = new RegExp( // eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for RegExp @@ -63,7 +65,7 @@ export function linkifyTeamMentionsInMarkdown( ); return text.replace(pattern, (_match: string, prefix: string, name: string) => { - const canonical = sorted.find((n) => n.toLowerCase() === name.toLowerCase()) ?? name; + const canonical = canonicalNameByLower.get(name.toLowerCase()) ?? name; return `${prefix}[${canonical}](team://${encodeURIComponent(canonical)})`; }); } diff --git a/src/renderer/utils/teamModelCatalog.ts b/src/renderer/utils/teamModelCatalog.ts index 81218262..d78cc6e8 100644 --- a/src/renderer/utils/teamModelCatalog.ts +++ b/src/renderer/utils/teamModelCatalog.ts @@ -19,6 +19,8 @@ type RuntimeAwareProviderStatus = Pick< CliProviderStatus, 'providerId' | 'authMethod' | 'backend' | 'modelCatalog' >; +type RuntimeAwareModelCatalog = NonNullable; +type RuntimeAwareCatalogModel = RuntimeAwareModelCatalog['models'][number]; export interface TeamProviderModelOption { value: string; @@ -216,6 +218,34 @@ const SUPPORTED_ANTHROPIC_TEAM_MODELS = new Set([ 'claude-haiku-4-5-20251001', ]); +const runtimeCatalogModelIndexCache = new WeakMap< + RuntimeAwareModelCatalog, + Map +>(); + +function getRuntimeCatalogModelIndex( + modelCatalog: RuntimeAwareModelCatalog +): Map { + const cached = runtimeCatalogModelIndexCache.get(modelCatalog); + if (cached) { + return cached; + } + + const index = new Map(); + for (const model of modelCatalog.models) { + const launchModel = model.launchModel.trim(); + if (launchModel) { + index.set(launchModel, model); + } + const id = model.id.trim(); + if (id) { + index.set(id, model); + } + } + runtimeCatalogModelIndexCache.set(modelCatalog, index); + return index; +} + export function isSupportedAnthropicTeamModel(model: string | undefined): boolean { const trimmed = model?.trim(); if (!trimmed) { @@ -294,17 +324,13 @@ function getRuntimeCatalogModel( providerId: SupportedProviderId | undefined, model: string | undefined, providerStatus?: RuntimeAwareProviderStatus | null -): NonNullable['models'][number] | null { +): RuntimeAwareCatalogModel | 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( @@ -456,11 +482,18 @@ export function sortTeamProviderModels( return sorted; } + const freeByModel = new Map( + sorted.map((model) => [ + model, + isFreeOpenCodeModelForOrdering(providerId, model, providerStatus), + ]) + ); + return sorted .map((model, index) => ({ model, index })) .sort((left, right) => { - const leftFree = isFreeOpenCodeModelForOrdering(providerId, left.model, providerStatus); - const rightFree = isFreeOpenCodeModelForOrdering(providerId, right.model, providerStatus); + const leftFree = freeByModel.get(left.model) ?? false; + const rightFree = freeByModel.get(right.model) ?? false; if (leftFree !== rightFree) { return leftFree ? -1 : 1; }