diff --git a/apps/desktop/electron/api.ts b/apps/desktop/electron/api.ts index d8a4645..3de3cfe 100644 --- a/apps/desktop/electron/api.ts +++ b/apps/desktop/electron/api.ts @@ -95,7 +95,7 @@ export async function listWorkflows() { groups: workflowConfig.groups, workflowConfig, taskInputFields: deriveTaskInputFields(raw), - worktree: raw.worktree || { enabled: false, files: [] }, + worktree: raw.worktree || { enabled: false, files: [], customFiles: [], removeOnComplete: false, useCustomSetupScript: false, setupScript: "" }, }); } catch {} } diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 9b3d433..fb5e994 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -1,5 +1,6 @@ import { spawn } from "child_process"; import { app, BrowserWindow, ipcMain } from "electron"; +import { mkdirSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { killAllChildren } from "../../../packages/core-lib/claude"; @@ -46,6 +47,12 @@ let mainWindow; const __dirname = dirname(fileURLToPath(import.meta.url)); const rendererDevServerUrl = process.env.ELECTRON_RENDERER_URL; const isDev = Boolean(rendererDevServerUrl); +const userDataDirOverride = process.env.DEV_WORKFLOW_USER_DATA_DIR; + +if (userDataDirOverride) { + mkdirSync(userDataDirOverride, { recursive: true }); + app.setPath("userData", userDataDirOverride); +} function spawnDetached(command, args) { return new Promise((resolve, reject) => { @@ -189,8 +196,9 @@ function registerIpcHandlers() { } app.whenReady().then(async () => { - setRuntimeStorageDir(app.getPath("userData")); - setRuntimeBaseDir(join(app.getPath("userData"), "tasks")); + const userDataDir = app.getPath("userData"); + setRuntimeStorageDir(userDataDir); + setRuntimeBaseDir(join(userDataDir, "tasks")); registerIpcHandlers(); try { await createWindow(); diff --git a/apps/desktop/local-server-controllers/workflowController.ts b/apps/desktop/local-server-controllers/workflowController.ts index cee4bf9..58ff471 100644 --- a/apps/desktop/local-server-controllers/workflowController.ts +++ b/apps/desktop/local-server-controllers/workflowController.ts @@ -211,7 +211,7 @@ router.get("/workflows", async (req, res) => { groups: workflowConfig.groups, workflowConfig, taskInputFields: deriveTaskInputFields(raw), - worktree: raw.worktree || { enabled: false, files: [] }, + worktree: raw.worktree || { enabled: false, files: [], customFiles: [], removeOnComplete: false, useCustomSetupScript: false, setupScript: "" }, }); } catch {} } diff --git a/apps/desktop/renderer/src/WorkflowEditor.test.tsx b/apps/desktop/renderer/src/WorkflowEditor.test.tsx index 7031989..14d1d44 100644 --- a/apps/desktop/renderer/src/WorkflowEditor.test.tsx +++ b/apps/desktop/renderer/src/WorkflowEditor.test.tsx @@ -1,5 +1,6 @@ -import { render, screen, waitFor } from "@testing-library/react"; -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { cleanup, render, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; const mocks = vi.hoisted(() => ({ desktopApi: {}, @@ -48,10 +49,12 @@ const workflow = { nodePositions: {}, }, worktree: { - enabled: false, - files: [], + enabled: true, + files: [".env"], customFiles: [], removeOnComplete: false, + useCustomSetupScript: false, + setupScript: "", }, steps: [ { @@ -85,6 +88,10 @@ function renderWorkflowEditor(filename = "default-codex.json") { } describe("WorkflowEditor", () => { + afterEach(() => { + cleanup(); + }); + beforeEach(() => { sessionStorage.clear(); for (const key of Object.keys(mocks.desktopApi)) { @@ -106,4 +113,43 @@ describe("WorkflowEditor", () => { }); expect(screen.getByDisplayValue("Default Codex")).toBeInTheDocument(); }); + + test("uses the built-in worktree setup until custom setup is enabled", async () => { + const user = userEvent.setup(); + renderWorkflowEditor(); + + await waitFor(() => { + expect(screen.getByDisplayValue("Default Codex")).toBeInTheDocument(); + }); + + await user.click(screen.getByRole("button", { name: "Workflow Setup" })); + + expect(screen.getByText(".env")).toBeInTheDocument(); + expect(screen.queryByPlaceholderText("pnpm install")).not.toBeInTheDocument(); + + const customSetup = screen.getByText("Use custom setup script").closest("label"); + expect(customSetup).not.toBeNull(); + await user.click(within(customSetup).getByRole("checkbox")); + + expect(screen.queryByText(".env")).not.toBeInTheDocument(); + expect(screen.getByPlaceholderText("pnpm install")).toBeInTheDocument(); + }); + + test("requires a setup script before saving when custom setup is enabled", async () => { + const user = userEvent.setup(); + renderWorkflowEditor(); + + await waitFor(() => { + expect(screen.getByDisplayValue("Default Codex")).toBeInTheDocument(); + }); + + await user.click(screen.getByRole("button", { name: "Workflow Setup" })); + const customSetup = screen.getByText("Use custom setup script").closest("label"); + expect(customSetup).not.toBeNull(); + await user.click(within(customSetup).getByRole("checkbox")); + await user.click(screen.getByRole("button", { name: "Save" })); + + expect(await screen.findByText("Setup script is required when custom setup is enabled.")).toBeInTheDocument(); + expect(mocks.desktopApi.updateWorkflow).not.toHaveBeenCalled(); + }); }); diff --git a/apps/desktop/renderer/src/WorkflowEditor.tsx b/apps/desktop/renderer/src/WorkflowEditor.tsx index c75f759..d237739 100644 --- a/apps/desktop/renderer/src/WorkflowEditor.tsx +++ b/apps/desktop/renderer/src/WorkflowEditor.tsx @@ -227,6 +227,8 @@ function createDefaultWorkflow({ includeStartStep = true } = {}) { files: [...COMMON_WORKTREE_FILES], customFiles: [], removeOnComplete: false, + useCustomSetupScript: false, + setupScript: "", }, steps: includeStartStep ? [{ @@ -371,6 +373,7 @@ function normalizeWorkflow(raw) { ...(raw?.worktree || {}), files: Array.from(new Set(worktreeFiles)), customFiles, + useCustomSetupScript: raw?.worktree?.useCustomSetupScript === true, }, }; return relinkSteps(workflow); @@ -1157,6 +1160,10 @@ export default function WorkflowEditor({ filename, onClose, onSaved }) { setError(t("editor.phaseRequired")); return; } + if (dsl.worktree?.enabled && dsl.worktree?.useCustomSetupScript && !String(dsl.worktree.setupScript || "").trim()) { + setError("Setup script is required when custom setup is enabled."); + return; + } setSaving(true); setError(""); try { @@ -1237,13 +1244,6 @@ export default function WorkflowEditor({ filename, onClose, onSaved }) {
-
-
-

Runtime

-

LangGraph StateGraph

-
- DSL -
)} {workflow.worktree.enabled && ( +
+ +
+ )} + {workflow.worktree.enabled && workflow.worktree.useCustomSetupScript !== true && (
{displayedWorktreeFiles.map((file) => (
)} - {workflow.worktree.enabled && ( + {workflow.worktree.enabled && workflow.worktree.useCustomSetupScript !== true && (
@@ -1311,6 +1323,17 @@ export default function WorkflowEditor({ filename, onClose, onSaved }) {
)} + {workflow.worktree.enabled && workflow.worktree.useCustomSetupScript === true && ( +
+