feat: workspace creation with branch dropdown and auto-select

- Fetch branches from selected repo via listRepositoryBranches API
- Branch dropdown with default branch pre-selected
- '+ Create new branch...' option reveals text input for custom branch
- Auto-select first option when only one available:
  - Project: auto-selects when only 1 project
  - Repo: auto-selects when only 1 repo
  - Branch: auto-selects when only 1 branch, otherwise defaults to remote default
- Falls back to free-text branch input if branch API fails
- TypeScript + eslint clean
This commit is contained in:
2026-06-01 17:48:27 +02:00
parent ab1843b1c3
commit b8fc4e6642
+94 -10
View File
@@ -8,7 +8,7 @@ import { WorkspaceCard } from "../components/workspace-card";
import { StartToolModal } from "../components/start-tool-modal";
import { createInstance, startInstance } from "../api/sessions";
import { listProjects } from "../api/projects";
import { listRepositories } from "../api/git_repositories";
import { listRepositories, listRepositoryBranches } from "../api/git_repositories";
import { createWorkspaceTopLevel } from "../api/workspaces";
import type { Workspace } from "../types/workspace";
import type { ProjectWithRepos } from "../types";
@@ -152,10 +152,13 @@ function WorkspaceCreateInline({
}) {
const [projects, setProjects] = useState<ProjectWithRepos[]>([]);
const [repos, setRepos] = useState<GitRepository[]>([]);
const [branches, setBranches] = useState<string[]>([]);
const [selectedProject, setSelectedProject] = useState("");
const [selectedRepo, setSelectedRepo] = useState("");
const [selectedBranch, setSelectedBranch] = useState("");
const [newBranchName, setNewBranchName] = useState("");
const [isNewBranch, setIsNewBranch] = useState(false);
const [name, setName] = useState("");
const [branch, setBranch] = useState("main");
const [loading, setLoading] = useState(false);
const [fetching, setFetching] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -198,6 +201,45 @@ function WorkspaceCreateInline({
void loadRepos();
}, [selectedProject]);
useEffect(() => {
if (!selectedProject || !selectedRepo) {
setBranches([]);
setSelectedBranch("");
setIsNewBranch(false);
return;
}
const loadBranches = async () => {
try {
const data = await listRepositoryBranches(selectedProject, selectedRepo);
const branchNames = data.branches.map((b) => b.name);
setBranches(branchNames);
if (branchNames.length === 1) {
setSelectedBranch(branchNames[0]);
setIsNewBranch(false);
} else if (data.default_branch) {
setSelectedBranch(data.default_branch);
setIsNewBranch(false);
}
} catch {
// If branch fetch fails, fall back to free-text
setBranches([]);
setIsNewBranch(true);
}
};
void loadBranches();
}, [selectedProject, selectedRepo]);
const handleBranchChange = (value: string) => {
if (value === "__new__") {
setIsNewBranch(true);
setSelectedBranch("__new__");
setNewBranchName("");
} else {
setIsNewBranch(false);
setSelectedBranch(value);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedRepo) {
@@ -208,17 +250,24 @@ function WorkspaceCreateInline({
setError("Workspace name is required");
return;
}
const branchName = isNewBranch ? newBranchName.trim() : selectedBranch;
if (!branchName) {
setError("Please select or enter a branch");
return;
}
setLoading(true);
setError(null);
try {
await createWorkspaceTopLevel({
repo_id: selectedRepo,
name: name.trim(),
branch: branch.trim() || "main",
branch: branchName,
});
onCreated();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create workspace");
setError(
err instanceof Error ? err.message : "Failed to create workspace",
);
} finally {
setLoading(false);
}
@@ -292,12 +341,47 @@ function WorkspaceCreateInline({
<label>
<Icon name="branch" size="sm" /> Branch
</label>
<input
type="text"
value={branch}
onChange={(e) => setBranch(e.target.value)}
placeholder="main"
/>
{branches.length > 0 ? (
<>
<select
value={selectedBranch}
onChange={(e) => handleBranchChange(e.target.value)}
required
disabled={!selectedRepo}
>
<option value="">Select branch...</option>
{branches.map((b) => (
<option key={b} value={b}>
{b}
</option>
))}
<option value="__new__">+ Create new branch...</option>
</select>
{isNewBranch && (
<input
type="text"
value={newBranchName}
onChange={(e) => setNewBranchName(e.target.value)}
placeholder="new-branch-name"
required
style={{ marginTop: "0.5rem" }}
/>
)}
</>
) : (
<input
type="text"
value={isNewBranch ? newBranchName : selectedBranch}
onChange={(e) => {
setIsNewBranch(true);
setNewBranchName(e.target.value);
setSelectedBranch("__new__");
}}
placeholder="main"
required
disabled={!selectedRepo}
/>
)}
</div>
{error && (