Skip to content

Commit ffd2566

Browse files
authored
feat: cloud/local run selection (#186)
1 parent 4846517 commit ffd2566

File tree

10 files changed

+218
-66
lines changed

10 files changed

+218
-66
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1+
import type { WorkspaceMode } from "@shared/types";
12
import { create } from "zustand";
23
import { persist } from "zustand/middleware";
34

45
export type DefaultRunMode = "local" | "cloud" | "last_used";
6+
export type LocalWorkspaceMode = "worktree" | "root";
57

68
interface SettingsStore {
79
autoRunTasks: boolean;
810
defaultRunMode: DefaultRunMode;
911
lastUsedRunMode: "local" | "cloud";
12+
lastUsedLocalWorkspaceMode: LocalWorkspaceMode;
13+
lastUsedWorkspaceMode: WorkspaceMode;
1014
createPR: boolean;
1115

1216
setAutoRunTasks: (autoRun: boolean) => void;
1317
setDefaultRunMode: (mode: DefaultRunMode) => void;
1418
setLastUsedRunMode: (mode: "local" | "cloud") => void;
19+
setLastUsedLocalWorkspaceMode: (mode: LocalWorkspaceMode) => void;
20+
setLastUsedWorkspaceMode: (mode: WorkspaceMode) => void;
1521
setCreatePR: (createPR: boolean) => void;
1622
}
1723

@@ -21,11 +27,16 @@ export const useSettingsStore = create<SettingsStore>()(
2127
autoRunTasks: true,
2228
defaultRunMode: "last_used",
2329
lastUsedRunMode: "local",
30+
lastUsedLocalWorkspaceMode: "worktree",
31+
lastUsedWorkspaceMode: "worktree",
2432
createPR: true,
2533

2634
setAutoRunTasks: (autoRun) => set({ autoRunTasks: autoRun }),
2735
setDefaultRunMode: (mode) => set({ defaultRunMode: mode }),
2836
setLastUsedRunMode: (mode) => set({ lastUsedRunMode: mode }),
37+
setLastUsedLocalWorkspaceMode: (mode) =>
38+
set({ lastUsedLocalWorkspaceMode: mode }),
39+
setLastUsedWorkspaceMode: (mode) => set({ lastUsedWorkspaceMode: mode }),
2940
setCreatePR: (createPR) => set({ createPR }),
3041
}),
3142
{

apps/array/src/renderer/features/sidebar/components/SidebarItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ interface SidebarItemProps {
66
depth: number;
77
icon?: React.ReactNode;
88
label: string;
9-
subtitle?: string;
9+
subtitle?: React.ReactNode;
1010
isActive?: boolean;
1111
onClick?: () => void;
1212
onContextMenu?: (e: React.MouseEvent) => void;

apps/array/src/renderer/features/sidebar/components/SidebarMenu.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { RenameTaskDialog } from "@components/RenameTaskDialog";
2+
import { useTaskExecutionStore } from "@features/task-detail/stores/taskExecutionStore";
23
import { useTasks } from "@features/tasks/hooks/useTasks";
34
import { useTaskStore } from "@features/tasks/stores/taskStore";
45
import { useMeQuery } from "@hooks/useMeQuery";
@@ -32,6 +33,7 @@ function SidebarMenuComponent() {
3233
const collapsedSections = useSidebarStore((state) => state.collapsedSections);
3334
const toggleSection = useSidebarStore((state) => state.toggleSection);
3435
const workspaces = useWorkspaceStore.use.workspaces();
36+
const taskStates = useTaskExecutionStore((state) => state.taskStates);
3537

3638
const { showContextMenu, renameTask, renameDialogOpen, setRenameDialogOpen } =
3739
useTaskContextMenu();
@@ -179,6 +181,7 @@ function SidebarMenuComponent() {
179181
workspaces[task.id]?.worktreePath ??
180182
workspaces[task.id]?.folderPath
181183
}
184+
workspaceMode={taskStates[task.id]?.workspaceMode}
182185
onClick={() => handleTaskClick(task.id)}
183186
onContextMenu={(e) => handleTaskContextMenu(task.id, e)}
184187
/>

apps/array/src/renderer/features/sidebar/components/items/TaskItem.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import {
22
CheckCircleIcon,
33
CircleIcon,
4+
Cloud,
45
XCircleIcon,
56
} from "@phosphor-icons/react";
7+
import type { WorkspaceMode } from "@shared/types";
68
import { useQuery } from "@tanstack/react-query";
79
import type { TaskStatus } from "../../types";
810
import { SidebarItem } from "../SidebarItem";
@@ -24,6 +26,7 @@ interface TaskItemProps {
2426
isActive: boolean;
2527
worktreeName?: string;
2628
worktreePath?: string;
29+
workspaceMode?: WorkspaceMode;
2730
onClick: () => void;
2831
onContextMenu: (e: React.MouseEvent) => void;
2932
}
@@ -101,11 +104,21 @@ export function TaskItem({
101104
isActive,
102105
worktreeName,
103106
worktreePath,
107+
workspaceMode,
104108
onClick,
105109
onContextMenu,
106110
}: TaskItemProps) {
107111
const { data: currentBranch } = useCurrentBranch(worktreePath, worktreeName);
108-
const subtitle = worktreeName ?? currentBranch;
112+
113+
const isCloudTask = workspaceMode === "cloud";
114+
const subtitle = isCloudTask ? (
115+
<span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
116+
<Cloud size={10} />
117+
<span>Cloud</span>
118+
</span>
119+
) : (
120+
(worktreeName ?? currentBranch)
121+
);
109122

110123
return (
111124
<SidebarItem
@@ -117,7 +130,7 @@ export function TaskItem({
117130
onClick={onClick}
118131
onContextMenu={onContextMenu}
119132
endContent={
120-
worktreePath ? (
133+
!isCloudTask && worktreePath ? (
121134
<DiffStatsDisplay worktreePath={worktreePath} />
122135
) : undefined
123136
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Cloud, Desktop } from "@phosphor-icons/react";
2+
import { ChevronDownIcon } from "@radix-ui/react-icons";
3+
import { Button, DropdownMenu, Flex, Text } from "@radix-ui/themes";
4+
import type { Responsive } from "@radix-ui/themes/dist/esm/props/prop-def.js";
5+
6+
export type RunMode = "local" | "cloud";
7+
8+
interface RunModeSelectProps {
9+
value: RunMode;
10+
onChange: (mode: RunMode) => void;
11+
size?: Responsive<"1" | "2">;
12+
}
13+
14+
const MODE_CONFIG: Record<RunMode, { label: string; icon: React.ReactNode }> = {
15+
local: {
16+
label: "Local",
17+
icon: <Desktop size={16} weight="regular" />,
18+
},
19+
cloud: {
20+
label: "Cloud",
21+
icon: <Cloud size={16} weight="regular" />,
22+
},
23+
};
24+
25+
export function RunModeSelect({
26+
value,
27+
onChange,
28+
size = "1",
29+
}: RunModeSelectProps) {
30+
return (
31+
<DropdownMenu.Root>
32+
<DropdownMenu.Trigger>
33+
<Button color="gray" variant="outline" size={size}>
34+
<Flex justify="between" align="center" gap="2">
35+
<Flex align="center" gap="2" style={{ minWidth: 0 }}>
36+
{MODE_CONFIG[value].icon}
37+
<Text size={size}>{MODE_CONFIG[value].label}</Text>
38+
</Flex>
39+
<ChevronDownIcon style={{ flexShrink: 0 }} />
40+
</Flex>
41+
</Button>
42+
</DropdownMenu.Trigger>
43+
44+
<DropdownMenu.Content
45+
align="start"
46+
style={{ minWidth: "var(--radix-dropdown-menu-trigger-width)" }}
47+
size={size}
48+
>
49+
<DropdownMenu.Item onSelect={() => onChange("local")}>
50+
<Flex align="center" gap="2">
51+
<Desktop size={12} />
52+
<Text size={size}>Local</Text>
53+
</Flex>
54+
</DropdownMenu.Item>
55+
<DropdownMenu.Item onSelect={() => onChange("cloud")}>
56+
<Flex align="center" gap="2">
57+
<Cloud size={12} />
58+
<Text size={size}>Cloud</Text>
59+
</Flex>
60+
</DropdownMenu.Item>
61+
</DropdownMenu.Content>
62+
</DropdownMenu.Root>
63+
);
64+
}

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
11
import { FolderPicker } from "@features/folder-picker/components/FolderPicker";
2+
import { useSettingsStore } from "@features/settings/stores/settingsStore";
23
import { useSetHeaderContent } from "@hooks/useSetHeaderContent";
3-
import { Box, Flex } from "@radix-ui/themes";
4+
import { Flex } from "@radix-ui/themes";
45
import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore";
56
import type { WorkspaceMode } from "@shared/types";
67
import { useNavigationStore } from "@stores/navigationStore";
78
import { useTaskDirectoryStore } from "@stores/taskDirectoryStore";
89
import { useEffect, useState } from "react";
910
import { useEditorSetup } from "../hooks/useEditorSetup";
1011
import { useTaskCreation } from "../hooks/useTaskCreation";
12+
import { type RunMode, RunModeSelect } from "./RunModeSelect";
1113
import { SuggestedTasks } from "./SuggestedTasks";
1214
import { TaskInputEditor } from "./TaskInputEditor";
1315

1416
const DOT_FILL = "var(--gray-6)";
1517

18+
type LocalWorkspaceMode = "worktree" | "root";
19+
1620
export function TaskInput() {
1721
useSetHeaderContent(null);
1822

1923
const { view } = useNavigationStore();
2024
const { lastUsedDirectory } = useTaskDirectoryStore();
2125
const { folders } = useRegisteredFoldersStore();
26+
const { lastUsedRunMode, lastUsedLocalWorkspaceMode } = useSettingsStore();
27+
2228
const [selectedDirectory, setSelectedDirectory] = useState(
2329
lastUsedDirectory || "",
2430
);
25-
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>("worktree");
31+
const [runMode, setRunMode] = useState<RunMode>(lastUsedRunMode);
32+
const [localWorkspaceMode, setLocalWorkspaceMode] =
33+
useState<LocalWorkspaceMode>(lastUsedLocalWorkspaceMode);
2634

2735
useEffect(() => {
2836
if (view.folderId) {
@@ -43,10 +51,14 @@ export function TaskInput() {
4351
repoPath: selectedDirectory,
4452
});
4553

54+
// Compute the effective workspace mode for task creation
55+
const effectiveWorkspaceMode: WorkspaceMode =
56+
runMode === "cloud" ? "cloud" : localWorkspaceMode;
57+
4658
const { isCreatingTask, canSubmit, handleSubmit } = useTaskCreation({
4759
editor,
4860
selectedDirectory,
49-
workspaceMode,
61+
workspaceMode: effectiveWorkspaceMode,
5062
});
5163

5264
return (
@@ -98,20 +110,22 @@ export function TaskInput() {
98110
zIndex: 1,
99111
}}
100112
>
101-
<Box>
113+
<Flex gap="2" align="center">
102114
<FolderPicker
103115
value={selectedDirectory}
104116
onChange={handleDirectoryChange}
105117
placeholder="Select working directory..."
106118
size="1"
107119
/>
108-
</Box>
120+
<RunModeSelect value={runMode} onChange={setRunMode} size="1" />
121+
</Flex>
109122

110123
<TaskInputEditor
111124
editor={editor}
112125
isCreatingTask={isCreatingTask}
113-
workspaceMode={workspaceMode}
114-
onWorkspaceModeChange={setWorkspaceMode}
126+
runMode={runMode}
127+
localWorkspaceMode={localWorkspaceMode}
128+
onLocalWorkspaceModeChange={setLocalWorkspaceMode}
115129
canSubmit={canSubmit}
116130
onSubmit={handleSubmit}
117131
hasDirectory={!!selectedDirectory}

apps/array/src/renderer/features/task-detail/components/TaskInputEditor.tsx

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { ArrowUpIcon, GitBranchIcon } from "@phosphor-icons/react";
22
import { Box, Flex, IconButton, Text, Tooltip } from "@radix-ui/themes";
3-
import type { WorkspaceMode } from "@shared/types";
43
import type { Editor } from "@tiptap/react";
54
import { EditorContent } from "@tiptap/react";
5+
import type { RunMode } from "./RunModeSelect";
66
import "./TaskInput.css";
77

8+
type LocalWorkspaceMode = "worktree" | "root";
9+
810
interface TaskInputEditorProps {
911
editor: Editor | null;
1012
isCreatingTask: boolean;
11-
workspaceMode: WorkspaceMode;
12-
onWorkspaceModeChange: (mode: WorkspaceMode) => void;
13+
runMode: RunMode;
14+
localWorkspaceMode: LocalWorkspaceMode;
15+
onLocalWorkspaceModeChange: (mode: LocalWorkspaceMode) => void;
1316
canSubmit: boolean;
1417
onSubmit: () => void;
1518
hasDirectory: boolean;
@@ -18,13 +21,15 @@ interface TaskInputEditorProps {
1821
export function TaskInputEditor({
1922
editor,
2023
isCreatingTask,
21-
workspaceMode,
22-
onWorkspaceModeChange,
24+
runMode,
25+
localWorkspaceMode,
26+
onLocalWorkspaceModeChange,
2327
canSubmit,
2428
onSubmit,
2529
hasDirectory,
2630
}: TaskInputEditorProps) {
27-
const isWorktreeMode = workspaceMode === "worktree";
31+
const isWorktreeMode = localWorkspaceMode === "worktree";
32+
const isCloudMode = runMode === "cloud";
2833

2934
const getSubmitTooltip = () => {
3035
if (canSubmit) return "Create task";
@@ -104,29 +109,33 @@ export function TaskInputEditor({
104109
</Flex>
105110

106111
<Flex justify="end" align="center" gap="4" px="3" pb="3">
107-
<Tooltip
108-
content={
109-
isWorktreeMode
110-
? "Work in a separate directory with its own branch"
111-
: "Work directly in the selected folder"
112-
}
113-
>
114-
<IconButton
115-
size="1"
116-
variant="ghost"
117-
onClick={(e) => {
118-
e.stopPropagation();
119-
onWorkspaceModeChange(isWorktreeMode ? "root" : "worktree");
120-
}}
121-
className="worktree-toggle-button"
122-
data-active={isWorktreeMode}
112+
{!isCloudMode && (
113+
<Tooltip
114+
content={
115+
isWorktreeMode
116+
? "Work in a separate directory with its own branch"
117+
: "Work directly in the selected folder"
118+
}
123119
>
124-
<GitBranchIcon
125-
size={16}
126-
weight={isWorktreeMode ? "fill" : "regular"}
127-
/>
128-
</IconButton>
129-
</Tooltip>
120+
<IconButton
121+
size="1"
122+
variant="ghost"
123+
onClick={(e) => {
124+
e.stopPropagation();
125+
onLocalWorkspaceModeChange(
126+
isWorktreeMode ? "root" : "worktree",
127+
);
128+
}}
129+
className="worktree-toggle-button"
130+
data-active={isWorktreeMode}
131+
>
132+
<GitBranchIcon
133+
size={16}
134+
weight={isWorktreeMode ? "fill" : "regular"}
135+
/>
136+
</IconButton>
137+
</Tooltip>
138+
)}
130139

131140
<Tooltip content={getSubmitTooltip()}>
132141
<IconButton

0 commit comments

Comments
 (0)