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:
centdix
2026-02-21 19:11:26 +00:00
co-authored by Claude Opus 4.6
parent bead746bb8
commit 1e0245ca9a
5 changed files with 63 additions and 17 deletions
+6 -3
View File
@@ -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);
}
+15 -5
View File
@@ -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)
+3 -3
View File
@@ -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
+3 -1
View File
@@ -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 }),
});
}