diff --git a/dev-dashboard/backend/src/server.ts b/dev-dashboard/backend/src/server.ts index c0fd777d37..270e33112b 100644 --- a/dev-dashboard/backend/src/server.ts +++ b/dev-dashboard/backend/src/server.ts @@ -4,8 +4,7 @@ import { addWorktree, removeWorktree, openWorktree, - closeWorktree, - sendPrompt, + mergeWorktree, readEnvLocal, type Profile, type Agent, @@ -226,22 +225,13 @@ async function handleApi(req: Request, url: URL): Promise { return jsonResponse({ message: await openWorktree(name) }); } - // POST /api/worktrees/:name/close - if (parts[0] === "worktrees" && parts.length === 3 && parts[2] === "close" && method === "POST") { + // POST /api/worktrees/:name/merge + if (parts[0] === "worktrees" && parts.length === 3 && parts[2] === "merge" && method === "POST") { const name = decodeURIComponent(parts[1]); - console.log(`[worktree:close] name=${name}`); - return jsonResponse({ message: await closeWorktree(name) }); - } - - // POST /api/worktrees/:name/send - if (parts[0] === "worktrees" && parts.length === 3 && parts[2] === "send" && method === "POST") { - const name = decodeURIComponent(parts[1]); - const body = await req.json() as { prompt?: string }; - if (!body.prompt) { - return errorResponse("prompt is required", 400); - } - console.log(`[worktree:send] name=${name} prompt="${body.prompt.slice(0, 80)}"`); - return jsonResponse({ message: await sendPrompt(name, body.prompt) }); + console.log(`[worktree:merge] name=${name}`); + const result = await mergeWorktree(name); + console.log(`[worktree:merge] done name=${name}: ${result}`); + return jsonResponse({ message: result }); } // GET /api/worktrees/:name/status diff --git a/dev-dashboard/backend/src/workmux.ts b/dev-dashboard/backend/src/workmux.ts index 3ae4d33599..edf06628b7 100644 --- a/dev-dashboard/backend/src/workmux.ts +++ b/dev-dashboard/backend/src/workmux.ts @@ -239,12 +239,12 @@ export async function openWorktree(name: string): Promise { return runChecked(["workmux", "open", name]); } -export async function closeWorktree(name: string): Promise { - return runChecked(["workmux", "close", name]); -} - -export async function sendPrompt(name: string, prompt: string): Promise { - return runChecked(["workmux", "send", name, prompt]); +export async function mergeWorktree(name: string): Promise { + console.log(`[workmux:merge] running: workmux merge ${name}`); + stopForwarding(name); + const result = await runChecked(["workmux", "merge", name]); + console.log(`[workmux:merge] result: ${result}`); + return result; } export async function getTmuxSession(): Promise { diff --git a/dev-dashboard/frontend/src/App.svelte b/dev-dashboard/frontend/src/App.svelte index 66a720c94c..3aa4a57957 100644 --- a/dev-dashboard/frontend/src/App.svelte +++ b/dev-dashboard/frontend/src/App.svelte @@ -13,6 +13,9 @@ let worktrees = $state([]); let selectedBranch = $state(null); let removeBranch = $state(null); + let mergeBranch = $state(null); + let merging = $state(false); + let mergeError = $state(""); let removingBranches = $state>(new Set()); const SSH_STORAGE_KEY = "wt-ssh-host"; let showCreateDialog = $state(false); @@ -84,6 +87,31 @@ } } + async function handleMerge() { + const branch = mergeBranch; + if (!branch) return; + + merging = true; + mergeError = ""; + try { + await api.mergeWorktree(branch); + mergeBranch = null; + + if (selectedBranch === branch) { + const idx = visibleWorktrees.findIndex((w) => w.branch === branch); + const neighbor = visibleWorktrees[idx - 1] ?? visibleWorktrees[idx + 1]; + const isNeighborMain = neighbor && (neighbor.path === "(here)" || neighbor.branch === "main"); + selectedBranch = neighbor && !isNeighborMain ? neighbor.branch : null; + } + + await refresh(); + } catch (err) { + mergeError = err instanceof Error ? err.message : String(err); + } finally { + merging = false; + } + } + function selectNeighborWorktree(direction: -1 | 1) { const selectable = visibleWorktrees.filter( (w) => w.path !== "(here)" && w.branch !== "main" && !removingBranches.has(w.branch) @@ -102,7 +130,7 @@ function handleKeydown(e: KeyboardEvent) { // Ignore shortcuts when a dialog is open (let dialog handle its own keys) - if (showCreateDialog || removeBranch) return; + if (showCreateDialog || removeBranch || mergeBranch) return; const mod = e.metaKey || e.ctrlKey; if (!mod) return; @@ -157,13 +185,14 @@ name={selectedBranch} worktree={selectedWorktree} {sshHost} + onmerge={() => { if (selectedBranch) mergeBranch = selectedBranch; }} onremove={() => { if (selectedBranch) removeBranch = selectedBranch; }} onsettings={() => (showSettingsDialog = true)} /> {#if canConnect} {#key selectedBranch} - + {/key} {:else}
@@ -195,6 +224,18 @@ /> {/if} +{#if mergeBranch} + { mergeBranch = null; mergeError = ""; }} + /> +{/if} + {#if showSettingsDialog} { sshHost = host; showSettingsDialog = false; }} diff --git a/dev-dashboard/frontend/src/lib/ConfirmDialog.svelte b/dev-dashboard/frontend/src/lib/ConfirmDialog.svelte index fa90579d85..c2c0f895bb 100644 --- a/dev-dashboard/frontend/src/lib/ConfirmDialog.svelte +++ b/dev-dashboard/frontend/src/lib/ConfirmDialog.svelte @@ -1,7 +1,10 @@