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 { StartToolModal } from "../components/start-tool-modal";
import { createInstance, startInstance } from "../api/sessions"; import { createInstance, startInstance } from "../api/sessions";
import { listProjects } from "../api/projects"; import { listProjects } from "../api/projects";
import { listRepositories } from "../api/git_repositories"; import { listRepositories, listRepositoryBranches } from "../api/git_repositories";
import { createWorkspaceTopLevel } from "../api/workspaces"; import { createWorkspaceTopLevel } from "../api/workspaces";
import type { Workspace } from "../types/workspace"; import type { Workspace } from "../types/workspace";
import type { ProjectWithRepos } from "../types"; import type { ProjectWithRepos } from "../types";
@@ -152,10 +152,13 @@ function WorkspaceCreateInline({
}) { }) {
const [projects, setProjects] = useState<ProjectWithRepos[]>([]); const [projects, setProjects] = useState<ProjectWithRepos[]>([]);
const [repos, setRepos] = useState<GitRepository[]>([]); const [repos, setRepos] = useState<GitRepository[]>([]);
const [branches, setBranches] = useState<string[]>([]);
const [selectedProject, setSelectedProject] = useState(""); const [selectedProject, setSelectedProject] = useState("");
const [selectedRepo, setSelectedRepo] = useState(""); const [selectedRepo, setSelectedRepo] = useState("");
const [selectedBranch, setSelectedBranch] = useState("");
const [newBranchName, setNewBranchName] = useState("");
const [isNewBranch, setIsNewBranch] = useState(false);
const [name, setName] = useState(""); const [name, setName] = useState("");
const [branch, setBranch] = useState("main");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [fetching, setFetching] = useState(true); const [fetching, setFetching] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -198,6 +201,45 @@ function WorkspaceCreateInline({
void loadRepos(); void loadRepos();
}, [selectedProject]); }, [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) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!selectedRepo) { if (!selectedRepo) {
@@ -208,17 +250,24 @@ function WorkspaceCreateInline({
setError("Workspace name is required"); setError("Workspace name is required");
return; return;
} }
const branchName = isNewBranch ? newBranchName.trim() : selectedBranch;
if (!branchName) {
setError("Please select or enter a branch");
return;
}
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
await createWorkspaceTopLevel({ await createWorkspaceTopLevel({
repo_id: selectedRepo, repo_id: selectedRepo,
name: name.trim(), name: name.trim(),
branch: branch.trim() || "main", branch: branchName,
}); });
onCreated(); onCreated();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to create workspace"); setError(
err instanceof Error ? err.message : "Failed to create workspace",
);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -292,12 +341,47 @@ function WorkspaceCreateInline({
<label> <label>
<Icon name="branch" size="sm" /> Branch <Icon name="branch" size="sm" /> Branch
</label> </label>
<input {branches.length > 0 ? (
type="text" <>
value={branch} <select
onChange={(e) => setBranch(e.target.value)} value={selectedBranch}
placeholder="main" 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> </div>
{error && ( {error && (