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
This commit is contained in:
2026-05-24 09:25:48 +00:00
parent 0d57e3501a
commit d7fb51f427
+93 -8
View File
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
import { listProjects } from "../api/projects"; import { listProjects } from "../api/projects";
import type { Project } from "../types"; import type { Project } from "../types";
import { listRepositories, type GitRepository } from "../api/git_repositories"; import { listRepositories, listRepositoryBranches, type GitRepository, type Branch } from "../api/git_repositories";
import { import {
getUserSessions, getUserSessions,
type Session, type Session,
@@ -43,6 +43,12 @@ export const SessionsPage = () => {
const [branch, setBranch] = useState("main"); const [branch, setBranch] = useState("main");
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]); const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
const [branches, setBranches] = useState<Branch[]>([]);
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
const [isCreatingNewBranch, setIsCreatingNewBranch] = useState(false);
const [newBranchName, setNewBranchName] = useState("");
const [baseBranch, setBaseBranch] = useState("");
const [dirtyDeleteSession, setDirtyDeleteSession] = useState<Session | null>(null); const [dirtyDeleteSession, setDirtyDeleteSession] = useState<Session | null>(null);
const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState<string[]>([]); const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState<string[]>([]);
@@ -116,6 +122,33 @@ export const SessionsPage = () => {
void loadSshKeys(); void loadSshKeys();
}, []); }, []);
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]);
// Poll health every 30 seconds for active instances // Poll health every 30 seconds for active instances
useEffect(() => { useEffect(() => {
const checkHealth = async () => { const checkHealth = async () => {
@@ -213,7 +246,8 @@ export const SessionsPage = () => {
selectedToolType, selectedToolType,
displayName || undefined, displayName || undefined,
cloneMode, cloneMode,
cloneMode === "clone" ? branch : undefined isCreatingNewBranch ? baseBranch : branch,
isCreatingNewBranch ? newBranchName : undefined
); );
// Auto-start the instance // Auto-start the instance
@@ -227,6 +261,10 @@ export const SessionsPage = () => {
setDisplayName(""); setDisplayName("");
setCloneMode("mount"); setCloneMode("mount");
setBranch("main"); setBranch("main");
setIsCreatingNewBranch(false);
setNewBranchName("");
setBaseBranch("");
setBranches([]);
await loadSessions(); await loadSessions();
} catch (error) { } catch (error) {
setCreateStatus("error"); setCreateStatus("error");
@@ -687,14 +725,61 @@ export const SessionsPage = () => {
<> <>
<label className="form-field"> <label className="form-field">
Branch Branch
<input {isLoadingBranches ? (
type="text" <span className="muted">Loading branches...</span>
value={branch} ) : (
onChange={(e) => setBranch(e.target.value)} <select
placeholder="main" 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> </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 && ( {selectedRepo && (
<div className="form-field ssh-key-info"> <div className="form-field ssh-key-info">
{(() => { {(() => {