Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("feature settingsStore cloud selections", () => {
useSettingsStore.setState({
allowBypassPermissions: false,
lastUsedCloudRepository: null,
cachedCloudRepositoryMap: {},
});
});

Expand Down Expand Up @@ -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({
Expand Down
13 changes: 13 additions & 0 deletions apps/code/src/renderer/features/settings/stores/settingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,6 +45,7 @@ interface SettingsStore {
lastUsedModel: string | null;
lastUsedReasoningEffort: string | null;
lastUsedCloudRepository: string | null;
cachedCloudRepositoryMap: Record<string, CachedCloudRepositoryRef>;
lastUsedEnvironments: Record<string, string>;
desktopNotifications: boolean;
dockBadgeNotifications: boolean;
Expand Down Expand Up @@ -74,6 +80,9 @@ interface SettingsStore {
setLastUsedModel: (model: string) => void;
setLastUsedReasoningEffort: (effort: string) => void;
setLastUsedCloudRepository: (repo: string | null) => void;
setCachedCloudRepositoryMap: (
map: Record<string, CachedCloudRepositoryRef>,
) => void;
setLastUsedEnvironment: (
repoPath: string,
environmentId: string | null,
Expand Down Expand Up @@ -107,6 +116,7 @@ export const useSettingsStore = create<SettingsStore>()(
lastUsedModel: null,
lastUsedReasoningEffort: null,
lastUsedCloudRepository: null,
cachedCloudRepositoryMap: {},
lastUsedEnvironments: {},
desktopNotifications: true,
dockBadgeNotifications: true,
Expand Down Expand Up @@ -166,6 +176,8 @@ export const useSettingsStore = create<SettingsStore>()(
set({ lastUsedReasoningEffort: effort }),
setLastUsedCloudRepository: (repo) =>
set({ lastUsedCloudRepository: repo }),
setCachedCloudRepositoryMap: (map) =>
set({ cachedCloudRepositoryMap: map }),
setLastUsedEnvironment: (repoPath, environmentId) =>
set((state) => {
const next = { ...state.lastUsedEnvironments };
Expand Down Expand Up @@ -215,6 +227,7 @@ export const useSettingsStore = create<SettingsStore>()(
lastUsedModel: state.lastUsedModel,
lastUsedReasoningEffort: state.lastUsedReasoningEffort,
lastUsedCloudRepository: state.lastUsedCloudRepository,
cachedCloudRepositoryMap: state.cachedCloudRepositoryMap,
lastUsedEnvironments: state.lastUsedEnvironments,
desktopNotifications: state.desktopNotifications,
dockBadgeNotifications: state.dockBadgeNotifications,
Expand Down
81 changes: 70 additions & 11 deletions apps/code/src/renderer/hooks/useIntegrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -503,32 +504,82 @@ export function useUserRepositoryIntegration() {
useUserGithubIntegrations();
const [isRefreshingRepos, setIsRefreshingRepos] = useState(false);

const cachedCloudRepositoryMap = useSettingsStore(
(s) => s.cachedCloudRepositoryMap,
);
const setCachedCloudRepositoryMap = useSettingsStore(
(s) => s.setCachedCloudRepositoryMap,
);

const {
repositoryMap,
reposByInstallationId,
isPending: reposPending,
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we can also add

if (reposPending) [..]
if (failedInstallationIds.length > 0) return;
if (Object.kyes(..)) [..]

so we guard against persisting a partial map

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 () => {
Expand Down Expand Up @@ -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,
};
Expand Down
Loading