mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-21 00:03:31 +00:00
feat: add codex as agent choice in worktree creation dialog
Add agent selector (Claude/Codex) to the create worktree dialog, orthogonal to the profile choice. Selection is persisted alongside the profile default. Backend builds the appropriate command per agent: codex uses --full-auto for sandbox, claude uses --dangerously-skip-permissions with --append-system-prompt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
bead746bb8
commit
1e0245ca9a
@@ -8,6 +8,7 @@ import {
|
||||
sendPrompt,
|
||||
readEnvLocal,
|
||||
type Profile,
|
||||
type Agent,
|
||||
} from "./workmux";
|
||||
import { reconcileForwarding, stopAll } from "./socat";
|
||||
import {
|
||||
@@ -191,14 +192,16 @@ async function handleApi(req: Request, url: URL): Promise<Response> {
|
||||
|
||||
// POST /api/worktrees
|
||||
if (parts[0] === "worktrees" && parts.length === 1 && method === "POST") {
|
||||
const body = await req.json() as { branch?: string; prompt?: string; profile?: string };
|
||||
const body = await req.json() as { branch?: string; prompt?: string; profile?: string; agent?: string };
|
||||
if (!body.branch) {
|
||||
return errorResponse("branch is required", 400);
|
||||
}
|
||||
const validProfiles = ["full", "agent-only", "agent-yolo"] as const;
|
||||
const validAgents = ["claude", "codex"] as const;
|
||||
const profile = validProfiles.includes(body.profile as any) ? body.profile as Profile : "agent-only";
|
||||
console.log(`[worktree:add] branch=${body.branch} profile=${profile}${body.prompt ? ` prompt="${body.prompt.slice(0, 80)}"` : ""}`);
|
||||
const result = await addWorktree(body.branch, { prompt: body.prompt, profile });
|
||||
const agent = validAgents.includes(body.agent as any) ? body.agent as Agent : "claude";
|
||||
console.log(`[worktree:add] branch=${body.branch} agent=${agent} profile=${profile}${body.prompt ? ` prompt="${body.prompt.slice(0, 80)}"` : ""}`);
|
||||
const result = await addWorktree(body.branch, { prompt: body.prompt, profile, agent });
|
||||
console.log(`[worktree:add] done branch=${body.branch}: ${result}`);
|
||||
return jsonResponse({ message: result }, 201);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ async function runChecked(args: string[]): Promise<string> {
|
||||
}
|
||||
|
||||
export type Profile = "full" | "agent-only" | "agent-yolo";
|
||||
export type Agent = "claude" | "codex";
|
||||
|
||||
export { readEnvLocal } from "./env";
|
||||
|
||||
@@ -99,9 +100,17 @@ function buildSystemPrompt(profile: Profile, env: Record<string, string>): strin
|
||||
return lines.join(" ");
|
||||
}
|
||||
|
||||
function buildClaudeCmd(profile: Profile, env: Record<string, string>): string {
|
||||
function buildAgentCmd(profile: Profile, env: Record<string, string>, agent: Agent): string {
|
||||
const prompt = buildSystemPrompt(profile, env);
|
||||
|
||||
if (agent === "codex") {
|
||||
if (profile === "agent-yolo") {
|
||||
return `workmux sandbox agent -- codex --full-auto`;
|
||||
}
|
||||
return "codex";
|
||||
}
|
||||
|
||||
// Claude
|
||||
if (profile === "agent-yolo") {
|
||||
// Double-escape: outer single quotes for the host shell,
|
||||
// inner double quotes to survive workmux sandbox's sh -c
|
||||
@@ -114,9 +123,10 @@ function buildClaudeCmd(profile: Profile, env: Record<string, string>): string {
|
||||
|
||||
export async function addWorktree(
|
||||
branch: string,
|
||||
opts?: { prompt?: string; profile?: Profile }
|
||||
opts?: { prompt?: string; profile?: Profile; agent?: Agent }
|
||||
): Promise<string> {
|
||||
const profile = opts?.profile ?? "full";
|
||||
const agent = opts?.agent ?? "claude";
|
||||
const args: string[] = ["workmux", "add", "-b"]; // -b = background (don't switch tmux)
|
||||
|
||||
// Skip default pane commands for non-full profiles
|
||||
@@ -160,9 +170,9 @@ export async function addWorktree(
|
||||
Bun.spawnSync(["tmux", "kill-pane", "-t", `${windowTarget}.${paneIds[i]}`]);
|
||||
}
|
||||
// Build and send claude command with environment-aware system prompt
|
||||
const claudeCmd = buildClaudeCmd(profile, env);
|
||||
console.log(`[workmux] sending command to ${windowTarget}.0:\n${claudeCmd}`);
|
||||
Bun.spawnSync(["tmux", "send-keys", "-t", `${windowTarget}.0`, claudeCmd, "Enter"]);
|
||||
const agentCmd = buildAgentCmd(profile, env, agent);
|
||||
console.log(`[workmux] sending command to ${windowTarget}.0:\n${agentCmd}`);
|
||||
Bun.spawnSync(["tmux", "send-keys", "-t", `${windowTarget}.0`, agentCmd, "Enter"]);
|
||||
// Open a shell pane on the right (1/3 width) in the worktree dir
|
||||
Bun.spawnSync(["tmux", "split-window", "-h", "-t", `${windowTarget}.0`, "-l", "33%", "-c", wtDir]);
|
||||
// Keep focus on the agent pane (left)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import ConfirmDialog from "./lib/ConfirmDialog.svelte";
|
||||
import CreateWorktreeDialog from "./lib/CreateWorktreeDialog.svelte";
|
||||
import type { WorktreeInfo } from "./lib/types";
|
||||
import type { Profile } from "./lib/api";
|
||||
import type { Profile, Agent } from "./lib/api";
|
||||
import * as api from "./lib/api";
|
||||
|
||||
let worktrees = $state<WorktreeInfo[]>([]);
|
||||
@@ -40,11 +40,11 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
async function handleCreate(name: string, profile: Profile) {
|
||||
async function handleCreate(name: string, profile: Profile, agent: Agent) {
|
||||
const branch = name || randomName(8);
|
||||
creating = true;
|
||||
try {
|
||||
await api.createWorktree(branch, profile);
|
||||
await api.createWorktree(branch, profile, agent);
|
||||
await api.openWorktree(branch);
|
||||
showCreateDialog = false;
|
||||
await refresh();
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { Profile } from "./api";
|
||||
import type { Profile, Agent } from "./api";
|
||||
|
||||
const AGENTS: { value: Agent; label: string }[] = [
|
||||
{ value: "claude", label: "Claude" },
|
||||
{ value: "codex", label: "Codex" },
|
||||
];
|
||||
|
||||
const PROFILES: { value: Profile; label: string }[] = [
|
||||
{ value: "agent-only", label: "Agent only" },
|
||||
@@ -9,14 +14,17 @@
|
||||
|
||||
let { loading = false, oncreate, oncancel }: {
|
||||
loading?: boolean;
|
||||
oncreate: (name: string, profile: Profile) => void;
|
||||
oncreate: (name: string, profile: Profile, agent: Agent) => void;
|
||||
oncancel: () => void;
|
||||
} = $props();
|
||||
|
||||
const STORAGE_KEY = "wt-default-profile";
|
||||
const AGENT_STORAGE_KEY = "wt-default-agent";
|
||||
const savedProfile = localStorage.getItem(STORAGE_KEY) as Profile | null;
|
||||
const savedAgent = localStorage.getItem(AGENT_STORAGE_KEY) as Agent | null;
|
||||
|
||||
let name = $state("");
|
||||
let agent = $state<Agent>(savedAgent ?? "claude");
|
||||
let profile = $state<Profile>(savedProfile ?? "agent-only");
|
||||
let saveDefault = $state(false);
|
||||
|
||||
@@ -32,9 +40,14 @@
|
||||
<dialog bind:this={dialogEl} onclose={oncancel} class="bg-sidebar text-primary border border-edge rounded-xl p-6 max-w-[380px] w-[90%]">
|
||||
<form method="dialog" onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (saveDefault) localStorage.setItem(STORAGE_KEY, profile);
|
||||
else localStorage.removeItem(STORAGE_KEY);
|
||||
oncreate(name.trim(), profile);
|
||||
if (saveDefault) {
|
||||
localStorage.setItem(STORAGE_KEY, profile);
|
||||
localStorage.setItem(AGENT_STORAGE_KEY, agent);
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
localStorage.removeItem(AGENT_STORAGE_KEY);
|
||||
}
|
||||
oncreate(name.trim(), profile, agent);
|
||||
}}>
|
||||
<h2 class="text-base mb-4">New Worktree</h2>
|
||||
<div class="mb-4">
|
||||
@@ -47,6 +60,24 @@
|
||||
bind:value={name}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2 mb-4">
|
||||
{#each AGENTS as a}
|
||||
<label
|
||||
class="flex-1 flex items-center justify-center gap-2 p-2.5 rounded-lg border cursor-pointer text-[13px] transition-colors
|
||||
{agent === a.value ? 'border-accent bg-accent/10' : 'border-edge hover:bg-hover'}"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="agent"
|
||||
value={a.value}
|
||||
checked={agent === a.value}
|
||||
onchange={() => (agent = a.value)}
|
||||
class="accent-[var(--accent)]"
|
||||
/>
|
||||
{a.label}
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 mb-6">
|
||||
{#each PROFILES as p}
|
||||
<label
|
||||
|
||||
@@ -15,14 +15,16 @@ export function fetchWorktrees(): Promise<WorktreeInfo[]> {
|
||||
}
|
||||
|
||||
export type Profile = "full" | "agent-only" | "agent-yolo";
|
||||
export type Agent = "claude" | "codex";
|
||||
|
||||
export function createWorktree(
|
||||
branch: string,
|
||||
profile: Profile = "agent-only",
|
||||
agent: Agent = "claude",
|
||||
): Promise<unknown> {
|
||||
return api("worktrees", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ branch, profile }),
|
||||
body: JSON.stringify({ branch, profile, agent }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user