feat: make session creation a sequential workflow

- Refactor CreateSessionForm into step-by-step workflow
- Steps unlock sequentially: Project → Repository → Tool → Clone Mode → Branch
- Add visual step indicators with numbered badges
- Disable controls until prerequisites are met
- Add CSS for workflow step styling
This commit is contained in:
2026-05-24 10:14:39 +00:00
parent 5cec4a7a6f
commit 96c8dd7402
10 changed files with 813 additions and 57 deletions
+112 -57
View File
@@ -80,7 +80,11 @@ export const CreateSessionForm = ({
const loadBranches = async () => {
setIsLoadingBranches(true);
try {
const branchList = await listRepositoryBranches(selectedRepo);
const branchResponse = await listRepositoryBranches(
fixedProjectId || selectedProject,
selectedRepo
);
const branchList = branchResponse.branches;
setBranches(branchList);
const defaultBranch = branchList.find((b) => b.is_default);
if (defaultBranch) {
@@ -94,7 +98,7 @@ export const CreateSessionForm = ({
}
};
void loadBranches();
}, [selectedRepo, showCloneMode]);
}, [selectedRepo, showCloneMode, selectedProject, fixedProjectId]);
// Filter repositories by selected project
const availableRepos = selectedProject
@@ -167,6 +171,32 @@ export const CreateSessionForm = ({
const isSubmitting = status === "creating";
// Determine which steps are active/unlocked
const hasProject = !!(fixedProjectId || selectedProject);
const hasRepo = !!(fixedRepoId || selectedRepo);
const hasToolType = !!selectedToolType;
const renderStep = (
label: string,
number: number,
isActive: boolean,
isComplete: boolean,
children: React.ReactNode
) => {
const stepClass = `workflow-step ${isActive ? "active" : ""} ${isComplete ? "complete" : ""}`;
return (
<div className={stepClass}>
<div className="workflow-step-header">
<span className="workflow-step-number">{number}</span>
<span className="workflow-step-label">{label}</span>
</div>
<div className="workflow-step-content">
{children}
</div>
</div>
);
};
return (
<div className={`create-session-form-wrapper ${className}`}>
{isSubmitting && (
@@ -178,11 +208,11 @@ export const CreateSessionForm = ({
</div>
)}
<form onSubmit={handleSubmit} className="stack create-session-form">
<div className="form-row">
{fixedProjectId && showFixedFields ? (
<form onSubmit={handleSubmit} className="stack create-session-form workflow-form">
{/* Step 1: Project */}
{renderStep("Select Project", 1, true, hasProject,
fixedProjectId && showFixedFields ? (
<label className="form-field">
Project
<input
type="text"
value={projectName || projects.find((p) => p.id === fixedProjectId)?.name || ""}
@@ -192,13 +222,15 @@ export const CreateSessionForm = ({
</label>
) : (
<label className="form-field">
Project
<select
value={selectedProject}
onChange={(e) => {
const value = e.target.value;
setSelectedProject(value);
setSelectedRepo("");
setSelectedToolType("");
setCloneMode("mount");
setIsCreatingNewBranch(false);
onProjectChange?.(value);
}}
disabled={isSubmitting}
@@ -211,11 +243,13 @@ export const CreateSessionForm = ({
))}
</select>
</label>
)}
)
)}
{fixedRepoId && showFixedFields ? (
{/* Step 2: Repository */}
{hasProject && renderStep("Select Repository", 2, true, hasRepo,
fixedRepoId && showFixedFields ? (
<label className="form-field">
Repository
<input
type="text"
value={repoName || repositories.find((r) => r.id === fixedRepoId)?.name || ""}
@@ -225,11 +259,15 @@ export const CreateSessionForm = ({
</label>
) : (
<label className="form-field">
Repository
<select
value={selectedRepo}
onChange={(e) => setSelectedRepo(e.target.value)}
disabled={!selectedProject || isSubmitting}
onChange={(e) => {
setSelectedRepo(e.target.value);
setSelectedToolType("");
setCloneMode("mount");
setIsCreatingNewBranch(false);
}}
disabled={!hasProject || isSubmitting}
>
<option value="">Select repository...</option>
{availableRepos.map((r) => (
@@ -239,14 +277,20 @@ export const CreateSessionForm = ({
))}
</select>
</label>
)}
)
)}
{/* Step 3: Tool Type */}
{hasRepo && renderStep("Select Tool", 3, true, hasToolType,
<label className="form-field">
Tool Type
<select
value={selectedToolType}
onChange={(e) => setSelectedToolType(e.target.value)}
disabled={isSubmitting}
onChange={(e) => {
setSelectedToolType(e.target.value);
setCloneMode("mount");
setIsCreatingNewBranch(false);
}}
disabled={!hasRepo || isSubmitting}
>
<option value="">Select tool...</option>
{toolTypes.map((t) => (
@@ -256,12 +300,12 @@ export const CreateSessionForm = ({
))}
</select>
</label>
</div>
)}
{showCloneMode && (
{/* Step 4: Clone Mode & Branch */}
{showCloneMode && hasToolType && renderStep("Repository Access", 4, true, false,
<div className="form-row">
<label className="form-field">
Repository Access
<div className="radio-group">
<label className="radio-label">
<input
@@ -269,7 +313,10 @@ export const CreateSessionForm = ({
name="cloneMode"
value="mount"
checked={cloneMode === "mount"}
onChange={(e) => setCloneMode(e.target.value as "mount" | "clone")}
onChange={(e) => {
setCloneMode(e.target.value as "mount" | "clone");
setIsCreatingNewBranch(false);
}}
disabled={isSubmitting}
/>
Mount (live sync)
@@ -280,7 +327,10 @@ export const CreateSessionForm = ({
name="cloneMode"
value="clone"
checked={cloneMode === "clone"}
onChange={(e) => setCloneMode(e.target.value as "mount" | "clone")}
onChange={(e) => {
setCloneMode(e.target.value as "mount" | "clone");
setIsCreatingNewBranch(false);
}}
disabled={isSubmitting}
/>
Clone fresh copy
@@ -376,48 +426,53 @@ export const CreateSessionForm = ({
</div>
)}
<label className="form-field">
Display Name (optional)
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="My Development Environment"
disabled={isSubmitting}
/>
</label>
{/* Step 5: Display Name */}
{hasToolType && renderStep("Display Name (optional)", 5, true, !!displayName,
<label className="form-field">
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="My Development Environment"
disabled={isSubmitting}
/>
</label>
)}
{/* Error & Submit */}
{error && <p className="error-text">{error}</p>}
<div className="form-actions">
{onCancel && (
{hasToolType && (
<div className="form-actions">
{onCancel && (
<button
className="secondary-button"
type="button"
onClick={onCancel}
disabled={isSubmitting}
>
Cancel
</button>
)}
<button
className="secondary-button"
type="button"
onClick={onCancel}
className="primary-button"
type="submit"
disabled={isSubmitting}
>
Cancel
{isSubmitting ? (
<>
<Icon name="loading" size="sm" />
Creating...
</>
) : (
<>
<Icon name="add" size="sm" />
{submitLabel}
</>
)}
</button>
)}
<button
className="primary-button"
type="submit"
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<Icon name="loading" size="sm" />
Creating...
</>
) : (
<>
<Icon name="add" size="sm" />
{submitLabel}
</>
)}
</button>
</div>
</div>
)}
</form>
</div>
);
+59
View File
@@ -2844,6 +2844,65 @@ a.nav-item,
gap: var(--space-4);
}
/* Workflow Step Styles */
.workflow-form {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.workflow-step {
opacity: 0.4;
pointer-events: none;
transition: opacity 0.2s ease;
}
.workflow-step.active {
opacity: 1;
pointer-events: auto;
}
.workflow-step.complete {
opacity: 0.7;
pointer-events: auto;
}
.workflow-step-header {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-3);
font-weight: 600;
color: var(--text-muted);
}
.workflow-step.active .workflow-step-header {
color: var(--text-primary);
}
.workflow-step-number {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--bg-muted);
color: var(--text-muted);
font-size: 14px;
font-weight: 600;
}
.workflow-step.active .workflow-step-number {
background: var(--primary);
color: white;
}
.workflow-step.complete .workflow-step-number {
background: var(--success);
color: white;
}
.status-badge {
display: inline-flex;
align-items: center;