Skip to content

Commit ea2b062

Browse files
authored
feat(cloud): cloud agent last-run repo remember (#1477)
1 parent 70f4a18 commit ea2b062

6 files changed

Lines changed: 295 additions & 15 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const { getItem, setItem, removeItem } = vi.hoisted(() => ({
4+
getItem: vi.fn(),
5+
setItem: vi.fn(),
6+
removeItem: vi.fn(),
7+
}));
8+
9+
vi.mock("@renderer/trpc/client", () => ({
10+
trpcClient: {
11+
secureStore: {
12+
getItem: { query: getItem },
13+
setItem: { query: setItem },
14+
removeItem: { query: removeItem },
15+
},
16+
},
17+
}));
18+
19+
import { useSettingsStore } from "./settingsStore";
20+
21+
describe("feature settingsStore cloud selections", () => {
22+
beforeEach(() => {
23+
getItem.mockReset();
24+
setItem.mockReset();
25+
removeItem.mockReset();
26+
getItem.mockResolvedValue(null);
27+
setItem.mockResolvedValue(undefined);
28+
removeItem.mockResolvedValue(undefined);
29+
30+
useSettingsStore.setState({
31+
lastUsedCloudRepository: null,
32+
});
33+
});
34+
35+
it("persists the last used cloud repository", async () => {
36+
useSettingsStore.getState().setLastUsedCloudRepository("posthog/posthog");
37+
38+
await vi.waitFor(() => {
39+
expect(setItem).toHaveBeenCalled();
40+
});
41+
42+
const lastCall = setItem.mock.calls[setItem.mock.calls.length - 1];
43+
const persisted = JSON.parse(lastCall[0].value);
44+
45+
expect(persisted.state.lastUsedCloudRepository).toBe("posthog/posthog");
46+
});
47+
48+
it("rehydrates the last used cloud repository", async () => {
49+
getItem.mockResolvedValue(
50+
JSON.stringify({
51+
state: {
52+
lastUsedCloudRepository: "posthog/posthog",
53+
},
54+
version: 0,
55+
}),
56+
);
57+
58+
useSettingsStore.setState({
59+
lastUsedCloudRepository: null,
60+
});
61+
62+
await useSettingsStore.persist.rehydrate();
63+
64+
expect(useSettingsStore.getState().lastUsedCloudRepository).toBe(
65+
"posthog/posthog",
66+
);
67+
});
68+
});

apps/code/src/renderer/features/settings/stores/settingsStore.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface SettingsStore {
2525
lastUsedWorkspaceMode: WorkspaceMode;
2626
lastUsedAdapter: AgentAdapter;
2727
lastUsedModel: string | null;
28+
lastUsedCloudRepository: string | null;
2829
lastUsedEnvironments: Record<string, string>;
2930
desktopNotifications: boolean;
3031
dockBadgeNotifications: boolean;
@@ -57,6 +58,7 @@ interface SettingsStore {
5758
setLastUsedWorkspaceMode: (mode: WorkspaceMode) => void;
5859
setLastUsedAdapter: (adapter: AgentAdapter) => void;
5960
setLastUsedModel: (model: string) => void;
61+
setLastUsedCloudRepository: (repo: string | null) => void;
6062
setLastUsedEnvironment: (
6163
repoPath: string,
6264
environmentId: string | null,
@@ -88,6 +90,7 @@ export const useSettingsStore = create<SettingsStore>()(
8890
lastUsedWorkspaceMode: "local",
8991
lastUsedAdapter: "claude",
9092
lastUsedModel: null,
93+
lastUsedCloudRepository: null,
9194
lastUsedEnvironments: {},
9295
desktopNotifications: true,
9396
dockBadgeNotifications: true,
@@ -143,6 +146,8 @@ export const useSettingsStore = create<SettingsStore>()(
143146
setLastUsedWorkspaceMode: (mode) => set({ lastUsedWorkspaceMode: mode }),
144147
setLastUsedAdapter: (adapter) => set({ lastUsedAdapter: adapter }),
145148
setLastUsedModel: (model) => set({ lastUsedModel: model }),
149+
setLastUsedCloudRepository: (repo) =>
150+
set({ lastUsedCloudRepository: repo }),
146151
setLastUsedEnvironment: (repoPath, environmentId) =>
147152
set((state) => {
148153
const next = { ...state.lastUsedEnvironments };
@@ -190,6 +195,7 @@ export const useSettingsStore = create<SettingsStore>()(
190195
lastUsedWorkspaceMode: state.lastUsedWorkspaceMode,
191196
lastUsedAdapter: state.lastUsedAdapter,
192197
lastUsedModel: state.lastUsedModel,
198+
lastUsedCloudRepository: state.lastUsedCloudRepository,
193199
lastUsedEnvironments: state.lastUsedEnvironments,
194200
desktopNotifications: state.desktopNotifications,
195201
dockBadgeNotifications: state.dockBadgeNotifications,

apps/code/src/renderer/features/task-detail/components/TaskInput.tsx

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { useAuthStore } from "@renderer/features/auth/stores/authStore";
2929
import { useTRPC } from "@renderer/trpc/client";
3030
import { useNavigationStore } from "@stores/navigationStore";
3131
import { useQuery } from "@tanstack/react-query";
32-
import { useCallback, useEffect, useRef, useState } from "react";
32+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3333
import { useHotkeys } from "react-hotkeys-hook";
3434
import { usePreviewConfig } from "../hooks/usePreviewConfig";
3535
import { useTaskCreation } from "../hooks/useTaskCreation";
@@ -59,6 +59,8 @@ export function TaskInput({
5959
setLastUsedWorkspaceMode,
6060
lastUsedAdapter,
6161
setLastUsedAdapter,
62+
lastUsedCloudRepository,
63+
setLastUsedCloudRepository,
6264
allowBypassPermissions,
6365
setLastUsedEnvironment,
6466
getLastUsedEnvironment,
@@ -103,13 +105,18 @@ export function TaskInput({
103105
const { githubIntegration, repositories, isLoadingRepos } =
104106
useRepositoryIntegration();
105107
const [selectedRepository, setSelectedRepository] = useState<string | null>(
106-
null,
108+
() => lastUsedCloudRepository?.toLowerCase() ?? null,
107109
);
110+
const selectedCloudRepository = useMemo(() => {
111+
if (!selectedRepository) return null;
112+
const lower = selectedRepository.toLowerCase();
113+
return repositories.includes(lower) ? lower : null;
114+
}, [selectedRepository, repositories]);
108115
const { currentBranch, branchLoading, defaultBranch } =
109116
useGitQueries(selectedDirectory);
110117

111118
const { data: cloudBranchData, isPending: cloudBranchesLoading } =
112-
useGithubBranches(githubIntegration?.id, selectedRepository);
119+
useGithubBranches(githubIntegration?.id, selectedCloudRepository);
113120
const cloudBranches = cloudBranchData?.branches;
114121
const cloudDefaultBranch = cloudBranchData?.defaultBranch ?? null;
115122

@@ -149,6 +156,15 @@ export function TaskInput({
149156
}
150157
}, [selectedDirectory, newBranchName, gitActions]);
151158

159+
const handleRepositorySelect = useCallback(
160+
(repo: string) => {
161+
const normalizedRepo = repo.toLowerCase();
162+
setSelectedRepository(normalizedRepo);
163+
setLastUsedCloudRepository(normalizedRepo);
164+
},
165+
[setLastUsedCloudRepository],
166+
);
167+
152168
const {
153169
modeOption,
154170
modelOption,
@@ -159,6 +175,37 @@ export function TaskInput({
159175

160176
const { folders } = useFolders();
161177

178+
useEffect(() => {
179+
if (selectedRepository || !lastUsedCloudRepository) {
180+
return;
181+
}
182+
183+
setSelectedRepository(lastUsedCloudRepository.toLowerCase());
184+
}, [lastUsedCloudRepository, selectedRepository]);
185+
186+
useEffect(() => {
187+
if (
188+
isLoadingRepos ||
189+
!githubIntegration ||
190+
!selectedRepository ||
191+
selectedCloudRepository
192+
) {
193+
return;
194+
}
195+
196+
setSelectedRepository(null);
197+
if (lastUsedCloudRepository === selectedRepository) {
198+
setLastUsedCloudRepository(null);
199+
}
200+
}, [
201+
githubIntegration,
202+
isLoadingRepos,
203+
lastUsedCloudRepository,
204+
selectedCloudRepository,
205+
selectedRepository,
206+
setLastUsedCloudRepository,
207+
]);
208+
162209
useEffect(() => {
163210
if (view.folderId) {
164211
const folder = folders.find((f) => f.id === view.folderId);
@@ -169,7 +216,7 @@ export function TaskInput({
169216
}, [view.folderId, folders]);
170217

171218
const effectiveRepoPath =
172-
workspaceMode === "cloud" ? selectedRepository : selectedDirectory;
219+
workspaceMode === "cloud" ? selectedCloudRepository : selectedDirectory;
173220

174221
const setSelectedEnvironment = useCallback(
175222
(envId: string | null) => {
@@ -183,6 +230,7 @@ export function TaskInput({
183230

184231
useEffect(() => {
185232
setSelectedBranch(null);
233+
186234
if (effectiveRepoPath) {
187235
setSelectedEnvironmentRaw(getLastUsedEnvironment(effectiveRepoPath));
188236
} else {
@@ -212,7 +260,7 @@ export function TaskInput({
212260
const { isCreatingTask, canSubmit, handleSubmit } = useTaskCreation({
213261
editorRef,
214262
selectedDirectory,
215-
selectedRepository,
263+
selectedRepository: selectedCloudRepository,
216264
githubIntegrationId: githubIntegration?.id,
217265
workspaceMode: effectiveWorkspaceMode,
218266
branch: branchForTaskCreation,
@@ -373,7 +421,7 @@ export function TaskInput({
373421
{workspaceMode === "cloud" ? (
374422
<GitHubRepoPicker
375423
value={selectedRepository}
376-
onChange={setSelectedRepository}
424+
onChange={handleRepositorySelect}
377425
repositories={repositories}
378426
isLoading={isLoadingRepos}
379427
placeholder="Select repository..."
@@ -398,7 +446,7 @@ export function TaskInput({
398446
<BranchSelector
399447
repoPath={
400448
workspaceMode === "cloud"
401-
? selectedRepository
449+
? selectedCloudRepository
402450
: selectedDirectory
403451
}
404452
currentBranch={currentBranch}
@@ -407,7 +455,7 @@ export function TaskInput({
407455
}
408456
disabled={
409457
isCreatingTask ||
410-
(workspaceMode === "cloud" && !selectedRepository)
458+
(workspaceMode === "cloud" && !selectedCloudRepository)
411459
}
412460
loading={branchLoading}
413461
workspaceMode={workspaceMode}
@@ -446,7 +494,7 @@ export function TaskInput({
446494
onSubmit={handleSubmit}
447495
hasDirectory={
448496
workspaceMode === "cloud"
449-
? !!selectedRepository
497+
? !!selectedCloudRepository
450498
: !!selectedDirectory
451499
}
452500
directoryTooltip={

apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ export function useTaskCreation({
111111
const { invalidateTasks } = useCreateTask();
112112
const { isOnline } = useConnectivity();
113113

114-
// Cloud mode can work with either selectedRepository (production) or selectedDirectory (dev testing)
115-
const hasRequiredPath = !!selectedRepository || !!selectedDirectory;
114+
const hasRequiredPath =
115+
workspaceMode === "cloud" ? !!selectedRepository : !!selectedDirectory;
116116
const canSubmit =
117117
!!editorRef.current &&
118118
isAuthenticated &&

0 commit comments

Comments
 (0)