WIP: frontend clone_mode removal in create form and API

- Remove clone_mode/branch/new_branch from createInstance API helper
- Remove clone mode UI and branch fields from CreateSessionForm

Remaining: wire workspace_id in form/tool-starter, remove session-card badge, tests
This commit is contained in:
Developer
2026-06-12 15:31:50 +00:00
parent 79aabd6f43
commit 3da7ea6408
5 changed files with 72 additions and 208 deletions
+2
View File
@@ -17,6 +17,7 @@ __pycache__/
*.so *.so
.python-version .python-version
.venv/ .venv/
.venv-test/
venv/ venv/
env/ env/
.pytest_cache/ .pytest_cache/
@@ -56,3 +57,4 @@ Thumbs.db
.pi-lens/ .pi-lens/
minerv3/ minerv3/
.cache/ .cache/
openspec-audit-report.md
@@ -2,13 +2,16 @@
import uuid import uuid
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user_id, get_db_session from src.auth.dependencies import get_current_user_id, get_db_session
from src.models import GitRepository
from src.models import ToolInstance from src.models import ToolInstance
from src.models import Workspace from src.models import Workspace
from src.schemas.tool import CreateInstanceRequest, CreateWorkspaceInstanceRequest
from src.services.tool.instance_service import create_tool_instance
router = APIRouter(prefix="/workspaces/{workspace_id}/instances") router = APIRouter(prefix="/workspaces/{workspace_id}/instances")
@@ -30,6 +33,63 @@ async def _get_workspace(
return workspace return workspace
@router.post(
"/",
summary="Create instance from workspace",
description="Create a new tool instance mounted on this workspace.",
status_code=status.HTTP_201_CREATED,
)
async def create_workspace_instance(
workspace_id: uuid.UUID,
data: CreateWorkspaceInstanceRequest,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
"""Create a tool instance directly on a workspace."""
workspace = await _get_workspace(session, workspace_id, user_id)
repo = await session.get(GitRepository, workspace.repo_id)
if repo is None:
raise HTTPException(status_code=404, detail="Repository not found")
if repo.project_id is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Repository is not associated with a project",
)
request = CreateInstanceRequest(
tool_type_id=data.tool_type_id,
display_name=data.display_name,
workspace_id=str(workspace.id),
config_profile_id=data.config_profile_id,
ssh_key_ids=data.ssh_key_ids,
)
try:
instance = await create_tool_instance(
session, user_id, repo.project_id, repo.id, request
)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
except RuntimeError as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)
)
return {
"id": str(instance.id),
"name": instance.name,
"display_name": instance.display_name,
"tool_type_id": str(instance.tool_type_id),
"status": instance.status,
"workspace_id": str(instance.workspace_id) if instance.workspace_id else None,
"selected_config_profile_id": str(instance.selected_config_profile_id)
if instance.selected_config_profile_id
else None,
"created_at": instance.created_at.isoformat(),
}
@router.get("/") @router.get("/")
async def list_workspace_instances( async def list_workspace_instances(
workspace_id: uuid.UUID, workspace_id: uuid.UUID,
+6 -1
View File
@@ -1,6 +1,10 @@
"""Tool schemas module.""" """Tool schemas module."""
from src.schemas.tool.tool_instance import CreateInstanceRequest, StartInstanceRequest from src.schemas.tool.tool_instance import (
CreateInstanceRequest,
CreateWorkspaceInstanceRequest,
StartInstanceRequest,
)
from src.schemas.tool.tool_type import ( from src.schemas.tool.tool_type import (
ToolTypeCreate, ToolTypeCreate,
ToolTypeResponse, ToolTypeResponse,
@@ -10,6 +14,7 @@ from src.schemas.tool.tool_type import (
__all__ = [ __all__ = [
"CreateInstanceRequest", "CreateInstanceRequest",
"CreateWorkspaceInstanceRequest",
"StartInstanceRequest", "StartInstanceRequest",
"ToolTypeCreate", "ToolTypeCreate",
"ToolTypeResponse", "ToolTypeResponse",
+1 -9
View File
@@ -31,8 +31,6 @@ export interface Session {
url: string | null; url: string | null;
container_status?: string; container_status?: string;
probe_status?: string; probe_status?: string;
clone_mode?: string;
branch?: string | null;
created_at?: string; created_at?: string;
} }
@@ -51,12 +49,9 @@ export async function createInstance(
repoId: string, repoId: string,
toolTypeId: string, toolTypeId: string,
displayName?: string, displayName?: string,
cloneMode?: string, workspaceId?: string,
branch?: string,
newBranch?: string,
configProfileId?: string, configProfileId?: string,
sshKeyIds?: string[], sshKeyIds?: string[],
workspaceId?: string,
): Promise<ToolInstance> { ): Promise<ToolInstance> {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances`, `/projects/${projectId}/repositories/${repoId}/instances`,
@@ -64,9 +59,6 @@ export async function createInstance(
tool_type_id: toolTypeId, tool_type_id: toolTypeId,
display_name: displayName, display_name: displayName,
workspace_id: workspaceId || undefined, workspace_id: workspaceId || undefined,
clone_mode: cloneMode || "mount",
branch: branch || undefined,
new_branch: newBranch || undefined,
config_profile_id: configProfileId, config_profile_id: configProfileId,
ssh_key_ids: sshKeyIds || [], ssh_key_ids: sshKeyIds || [],
}, },
@@ -6,11 +6,7 @@ import {
type ToolInstance, type ToolInstance,
} from "../../../api/sessions"; } from "../../../api/sessions";
import type { Project } from "../../../types"; import type { Project } from "../../../types";
import { import type { GitRepository } from "../../../api/git-repositories";
listRepositoryBranches,
type GitRepository,
type Branch,
} from "../../../api/git-repositories";
import type { ToolType } from "../../../api/tool-types"; import type { ToolType } from "../../../api/tool-types";
import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys"; import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys";
import { import {
@@ -26,7 +22,6 @@ interface CreateSessionFormProps {
fixedRepoId?: string; fixedRepoId?: string;
projectName?: string; projectName?: string;
repoName?: string; repoName?: string;
showCloneMode?: boolean;
showFixedFields?: boolean; showFixedFields?: boolean;
onProjectChange?: (projectId: string) => void; onProjectChange?: (projectId: string) => void;
onSuccess?: (instance: ToolInstance) => void; onSuccess?: (instance: ToolInstance) => void;
@@ -43,7 +38,6 @@ export const CreateSessionForm = ({
fixedRepoId, fixedRepoId,
projectName, projectName,
repoName, repoName,
showCloneMode = true,
showFixedFields = true, showFixedFields = true,
onProjectChange, onProjectChange,
onSuccess, onSuccess,
@@ -55,19 +49,11 @@ export const CreateSessionForm = ({
const [selectedRepo, setSelectedRepo] = useState(fixedRepoId || ""); const [selectedRepo, setSelectedRepo] = useState(fixedRepoId || "");
const [selectedToolType, setSelectedToolType] = useState(""); const [selectedToolType, setSelectedToolType] = useState("");
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState("");
const [cloneMode, setCloneMode] = useState<"mount" | "clone">("mount");
const [branch, setBranch] = useState("main");
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]); const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
const [configProfiles, setConfigProfiles] = useState<ConfigProfile[]>([]); const [configProfiles, setConfigProfiles] = useState<ConfigProfile[]>([]);
const [selectedConfigProfile, setSelectedConfigProfile] = useState(""); const [selectedConfigProfile, setSelectedConfigProfile] = useState("");
const [selectedSshKeyIds, setSelectedSshKeyIds] = useState<string[]>([]); const [selectedSshKeyIds, setSelectedSshKeyIds] = useState<string[]>([]);
const [branches, setBranches] = useState<Branch[]>([]);
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
const [isCreatingNewBranch, setIsCreatingNewBranch] = useState(false);
const [newBranchName, setNewBranchName] = useState("");
const [baseBranch, setBaseBranch] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -108,31 +94,6 @@ export const CreateSessionForm = ({
void loadProfiles(); void loadProfiles();
}, [selectedToolType, selectedProject, fixedProjectId]); }, [selectedToolType, selectedProject, fixedProjectId]);
// Load branches when selected repo changes
useEffect(() => {
const projectId = fixedProjectId || selectedProject;
if (!selectedRepo || !projectId || !showCloneMode) {
setBranches([]);
return;
}
const loadBranches = async () => {
setIsLoadingBranches(true);
try {
const response = await listRepositoryBranches(projectId, selectedRepo);
setBranches(response.branches);
if (response.default_branch) {
setBranch(response.default_branch);
setBaseBranch(response.default_branch);
}
} catch {
// ignore
} finally {
setIsLoadingBranches(false);
}
};
void loadBranches();
}, [selectedRepo, selectedProject, fixedProjectId, showCloneMode]);
// Filter repositories by selected project // Filter repositories by selected project
const availableRepos = selectedProject const availableRepos = selectedProject
? repositories.filter((r) => r.project_id === selectedProject) ? repositories.filter((r) => r.project_id === selectedProject)
@@ -143,12 +104,6 @@ export const CreateSessionForm = ({
if (!fixedRepoId) setSelectedRepo(""); if (!fixedRepoId) setSelectedRepo("");
setSelectedToolType(""); setSelectedToolType("");
setDisplayName(""); setDisplayName("");
setCloneMode("mount");
setBranch("main");
setIsCreatingNewBranch(false);
setNewBranchName("");
setBaseBranch("");
setBranches([]);
setSelectedSshKeyIds([]); setSelectedSshKeyIds([]);
setSelectedConfigProfile(""); setSelectedConfigProfile("");
}; };
@@ -165,14 +120,6 @@ export const CreateSessionForm = ({
return; return;
} }
if (showCloneMode && cloneMode === "clone") {
const repo = repositories.find((r) => r.id === repoId);
if (!repo?.ssh_key_id) {
setError("Repository must have an SSH key assigned for clone mode");
return;
}
}
setIsSubmitting(true); setIsSubmitting(true);
try { try {
@@ -181,15 +128,7 @@ export const CreateSessionForm = ({
repoId, repoId,
selectedToolType, selectedToolType,
displayName || undefined, displayName || undefined,
showCloneMode ? cloneMode : undefined, undefined,
showCloneMode && cloneMode === "clone"
? isCreatingNewBranch
? baseBranch
: branch
: undefined,
showCloneMode && cloneMode === "clone" && isCreatingNewBranch
? newBranchName
: undefined,
selectedConfigProfile || undefined, selectedConfigProfile || undefined,
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined, selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined,
); );
@@ -240,8 +179,6 @@ export const CreateSessionForm = ({
setSelectedProject(value); setSelectedProject(value);
setSelectedRepo(""); setSelectedRepo("");
setSelectedToolType(""); setSelectedToolType("");
setCloneMode("mount");
setIsCreatingNewBranch(false);
onProjectChange?.(value); onProjectChange?.(value);
}} }}
disabled={isSubmitting} disabled={isSubmitting}
@@ -277,8 +214,6 @@ export const CreateSessionForm = ({
onChange={(e) => { onChange={(e) => {
setSelectedRepo(e.target.value); setSelectedRepo(e.target.value);
setSelectedToolType(""); setSelectedToolType("");
setCloneMode("mount");
setIsCreatingNewBranch(false);
}} }}
disabled={!hasProject || isSubmitting} disabled={!hasProject || isSubmitting}
> >
@@ -301,8 +236,6 @@ export const CreateSessionForm = ({
value={selectedToolType} value={selectedToolType}
onChange={(e) => { onChange={(e) => {
setSelectedToolType(e.target.value); setSelectedToolType(e.target.value);
setCloneMode("mount");
setIsCreatingNewBranch(false);
}} }}
disabled={!hasRepo || isSubmitting} disabled={!hasRepo || isSubmitting}
> >
@@ -382,134 +315,6 @@ export const CreateSessionForm = ({
</div> </div>
)} )}
{/* Clone Mode & Branch */}
{showCloneMode && hasToolType && (
<div className="form-field">
<label>Repository Access</label>
<div className="radio-group">
<label className="radio-label">
<input
type="radio"
name="cloneMode"
value="mount"
checked={cloneMode === "mount"}
onChange={(e) => {
setCloneMode(e.target.value as "mount" | "clone");
setIsCreatingNewBranch(false);
}}
disabled={isSubmitting}
/>
Mount (live sync)
</label>
<label className="radio-label">
<input
type="radio"
name="cloneMode"
value="clone"
checked={cloneMode === "clone"}
onChange={(e) => {
setCloneMode(e.target.value as "mount" | "clone");
setIsCreatingNewBranch(false);
}}
disabled={isSubmitting}
/>
Clone fresh copy
</label>
</div>
{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);
}
}}
disabled={isSubmitting}
>
{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
disabled={isSubmitting}
/>
</label>
<label className="form-field">
Base Branch
<select
value={baseBranch}
onChange={(e) => setBaseBranch(e.target.value)}
disabled={isSubmitting}
>
{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>
)}
</>
)}
</div>
)}
{/* Display Name */} {/* Display Name */}
{hasToolType && ( {hasToolType && (
<div className="form-field"> <div className="form-field">