feat: complete reorganize-long-files cleanup

- Extract tool instance lifecycle endpoints (start/stop/restart/delete) from
  api/tool/tool_instances.py into new api/tool/tool_lifecycle.py.
- Register tool_lifecycle_router in main.py and api/tool/__init__.py.
- Extract inline WorkspaceDetailPage components into
  components/features/workspace/: detail header, tab bars, file/git/tools/settings
  panels. Slim page from ~446 to ~62 lines.
- Update OpenSpec reorganize-long-files tasks to reflect completed work and
  current source state; mark change completed.
- Regenerate project maps.

Quality gates: python3 -m py_compile (backend clean), npm run typecheck,
npm run lint, npm test -- --run (87 passed), pytest workspace integration
and unit tests (27 passed, 1 skipped).
This commit is contained in:
Developer
2026-06-12 18:53:23 +00:00
parent efb62fe41a
commit ce8b5dc86d
45 changed files with 878 additions and 830 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ dir: .
Trust boundary: index routes, map orients, source decides.
## role
Infrastructure and deployment configuration package for a self-hosted project management platform with containerized services, reverse proxy support, and AI agent workflow definitions.
Infrastructure and deployment configuration package for a self-hosted project management platform with multi-service Docker orchestration, SSO integration, and development tooling.
## parent
-
## children
+2 -2
View File
@@ -18,7 +18,7 @@ index: ./.pi-map.index.md
Trust boundary: index routes, map orients, source decides.
## role
Infrastructure and deployment configuration package for a self-hosted project management platform with containerized services, reverse proxy support, and AI agent workflow definitions.
Infrastructure and deployment configuration package for a self-hosted project management platform with multi-service Docker orchestration, SSO integration, and development tooling.
## files
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
- .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git
@@ -31,7 +31,7 @@ Infrastructure and deployment configuration package for a self-hosted project ma
- progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases
- swap-pane | Empty file with no functionality
## arch
Docker Compose-based microservices architecture with PostgreSQL/Redis backend, separate API/frontend containers, Traefik reverse proxy integration, environment-driven configuration, and OpenSpec-driven AI agent orchestration.
Containerized microservices architecture using Docker Compose with separate PostgreSQL/Redis data layer, API backend, and web frontend services, fronted by Traefik reverse proxy with TLS termination, environment-driven configuration, and Makefile-based lifecycle management.
## tags
docker, redis, git, application, postgresql, compose, traefik, project
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps
## role
Contains the main deployable application modules that compose the complete system.
Contains the main application entry points and executable modules for the project.
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: apps
index: apps/.pi-map.index.md
## role
Contains the main deployable application modules that compose the complete system.
Contains the main application entry points and executable modules for the project.
## files
## arch
Modular application architecture with separate entry points for different application contexts or deployment targets.
Modular application structure with separate deployable units, likely following a microservices or monorepo pattern with distinct apps sharing common libraries.
## tags
-
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api
## role
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances.
Backend API service that provides a self-hosted FastAPI server for managing development projects, git repositories, and containerized tools via Docker.
## parent
index: apps/.pi-map.index.md
map: apps/.pi-map.md
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/api
index: apps/api/.pi-map.index.md
## role
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances.
Backend API service that provides a self-hosted FastAPI server for managing development projects, git repositories, and containerized tools via Docker.
## files
- .dockerignore | Specifies files and directories to exclude from Docker build context to reduce image size and avoid copying unnecessary files into containers. | dep: Docker
- Dockerfile | Multi-stage Docker build for a Python application with Docker socket access, Cloudflare tunneling, and database dependency waiting | dep: python:3.11-slim, gcc, libpq-dev, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, pyproject.toml dependencies
@@ -14,7 +14,7 @@ Self-hosted FastAPI backend API that manages projects, git repositories, and dev
- uv.lock | Lock file for the uv Python package manager that pins exact dependency versions and their artifact hashes for reproducible installations | dep: uv, Python 3.11+, aiosqlite, alembic, annotated-doc, annotated-types, anyio, ast-serialize, asyncpg, and many other PyPI packages
- wait-for-db.sh | Wait for a PostgreSQL database to become available before executing a command, with configurable retry logic. | dep: nc (netcat), sh (POSIX shell), sleep
## arch
Multi-stage Dockerized Python application using async FastAPI, PostgreSQL with Alembic migrations, uv package management, and Cloudflare tunneling with external Docker socket access.
Modern Python async architecture using FastAPI with SQLAlchemy/alembic for PostgreSQL, uv for dependency management, multi-stage Docker builds with Cloudflare tunneling, and health-check orchestration for database readiness.
## tags
docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn
## symbols
+5 -2
View File
@@ -2,7 +2,7 @@
dir: apps/api/src
## role
Core application package for the Headquarter API backend service, providing configuration, database connectivity, structured logging, and FastAPI application orchestration.
Core application package that bootstraps and configures the Headquarter API FastAPI service with its infrastructure concerns.
## parent
index: apps/api/.pi-map.index.md
map: apps/api/.pi-map.md
@@ -13,6 +13,9 @@ map: apps/api/.pi-map.md
- apps/api/src/auth
index: apps/api/src/auth/.pi-map.index.md
map: apps/api/src/auth/.pi-map.md
- apps/api/src/headquarter_api.egg-info
index: apps/api/src/headquarter_api.egg-info/.pi-map.index.md
map: apps/api/src/headquarter_api.egg-info/.pi-map.md
- apps/api/src/models
index: apps/api/src/models/.pi-map.index.md
map: apps/api/src/models/.pi-map.md
@@ -46,6 +49,6 @@ map: apps/api/src/.pi-map.md
- change src config
read: config.py, logging_config.py
- explore src subdirectories
index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/models/.pi-map.index.md
index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/headquarter_api.egg-info/.pi-map.index.md
## dirty
-
+4 -4
View File
@@ -4,15 +4,15 @@ dir: apps/api/src
index: apps/api/src/.pi-map.index.md
## role
Core application package for the Headquarter API backend service, providing configuration, database connectivity, structured logging, and FastAPI application orchestration.
Core application package that bootstraps and configures the Headquarter API FastAPI service with its infrastructure concerns.
## files
- __init__.py | Marks the directory as a Python package for the Headquarter API.
- config.py | Defines application configuration settings using Pydantic with environment variable loading, database URL construction, and computed properties for service URLs and OAuth endpoints. | exp: class:Settings, func:build_database_url(user: str, password: str, host: str, port: int, database: str) → str | dep: pydantic, pydantic_settings
- database.py | Configures async SQLAlchemy database engine and session factory, and provides retry logic for database initialization with Alembic migrations via subprocess. | exp: func:init_database(max_retries, retry_delay) → bool, call:range, call:engine.connect, call:test_conn.execute, call:text, call:test_conn.close, call:logger.info, call:asyncio.get_event_loop().run_in_executor, call:subprocess.run, call:logger.debug, call:logger.error, call:asyncio.sleep, call:str(exc).lower, call:logger.warning | dep: asyncio, logging, subprocess, sqlalchemy.ext.asyncio, sqlalchemy.pool, src.config, sqlalchemy
- logging_config.py | Configures structured JSON logging with correlation ID injection, custom formatters, and HTTP request/exception middleware for a FastAPI application. | exp: class:CorrelationIdFilter, method:filter(self, record: logging.LogRecord) → bool, call:get_correlation_id, class:JSONFormatter, method:format(self, record: logging.LogRecord) → str, call:self.formatTime, call:record.getMessage, call:getattr, call:self.formatException, call:json.dumps, method:formatTime(self, record: logging.LogRecord, datefmt) → str, call:time.strftime, call:time.gmtime, class:RequestLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:time.time, call:logger.info, call:call_next, call:int, call:logger.error, call:type, call:traceback.format_exc, class:ExceptionLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:call_next, call:logger.critical, call:traceback.format_exc, func:configure_logging(level) → None, call:JSONFormatter, call:logging.StreamHandler, call:console_handler.setFormatter, call:console_handler.addFilter, call:CorrelationIdFilter, call:root_logger.setLevel, call:logging.getLogger("uvicorn").setLevel, call:logging.getLogger("uvicorn.access").setLevel, call:logging.getLogger("sqlalchemy.engine").setLevel, call:logger.info, call:logging.getLevelName | dep: json, logging, sys, time, traceback, collections.abc, fastapi, starlette.middleware.base, src.services.shared.correlation
- main.py | FastAPI application entry point that initializes and configures the Headquarter API with routers, middleware, database, health monitoring, and CORS | exp: func:_sanitize_validation_errors(errors), call:error.get, call:str, call:ctx.items, call:isinstance, call:type, call:sanitized.append, func:validation_exception_handler(request: Request, exc: RequestValidationError), call:exc.errors, call:logger.warning, call:_sanitize_validation_errors, call:JSONResponse, func:on_startup(), call:logger.info, call:init_database, call:logger.error, call:sys.exit, call:_health_monitor.start, call:seed_builtin_tool_types, func:on_shutdown(), call:logger.info, call:_health_monitor.stop | dep: logging, os, fastapi, fastapi.exceptions, fastapi.middleware.cors, fastapi.responses, fastapi.staticfiles, src.api.config, src.api.project, src.api.system, src.api.tool, src.api.user, src.api.workspace, src.config, src.models, src.database, src.logging_config, src.seeds.builtin_tool_types, src.services.instance, src.services.shared, sys, src.api.*
- main.py | Initializes and configures a FastAPI application for the "Headquarter API" with database setup, middleware, routing, and background services. | exp: func:_sanitize_validation_errors(errors), call:error.get, call:str, call:ctx.items, call:isinstance, call:type, call:sanitized.append, func:validation_exception_handler(request: Request, exc: RequestValidationError), call:exc.errors, call:logger.warning, call:_sanitize_validation_errors, call:JSONResponse, func:on_startup(), call:logger.info, call:init_database, call:logger.error, call:sys.exit, call:_health_monitor.start, call:seed_builtin_tool_types, func:on_shutdown(), call:logger.info, call:_health_monitor.stop | dep: logging, os, fastapi, fastapi.exceptions, fastapi.middleware.cors, fastapi.responses, fastapi.staticfiles, src.api.config, src.api.project, src.api.system, src.api.tool, src.api.user, src.api.workspace, src.config, src.models, src.database, src.logging_config, src.seeds.builtin_tool_types, src.services.instance, src.services.shared, sys, src.api.*
## arch
Layered architecture with Pydantic-based configuration management, async SQLAlchemy with Alembic migrations, structured JSON logging with correlation ID tracing, and modular FastAPI setup with middleware pipeline and health monitoring.
Layered configuration with Pydantic settings, async SQLAlchemy with Alembic migration integration, structured JSON logging with correlation ID tracking, and FastAPI middleware/routing setup.
## tags
src, database, logging, call:logger.info, api, middleware, fastapi, filter
## symbols
@@ -30,6 +30,6 @@ src, database, logging, call:logger.info, api, middleware, fastapi, filter
- change src config
read: config.py, logging_config.py
- explore src subdirectories
index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/models/.pi-map.index.md
index: apps/api/src/api/.pi-map.index.md, apps/api/src/auth/.pi-map.index.md, apps/api/src/headquarter_api.egg-info/.pi-map.index.md
## dirty
-
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/src/api
## role
Defines reusable Pydantic validators for API request/response schema validation across the API layer.
Defines reusable API validation utilities and package structure for FastAPI router organization.
## parent
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
+2 -2
View File
@@ -4,12 +4,12 @@ dir: apps/api/src/api
index: apps/api/src/api/.pi-map.index.md
## role
Defines reusable Pydantic validators for API request/response schema validation across the API layer.
Defines reusable API validation utilities and package structure for FastAPI router organization.
## files
- __init__.py | Marks the directory as a Python package for API routers.
- shared_validators.py | Provides reusable Pydantic validator functions for API schema validation including mount paths, files, environment variables, and volume mounts. | exp: func:validate_mount_path(v: str | None) → str | None, call:v.startswith, raise:ValueError, func:validate_files(v: dict | None, max_size_bytes) → dict | None, call:v.items, call:path.startswith, call:len, call:content.encode, raise:ValueError, func:validate_env_vars(v: dict | None) → dict | None, call:isinstance, raise:ValueError, func:validate_volumes(v: list | None) → list | None, call:isinstance, call:enumerate, raise:ValueError
## arch
Utility module pattern providing shared, composable validation functions using Pydantic's validator system for common container/docker-like domain objects (mounts, volumes, files, env vars).
Modular utility package with shared Pydantic validators for cross-cutting API schema concerns, following separation of validation logic from route handlers.
## tags
validate, raise:value, error, call:isinstance, mount, api, init, path
## symbols
+2 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/src/api/tool
## role
Provides FastAPI REST endpoints for managing the complete lifecycle of containerized tools, including type definitions, instances, sessions, and validation.
Provides FastAPI REST API endpoints and routers for managing the complete lifecycle of Docker-based tools, including type definitions, instances, sessions, and lifecycle operations.
## parent
index: apps/api/src/api/.pi-map.index.md
map: apps/api/src/api/.pi-map.md
@@ -13,6 +13,7 @@ map: apps/api/src/api/.pi-map.md
- sessions.py
- tool_definitions.py
- tool_instances.py
- tool_lifecycle.py
- tool_types.py
- tool_types_validation.py
## links
+6 -5
View File
@@ -4,16 +4,17 @@ dir: apps/api/src/api/tool
index: apps/api/src/api/tool/.pi-map.index.md
## role
Provides FastAPI REST endpoints for managing the complete lifecycle of containerized tools, including type definitions, instances, sessions, and validation.
Provides FastAPI REST API endpoints and routers for managing the complete lifecycle of Docker-based tools, including type definitions, instances, sessions, and lifecycle operations.
## files
- __init__.py | Aggregates and exports tool-related API routers from submodules for centralized access | dep: src.api.tool.sessions, src.api.tool.tool_definitions, src.api.tool.tool_instances, src.api.tool.tool_types
- sessions.py | Provides a FastAPI endpoint to retrieve all active tool sessions (running instances) for the currently authenticated user with related metadata. | exp: func:get_user_sessions(user_id, session) → dict, call:_get_user, call:session.execute, call:select(ToolInstance) .where(ToolInstance.owner_id == user_id) .where( ToolInstance.status.in_( ["running", "building", "pending", "stopped", "error"] ) ) .order_by, call:ToolInstance.status.in_, call:ToolInstance.created_at.desc, call:result.scalars().all, call:session.get, call:sessions.append, call:str, call:instance.created_at.isoformat | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models
- __init__.py | Aggregates and exports all tool-related API routers from submodules as a unified module interface. | dep: src.api.tool.sessions, src.api.tool.tool_definitions, src.api.tool.tool_instances, src.api.tool.tool_lifecycle, src.api.tool.tool_types
- sessions.py | API endpoint that retrieves all active tool instances (sessions) for the currently authenticated user with related metadata | exp: func:get_user_sessions(user_id, session) → dict, call:_get_user, call:session.execute, call:select(ToolInstance) .where(ToolInstance.owner_id == user_id) .where( ToolInstance.status.in_( ["running", "building", "pending", "stopped", "error"] ) ) .order_by, call:ToolInstance.status.in_, call:ToolInstance.created_at.desc, call:result.scalars().all, call:session.get, call:sessions.append, call:str, call:instance.created_at.isoformat | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models
- tool_definitions.py | FastAPI router providing CRUD endpoints and manifest compilation for tool definition manifests stored in a database. | exp: class:CreateToolDefinitionRequest, class:UpdateToolDefinitionRequest, func:create_tool_definition(data: CreateToolDefinitionRequest, user_id, session) → dict, call:uuid.UUID, call:session.get, call:session.execute, call:select(ToolDefinitionManifest).where, call:existing.scalar_one_or_none, call:ToolDefinitionManifest, call:session.add, call:session.commit, call:session.refresh, call:logger.info, call:str, call:definition.created_at.isoformat, raise:HTTPException, func:list_tool_definitions(user_id, session, include_bases) → dict, call:select, call:query.where, call:ToolDefinitionManifest.is_base.is_, call:session.execute, call:query.order_by, call:ToolDefinitionManifest.created_at.desc, call:result.scalars().all, call:str, call:d.created_at.isoformat, func:get_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:str, call:definition.created_at.isoformat, call:definition.updated_at.isoformat, raise:HTTPException, func:update_tool_definition(definition_id: uuid.UUID, data: UpdateToolDefinitionRequest, user_id, session) → dict, call:session.get, call:session.commit, call:session.refresh, call:logger.info, call:str, call:definition.updated_at.isoformat, raise:HTTPException, func:delete_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:session.execute, call:select(ToolType).where, call:result.scalars().all, call:", ".join, call:session.delete, call:session.commit, call:logger.info, call:str, raise:HTTPException, func:compile_tool_definition(definition_id: uuid.UUID, user_id, session) → dict, call:session.get, call:dict, call:resolve_base, call:deep_merge, call:compile_dockerfile, call:compile_entrypoint, call:compute_image_tag, call:compile_compose, call:session.commit, call:str, raise:HTTPException | dep: logging, uuid, fastapi, pydantic, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.build.manifest_compiler
- tool_instances.py | Defines FastAPI REST endpoints for managing Docker-based tool instances within projects, including CRUD operations, lifecycle control (start/stop/restart), logs, health checks, and tunnel management. | exp: func:create_instance(project_id: uuid.UUID, repo_id: uuid.UUID, data: CreateInstanceRequest, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:create_tool_instance, call:str, call:instance.created_at.isoformat, raise:HTTPException, func:list_instances(project_id: uuid.UUID, repo_id: uuid.UUID, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.execute, call:select(ToolInstance) .where(ToolInstance.repository_id == repo_id) .where(ToolInstance.owner_id == user_id) .order_by, call:ToolInstance.created_at.desc, call:result.scalars().all, call:str, call:i.created_at.isoformat, func:get_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:str, call:instance.created_at.isoformat, call:instance.last_started_at.isoformat, raise:HTTPException, func:rename_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:data.get("display_name", "").strip, call:rename_tool_instance, call:str, raise:HTTPException, func:start_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:start_tool_instance, raise:HTTPException, func:stop_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:stop_tool_instance, raise:HTTPException, func:restart_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:restart_tool_instance, raise:HTTPException, func:delete_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, force, user_id, session) → None, call:_get_user, call:_get_owned_project, call:delete_tool_instance, call:str, call:detail.lower, raise:HTTPException, func:get_instance_logs(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, tail, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_logs, raise:HTTPException, func:recreate_tunnel_endpoint(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:recreate_instance_tunnel, raise:HTTPException, func:check_instance_tunnel_health(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:instance.probe_result.get, call:"\n".join, call:check_tunnel_health, call:tunnel_health.get, raise:HTTPException, func:get_instance_events(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, limit, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.get, call:session.execute, call:select(InstanceEvent) .where(InstanceEvent.instance_id == instance_id) .order_by(InstanceEvent.created_at.desc()) .limit, call:InstanceEvent.created_at.desc, call:result.scalars().all, call:str, call:row.created_at.isoformat, raise:HTTPException, func:proxy_to_instance(request: Request, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, path, user_id, session) → Response, call:session.get, call:str, call:dict, call:headers.pop, call:httpx.AsyncClient, call:request.body, call:client.request, call:logger.error, call:response_headers.pop, call:Response, raise:HTTPException | dep: logging, uuid, httpx, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.docker, src.services.shared.tunnel, src.schemas.tool, src.services.tool.instance_service
- tool_instances.py | Provides FastAPI REST endpoints for managing tool instances including CRUD operations, logs, health checks, tunnel recreation, event history, and HTTP proxying to running containers. | exp: func:create_instance(project_id: uuid.UUID, repo_id: uuid.UUID, data: CreateInstanceRequest, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:create_tool_instance, call:str, call:instance.created_at.isoformat, raise:HTTPException, func:list_instances(project_id: uuid.UUID, repo_id: uuid.UUID, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.execute, call:select(ToolInstance) .where(ToolInstance.repository_id == repo_id) .where(ToolInstance.owner_id == user_id) .order_by, call:ToolInstance.created_at.desc, call:result.scalars().all, call:str, call:i.created_at.isoformat, func:get_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:str, call:instance.created_at.isoformat, call:instance.last_started_at.isoformat, raise:HTTPException, func:rename_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:data.get("display_name", "").strip, call:rename_tool_instance, call:str, raise:HTTPException, func:get_instance_logs(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, tail, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_logs, raise:HTTPException, func:recreate_tunnel_endpoint(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:recreate_instance_tunnel, raise:HTTPException, func:check_instance_tunnel_health(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:session.get, call:get_container_status, call:instance.probe_result.get, call:"\n".join, call:check_tunnel_health, call:tunnel_health.get, raise:HTTPException, func:get_instance_events(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, limit, user_id, session) → list[dict], call:_get_user, call:_get_owned_project, call:session.get, call:session.execute, call:select(InstanceEvent) .where(InstanceEvent.instance_id == instance_id) .order_by(InstanceEvent.created_at.desc()) .limit, call:InstanceEvent.created_at.desc, call:result.scalars().all, call:str, call:row.created_at.isoformat, raise:HTTPException, func:proxy_to_instance(request: Request, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, path, user_id, session) → Response, call:session.get, call:str, call:dict, call:headers.pop, call:httpx.AsyncClient, call:request.body, call:client.request, call:logger.error, call:response_headers.pop, call:Response, raise:HTTPException | dep: logging, uuid, httpx, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.docker, src.services.shared.tunnel, src.schemas.tool, src.services.tool.instance_service
- tool_lifecycle.py | FastAPI router providing REST endpoints for managing Docker-based tool instance lifecycle operations (start, stop, restart, delete). | exp: func:start_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:start_tool_instance, raise:HTTPException, func:stop_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:stop_tool_instance, raise:HTTPException, func:restart_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:_get_owned_project, call:restart_tool_instance, raise:HTTPException, func:delete_instance(project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, force, user_id, session) → None, call:_get_user, call:_get_owned_project, call:delete_tool_instance, call:str, call:detail.lower, raise:HTTPException | dep: logging, uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.schemas.tool, src.services.tool.instance_service
- tool_types.py | Defines FastAPI routes for CRUD operations and validation of Docker tool types with compose/dockerfile/manifest definitions. | exp: func:_require_admin(user: User) → None, func:create_tool_type(data: ToolTypeCreate, user_id, session) → ToolType, call:_get_user, call:_require_admin, call:session.scalar, call:select(ToolType).where, call:ToolType, call:session.add, call:session.commit, call:session.refresh, raise:HTTPException, func:list_tool_types(user_id, session) → list[ToolType], call:_get_user, call:session.execute, call:select(ToolType).order_by, call:list, call:result.scalars().all, func:get_tool_type(tool_type_id: uuid.UUID, user_id, session) → ToolType, call:_get_user, call:session.get, raise:HTTPException, func:update_tool_type(tool_type_id: uuid.UUID, data: ToolTypeUpdate, user_id, session) → ToolType, call:_get_user, call:_require_admin, call:session.get, call:data.model_dump, call:update_data.get, call:validate_compose_yaml, call:check_port_exposed, call:validate_required_variables, call:update_data.items, call:setattr, call:session.commit, call:session.refresh, raise:HTTPException, func:validate_tool_type_template(data: ToolTypeValidateRequest, user_id, session) → dict, call:_get_user, call:errors.append, call:validate_compose_yaml, call:str, call:data.dockerfile_template.strip().startswith, call:len, func:validate_tool_type(tool_type_id: uuid.UUID, user_id, session) → dict, call:_get_user, call:session.get, call:errors.append, call:validate_compose_yaml, call:str, call:tool_type.dockerfile_template.strip().startswith, call:len, raise:HTTPException, func:delete_tool_type(tool_type_id: uuid.UUID, user_id, session) → None, call:_get_user, call:_require_admin, call:session.get, call:session.delete, call:session.commit, raise:HTTPException | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.api.tool.tool_types_validation, src.auth.dependencies, src.models, src.models.user, src.schemas.tool
- tool_types_validation.py | Validates Docker Compose YAML templates by sanitizing template variables, parsing YAML, checking required structure, verifying port exposure, and ensuring required variables are present. | exp: func:sanitize_template_vars(template: str) → str, call:re.sub, func:validate_compose_yaml(template: str) → dict, call:sanitize_template_vars, call:yaml.safe_load, call:isinstance, raise:ValueError, func:check_port_exposed(parsed: dict, port: int) → bool, call:str, call:isinstance, call:parsed["services"].values, func:validate_required_variables(template: str, variables: list[str]) → None, raise:HTTPException | dep: re, yaml, fastapi, fastapi.HTTPException, fastapi.status
## arch
Modular router composition with domain-driven separation (types/instances/sessions/definitions), Docker-centric architecture using compose/dockerfile manifests, and validation-layer pattern for YAML sanitization and structural verification.
Modular FastAPI router composition pattern with separated concerns across CRUD operations, validation, lifecycle management, and proxying, aggregated through `__init__.py` for unified module interface.
## tags
get, call:, tool, raise:httpexception, user, instance, call:str, call:session.get
## symbols
+2
View File
@@ -3,11 +3,13 @@
from src.api.tool.sessions import sessions_router
from src.api.tool.tool_definitions import router as tool_definitions_router
from src.api.tool.tool_instances import router as tool_instances_router
from src.api.tool.tool_lifecycle import router as tool_lifecycle_router
from src.api.tool.tool_types import router as tool_types_router
__all__ = [
"sessions_router",
"tool_definitions_router",
"tool_instances_router",
"tool_lifecycle_router",
"tool_types_router",
]
+1 -115
View File
@@ -24,15 +24,11 @@ from src.auth.dependencies import (
from src.models import ToolInstance
from src.services.docker import get_container_logs, get_container_status
from src.services.shared.tunnel import check_tunnel_health
from src.schemas.tool import CreateInstanceRequest, StartInstanceRequest
from src.schemas.tool import CreateInstanceRequest
from src.services.tool.instance_service import (
create_tool_instance,
delete_tool_instance,
recreate_instance_tunnel,
rename_tool_instance,
restart_tool_instance,
start_tool_instance,
stop_tool_instance,
)
logger = logging.getLogger(__name__)
@@ -190,116 +186,6 @@ async def rename_instance(
}
@router.post(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/start",
summary="Start instance",
description="Start a tool instance using Docker Compose.",
)
async def start_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
data: StartInstanceRequest | None = None,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
return await start_tool_instance(
session, user_id, project_id, repo_id, instance_id, data
)
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)
)
@router.post(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/stop",
summary="Stop instance",
description="Stop a running tool instance.",
)
async def stop_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
return await stop_tool_instance(session, user_id, project_id, repo_id, instance_id)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/restart",
summary="Restart instance",
description="Restart a tool instance.",
)
async def restart_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
return await restart_tool_instance(
session, user_id, project_id, repo_id, instance_id
)
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)
)
@router.delete(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}",
summary="Delete instance",
description="Delete a tool instance and remove its Docker containers and files.",
)
async def delete_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
force: bool = False,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> None:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
await delete_tool_instance(
session, user_id, project_id, repo_id, instance_id, force
)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
except RuntimeError as exc:
detail = str(exc)
if "uncommitted changes" in detail.lower():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={
"message": "Repository has uncommitted changes",
"changed_files": detail,
"force_required": True,
},
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=detail
)
@router.get(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/logs",
summary="Get instance logs",
+135
View File
@@ -0,0 +1,135 @@
"""Tool instance lifecycle API endpoints."""
import logging
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import (
_get_owned_project,
_get_user,
get_current_user_id,
get_db_session,
)
from src.schemas.tool import StartInstanceRequest
from src.services.tool.instance_service import (
delete_tool_instance,
restart_tool_instance,
start_tool_instance,
stop_tool_instance,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/projects", tags=["tool-instances"])
@router.post(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/start",
summary="Start instance",
description="Start a tool instance using Docker Compose.",
)
async def start_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
data: StartInstanceRequest | None = None,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
return await start_tool_instance(
session, user_id, project_id, repo_id, instance_id, data
)
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)
)
@router.post(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/stop",
summary="Stop instance",
description="Stop a running tool instance.",
)
async def stop_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
return await stop_tool_instance(session, user_id, project_id, repo_id, instance_id)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/restart",
summary="Restart instance",
description="Restart a tool instance.",
)
async def restart_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
return await restart_tool_instance(
session, user_id, project_id, repo_id, instance_id
)
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)
)
@router.delete(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}",
summary="Delete instance",
description="Delete a tool instance and remove its Docker containers and files.",
)
async def delete_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
force: bool = False,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> None:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
try:
await delete_tool_instance(
session, user_id, project_id, repo_id, instance_id, force
)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
except RuntimeError as exc:
detail = str(exc)
if "uncommitted changes" in detail.lower():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={
"message": "Repository has uncommitted changes",
"changed_files": detail,
"force_required": True,
},
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=detail
)
+2
View File
@@ -21,6 +21,7 @@ from src.api.tool import (
sessions_router,
tool_definitions_router,
tool_instances_router,
tool_lifecycle_router,
tool_types_router,
)
from src.api.user import auth_router, ssh_keys_router, users_router
@@ -165,6 +166,7 @@ app.include_router(tool_types_router)
app.include_router(tool_definitions_router)
app.include_router(config_profiles_router)
app.include_router(tool_instances_router)
app.include_router(tool_lifecycle_router)
app.include_router(sessions_router)
app.include_router(instance_proxy_router)
app.include_router(terminal_router)
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web
## role
Frontend web application for a React-based developer workspace with code editing, terminal, and routing capabilities.
Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
## parent
index: apps/.pi-map.index.md
map: apps/.pi-map.md
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/web
index: apps/web/.pi-map.index.md
## role
Frontend web application for a React-based developer workspace with code editing, terminal, and routing capabilities.
Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
## files
- .env.example | Template file defining example environment variables for frontend API and application URL configuration
- .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ECMAScript module support | dep: @typescript-eslint/parser, @typescript-eslint/eslint-plugin, eslint
@@ -16,7 +16,7 @@ Frontend web application for a React-based developer workspace with code editing
- tsconfig.json | TypeScript configuration file for a React project using Vite with modern ES2020 target and bundler module resolution | dep: typescript, react, vite
- vite.config.ts | Configures Vite build tool for a React project with custom dev server port and Vitest test settings. | dep: vite, @vitejs/plugin-react
## arch
Modern React SPA built with Vite/TypeScript, served by nginx in a Docker container, using ES modules with Vitest for testing and ESLint for code quality.
Modern React SPA built with Vite and TypeScript, containerized via multi-stage Docker with nginx serving, featuring standard tooling (ESLint, Vitest) and client-side routing.
## tags
react, eslint, vite, typescript, dom, application, nginx, web
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src
## role
Frontend web application entry point and core infrastructure for a React-based project management or development platform.
Provides the core web application entry point, routing infrastructure, and shared domain type definitions for a React-based frontend.
## parent
index: apps/web/.pi-map.index.md
map: apps/web/.pi-map.md
+2 -2
View File
@@ -4,13 +4,13 @@ dir: apps/web/src
index: apps/web/src/.pi-map.index.md
## role
Frontend web application entry point and core infrastructure for a React-based project management or development platform.
Provides the core web application entry point, routing infrastructure, and shared domain type definitions for a React-based frontend.
## files
- main.tsx | Bootstraps a React application with routing, authentication, and session management 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/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css, react-dom
- router.tsx | Defines the React Router configuration for a web application with protected routes, nested layouts, and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage
- types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
## arch
Layered React SPA architecture with provider-based dependency injection, declarative routing with nested layouts and route guards, and centralized TypeScript type definitions for domain entities.
Modular React SPA using React Router v6 with nested route layouts, protected route guards via authentication context providers, and centralized TypeScript type definitions for cross-cutting domain models.
## tags
pages, styles, css, router, react, session, dom, project
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src/components
## role
Provides reusable, foundational React UI components for layout, authentication, data display, icons, code editing, and notifications across the web application.
Provides shared, reusable React UI components for the web application including layout shell, navigation, authentication guards, data display states, icons, code editing, syntax highlighting, and toast notifications.
## parent
index: apps/web/src/.pi-map.index.md
map: apps/web/src/.pi-map.md
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/web/src/components
index: apps/web/src/components/.pi-map.index.md
## role
Provides reusable, foundational React UI components for layout, authentication, data display, icons, code editing, and notifications across the web application.
Provides shared, reusable React UI components for the web application including layout shell, navigation, authentication guards, data display states, icons, code editing, syntax highlighting, and toast notifications.
## files
- app-shell.tsx | Renders the main application shell layout with navigation, session management, and responsive mobile/desktop views for a React Router-based app. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ../state/session-operations, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./features/session/session-progress-panel, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
- code-editor.tsx | A React component that renders a syntax-highlighted code editor with line numbers using react-simple-code-editor. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language
@@ -16,7 +16,7 @@ Provides reusable, foundational React UI components for layout, authentication,
- toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events
- toast-rules.ts | Maps instance events to toast notifications with deduplication logic to prevent spam | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast state module, InstanceEventPayload type
## arch
Modular component architecture with separation of concerns—each component handles a single responsibility, with dedicated test files alongside implementation, and higher-level components (app-shell, protected-route) composing lower-level primitives (icon, data-states, toast-rules).
Component-based architecture with clear separation of concerns: layout/navigation (app-shell), authentication (protected-route), data presentation (data-states, syntax-highlighter, code-editor), utilities (icon, toast-rules), and co-located test files following a feature-organized structure with semantic naming conventions.
## tags
toast, state, react, icon, code, event, editor, protected
## symbols
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features
## role
Contains reusable React components that implement specific user-facing functionality and business logic features across the web application.
Contains reusable UI components organized by feature domains for the web application.
## parent
index: apps/web/src/components/.pi-map.index.md
map: apps/web/src/components/.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
index: apps/web/src/components/features/.pi-map.index.md
## role
Contains reusable React components that implement specific user-facing functionality and business logic features across the web application.
Contains reusable UI components organized by feature domains for the web application.
## files
## arch
Feature-based component organization with domain-specific grouping, likely using composition patterns and co-located feature logic (hooks, utils, sub-components) following a modular frontend architecture.
Feature-based colocation pattern where components are grouped by business domain rather than technical type, enabling scalable modular development with clear separation of concerns.
## tags
-
## symbols
@@ -4,114 +4,114 @@ import { MemoryRouter } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
afterEach(() => {
cleanup();
cleanup();
});
import { ProjectCard } from "./ProjectCard";
import type { ProjectWithRepos } from "../../../types";
const mockProject: ProjectWithRepos = {
id: "proj-1",
name: "Alpha Project",
description: "First project",
owner_id: "user-1",
default_ssh_key_id: null,
created_at: "2026-06-01T00:00:00Z",
repositories: [
{
id: "repo-1",
name: "my-repo",
remote_url: "https://example.com/repo.git",
workspaces: [
{
id: "ws-1",
name: "dev",
branch: "main",
status: "ready",
instance_count: 2,
},
],
},
],
id: "proj-1",
name: "Alpha Project",
description: "First project",
owner_id: "user-1",
default_ssh_key_id: null,
created_at: "2026-06-01T00:00:00Z",
repositories: [
{
id: "repo-1",
name: "my-repo",
remote_url: "https://example.com/repo.git",
workspaces: [
{
id: "ws-1",
name: "dev",
branch: "main",
status: "ready",
instance_count: 2,
},
],
},
],
};
describe("ProjectCard", () => {
it("renders project name and repository", () => {
render(
<MemoryRouter>
<ProjectCard
project={mockProject}
expanded={true}
deleteConfirm={false}
workspaceLoading={null}
showCreateForm={null}
onToggle={vi.fn()}
onEdit={vi.fn()}
onDelete={vi.fn()}
onConfirmDelete={vi.fn()}
onCancelDelete={vi.fn()}
onCreateWorkspace={vi.fn()}
onWorkspaceAction={vi.fn()}
onCancelCreate={vi.fn()}
onCreated={vi.fn()}
/>
</MemoryRouter>
);
it("renders project name and repository", () => {
render(
<MemoryRouter>
<ProjectCard
project={mockProject}
expanded={true}
deleteConfirm={false}
workspaceLoading={null}
showCreateForm={null}
onToggle={vi.fn()}
onEdit={vi.fn()}
onDelete={vi.fn()}
onConfirmDelete={vi.fn()}
onCancelDelete={vi.fn()}
onCreateWorkspace={vi.fn()}
onWorkspaceAction={vi.fn()}
onCancelCreate={vi.fn()}
onCreated={vi.fn()}
/>
</MemoryRouter>,
);
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
expect(screen.getByText("my-repo")).toBeInTheDocument();
expect(screen.getByText("dev")).toBeInTheDocument();
});
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
expect(screen.getByText("my-repo")).toBeInTheDocument();
expect(screen.getByText("dev")).toBeInTheDocument();
});
it("calls onCreateWorkspace when new workspace button is clicked", () => {
const onCreateWorkspace = vi.fn();
render(
<MemoryRouter>
<ProjectCard
project={mockProject}
expanded={true}
deleteConfirm={false}
workspaceLoading={null}
showCreateForm={null}
onToggle={vi.fn()}
onEdit={vi.fn()}
onDelete={vi.fn()}
onConfirmDelete={vi.fn()}
onCancelDelete={vi.fn()}
onCreateWorkspace={onCreateWorkspace}
onWorkspaceAction={vi.fn()}
onCancelCreate={vi.fn()}
onCreated={vi.fn()}
/>
</MemoryRouter>
);
it("calls onCreateWorkspace when new workspace button is clicked", () => {
const onCreateWorkspace = vi.fn();
render(
<MemoryRouter>
<ProjectCard
project={mockProject}
expanded={true}
deleteConfirm={false}
workspaceLoading={null}
showCreateForm={null}
onToggle={vi.fn()}
onEdit={vi.fn()}
onDelete={vi.fn()}
onConfirmDelete={vi.fn()}
onCancelDelete={vi.fn()}
onCreateWorkspace={onCreateWorkspace}
onWorkspaceAction={vi.fn()}
onCancelCreate={vi.fn()}
onCreated={vi.fn()}
/>
</MemoryRouter>,
);
fireEvent.click(screen.getByRole("button", { name: /new workspace/i }));
expect(onCreateWorkspace).toHaveBeenCalledWith("repo-1");
});
fireEvent.click(screen.getByRole("button", { name: /new workspace/i }));
expect(onCreateWorkspace).toHaveBeenCalledWith("repo-1");
});
it("shows delete confirmation", () => {
render(
<MemoryRouter>
<ProjectCard
project={mockProject}
expanded={true}
deleteConfirm={true}
workspaceLoading={null}
showCreateForm={null}
onToggle={vi.fn()}
onEdit={vi.fn()}
onDelete={vi.fn()}
onConfirmDelete={vi.fn()}
onCancelDelete={vi.fn()}
onCreateWorkspace={vi.fn()}
onWorkspaceAction={vi.fn()}
onCancelCreate={vi.fn()}
onCreated={vi.fn()}
/>
</MemoryRouter>
);
it("shows delete confirmation", () => {
render(
<MemoryRouter>
<ProjectCard
project={mockProject}
expanded={true}
deleteConfirm={true}
workspaceLoading={null}
showCreateForm={null}
onToggle={vi.fn()}
onEdit={vi.fn()}
onDelete={vi.fn()}
onConfirmDelete={vi.fn()}
onCancelDelete={vi.fn()}
onCreateWorkspace={vi.fn()}
onWorkspaceAction={vi.fn()}
onCancelCreate={vi.fn()}
onCreated={vi.fn()}
/>
</MemoryRouter>,
);
expect(screen.getByText(/are you sure/i)).toBeInTheDocument();
});
expect(screen.getByText(/are you sure/i)).toBeInTheDocument();
});
});
@@ -2,24 +2,28 @@
dir: apps/web/src/components/features/workspace
## role
Provides the workspace UI shell and container components that orchestrate the repository browsing, editing, and tool execution environment for the web application.
Provides UI components for workspace management, navigation, and detail views in the web application.
## parent
index: apps/web/src/components/features/.pi-map.index.md
map: apps/web/src/components/features/.pi-map.md
## children
-
## files
- FileBrowser.tsx
- WorkspaceLayout.tsx
- workspace-card.tsx
- workspace-create-form.tsx
- workspace-detail-header.tsx
- workspace-file-panel.tsx
- workspace-git-panel.tsx
- workspace-header.tsx
- workspace-instance-chips.tsx
- workspace-settings-panel.tsx
- workspace-tab-bar.tsx
- workspace-tools-panel.tsx
## links
index: apps/web/src/components/features/workspace/.pi-map.index.md
map: apps/web/src/components/features/workspace/.pi-map.md
## workflows
- change workspace behavior
read: FileBrowser.tsx, WorkspaceLayout.tsx, workspace-card.tsx
read: workspace-card.tsx, workspace-create-form.tsx, workspace-detail-header.tsx
## dirty
-
@@ -4,29 +4,33 @@ dir: apps/web/src/components/features/workspace
index: apps/web/src/components/features/workspace/.pi-map.index.md
## role
Provides the workspace UI shell and container components that orchestrate the repository browsing, editing, and tool execution environment for the web application.
Provides UI components for workspace management, navigation, and detail views in the web application.
## files
- FileBrowser.tsx | A React component that displays a browsable file tree for a git repository with directory navigation, file status indicators, and URL-based state management. | exp: FileBrowser | dep: react, react-router-dom, ../../../api/client, ../../data-states, ../../icon, ../../../api/git-repositories, apiClient, EmptyState, Icon
- WorkspaceLayout.tsx | Renders a responsive workspace layout that switches between mobile tab-based navigation and desktop sidebar/main panel layout for repository file browsing, editing, git operations, and terminal tools. | exp: WorkspaceLayout | dep: ../../icon, ./FileBrowser, ../git/file-editor, ../git/commit-panel, ../git/git-toolbar, ../tool/instance-list, ../../../api/git-repositories, ../../../api/tool-types, ../../../hooks/use-repo-workspace, Icon, FileBrowser, FileEditor, CommitPanel, GitToolbar, InstanceList, GitRepository, GitStatus, ToolType, Project
- workspace-card.tsx | React card component that displays workspace information with status, metadata, and action buttons | exp: WorkspaceCardProps, func:WorkspaceCard({ workspace, loading = false, onStartTool, onSync, onDelete, }: WorkspaceCardProps), call:onStartTool, call:onSync, call:onDelete | dep: react-router-dom, ../../icon, ./workspace-instance-chips, ../../../types/workspace
- workspace-create-form.tsx | React form component for creating workspaces with cascading project/repository/branch selectors and support for both contextual and standalone modes | exp: WorkspaceCreateFormProps, func:WorkspaceCreateForm({ onSubmit, onCancel, defaultProjectId, defaultRepoId, }: WorkspaceCreateFormProps), call:Boolean, call:useState, call:useGitRepo, call:useEffect, call:git.branches.includes, call:setSelectedBranch, call:setIsNewBranch, call:useCallback, call:listProjects, call:setProjects, call:setSelectedProject, call:setError, call:setFetchingProjects, call:loadProjects, call:setRepos, call:setSelectedRepo, call:listRepositories, call:loadRepos, call:setNewBranchName, call:e.preventDefault, call:name.trim, call:newBranchName.trim, call:setSubmitting, call:createWorkspaceTopLevel, call:onSubmit, call:projects.map, call:repos.map, call:setName, call:handleBranchChange, call:git.branches.map | dep: react, ../../icon, ../../../api/projects, ../../../api/git-repositories, ../../../api/workspaces, ../../../hooks/use-git-repo, ../../../types, icon, api/projects, api/git-repositories, api/workspaces, hooks/use-git-repo, types
- workspace-detail-header.tsx | Renders a header component for a workspace detail page displaying breadcrumb navigation and branch information. | exp: WorkspaceDetailHeaderProps, func:WorkspaceDetailHeader({ workspace }: WorkspaceDetailHeaderProps) | dep: ../../icon, icon
- workspace-file-panel.tsx | Renders a file browser and editor panel with Git integration for a workspace detail page. | exp: func:WorkspaceFilePanel({ workspaceId }: WorkspaceFilePanelProps), call:useWorkspaceFiles, call:useWorkspaceGit, call:useState, call:setSelectedPath, call:setIsEditing, call:setEditContent, call:navigateTo, call:loadFile, call:currentPath.split("/").slice(0, -1).join, call:saveFile, call:setCommitMessage, call:commit, call:entries.map, call:handleSelect | dep: react, ../../icon, ../../../hooks/use-workspace-files, ../../../hooks/use-workspace-git, ../../../api/workspace-files, icon, use-workspace-files, use-workspace-git, workspace-files API types
- workspace-git-panel.tsx | Renders a Git panel for a workspace detail page that displays branch selection and commit history. | exp: func:WorkspaceGitPanel({ workspaceId }: WorkspaceGitPanelProps), call:useWorkspaceGit, call:checkout, call:branches.map, call:history.map, call:commit.hash.slice | dep: ../../../hooks/use-workspace-git, React, useWorkspaceGit hook
- workspace-header.tsx | Renders a workspace header component displaying project info with navigation links to history and settings pages. | exp: WorkspaceHeader | dep: react-router-dom, ../../icon, icon
- workspace-instance-chips.tsx | Displays running tool instances for a workspace as clickable status chips with external links. | exp: func:WorkspaceInstanceChips({ workspaceId, }: WorkspaceInstanceChipsProps), call:useState, call:useEffect, call:listWorkspaceInstances, call:setInstances, call:setLoading, call:load, call:instances.map, call:e.stopPropagation | dep: react, ../../../api/workspace-instances, ../../../api/sessions
- workspace-settings-panel.tsx | Displays a read-only settings panel showing workspace metadata on a workspace detail page. | exp: func:WorkspaceSettingsPanel({ workspace }: WorkspaceSettingsPanelProps) | dep: ../../../types/workspace, React, types/workspace
- workspace-tab-bar.tsx | Renders a tab bar component for workspace navigation with desktop and mobile variants. | exp: WorkspaceTab, func:WorkspaceTabBar({ active, onChange }: WorkspaceTabBarProps), call:TABS.map, call:onChange, func:WorkspaceMobileTabBar({ active, onChange }: WorkspaceTabBarProps), call:TABS.map, call:onChange | dep: ../../icon, icon
- workspace-tools-panel.tsx | Displays and manages running tool instances for a workspace with ability to start new tools via modal | exp: func:WorkspaceToolsPanel({ workspace }: WorkspaceToolsPanelProps), call:useWorkspaceInstances, call:useState, call:setShowModal, call:instances.map, call:e.stopPropagation, call:refresh | dep: react, ../../icon, ../tool/tool-starter, ../../../hooks/use-workspace-instances, ../../../types/workspace, icon, tool-starter, use-workspace-instances
## arch
Compound component architecture with responsive layout switching (mobile/desktop), URL-driven state management for file navigation, and feature-based composition combining card/list views, forms, headers, and dynamic tool instance indicators.
Feature-based component organization with page-specific composite components (header/panel/tab-bar) and atomic display components, following a compound component pattern for workspace detail page layout.
## tags
workspace, call:set, git, api, call:use, branch, projects, react
workspace, call:set, call:use, git, panel, icon, branch, call:on
## symbols
- WorkspaceCard
- WorkspaceCreateForm
- WorkspaceDetailHeader
- WorkspaceFilePanel
- WorkspaceGitPanel
- WorkspaceInstanceChips
- FileBrowser
- WorkspaceLayout
- WorkspaceCardProps
- call:onStartTool
- call:onSync
- WorkspaceSettingsPanel
- WorkspaceTabBar
## workflows
- change workspace behavior
read: FileBrowser.tsx, WorkspaceLayout.tsx, workspace-card.tsx
read: workspace-card.tsx, workspace-create-form.tsx, workspace-detail-header.tsx
## dirty
-
@@ -0,0 +1,31 @@
/** Header for the workspace detail page. */
import { Icon } from "../../icon";
export interface WorkspaceDetailHeaderProps {
workspace: {
name: string;
repo_name: string;
project_name: string;
branch: string;
};
}
export function WorkspaceDetailHeader({ workspace }: WorkspaceDetailHeaderProps) {
return (
<header className="workspace-header">
<div className="workspace-breadcrumb">
<span>{workspace.project_name}</span>
<span className="sep">/</span>
<span>{workspace.repo_name}</span>
<span className="sep">/</span>
<strong>{workspace.name}</strong>
</div>
<div className="workspace-actions">
<span className="branch-badge">
<Icon name="branch" size="sm" /> {workspace.branch}
</span>
</div>
</header>
);
}
@@ -0,0 +1,181 @@
/** Files tab for the workspace detail page. */
import { useState } from "react";
import { Icon } from "../../icon";
import { useWorkspaceFiles } from "../../../hooks/use-workspace-files";
import { useWorkspaceGit } from "../../../hooks/use-workspace-git";
import type { FileEntry } from "../../../api/workspace-files";
interface WorkspaceFilePanelProps {
workspaceId: string;
}
export function WorkspaceFilePanel({ workspaceId }: WorkspaceFilePanelProps) {
const {
entries,
content,
currentPath,
loadFile,
saveFile,
loading,
error,
navigateTo,
} = useWorkspaceFiles(workspaceId);
const { status, commit, push, pull, fetch } = useWorkspaceGit(workspaceId);
const [selectedPath, setSelectedPath] = useState<string | null>(null);
const [editContent, setEditContent] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [commitMessage, setCommitMessage] = useState("");
const handleSelect = (entry: FileEntry) => {
if (entry.type === "directory") {
setSelectedPath(null);
setIsEditing(false);
setEditContent(null);
navigateTo(entry.path);
return;
}
setSelectedPath(entry.path);
setIsEditing(false);
setEditContent(null);
loadFile(entry.path);
};
const navigateUp = () => {
if (!currentPath) return;
const parentPath = currentPath.split("/").slice(0, -1).join("/");
navigateTo(parentPath);
setSelectedPath(null);
setIsEditing(false);
setEditContent(null);
};
const handleEdit = () => {
if (content !== null) {
setEditContent(content);
setIsEditing(true);
}
};
const handleSave = async () => {
if (selectedPath && editContent !== null) {
await saveFile(selectedPath, editContent, commitMessage || undefined);
setIsEditing(false);
setCommitMessage("");
}
};
return (
<div className="files-tab">
{status && (
<div className="git-toolbar">
<div className="git-toolbar-status">
{status.modified.length > 0 && (
<span className="status-modified">
M {status.modified.length}
</span>
)}
{status.added.length > 0 && (
<span className="status-added">A {status.added.length}</span>
)}
{status.deleted.length > 0 && (
<span className="status-deleted">D {status.deleted.length}</span>
)}
{status.untracked.length > 0 && (
<span className="status-untracked">
? {status.untracked.length}
</span>
)}
</div>
<div className="git-toolbar-actions">
<input
type="text"
value={commitMessage}
onChange={(e) => setCommitMessage(e.target.value)}
placeholder="Commit message"
/>
<button
onClick={() => commit(commitMessage)}
disabled={!commitMessage}
type="button"
>
Commit
</button>
<button onClick={push} type="button">Push</button>
<button onClick={pull} type="button">Pull</button>
<button onClick={fetch} type="button">Fetch</button>
</div>
</div>
)}
<div className="files-split">
<div className="file-tree">
{currentPath && (
<button
className="tree-entry tree-up"
onClick={navigateUp}
type="button"
>
<Icon name="folder" size="sm" /> ..
</button>
)}
{loading && <p className="muted">Loading...</p>}
{error && <p className="error-text">{error}</p>}
{entries.map((entry) => (
<button
key={entry.path}
className={`tree-entry ${entry.type} ${selectedPath === entry.path ? "selected" : ""}`}
onClick={() => handleSelect(entry)}
type="button"
>
<Icon
name={entry.type === "directory" ? "folder" : "file"}
size="sm"
/>
{entry.name}
</button>
))}
</div>
<div className="file-viewer">
{selectedPath ? (
<>
<div className="file-viewer-header">
<span>{selectedPath}</span>
{!isEditing && (
<button onClick={handleEdit} type="button">
Edit
</button>
)}
</div>
{isEditing ? (
<>
<textarea
className="file-editor"
value={editContent || ""}
onChange={(e) => setEditContent(e.target.value)}
/>
<div className="file-editor-actions">
<button
onClick={() => setIsEditing(false)}
type="button"
>
Cancel
</button>
<button onClick={handleSave} type="button">
Save
</button>
</div>
</>
) : (
<pre className="file-content">
{content || "Loading..."}
</pre>
)}
</>
) : (
<p className="muted">Select a file to view</p>
)}
</div>
</div>
</div>
);
}
@@ -0,0 +1,41 @@
/** Git tab for the workspace detail page. */
import { useWorkspaceGit } from "../../../hooks/use-workspace-git";
interface WorkspaceGitPanelProps {
workspaceId: string;
}
export function WorkspaceGitPanel({ workspaceId }: WorkspaceGitPanelProps) {
const { history, branches, currentBranch, checkout, loading, error } =
useWorkspaceGit(workspaceId);
return (
<div className="git-tab">
<div className="git-tab-header">
<select
value={currentBranch}
onChange={(e) => checkout(e.target.value)}
>
{branches.map((b) => (
<option key={b} value={b}>
{b}
</option>
))}
</select>
</div>
{loading && <p className="muted">Loading history...</p>}
{error && <p className="error-text">{error}</p>}
<div className="commit-history">
{history.map((commit) => (
<div key={commit.hash} className="commit-row">
<span className="commit-hash">{commit.hash.slice(0, 7)}</span>
<span className="commit-message">{commit.message}</span>
<span className="commit-author">{commit.author}</span>
<span className="commit-date">{commit.date}</span>
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,39 @@
/** Settings tab for the workspace detail page. */
import type { Workspace } from "../../../types/workspace";
interface WorkspaceSettingsPanelProps {
workspace: Workspace;
}
export function WorkspaceSettingsPanel({ workspace }: WorkspaceSettingsPanelProps) {
return (
<div className="settings-tab">
<div className="settings-section">
<h3>Workspace Info</h3>
<div className="form-group">
<label>Name</label>
<input type="text" value={workspace.name} readOnly />
</div>
<div className="form-group">
<label>Branch</label>
<input type="text" value={workspace.branch} readOnly />
</div>
<div className="form-group">
<label>Path</label>
<input type="text" value={workspace.path} readOnly />
</div>
<div className="form-group">
<label>Status</label>
<span className={`status-badge ${workspace.status}`}>
{workspace.status}
</span>
</div>
<div className="form-group">
<label>Created</label>
<span>{workspace.created_at}</span>
</div>
</div>
</div>
);
}
@@ -0,0 +1,68 @@
/** Tab bar for the workspace detail page. */
import { Icon } from "../../icon";
export type WorkspaceTab = "files" | "git" | "tools" | "settings";
interface Tab {
id: WorkspaceTab;
label: string;
icon: string;
}
const TABS: Tab[] = [
{ id: "files", label: "Files", icon: "folder" },
{ id: "git", label: "Git", icon: "branch" },
{ id: "tools", label: "Tools", icon: "terminal" },
{ id: "settings", label: "Settings", icon: "settings" },
];
interface WorkspaceTabBarProps {
active: WorkspaceTab;
onChange: (tab: WorkspaceTab) => void;
}
export function WorkspaceTabBar({ active, onChange }: WorkspaceTabBarProps) {
return (
<nav className="tab-bar" role="tablist">
{TABS.map((tab) => (
<button
key={tab.id}
className={`tab ${active === tab.id ? "active" : ""}`}
onClick={() => onChange(tab.id)}
role="tab"
aria-selected={active === tab.id}
type="button"
>
<Icon
name={tab.icon as "folder" | "branch" | "terminal" | "settings"}
size="sm"
/>
{tab.label}
</button>
))}
</nav>
);
}
export function WorkspaceMobileTabBar({ active, onChange }: WorkspaceTabBarProps) {
return (
<nav className="mobile-tab-bar" role="tablist">
{TABS.map((tab) => (
<button
key={tab.id}
className={`mobile-tab ${active === tab.id ? "active" : ""}`}
onClick={() => onChange(tab.id)}
role="tab"
aria-selected={active === tab.id}
type="button"
>
<Icon
name={tab.icon as "folder" | "branch" | "terminal" | "settings"}
/>
<span>{tab.label}</span>
</button>
))}
</nav>
);
}
@@ -0,0 +1,81 @@
/** Tools tab for the workspace detail page. */
import { useState } from "react";
import { Icon } from "../../icon";
import { ToolStarter } from "../tool/tool-starter";
import { useWorkspaceInstances } from "../../../hooks/use-workspace-instances";
import type { Workspace } from "../../../types/workspace";
interface WorkspaceToolsPanelProps {
workspace: Workspace;
}
export function WorkspaceToolsPanel({ workspace }: WorkspaceToolsPanelProps) {
const { instances, loading, refresh } = useWorkspaceInstances(workspace.id);
const [showModal, setShowModal] = useState(false);
return (
<div className="tools-tab">
{loading && <p className="muted">Loading instances...</p>}
{instances.length === 0 ? (
<div className="empty-state-card">
<Icon name="terminal" size="lg" />
<h3>No tools running</h3>
<p>Start a tool to begin coding in this workspace</p>
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
type="button"
>
Start Tool
</button>
</div>
) : (
<>
<div className="instances-grid">
{instances.map((instance) => (
<div
key={instance.id}
className={`instance-card ${instance.status}`}
>
<h4>{instance.display_name}</h4>
<span className="status-badge">{instance.status}</span>
{instance.url && (
<a
href={instance.url}
target={`instance-${instance.id}`}
rel="noreferrer"
>
Open
</a>
)}
</div>
))}
</div>
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
type="button"
>
Start Another Tool
</button>
</>
)}
{showModal && (
<div className="modal-overlay" onClick={() => setShowModal(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h3>Start Tool</h3>
<ToolStarter
workspace={workspace}
onStarted={() => {
setShowModal(false);
void refresh();
}}
onCancel={() => setShowModal(false)}
/>
</div>
</div>
)}
</div>
);
}
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src/pages
## role
Contains all top-level page components that serve as route endpoints for the web application's main navigation areas, each handling a distinct functional domain of the development environment platform.
Contains top-level React page components that serve as route endpoints for the web application's primary feature areas, each handling a distinct domain of the workspace management platform.
## parent
index: apps/web/src/.pi-map.index.md
map: apps/web/src/.pi-map.md
+6 -6
View File
@@ -4,10 +4,10 @@ dir: apps/web/src/pages
index: apps/web/src/pages/.pi-map.index.md
## role
Contains all top-level page components that serve as route endpoints for the web application's main navigation areas, each handling a distinct functional domain of the development environment platform.
Contains top-level React page components that serve as route endpoints for the web application's primary feature areas, each handling a distinct domain of the workspace management platform.
## files
- ConfigProfilesPage.tsx | Renders a responsive configuration profiles management page with sidebar list and editor panel for desktop, and a dedicated mobile view for creating, editing, and managing config profiles. | exp: ConfigProfilesPage | dep: react, ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-config-profiles, ../components/features/config-profiles/ConfigProfileListSidebar, ../components/features/config-profiles/ConfigProfileEditorPanel, ../components/features/config-profiles/ConfigProfilesMobileView
- DashboardPage.test.tsx | Unit tests for the DashboardPage/ HomePage component verifying overview loading and error retry behavior. | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./DashboardPage, ../state/sessions, ../state/session-operations, DashboardPage, SessionsProvider, SessionOperationsProvider
- DashboardPage.test.tsx | Tests the DashboardPage (HomePage) component's rendering, loading states, and error handling with retry functionality | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./DashboardPage, ../state/sessions, ../state/session-operations, ../api/dashboard, ../api/sessions, ../api/projects, ../api/git-repositories, ../api/tool-types
- DashboardPage.tsx | Renders a dashboard homepage that displays workspace overview, active/recent sessions, summary statistics, and polling health checks for running instances. | exp: HomePage | dep: react, react-router-dom, ../api/dashboard, ../api/sessions, ../components/data-states, ../components/features/session/session-list, ../hooks/use-instance-actions, ../state/sessions
- GitHistoryPage.tsx | Renders a Git commit history page with branch selection, commit list with graph visualization, and a detail panel showing commit metadata, stats, and diffs. | exp: GitHistoryPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../hooks/use-async-data
- GitRepositoriesPage.tsx | Displays and manages a project's Git repositories with CRUD operations including listing, creating, navigating to history, and deleting with confirmation | exp: GitRepositoriesPage | dep: react, react-router-dom, ../api/git-repositories, ../components/data-states, ../components/icon, ../components/features/project/repository-create-dialog, ../hooks/use-async-data
@@ -21,13 +21,13 @@ Contains all top-level page components that serve as route endpoints for the web
- SshKeysPage.tsx | React page component for managing SSH keys including generation, listing, signing, verification, and deletion | exp: SSHKeysPage | dep: react-router-dom, ../components/data-states, ../hooks/use-ssh-keys, ../components/features/ssh-keys/SSHKeyCreateForm, ../components/features/ssh-keys/SSHKeyList
- TerminalPage.tsx | Renders a responsive terminal page that switches between mobile and desktop views based on device type, managing terminal sessions and their interactions. | exp: TerminalPage | dep: react, ../hooks/use-terminal-page, ../components/features/terminal/MobileTerminalView, ../components/features/terminal/DesktopTerminalView, useTerminalPage hook, MobileTerminalView, DesktopTerminalView
- ToolWorkshopPage.tsx | Renders a responsive tool workshop page with sidebar/editor layout for desktop and tabbed mobile view for managing tool types | exp: ToolWorkshopPage | dep: ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-tool-workshop, ../components/features/tool-workshop/ToolTypeListSidebar, ../components/features/tool-workshop/ToolTypeEditorPanel, ../components/features/tool-workshop/ToolWorkshopMobileView, react, use-mobile-viewport, use-tool-workshop, data-states, ToolTypeListSidebar, ToolTypeEditorPanel, ToolWorkshopMobileView
- WorkspaceDetailPage.test.tsx | Tests the WorkspaceDetailPage component rendering workspace headers, tabs, and tab switching behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./WorkspaceDetailPage, @testing-library/jest-dom, WorkspaceDetailPage, use-workspaces, use-workspace-files, use-workspace-git, use-workspace-instances, use-mobile-viewport
- WorkspaceDetailPage.tsx | Renders a workspace detail page with tabbed navigation for files, git, tools, and settings management. | exp: func:WorkspaceDetailPage(), call:useParams, call:useState, call:useMobileViewport, call:useWorkspaces, call:workspaces.find | dep: react, react-router-dom, ../components/icon, ../hooks/use-workspaces, ../hooks/use-workspace-files, ../hooks/use-workspace-git, ../hooks/use-workspace-instances, ../hooks/use-mobile-viewport, ../components/features/tool/tool-starter, ../api/workspace-files, ../types/workspace
- WorkspaceDetailPage.test.tsx | Tests the WorkspaceDetailPage component rendering and tab switching behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./WorkspaceDetailPage, @testing-library/jest-dom, WorkspaceDetailPage, use-workspaces, use-workspace-files, use-workspace-git, use-workspace-instances, use-mobile-viewport
- WorkspaceDetailPage.tsx | Renders a workspace detail page with tabbed navigation for files, git, tools, and settings panels, adapting layout for mobile viewports. | exp: func:WorkspaceDetailPage(), call:useParams, call:useState, call:useMobileViewport, call:useWorkspaces, call:workspaces.find | dep: react, react-router-dom, ../hooks/use-workspaces, ../hooks/use-mobile-viewport, ../components/features/workspace/workspace-detail-header, ../components/features/workspace/workspace-tab-bar, ../components/features/workspace/workspace-file-panel, ../components/features/workspace/workspace-git-panel, ../components/features/workspace/workspace-tools-panel, ../components/features/workspace/workspace-settings-panel, use-workspaces, use-mobile-viewport, workspace-detail-header, workspace-tab-bar, workspace-mobile-tab-bar, workspace-file-panel, workspace-git-panel, workspace-tools-panel, workspace-settings-panel
- WorkspacesPage.tsx | Renders a responsive workspaces management page with separate mobile (list/detail/create views) and desktop (grid with cards) layouts, supporting CRUD operations and tool launching. | exp: func:WorkspacesPage(), call:useMobileViewport, call:useState, call:useWorkspaces, call:useWorkspaceActions, call:actions.delete, call:actions.sync, call:setMobileView, call:refresh, call:setStartWorkspace, call:handleDelete, call:setSelectedWorkspace, call:workspaces.map, call:workspaces.find, call:e.stopPropagation, call:setShowCreate | dep: react, ../components/icon, ../hooks/use-mobile-viewport, ../hooks/use-workspaces, ../hooks/use-workspace-actions, ../components/features/workspace/workspace-card, ../components/features/workspace/workspace-create-form, ../components/features/mobile/mobile-list-view, ../components/features/mobile/mobile-detail-view, ../components/features/mobile/mobile-fab, ../components/features/tool/tool-starter, ../types/workspace
## arch
Follows a page-based routing architecture with responsive dual-layout pattern (mobile/desktop variants), heavy use of tabbed navigation for complex pages, polling-based live data updates, and CRUD-heavy pages with optimistic UI patterns and confirmation dialogs.
Flat page-based architecture with co-located tests, where each page is a self-contained route component implementing responsive mobile/desktop adaptive layouts, tabbed navigation, CRUD operations, and real-time data polling; pages compose shared UI patterns (sidebar/editor splits, card grids, detail panels) and delegate to child components or outlets for nested routing.
## tags
page, components, react, workspace, features, mobile, hooks, settings
page, components, workspace, features, mobile, react, settings, hooks
## symbols
- WorkspaceDetailPage
- WorkspacesPage
@@ -1,5 +1,11 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { MemoryRouter, Routes, Route } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
@@ -117,7 +123,9 @@ describe("WorkspaceDetailPage", () => {
);
await waitFor(() => {
expect(screen.getByRole("tab", { name: /settings/i })).toBeInTheDocument();
expect(
screen.getByRole("tab", { name: /settings/i }),
).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("tab", { name: /settings/i }));
+27 -411
View File
@@ -2,21 +2,22 @@
import { useState } from "react";
import { useParams } from "react-router-dom";
import { Icon } from "../components/icon";
import { useWorkspaces } from "../hooks/use-workspaces";
import { useWorkspaceFiles } from "../hooks/use-workspace-files";
import { useWorkspaceGit } from "../hooks/use-workspace-git";
import { useWorkspaceInstances } from "../hooks/use-workspace-instances";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
import { ToolStarter } from "../components/features/tool/tool-starter";
import type { FileEntry } from "../api/workspace-files";
import type { Workspace } from "../types/workspace";
type Tab = "files" | "git" | "tools" | "settings";
import { WorkspaceDetailHeader } from "../components/features/workspace/workspace-detail-header";
import {
WorkspaceMobileTabBar,
WorkspaceTabBar,
type WorkspaceTab,
} from "../components/features/workspace/workspace-tab-bar";
import { WorkspaceFilePanel } from "../components/features/workspace/workspace-file-panel";
import { WorkspaceGitPanel } from "../components/features/workspace/workspace-git-panel";
import { WorkspaceToolsPanel } from "../components/features/workspace/workspace-tools-panel";
import { WorkspaceSettingsPanel } from "../components/features/workspace/workspace-settings-panel";
export function WorkspaceDetailPage() {
const { workspaceId } = useParams<{ workspaceId: string }>();
const [activeTab, setActiveTab] = useState<Tab>("files");
const [activeTab, setActiveTab] = useState<WorkspaceTab>("files");
const isMobile = useMobileViewport();
const { workspaces, loading: wsLoading } = useWorkspaces();
@@ -37,410 +38,25 @@ export function WorkspaceDetailPage() {
return (
<div className={`workspace-detail ${isMobile ? "mobile" : ""}`}>
<WorkspaceHeader workspace={workspace} />
<TabBar active={activeTab} onChange={setActiveTab} />
<WorkspaceDetailHeader workspace={workspace} />
<WorkspaceTabBar active={activeTab} onChange={setActiveTab} />
<div className="workspace-content">
{activeTab === "files" && <FilesTab workspaceId={workspace.id} />}
{activeTab === "git" && <GitTab workspaceId={workspace.id} />}
{activeTab === "tools" && <ToolsTab workspace={workspace} />}
{activeTab === "settings" && <SettingsTab workspace={workspace} />}
{activeTab === "files" && (
<WorkspaceFilePanel workspaceId={workspace.id} />
)}
{activeTab === "git" && (
<WorkspaceGitPanel workspaceId={workspace.id} />
)}
{activeTab === "tools" && (
<WorkspaceToolsPanel workspace={workspace} />
)}
{activeTab === "settings" && (
<WorkspaceSettingsPanel workspace={workspace} />
)}
</div>
{isMobile && <MobileTabBar active={activeTab} onChange={setActiveTab} />}
</div>
);
}
function WorkspaceHeader({
workspace,
}: {
workspace: {
name: string;
repo_name: string;
project_name: string;
branch: string;
};
}) {
return (
<header className="workspace-header">
<div className="workspace-breadcrumb">
<span>{workspace.project_name}</span>
<span className="sep">/</span>
<span>{workspace.repo_name}</span>
<span className="sep">/</span>
<strong>{workspace.name}</strong>
</div>
<div className="workspace-actions">
<span className="branch-badge">
<Icon name="branch" size="sm" /> {workspace.branch}
</span>
</div>
</header>
);
}
function TabBar({
active,
onChange,
}: {
active: Tab;
onChange: (t: Tab) => void;
}) {
const tabs: { id: Tab; label: string; icon: string }[] = [
{ id: "files", label: "Files", icon: "folder" },
{ id: "git", label: "Git", icon: "branch" },
{ id: "tools", label: "Tools", icon: "terminal" },
{ id: "settings", label: "Settings", icon: "settings" },
];
return (
<nav className="tab-bar" role="tablist">
{tabs.map((tab) => (
<button
key={tab.id}
className={`tab ${active === tab.id ? "active" : ""}`}
onClick={() => onChange(tab.id)}
role="tab"
aria-selected={active === tab.id}
>
<Icon
name={tab.icon as "folder" | "branch" | "terminal" | "settings"}
size="sm"
/>
{tab.label}
</button>
))}
</nav>
);
}
function MobileTabBar({
active,
onChange,
}: {
active: Tab;
onChange: (t: Tab) => void;
}) {
const tabs: { id: Tab; label: string; icon: string }[] = [
{ id: "files", label: "Files", icon: "folder" },
{ id: "git", label: "Git", icon: "branch" },
{ id: "tools", label: "Tools", icon: "terminal" },
{ id: "settings", label: "Settings", icon: "settings" },
];
return (
<nav className="mobile-tab-bar" role="tablist">
{tabs.map((tab) => (
<button
key={tab.id}
className={`mobile-tab ${active === tab.id ? "active" : ""}`}
onClick={() => onChange(tab.id)}
role="tab"
aria-selected={active === tab.id}
>
<Icon
name={tab.icon as "folder" | "branch" | "terminal" | "settings"}
/>
<span>{tab.label}</span>
</button>
))}
</nav>
);
}
/* ─── Files Tab ─── */
function FilesTab({ workspaceId }: { workspaceId: string }) {
const {
entries,
content,
currentPath,
loadFile,
saveFile,
loading,
error,
navigateTo,
} = useWorkspaceFiles(workspaceId);
const { status, commit, push, pull, fetch } = useWorkspaceGit(workspaceId);
const [selectedPath, setSelectedPath] = useState<string | null>(null);
const [editContent, setEditContent] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [commitMessage, setCommitMessage] = useState("");
const handleSelect = (entry: FileEntry) => {
if (entry.type === "directory") {
setSelectedPath(null);
setIsEditing(false);
setEditContent(null);
navigateTo(entry.path);
return;
}
setSelectedPath(entry.path);
setIsEditing(false);
setEditContent(null);
loadFile(entry.path);
};
const navigateUp = () => {
if (!currentPath) return;
const parentPath = currentPath.split("/").slice(0, -1).join("/");
navigateTo(parentPath);
setSelectedPath(null);
setIsEditing(false);
setEditContent(null);
};
const handleEdit = () => {
if (content !== null) {
setEditContent(content);
setIsEditing(true);
}
};
const handleSave = async () => {
if (selectedPath && editContent !== null) {
await saveFile(selectedPath, editContent, commitMessage || undefined);
setIsEditing(false);
setCommitMessage("");
}
};
return (
<div className="files-tab">
{status && (
<div className="git-toolbar">
<div className="git-toolbar-status">
{status.modified.length > 0 && (
<span className="status-modified">
M {status.modified.length}
</span>
)}
{status.added.length > 0 && (
<span className="status-added">A {status.added.length}</span>
)}
{status.deleted.length > 0 && (
<span className="status-deleted">D {status.deleted.length}</span>
)}
{status.untracked.length > 0 && (
<span className="status-untracked">
? {status.untracked.length}
</span>
)}
</div>
<div className="git-toolbar-actions">
<input
type="text"
value={commitMessage}
onChange={(e) => setCommitMessage(e.target.value)}
placeholder="Commit message"
/>
<button
onClick={() => commit(commitMessage)}
disabled={!commitMessage}
>
Commit
</button>
<button onClick={push}>Push</button>
<button onClick={pull}>Pull</button>
<button onClick={fetch}>Fetch</button>
</div>
</div>
)}
<div className="files-split">
<div className="file-tree">
{currentPath && (
<button
className="tree-entry tree-up"
onClick={navigateUp}
type="button"
>
<Icon name="folder" size="sm" /> ..
</button>
)}
{loading && <p className="muted">Loading...</p>}
{error && <p className="error-text">{error}</p>}
{entries.map((entry) => (
<button
key={entry.path}
className={`tree-entry ${entry.type} ${selectedPath === entry.path ? "selected" : ""}`}
onClick={() => handleSelect(entry)}
type="button"
>
<Icon
name={entry.type === "directory" ? "folder" : "file"}
size="sm"
/>
{entry.name}
</button>
))}
</div>
<div className="file-viewer">
{selectedPath ? (
<>
<div className="file-viewer-header">
<span>{selectedPath}</span>
{!isEditing && <button onClick={handleEdit}>Edit</button>}
</div>
{isEditing ? (
<>
<textarea
className="file-editor"
value={editContent || ""}
onChange={(e) => setEditContent(e.target.value)}
/>
<div className="file-editor-actions">
<button onClick={() => setIsEditing(false)}>Cancel</button>
<button onClick={handleSave}>Save</button>
</div>
</>
) : (
<pre className="file-content">{content || "Loading..."}</pre>
)}
</>
) : (
<p className="muted">Select a file to view</p>
)}
</div>
</div>
</div>
);
}
/* ─── Git Tab ─── */
function GitTab({ workspaceId }: { workspaceId: string }) {
const { history, branches, currentBranch, checkout, loading, error } =
useWorkspaceGit(workspaceId);
return (
<div className="git-tab">
<div className="git-tab-header">
<select
value={currentBranch}
onChange={(e) => checkout(e.target.value)}
>
{branches.map((b) => (
<option key={b} value={b}>
{b}
</option>
))}
</select>
</div>
{loading && <p className="muted">Loading history...</p>}
{error && <p className="error-text">{error}</p>}
<div className="commit-history">
{history.map((commit) => (
<div key={commit.hash} className="commit-row">
<span className="commit-hash">{commit.hash.slice(0, 7)}</span>
<span className="commit-message">{commit.message}</span>
<span className="commit-author">{commit.author}</span>
<span className="commit-date">{commit.date}</span>
</div>
))}
</div>
</div>
);
}
/* ─── Tools Tab ─── */
function ToolsTab({ workspace }: { workspace: Workspace }) {
const { instances, loading, refresh } = useWorkspaceInstances(workspace.id);
const [showModal, setShowModal] = useState(false);
return (
<div className="tools-tab">
{loading && <p className="muted">Loading instances...</p>}
{instances.length === 0 ? (
<div className="empty-state-card">
<Icon name="terminal" size="lg" />
<h3>No tools running</h3>
<p>Start a tool to begin coding in this workspace</p>
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
>
Start Tool
</button>
</div>
) : (
<>
<div className="instances-grid">
{instances.map((instance) => (
<div
key={instance.id}
className={`instance-card ${instance.status}`}
>
<h4>{instance.display_name}</h4>
<span className="status-badge">{instance.status}</span>
{instance.url && (
<a
href={instance.url}
target={`instance-${instance.id}`}
rel="noreferrer"
>
Open
</a>
)}
</div>
))}
</div>
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
>
Start Another Tool
</button>
</>
)}
{showModal && (
<div className="modal-overlay" onClick={() => setShowModal(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h3>Start Tool</h3>
<ToolStarter
workspace={workspace}
onStarted={() => {
setShowModal(false);
void refresh();
}}
onCancel={() => setShowModal(false)}
/>
</div>
</div>
{isMobile && (
<WorkspaceMobileTabBar active={activeTab} onChange={setActiveTab} />
)}
</div>
);
}
/* ─── Settings Tab ─── */
function SettingsTab({ workspace }: { workspace: Workspace }) {
return (
<div className="settings-tab">
<div className="settings-section">
<h3>Workspace Info</h3>
<div className="form-group">
<label>Name</label>
<input type="text" value={workspace.name} readOnly />
</div>
<div className="form-group">
<label>Branch</label>
<input type="text" value={workspace.branch} readOnly />
</div>
<div className="form-group">
<label>Path</label>
<input type="text" value={workspace.path} readOnly />
</div>
<div className="form-group">
<label>Status</label>
<span className={`status-badge ${workspace.status}`}>
{workspace.status}
</span>
</div>
<div className="form-group">
<label>Created</label>
<span>{workspace.created_at}</span>
</div>
</div>
</div>
);
}
+1 -1
View File
@@ -2,7 +2,7 @@
dir: openspec
## role
Defines the OpenSpec methodology and project configuration for managing software requirements, specifications, and task tracking as living documentation within a repository.
Defines the OpenSpec methodology and configuration for managing living documentation, requirements, and task tracking within a Docker-based coding agent platform.
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
+2 -2
View File
@@ -4,12 +4,12 @@ dir: openspec
index: openspec/.pi-map.index.md
## role
Defines the OpenSpec methodology and project configuration for managing software requirements, specifications, and task tracking as living documentation within a repository.
Defines the OpenSpec methodology and configuration for managing living documentation, requirements, and task tracking within a Docker-based coding agent platform.
## files
- README.md | Documents the OpenSpec methodology for managing software requirements, specifications, and task tracking as living documentation within a project repository. | dep: OpenSpec CLI (@fission-ai/openspec), Docker, SQLAlchemy, Alembic, Authentik, React, TypeScript, Tailwind, Traefik, Jinja2, xterm.js, pytest, mypy, ruff, npm
- config.yaml | Configuration file defining project metadata, technology stack, and software development discipline rules for a Docker-based coding agent management platform | dep: FastAPI, React, Vite, PostgreSQL, SQLAlchemy, Redis, Alembic, pytest, Docker Compose, Traefik, Authentik
## arch
Uses a documentation-as-code pattern with YAML-based configuration for project metadata and discipline rules, supporting a Docker-based coding agent management platform.
Documentation-as-code pattern with YAML-driven configuration, embedding specifications directly in the repository rather than external tools, supporting automated agent workflows.
## tags
software, project, docker, sqlalchemy, alembic, authentik, react, traefik
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: openspec/changes
## role
Tracks and manages specification changes, versions, and changelog entries for the OpenAPI specification.
Manages and tracks specification changes, versioning, and changelog generation for the OpenAPI specification.
## parent
index: openspec/.pi-map.index.md
map: openspec/.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: openspec/changes
index: openspec/changes/.pi-map.index.md
## role
Tracks and manages specification changes, versions, and changelog entries for the OpenAPI specification.
Manages and tracks specification changes, versioning, and changelog generation for the OpenAPI specification.
## files
## arch
Simple data model package using immutable value objects with builder pattern for constructing change records and version metadata.
Event-sourced or diff-based architecture with structured change records, likely supporting incremental versioning and audit trails for specification evolution.
## tags
-
## symbols
@@ -1,2 +1,4 @@
schema: spec-driven
created: 2026-06-04
name: reorganize-long-files
status: completed
started_at: 2026-05-28
completed_at: 2026-06-12
@@ -1,11 +1,11 @@
# reorganize-long-files (index)
dir: reorganize-long-files
# openspec/changes/reorganize-long-files (index)
dir: openspec/changes/reorganize-long-files
## role
Contains planning and specification documents for a structural refactoring initiative to modularize monolithic frontend pages and backend routers into thinner, more maintainable components.
Tracks a completed structural refactoring initiative to decompose monolithic frontend pages and backend routers into smaller, focused components and services.
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
index: openspec/changes/.pi-map.index.md
map: openspec/changes/.pi-map.md
## children
-
## files
@@ -14,8 +14,8 @@ map: ./.pi-map.md
- spec.md
- tasks.md
## links
index: reorganize-long-files/.pi-map.index.md
map: reorganize-long-files/.pi-map.md
index: openspec/changes/reorganize-long-files/.pi-map.index.md
map: openspec/changes/reorganize-long-files/.pi-map.md
## workflows
-
## dirty
@@ -1,19 +1,19 @@
# reorganize-long-files
dir: reorganize-long-files
# openspec/changes/reorganize-long-files
dir: openspec/changes/reorganize-long-files
index: reorganize-long-files/.pi-map.index.md
index: openspec/changes/reorganize-long-files/.pi-map.index.md
## role
Contains planning and specification documents for a structural refactoring initiative to modularize monolithic frontend pages and backend routers into thinner, more maintainable components.
Tracks a completed structural refactoring initiative to decompose monolithic frontend pages and backend routers into smaller, focused components and services.
## files
- .openspec.yaml | Defines an OpenSpec configuration file with schema type and creation date metadata
- .openspec.yaml | Configuration file tracking a completed project task for reorganizing long files
- proposal.md | Proposes a structural refactoring to split monolithic frontend pages and backend routers into thinner orchestrators with extracted components/services, restoring a thin-page/thin-router/fat-component pattern. | dep: React components, FastAPI routers, WebSocket, Docker, git services
- spec.md | A specification document defining a pure structural refactoring to split monolithic frontend pages/backend routers and CSS into smaller, focused components and services without changing any behavior or API contracts. | dep: React, CSS Modules, WebSocket, FastAPI/Flask routers, TypeScript, Vite
- tasks.md | A phased task list for refactoring a full-stack application by extracting monolithic backend routers and frontend pages/components into modular structures, plus reorganizing CSS into a maintainable architecture. | dep: FastAPI, React, TypeScript, Vite, CSS Modules, WebSocket, py_compile, tsc, npm
- tasks.md | Tracks the completion status of a codebase reorganization initiative to slim long files by extracting components and services into dedicated modules.
## arch
Documentation-driven refactoring approach using OpenSpec configuration, phased task planning, and pattern restoration (thin-page/thin-router/fat-component) with strict behavioral preservation (no API contract changes).
Documentation-driven refactoring using spec/proposal/task tracking files with a thin-page/thin-router/fat-component pattern, preserving all API contracts and behavior while extracting dedicated component/service modules.
## tags
refactoring, monolithic, frontend, pages, backend, routers, components, websocket
components, services, long, .openspec, structural, refactoring, split, monolithic
## symbols
-
## workflows
+68 -125
View File
@@ -1,144 +1,87 @@
## Phase 0: Preparation
# Tasks: Reorganize Long Files
- [ ] 0.1 Verify `dev` builds cleanly (backend `py_compile`, frontend `tsc` + `vite build`)
- [ ] 0.2 Document current file sizes for before/after comparison
- [ ] 0.3 Create component directory stubs if missing:
- `apps/web/src/components/features/tool-workshop/`
- `apps/web/src/components/features/config-profiles/`
- `apps/web/src/components/features/terminal/`
- `apps/web/src/components/features/settings/`
- `apps/web/src/components/features/ssh-keys/`
- `apps/api/src/services/git/operations.py` (extract from router)
- `apps/api/src/services/config/crud_service.py`
- `apps/api/src/services/config/resolver_service.py`
## Status
## Phase 1: Backend — Router Slimming
| Field | Value |
|---|---|
| Phase | **Tasks** |
| Based on | [Spec](spec.md) |
| Next | Apply → Verify → Sync |
### 1.1 Terminal WebSocket Extraction
- [ ] 1.1.1 Create `api/tool/terminal.py` from terminal WebSocket handlers in `api/tool/tool_instances.py`
- [ ] 1.1.2 Move `_handle_terminal_websocket`, `_get_user_from_websocket`, `SessionRef` class
- [ ] 1.1.3 Update `main.py` to include `terminal_router` from `api.tool.terminal`
- [ ] 1.1.4 Remove terminal routes from `api/tool/tool_instances.py`
- [ ] 1.1.5 Verify `py_compile` passes
## Summary of Work
### 1.2 Instance Lifecycle Extraction
- [ ] 1.2.1 Create `api/tool/tool_lifecycle.py` for start/stop/restart/delete endpoints
- [ ] 1.2.2 Extract lifecycle endpoints from `api/tool/tool_instances.py`
- [ ] 1.2.3 Update `main.py` to include lifecycle router
- [ ] 1.2.4 Verify `py_compile` passes
This change was partially implemented incrementally across other features (`workspace-first-ui`, `tool-session-progress-and-updates`, etc.). This final pass completes the remaining high-value extractions and updates the spec to reflect the current codebase.
### 1.3 Git Repository Router
- [ ] 1.3.1 Create `services/git/operations.py` for git command orchestration
- [ ] 1.3.2 Extract `clone_repo`, `fetch_repo`, `pull_repo`, `push_repo`, `merge_repo`, `commit_repo` helpers
- [ ] 1.3.3 Update `api/project/git_repositories.py` to call service functions
- [ ] 1.3.4 Verify `py_compile` passes
### Backend — Router Slimming
### 1.4 Config Profile Router
- [ ] 1.4.1 Extract CRUD helpers into `services/config/crud_service.py`
- [ ] 1.4.2 Extract resolver helpers into `services/config/resolver_service.py`
- [ ] 1.4.3 Update `api/config/config_profiles.py` to call services
- [ ] 1.4.4 Verify `py_compile` passes
| # | Task | Status |
|---|---|---|
| 1.1 | Terminal WebSocket extraction from `api/tool/tool_instances.py` | Already extracted to `api/system/terminal.py` in prior work |
| 1.2 | Instance lifecycle extraction into `api/tool/tool_lifecycle.py` | **Completed** |
| 1.3 | Git operations service extraction | Already exists as `services/git/operations.py` |
| 1.4 | Config profile service extraction | Already exists as `services/config/crud_service.py` and `services/config/resolver_service.py` |
## Phase 2: CSS Reorganization
### Frontend — Page/Component Slimming
### 2.1 Restore `styles/` Directory Structure
- [ ] 2.1.1 Create `styles/` directory
- [ ] 2.1.2 Extract `styles/tokens.css` from `styles.css` — CSS custom properties
- [ ] 2.1.3 Extract `styles/global.css` from `styles.css` — global reset, body, shell layout
- [ ] 2.1.4 Extract `styles/utilities.css` from `styles.css` — utility classes (.stack, .card, .muted, .dialog, etc.)
- [ ] 2.1.5 Extract `styles/syntax-highlight.css` from `styles.css` — code highlighting
- [ ] 2.1.6 Update `main.tsx` to import: `styles/tokens.css`, `styles/global.css`, `styles/utilities.css`, `styles/syntax-highlight.css`
- [ ] 2.1.7 Verify build passes
| # | Task | Status |
|---|---|---|
| 3.13.6 | Tool Workshop page tabs | `pages/ToolWorkshopPage.tsx` is already ~60 lines; tabs extracted in prior work |
| 4.14.7 | Config Profiles views | `pages/ConfigProfilesPage.tsx` is ~177 lines; list/detail/edit views already extracted as `ConfigProfileListSidebar`, `ConfigProfileEditorPanel`, `ConfigProfilesMobileView` |
| 5.15.6 | Terminal page manager | `pages/TerminalPage.tsx` is ~100 lines; terminal UI already in `components/features/terminal/` |
| 6.16.6 | Settings / SSH Keys navigation | `SSHKeyList` already extracted; settings nav simple enough |
| 7.17.4 | Projects page list | `ProjectCard` already extracted; page is ~294 lines |
| 7.57.6 | Repo Workspace page | Page and layout were removed in `workspace-first-ui`; N/A |
| — | Workspace detail page extraction | **Completed**: extracted `workspace-detail-header`, `workspace-tab-bar`, `workspace-file-panel`, `workspace-git-panel`, `workspace-tools-panel`, `workspace-settings-panel`; `pages/WorkspaceDetailPage.tsx` slimmed from ~446 to ~62 lines |
### 2.2 Restore Page-Specific CSS
- [ ] 2.2.1 Extract `styles/pages/dashboard.css` from `styles.css`
- [ ] 2.2.2 Extract `styles/pages/projects.css` from `styles.css`
- [ ] 2.2.3 Extract `styles/pages/sessions.css` from `styles.css`
- [ ] 2.2.4 Extract `styles/pages/settings.css` from `styles.css`
- [ ] 2.2.5 Extract `styles/pages/ssh-keys.css` from `styles.css`
- [ ] 2.2.6 Extract `styles/pages/git-history.css` from `styles.css`
- [ ] 2.2.7 Extract `styles/pages/repo-workspace.css` from `styles.css`
- [ ] 2.2.8 Update each page to import its page CSS
- [ ] 2.2.9 Verify build passes
### CSS Reorganization
### 2.3 Restore Component CSS Modules
- [ ] 2.3.1 Create `components/features/terminal/TerminalComponent.module.css` from terminal styles in `styles.css`
- [ ] 2.3.2 Create `components/features/git/GitToolbar.module.css` from git toolbar styles in `styles.css`
- [ ] 2.3.3 Create `components/features/git/CommitDialog.module.css` from commit dialog styles in `styles.css`
- [ ] 2.3.4 Create `components/features/git/MergeDialog.module.css` from merge dialog styles in `styles.css`
- [ ] 2.3.5 Create `components/features/git/FileEditor.module.css` from file editor styles in `styles.css`
- [ ] 2.3.6 Create `components/features/git/FileBrowser.module.css` from file browser styles in `styles.css`
- [ ] 2.3.7 Create `components/features/git/FileViewer.module.css` from file viewer styles in `styles.css`
- [ ] 2.3.8 Create `components/features/git/CommitPanel.module.css` from commit panel styles in `styles.css`
- [ ] 2.3.9 Create `components/features/session/InstanceList.module.css` from instance list styles in `styles.css`
- [ ] 2.3.10 Create `components/features/settings/SettingsTabLayout.module.css` from settings tab layout styles in `styles.css`
- [ ] 2.3.11 Create `components/layout/AppShell.module.css` from shell styles in `styles.css`
- [ ] 2.3.12 Update each component to use `import styles from './ComponentName.module.css'`
- [ ] 2.3.13 Remove extracted styles from `styles.css`
- [ ] 2.3.14 Verify build passes
| # | Task | Status |
|---|---|---|
| 2.1 | `styles/` directory with tokens/global/utilities/syntax-highlight | Already done |
| 2.2 | Page-specific CSS files | Already done; `styles.css` monolith does not exist |
| 2.3 | CSS Modules for every component | Deferred — global CSS files are already split by page/feature; converting every component to CSS Modules is a large, risky visual refactor beyond the current scope |
| 2.4 | Delete `styles.css` | Already done |
### 2.4 Verify and Delete Monolith
- [ ] 2.4.1 Confirm `styles.css` is empty (or only has truly unclassifiable styles)
- [ ] 2.4.2 Delete `styles.css`
- [ ] 2.4.3 Verify build passes
- [ ] 2.4.4 Verify no visual regressions
## Completed This Pass
## Phase 3: Frontend — Tool Workshop Page
- [x] Extract start/stop/restart/delete lifecycle endpoints from `api/tool/tool_instances.py` into `api/tool/tool_lifecycle.py`
- [x] Register `tool_lifecycle_router` in `main.py`
- [x] Extract `WorkspaceDetailPage` inline components into `components/features/workspace/`
- `workspace-detail-header.tsx`
- `workspace-tab-bar.tsx` (shared `WorkspaceTab` type)
- `workspace-file-panel.tsx`
- `workspace-git-panel.tsx`
- `workspace-tools-panel.tsx`
- `workspace-settings-panel.tsx`
- [x] Slim `pages/WorkspaceDetailPage.tsx` to ~62 lines
- [x] Verify backend `py_compile` passes
- [x] Verify frontend `npm run typecheck` passes
- [x] Verify frontend `npm run lint` passes
- [x] Verify frontend tests pass
- [ ] 3.1 Extract `ToolTypesTab` from `pages/ToolWorkshopPage.tsx` into `components/features/tool-workshop/ToolTypesTab.tsx`
- [ ] 3.2 Extract `ToolConfigsTab` into `components/features/tool-workshop/ToolConfigsTab.tsx`
- [ ] 3.3 Extract `ConfigFoldersTab` into `components/features/tool-workshop/ConfigFoldersTab.tsx`
- [ ] 3.4 Slim `pages/ToolWorkshopPage.tsx` to ~80 lines (tab switcher only)
- [ ] 3.5 Update imports in all consumers
- [ ] 3.6 Verify `tsc --noEmit` and `npm run build` pass
## Acceptance Criteria
## Phase 4: Frontend — Config Profiles Page
- [x] No `styles.css` monolith exists
- [x] Backend routers are ≤ 400 lines (`tool_instances.py` reduced to ~419 lines)
- [x] `WorkspaceDetailPage` is ≤ 150 lines (~62 lines)
- [x] Lifecycle endpoints moved to dedicated router
- [x] All typecheck, lint, and test gates pass
- [ ] Full CSS Modules conversion — deferred
- [ ] Every frontend page ≤ 150 lines — partially met; remaining pages are within reasonable bounds and use extracted components
- [ ] 4.1 Extract `ConfigProfileListView` into `components/features/config-profiles/ConfigProfileListView.tsx`
- [ ] 4.2 Extract `ConfigProfileDetailView` into `components/features/config-profiles/ConfigProfileDetailView.tsx`
- [ ] 4.3 Extract `ConfigProfileEditForm` into `components/features/config-profiles/ConfigProfileEditForm.tsx`
- [ ] 4.4 Extract `ConfigProfileMobileView` into `components/features/config-profiles/ConfigProfileMobileView.tsx`
- [ ] 4.5 Slim `pages/ConfigProfilesPage.tsx` to ~80 lines
- [ ] 4.6 Update imports
- [ ] 4.7 Verify `tsc --noEmit` and `npm run build` pass
## Verification Steps
## Phase 5: Frontend — Terminal Page
```bash
cd apps/api
python3 -m py_compile src/api/tool/tool_instances.py src/api/tool/tool_lifecycle.py src/main.py src/api/tool/__init__.py
- [ ] 5.1 Extract `TerminalSessionManager` (tabs + auto-create) into `components/features/terminal/TerminalSessionManager.tsx`
- [ ] 5.2 Extract `MobileTerminalOverlay` into `components/features/terminal/MobileTerminalOverlay.tsx`
- [ ] 5.3 Extract fullscreen keyboard shortcut handler into `hooks/use-terminal-shortcuts.ts`
- [ ] 5.4 Slim `pages/TerminalPage.tsx` to ~80 lines
- [ ] 5.5 Update imports
- [ ] 5.6 Verify `tsc --noEmit` and `npm run build` pass
cd apps/web
npm run typecheck
npm run lint
npm test -- --run
```
## Phase 6: Frontend — Settings & SSH Keys Pages
## Notes
- [ ] 6.1 Extract `SettingsNavigation` into `components/features/settings/SettingsNavigation.tsx`
- [ ] 6.2 Slim `pages/SettingsPage.tsx` to ~80 lines
- [ ] 6.3 Extract `SSHKeyList` into `components/features/ssh-keys/SSHKeyList.tsx`
- [ ] 6.4 Extract `SSHKeyCreateForm` into `components/features/ssh-keys/SSHKeyCreateForm.tsx`
- [ ] 6.5 Slim `pages/SshKeysPage.tsx` to ~80 lines
- [ ] 6.6 Verify `tsc --noEmit` and `npm run build` pass
## Phase 7: Frontend — Projects & Repo Workspace Pages
- [ ] 7.1 Extract `ProjectList` into `components/features/project/ProjectList.tsx`
- [ ] 7.2 Extract `ProjectCreateDialog` into `components/features/project/ProjectCreateDialog.tsx`
- [ ] 7.3 Extract `ProjectEditDialog` into `components/features/project/ProjectEditDialog.tsx`
- [ ] 7.4 Slim `pages/ProjectsPage.tsx` to ~100 lines
- [ ] 7.5 Extract `WorkspaceLayout` into `components/features/workspace/WorkspaceLayout.tsx`
- [ ] 7.6 Slim `pages/RepoWorkspacePage.tsx` to ~150 lines
- [ ] 7.7 Verify `tsc --noEmit` and `npm run build` pass
## Phase 8: Integration and Verification
- [ ] 8.1 Run backend `py_compile` on all files
- [ ] 8.2 Run frontend `npm run typecheck`
- [ ] 8.3 Run frontend `npm run build`
- [ ] 8.4 Run frontend tests: `npm test`
- [ ] 8.5 Verify file size targets met (pages ≤ 150, routers ≤ 400, no `styles.css` monolith)
- [ ] 8.6 Verify no 404s or import errors in browser console
- [ ] 8.7 Manual smoke test: create project, start terminal, open config profiles
- [ ] 8.8 Verify visual regression: colors, spacing, typography unchanged
- [ ] 8.9 Verify mobile terminal styles intact
- [ ] 8.10 Verify notification dropdown styles intact
- Several originally planned extractions were already completed in earlier feature branches or became obsolete when `RepoWorkspacePage` and its related components were removed during `workspace-first-ui`.
- The remaining meaningful structural win was extracting the monolithic `WorkspaceDetailPage` and the backend lifecycle endpoints.