feat: merge working-copies completion
See feature commit for details.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ dir: .
|
|||||||
Trust boundary: index routes, map orients, source decides.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
-
|
-
|
||||||
## children
|
## children
|
||||||
|
|||||||
+3
-3
@@ -18,10 +18,10 @@ index: ./.pi-map.index.md
|
|||||||
Trust boundary: index routes, map orients, source decides.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## 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
|
## files
|
||||||
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
|
- .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
|
- 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
|
- 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
|
- 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
|
- 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
|
- swap-pane | Empty file with no functionality
|
||||||
## arch
|
## 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
|
## tags
|
||||||
docker, redis, git, application, postgresql, compose, traefik, project
|
docker, redis, git, application, postgresql, compose, traefik, project
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps
|
dir: apps
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: ./.pi-map.index.md
|
index: ./.pi-map.index.md
|
||||||
map: ./.pi-map.md
|
map: ./.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,10 +4,10 @@ dir: apps
|
|||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## files
|
||||||
## arch
|
## 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
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -74,8 +74,6 @@ async def get_user_sessions(
|
|||||||
"workspace_name": workspace_name,
|
"workspace_name": workspace_name,
|
||||||
"status": instance.status,
|
"status": instance.status,
|
||||||
"url": instance.url,
|
"url": instance.url,
|
||||||
"clone_mode": instance.clone_mode,
|
|
||||||
"branch": instance.branch,
|
|
||||||
"selected_config_profile_id": str(instance.selected_config_profile_id)
|
"selected_config_profile_id": str(instance.selected_config_profile_id)
|
||||||
if instance.selected_config_profile_id
|
if instance.selected_config_profile_id
|
||||||
else None,
|
else None,
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ async def create_instance(
|
|||||||
"display_name": instance.display_name,
|
"display_name": instance.display_name,
|
||||||
"tool_type_id": str(instance.tool_type_id),
|
"tool_type_id": str(instance.tool_type_id),
|
||||||
"status": instance.status,
|
"status": instance.status,
|
||||||
"clone_mode": instance.clone_mode,
|
"workspace_id": str(instance.workspace_id) if instance.workspace_id else None,
|
||||||
"branch": instance.branch,
|
|
||||||
"selected_config_profile_id": str(instance.selected_config_profile_id) if instance.selected_config_profile_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(),
|
"created_at": instance.created_at.isoformat(),
|
||||||
}
|
}
|
||||||
@@ -143,8 +142,6 @@ async def get_instance(
|
|||||||
"container_id": instance.container_id,
|
"container_id": instance.container_id,
|
||||||
"container_name": instance.container_name,
|
"container_name": instance.container_name,
|
||||||
"compose_path": instance.compose_path,
|
"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,
|
"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,
|
"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,
|
"ssh_key_ids": instance.ssh_key_ids,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ class ToolInstance(UUIDPrimaryKeyMixin, TimestampMixin, Base):
|
|||||||
)
|
)
|
||||||
image_tag: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
image_tag: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||||
probe_result: Mapped[dict | None] = mapped_column(JSON, 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(
|
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(
|
selected_config_profile_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
UUID(), ForeignKey("config_profiles.id", ondelete="SET NULL"), nullable=True
|
UUID(), ForeignKey("config_profiles.id", ondelete="SET NULL"), nullable=True
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -13,16 +13,24 @@ class CreateInstanceRequest(BaseModel):
|
|||||||
default=None, description="Optional display name for the instance"
|
default=None, description="Optional display name for the instance"
|
||||||
)
|
)
|
||||||
workspace_id: str | None = Field(
|
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(
|
config_profile_id: str | None = Field(
|
||||||
default="mount", description="Repository access mode: 'mount' or 'clone'"
|
default=None, description="Optional config profile ID for launch"
|
||||||
)
|
)
|
||||||
branch: str | None = Field(
|
ssh_key_ids: list[str] = Field(
|
||||||
default="main", description="Branch to clone (when clone_mode='clone')"
|
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(
|
config_profile_id: str | None = Field(
|
||||||
default=None, description="Optional config profile ID for launch"
|
default=None, description="Optional config profile ID for launch"
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class WorkspaceManager:
|
|||||||
|
|
||||||
# Stop and delete all instances
|
# Stop and delete all instances
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
await self._stop_and_delete_instance(instance)
|
await self._stop_and_delete_instance(instance, session)
|
||||||
|
|
||||||
# Delete directory
|
# Delete directory
|
||||||
if os.path.exists(workspace.path):
|
if os.path.exists(workspace.path):
|
||||||
@@ -248,10 +248,154 @@ class WorkspaceManager:
|
|||||||
)
|
)
|
||||||
return list(result.scalars().all())
|
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.
|
"""Stop and delete a tool instance.
|
||||||
|
|
||||||
TODO(PR-2): Wire up to actual instance stop/delete logic.
|
Delegates to the instance lifecycle service to ensure containers,
|
||||||
For now, this is a placeholder.
|
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
|
||||||
|
|||||||
@@ -830,8 +830,6 @@ async def prepare_manifest_instance(
|
|||||||
|
|
||||||
# Prepare SSH path for mount resolution
|
# Prepare SSH path for mount resolution
|
||||||
ssh_path = ""
|
ssh_path = ""
|
||||||
if instance.clone_mode == "clone":
|
|
||||||
ssh_path = os.path.join(instance_dir, ".ssh")
|
|
||||||
|
|
||||||
# Resolve git mount variables from config profile
|
# Resolve git mount variables from config profile
|
||||||
git_mount_vars = {}
|
git_mount_vars = {}
|
||||||
@@ -848,6 +846,7 @@ async def prepare_manifest_instance(
|
|||||||
"IMAGE_TAG": image_tag,
|
"IMAGE_TAG": image_tag,
|
||||||
"INSTANCE_NAME": instance.name.lower(),
|
"INSTANCE_NAME": instance.name.lower(),
|
||||||
"INSTANCE_DIR": instance_dir,
|
"INSTANCE_DIR": instance_dir,
|
||||||
|
"WORKSPACE_PATH": repo_path,
|
||||||
"REPO_PATH": repo_path,
|
"REPO_PATH": repo_path,
|
||||||
"SSH_PATH": ssh_path,
|
"SSH_PATH": ssh_path,
|
||||||
"TOOL_PORT": instance.port or 0,
|
"TOOL_PORT": instance.port or 0,
|
||||||
@@ -922,13 +921,6 @@ async def create_tool_instance(
|
|||||||
if workspace.repo_id != repo_id:
|
if workspace.repo_id != repo_id:
|
||||||
raise ValueError("workspace does not belong to this repository")
|
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
|
# Generate unique name
|
||||||
instance_name = f"{tool_type.name}-{repo.name}-{uuid.uuid4().hex[:8]}"
|
instance_name = f"{tool_type.name}-{repo.name}-{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
@@ -970,71 +962,13 @@ async def create_tool_instance(
|
|||||||
# Find free port
|
# Find free port
|
||||||
tool_port = 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:
|
if workspace:
|
||||||
repo_path = workspace.path
|
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:
|
else:
|
||||||
repo_path = repo.path
|
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
|
# Handle based on definition type
|
||||||
if tool_type.definition_type == "dockerfile":
|
if tool_type.definition_type == "dockerfile":
|
||||||
image_tag = f"headquarter/{instance_name}:latest".lower()
|
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)
|
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 = {
|
variables = {
|
||||||
"IMAGE_TAG": image_tag,
|
"IMAGE_TAG": image_tag,
|
||||||
"INSTANCE_NAME": instance_name.lower(),
|
"INSTANCE_NAME": instance_name.lower(),
|
||||||
"INSTANCE_DIR": instance_dir,
|
"INSTANCE_DIR": instance_dir,
|
||||||
|
"WORKSPACE_PATH": repo_path,
|
||||||
"REPO_PATH": repo_path,
|
"REPO_PATH": repo_path,
|
||||||
"SSH_PATH": "",
|
"SSH_PATH": "",
|
||||||
"TOOL_PORT": tool_port,
|
"TOOL_PORT": tool_port,
|
||||||
@@ -1109,6 +1046,7 @@ async def create_tool_instance(
|
|||||||
if not tool_type.compose_template:
|
if not tool_type.compose_template:
|
||||||
raise ValueError("Tool type has no compose template configured")
|
raise ValueError("Tool type has no compose template configured")
|
||||||
variables = {
|
variables = {
|
||||||
|
"WORKSPACE_PATH": repo_path,
|
||||||
"REPO_PATH": repo_path,
|
"REPO_PATH": repo_path,
|
||||||
"INSTANCE_NAME": instance_name,
|
"INSTANCE_NAME": instance_name,
|
||||||
"INSTANCE_ID": instance_name,
|
"INSTANCE_ID": instance_name,
|
||||||
@@ -1121,37 +1059,6 @@ async def create_tool_instance(
|
|||||||
tool_type.compose_template, variables
|
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)
|
write_compose_file(instance_dir, compose_content)
|
||||||
|
|
||||||
# Create database record
|
# Create database record
|
||||||
@@ -1166,10 +1073,8 @@ async def create_tool_instance(
|
|||||||
compose_path=compose_path,
|
compose_path=compose_path,
|
||||||
port=tool_port,
|
port=tool_port,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
clone_mode=data.clone_mode,
|
clone_mode=None,
|
||||||
branch=data.new_branch
|
branch=None,
|
||||||
if data.new_branch
|
|
||||||
else (data.branch if data.clone_mode == "clone" else None),
|
|
||||||
selected_config_profile_id=selected_profile_id,
|
selected_config_profile_id=selected_profile_id,
|
||||||
ssh_key_ids=data.ssh_key_ids or None,
|
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
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web
|
dir: apps/web
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
map: apps/.pi-map.md
|
map: apps/.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ dir: apps/web
|
|||||||
index: apps/web/.pi-map.index.md
|
index: apps/web/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## files
|
||||||
- .env.example | Template file defining example environment variables for frontend API and application URL configuration
|
- .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
|
- .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
|
- 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
|
- 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
|
## 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
|
## tags
|
||||||
react, eslint, vite, typescript, dom, application, nginx, web
|
react, eslint, vite, typescript, dom, application, nginx, web
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src
|
dir: apps/web/src
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/.pi-map.index.md
|
index: apps/web/.pi-map.index.md
|
||||||
map: apps/web/.pi-map.md
|
map: apps/web/.pi-map.md
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ dir: apps/web/src
|
|||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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/*
|
- 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
|
- 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
|
- types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
|
||||||
## arch
|
## 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
|
## tags
|
||||||
pages, styles, css, router, react, session, dom, workspace
|
pages, styles, css, router, react, session, dom, workspace
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -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 || [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components
|
dir: apps/web/src/components
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.md
|
map: apps/web/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ dir: apps/web/src/components
|
|||||||
index: apps/web/src/components/.pi-map.index.md
|
index: apps/web/src/components/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- 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
|
- 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.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
|
- 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
|
## 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
|
## tags
|
||||||
toast, state, react, icon, code, event, editor, protected
|
toast, state, react, icon, code, event, editor, protected
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features
|
dir: apps/web/src/components/features
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/components/.pi-map.index.md
|
index: apps/web/src/components/.pi-map.index.md
|
||||||
map: apps/web/src/components/.pi-map.md
|
map: apps/web/src/components/.pi-map.md
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
|
|||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## files
|
||||||
## arch
|
## 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
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/session
|
dir: apps/web/src/components/features/session
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.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
|
index: apps/web/src/components/features/session/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- 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 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
|
- 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-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
|
- 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
|
## 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
|
## tags
|
||||||
session, call:use, call:on, card, mobile, call:set, event, api
|
session, call:use, call:on, api, card, call:set, event, mobile
|
||||||
## symbols
|
## symbols
|
||||||
- SessionCard
|
- SessionCard
|
||||||
- SessionList
|
- SessionList
|
||||||
|
|||||||
@@ -6,17 +6,15 @@ 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 {
|
||||||
listConfigProfiles,
|
listConfigProfiles,
|
||||||
type ConfigProfile,
|
type ConfigProfile,
|
||||||
} from "../../../api/config-profiles";
|
} from "../../../api/config-profiles";
|
||||||
|
import { listWorkspaces, createWorkspace } from "../../../api/workspaces";
|
||||||
|
import type { Workspace } from "../../../types/workspace";
|
||||||
|
|
||||||
interface CreateSessionFormProps {
|
interface CreateSessionFormProps {
|
||||||
projects: Project[];
|
projects: Project[];
|
||||||
@@ -26,7 +24,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 +40,6 @@ export const CreateSessionForm = ({
|
|||||||
fixedRepoId,
|
fixedRepoId,
|
||||||
projectName,
|
projectName,
|
||||||
repoName,
|
repoName,
|
||||||
showCloneMode = true,
|
|
||||||
showFixedFields = true,
|
showFixedFields = true,
|
||||||
onProjectChange,
|
onProjectChange,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
@@ -55,18 +51,14 @@ 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 [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
||||||
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
|
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
|
||||||
const [isCreatingNewBranch, setIsCreatingNewBranch] = useState(false);
|
const [isLoadingWorkspaces, setIsLoadingWorkspaces] = 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,30 +100,49 @@ export const CreateSessionForm = ({
|
|||||||
void loadProfiles();
|
void loadProfiles();
|
||||||
}, [selectedToolType, selectedProject, fixedProjectId]);
|
}, [selectedToolType, selectedProject, fixedProjectId]);
|
||||||
|
|
||||||
// Load branches when selected repo changes
|
// Load workspaces when repository is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const projectId = fixedProjectId || selectedProject;
|
const projectId = fixedProjectId || selectedProject;
|
||||||
if (!selectedRepo || !projectId || !showCloneMode) {
|
const repoId = fixedRepoId || selectedRepo;
|
||||||
setBranches([]);
|
if (!projectId || !repoId) {
|
||||||
|
setWorkspaces([]);
|
||||||
|
setSelectedWorkspaceId("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const loadBranches = async () => {
|
const loadWorkspaces = async () => {
|
||||||
setIsLoadingBranches(true);
|
setIsLoadingWorkspaces(true);
|
||||||
try {
|
try {
|
||||||
const response = await listRepositoryBranches(projectId, selectedRepo);
|
const data = await listWorkspaces(projectId, repoId);
|
||||||
setBranches(response.branches);
|
setWorkspaces(data);
|
||||||
if (response.default_branch) {
|
if (data.length > 0) {
|
||||||
setBranch(response.default_branch);
|
setSelectedWorkspaceId(data[0].id);
|
||||||
setBaseBranch(response.default_branch);
|
} 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 {
|
} catch (err) {
|
||||||
// ignore
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to load workspaces",
|
||||||
|
);
|
||||||
|
setWorkspaces([]);
|
||||||
|
setSelectedWorkspaceId("");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingBranches(false);
|
setIsLoadingWorkspaces(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
void loadBranches();
|
void loadWorkspaces();
|
||||||
}, [selectedRepo, selectedProject, fixedProjectId, showCloneMode]);
|
}, [
|
||||||
|
fixedProjectId,
|
||||||
|
selectedProject,
|
||||||
|
fixedRepoId,
|
||||||
|
selectedRepo,
|
||||||
|
repositories,
|
||||||
|
]);
|
||||||
|
|
||||||
// Filter repositories by selected project
|
// Filter repositories by selected project
|
||||||
const availableRepos = selectedProject
|
const availableRepos = selectedProject
|
||||||
@@ -143,14 +154,10 @@ 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("");
|
||||||
|
setWorkspaces([]);
|
||||||
|
setSelectedWorkspaceId("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (event: React.FormEvent) => {
|
const handleSubmit = async (event: React.FormEvent) => {
|
||||||
@@ -165,31 +172,22 @@ 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 {
|
||||||
|
const workspaceId = selectedWorkspaceId || undefined;
|
||||||
|
if (!workspaceId) {
|
||||||
|
setError("No workspace available for the selected repository");
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const instance = await createInstance(
|
const instance = await createInstance(
|
||||||
projectId,
|
projectId,
|
||||||
repoId,
|
repoId,
|
||||||
selectedToolType,
|
selectedToolType,
|
||||||
displayName || undefined,
|
displayName || undefined,
|
||||||
showCloneMode ? cloneMode : undefined,
|
workspaceId,
|
||||||
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 +238,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 +273,7 @@ export const CreateSessionForm = ({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedRepo(e.target.value);
|
setSelectedRepo(e.target.value);
|
||||||
setSelectedToolType("");
|
setSelectedToolType("");
|
||||||
setCloneMode("mount");
|
setSelectedWorkspaceId("");
|
||||||
setIsCreatingNewBranch(false);
|
|
||||||
}}
|
}}
|
||||||
disabled={!hasProject || isSubmitting}
|
disabled={!hasProject || isSubmitting}
|
||||||
>
|
>
|
||||||
@@ -293,6 +288,30 @@ export const CreateSessionForm = ({
|
|||||||
</div>
|
</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 */}
|
{/* Tool Type */}
|
||||||
{hasRepo && (
|
{hasRepo && (
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
@@ -301,8 +320,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 +399,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">
|
||||||
|
|||||||
@@ -176,14 +176,6 @@ export function SessionCard({
|
|||||||
)}{" "}
|
)}{" "}
|
||||||
· {session.tool_type_name}
|
· {session.tool_type_name}
|
||||||
</p>
|
</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 && (
|
{session.url && (
|
||||||
<p className="session-card-url">
|
<p className="session-card-url">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/tool
|
dir: apps/web/src/components/features/tool
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.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
|
index: apps/web/src/components/features/tool/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- 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
|
- 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-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
|
- 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
|
- 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
|
## 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
|
## tags
|
||||||
tool, call:set, types, call:use, api, start, ssh, icon
|
tool, call:set, types, call:use, api, start, ssh, icon
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -131,12 +131,9 @@ export function ToolStarter({
|
|||||||
workspace.repo_id,
|
workspace.repo_id,
|
||||||
selectedToolTypeId,
|
selectedToolTypeId,
|
||||||
displayName.trim() || undefined,
|
displayName.trim() || undefined,
|
||||||
undefined,
|
workspace.id,
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
selectedProfileId || undefined,
|
selectedProfileId || undefined,
|
||||||
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined,
|
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined,
|
||||||
workspace.id,
|
|
||||||
);
|
);
|
||||||
await startInstance(
|
await startInstance(
|
||||||
workspace.project_id,
|
workspace.project_id,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/hooks
|
dir: apps/web/src/hooks
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.md
|
map: apps/web/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ dir: apps/web/src/hooks
|
|||||||
index: apps/web/src/hooks/.pi-map.index.md
|
index: apps/web/src/hooks/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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-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
|
- 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-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-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-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-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-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
|
- 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-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
|
- 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
|
## 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
|
## tags
|
||||||
call:set, call:use, workspace, react, state, terminal, api, git
|
call:set, call:use, workspace, react, state, terminal, api, git
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -35,12 +35,9 @@ export function useStartTool(): UseStartToolResult {
|
|||||||
workspace.repo_id,
|
workspace.repo_id,
|
||||||
toolTypeId,
|
toolTypeId,
|
||||||
displayName || workspace.name,
|
displayName || workspace.name,
|
||||||
undefined,
|
workspace.id,
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
configProfileId,
|
configProfileId,
|
||||||
[],
|
[],
|
||||||
workspace.id,
|
|
||||||
);
|
);
|
||||||
await startInstance(
|
await startInstance(
|
||||||
workspace.project_id,
|
workspace.project_id,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: openspec
|
dir: openspec
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: ./.pi-map.index.md
|
index: ./.pi-map.index.md
|
||||||
map: ./.pi-map.md
|
map: ./.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,12 +4,12 @@ dir: openspec
|
|||||||
index: openspec/.pi-map.index.md
|
index: openspec/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- 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
|
- 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
|
## 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
|
## tags
|
||||||
software, project, docker, sqlalchemy, alembic, authentik, react, traefik
|
software, project, docker, sqlalchemy, alembic, authentik, react, traefik
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: openspec/changes
|
dir: openspec/changes
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Tracks and manages specification changes/versions for OpenAPI documents
|
Manages change tracking, versioning, and audit logging for OpenSpec schema or configuration modifications.
|
||||||
## parent
|
## parent
|
||||||
index: openspec/.pi-map.index.md
|
index: openspec/.pi-map.index.md
|
||||||
map: openspec/.pi-map.md
|
map: openspec/.pi-map.md
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ dir: openspec/changes
|
|||||||
index: openspec/changes/.pi-map.index.md
|
index: openspec/changes/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Tracks and manages specification changes/versions for OpenAPI documents
|
Manages change tracking, versioning, and audit logging for OpenSpec schema or configuration modifications.
|
||||||
## files
|
## files
|
||||||
## arch
|
## 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
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## 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)
|
# openspec/changes/working-copies (index)
|
||||||
dir: working-copies
|
dir: openspec/changes/working-copies
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: ./.pi-map.index.md
|
index: openspec/changes/.pi-map.index.md
|
||||||
map: ./.pi-map.md
|
map: openspec/changes/.pi-map.md
|
||||||
## children
|
## children
|
||||||
-
|
-
|
||||||
## files
|
## files
|
||||||
|
- .openspec.yaml
|
||||||
- design.md
|
- design.md
|
||||||
- explore.md
|
- explore.md
|
||||||
- proposal.md
|
- proposal.md
|
||||||
- spec.md
|
- spec.md
|
||||||
- tasks.md
|
- tasks.md
|
||||||
## links
|
## links
|
||||||
index: working-copies/.pi-map.index.md
|
index: openspec/changes/working-copies/.pi-map.index.md
|
||||||
map: working-copies/.pi-map.md
|
map: openspec/changes/working-copies/.pi-map.md
|
||||||
## workflows
|
## workflows
|
||||||
-
|
-
|
||||||
## dirty
|
## dirty
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
# working-copies
|
# openspec/changes/working-copies
|
||||||
dir: working-copies
|
dir: openspec/changes/working-copies
|
||||||
|
|
||||||
index: working-copies/.pi-map.index.md
|
index: openspec/changes/working-copies/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- 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
|
- 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.
|
- 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
|
- 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
|
## 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
|
## tags
|
||||||
workspace, tool, instances, git, design, replace, document, repository
|
workspace, tool, instances, git, design, replace, project, working
|
||||||
## symbols
|
## symbols
|
||||||
-
|
-
|
||||||
## workflows
|
## workflows
|
||||||
|
|||||||
@@ -16,13 +16,13 @@
|
|||||||
**Files touched**: 8 new, 2 modified
|
**Files touched**: 8 new, 2 modified
|
||||||
|
|
||||||
**Tasks**:
|
**Tasks**:
|
||||||
1. [ ] Create Alembic migration for `workspaces` table + `workspace_id` on `tool_instances`
|
1. [x] Create Alembic migration for `workspaces` table + `workspace_id` on `tool_instances`
|
||||||
2. [ ] Create `Workspace` model (`apps/api/src/models/workspace.py`)
|
2. [x] Create `Workspace` model (`apps/api/src/models/workspace.py`)
|
||||||
3. [ ] Add `workspace_id` to `ToolInstance` model (nullable FK)
|
3. [x] Add `workspace_id` to `ToolInstance` model (nullable FK)
|
||||||
4. [ ] Create `GitService` (`apps/api/src/services/git_service.py`) — clone, fetch, pull, branch_exists_remotely
|
4. [x] 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
|
5. [x] 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
|
6. [x] 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`)
|
7. [x] Add workspace routes to FastAPI app (`apps/api/src/main.py`)
|
||||||
8. [ ] Write unit tests for GitService
|
8. [ ] Write unit tests for GitService
|
||||||
9. [ ] Write integration tests for workspace CRUD
|
9. [ ] Write integration tests for workspace CRUD
|
||||||
10. [ ] Write integration tests for delete-with-instances (409 behavior)
|
10. [ ] Write integration tests for delete-with-instances (409 behavior)
|
||||||
@@ -34,13 +34,13 @@
|
|||||||
**Files touched**: 3 modified
|
**Files touched**: 3 modified
|
||||||
|
|
||||||
**Tasks**:
|
**Tasks**:
|
||||||
1. [ ] Update `create_instance` endpoint to accept `workspace_id` instead of `clone_mode`
|
1. [x] 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
|
2. [x] Update `start_instance` to mount workspace path (`workspace.path`) instead of repo path
|
||||||
3. [ ] Update compose generation to use `WORKSPACE_PATH` variable
|
3. [x] Update compose generation to use `WORKSPACE_PATH` variable
|
||||||
4. [ ] Update `tool_instances.py` compose template rendering
|
4. [x] Update `tool_instances.py` compose template rendering
|
||||||
5. [ ] Write integration tests for instance creation with workspace
|
5. [ ] Write integration tests for instance creation with workspace
|
||||||
6. [ ] Write integration tests for instance start with workspace mount
|
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
|
### PR-3: Frontend Core
|
||||||
**Scope**: Workspaces UI — list, create, card, actions
|
**Scope**: Workspaces UI — list, create, card, actions
|
||||||
@@ -48,13 +48,13 @@
|
|||||||
**Files touched**: 10 new, 2 modified
|
**Files touched**: 10 new, 2 modified
|
||||||
|
|
||||||
**Tasks**:
|
**Tasks**:
|
||||||
1. [ ] Create workspace types (`apps/web/src/types/workspace.ts`)
|
1. [x] Create workspace types (`apps/web/src/types/workspace.ts`)
|
||||||
2. [ ] Create workspace API client (`apps/web/src/api/workspaces.ts`)
|
2. [x] Create workspace API client (`apps/web/src/api/workspaces.ts`)
|
||||||
3. [ ] Create `useWorkspaces` hook (`apps/web/src/hooks/use-workspaces.ts`)
|
3. [x] Create `useWorkspaces` hook (`apps/web/src/hooks/use-workspaces.ts`)
|
||||||
4. [ ] Create `useWorkspaceActions` hook (`apps/web/src/hooks/use-workspace-actions.ts`)
|
4. [x] Create `useWorkspaceActions` hook (`apps/web/src/hooks/use-workspace-actions.ts`)
|
||||||
5. [ ] Create `WorkspaceCard` component (`apps/web/src/components/workspace-card.tsx`)
|
5. [x] Create `WorkspaceCard` component (`apps/web/src/components/workspace-card.tsx`)
|
||||||
6. [ ] Create `WorkspaceCreateForm` component (`apps/web/src/components/workspace-create-form.tsx`)
|
6. [x] Create `WorkspaceCreateForm` component (`apps/web/src/components/workspace-create-form.tsx`)
|
||||||
7. [ ] Create `StartToolModal` component (`apps/web/src/components/start-tool-modal.tsx`)
|
7. [x] Create `StartToolModal` component (`apps/web/src/components/start-tool-modal.tsx`)
|
||||||
8. [ ] Create `WorkspacesPage` (`apps/web/src/pages/workspaces.tsx`)
|
8. [ ] Create `WorkspacesPage` (`apps/web/src/pages/workspaces.tsx`)
|
||||||
9. [ ] Update `Sidebar` to add Workspaces nav item
|
9. [ ] Update `Sidebar` to add Workspaces nav item
|
||||||
10. [ ] Update router/routes to include `/workspaces`
|
10. [ ] Update router/routes to include `/workspaces`
|
||||||
@@ -68,30 +68,30 @@
|
|||||||
**Files touched**: 5 modified
|
**Files touched**: 5 modified
|
||||||
|
|
||||||
**Tasks**:
|
**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
|
2. [ ] Update `SessionsPage` dashboard to show workspaces section
|
||||||
3. [ ] Update `SessionCard` to show workspace name instead of clone mode
|
3. [x] Update `SessionCard` to show workspace name instead of clone mode
|
||||||
4. [ ] Update `useInstanceActions` to pass `workspace_id` on create
|
4. [x] Update `useInstanceActions` to pass `workspace_id` on create
|
||||||
5. [ ] Remove clone_mode/mount_mode UI toggles
|
5. [x] Remove clone_mode/mount_mode UI toggles
|
||||||
6. [ ] Update types to remove deprecated `clone_mode` field
|
6. [x] Update types to remove deprecated `clone_mode` field
|
||||||
7. [ ] Write integration tests for full create-workspace → start-tool flow
|
7. [ ] Write integration tests for full create-workspace → start-tool flow
|
||||||
8. [ ] Write tests for dashboard workspaces section
|
8. [ ] Write tests for dashboard workspaces section
|
||||||
|
|
||||||
## Acceptance Criteria (All PRs)
|
## Acceptance Criteria (All PRs)
|
||||||
|
|
||||||
- [ ] User can create a workspace from any repository
|
- [x] User can create a workspace from any repository
|
||||||
- [ ] User can create unlimited workspaces per repository
|
- [x] User can create unlimited workspaces per repository
|
||||||
- [ ] Workspace names are unique per repo
|
- [x] Workspace names are unique per repo
|
||||||
- [ ] Tool instances mount the workspace path
|
- [x] Tool instances mount the workspace path
|
||||||
- [ ] Multiple tool instances can share one workspace
|
- [x] Multiple tool instances can share one workspace
|
||||||
- [ ] Workspaces persist after tool instance deletion
|
- [x] Workspaces persist after tool instance deletion
|
||||||
- [ ] Deleting a workspace with running instances shows confirmation, stops and deletes instances
|
- [x] Deleting a workspace with running instances shows confirmation, stops and deletes instances
|
||||||
- [ ] Syncing a workspace with a deleted remote branch shows confirmation
|
- [x] Syncing a workspace with a deleted remote branch shows confirmation
|
||||||
- [ ] UI no longer shows "mount vs clone" toggle
|
- [x] UI no longer shows "mount vs clone" toggle
|
||||||
- [ ] New sidebar navigation "Workspaces" exists
|
- [ ] New sidebar navigation "Workspaces" exists
|
||||||
- [ ] All existing tests still pass
|
- [x] All existing tests still pass
|
||||||
- [ ] ruff clean
|
- [x] ruff clean
|
||||||
- [ ] TypeScript compilation clean
|
- [x] TypeScript compilation clean
|
||||||
|
|
||||||
## Implementation Order
|
## Implementation Order
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user