feat: merge working-copies completion

See feature commit for details.
This commit is contained in:
Developer
2026-06-12 15:47:41 +00:00
42 changed files with 420 additions and 1706 deletions
+2
View File
@@ -17,6 +17,7 @@ __pycache__/
*.so
.python-version
.venv/
.venv-test/
venv/
env/
.pytest_cache/
@@ -56,3 +57,4 @@ Thumbs.db
.pi-lens/
minerv3/
.cache/
openspec-audit-report.md
+1 -1
View File
@@ -16,7 +16,7 @@ dir: .
Trust boundary: index routes, map orients, source decides.
## role
Infrastructure and deployment configuration for a self-hosted project management platform with OAuth2 authentication, providing Docker Compose orchestration, environment templates, and development tooling.
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing Docker orchestration, environment templates, and development tooling.
## parent
-
## children
+3 -3
View File
@@ -18,10 +18,10 @@ index: ./.pi-map.index.md
Trust boundary: index routes, map orients, source decides.
## role
Infrastructure and deployment configuration for a self-hosted project management platform with OAuth2 authentication, providing Docker Compose orchestration, environment templates, and development tooling.
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing Docker orchestration, environment templates, and development tooling.
## files
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
- .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and various tooling | dep: git
- .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git
- AGENTS.md | Defines operational rules, workflows, and constraints for AI agents working within an OpenSpec-driven software development project. | dep: OpenSpec, superpowers, git, docker compose, conventional commits
- CHANGELOG.md | Documents version history and notable changes for a Git-based project management web application
- Makefile | Provides standard development commands for containerized web application lifecycle management via Docker Compose | dep: docker compose, alembic, pytest, ruff, mypy, playwright, npm, postgres, redis
@@ -31,7 +31,7 @@ Infrastructure and deployment configuration for a self-hosted project management
- progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases
- swap-pane | Empty file with no functionality
## arch
Containerized microservices architecture using Docker Compose with separate frontend/API/PostgreSQL/Redis services, Traefik reverse proxy integration, and environment-driven configuration; includes AI agent governance via OpenSpec and Makefile-driven development workflows.
Containerized microservices architecture using Docker Compose with PostgreSQL/Redis data layer, Traefik reverse proxy for TLS/ingress, and environment-driven configuration; includes Python/Node multi-language backend-frontend split with Makefile-driven lifecycle management.
## tags
docker, redis, git, application, postgresql, compose, traefik, project
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps
## role
Contains the main application entry points and executable modules for the project.
Contains the top-level application entry points and executable binaries for the project.
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: apps
index: apps/.pi-map.index.md
## role
Contains the main application entry points and executable modules for the project.
Contains the top-level application entry points and executable binaries for the project.
## files
## arch
Typically follows a modular architecture where each subdirectory represents a separate deployable application sharing common domain libraries.
Follows a workspace/monorepo pattern where each subdirectory is a distinct deployable application sharing common libraries.
## tags
-
## symbols
-2
View File
@@ -74,8 +74,6 @@ async def get_user_sessions(
"workspace_name": workspace_name,
"status": instance.status,
"url": instance.url,
"clone_mode": instance.clone_mode,
"branch": instance.branch,
"selected_config_profile_id": str(instance.selected_config_profile_id)
if instance.selected_config_profile_id
else None,
+1 -4
View File
@@ -68,8 +68,7 @@ async def create_instance(
"display_name": instance.display_name,
"tool_type_id": str(instance.tool_type_id),
"status": instance.status,
"clone_mode": instance.clone_mode,
"branch": instance.branch,
"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(),
}
@@ -143,8 +142,6 @@ async def get_instance(
"container_id": instance.container_id,
"container_name": instance.container_name,
"compose_path": instance.compose_path,
"clone_mode": instance.clone_mode,
"branch": instance.branch,
"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,
"ssh_key_ids": instance.ssh_key_ids,
@@ -2,13 +2,16 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
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 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")
@@ -30,6 +33,63 @@ async def _get_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("/")
async def list_workspace_instances(
workspace_id: uuid.UUID,
+4 -2
View File
@@ -53,9 +53,11 @@ class ToolInstance(UUIDPrimaryKeyMixin, TimestampMixin, Base):
)
image_tag: Mapped[str | None] = mapped_column(String(256), nullable=True)
probe_result: Mapped[dict | None] = mapped_column(JSON, nullable=True)
clone_mode: Mapped[str] = mapped_column(String(20), nullable=False, default="mount")
clone_mode: Mapped[str | None] = mapped_column(
String(20), nullable=True, default=None
)
branch: Mapped[str | None] = mapped_column(
String(255), nullable=True, default="main"
String(255), nullable=True, default=None
)
selected_config_profile_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(), ForeignKey("config_profiles.id", ondelete="SET NULL"), nullable=True
+6 -1
View File
@@ -1,6 +1,10 @@
"""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 (
ToolTypeCreate,
ToolTypeResponse,
@@ -10,6 +14,7 @@ from src.schemas.tool.tool_type import (
__all__ = [
"CreateInstanceRequest",
"CreateWorkspaceInstanceRequest",
"StartInstanceRequest",
"ToolTypeCreate",
"ToolTypeResponse",
+15 -7
View File
@@ -13,16 +13,24 @@ class CreateInstanceRequest(BaseModel):
default=None, description="Optional display name for the instance"
)
workspace_id: str | None = Field(
default=None, description="UUID of workspace to mount (replaces clone_mode)"
default=None, description="UUID of workspace to mount"
)
clone_mode: str = Field(
default="mount", description="Repository access mode: 'mount' or 'clone'"
config_profile_id: str | None = Field(
default=None, description="Optional config profile ID for launch"
)
branch: str | None = Field(
default="main", description="Branch to clone (when clone_mode='clone')"
ssh_key_ids: list[str] = Field(
default_factory=list, description="SSH key IDs to mount into container ~/.ssh"
)
new_branch: str | None = Field(
default=None, description="Create a new local branch after cloning"
class CreateWorkspaceInstanceRequest(BaseModel):
"""Request body for creating a tool instance directly on a workspace."""
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"
)
config_profile_id: str | None = Field(
default=None, description="Optional config profile ID for launch"
@@ -150,7 +150,7 @@ class WorkspaceManager:
# Stop and delete all instances
for instance in instances:
await self._stop_and_delete_instance(instance)
await self._stop_and_delete_instance(instance, session)
# Delete directory
if os.path.exists(workspace.path):
@@ -248,10 +248,154 @@ class WorkspaceManager:
)
return list(result.scalars().all())
async def _stop_and_delete_instance(self, instance: ToolInstance) -> None:
async def _stop_and_delete_instance(
self,
instance: ToolInstance,
session: AsyncSession,
) -> None:
"""Stop and delete a tool instance.
TODO(PR-2): Wire up to actual instance stop/delete logic.
For now, this is a placeholder.
Delegates to the instance lifecycle service to ensure containers,
tunnels, and on-disk files are cleaned up.
"""
logger.warning("Placeholder: stopping and deleting instance %s", instance.id)
# Local import avoids a circular dependency between workspace and
# instance lifecycle modules.
from src.services.tool.instance_service import delete_tool_instance
try:
await delete_tool_instance(
session=session,
user_id=instance.owner_id,
project_id=instance.project_id,
repo_id=instance.repository_id,
instance_id=instance.id,
force=True,
)
logger.info("Stopped and deleted instance %s", instance.id)
except Exception as exc:
logger.error(
"Failed to stop/delete instance %s during workspace cleanup: %s",
instance.id,
exc,
)
async def ensure_instance_workspace(
self,
instance: ToolInstance,
session: AsyncSession,
) -> Workspace:
"""Return the workspace for an instance, creating/binding one if needed.
This migrates legacy instances that were created before workspaces
existed. Clone-mode instances have their existing clone moved into a
workspace, while mount-mode instances get a fresh workspace from the
canonical repository.
The operation is best-effort: failures are re-raised so the caller can
decide whether to continue with a legacy fallback path.
"""
if instance.workspace_id is not None:
workspace = await session.get(Workspace, instance.workspace_id)
if workspace is not None:
return workspace
from src.models import GitRepository
repo = await session.get(GitRepository, instance.repository_id)
if repo is None:
raise RuntimeError(f"Repository {instance.repository_id} not found")
base_name = (
f"{instance.name}-migrated"
if instance.clone_mode == "clone"
else f"{instance.name}-legacy"
)
name = base_name
counter = 1
while await self._workspace_name_exists(session, repo.id, name):
name = f"{base_name}-{counter}"
counter += 1
if instance.clone_mode == "clone":
workspace = await self._migrate_clone_into_workspace(
instance, repo, session, name
)
else:
workspace = await self.create(
repo=repo,
user_id=instance.owner_id,
name=name,
branch=instance.branch or "main",
session=session,
)
instance.workspace_id = workspace.id
instance.clone_mode = None
session.add(instance)
await session.commit()
await session.refresh(instance)
logger.info(
"Migrated instance %s to workspace %s (%s)",
instance.id,
workspace.id,
name,
)
return workspace
async def _workspace_name_exists(
self,
session: AsyncSession,
repo_id: uuid.UUID,
name: str,
) -> bool:
"""Check whether a workspace name already exists for a repository."""
result = await session.execute(
select(Workspace).where(
Workspace.repo_id == repo_id,
Workspace.name == name,
)
)
return result.scalar_one_or_none() is not None
async def _migrate_clone_into_workspace(
self,
instance: ToolInstance,
repo: "GitRepository",
session: AsyncSession,
name: str,
) -> Workspace:
"""Move an existing clone-mode repo into a new workspace path."""
import shutil
if not instance.compose_path:
raise RuntimeError("Instance has no compose path")
instance_dir = os.path.dirname(instance.compose_path)
clone_path = os.path.join(instance_dir, "repo-clone")
if not os.path.exists(clone_path):
raise RuntimeError(f"Clone path not found: {clone_path}")
path = self._workspace_path(repo.id, name)
parent = os.path.dirname(path)
os.makedirs(parent, exist_ok=True)
with contextlib.suppress(OSError):
os.chmod(parent, 0o777)
if os.path.exists(path):
shutil.rmtree(path, ignore_errors=True)
shutil.move(clone_path, path)
self._make_world_writable(path)
workspace = Workspace(
name=name,
repo_id=repo.id,
user_id=instance.owner_id,
branch=instance.branch or "main",
path=path,
status="ready",
last_sync_at=datetime.now(),
)
session.add(workspace)
await session.flush()
return workspace
+9 -104
View File
@@ -830,8 +830,6 @@ async def prepare_manifest_instance(
# Prepare SSH path for mount resolution
ssh_path = ""
if instance.clone_mode == "clone":
ssh_path = os.path.join(instance_dir, ".ssh")
# Resolve git mount variables from config profile
git_mount_vars = {}
@@ -848,6 +846,7 @@ async def prepare_manifest_instance(
"IMAGE_TAG": image_tag,
"INSTANCE_NAME": instance.name.lower(),
"INSTANCE_DIR": instance_dir,
"WORKSPACE_PATH": repo_path,
"REPO_PATH": repo_path,
"SSH_PATH": ssh_path,
"TOOL_PORT": instance.port or 0,
@@ -922,13 +921,6 @@ async def create_tool_instance(
if workspace.repo_id != repo_id:
raise ValueError("workspace does not belong to this repository")
# Validate clone mode requirements (legacy path)
if data.clone_mode == "clone" and not workspace:
if not repo.remote_url:
raise ValueError("repository does not have a remote URL for cloning")
if not repo.ssh_key_id:
raise ValueError("repository must have an SSH key assigned for clone mode")
# Generate unique name
instance_name = f"{tool_type.name}-{repo.name}-{uuid.uuid4().hex[:8]}"
@@ -970,71 +962,13 @@ async def create_tool_instance(
# Find free port
tool_port = find_free_port()
# Determine repo path based on workspace or clone mode
# Determine the source path to mount. Workspaces are the canonical path;
# legacy instances without a workspace fall back to the repository path.
if workspace:
repo_path = workspace.path
elif data.clone_mode == "clone":
ssh_key = await session.get(SSHKey, repo.ssh_key_id)
if ssh_key is None:
raise ValueError("repository SSH key not found")
ssh_key_path = None
try:
ssh_dir = prepare_ssh_key_files(instance_dir, ssh_key)
ssh_key_path = os.path.join(ssh_dir, "id_ed25519")
clone_path = clone_repository(
remote_url=repo.remote_url,
ssh_key_path=ssh_key_path,
instance_dir=instance_dir,
branch=data.branch or "main",
)
repo_path = clone_path
except Exception as exc:
logger.exception("Failed to clone repository: %s", exc)
cleanup_ssh_key_files(instance_dir)
raise RuntimeError(f"Failed to clone repository: {exc}")
else:
repo_path = repo.path
# Verify cloned repo has files
if data.clone_mode == "clone" and repo_path:
try:
repo_contents = os.listdir(repo_path)
if not repo_contents or (
len(repo_contents) == 1 and repo_contents[0] == ".git"
):
logger.error("Cloned repository at %s appears empty", repo_path)
raise RuntimeError("Cloned repository is empty")
logger.debug(
"Verified cloned repo at %s has %d items",
repo_path,
len(repo_contents),
)
except Exception as exc:
logger.exception("Failed to verify cloned repository: %s", exc)
raise RuntimeError(f"Cloned repository verification failed: {exc}")
# 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.debug(
"Created local branch %s in cloned repository", data.new_branch
)
except Exception as exc:
logger.exception("Failed to create local branch: %s", exc)
raise RuntimeError(f"Failed to create local branch: {exc}")
# Handle based on definition type
if tool_type.definition_type == "dockerfile":
image_tag = f"headquarter/{instance_name}:latest".lower()
@@ -1092,10 +1026,13 @@ async def create_tool_instance(
image_tag = compute_image_tag(tool_type.name, manifest)
# Manifest templates use WORKSPACE_PATH; REPO_PATH is retained as a
# deprecated alias for backward compatibility with older templates.
variables = {
"IMAGE_TAG": image_tag,
"INSTANCE_NAME": instance_name.lower(),
"INSTANCE_DIR": instance_dir,
"WORKSPACE_PATH": repo_path,
"REPO_PATH": repo_path,
"SSH_PATH": "",
"TOOL_PORT": tool_port,
@@ -1109,6 +1046,7 @@ async def create_tool_instance(
if not tool_type.compose_template:
raise ValueError("Tool type has no compose template configured")
variables = {
"WORKSPACE_PATH": repo_path,
"REPO_PATH": repo_path,
"INSTANCE_NAME": instance_name,
"INSTANCE_ID": instance_name,
@@ -1121,37 +1059,6 @@ async def create_tool_instance(
tool_type.compose_template, variables
)
if data.clone_mode == "clone" and repo_path:
import yaml
compose_data = yaml.safe_load(compose_content)
repo_mounted = False
if compose_data and "services" in compose_data:
for svc in compose_data["services"].values():
volumes = svc.get("volumes", [])
for vol in volumes:
vol_str = str(vol)
if repo_path in vol_str:
repo_mounted = True
break
if repo_mounted:
break
if not repo_mounted:
logger.warning(
"Compose template for tool type %s does not mount repo path; adding default mount",
tool_type.name,
)
if compose_data and "services" in compose_data:
for svc in compose_data["services"].values():
if "volumes" not in svc:
svc["volumes"] = []
svc["volumes"].append(f"{repo_path}:/workspace")
break
compose_content = yaml.dump(
compose_data, default_flow_style=False
)
write_compose_file(instance_dir, compose_content)
# Create database record
@@ -1166,10 +1073,8 @@ async def create_tool_instance(
compose_path=compose_path,
port=tool_port,
workspace_id=workspace_id,
clone_mode=data.clone_mode,
branch=data.new_branch
if data.new_branch
else (data.branch if data.clone_mode == "clone" else None),
clone_mode=None,
branch=None,
selected_config_profile_id=selected_profile_id,
ssh_key_ids=data.ssh_key_ids or None,
)
@@ -1,179 +0,0 @@
"""Tests for session creation with branch selection and new branch creation."""
import os
import subprocess
import tempfile
from src.api.tool_instances import CreateInstanceRequest
class TestCreateInstanceRequest:
"""Tests for CreateInstanceRequest model."""
def test_default_values(self):
"""Test default values for CreateInstanceRequest."""
request = CreateInstanceRequest(tool_type_id="123")
assert request.clone_mode == "mount"
assert request.branch == "main"
assert request.new_branch is None
assert request.display_name is None
def test_clone_mode_with_branch(self):
"""Test CreateInstanceRequest with clone mode and branch."""
request = CreateInstanceRequest(
tool_type_id="123",
clone_mode="clone",
branch="dev",
)
assert request.clone_mode == "clone"
assert request.branch == "dev"
def test_new_branch_field(self):
"""Test CreateInstanceRequest with new_branch field."""
request = CreateInstanceRequest(
tool_type_id="123",
clone_mode="clone",
branch="main",
new_branch="feature/test",
)
assert request.new_branch == "feature/test"
class TestBranchCreationInClone:
"""Tests for branch creation logic in clone process."""
def test_create_local_branch_success(self):
"""Test successful local branch creation."""
with tempfile.TemporaryDirectory() as tmpdir:
# Initialize repo
subprocess.run(
["git", "init", tmpdir],
capture_output=True,
check=True,
)
subprocess.run(
["git", "-C", tmpdir, "config", "user.email", "test@test.com"],
capture_output=True,
check=True,
)
subprocess.run(
["git", "-C", tmpdir, "config", "user.name", "Test User"],
capture_output=True,
check=True,
)
# Create initial commit
readme = os.path.join(tmpdir, "README.md")
with open(readme, "w") as f:
f.write("# Test\n")
subprocess.run(
["git", "-C", tmpdir, "add", "README.md"],
capture_output=True,
check=True,
)
subprocess.run(
["git", "-C", tmpdir, "commit", "-m", "Initial commit"],
capture_output=True,
check=True,
)
# Create new branch
result = subprocess.run(
["git", "-C", tmpdir, "checkout", "-b", "feature/new-branch"],
capture_output=True,
text=True,
)
assert result.returncode == 0
# Verify branch exists
branches_result = subprocess.run(
["git", "-C", tmpdir, "branch", "--show-current"],
capture_output=True,
text=True,
)
assert branches_result.stdout.strip() == "feature/new-branch"
def test_create_local_branch_invalid_name(self):
"""Test local branch creation with invalid name fails."""
with tempfile.TemporaryDirectory() as tmpdir:
# Initialize repo
subprocess.run(
["git", "init", tmpdir],
capture_output=True,
check=True,
)
subprocess.run(
["git", "-C", tmpdir, "config", "user.email", "test@test.com"],
capture_output=True,
check=True,
)
subprocess.run(
["git", "-C", tmpdir, "config", "user.name", "Test User"],
capture_output=True,
check=True,
)
# Create initial commit
readme = os.path.join(tmpdir, "README.md")
with open(readme, "w") as f:
f.write("# Test\n")
subprocess.run(
["git", "-C", tmpdir, "add", "README.md"],
capture_output=True,
check=True,
)
subprocess.run(
["git", "-C", tmpdir, "commit", "-m", "Initial commit"],
capture_output=True,
check=True,
)
# Try to create branch with invalid name (contains spaces)
result = subprocess.run(
["git", "-C", tmpdir, "checkout", "-b", "invalid branch name"],
capture_output=True,
text=True,
)
# Git accepts branch names with spaces but it's not recommended
# This test verifies the command structure
assert result.returncode == 0 or "fatal" in result.stderr
class TestCreateInstanceAPI:
"""Tests for create instance API endpoint with branch options."""
def test_create_instance_request_validation(self):
"""Test that CreateInstanceRequest validates correctly."""
# Valid request with new_branch
request = CreateInstanceRequest(
tool_type_id="550e8400-e29b-41d4-a716-446655440000",
clone_mode="clone",
branch="main",
new_branch="feature/test",
)
assert request.new_branch == "feature/test"
# Valid request without new_branch
request2 = CreateInstanceRequest(
tool_type_id="550e8400-e29b-41d4-a716-446655440000",
clone_mode="clone",
branch="dev",
)
assert request2.new_branch is None
def test_create_instance_with_new_branch_sets_instance_branch(self):
"""Test that instance branch is set to new_branch when provided."""
# This tests the logic: data.new_branch if data.new_branch else data.branch
new_branch = "feature/test"
base_branch = "main"
# Simulate the logic from create_instance
stored_branch = new_branch if new_branch else base_branch
assert stored_branch == "feature/test"
# Without new_branch
stored_branch2 = None if None else base_branch
assert stored_branch2 == "main"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web
## role
Browser-based web frontend providing the user-facing React application for code editing, terminal access, and routing functionality.
Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
## parent
index: apps/.pi-map.index.md
map: apps/.pi-map.md
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/web
index: apps/web/.pi-map.index.md
## role
Browser-based web frontend providing the user-facing React application for code editing, terminal access, and routing functionality.
Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
## files
- .env.example | Template file defining example environment variables for frontend API and application URL configuration
- .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ECMAScript module support | dep: @typescript-eslint/parser, @typescript-eslint/eslint-plugin, eslint
@@ -16,7 +16,7 @@ Browser-based web frontend providing the user-facing React application for code
- tsconfig.json | TypeScript configuration file for a React project using Vite with modern ES2020 target and bundler module resolution | dep: typescript, react, vite
- vite.config.ts | Configures Vite build tool for a React project with custom dev server port and Vitest test settings. | dep: vite, @vitejs/plugin-react
## arch
Modern React SPA built with Vite and TypeScript, containerized via multi-stage Docker/nginx deployment with client-side routing and optimized static asset delivery.
Modern React SPA built with Vite and TypeScript, containerized via multi-stage Docker with nginx serving, featuring client-side routing, optimized static asset delivery, and development tooling (ESLint, Vitest).
## tags
react, eslint, vite, typescript, dom, application, nginx, web
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src
## role
Frontend web application entry point and core infrastructure for a React-based single-page application with authentication, routing, and domain type definitions.
Frontend web application entry point and core infrastructure for a React single-page application with authentication, routing, and domain type definitions.
## parent
index: apps/web/.pi-map.index.md
map: apps/web/.pi-map.md
+2 -2
View File
@@ -4,13 +4,13 @@ dir: apps/web/src
index: apps/web/src/.pi-map.index.md
## role
Frontend web application entry point and core infrastructure for a React-based single-page application with authentication, routing, and domain type definitions.
Frontend web application entry point and core infrastructure for a React single-page application with authentication, routing, and domain type definitions.
## files
- main.tsx | Entry point that bootstraps a React SPA with routing, authentication, and session management context providers. | dep: react, react-dom/client, react-router-dom, ./router, ./state/auth, ./state/sessions, ./styles/tokens.css, ./styles/global.css, ./styles/utilities.css, ./styles/syntax-highlight.css, ./styles/pages/git-history.css, ./styles/pages/repo-workspace.css, ./styles/pages/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css, react-dom, ./styles/*
- router.tsx | Defines the React Router configuration for a web application with protected routes, nested layouts, and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage
- types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
## arch
Layered React SPA architecture using context providers for cross-cutting concerns (auth/session), declarative routing with protected route guards and nested layouts, and centralized TypeScript type definitions for domain models.
Layered React SPA architecture using context providers for cross-cutting concerns (auth/session), declarative routing with nested layouts and route guards, and centralized TypeScript type definitions for domain models.
## tags
pages, styles, css, router, react, session, dom, workspace
## symbols
+1 -9
View File
@@ -31,8 +31,6 @@ export interface Session {
url: string | null;
container_status?: string;
probe_status?: string;
clone_mode?: string;
branch?: string | null;
created_at?: string;
}
@@ -51,12 +49,9 @@ export async function createInstance(
repoId: string,
toolTypeId: string,
displayName?: string,
cloneMode?: string,
branch?: string,
newBranch?: string,
workspaceId?: string,
configProfileId?: string,
sshKeyIds?: string[],
workspaceId?: string,
): Promise<ToolInstance> {
const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances`,
@@ -64,9 +59,6 @@ export async function createInstance(
tool_type_id: toolTypeId,
display_name: displayName,
workspace_id: workspaceId || undefined,
clone_mode: cloneMode || "mount",
branch: branch || undefined,
new_branch: newBranch || undefined,
config_profile_id: configProfileId,
ssh_key_ids: sshKeyIds || [],
},
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src/components
## role
Provides reusable, foundational React UI components and utilities for the web application, including layout shell, data visualization, navigation guards, and user feedback systems.
Provides foundational, reusable UI components and utilities for the web application, including layout shell, data states, icon system, code display, routing guards, and toast notification rules.
## parent
index: apps/web/src/.pi-map.index.md
map: apps/web/src/.pi-map.md
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/web/src/components
index: apps/web/src/components/.pi-map.index.md
## role
Provides reusable, foundational React UI components and utilities for the web application, including layout shell, data visualization, navigation guards, and user feedback systems.
Provides foundational, reusable UI components and utilities for the web application, including layout shell, data states, icon system, code display, routing guards, and toast notification rules.
## files
- app-shell.tsx | Renders the main application shell layout with navigation, session management, and responsive mobile/desktop views for a React Router-based app. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ../state/session-operations, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./features/session/session-progress-panel, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
- code-editor.tsx | A React component that renders a syntax-highlighted code editor with line numbers using react-simple-code-editor. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language
@@ -16,7 +16,7 @@ Provides reusable, foundational React UI components and utilities for the web ap
- toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events
- toast-rules.ts | Maps instance events to toast notifications with deduplication logic to prevent spam | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast state module, InstanceEventPayload type
## arch
Component-based architecture with functional React patterns, composition of specialized sub-components (icon, editor, syntax highlighter), separation of concerns via dedicated state-management components (data states, protected routes), and utility modules with pure logic for cross-cutting concerns (toast rules with deduplication).
Follows a component-based React architecture with separation of concerns between presentational components (icon, data-states, code-editor), layout/app-shell orchestration, auth-guarded routing (protected-route), and domain-specific utility modules (toast-rules) with colocated unit tests.
## tags
toast, state, react, icon, code, event, editor, protected
## symbols
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features
## role
Contains reusable React components that implement specific product features and business logic for the web application.
Contains specialized UI components for major feature areas of the web application, organizing components by business domain rather than by atomic design level.
## parent
index: apps/web/src/components/.pi-map.index.md
map: apps/web/src/components/.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
index: apps/web/src/components/features/.pi-map.index.md
## role
Contains reusable React components that implement specific product features and business logic for the web application.
Contains specialized UI components for major feature areas of the web application, organizing components by business domain rather than by atomic design level.
## files
## arch
Feature-based component organization with domain-specific UI building blocks, likely composed of atomic design elements (from components/ui) and consumed by page-level routes.
Feature-based colocation pattern where components are grouped by product functionality (e.g., checkout, dashboard, settings) rather than by component type, typically combining multiple atomic components with domain-specific logic and data fetching.
## tags
-
## symbols
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features/session
## role
Provides UI components for managing development environment sessions including creation, listing, monitoring progress, and interacting with individual sessions.
Provides UI components for managing development sessions including creation, listing, monitoring progress, and interacting with individual sessions.
## parent
index: apps/web/src/components/features/.pi-map.index.md
map: apps/web/src/components/features/.pi-map.md
@@ -4,16 +4,16 @@ dir: apps/web/src/components/features/session
index: apps/web/src/components/features/session/.pi-map.index.md
## role
Provides UI components for managing development environment sessions including creation, listing, monitoring progress, and interacting with individual sessions.
Provides UI components for managing development sessions including creation, listing, monitoring progress, and interacting with individual sessions.
## files
- create-session-form.tsx | React form component for creating development sessions with configurable project, repository, tool type, branch, SSH keys, and config profile options | exp: CreateSessionForm | dep: react, ../../icon, ../../../api/sessions, ../../../types, ../../../api/git-repositories, ../../../api/tool-types, ../../../api/ssh-keys, ../../../api/config-profiles, icon component, sessions API, git-repositories API, ssh-keys API, config-profiles API, tool-types API, types
- session-card.tsx | Renders a React card component displaying session information with status badges, inline editing, action buttons, and responsive mobile/desktop layouts. | exp: SessionCardProps, func:SessionCard({ session, onOpen, onStart, onStop, onDelete, onRecreateTunnel, onRename, isBusy = false, tunnelHealth = null, }: SessionCardProps), call:useState, call:useRef, call:useMobileViewport, call:session.tool_type_interfaces?.includes, call:[ "running", "building", "starting", "probing", "pending", "unhealthy", ].includes, call:useEffect, call:optionsRef.current.contains, call:setOptionsOpen, call:document.addEventListener, call:document.removeEventListener, call:window.confirm, call:onDelete, call:setEditName, call:onRename, call:setIsEditingName, call:editName.trim, call:onOpen, call:new Date(session.created_at).toLocaleString, call:setShowActionSheet, call:onStart, call:onStop, call:onRecreateTunnel | dep: react, ../../../api/sessions, ../../icon, ../../../hooks/use-mobile-viewport, ../mobile/mobile-action-sheet, icon, use-mobile-viewport, mobile-action-sheet, sessions api types
- create-session-form.tsx | A React form component for creating and starting a new development session with configurable project, repository, workspace, tool type, config profile, and SSH key options. | exp: CreateSessionForm | dep: react, ../../icon, ../../../api/sessions, ../../../types, ../../../api/git-repositories, ../../../api/tool-types, ../../../api/ssh-keys, ../../../api/config-profiles, ../../../api/workspaces, ../../../types/workspace, icon, sessions API, git-repositories API, tool-types API, ssh-keys API, config-profiles API, workspaces API, types
- session-card.tsx | Renders a card component displaying session information with status badges, inline rename editing, action buttons, and responsive mobile/desktop layouts including a dropdown options menu and mobile action sheet. | exp: SessionCardProps, func:SessionCard({ session, onOpen, onStart, onStop, onDelete, onRecreateTunnel, onRename, isBusy = false, tunnelHealth = null, }: SessionCardProps), call:useState, call:useRef, call:useMobileViewport, call:session.tool_type_interfaces?.includes, call:[ "running", "building", "starting", "probing", "pending", "unhealthy", ].includes, call:useEffect, call:optionsRef.current.contains, call:setOptionsOpen, call:document.addEventListener, call:document.removeEventListener, call:window.confirm, call:onDelete, call:setEditName, call:onRename, call:setIsEditingName, call:editName.trim, call:onOpen, call:new Date(session.created_at).toLocaleString, call:setShowActionSheet, call:onStart, call:onStop, call:onRecreateTunnel | dep: react, ../../../api/sessions, ../../icon, ../../../hooks/use-mobile-viewport, ../mobile/mobile-action-sheet
- session-list.tsx | Renders a list of sessions grouped by active/recent status or as a flat grid, delegating to SessionCard for individual session display. | exp: SessionListProps, func:SessionList({ sessions, onOpen, onStart, onStop, onDelete, onRecreateTunnel, onRename, actionBusyId = null, tunnelHealth = {}, showGrouping = true, activeTitle = "Active Sessions", recentTitle = "Recent Sessions", maxRecent = 5, emptyMessage = "No sessions", }: SessionListProps), call:sessions.filter, call:activeStatuses.includes, call:sessions .filter((s) => recentStatuses.includes(s.status)) .slice, call:recentStatuses.includes, call:sessions.map, call:activeSessions.map, call:recentSessions.map | dep: ../../../api/sessions, ./session-card, Session, SessionCard, InstanceHealth
- session-progress-panel.tsx | Renders a panel displaying active and recently completed session operations with step-by-step progress indicators and dismissible notifications. | exp: func:SessionProgressPanel(), call:useSessionOperations, call:useEventContext, call:useEffect, call:updateOperationFromEvent, call:operations.filter, call:Date.now, call:visibleOperations.map, call:dismissOperation | dep: react, ../../../state/session-operations, ../../../state/events, ../../icon, useSessionOperations, useEventContext, Icon
## arch
Feature-based component composition with presentational components following a container/presenter pattern, using status-driven conditional rendering and responsive layout adaptations.
React component composition with feature-specific grouping, responsive design patterns (mobile/desktop layouts), and status-driven conditional rendering with delegated sub-components.
## tags
session, call:use, call:on, card, mobile, call:set, event, api
session, call:use, call:on, api, card, call:set, event, mobile
## symbols
- SessionCard
- SessionList
@@ -6,17 +6,15 @@ import {
type ToolInstance,
} from "../../../api/sessions";
import type { Project } from "../../../types";
import {
listRepositoryBranches,
type GitRepository,
type Branch,
} from "../../../api/git-repositories";
import type { GitRepository } from "../../../api/git-repositories";
import type { ToolType } from "../../../api/tool-types";
import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys";
import {
listConfigProfiles,
type ConfigProfile,
} from "../../../api/config-profiles";
import { listWorkspaces, createWorkspace } from "../../../api/workspaces";
import type { Workspace } from "../../../types/workspace";
interface CreateSessionFormProps {
projects: Project[];
@@ -26,7 +24,6 @@ interface CreateSessionFormProps {
fixedRepoId?: string;
projectName?: string;
repoName?: string;
showCloneMode?: boolean;
showFixedFields?: boolean;
onProjectChange?: (projectId: string) => void;
onSuccess?: (instance: ToolInstance) => void;
@@ -43,7 +40,6 @@ export const CreateSessionForm = ({
fixedRepoId,
projectName,
repoName,
showCloneMode = true,
showFixedFields = true,
onProjectChange,
onSuccess,
@@ -55,18 +51,14 @@ export const CreateSessionForm = ({
const [selectedRepo, setSelectedRepo] = useState(fixedRepoId || "");
const [selectedToolType, setSelectedToolType] = useState("");
const [displayName, setDisplayName] = useState("");
const [cloneMode, setCloneMode] = useState<"mount" | "clone">("mount");
const [branch, setBranch] = useState("main");
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
const [configProfiles, setConfigProfiles] = useState<ConfigProfile[]>([]);
const [selectedConfigProfile, setSelectedConfigProfile] = useState("");
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 [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
const [isLoadingWorkspaces, setIsLoadingWorkspaces] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -108,30 +100,49 @@ export const CreateSessionForm = ({
void loadProfiles();
}, [selectedToolType, selectedProject, fixedProjectId]);
// Load branches when selected repo changes
// Load workspaces when repository is selected
useEffect(() => {
const projectId = fixedProjectId || selectedProject;
if (!selectedRepo || !projectId || !showCloneMode) {
setBranches([]);
const repoId = fixedRepoId || selectedRepo;
if (!projectId || !repoId) {
setWorkspaces([]);
setSelectedWorkspaceId("");
return;
}
const loadBranches = async () => {
setIsLoadingBranches(true);
const loadWorkspaces = async () => {
setIsLoadingWorkspaces(true);
try {
const response = await listRepositoryBranches(projectId, selectedRepo);
setBranches(response.branches);
if (response.default_branch) {
setBranch(response.default_branch);
setBaseBranch(response.default_branch);
const data = await listWorkspaces(projectId, repoId);
setWorkspaces(data);
if (data.length > 0) {
setSelectedWorkspaceId(data[0].id);
} else {
// Auto-create a default workspace so the user can start a tool
const workspace = await createWorkspace(projectId, repoId, {
name: "default",
branch: "main",
});
setWorkspaces([workspace]);
setSelectedWorkspaceId(workspace.id);
}
} catch {
// ignore
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to load workspaces",
);
setWorkspaces([]);
setSelectedWorkspaceId("");
} finally {
setIsLoadingBranches(false);
setIsLoadingWorkspaces(false);
}
};
void loadBranches();
}, [selectedRepo, selectedProject, fixedProjectId, showCloneMode]);
void loadWorkspaces();
}, [
fixedProjectId,
selectedProject,
fixedRepoId,
selectedRepo,
repositories,
]);
// Filter repositories by selected project
const availableRepos = selectedProject
@@ -143,14 +154,10 @@ export const CreateSessionForm = ({
if (!fixedRepoId) setSelectedRepo("");
setSelectedToolType("");
setDisplayName("");
setCloneMode("mount");
setBranch("main");
setIsCreatingNewBranch(false);
setNewBranchName("");
setBaseBranch("");
setBranches([]);
setSelectedSshKeyIds([]);
setSelectedConfigProfile("");
setWorkspaces([]);
setSelectedWorkspaceId("");
};
const handleSubmit = async (event: React.FormEvent) => {
@@ -165,31 +172,22 @@ export const CreateSessionForm = ({
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);
try {
const workspaceId = selectedWorkspaceId || undefined;
if (!workspaceId) {
setError("No workspace available for the selected repository");
setIsSubmitting(false);
return;
}
const instance = await createInstance(
projectId,
repoId,
selectedToolType,
displayName || undefined,
showCloneMode ? cloneMode : undefined,
showCloneMode && cloneMode === "clone"
? isCreatingNewBranch
? baseBranch
: branch
: undefined,
showCloneMode && cloneMode === "clone" && isCreatingNewBranch
? newBranchName
: undefined,
workspaceId,
selectedConfigProfile || undefined,
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined,
);
@@ -240,8 +238,6 @@ export const CreateSessionForm = ({
setSelectedProject(value);
setSelectedRepo("");
setSelectedToolType("");
setCloneMode("mount");
setIsCreatingNewBranch(false);
onProjectChange?.(value);
}}
disabled={isSubmitting}
@@ -277,8 +273,7 @@ export const CreateSessionForm = ({
onChange={(e) => {
setSelectedRepo(e.target.value);
setSelectedToolType("");
setCloneMode("mount");
setIsCreatingNewBranch(false);
setSelectedWorkspaceId("");
}}
disabled={!hasProject || isSubmitting}
>
@@ -293,6 +288,30 @@ export const CreateSessionForm = ({
</div>
)}
{/* Workspace */}
{hasRepo && (
<div className="form-field">
<label>Workspace</label>
{isLoadingWorkspaces ? (
<span className="muted">Loading workspaces...</span>
) : workspaces.length === 0 ? (
<span className="muted">No workspace available</span>
) : (
<select
value={selectedWorkspaceId}
onChange={(e) => setSelectedWorkspaceId(e.target.value)}
disabled={!hasRepo || isSubmitting || isLoadingWorkspaces}
>
{workspaces.map((w) => (
<option key={w.id} value={w.id}>
{w.name} ({w.branch})
</option>
))}
</select>
)}
</div>
)}
{/* Tool Type */}
{hasRepo && (
<div className="form-field">
@@ -301,8 +320,6 @@ export const CreateSessionForm = ({
value={selectedToolType}
onChange={(e) => {
setSelectedToolType(e.target.value);
setCloneMode("mount");
setIsCreatingNewBranch(false);
}}
disabled={!hasRepo || isSubmitting}
>
@@ -382,134 +399,6 @@ export const CreateSessionForm = ({
</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 */}
{hasToolType && (
<div className="form-field">
@@ -176,14 +176,6 @@ export function SessionCard({
)}{" "}
· {session.tool_type_name}
</p>
{session.clone_mode && (
<p className="muted session-card-meta">
<Icon name="branch" size="sm" />
{session.clone_mode === "clone"
? `Clone${session.branch ? ` (${session.branch})` : ""}`
: "Mount"}
</p>
)}
{session.url && (
<p className="session-card-url">
<button
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features/tool
## role
Provides UI components for managing container-based development tools, including instance lifecycle operations, manifest editing, and workspace-integrated tool launching.
Provides UI components for managing containerized development tools, including instance lifecycle operations, manifest editing, and workspace-integrated tool launching.
## parent
index: apps/web/src/components/features/.pi-map.index.md
map: apps/web/src/components/features/.pi-map.md
@@ -4,16 +4,16 @@ dir: apps/web/src/components/features/tool
index: apps/web/src/components/features/tool/.pi-map.index.md
## role
Provides UI components for managing container-based development tools, including instance lifecycle operations, manifest editing, and workspace-integrated tool launching.
Provides UI components for managing containerized development tools, including instance lifecycle operations, manifest editing, and workspace-integrated tool launching.
## files
- instance-list.tsx | Displays and manages a list of tool instances with CRUD operations, real-time status updates, and configuration profile selection. | exp: InstanceList | dep: react, react-router-dom, ../../icon, ../../../api/sessions, ../../../api/tool-types, ../session/create-session-form, ../../../api/config-profiles, ../../../api/ssh-keys, ../../../state/events, ../../../state/sessions, icon, api/sessions, api/tool-types, api/config-profiles, api/ssh-keys, state/events, state/sessions
- manifest-editor.tsx | A React component that provides a form-based UI for editing container tool definition manifests with fields for base images, packages, user config, environment variables, scripts, mounts, and runtime settings, including compilation preview functionality. | exp: ManifestEditor | dep: react, ../../icon, ../../../utils/errors, ../../../api/tool-definitions, icon, errors, tool-definitions
- start-tool-fab.tsx | A floating action button component that opens a modal to select a workspace and start a tool. | exp: func:StartToolFAB(), call:useState, call:setOpen, call:setWorkspacesLoading, call:listAllWorkspaces, call:setWorkspaces, call:setSelectedWorkspace, call:e.stopPropagation, call:workspaces.find, call:workspaces.map | dep: react, ../../icon, ./tool-starter, ../../../types/workspace, ../../../api/workspaces, icon, tool-starter, workspaces api, workspace types
- start-tool-modal.tsx | Renders a modal dialog for selecting a tool type and optional config profile to start on a workspace. | exp: StartToolModalProps, func:StartToolModal({ workspace, onClose, onStart, }: StartToolModalProps), call:useState, call:useAsyncData, call:e.preventDefault, call:setError, call:setSubmitting, call:onStart, call:onClose, call:e.stopPropagation, call:setToolTypeId, call:toolTypes?.map, call:setConfigProfileId | dep: react, ../../icon, ../../../api/tool-types, ../../../hooks/use-async-data, ../../../types/workspace, icon, tool-types, use-async-data, workspace
- tool-starter.tsx | React component that provides a workspace-first form for starting a new tool instance, fetching tool types, config profiles, and SSH keys dynamically. | exp: ToolStarterProps, func:ToolStarter({ workspace, onStarted, onCancel, }: ToolStarterProps), call:useSessions, call:useSessionOperations, call:useState, call:useEffect, call:listToolTypes, call:setToolTypes, call:setToolTypesError, call:setToolTypesLoading, call:load, call:setProfiles, call:setSelectedProfileId, call:setProfilesLoading, call:listConfigProfiles, call:data.find, call:listSSHKeys, call:setSshKeys, call:setSelectedSshKeyIds, call:console.error, call:setSshKeysLoading, call:sshKeys.find, call:useCallback, call:setError, call:setStarting, call:createInstance, call:displayName.trim, call:startInstance, call:addOrUpdateSession, call:startOperation, call:onStarted, call:setSelectedToolTypeId, call:toolTypes.find, call:setDisplayName, call:toolTypes.map, call:setNameEdited, call:profiles.map, call:sshKeys.map, call:selectedSshKeyIds.includes, call:prev.filter | dep: react, ../../icon, ../../../api/tool-types, ../../../api/config-profiles, ../../../api/ssh-keys, ../../../state/sessions, ../../../state/session-operations, ../../../types/workspace, ../../../api/sessions, icon, tool-types, config-profiles, ssh-keys, sessions, session-operations, workspace
- tool-starter.tsx | A React component that provides a workspace-first UI for selecting tool types, config profiles, and SSH keys to create and start a new tool instance/session. | exp: ToolStarterProps, func:ToolStarter({ workspace, onStarted, onCancel, }: ToolStarterProps), call:useSessions, call:useSessionOperations, call:useState, call:useEffect, call:listToolTypes, call:setToolTypes, call:setToolTypesError, call:setToolTypesLoading, call:load, call:setProfiles, call:setSelectedProfileId, call:setProfilesLoading, call:listConfigProfiles, call:data.find, call:listSSHKeys, call:setSshKeys, call:setSelectedSshKeyIds, call:console.error, call:setSshKeysLoading, call:sshKeys.find, call:useCallback, call:setError, call:setStarting, call:createInstance, call:displayName.trim, call:startInstance, call:addOrUpdateSession, call:startOperation, call:onStarted, call:setSelectedToolTypeId, call:toolTypes.find, call:setDisplayName, call:toolTypes.map, call:setNameEdited, call:profiles.map, call:sshKeys.map, call:selectedSshKeyIds.includes, call:prev.filter | dep: react, ../../icon, ../../../api/tool-types, ../../../api/config-profiles, ../../../api/ssh-keys, ../../../state/sessions, ../../../state/session-operations, ../../../types/workspace, ../../../api/sessions, icon, tool-types, config-profiles, ssh-keys, sessions, session-operations, workspace
- tools-bottom-sheet.tsx | Renders a mobile bottom sheet navigation menu for tools with active route highlighting | exp: ToolsBottomSheet | dep: react-router-dom, ../../icon, icon
## arch
Feature-based component architecture with form-driven modals, real-time status integration, and responsive mobile/desktop patterns (bottom sheets vs modals)
Feature-based component composition with modal/bottom-sheet navigation patterns, real-time status integration, and form-driven configuration management with preview capabilities.
## tags
tool, call:set, types, call:use, api, start, ssh, icon
## symbols
@@ -131,12 +131,9 @@ export function ToolStarter({
workspace.repo_id,
selectedToolTypeId,
displayName.trim() || undefined,
undefined,
undefined,
undefined,
workspace.id,
selectedProfileId || undefined,
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined,
workspace.id,
);
await startInstance(
workspace.project_id,
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src/hooks
## role
Provides a comprehensive collection of custom React hooks that encapsulate reusable stateful logic, side effects, and API interactions for the web application's UI, data management, and domain-specific operations.
Provides reusable React custom hooks that encapsulate UI state management, side effects, API integrations, and domain-specific logic for the web application frontend.
## parent
index: apps/web/src/.pi-map.index.md
map: apps/web/src/.pi-map.md
+3 -3
View File
@@ -4,7 +4,7 @@ dir: apps/web/src/hooks
index: apps/web/src/hooks/.pi-map.index.md
## role
Provides a comprehensive collection of custom React hooks that encapsulate reusable stateful logic, side effects, and API interactions for the web application's UI, data management, and domain-specific operations.
Provides reusable React custom hooks that encapsulate UI state management, side effects, API integrations, and domain-specific logic for the web application frontend.
## files
- use-async-data.ts | A custom React hook that manages asynchronous data fetching with loading, error, and ready states, plus a manual reload capability. | exp: func:useAsyncData(fetcher: () => Promise<T>, deps: React.DependencyList) → UseAsyncDataResult<T>, call:useState, call:useCallback, call:setStatus, call:setError, call:fetcher, call:setData, call:load, call:useEffect | dep: react
- use-auto-hide.ts | A React custom hook that automatically hides an element after a specified timeout and provides manual controls for showing, hiding, and toggling visibility. | exp: func:useAutoHide(options: AutoHideOptions), call:useState, call:useRef, call:Date.now, call:useCallback, call:setIsVisible, call:clearTimeout, call:setTimeout, call:hide, call:show, call:useEffect | dep: react
@@ -20,7 +20,7 @@ Provides a comprehensive collection of custom React hooks that encapsulate reusa
- use-repo-workspace.ts | A React custom hook that manages workspace state for a repository-based project, including loading project data, repositories, branches, git status, and tool types while synchronizing selection state with URL search parameters. | exp: Project, useRepoWorkspace | dep: react, react-router-dom, ../api/client, ../api/git-repositories, ../api/tool-types
- use-special-keys.ts | Maps special keys and modifier+character combinations to ANSI escape sequences for terminal input simulation. | exp: SpecialKey, ModifierKey, func:getSequenceWithModifier(key: SpecialKey, activeModifier: ModifierKey | null) → { sequence: string; clearModifier: boolean } | null, call:char.toLowerCase, func:applyModifierToChar(char: string, modifier: ModifierKey) → string | null, call:char.toLowerCase
- use-ssh-keys.ts | A React custom hook that manages SSH key operations including listing, generating, deleting, signing payloads, and verifying signatures. | exp: useSSHKeys | dep: react, ../api/ssh-keys, ./use-async-data
- use-start-tool.ts | React hook that manages the state and API calls for creating and starting a tool instance on a workspace | exp: UseStartToolResult, func:useStartTool() → UseStartToolResult, call:useState, call:useCallback, call:setStarting, call:setError, call:createInstance, call:startInstance | dep: react, ../api/sessions, ../types/workspace
- use-start-tool.ts | React hook for managing the state and API calls to create and start a tool instance on a workspace. | exp: UseStartToolResult, func:useStartTool() → UseStartToolResult, call:useState, call:useCallback, call:setStarting, call:setError, call:createInstance, call:startInstance | dep: react, ../api/sessions, ../types/workspace
- use-terminal-page.ts | Manages terminal page state including sessions, keyboard shortcuts, fullscreen mode, mobile viewport handling, and terminal lifecycle operations. | exp: useTerminalPage | dep: react, react-router-dom, ../components/features/terminal/terminal, ../components/features/terminal/terminal-session-tabs, ./use-mobile-viewport, ./use-auto-hide, ./use-virtual-keyboard, ./use-terminal-sessions, ../api/terminal, ./use-special-keys, use-mobile-viewport, use-auto-hide, use-virtual-keyboard, use-terminal-sessions, terminal, terminal-session-tabs, api/terminal, api/sessions, use-special-keys
- use-terminal-sessions.ts | React custom hook that manages terminal session state (CRUD operations, active session tracking) for a given instance | exp: UseTerminalSessionsResult, func:useTerminalSessions(instanceId: string) → UseTerminalSessionsResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listTerminalSessions, call:setSessions, call:setActiveSessionId, call:createTerminalSession, call:closeTerminalSession, call:prev.filter, call:renameTerminalSession, call:prev.map, call:resetTerminalSession, call:loadSessions, call:useEffect | dep: react, ../api/terminal
- use-theme.ts | React hook that fetches user theme preference on mount and applies it to the document root element via data-theme attribute | exp: func:useTheme(), call:useEffect, call:getUserConfig, call:document.documentElement.removeAttribute, call:document.documentElement.setAttribute | dep: react, ../api/settings
@@ -32,7 +32,7 @@ Provides a comprehensive collection of custom React hooks that encapsulate reusa
- use-workspace-instances.ts | Custom React hook for managing workspace instances with CRUD operations, loading states, and error handling. | exp: UseWorkspaceInstancesResult, func:useWorkspaceInstances(workspaceId: string) → UseWorkspaceInstancesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceInstances, call:setInstances, call:createWorkspaceInstance, call:refresh, call:useEffect | dep: react, ../api/workspace-instances, ../api/sessions
- use-workspaces.ts | Custom React hook that fetches and manages workspace data with loading and error states. | exp: UseWorkspacesResult, func:useWorkspaces(projectId: string, repoId: string) → UseWorkspacesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaces, call:listAllWorkspaces, call:setWorkspaces, call:useEffect, call:refresh | dep: react, ../api/workspaces, ../types/workspace
## arch
Follows the React Hooks pattern with custom hooks as the primary abstraction—each hook typically combines useState/useEffect/useCallback to manage local state, API calls, and side effects; many hooks integrate with TanStack Query/SWR-like patterns for server state (loading/error/reload), use context providers (e.g., NotificationProvider), and synchronize with URL search params; complex hooks compose simpler ones (e.g., use-terminal-page composes use-mobile-viewport, use-auto-hide) and use reducer-like state management for multi-faceted domain operations (CRUD, drag-and-drop, form handling).
Layered utility hooks following React composition patterns, with separation between generic UI behavior hooks (async data, auto-hide, viewport, theme), infrastructure hooks (SSE, terminal, keyboard), and domain-specific data/operation hooks (workspace, git, SSH, projects, instances) that wrap API calls with loading/error states and optimistic updates.
## tags
call:set, call:use, workspace, react, state, terminal, api, git
## symbols
+1 -4
View File
@@ -35,12 +35,9 @@ export function useStartTool(): UseStartToolResult {
workspace.repo_id,
toolTypeId,
displayName || workspace.name,
undefined,
undefined,
undefined,
workspace.id,
configProfileId,
[],
workspace.id,
);
await startInstance(
workspace.project_id,
+1 -1
View File
@@ -2,7 +2,7 @@
dir: openspec
## role
Defines the OpenSpec methodology and project configuration for managing software requirements, specifications, and task tracking as living documentation within a Docker-based coding agent management platform.
Defines the OpenSpec methodology and project configuration for managing living documentation and development discipline rules in a Docker-based coding agent platform.
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
+2 -2
View File
@@ -4,12 +4,12 @@ dir: openspec
index: openspec/.pi-map.index.md
## role
Defines the OpenSpec methodology and project configuration for managing software requirements, specifications, and task tracking as living documentation within a Docker-based coding agent management platform.
Defines the OpenSpec methodology and project configuration for managing living documentation and development discipline rules in a Docker-based coding agent platform.
## files
- README.md | Documents the OpenSpec methodology for managing software requirements, specifications, and task tracking as living documentation within a project repository. | dep: OpenSpec CLI (@fission-ai/openspec), Docker, SQLAlchemy, Alembic, Authentik, React, TypeScript, Tailwind, Traefik, Jinja2, xterm.js, pytest, mypy, ruff, npm
- config.yaml | Configuration file defining project metadata, technology stack, and software development discipline rules for a Docker-based coding agent management platform | dep: FastAPI, React, Vite, PostgreSQL, SQLAlchemy, Redis, Alembic, pytest, Docker Compose, Traefik, Authentik
## arch
Documentation-as-code pattern with YAML-based configuration management, combining structured metadata (config.yaml) with methodology documentation (README.md) to enforce software development discipline rules for automated agent workflows.
Documentation-as-code pattern with YAML-based configuration management, embedding requirements/specifications directly in the repository alongside structured project metadata and technology stack definitions.
## tags
software, project, docker, sqlalchemy, alembic, authentik, react, traefik
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: openspec/changes
## role
Tracks and manages specification changes/versions for OpenAPI documents
Manages change tracking, versioning, and audit logging for OpenSpec schema or configuration modifications.
## parent
index: openspec/.pi-map.index.md
map: openspec/.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: openspec/changes
index: openspec/changes/.pi-map.index.md
## role
Tracks and manages specification changes/versions for OpenAPI documents
Manages change tracking, versioning, and audit logging for OpenSpec schema or configuration modifications.
## files
## arch
Simple data structure package with record types for change metadata, likely used by diff/merge tooling
Event-sourced or changelog-based architecture with immutable change records, likely supporting rollback, diff computation, and history querying patterns.
## tags
-
## symbols
@@ -0,0 +1,4 @@
name: working-copies
status: completed
started_at: 2026-05-28
completed_at: 2026-06-12
@@ -1,22 +1,23 @@
# working-copies (index)
dir: working-copies
# openspec/changes/working-copies (index)
dir: openspec/changes/working-copies
## role
This package contains design documents and implementation planning for a workspace-based repository access system that replaces direct repository mounting/cloning with isolated, persistent, writable git working copies shared across tool instances.
Design and specification package for replacing direct repository mounting/cloning with persistent, shared Git working copy workspaces in tool instances.
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
index: openspec/changes/.pi-map.index.md
map: openspec/changes/.pi-map.md
## children
-
## files
- .openspec.yaml
- design.md
- explore.md
- proposal.md
- spec.md
- tasks.md
## links
index: working-copies/.pi-map.index.md
map: working-copies/.pi-map.md
index: openspec/changes/working-copies/.pi-map.index.md
map: openspec/changes/working-copies/.pi-map.md
## workflows
-
## dirty
+8 -7
View File
@@ -1,20 +1,21 @@
# working-copies
dir: working-copies
# openspec/changes/working-copies
dir: openspec/changes/working-copies
index: working-copies/.pi-map.index.md
index: openspec/changes/working-copies/.pi-map.index.md
## role
This package contains design documents and implementation planning for a workspace-based repository access system that replaces direct repository mounting/cloning with isolated, persistent, writable git working copies shared across tool instances.
Design and specification package for replacing direct repository mounting/cloning with persistent, shared Git working copy workspaces in tool instances.
## files
- .openspec.yaml | Defines metadata for a completed project named "working-copies" with timeline tracking
- design.md | Design document for implementing workspace-based tool instances that replace direct repository mounting with isolated git working copies | dep: FastAPI, SQLAlchemy, Alembic, Docker Compose, Git, React/TypeScript, asyncio subprocess
- explore.md | Design document proposing "Working Copies" (named "Workspace") as persistent writable clones of repositories to replace direct repo mounting/cloning in tool instances | dep: GitRepository, ToolInstance, Project, User, database, compose generation, filesystem mount system
- proposal.md | Proposes a new "Workspace" entity to replace the confusing mount/clone mode dichotomy for tool instances, enabling persistent writable repository clones that multiple tools can share.
- spec.md | Technical specification for implementing persistent workspace-based tool instances that replace mount/clone modes with explicit Git repository workspaces | dep: Git, PostgreSQL, REST API, React/TypeScript frontend, Docker containers, Python backend
- tasks.md | A project task breakdown document defining a phased implementation plan for adding workspace-based tool instances to a full-stack application, including backend foundation, backend integration, frontend core, and frontend integration PRs with detailed tasks, acceptance criteria, and verification steps. | dep: Alembic, FastAPI, SQLAlchemy, React, TypeScript, Git, Docker Compose, pytest, ruff, ESLint, npm
- tasks.md | Project task tracking document for implementing workspace-based tool instances across backend and frontend in a multi-PR phased approach | dep: Alembic, FastAPI, SQLAlchemy, React, TypeScript, pytest, ruff, ESLint, Git, Docker Compose
## arch
Design-driven documentation package using phased specification approach (exploration → proposal → design → spec → tasks) to transition from a dual-mode (mount/clone) architecture to a unified workspace entity model with full-stack implementation planning.
Document-driven design process using layered specification documents (exploration → proposal → design → spec → tasks) with YAML metadata tracking, following a phased multi-PR implementation strategy across backend and frontend systems.
## tags
workspace, tool, instances, git, design, replace, document, repository
workspace, tool, instances, git, design, replace, project, working
## symbols
-
## workflows
+36 -36
View File
@@ -16,13 +16,13 @@
**Files touched**: 8 new, 2 modified
**Tasks**:
1. [ ] Create Alembic migration for `workspaces` table + `workspace_id` on `tool_instances`
2. [ ] Create `Workspace` model (`apps/api/src/models/workspace.py`)
3. [ ] Add `workspace_id` to `ToolInstance` model (nullable FK)
4. [ ] Create `GitService` (`apps/api/src/services/git_service.py`) — clone, fetch, pull, branch_exists_remotely
5. [ ] Create `WorkspaceManager` (`apps/api/src/services/workspace_manager.py`) — create, delete, sync
6. [ ] Create workspace API router (`apps/api/src/api/workspaces.py`) — CRUD + sync endpoints
7. [ ] Add workspace routes to FastAPI app (`apps/api/src/main.py`)
1. [x] Create Alembic migration for `workspaces` table + `workspace_id` on `tool_instances`
2. [x] Create `Workspace` model (`apps/api/src/models/workspace.py`)
3. [x] Add `workspace_id` to `ToolInstance` model (nullable FK)
4. [x] Create `GitService` (`apps/api/src/services/git_service.py`) — clone, fetch, pull, branch_exists_remotely
5. [x] Create `WorkspaceManager` (`apps/api/src/services/workspace_manager.py`) — create, delete, sync
6. [x] Create workspace API router (`apps/api/src/api/workspaces.py`) — CRUD + sync endpoints
7. [x] Add workspace routes to FastAPI app (`apps/api/src/main.py`)
8. [ ] Write unit tests for GitService
9. [ ] Write integration tests for workspace CRUD
10. [ ] Write integration tests for delete-with-instances (409 behavior)
@@ -34,13 +34,13 @@
**Files touched**: 3 modified
**Tasks**:
1. [ ] Update `create_instance` endpoint to accept `workspace_id` instead of `clone_mode`
2. [ ] Update `start_instance` to mount workspace path (`workspace.path`) instead of repo path
3. [ ] Update compose generation to use `WORKSPACE_PATH` variable
4. [ ] Update `tool_instances.py` compose template rendering
1. [x] Update `create_instance` endpoint to accept `workspace_id` instead of `clone_mode`
2. [x] Update `start_instance` to mount workspace path (`workspace.path`) instead of repo path
3. [x] Update compose generation to use `WORKSPACE_PATH` variable
4. [x] Update `tool_instances.py` compose template rendering
5. [ ] Write integration tests for instance creation with workspace
6. [ ] Write integration tests for instance start with workspace mount
7. [ ] Verify old mount_mode instances still work (backward compat)
7. [x] Verify old mount_mode instances still work (backward compat)
### PR-3: Frontend Core
**Scope**: Workspaces UI — list, create, card, actions
@@ -48,13 +48,13 @@
**Files touched**: 10 new, 2 modified
**Tasks**:
1. [ ] Create workspace types (`apps/web/src/types/workspace.ts`)
2. [ ] Create workspace API client (`apps/web/src/api/workspaces.ts`)
3. [ ] Create `useWorkspaces` hook (`apps/web/src/hooks/use-workspaces.ts`)
4. [ ] Create `useWorkspaceActions` hook (`apps/web/src/hooks/use-workspace-actions.ts`)
5. [ ] Create `WorkspaceCard` component (`apps/web/src/components/workspace-card.tsx`)
6. [ ] Create `WorkspaceCreateForm` component (`apps/web/src/components/workspace-create-form.tsx`)
7. [ ] Create `StartToolModal` component (`apps/web/src/components/start-tool-modal.tsx`)
1. [x] Create workspace types (`apps/web/src/types/workspace.ts`)
2. [x] Create workspace API client (`apps/web/src/api/workspaces.ts`)
3. [x] Create `useWorkspaces` hook (`apps/web/src/hooks/use-workspaces.ts`)
4. [x] Create `useWorkspaceActions` hook (`apps/web/src/hooks/use-workspace-actions.ts`)
5. [x] Create `WorkspaceCard` component (`apps/web/src/components/workspace-card.tsx`)
6. [x] Create `WorkspaceCreateForm` component (`apps/web/src/components/workspace-create-form.tsx`)
7. [x] Create `StartToolModal` component (`apps/web/src/components/start-tool-modal.tsx`)
8. [ ] Create `WorkspacesPage` (`apps/web/src/pages/workspaces.tsx`)
9. [ ] Update `Sidebar` to add Workspaces nav item
10. [ ] Update router/routes to include `/workspaces`
@@ -68,30 +68,30 @@
**Files touched**: 5 modified
**Tasks**:
1. [ ] Update `CreateSessionForm` to use workspace picker instead of repo+clone_mode
1. [x] Update `CreateSessionForm` to use workspace picker instead of repo+clone_mode
2. [ ] Update `SessionsPage` dashboard to show workspaces section
3. [ ] Update `SessionCard` to show workspace name instead of clone mode
4. [ ] Update `useInstanceActions` to pass `workspace_id` on create
5. [ ] Remove clone_mode/mount_mode UI toggles
6. [ ] Update types to remove deprecated `clone_mode` field
3. [x] Update `SessionCard` to show workspace name instead of clone mode
4. [x] Update `useInstanceActions` to pass `workspace_id` on create
5. [x] Remove clone_mode/mount_mode UI toggles
6. [x] Update types to remove deprecated `clone_mode` field
7. [ ] Write integration tests for full create-workspace → start-tool flow
8. [ ] Write tests for dashboard workspaces section
## Acceptance Criteria (All PRs)
- [ ] User can create a workspace from any repository
- [ ] User can create unlimited workspaces per repository
- [ ] Workspace names are unique per repo
- [ ] Tool instances mount the workspace path
- [ ] Multiple tool instances can share one workspace
- [ ] Workspaces persist after tool instance deletion
- [ ] Deleting a workspace with running instances shows confirmation, stops and deletes instances
- [ ] Syncing a workspace with a deleted remote branch shows confirmation
- [ ] UI no longer shows "mount vs clone" toggle
- [x] User can create a workspace from any repository
- [x] User can create unlimited workspaces per repository
- [x] Workspace names are unique per repo
- [x] Tool instances mount the workspace path
- [x] Multiple tool instances can share one workspace
- [x] Workspaces persist after tool instance deletion
- [x] Deleting a workspace with running instances shows confirmation, stops and deletes instances
- [x] Syncing a workspace with a deleted remote branch shows confirmation
- [x] UI no longer shows "mount vs clone" toggle
- [ ] New sidebar navigation "Workspaces" exists
- [ ] All existing tests still pass
- [ ] ruff clean
- [ ] TypeScript compilation clean
- [x] All existing tests still pass
- [x] ruff clean
- [x] TypeScript compilation clean
## Implementation Order