96c8dd7402
- 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
416 lines
13 KiB
Markdown
416 lines
13 KiB
Markdown
# 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` — Add `listRepositoryBranches` API function
|
|
- `apps/web/src/pages/sessions.tsx` — Replace branch input with dropdown + new branch form
|
|
- `apps/api/src/api/tool_instances.py` — Extend `CreateInstanceRequest`, add local branch creation
|
|
- `apps/web/src/api/sessions.ts` — Update `createInstance` signature to accept `newBranch`
|
|
|
|
---
|
|
|
|
### 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`:
|
|
|
|
```typescript
|
|
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**
|
|
|
|
```bash
|
|
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):
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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):
|
|
|
|
```python
|
|
branch=data.new_branch if data.new_branch else (data.branch if data.clone_mode == "clone" else None),
|
|
```
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
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):
|
|
|
|
```typescript
|
|
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**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```typescript
|
|
import { listRepositoryBranches, type Branch } from "../api/git_repositories";
|
|
```
|
|
|
|
- [ ] **Step 2: Add state variables**
|
|
|
|
Add after `const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);` (around line 44):
|
|
|
|
```typescript
|
|
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):
|
|
|
|
```typescript
|
|
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):
|
|
|
|
```tsx
|
|
{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):
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
setIsCreatingNewBranch(false);
|
|
setNewBranchName("");
|
|
setBaseBranch("");
|
|
setBranches([]);
|
|
```
|
|
|
|
- [ ] **Step 7: Commit**
|
|
|
|
```bash
|
|
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**
|
|
|
|
```bash
|
|
cd apps/web && npm run typecheck
|
|
```
|
|
|
|
Expected: No errors
|
|
|
|
- [ ] **Step 2: Check Python syntax**
|
|
|
|
```bash
|
|
cd apps/api && python -m py_compile src/api/tool_instances.py
|
|
```
|
|
|
|
Expected: No errors
|
|
|
|
- [ ] **Step 3: Run backend tests if available**
|
|
|
|
```bash
|
|
cd apps/api && pytest src/tests/ -v -k "instance" || echo "No instance tests found"
|
|
```
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
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
|
|
|
|
- `Branch` interface used consistently in Task 1 and Task 4
|
|
- `new_branch` / `newBranch` naming consistent between frontend and backend
|
|
- `CreateInstanceRequest` fields match API call in `sessions.ts`
|