diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 7ac6a5bd36c5..13fec7f9a7e3 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -371,16 +371,12 @@ function notifyThemeSaveFailure(): void { ); } -// These helpers keep "use memo" until the palette's item building moves out of this -// component: inlined, each result reaches the returned through -// the action and thread item arrays built after the browse hooks, and the compiler -// leaves all of them uncached. -function useThemeCards( - customThemes: ReturnType, - environmentThemes: ReturnType, -) { - // oxlint-disable-next-line t3code/no-manual-memo -- Kept until the palette's item building moves out of this component (see above). - "use memo"; +// Each helper reads its own source hook so the compiler caches the derived value on that +// source. Taking the source as an argument instead leaves the helper hook-free, and the +// compiler then treats it as a plain function and caches nothing. +function useThemeCards() { + const customThemes = useCustomThemes(); + const environmentThemes = useEnvironmentThemeDefinitions(); const seen = new Set(); return [ ...STANDARD_THEME_CARDS.map((card) => ({ ...card, id: null })), @@ -394,21 +390,23 @@ function useThemeCards( ]; } -function useThreadContentMatchByKey(matches: ReturnType["matches"]) { - // oxlint-disable-next-line t3code/no-manual-memo -- Kept until the palette's item building moves out of this component (see above). - "use memo"; - return new Map( - matches.flatMap((match) => +function useThreadSearchWithContentMatches( + environmentIds: ReadonlyArray, + query: string, +) { + const threadSearch = useThreadSearch(environmentIds, query); + const contentMatchByKey = new Map( + threadSearch.matches.flatMap((match) => match.source === "user" || match.source === "assistant" ? [[threadSearchMatchKey(match), match] as const] : [], ), ); + return { threadSearch, contentMatchByKey }; } -function useEnvironmentLookups(environments: ReturnType["environments"]) { - // oxlint-disable-next-line t3code/no-manual-memo -- Kept until the palette's item building moves out of this component (see above). - "use memo"; +function useEnvironmentLookups() { + const { environments } = useEnvironments(); const environmentLabelById = new Map( environments.map((environment) => [environment.environmentId, environment.label] as const), ); @@ -429,9 +427,8 @@ function useEnvironmentLookups(environments: ReturnType[ return { environmentLabelById, projectEnvironmentLocationById }; } -function useProjectLookups(projects: ReturnType) { - // oxlint-disable-next-line t3code/no-manual-memo -- Kept until the palette's item building moves out of this component (see above). - "use memo"; +function useProjectLookups() { + const projects = useProjects(); const projectCwdById = new Map( projects.map((project) => [project.id, project.workspaceRoot]), ); @@ -450,25 +447,13 @@ const selectCommandPaletteClientSettings = (settings: ClientSettings) => ({ sidebarProjectSortOrder: settings.sidebarProjectSortOrder, sidebarThreadSortOrder: settings.sidebarThreadSortOrder, }); -type CommandPaletteClientSettings = ReturnType; - -function useSortedProjectGroups({ - projects, - projectOrder, - clientSettings, - primaryEnvironmentId, - environmentLabelById, - threads, -}: { - readonly projects: ReturnType; - readonly projectOrder: ReturnType["projectOrder"]; - readonly clientSettings: CommandPaletteClientSettings; - readonly primaryEnvironmentId: ReturnType; - readonly environmentLabelById: ReadonlyMap; - readonly threads: ReturnType; -}) { - // oxlint-disable-next-line t3code/no-manual-memo -- Kept until the palette's item building moves out of this component (see above). - "use memo"; + +function useSortedProjectGroups(environmentLabelById: ReadonlyMap) { + const projects = useProjects(); + const threads = useThreadShells(); + const projectOrder = useUiStateStore((store) => store.projectOrder); + const clientSettings = useClientSettings(selectCommandPaletteClientSettings); + const primaryEnvironmentId = usePrimaryEnvironmentId(); const projectGroupingSettings = { sidebarProjectGroupingMode: clientSettings.sidebarProjectGroupingMode, sidebarProjectGroupingOverrides: clientSettings.sidebarProjectGroupingOverrides, @@ -806,7 +791,6 @@ function OpenCommandPaletteDialog(props: { ); } }; - const projectOrder = useUiStateStore((store) => store.projectOrder); const threads = useThreadShells(); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const { @@ -818,9 +802,7 @@ function OpenCommandPaletteDialog(props: { setTheme, setThemeHalf, } = useTheme(); - const customThemes = useCustomThemes(); - const environmentThemes = useEnvironmentThemeDefinitions(); - const themeCards = useThemeCards(customThemes, environmentThemes); + const themeCards = useThemeCards(); const providers = useAtomValue(primaryServerProvidersAtom); const providersByEnvironment = useEnvironmentProviders(); const providerEntryByEnvironmentAndInstanceId = new Map( @@ -844,8 +826,8 @@ function OpenCommandPaletteDialog(props: { .filter((environment) => environment.connection.phase === "connected") .map((environment) => environment.environmentId); const threadSearchQuery = currentView === null && !isActionsOnly ? deferredQuery : ""; - const threadSearch = useThreadSearch(environmentIds, threadSearchQuery); - const threadContentMatchByKey = useThreadContentMatchByKey(threadSearch.matches); + const { threadSearch, contentMatchByKey: threadContentMatchByKey } = + useThreadSearchWithContentMatches(environmentIds, threadSearchQuery); const [browseGeneration, setBrowseGeneration] = useState(0); const [browseNavigation] = useState(createBrowseNavigationCoordinator); const [addProjectEnvironmentId, setAddProjectEnvironmentId] = useState( @@ -868,16 +850,8 @@ function OpenCommandPaletteDialog(props: { const cloneLookupGeneration = useRef(0); const [isRemoteProjectLookingUp, setIsRemoteProjectLookingUp] = useState(false); const [isRemoteProjectCloning, setIsRemoteProjectCloning] = useState(false); - const { environmentLabelById, projectEnvironmentLocationById } = - useEnvironmentLookups(environments); - const projectGroups = useSortedProjectGroups({ - projects, - projectOrder, - clientSettings, - primaryEnvironmentId, - environmentLabelById, - threads, - }); + const { environmentLabelById, projectEnvironmentLocationById } = useEnvironmentLookups(); + const projectGroups = useSortedProjectGroups(environmentLabelById); const contextualProjectRef = resolveThreadActionProjectRef({ activeDraftThread, activeThread: activeThread ?? undefined, @@ -1000,7 +974,7 @@ function OpenCommandPaletteDialog(props: { return ensureBrowseDirectoryPath(baseDirectory); }; - const { projectCwdById, projectByKey, projectTitleById } = useProjectLookups(projects); + const { projectCwdById, projectByKey, projectTitleById } = useProjectLookups(); const activeThreadId = activeThread?.id; const supportsThreadFind = diff --git a/docs/internals/web-ui.md b/docs/internals/web-ui.md index c1975a4f98e3..31b98c5c2c1d 100644 --- a/docs/internals/web-ui.md +++ b/docs/internals/web-ui.md @@ -66,7 +66,9 @@ Every file under `apps/web/src` must compile. Lint makes those rules errors ther no upstream rule bans them outright. It also rejects `"use memo"`. The compiler withholds a cache scope when a value reaches plain function calls on both sides of a hook call. A `"use memo"` helper forces one, but hides the ordering problem. Fix the order instead: plain uses of a value go -before the hook, or the helper is inlined. Lint misses some bailouts, notably `try` with `finally` and +before the hook, or the helper is inlined. A `use*` helper that calls no hook is compiled as a +plain function and caches nothing, so a derived-value helper reads its source hook itself instead +of taking the value as an argument. Lint misses some bailouts, notably `try` with `finally` and `throw` inside `try`, so [`reactCompilerCoverage.test.ts`](../../apps/web/src/reactCompilerCoverage.test.ts) compiles every file and fails on any skip. Async handlers rarely need `try`: an atom command settles into a result, so cleanup can follow the `await`. `useStableCallback` still