From 8324949b54c9aadc5d5db1b6cebbd10325a0ea79 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Feb 2026 15:22:17 -0800 Subject: [PATCH] Enrich branches with worktree paths, fix toolbar display, and improve HMR config - Add worktreePath to gitBranchSchema and populate from `git worktree list --porcelain` - Redirect to existing worktree when selecting a branch that's already checked out (instead of failing with "already used by worktree") - Distinguish main worktree (project cwd) from secondary worktrees in selection logic - Fix toolbar to show three states: Local, Worktree (existing), New worktree (pending) - Add explicit protocol/host to Vite HMR config for reliable Electron WebSocket - Auto-open DevTools in dev mode for HMR debugging Co-Authored-By: Claude Opus 4.6 --- apps/desktop/src/main.ts | 1 + apps/server/src/git.ts | 34 +++++++++++++++++++---- apps/web/src/components/BranchToolbar.tsx | 23 +++++++++++---- apps/web/vite.config.ts | 11 +++++++- packages/contracts/src/git.ts | 1 + 5 files changed, 58 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index de9e839fdc56f95fb317bad9f653c79b91a1d56e..eada6aae7747f54cd6862eb2fc3d34bee0a4de4b 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -186,6 +186,7 @@ function createWindow(): BrowserWindow { if (isDevelopment) { void window.loadURL(process.env.VITE_DEV_SERVER_URL as string); + window.webContents.openDevTools({ mode: "detach" }); } else { if (!fs.existsSync(WEB_ENTRY)) { throw new Error(`Web bundle missing at ${WEB_ENTRY}`); diff --git a/apps/server/src/git.ts b/apps/server/src/git.ts index 59fb1b6946cfdf86c964dc0722b68036f2756035..718cf7e47ed392a5206c8e9554b03e4cf8950d7c 100644 --- a/apps/server/src/git.ts +++ b/apps/server/src/git.ts @@ -123,19 +123,41 @@ export async function listGitBranches(input: GitListBranchesInput): Promise(); + if (worktreeList.code === 0) { + let currentPath: string | null = null; + for (const line of worktreeList.stdout.split("\n")) { + if (line.startsWith("worktree ")) { + currentPath = line.slice("worktree ".length); + } else if (line.startsWith("branch refs/heads/") && currentPath) { + worktreeMap.set(line.slice("branch refs/heads/".length), currentPath); + } else if (line === "") { + currentPath = null; + } + } + } + const branches = result.stdout .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0) - .map((line) => ({ - name: line.replace(/^[*+]\s+/, ""), - current: line.startsWith("* "), - isDefault: line.replace(/^[*+]\s+/, "") === defaultBranch, - })) + .map((line) => { + const name = line.replace(/^[*+]\s+/, ""); + return { + name, + current: line.startsWith("* "), + isDefault: name === defaultBranch, + worktreePath: worktreeMap.get(name) ?? null, + }; + }) .toSorted((a, b) => { if (a.current !== b.current) return a.current ? -1 : 1; if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1; diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index aed876f2035041d0faf0a451d5d8615f93bc7c41..f28aacac6174252eb3777705c59773abbd16a556 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -122,6 +122,17 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B return; } + // If the branch already lives in a secondary worktree, point the thread + // there instead of trying to checkout (which git would reject). + // Ignore the main worktree (project cwd) — that's just a normal local checkout. + const isSecondaryWorktree = branch.worktreePath && branch.worktreePath !== activeProject?.cwd; + if (isSecondaryWorktree) { + setThreadError(null); + setThreadBranch(branch.name, branch.worktreePath); + setIsBranchMenuOpen(false); + return; + } + checkoutMutation.mutate(branch.name, { onSuccess: () => { setThreadError(null); @@ -150,9 +161,9 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B return (
- {envLocked ? ( + {envLocked || activeWorktreePath ? ( - {activeThread.worktreePath ? "Worktree" : "Local"} + {activeWorktreePath ? "Worktree" : "Local"} ) : ( diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index f65e603210424b6b5a928b2abd1165d2752198cb..34f6a3f6312f88b63e65b76776fdcd3014af7484 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -2,6 +2,8 @@ import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; +const port = Number(process.env.PORT ?? 5173); + export default defineConfig({ plugins: [ react({ @@ -16,8 +18,15 @@ export default defineConfig({ "import.meta.env.VITE_WS_URL": JSON.stringify(process.env.VITE_WS_URL ?? ""), }, server: { - port: Number(process.env.PORT ?? 5173), + port, strictPort: true, + hmr: { + // Explicit config so Vite's HMR WebSocket connects reliably + // inside Electron's BrowserWindow. Vite 8 uses console.debug for + // connection logs — enable "Verbose" in DevTools to see them. + protocol: "ws", + host: "localhost", + }, }, build: { outDir: "dist", diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index be5d568c202a83edf09bf252a39b732671bddecf..e53a600c59bc74134d72c114431feff718784486 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -40,6 +40,7 @@ export const gitBranchSchema = z.object({ name: z.string().min(1), current: z.boolean(), isDefault: z.boolean(), + worktreePath: z.string().min(1).nullable(), }); export const gitWorktreeSchema = z.object({ -- 2.54.0