Skip to content

Commit 1efbaef

Browse files
X-Skoprioclaudenjbrake
authored
feat(web): worktree toggle + cleaner new-session wizard (#978)
* feat(web): expose worktree toggle and restructure new-session wizard Adds a "Create a worktree" toggle to the web dashboard's new-session wizard so users can opt out of worktree creation (the backend already supported worktree_branch=null but the frontend always sent a value). Also restructures the wizard from 3 steps (Project, Settings, Review) into 4 linear steps (Project, Session, Agent, Review). Title and the worktree toggle/branch name now live on a dedicated Session step instead of being buried under Advanced settings on the Settings step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(web): move Group field to Session step Group is session metadata (organization/naming), not agent runtime config. Co-locating it with Title on the Session step keeps the Agent step focused on tool/sandbox/yolo plus advanced runtime settings, and gives the Session step more substance so it doesn't feel like a speed-bump for users who skip the worktree toggle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: njbrake <nathan@mozilla.ai>
1 parent 0cb9e97 commit 1efbaef

5 files changed

Lines changed: 126 additions & 54 deletions

File tree

web/src/components/session-wizard/SessionWizard.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fetchAgents, fetchGroups, fetchDockerStatus, fetchProfiles, fetchSettin
44
import { StepIndicator } from "./StepIndicator";
55
import type { StepDef, StepId } from "./StepIndicator";
66
import { ProjectStep } from "./steps/ProjectStep";
7+
import { SessionStep } from "./steps/SessionStep";
78
import { AgentStep } from "./steps/AgentStep";
89
import { ReviewStep } from "./steps/ReviewStep";
910
import { applyBranchOverride, getSubmittedBranch } from "./sessionNames";
@@ -13,6 +14,7 @@ export interface WizardData {
1314
title: string;
1415
worktreeBranch: string;
1516
worktreeBranchDirty: boolean;
17+
useWorktree: boolean;
1618
group: string;
1719
tool: string;
1820
profile: string;
@@ -54,6 +56,7 @@ type Action =
5456

5557
const initialData: WizardData = {
5658
path: "", title: "", worktreeBranch: "", worktreeBranchDirty: false,
59+
useWorktree: true,
5760
group: "", tool: "claude", profile: "",
5861
yoloMode: false, sandboxEnabled: false, sandboxImage: "", extraEnv: [],
5962
advancedEnabled: false, profileDirty: false,
@@ -114,11 +117,12 @@ function reducer(state: WizardState, action: Action): WizardState {
114117
}
115118
}
116119

117-
// Wizard: project path → agent/settings → review
120+
// Wizard: project path → session (title + worktree) → agent → review
118121
function computeSteps(_data: WizardData): StepDef[] {
119122
return [
120123
{ id: "project", label: "Project" },
121-
{ id: "agent", label: "Settings" },
124+
{ id: "session", label: "Session" },
125+
{ id: "agent", label: "Agent" },
122126
{ id: "review", label: "Review" },
123127
];
124128
}
@@ -156,7 +160,7 @@ export function SessionWizard({ onClose, onCreated, prefill }: Props) {
156160
: initialData;
157161

158162
const [state, dispatch] = useReducer(reducer, {
159-
currentStep: prefill?.skipToReview ? 2 : (prefill?.path ? 1 : 0),
163+
currentStep: prefill?.skipToReview ? 3 : (prefill?.path ? 1 : 0),
160164
data: prefillData, isSubmitting: false, error: null,
161165
agents: [], groups: [], profiles: [], dockerAvailable: false,
162166
});
@@ -201,8 +205,8 @@ export function SessionWizard({ onClose, onCreated, prefill }: Props) {
201205
path: d.path, tool: d.tool,
202206
title: d.title || undefined, group: d.group || undefined,
203207
yolo_mode: d.yoloMode,
204-
worktree_branch: getSubmittedBranch(d.title, d.worktreeBranch),
205-
create_new_branch: true,
208+
worktree_branch: d.useWorktree ? getSubmittedBranch(d.title, d.worktreeBranch) : undefined,
209+
create_new_branch: d.useWorktree,
206210
sandbox: d.sandboxEnabled,
207211
sandbox_image: d.sandboxEnabled ? d.sandboxImage : undefined,
208212
extra_env: d.sandboxEnabled && d.extraEnv.length > 0 ? d.extraEnv.filter(Boolean) : undefined,
@@ -224,6 +228,8 @@ export function SessionWizard({ onClose, onCreated, prefill }: Props) {
224228
switch (currentStepDef?.id) {
225229
case "project":
226230
return <ProjectStep data={state.data} onChange={handleChange} initialTab={prefill?.initialTab} />;
231+
case "session":
232+
return <SessionStep data={state.data} onChange={handleChange} />;
227233
case "agent":
228234
return (
229235
<AgentStep

web/src/components/session-wizard/StepIndicator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type StepId = "project" | "agent" | "container" | "advanced" | "review";
1+
export type StepId = "project" | "session" | "agent" | "container" | "advanced" | "review";
22

33
export interface StepDef {
44
id: StepId;

web/src/components/session-wizard/steps/AgentStep.tsx

Lines changed: 5 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface WizardData {
66
tool: string;
77
title: string;
88
worktreeBranch: string;
9+
useWorktree: boolean;
910
profile: string;
1011
profileDirty: boolean;
1112
sandboxEnabled: boolean;
@@ -16,7 +17,6 @@ interface WizardData {
1617
customInstruction: string;
1718
extraArgs: string;
1819
commandOverride: string;
19-
group: string;
2020
[key: string]: unknown;
2121
}
2222

@@ -191,7 +191,10 @@ export function AgentStep({ data, onChange, agents, profiles, dockerAvailable, o
191191
</div>
192192

193193
{isHostOnly && (
194-
<p className="text-xs text-status-warning mt-3 mb-3">{selectedAgent?.name} can only run on the host. Container option is disabled.</p>
194+
<p className="text-xs text-status-warning mt-3 mb-3">
195+
{selectedAgent?.name} can only run on the host. Container is disabled
196+
{data.useWorktree ? "; go back and turn off “Create a worktree” too." : "."}
197+
</p>
195198
)}
196199

197200
{/* Advanced settings (collapsible) */}
@@ -207,31 +210,6 @@ export function AgentStep({ data, onChange, agents, profiles, dockerAvailable, o
207210

208211
{showAdvanced && (
209212
<div className="mt-2 space-y-4 border-t border-surface-700/30 pt-4">
210-
{/* Session title / branch */}
211-
<div>
212-
<label className="block text-sm text-text-dim mb-1.5">Session title</label>
213-
<input
214-
type="text"
215-
value={data.title}
216-
onChange={(e) => onChange("title", e.target.value)}
217-
placeholder="Shown in Agent of Empires"
218-
className="w-full bg-surface-900 border border-surface-700 rounded-lg px-3 py-2.5 text-base font-mono text-text-primary placeholder:text-text-dim focus:border-brand-600 focus:outline-none"
219-
/>
220-
<p className="text-xs text-text-dim mt-1">Shown in the dashboard and session lists. Renaming it later does not rename the git branch.</p>
221-
</div>
222-
223-
<div>
224-
<label className="block text-sm text-text-dim mb-1.5">Branch name</label>
225-
<input
226-
type="text"
227-
value={data.worktreeBranch}
228-
onChange={(e) => onChange("worktreeBranch", e.target.value)}
229-
placeholder="Uses session title if empty"
230-
className="w-full bg-surface-900 border border-surface-700 rounded-lg px-3 py-2.5 text-base font-mono text-text-primary placeholder:text-text-dim focus:border-brand-600 focus:outline-none"
231-
/>
232-
<p className="text-xs text-text-dim mt-1">Git branch and worktree name. Clearing it switches back to the session title. Leave both title and branch empty to auto-generate.</p>
233-
</div>
234-
235213
{/* Container config (if sandbox enabled) */}
236214
{data.sandboxEnabled && (
237215
<>
@@ -309,18 +287,6 @@ export function AgentStep({ data, onChange, agents, profiles, dockerAvailable, o
309287
className="w-full bg-surface-900 border border-surface-700 rounded-lg px-3 py-2.5 text-sm font-mono text-text-primary placeholder:text-text-dim focus:border-brand-600 focus:outline-none"
310288
/>
311289
</div>
312-
313-
{/* Group */}
314-
<div>
315-
<label className="block text-sm text-text-dim mb-1.5">Group</label>
316-
<input
317-
type="text"
318-
value={data.group}
319-
onChange={(e) => onChange("group", e.target.value)}
320-
placeholder="Optional session group"
321-
className="w-full bg-surface-900 border border-surface-700 rounded-lg px-3 py-2.5 text-sm font-mono text-text-primary placeholder:text-text-dim focus:border-brand-600 focus:outline-none"
322-
/>
323-
</div>
324290
</div>
325291
)}
326292
</div>

web/src/components/session-wizard/steps/ReviewStep.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
22
import type { StepDef, StepId } from "../StepIndicator";
33
import { getReviewSummary } from "../sessionNames";
44

5-
interface WizardData { path: string; title: string; worktreeBranch: string; group: string; tool: string; profile: string; profileDirty: boolean; yoloMode: boolean; sandboxEnabled: boolean; sandboxImage: string; extraArgs: string; customInstruction: string; commandOverride: string; [key: string]: unknown; }
5+
interface WizardData { path: string; title: string; worktreeBranch: string; useWorktree: boolean; group: string; tool: string; profile: string; profileDirty: boolean; yoloMode: boolean; sandboxEnabled: boolean; sandboxImage: string; extraArgs: string; customInstruction: string; commandOverride: string; [key: string]: unknown; }
66
interface Props { data: WizardData; onChange: (field: string, value: unknown) => void; isSubmitting: boolean; error: string | null; onSubmit: () => void; onJumpTo: (stepId: StepId) => void; steps: StepDef[]; }
77

88
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent);
@@ -122,14 +122,18 @@ export function ReviewStep({ data, onChange, isSubmitting, error, onSubmit, onJu
122122
placeholder="Auto-generated"
123123
onChange={(v) => onChange("title", v)}
124124
/>
125-
<EditableRow
126-
label="Branch"
127-
value={data.worktreeBranch}
128-
displayValue={summary.branch}
129-
placeholder="Auto-generated"
130-
onChange={(v) => onChange("worktreeBranch", v)}
131-
accent
132-
/>
125+
{data.useWorktree ? (
126+
<EditableRow
127+
label="Branch / worktree"
128+
value={data.worktreeBranch}
129+
displayValue={summary.branch}
130+
placeholder="Auto-generated"
131+
onChange={(v) => onChange("worktreeBranch", v)}
132+
accent
133+
/>
134+
) : (
135+
<Row label="Worktree" value="None — runs in repo folder" />
136+
)}
133137
<Row label="Agent" value={data.tool || "(not set)"} stepId="agent" onJumpTo={onJumpTo} />
134138
{data.profile && (
135139
<Row label="Profile" value={data.profileDirty ? `${data.profile} (Custom)` : data.profile} stepId="agent" onJumpTo={onJumpTo} accent />
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
interface WizardData {
2+
title: string;
3+
worktreeBranch: string;
4+
useWorktree: boolean;
5+
group: string;
6+
tool: string;
7+
[key: string]: unknown;
8+
}
9+
10+
interface Props {
11+
data: WizardData;
12+
onChange: (field: string, value: unknown) => void;
13+
}
14+
15+
function Toggle({ checked, onChange, disabled }: { checked: boolean; onChange: (v: boolean) => void; disabled?: boolean }) {
16+
return (
17+
<button
18+
type="button"
19+
role="switch"
20+
aria-checked={checked}
21+
disabled={disabled}
22+
onClick={() => !disabled && onChange(!checked)}
23+
className={`relative inline-flex h-7 w-12 shrink-0 items-center rounded-full transition-colors duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-600 ${
24+
disabled ? "opacity-40 cursor-not-allowed" : "cursor-pointer"
25+
} ${checked ? "bg-brand-600" : "bg-surface-700"}`}
26+
>
27+
<span
28+
className={`inline-block h-5 w-5 rounded-full bg-white shadow-sm transition-transform duration-200 ${
29+
checked ? "translate-x-6" : "translate-x-1"
30+
}`}
31+
/>
32+
</button>
33+
);
34+
}
35+
36+
export function SessionStep({ data, onChange }: Props) {
37+
return (
38+
<div>
39+
<h2 className="text-lg font-semibold text-text-primary mb-1">Name your session</h2>
40+
<p className="text-sm text-text-muted mb-5">Give it a title and decide whether to work in a git worktree.</p>
41+
42+
<div className="mb-5">
43+
<label className="block text-sm text-text-dim mb-1.5">Session title</label>
44+
<input
45+
type="text"
46+
value={data.title}
47+
onChange={(e) => onChange("title", e.target.value)}
48+
placeholder="Auto-generated if empty"
49+
className="w-full bg-surface-900 border border-surface-700 rounded-lg px-3 py-2.5 text-base font-mono text-text-primary placeholder:text-text-dim focus:border-brand-600 focus:outline-none"
50+
/>
51+
<p className="text-xs text-text-dim mt-1">Shown in the dashboard. Renaming it later does not rename the git branch.</p>
52+
</div>
53+
54+
<label
55+
className="flex items-center justify-between gap-3 p-3 bg-surface-900 border border-surface-700 rounded-lg cursor-pointer mb-3"
56+
onClick={() => onChange("useWorktree", !data.useWorktree)}
57+
>
58+
<div className="flex-1">
59+
<div className="text-sm font-medium text-text-primary">Create a worktree</div>
60+
<div className="text-xs text-text-dim mt-0.5 leading-snug">
61+
Run the agent in a new git worktree branched off the current HEAD. Off = run directly in the repo folder.
62+
</div>
63+
</div>
64+
<Toggle
65+
checked={data.useWorktree}
66+
onChange={(v) => onChange("useWorktree", v)}
67+
/>
68+
</label>
69+
70+
{data.useWorktree && (
71+
<div className="mb-5">
72+
<label className="block text-sm text-text-dim mb-1.5">Branch / worktree name</label>
73+
<input
74+
type="text"
75+
value={data.worktreeBranch}
76+
onChange={(e) => onChange("worktreeBranch", e.target.value)}
77+
placeholder="Uses session title if empty"
78+
className="w-full bg-surface-900 border border-surface-700 rounded-lg px-3 py-2.5 text-base font-mono text-text-primary placeholder:text-text-dim focus:border-brand-600 focus:outline-none"
79+
/>
80+
<p className="text-xs text-text-dim mt-1">The branch name is also the worktree directory name. Leave blank to use the session title.</p>
81+
</div>
82+
)}
83+
84+
<div>
85+
<label className="block text-sm text-text-dim mb-1.5">Group</label>
86+
<input
87+
type="text"
88+
value={data.group}
89+
onChange={(e) => onChange("group", e.target.value)}
90+
placeholder="Optional, for organizing related sessions"
91+
className="w-full bg-surface-900 border border-surface-700 rounded-lg px-3 py-2.5 text-sm font-mono text-text-primary placeholder:text-text-dim focus:border-brand-600 focus:outline-none"
92+
/>
93+
</div>
94+
</div>
95+
);
96+
}

0 commit comments

Comments
 (0)