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
2 changes: 1 addition & 1 deletion apps/desktop/electron/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}
Expand Down
12 changes: 10 additions & 2 deletions apps/desktop/electron/main.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}
Expand Down
54 changes: 50 additions & 4 deletions apps/desktop/renderer/src/WorkflowEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -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: {},
Expand Down Expand Up @@ -48,10 +49,12 @@ const workflow = {
nodePositions: {},
},
worktree: {
enabled: false,
files: [],
enabled: true,
files: [".env"],
customFiles: [],
removeOnComplete: false,
useCustomSetupScript: false,
setupScript: "",
},
steps: [
{
Expand Down Expand Up @@ -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)) {
Expand All @@ -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();
});
});
39 changes: 31 additions & 8 deletions apps/desktop/renderer/src/WorkflowEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ function createDefaultWorkflow({ includeStartStep = true } = {}) {
files: [...COMMON_WORKTREE_FILES],
customFiles: [],
removeOnComplete: false,
useCustomSetupScript: false,
setupScript: "",
},
steps: includeStartStep
? [{
Expand Down Expand Up @@ -371,6 +373,7 @@ function normalizeWorkflow(raw) {
...(raw?.worktree || {}),
files: Array.from(new Set(worktreeFiles)),
customFiles,
useCustomSetupScript: raw?.worktree?.useCustomSetupScript === true,
},
};
return relinkSteps(workflow);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1237,13 +1244,6 @@ export default function WorkflowEditor({ filename, onClose, onSaved }) {
</div>
<div className="min-h-0 overflow-y-auto p-5">
<section className="mb-4 rounded-lg border border-border bg-card/70 p-3">
<div className="mb-3 flex items-center justify-between gap-3">
<div>
<h2 className="text-xs font-semibold text-foreground">Runtime</h2>
<p className="text-[10px] text-muted-foreground">LangGraph StateGraph</p>
</div>
<Badge variant="success" className="text-[10px]">DSL</Badge>
</div>
<label className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
<span className="flex items-center gap-2">
<GitBranch className="h-3.5 w-3.5" />
Expand All @@ -1266,6 +1266,18 @@ export default function WorkflowEditor({ filename, onClose, onSaved }) {
</label>
)}
{workflow.worktree.enabled && (
<div className="mt-3">
<label className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
<span>Use custom setup script</span>
<input
type="checkbox"
checked={workflow.worktree.useCustomSetupScript === true}
onChange={(event) => updateWorktree({ useCustomSetupScript: event.target.checked })}
/>
</label>
</div>
)}
{workflow.worktree.enabled && workflow.worktree.useCustomSetupScript !== true && (
<div className="mt-3 space-y-2 rounded-md border border-border bg-background/70 p-2">
{displayedWorktreeFiles.map((file) => (
<label key={file} className="flex items-center gap-2 text-[11px] text-foreground">
Expand All @@ -1291,7 +1303,7 @@ export default function WorkflowEditor({ filename, onClose, onSaved }) {
))}
</div>
)}
{workflow.worktree.enabled && (
{workflow.worktree.enabled && workflow.worktree.useCustomSetupScript !== true && (
<div className="mt-3">
<label className="mb-1.5 block text-[10px] text-muted-foreground">Extra files or folders</label>
<div className="flex gap-2">
Expand All @@ -1311,6 +1323,17 @@ export default function WorkflowEditor({ filename, onClose, onSaved }) {
</div>
</div>
)}
{workflow.worktree.enabled && workflow.worktree.useCustomSetupScript === true && (
<div>
<textarea
value={workflow.worktree.setupScript || ""}
onChange={(event) => updateWorktree({ setupScript: event.target.value })}
placeholder="pnpm install"
rows={3}
className="w-full resize-y rounded-md border border-input bg-background px-3 py-2 font-mono text-xs text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-ring focus:ring-2 focus:ring-ring/20"
/>
</div>
)}
</section>

<section className="rounded-lg border border-border bg-card/70 p-3">
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/renderer/src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const messages = {
"home.gitWorktreeHint": "A sibling worktree will be created before the workflow starts.",
"home.gitWorktreeRemoveHint": "It will be removed automatically when the task is completed.",
"home.gitWorktreeFiles": "Migrated files: {files}",
"home.gitWorktreeCustomSetup": "Custom setup script will run instead of the built-in setup.",
"home.worktreeName": "Branch / Worktree Name",
"home.worktreeNamePlaceholder": "Leave blank to generate automatically",
"home.worktreeNameHint": "Branch keeps slashes, and the worktree folder uses hyphens.",
Expand Down Expand Up @@ -340,6 +341,7 @@ export const messages = {
"home.gitWorktreeHint": "工作流启动前会先创建一个同级 worktree。",
"home.gitWorktreeRemoveHint": "任务完成后会自动移除。",
"home.gitWorktreeFiles": "迁移文件:{files}",
"home.gitWorktreeCustomSetup": "会运行自定义 setup script,而不是系统内置 setup。",
"home.worktreeName": "Branch / Worktree 名称",
"home.worktreeNamePlaceholder": "留空时自动生成",
"home.worktreeNameHint": "branch 保留斜杠,worktree 文件夹会使用连字符。",
Expand Down
37 changes: 37 additions & 0 deletions apps/desktop/renderer/src/lib/worktree-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, test } from "vitest";
import { normalizeWorktreeConfig } from "../../../../../packages/core-lib/worktree";

describe("normalizeWorktreeConfig", () => {
test("uses built-in setup when custom setup is not enabled", () => {
const config = normalizeWorktreeConfig({
enabled: true,
files: [".env"],
useCustomSetupScript: false,
setupScript: "pnpm install",
});

expect(config.files).toEqual([".env"]);
expect(config.useCustomSetupScript).toBe(false);
expect(config.setupScript).toBe("");
});

test("requires a custom script when custom setup is enabled", () => {
expect(() => normalizeWorktreeConfig({
enabled: true,
useCustomSetupScript: true,
setupScript: " ",
})).toThrow("git worktree custom setup script is required");
});

test("keeps the custom script when custom setup is enabled", () => {
const config = normalizeWorktreeConfig({
enabled: true,
files: [".env"],
useCustomSetupScript: true,
setupScript: " pnpm install ",
});

expect(config.useCustomSetupScript).toBe(true);
expect(config.setupScript).toBe("pnpm install");
});
});
6 changes: 5 additions & 1 deletion apps/desktop/renderer/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,11 @@ function StartWorkflowModal({ workflows, workFolders, defaultFolder, defaultWork
{t("home.gitWorktreeRemoveHint")}
</div>
)}
{worktreeConfig.files.length > 0 && (
{worktreeConfig.useCustomSetupScript ? (
<div className="text-[10px] text-muted-foreground mt-1">
{t("home.gitWorktreeCustomSetup")}
</div>
) : worktreeConfig.files.length > 0 && (
<div className="text-[10px] text-muted-foreground mt-1">
{t("home.gitWorktreeFiles", { files: worktreeConfig.files.join(", ") })}
</div>
Expand Down
7 changes: 5 additions & 2 deletions apps/desktop/renderer/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

const localServerPort = process.env.LOCAL_SERVER_PORT || process.env.PORT || "3000";
const localServerUrl = `http://localhost:${localServerPort}`;

export default defineConfig({
base: "./",
plugins: [react(), tailwindcss()],
Expand All @@ -11,9 +14,9 @@ export default defineConfig({
},
server: {
proxy: {
"/api": "http://localhost:3000",
"/api": localServerUrl,
"/ws": {
target: "http://localhost:3000",
target: localServerUrl,
ws: true,
},
},
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { activeWorkflows } from "../../packages/core-lib/claude";

const __dirname = dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = process.env.PORT || 3000;
const PORT = process.env.LOCAL_SERVER_PORT || process.env.PORT || 3000;

app.use(cors());
app.use(express.json());
Expand Down
15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@
"main": "apps/desktop/electron/main.ts",
"packageManager": "pnpm@10.33.2",
"scripts": {
"dev:renderer": "pnpm --dir apps/desktop/renderer dev --host 127.0.0.1 --port 5173 --strictPort",
"dev:renderer": "pnpm --dir apps/desktop/renderer dev --host 127.0.0.1 --port ${RENDERER_PORT:-5173} --strictPort",
"prepare:electron": "tsc -p tsconfig.preload.json",
"dev:electron": "pnpm run prepare:electron && wait-on http://127.0.0.1:5173 && ELECTRON_RENDERER_URL=http://127.0.0.1:5173 NODE_OPTIONS='--import tsx' electronmon .",
"dev:desktop-ui": "concurrently -k --names \"renderer,electron\" --prefix name --prefix-colors \"blue,magenta\" \"pnpm exec tsx scripts/run-if-port-free.ts 5173 pnpm dev:renderer\" \"pnpm dev:electron\"",
"dev:desktop": "concurrently -k --names \"desktop,server,connector\" --prefix name --prefix-colors \"green,blue,cyan\" \"pnpm run dev:desktop-ui\" \"pnpm exec tsx scripts/run-if-port-free.ts 3000 pnpm run dev:server\" \"pnpm run dev:connector\"",
"dev:electron": "pnpm run prepare:electron && wait-on http://127.0.0.1:${RENDERER_PORT:-5173} && ELECTRON_RENDERER_URL=http://127.0.0.1:${RENDERER_PORT:-5173} NODE_OPTIONS='--import tsx' electronmon .",
"dev:desktop-ui": "concurrently -k --names \"renderer,electron\" --prefix name --prefix-colors \"blue,magenta\" \"pnpm exec tsx scripts/run-if-port-free.ts ${RENDERER_PORT:-5173} pnpm dev:renderer\" \"pnpm dev:electron\"",
"dev:desktop": "concurrently -k --names \"desktop,server,connector\" --prefix name --prefix-colors \"green,blue,cyan\" \"pnpm run dev:desktop-ui\" \"pnpm exec tsx scripts/run-if-port-free.ts ${LOCAL_SERVER_PORT:-3000} pnpm run dev:server\" \"pnpm run dev:connector\"",
"dev:backend": "pnpm exec tsx watch apps/backend/src/server.ts",
"dev:mobile": "cd apps/mobile && flutter run",
"dev:server": "pnpm exec tsx apps/desktop/server.ts",
"dev:connector": "pnpm exec tsx apps/desktop/connector/src/index.ts",
"dev:desktop-stack": "concurrently -k --names \"server,backend,connector\" --prefix name --prefix-colors \"blue,magenta,cyan\" \"pnpm exec tsx scripts/run-if-port-free.ts 3000 pnpm run dev:server\" \"pnpm exec tsx scripts/run-if-port-free.ts 8787 pnpm run dev:backend\" \"pnpm run dev:connector\"",
"dev:mobile-stack": "concurrently -k --names \"server,backend,connector\" --prefix name --prefix-colors \"blue,magenta,cyan\" \"pnpm exec tsx scripts/run-if-port-free.ts 3000 pnpm run dev:server\" \"pnpm exec tsx scripts/run-if-port-free.ts 8787 pnpm run dev:backend\" \"pnpm run dev:connector\"",
"dev:all": "concurrently -k --names \"desktop,backend\" --prefix name --prefix-colors \"green,magenta\" \"pnpm run dev:desktop\" \"pnpm exec tsx scripts/run-if-port-free.ts 8787 pnpm run dev:backend\"",
"dev:desktop-stack": "concurrently -k --names \"server,backend,connector\" --prefix name --prefix-colors \"blue,magenta,cyan\" \"pnpm exec tsx scripts/run-if-port-free.ts ${LOCAL_SERVER_PORT:-3000} pnpm run dev:server\" \"pnpm exec tsx scripts/run-if-port-free.ts ${BACKEND_PORT:-8787} pnpm run dev:backend\" \"pnpm run dev:connector\"",
"dev:mobile-stack": "concurrently -k --names \"server,backend,connector\" --prefix name --prefix-colors \"blue,magenta,cyan\" \"pnpm exec tsx scripts/run-if-port-free.ts ${LOCAL_SERVER_PORT:-3000} pnpm run dev:server\" \"pnpm exec tsx scripts/run-if-port-free.ts ${BACKEND_PORT:-8787} pnpm run dev:backend\" \"pnpm run dev:connector\"",
"dev:all": "concurrently -k --names \"desktop,backend\" --prefix name --prefix-colors \"green,magenta\" \"pnpm run dev:desktop\" \"pnpm exec tsx scripts/run-if-port-free.ts ${BACKEND_PORT:-8787} pnpm run dev:backend\"",
"dev:desktop:instance": "pnpm exec tsx scripts/dev-desktop-instance.ts",
"build:desktop": "pnpm run prepare:electron && pnpm --dir apps/desktop/renderer build",
"package:desktop": "pnpm build:desktop && electron-forge package",
"make:desktop": "pnpm build:desktop && electron-forge make",
Expand Down
4 changes: 4 additions & 0 deletions packages/core-lib/langgraph-runtime/dsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ function normalizeWorktree(rawWorktree) {
files: [],
customFiles: [],
removeOnComplete: false,
useCustomSetupScript: false,
setupScript: "",
};
}
if (!isObject(rawWorktree)) throw new Error("worktree must be an object");
Expand All @@ -107,6 +109,8 @@ function normalizeWorktree(rawWorktree) {
files: normalizeOptionalStringArray(rawWorktree.files, "worktree.files"),
customFiles: normalizeOptionalStringArray(rawWorktree.customFiles, "worktree.customFiles"),
removeOnComplete: rawWorktree.removeOnComplete === true,
useCustomSetupScript: rawWorktree.useCustomSetupScript === true,
setupScript: normalizeOptionalString(rawWorktree.setupScript),
};
}

Expand Down
Loading
Loading