Skip to content
Merged
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
56 changes: 46 additions & 10 deletions apps/array/src/main/services/contextMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,30 @@ export const showTaskContextMenuService = createIpcService({
worktreePath?: string,
): Promise<TaskContextMenuResult> => {
return new Promise((resolve) => {
let pendingDialog = false;
let resolved = false;

const safeResolve = (result: TaskContextMenuResult) => {
if (!resolved) {
resolved = true;
resolve(result);
}
};

const template: MenuItemConstructorOptions[] = [
{
label: "Rename",
click: () => resolve({ action: "rename" }),
click: () => safeResolve({ action: "rename" }),
},
{
label: "Duplicate",
click: () => resolve({ action: "duplicate" }),
click: () => safeResolve({ action: "duplicate" }),
},
{ type: "separator" },
{
label: "Delete",
click: async () => {
pendingDialog = true;
const result = await dialog.showMessageBox({
type: "question",
title: "Delete Task",
Expand All @@ -132,9 +143,9 @@ export const showTaskContextMenuService = createIpcService({
});

if (result.response === 1) {
resolve({ action: "delete" });
safeResolve({ action: "delete" });
} else {
resolve({ action: null });
safeResolve({ action: null });
}
},
},
Expand All @@ -145,12 +156,19 @@ export const showTaskContextMenuService = createIpcService({
template.push({ type: "separator" });
const externalAppsItems = await buildExternalAppsMenuItems(
worktreePath,
resolve,
safeResolve,
);
template.push(...externalAppsItems);
}

showContextMenu(template, { action: null }).then(resolve);
const menu = Menu.buildFromTemplate(template);
menu.popup({
callback: () => {
if (!pendingDialog) {
safeResolve({ action: null });
}
},
});
};

setupMenu();
Expand All @@ -167,10 +185,21 @@ export const showFolderContextMenuService = createIpcService({
folderPath?: string,
): Promise<FolderContextMenuResult> => {
return new Promise((resolve) => {
let pendingDialog = false;
let resolved = false;

const safeResolve = (result: FolderContextMenuResult) => {
if (!resolved) {
resolved = true;
resolve(result);
}
};

const template: MenuItemConstructorOptions[] = [
{
label: "Remove folder",
click: async () => {
pendingDialog = true;
const result = await dialog.showMessageBox({
type: "question",
title: "Remove Folder",
Expand All @@ -183,9 +212,9 @@ export const showFolderContextMenuService = createIpcService({
});

if (result.response === 1) {
resolve({ action: "remove" });
safeResolve({ action: "remove" });
} else {
resolve({ action: null });
safeResolve({ action: null });
}
},
},
Expand All @@ -196,12 +225,19 @@ export const showFolderContextMenuService = createIpcService({
template.push({ type: "separator" });
const externalAppsItems = await buildExternalAppsMenuItems(
folderPath,
resolve,
safeResolve,
);
template.push(...externalAppsItems);
}

showContextMenu(template, { action: null }).then(resolve);
const menu = Menu.buildFromTemplate(template);
menu.popup({
callback: () => {
if (!pendingDialog) {
safeResolve({ action: null });
}
},
});
};

setupMenu();
Expand Down
2 changes: 0 additions & 2 deletions apps/array/src/main/services/workspace/configLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ export async function loadConfig(
return { config: null, source: null };
}

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

Expand Down
38 changes: 38 additions & 0 deletions apps/array/src/main/services/workspace/workspaceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,32 @@ export class WorkspaceService {
`Creating workspace for task ${taskId} in ${mainRepoPath} (mode: ${mode})`,
);

if (mode === "cloud") {
const associations = getTaskAssociations();
const existingIndex = associations.findIndex((a) => a.taskId === taskId);
const association: TaskFolderAssociation = {
taskId,
folderId,
folderPath,
mode,
};

if (existingIndex >= 0) {
associations[existingIndex] = association;
} else {
associations.push(association);
}
foldersStore.set("taskAssociations", associations);

return {
taskId,
mode,
worktree: null,
terminalSessionIds: [],
hasStartScripts: false,
};
}

// Root mode: skip worktree creation entirely
if (mode === "root") {
// Save task association without worktree
Expand Down Expand Up @@ -310,6 +336,13 @@ export class WorkspaceService {
return;
}

// Cloud mode: just remove the association, no local cleanup needed
if (association.mode === "cloud") {
this.removeTaskAssociation(taskId);
log.info(`Cloud workspace deleted for task ${taskId}`);
return;
}

const folderId = association.folderId;
const folderPath = association.folderPath;
const isWorktreeMode =
Expand Down Expand Up @@ -409,6 +442,11 @@ export class WorkspaceService {
return false;
}

// Cloud mode: always exists (no local files to verify)
if (association.mode === "cloud") {
return true;
}

// Root mode: check if folder still exists
if (association.mode === "root") {
const exists = fs.existsSync(association.folderPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,16 @@ export function useTaskCreation({
}

if (workspaceMode === "cloud") {
// Cloud execution - no local workspace needed
if (selectedDirectory) {
try {
await useWorkspaceStore
.getState()
.ensureWorkspace(newTask.id, selectedDirectory, "cloud");
} catch (error) {
log.error("Failed to create cloud workspace:", error);
}
}

try {
await client.runTaskInCloud(newTask.id);
log.info("Started cloud task", { taskId: newTask.id });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ const useWorkspaceStoreBase = create<WorkspaceState>()((set, get) => {
const workspaces = await window.electronAPI?.workspace.getAll();
if (workspaces) {
set({ workspaces, isLoaded: true });
log.info(`Loaded ${Object.keys(workspaces).length} workspace(s)`);
} else {
set({ workspaces: {}, isLoaded: true });
}
Expand Down Expand Up @@ -167,17 +166,51 @@ const useWorkspaceStoreBase = create<WorkspaceState>()((set, get) => {
repoPath: string,
mode: WorkspaceMode = "worktree",
) => {
// Cloud tasks don't need local workspaces
if (mode === "cloud") {
return null as unknown as Workspace;
}

// Return existing workspace if it exists
const existing = get().workspaces[taskId];
if (existing) {
return existing;
}

// For cloud tasks, create a minimal workspace entry (no local worktree)
if (mode === "cloud") {
const { getFolderByPath, addFolder } =
useRegisteredFoldersStore.getState();
let folder = getFolderByPath(repoPath);
if (!folder) {
folder = await addFolder(repoPath);
}

const cloudWorkspace: Workspace = {
taskId,
folderId: folder.id,
folderPath: repoPath,
mode: "cloud",
worktreePath: null,
worktreeName: null,
branchName: null,
baseBranch: null,
createdAt: new Date().toISOString(),
terminalSessionIds: [],
hasStartScripts: false,
};

set((state) => ({
workspaces: { ...state.workspaces, [taskId]: cloudWorkspace },
}));

// Persist cloud workspace to main process
await window.electronAPI?.workspace.create({
taskId,
mainRepoPath: repoPath,
folderId: folder.id,
folderPath: repoPath,
mode: "cloud",
});

return cloudWorkspace;
}

// Atomically check if creating and set if not - this prevents race conditions
let wasAlreadyCreating = false;
set((state) => {
Expand Down
12 changes: 9 additions & 3 deletions apps/array/src/renderer/stores/navigationStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useTaskExecutionStore } from "@features/task-detail/stores/taskExecutio
import { useWorkspaceStore } from "@features/workspace/stores/workspaceStore";
import { track } from "@renderer/lib/analytics";
import { logger } from "@renderer/lib/logger";
import type { Task } from "@shared/types";
import type { Task, WorkspaceMode } from "@shared/types";
import { useRegisteredFoldersStore } from "@stores/registeredFoldersStore";
import { useTaskDirectoryStore } from "@stores/taskDirectoryStore";
import { expandTildePath } from "@utils/path";
Expand Down Expand Up @@ -98,12 +98,18 @@ export const useNavigationStore = create<NavigationStore>((set, get) => {
if (directory) {
try {
await useRegisteredFoldersStore.getState().addFolder(directory);
const storedMode = useTaskExecutionStore

let workspaceMode: WorkspaceMode = useTaskExecutionStore
.getState()
.getTaskState(task.id).workspaceMode;

if (task.latest_run?.environment === "cloud") {
workspaceMode = "cloud";
}

await useWorkspaceStore
.getState()
.ensureWorkspace(task.id, directory, storedMode);
.ensureWorkspace(task.id, directory, workspaceMode);
} catch (error) {
log.error("Failed to auto-register folder on task open:", error);
}
Expand Down