From d13dfd1be40776f2622b6de16d2f5861d43d2375 Mon Sep 17 00:00:00 2001 From: Tim Glaser Date: Mon, 18 May 2026 23:22:16 +0000 Subject: [PATCH] feat(task-input): cache cloud repositories so picker renders instantly The cloud repo picker waited on a full integrations + per-installation repo fetch every time TaskInput mounted, even though the last-used repo was already known. Persist the repository map in settingsStore and use it as a stand-in while live data loads, so clicking "new task" shows the previously selected repo immediately and only kicks the network request in the background. Generated-By: PostHog Code Task-Id: 0b2985a5-f92f-40ad-b3b8-2dc44366566a --- .../settings/stores/settingsStore.test.ts | 51 ++++++++++++ .../features/settings/stores/settingsStore.ts | 13 +++ .../src/renderer/hooks/useIntegrations.ts | 81 ++++++++++++++++--- 3 files changed, 134 insertions(+), 11 deletions(-) diff --git a/apps/code/src/renderer/features/settings/stores/settingsStore.test.ts b/apps/code/src/renderer/features/settings/stores/settingsStore.test.ts index 132bc0261..bb3ecb6e1 100644 --- a/apps/code/src/renderer/features/settings/stores/settingsStore.test.ts +++ b/apps/code/src/renderer/features/settings/stores/settingsStore.test.ts @@ -30,6 +30,7 @@ describe("feature settingsStore cloud selections", () => { useSettingsStore.setState({ allowBypassPermissions: false, lastUsedCloudRepository: null, + cachedCloudRepositoryMap: {}, }); }); @@ -67,6 +68,56 @@ describe("feature settingsStore cloud selections", () => { ); }); + it("persists the cached cloud repository map", async () => { + useSettingsStore.getState().setCachedCloudRepositoryMap({ + "posthog/posthog": { + userIntegrationId: "user-1", + installationId: "install-1", + }, + }); + + await vi.waitFor(() => { + expect(setItem).toHaveBeenCalled(); + }); + + const lastCall = setItem.mock.calls[setItem.mock.calls.length - 1]; + const persisted = JSON.parse(lastCall[0].value); + + expect(persisted.state.cachedCloudRepositoryMap).toEqual({ + "posthog/posthog": { + userIntegrationId: "user-1", + installationId: "install-1", + }, + }); + }); + + it("rehydrates the cached cloud repository map", async () => { + getItem.mockResolvedValue( + JSON.stringify({ + state: { + cachedCloudRepositoryMap: { + "posthog/code": { + userIntegrationId: "user-2", + installationId: "install-2", + }, + }, + }, + version: 0, + }), + ); + + useSettingsStore.setState({ cachedCloudRepositoryMap: {} }); + + await useSettingsStore.persist.rehydrate(); + + expect(useSettingsStore.getState().cachedCloudRepositoryMap).toEqual({ + "posthog/code": { + userIntegrationId: "user-2", + installationId: "install-2", + }, + }); + }); + it("rehydrates the unsafe mode toggle", async () => { getItem.mockResolvedValue( JSON.stringify({ diff --git a/apps/code/src/renderer/features/settings/stores/settingsStore.ts b/apps/code/src/renderer/features/settings/stores/settingsStore.ts index 1ea969bf2..550e1d517 100644 --- a/apps/code/src/renderer/features/settings/stores/settingsStore.ts +++ b/apps/code/src/renderer/features/settings/stores/settingsStore.ts @@ -25,6 +25,11 @@ export type AgentAdapter = "claude" | "codex"; export type AutoConvertLongText = "off" | "1000" | "2500" | "5000" | "10000"; export type DefaultInitialTaskMode = "plan" | "last_used"; +export interface CachedCloudRepositoryRef { + userIntegrationId: string; + installationId: string; +} + export interface HintState { count: number; learned: boolean; @@ -40,6 +45,7 @@ interface SettingsStore { lastUsedModel: string | null; lastUsedReasoningEffort: string | null; lastUsedCloudRepository: string | null; + cachedCloudRepositoryMap: Record; lastUsedEnvironments: Record; desktopNotifications: boolean; dockBadgeNotifications: boolean; @@ -74,6 +80,9 @@ interface SettingsStore { setLastUsedModel: (model: string) => void; setLastUsedReasoningEffort: (effort: string) => void; setLastUsedCloudRepository: (repo: string | null) => void; + setCachedCloudRepositoryMap: ( + map: Record, + ) => void; setLastUsedEnvironment: ( repoPath: string, environmentId: string | null, @@ -107,6 +116,7 @@ export const useSettingsStore = create()( lastUsedModel: null, lastUsedReasoningEffort: null, lastUsedCloudRepository: null, + cachedCloudRepositoryMap: {}, lastUsedEnvironments: {}, desktopNotifications: true, dockBadgeNotifications: true, @@ -166,6 +176,8 @@ export const useSettingsStore = create()( set({ lastUsedReasoningEffort: effort }), setLastUsedCloudRepository: (repo) => set({ lastUsedCloudRepository: repo }), + setCachedCloudRepositoryMap: (map) => + set({ cachedCloudRepositoryMap: map }), setLastUsedEnvironment: (repoPath, environmentId) => set((state) => { const next = { ...state.lastUsedEnvironments }; @@ -215,6 +227,7 @@ export const useSettingsStore = create()( lastUsedModel: state.lastUsedModel, lastUsedReasoningEffort: state.lastUsedReasoningEffort, lastUsedCloudRepository: state.lastUsedCloudRepository, + cachedCloudRepositoryMap: state.cachedCloudRepositoryMap, lastUsedEnvironments: state.lastUsedEnvironments, desktopNotifications: state.desktopNotifications, dockBadgeNotifications: state.dockBadgeNotifications, diff --git a/apps/code/src/renderer/hooks/useIntegrations.ts b/apps/code/src/renderer/hooks/useIntegrations.ts index c6d9ad9f6..4cba6dbb1 100644 --- a/apps/code/src/renderer/hooks/useIntegrations.ts +++ b/apps/code/src/renderer/hooks/useIntegrations.ts @@ -5,6 +5,7 @@ import { useIntegrationSelectors, useIntegrationStore, } from "@features/integrations/stores/integrationStore"; +import { useSettingsStore } from "@features/settings/stores/settingsStore"; import type { UserGitHubIntegration } from "@renderer/api/posthogClient"; import { useQueries, useQueryClient } from "@tanstack/react-query"; import { @@ -503,6 +504,13 @@ export function useUserRepositoryIntegration() { useUserGithubIntegrations(); const [isRefreshingRepos, setIsRefreshingRepos] = useState(false); + const cachedCloudRepositoryMap = useSettingsStore( + (s) => s.cachedCloudRepositoryMap, + ); + const setCachedCloudRepositoryMap = useSettingsStore( + (s) => s.setCachedCloudRepositoryMap, + ); + const { repositoryMap, reposByInstallationId, @@ -510,25 +518,68 @@ export function useUserRepositoryIntegration() { failedInstallationIds, } = useAllUserGithubRepositories(githubIntegrations); + // Keep the persisted cache in sync with the freshly fetched map so that the + // next cold start can render the picker without waiting on the network. + // Clear the cache when the user has no integrations; otherwise only write + // once everything has loaded so we don't blow it away with partial data. + useEffect(() => { + if (integrationsPending) return; + if (githubIntegrations.length === 0) { + if (Object.keys(cachedCloudRepositoryMap).length > 0) { + setCachedCloudRepositoryMap({}); + } + return; + } + if (reposPending) return; + if (Object.keys(repositoryMap).length === 0) return; + setCachedCloudRepositoryMap(repositoryMap); + }, [ + integrationsPending, + reposPending, + githubIntegrations.length, + repositoryMap, + cachedCloudRepositoryMap, + setCachedCloudRepositoryMap, + ]); + + // Use the cached map as a stand-in while the live queries are loading so + // the picker can render the last-used repo immediately on a cold start. + const effectiveRepositoryMap = useMemo(() => { + const liveLoading = integrationsPending || reposPending; + const hasLiveData = Object.keys(repositoryMap).length > 0; + if (hasLiveData) return repositoryMap; + if (liveLoading && Object.keys(cachedCloudRepositoryMap).length > 0) { + return cachedCloudRepositoryMap; + } + return repositoryMap; + }, [ + integrationsPending, + reposPending, + repositoryMap, + cachedCloudRepositoryMap, + ]); + const repositories = useMemo( - () => Object.keys(repositoryMap), - [repositoryMap], + () => Object.keys(effectiveRepositoryMap), + [effectiveRepositoryMap], ); const getUserIntegrationIdForRepo = useCallback( (repoKey: string) => - repositoryMap[repoKey?.toLowerCase()]?.userIntegrationId, - [repositoryMap], + effectiveRepositoryMap[repoKey?.toLowerCase()]?.userIntegrationId, + [effectiveRepositoryMap], ); const getInstallationIdForRepo = useCallback( - (repoKey: string) => repositoryMap[repoKey?.toLowerCase()]?.installationId, - [repositoryMap], + (repoKey: string) => + effectiveRepositoryMap[repoKey?.toLowerCase()]?.installationId, + [effectiveRepositoryMap], ); const isRepoInIntegration = useCallback( - (repoKey: string) => !repoKey || repoKey.toLowerCase() in repositoryMap, - [repositoryMap], + (repoKey: string) => + !repoKey || repoKey.toLowerCase() in effectiveRepositoryMap, + [effectiveRepositoryMap], ); const refreshRepositories = useCallback(async () => { @@ -564,15 +615,23 @@ export function useUserRepositoryIntegration() { } }, [client, githubIntegrations, queryClient]); + const liveLoading = integrationsPending || reposPending; + const servingFromCache = + liveLoading && + Object.keys(repositoryMap).length === 0 && + Object.keys(cachedCloudRepositoryMap).length > 0; + return { repositories, getUserIntegrationIdForRepo, getInstallationIdForRepo, isRepoInIntegration, - isLoadingRepos: integrationsPending || reposPending, - isRefreshingRepos, + isLoadingRepos: liveLoading && !servingFromCache, + isRefreshingRepos: isRefreshingRepos || servingFromCache, refreshRepositories, - hasGithubIntegration: githubIntegrations.length > 0, + hasGithubIntegration: + githubIntegrations.length > 0 || + (integrationsPending && Object.keys(cachedCloudRepositoryMap).length > 0), failedInstallationIds, reposByInstallationId, };