Skip to content

Commit 37262ba

Browse files
authored
fix: create proper workspace for cloud runs (#188)
1 parent ffd2566 commit 37262ba

File tree

6 files changed

+142
-22
lines changed

6 files changed

+142
-22
lines changed

apps/array/src/main/services/contextMenu.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,19 +106,30 @@ export const showTaskContextMenuService = createIpcService({
106106
worktreePath?: string,
107107
): Promise<TaskContextMenuResult> => {
108108
return new Promise((resolve) => {
109+
let pendingDialog = false;
110+
let resolved = false;
111+
112+
const safeResolve = (result: TaskContextMenuResult) => {
113+
if (!resolved) {
114+
resolved = true;
115+
resolve(result);
116+
}
117+
};
118+
109119
const template: MenuItemConstructorOptions[] = [
110120
{
111121
label: "Rename",
112-
click: () => resolve({ action: "rename" }),
122+
click: () => safeResolve({ action: "rename" }),
113123
},
114124
{
115125
label: "Duplicate",
116-
click: () => resolve({ action: "duplicate" }),
126+
click: () => safeResolve({ action: "duplicate" }),
117127
},
118128
{ type: "separator" },
119129
{
120130
label: "Delete",
121131
click: async () => {
132+
pendingDialog = true;
122133
const result = await dialog.showMessageBox({
123134
type: "question",
124135
title: "Delete Task",
@@ -132,9 +143,9 @@ export const showTaskContextMenuService = createIpcService({
132143
});
133144

134145
if (result.response === 1) {
135-
resolve({ action: "delete" });
146+
safeResolve({ action: "delete" });
136147
} else {
137-
resolve({ action: null });
148+
safeResolve({ action: null });
138149
}
139150
},
140151
},
@@ -145,12 +156,19 @@ export const showTaskContextMenuService = createIpcService({
145156
template.push({ type: "separator" });
146157
const externalAppsItems = await buildExternalAppsMenuItems(
147158
worktreePath,
148-
resolve,
159+
safeResolve,
149160
);
150161
template.push(...externalAppsItems);
151162
}
152163

153-
showContextMenu(template, { action: null }).then(resolve);
164+
const menu = Menu.buildFromTemplate(template);
165+
menu.popup({
166+
callback: () => {
167+
if (!pendingDialog) {
168+
safeResolve({ action: null });
169+
}
170+
},
171+
});
154172
};
155173

156174
setupMenu();
@@ -167,10 +185,21 @@ export const showFolderContextMenuService = createIpcService({
167185
folderPath?: string,
168186
): Promise<FolderContextMenuResult> => {
169187
return new Promise((resolve) => {
188+
let pendingDialog = false;
189+
let resolved = false;
190+
191+
const safeResolve = (result: FolderContextMenuResult) => {
192+
if (!resolved) {
193+
resolved = true;
194+
resolve(result);
195+
}
196+
};
197+
170198
const template: MenuItemConstructorOptions[] = [
171199
{
172200
label: "Remove folder",
173201
click: async () => {
202+
pendingDialog = true;
174203
const result = await dialog.showMessageBox({
175204
type: "question",
176205
title: "Remove Folder",
@@ -183,9 +212,9 @@ export const showFolderContextMenuService = createIpcService({
183212
});
184213

185214
if (result.response === 1) {
186-
resolve({ action: "remove" });
215+
safeResolve({ action: "remove" });
187216
} else {
188-
resolve({ action: null });
217+
safeResolve({ action: null });
189218
}
190219
},
191220
},
@@ -196,12 +225,19 @@ export const showFolderContextMenuService = createIpcService({
196225
template.push({ type: "separator" });
197226
const externalAppsItems = await buildExternalAppsMenuItems(
198227
folderPath,
199-
resolve,
228+
safeResolve,
200229
);
201230
template.push(...externalAppsItems);
202231
}
203232

204-
showContextMenu(template, { action: null }).then(resolve);
233+
const menu = Menu.buildFromTemplate(template);
234+
menu.popup({
235+
callback: () => {
236+
if (!pendingDialog) {
237+
safeResolve({ action: null });
238+
}
239+
},
240+
});
205241
};
206242

207243
setupMenu();

apps/array/src/main/services/workspace/configLoader.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ export async function loadConfig(
5959
return { config: null, source: null };
6060
}
6161

62-
// No config found
63-
log.debug(`No array.json config found for workspace ${worktreeName}`);
6462
return { config: null, source: null };
6563
}
6664

apps/array/src/main/services/workspace/workspaceService.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,32 @@ export class WorkspaceService {
8787
`Creating workspace for task ${taskId} in ${mainRepoPath} (mode: ${mode})`,
8888
);
8989

90+
if (mode === "cloud") {
91+
const associations = getTaskAssociations();
92+
const existingIndex = associations.findIndex((a) => a.taskId === taskId);
93+
const association: TaskFolderAssociation = {
94+
taskId,
95+
folderId,
96+
folderPath,
97+
mode,
98+
};
99+
100+
if (existingIndex >= 0) {
101+
associations[existingIndex] = association;
102+
} else {
103+
associations.push(association);
104+
}
105+
foldersStore.set("taskAssociations", associations);
106+
107+
return {
108+
taskId,
109+
mode,
110+
worktree: null,
111+
terminalSessionIds: [],
112+
hasStartScripts: false,
113+
};
114+
}
115+
90116
// Root mode: skip worktree creation entirely
91117
if (mode === "root") {
92118
// Save task association without worktree
@@ -310,6 +336,13 @@ export class WorkspaceService {
310336
return;
311337
}
312338

339+
// Cloud mode: just remove the association, no local cleanup needed
340+
if (association.mode === "cloud") {
341+
this.removeTaskAssociation(taskId);
342+
log.info(`Cloud workspace deleted for task ${taskId}`);
343+
return;
344+
}
345+
313346
const folderId = association.folderId;
314347
const folderPath = association.folderPath;
315348
const isWorktreeMode =
@@ -409,6 +442,11 @@ export class WorkspaceService {
409442
return false;
410443
}
411444

445+
// Cloud mode: always exists (no local files to verify)
446+
if (association.mode === "cloud") {
447+
return true;
448+
}
449+
412450
// Root mode: check if folder still exists
413451
if (association.mode === "root") {
414452
const exists = fs.existsSync(association.folderPath);

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,16 @@ export function useTaskCreation({
122122
}
123123

124124
if (workspaceMode === "cloud") {
125-
// Cloud execution - no local workspace needed
125+
if (selectedDirectory) {
126+
try {
127+
await useWorkspaceStore
128+
.getState()
129+
.ensureWorkspace(newTask.id, selectedDirectory, "cloud");
130+
} catch (error) {
131+
log.error("Failed to create cloud workspace:", error);
132+
}
133+
}
134+
126135
try {
127136
await client.runTaskInCloud(newTask.id);
128137
log.info("Started cloud task", { taskId: newTask.id });

apps/array/src/renderer/features/workspace/stores/workspaceStore.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ const useWorkspaceStoreBase = create<WorkspaceState>()((set, get) => {
9191
const workspaces = await window.electronAPI?.workspace.getAll();
9292
if (workspaces) {
9393
set({ workspaces, isLoaded: true });
94-
log.info(`Loaded ${Object.keys(workspaces).length} workspace(s)`);
9594
} else {
9695
set({ workspaces: {}, isLoaded: true });
9796
}
@@ -167,17 +166,51 @@ const useWorkspaceStoreBase = create<WorkspaceState>()((set, get) => {
167166
repoPath: string,
168167
mode: WorkspaceMode = "worktree",
169168
) => {
170-
// Cloud tasks don't need local workspaces
171-
if (mode === "cloud") {
172-
return null as unknown as Workspace;
173-
}
174-
175169
// Return existing workspace if it exists
176170
const existing = get().workspaces[taskId];
177171
if (existing) {
178172
return existing;
179173
}
180174

175+
// For cloud tasks, create a minimal workspace entry (no local worktree)
176+
if (mode === "cloud") {
177+
const { getFolderByPath, addFolder } =
178+
useRegisteredFoldersStore.getState();
179+
let folder = getFolderByPath(repoPath);
180+
if (!folder) {
181+
folder = await addFolder(repoPath);
182+
}
183+
184+
const cloudWorkspace: Workspace = {
185+
taskId,
186+
folderId: folder.id,
187+
folderPath: repoPath,
188+
mode: "cloud",
189+
worktreePath: null,
190+
worktreeName: null,
191+
branchName: null,
192+
baseBranch: null,
193+
createdAt: new Date().toISOString(),
194+
terminalSessionIds: [],
195+
hasStartScripts: false,
196+
};
197+
198+
set((state) => ({
199+
workspaces: { ...state.workspaces, [taskId]: cloudWorkspace },
200+
}));
201+
202+
// Persist cloud workspace to main process
203+
await window.electronAPI?.workspace.create({
204+
taskId,
205+
mainRepoPath: repoPath,
206+
folderId: folder.id,
207+
folderPath: repoPath,
208+
mode: "cloud",
209+
});
210+
211+
return cloudWorkspace;
212+
}
213+
181214
// Atomically check if creating and set if not - this prevents race conditions
182215
let wasAlreadyCreating = false;
183216
set((state) => {

apps/array/src/renderer/stores/navigationStore.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useTaskExecutionStore } from "@features/task-detail/stores/taskExecutio
33
import { useWorkspaceStore } from "@features/workspace/stores/workspaceStore";
44
import { track } from "@renderer/lib/analytics";
55
import { logger } from "@renderer/lib/logger";
6-
import type { Task } from "@shared/types";
6+
import type { Task, WorkspaceMode } from "@shared/types";
77
import { useRegisteredFoldersStore } from "@stores/registeredFoldersStore";
88
import { useTaskDirectoryStore } from "@stores/taskDirectoryStore";
99
import { expandTildePath } from "@utils/path";
@@ -98,12 +98,18 @@ export const useNavigationStore = create<NavigationStore>((set, get) => {
9898
if (directory) {
9999
try {
100100
await useRegisteredFoldersStore.getState().addFolder(directory);
101-
const storedMode = useTaskExecutionStore
101+
102+
let workspaceMode: WorkspaceMode = useTaskExecutionStore
102103
.getState()
103104
.getTaskState(task.id).workspaceMode;
105+
106+
if (task.latest_run?.environment === "cloud") {
107+
workspaceMode = "cloud";
108+
}
109+
104110
await useWorkspaceStore
105111
.getState()
106-
.ensureWorkspace(task.id, directory, storedMode);
112+
.ensureWorkspace(task.id, directory, workspaceMode);
107113
} catch (error) {
108114
log.error("Failed to auto-register folder on task open:", error);
109115
}

0 commit comments

Comments
 (0)