- 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
13 KiB
Session Branch Selection Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace free-text branch input with a dropdown of available branches and add "Create new branch" functionality in the session creation form.
Architecture: Frontend fetches branches from existing API, displays them in a dropdown with a "Create new branch..." option. When creating a new branch, frontend sends both base branch and new branch name to backend. Backend clones the base branch then creates a local branch in the cloned workspace.
Tech Stack: React + TypeScript (frontend), FastAPI + Python (backend), Git via subprocess
File Structure
apps/web/src/api/git_repositories.ts— AddlistRepositoryBranchesAPI functionapps/web/src/pages/sessions.tsx— Replace branch input with dropdown + new branch formapps/api/src/api/tool_instances.py— ExtendCreateInstanceRequest, add local branch creationapps/web/src/api/sessions.ts— UpdatecreateInstancesignature to acceptnewBranch
Task 1: Add Branch Listing API to Frontend
Files:
-
Modify:
apps/web/src/api/git_repositories.ts -
Step 1: Add Branch types and listRepositoryBranches function
Add after the existing imports and before export interface CommitHistoryEntry:
export interface Branch {
name: string;
is_default: boolean;
last_commit: string | null;
}
export interface BranchesResponse {
branches: Branch[];
default_branch: string;
}
export async function listRepositoryBranches(
projectId: string,
repoId: string
): Promise<BranchesResponse> {
const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/branches`
);
return response.data;
}
- Step 2: Commit
git add apps/web/src/api/git_repositories.ts
git commit -m "feat: add branch listing API function"
Task 2: Update Backend Request Model and Clone Logic
Files:
-
Modify:
apps/api/src/api/tool_instances.py -
Step 1: Extend CreateInstanceRequest with new_branch field
Change the CreateInstanceRequest class (around line 55-64):
class CreateInstanceRequest(BaseModel):
"""Request body for creating a tool instance."""
model_config = {"extra": "ignore"}
tool_type_id: str = Field(description="UUID of the tool type to instantiate")
display_name: str | None = Field(default=None, description="Optional display name for the instance")
clone_mode: str = Field(default="mount", description="Repository access mode: 'mount' or 'clone'")
branch: str | None = Field(default="main", description="Branch to clone (when clone_mode='clone')")
new_branch: str | None = Field(default=None, description="Create a new local branch after cloning")
- Step 2: Add local branch creation after clone
After the clone block (around line 248), add:
# Create new local branch if requested
if data.clone_mode == "clone" and data.new_branch:
try:
result = subprocess.run(
["git", "-C", repo_path, "checkout", "-b", data.new_branch],
capture_output=True,
text=True,
)
if result.returncode != 0:
logger.error("Failed to create branch %s: %s", data.new_branch, result.stderr)
raise RuntimeError(f"Failed to create branch: {result.stderr}")
logger.info("Created local branch %s in cloned repository", data.new_branch)
except Exception as exc:
logger.exception("Failed to create local branch: %s", exc)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to create local branch: {exc}"
)
- Step 3: Update ToolInstance record to store new branch name
Change the instance creation (around line 321):
branch=data.new_branch if data.new_branch else (data.branch if data.clone_mode == "clone" else None),
- Step 4: Commit
git add apps/api/src/api/tool_instances.py
git commit -m "feat: support creating local branch during session creation
- Add new_branch field to CreateInstanceRequest
- Run git checkout -b after cloning when new_branch is provided
- Store new branch name in ToolInstance record"
Task 3: Update Frontend API to Support newBranch
Files:
-
Modify:
apps/web/src/api/sessions.ts -
Step 1: Update createInstance signature
Change the function signature (around line 44):
export async function createInstance(
projectId: string,
repoId: string,
toolTypeId: string,
displayName?: string,
cloneMode?: string,
branch?: string,
newBranch?: string
): Promise<ToolInstance> {
const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances`,
{
tool_type_id: toolTypeId,
display_name: displayName,
clone_mode: cloneMode || "mount",
branch: branch || undefined,
new_branch: newBranch || undefined,
}
);
return response.data;
}
- Step 2: Commit
git add apps/web/src/api/sessions.ts
git commit -m "feat: add newBranch parameter to createInstance"
Task 4: Update Session Creation UI
Files:
-
Modify:
apps/web/src/pages/sessions.tsx -
Step 1: Add new imports
Add to existing imports:
import { listRepositoryBranches, type Branch } from "../api/git_repositories";
- Step 2: Add state variables
Add after const [sshKeys, setSshKeys] = useState<SSHKey[]>([]); (around line 44):
const [branches, setBranches] = useState<Branch[]>([]);
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
const [isCreatingNewBranch, setIsCreatingNewBranch] = useState(false);
const [newBranchName, setNewBranchName] = useState("");
const [baseBranch, setBaseBranch] = useState("");
- Step 3: Add branch loading effect
Add after the loadSshKeys effect (around line 117):
useEffect(() => {
const loadBranches = async () => {
if (!selectedRepo || !selectedProject || cloneMode !== "clone") {
setBranches([]);
setIsCreatingNewBranch(false);
setNewBranchName("");
setBaseBranch("");
return;
}
setIsLoadingBranches(true);
try {
const data = await listRepositoryBranches(selectedProject, selectedRepo);
setBranches(data.branches);
const defaultBranch = data.default_branch;
setBaseBranch(defaultBranch);
if (!branch || !data.branches.find((b) => b.name === branch)) {
setBranch(defaultBranch);
}
} catch {
setBranches([]);
} finally {
setIsLoadingBranches(false);
}
};
void loadBranches();
}, [selectedRepo, selectedProject, cloneMode]);
- Step 4: Replace branch input with dropdown
Replace the branch input section (around lines 686-696):
{cloneMode === "clone" && (
<>
<label className="form-field">
Branch
{isLoadingBranches ? (
<span className="muted">Loading branches...</span>
) : (
<select
value={isCreatingNewBranch ? "__new__" : branch}
onChange={(e) => {
const value = e.target.value;
if (value === "__new__") {
setIsCreatingNewBranch(true);
setNewBranchName("");
} else {
setIsCreatingNewBranch(false);
setBranch(value);
setBaseBranch(value);
}
}}
>
{branches.map((b) => (
<option key={b.name} value={b.name}>
{b.name} {b.is_default ? "(default)" : ""}
</option>
))}
<option value="__new__">Create new branch...</option>
</select>
)}
</label>
{isCreatingNewBranch && (
<>
<label className="form-field">
New Branch Name
<input
type="text"
value={newBranchName}
onChange={(e) => setNewBranchName(e.target.value)}
placeholder="feature/my-new-branch"
required
/>
</label>
<label className="form-field">
Base Branch
<select
value={baseBranch}
onChange={(e) => setBaseBranch(e.target.value)}
>
{branches.map((b) => (
<option key={b.name} value={b.name}>
{b.name} {b.is_default ? "(default)" : ""}
</option>
))}
</select>
</label>
</>
)}
{selectedRepo && (
<div className="form-field ssh-key-info">
{(() => {
const repo = repositories.find((r) => r.id === selectedRepo);
if (!repo) return null;
if (repo.ssh_key_id) {
const key = sshKeys.find((k) => k.id === repo.ssh_key_id);
return (
<span className="success-text">
SSH key: {key?.name || "Assigned"}
</span>
);
}
return (
<span className="warning-text">
No SSH key assigned to this repository. Clone mode requires an SSH key.
</span>
);
})()}
</div>
)}
</>
)}
- Step 5: Update handleCreate to pass newBranch
Change the createInstance call (around line 210):
const instance = await createInstance(
selectedProject,
selectedRepo,
selectedToolType,
displayName || undefined,
cloneMode,
isCreatingNewBranch ? baseBranch : branch,
isCreatingNewBranch ? newBranchName : undefined
);
- Step 6: Reset new branch state on successful creation
In the success handler (around line 227), add:
setIsCreatingNewBranch(false);
setNewBranchName("");
setBaseBranch("");
setBranches([]);
- Step 7: Commit
git add apps/web/src/pages/sessions.tsx
git commit -m "feat: add branch dropdown and new branch creation UI
- Replace free-text branch input with dropdown of available branches
- Add 'Create new branch...' option with name and base branch inputs
- Load branches from API when repository is selected in clone mode
- Pass newBranch parameter to createInstance API"
Task 5: Verify and Test
- Step 1: Check TypeScript compilation
cd apps/web && npm run typecheck
Expected: No errors
- Step 2: Check Python syntax
cd apps/api && python -m py_compile src/api/tool_instances.py
Expected: No errors
- Step 3: Run backend tests if available
cd apps/api && pytest src/tests/ -v -k "instance" || echo "No instance tests found"
- Step 4: Commit
git add -A
git commit -m "test: verify branch selection implementation compiles"
Spec Coverage Check
| Spec Requirement | Task |
|---|---|
| Branch dropdown with available branches | Task 4 |
| "Create new branch..." option | Task 4 |
| New branch name input | Task 4 |
| Base branch dropdown | Task 4 |
| Backend clone + local branch creation | Task 2 |
| Frontend API integration | Task 1, 3 |
Placeholder Scan
- No TBD, TODO, or "implement later" references
- All code is complete and copy-paste ready
- No vague instructions like "add appropriate error handling"
Type Consistency Check
Branchinterface used consistently in Task 1 and Task 4new_branch/newBranchnaming consistent between frontend and backendCreateInstanceRequestfields match API call insessions.ts