Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions web/src/components/session-wizard/SessionWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fetchAgents, fetchGroups, fetchDockerStatus, fetchProfiles, fetchSettin
import { StepIndicator } from "./StepIndicator";
import type { StepDef, StepId } from "./StepIndicator";
import { ProjectStep } from "./steps/ProjectStep";
import { SessionStep } from "./steps/SessionStep";
import { AgentStep } from "./steps/AgentStep";
import { ReviewStep } from "./steps/ReviewStep";
import { applyBranchOverride, getSubmittedBranch } from "./sessionNames";
Expand All @@ -13,6 +14,7 @@ export interface WizardData {
title: string;
worktreeBranch: string;
worktreeBranchDirty: boolean;
useWorktree: boolean;
group: string;
tool: string;
profile: string;
Expand Down Expand Up @@ -54,6 +56,7 @@ type Action =

const initialData: WizardData = {
path: "", title: "", worktreeBranch: "", worktreeBranchDirty: false,
useWorktree: true,
group: "", tool: "claude", profile: "",
yoloMode: false, sandboxEnabled: false, sandboxImage: "", extraEnv: [],
advancedEnabled: false, profileDirty: false,
Expand Down Expand Up @@ -114,11 +117,12 @@ function reducer(state: WizardState, action: Action): WizardState {
}
}

// Wizard: project path → agent/settings → review
// Wizard: project path → session (title + worktree) → agent → review
function computeSteps(_data: WizardData): StepDef[] {
return [
{ id: "project", label: "Project" },
{ id: "agent", label: "Settings" },
{ id: "session", label: "Session" },
{ id: "agent", label: "Agent" },
{ id: "review", label: "Review" },
];
}
Expand Down Expand Up @@ -156,7 +160,7 @@ export function SessionWizard({ onClose, onCreated, prefill }: Props) {
: initialData;

const [state, dispatch] = useReducer(reducer, {
currentStep: prefill?.skipToReview ? 2 : (prefill?.path ? 1 : 0),
currentStep: prefill?.skipToReview ? 3 : (prefill?.path ? 1 : 0),
data: prefillData, isSubmitting: false, error: null,
agents: [], groups: [], profiles: [], dockerAvailable: false,
});
Expand Down Expand Up @@ -201,8 +205,8 @@ export function SessionWizard({ onClose, onCreated, prefill }: Props) {
path: d.path, tool: d.tool,
title: d.title || undefined, group: d.group || undefined,
yolo_mode: d.yoloMode,
worktree_branch: getSubmittedBranch(d.title, d.worktreeBranch),
create_new_branch: true,
worktree_branch: d.useWorktree ? getSubmittedBranch(d.title, d.worktreeBranch) : undefined,
create_new_branch: d.useWorktree,
sandbox: d.sandboxEnabled,
sandbox_image: d.sandboxEnabled ? d.sandboxImage : undefined,
extra_env: d.sandboxEnabled && d.extraEnv.length > 0 ? d.extraEnv.filter(Boolean) : undefined,
Expand All @@ -224,6 +228,8 @@ export function SessionWizard({ onClose, onCreated, prefill }: Props) {
switch (currentStepDef?.id) {
case "project":
return <ProjectStep data={state.data} onChange={handleChange} initialTab={prefill?.initialTab} />;
case "session":
return <SessionStep data={state.data} onChange={handleChange} />;
case "agent":
return (
<AgentStep
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/session-wizard/StepIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type StepId = "project" | "agent" | "container" | "advanced" | "review";
export type StepId = "project" | "session" | "agent" | "container" | "advanced" | "review";

export interface StepDef {
id: StepId;
Expand Down
44 changes: 5 additions & 39 deletions web/src/components/session-wizard/steps/AgentStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface WizardData {
tool: string;
title: string;
worktreeBranch: string;
useWorktree: boolean;
profile: string;
profileDirty: boolean;
sandboxEnabled: boolean;
Expand All @@ -16,7 +17,6 @@ interface WizardData {
customInstruction: string;
extraArgs: string;
commandOverride: string;
group: string;
[key: string]: unknown;
}

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

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

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

{showAdvanced && (
<div className="mt-2 space-y-4 border-t border-surface-700/30 pt-4">
{/* Session title / branch */}
<div>
<label className="block text-sm text-text-dim mb-1.5">Session title</label>
<input
type="text"
value={data.title}
onChange={(e) => onChange("title", e.target.value)}
placeholder="Shown in Agent of Empires"
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"
/>
<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>
</div>

<div>
<label className="block text-sm text-text-dim mb-1.5">Branch name</label>
<input
type="text"
value={data.worktreeBranch}
onChange={(e) => onChange("worktreeBranch", e.target.value)}
placeholder="Uses session title if empty"
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"
/>
<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>
</div>

{/* Container config (if sandbox enabled) */}
{data.sandboxEnabled && (
<>
Expand Down Expand Up @@ -309,18 +287,6 @@ export function AgentStep({ data, onChange, agents, profiles, dockerAvailable, o
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"
/>
</div>

{/* Group */}
<div>
<label className="block text-sm text-text-dim mb-1.5">Group</label>
<input
type="text"
value={data.group}
onChange={(e) => onChange("group", e.target.value)}
placeholder="Optional session group"
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"
/>
</div>
</div>
)}
</div>
Expand Down
22 changes: 13 additions & 9 deletions web/src/components/session-wizard/steps/ReviewStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
import type { StepDef, StepId } from "../StepIndicator";
import { getReviewSummary } from "../sessionNames";

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; }
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; }
interface Props { data: WizardData; onChange: (field: string, value: unknown) => void; isSubmitting: boolean; error: string | null; onSubmit: () => void; onJumpTo: (stepId: StepId) => void; steps: StepDef[]; }

const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent);
Expand Down Expand Up @@ -122,14 +122,18 @@ export function ReviewStep({ data, onChange, isSubmitting, error, onSubmit, onJu
placeholder="Auto-generated"
onChange={(v) => onChange("title", v)}
/>
<EditableRow
label="Branch"
value={data.worktreeBranch}
displayValue={summary.branch}
placeholder="Auto-generated"
onChange={(v) => onChange("worktreeBranch", v)}
accent
/>
{data.useWorktree ? (
<EditableRow
label="Branch / worktree"
value={data.worktreeBranch}
displayValue={summary.branch}
placeholder="Auto-generated"
onChange={(v) => onChange("worktreeBranch", v)}
accent
/>
) : (
<Row label="Worktree" value="None — runs in repo folder" />
)}
<Row label="Agent" value={data.tool || "(not set)"} stepId="agent" onJumpTo={onJumpTo} />
{data.profile && (
<Row label="Profile" value={data.profileDirty ? `${data.profile} (Custom)` : data.profile} stepId="agent" onJumpTo={onJumpTo} accent />
Expand Down
96 changes: 96 additions & 0 deletions web/src/components/session-wizard/steps/SessionStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
interface WizardData {
title: string;
worktreeBranch: string;
useWorktree: boolean;
group: string;
tool: string;
[key: string]: unknown;
}

interface Props {
data: WizardData;
onChange: (field: string, value: unknown) => void;
}

function Toggle({ checked, onChange, disabled }: { checked: boolean; onChange: (v: boolean) => void; disabled?: boolean }) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => !disabled && onChange(!checked)}
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 ${
disabled ? "opacity-40 cursor-not-allowed" : "cursor-pointer"
} ${checked ? "bg-brand-600" : "bg-surface-700"}`}
>
<span
className={`inline-block h-5 w-5 rounded-full bg-white shadow-sm transition-transform duration-200 ${
checked ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
);
}

export function SessionStep({ data, onChange }: Props) {
return (
<div>
<h2 className="text-lg font-semibold text-text-primary mb-1">Name your session</h2>
<p className="text-sm text-text-muted mb-5">Give it a title and decide whether to work in a git worktree.</p>

<div className="mb-5">
<label className="block text-sm text-text-dim mb-1.5">Session title</label>
<input
type="text"
value={data.title}
onChange={(e) => onChange("title", e.target.value)}
placeholder="Auto-generated if empty"
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"
/>
<p className="text-xs text-text-dim mt-1">Shown in the dashboard. Renaming it later does not rename the git branch.</p>
</div>

<label
className="flex items-center justify-between gap-3 p-3 bg-surface-900 border border-surface-700 rounded-lg cursor-pointer mb-3"
onClick={() => onChange("useWorktree", !data.useWorktree)}
>
<div className="flex-1">
<div className="text-sm font-medium text-text-primary">Create a worktree</div>
<div className="text-xs text-text-dim mt-0.5 leading-snug">
Run the agent in a new git worktree branched off the current HEAD. Off = run directly in the repo folder.
</div>
</div>
<Toggle
checked={data.useWorktree}
onChange={(v) => onChange("useWorktree", v)}
/>
</label>

{data.useWorktree && (
<div className="mb-5">
<label className="block text-sm text-text-dim mb-1.5">Branch / worktree name</label>
<input
type="text"
value={data.worktreeBranch}
onChange={(e) => onChange("worktreeBranch", e.target.value)}
placeholder="Uses session title if empty"
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"
/>
<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>
</div>
)}

<div>
<label className="block text-sm text-text-dim mb-1.5">Group</label>
<input
type="text"
value={data.group}
onChange={(e) => onChange("group", e.target.value)}
placeholder="Optional, for organizing related sessions"
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"
/>
</div>
</div>
);
}
Loading