mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-28 00:02:42 +00:00
feat: add merge endpoint and button, remove unused close/send endpoints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5eb308ad35
commit
96c2d88d91
@@ -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<Response> {
|
||||
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
|
||||
|
||||
@@ -239,12 +239,12 @@ export async function openWorktree(name: string): Promise<string> {
|
||||
return runChecked(["workmux", "open", name]);
|
||||
}
|
||||
|
||||
export async function closeWorktree(name: string): Promise<string> {
|
||||
return runChecked(["workmux", "close", name]);
|
||||
}
|
||||
|
||||
export async function sendPrompt(name: string, prompt: string): Promise<string> {
|
||||
return runChecked(["workmux", "send", name, prompt]);
|
||||
export async function mergeWorktree(name: string): Promise<string> {
|
||||
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<string> {
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
let worktrees = $state<WorktreeInfo[]>([]);
|
||||
let selectedBranch = $state<string | null>(null);
|
||||
let removeBranch = $state<string | null>(null);
|
||||
let mergeBranch = $state<string | null>(null);
|
||||
let merging = $state(false);
|
||||
let mergeError = $state("");
|
||||
let removingBranches = $state<Set<string>>(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}
|
||||
<Terminal worktree={selectedBranch} />
|
||||
<Terminal worktree={selectedBranch!} />
|
||||
{/key}
|
||||
{:else}
|
||||
<div class="flex-1 flex items-center justify-center text-muted text-sm">
|
||||
@@ -195,6 +224,18 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if mergeBranch}
|
||||
<ConfirmDialog
|
||||
message={`Merge worktree "${mergeBranch}" into main? The worktree will be removed after merging.`}
|
||||
confirmLabel="Merge"
|
||||
variant="accent"
|
||||
loading={merging}
|
||||
error={mergeError}
|
||||
onconfirm={handleMerge}
|
||||
oncancel={() => { mergeBranch = null; mergeError = ""; }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if showSettingsDialog}
|
||||
<SettingsDialog
|
||||
onsave={(host) => { sshHost = host; showSettingsDialog = false; }}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
let { message, loading = false, onconfirm, oncancel }: {
|
||||
let { message, loading = false, error = "", confirmLabel = "Remove", variant = "danger", onconfirm, oncancel }: {
|
||||
message: string;
|
||||
loading?: boolean;
|
||||
error?: string;
|
||||
confirmLabel?: string;
|
||||
variant?: "danger" | "accent";
|
||||
onconfirm: () => void;
|
||||
oncancel: () => void;
|
||||
} = $props();
|
||||
@@ -19,14 +22,14 @@
|
||||
<form method="dialog" onsubmit={(e) => { e.preventDefault(); onconfirm(); }}>
|
||||
<h2 class="text-base mb-4">Confirm</h2>
|
||||
<p class="text-[13px] text-muted mb-6">{message}</p>
|
||||
{#if error}<p class="text-[12px] text-danger mb-4 -mt-2 whitespace-pre-wrap">{error}</p>{/if}
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" class={btn} onclick={oncancel} disabled={loading}>Cancel</button>
|
||||
<button
|
||||
autofocus
|
||||
type="submit"
|
||||
class="{btn} !bg-danger !text-white !border-danger hover:!opacity-90 disabled:!opacity-50 disabled:!cursor-not-allowed flex items-center gap-1.5"
|
||||
class="{btn} !text-white hover:!opacity-90 disabled:!opacity-50 disabled:!cursor-not-allowed flex items-center gap-1.5 {variant === 'accent' ? '!bg-accent !border-accent' : '!bg-danger !border-danger'}"
|
||||
disabled={loading}
|
||||
>{#if loading}<span class="spinner"></span>{/if} Remove</button>
|
||||
>{#if loading}<span class="spinner"></span>{/if} {confirmLabel}</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
class="w-full px-2.5 py-1.5 rounded-md border border-edge bg-surface text-primary text-[13px] placeholder:text-muted/50 outline-none focus:border-accent"
|
||||
placeholder="e.g. devbox or 10.0.0.5"
|
||||
bind:value={sshHost}
|
||||
autofocus
|
||||
/>
|
||||
<p class="text-[11px] text-muted mt-1.5">
|
||||
Must match an entry in your local <code class="text-accent/80">~/.ssh/config</code>. Leave empty for local mode.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { WorktreeInfo } from "./types";
|
||||
|
||||
let { name, worktree, sshHost, onremove, onsettings }: {
|
||||
let { name, worktree, sshHost, onmerge, onremove, onsettings }: {
|
||||
name: string | null;
|
||||
worktree: WorktreeInfo | undefined;
|
||||
sshHost: string;
|
||||
onmerge: () => void;
|
||||
onremove: () => void;
|
||||
onsettings: () => void;
|
||||
} = $props();
|
||||
@@ -58,6 +59,7 @@
|
||||
{#if name}
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="text-xs px-2 py-0.5 rounded-xl bg-hover">{worktree?.status || worktree?.agent || ""}</span>
|
||||
<button class="{btn} !text-accent !border-accent hover:!bg-accent/10" onclick={onmerge} title="Merge worktree">Merge</button>
|
||||
<button class="{btn} !text-danger !border-danger hover:!bg-danger/10" onclick={onremove} title="Remove worktree">Remove</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -36,13 +36,6 @@ export function openWorktree(name: string): Promise<unknown> {
|
||||
return api(`worktrees/${encodeURIComponent(name)}/open`, { method: "POST" });
|
||||
}
|
||||
|
||||
export function closeWorktree(name: string): Promise<unknown> {
|
||||
return api(`worktrees/${encodeURIComponent(name)}/close`, { method: "POST" });
|
||||
}
|
||||
|
||||
export function sendPrompt(name: string, prompt: string): Promise<unknown> {
|
||||
return api(`worktrees/${encodeURIComponent(name)}/send`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ prompt }),
|
||||
});
|
||||
export function mergeWorktree(name: string): Promise<unknown> {
|
||||
return api(`worktrees/${encodeURIComponent(name)}/merge`, { method: "POST" });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user