feat: merge reorganize-long-files completion
See feature commit for details.
This commit is contained in:
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
-
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.1–3.6 | Tool Workshop page tabs | `pages/ToolWorkshopPage.tsx` is already ~60 lines; tabs extracted in prior work |
|
||||
| 4.1–4.7 | Config Profiles views | `pages/ConfigProfilesPage.tsx` is ~177 lines; list/detail/edit views already extracted as `ConfigProfileListSidebar`, `ConfigProfileEditorPanel`, `ConfigProfilesMobileView` |
|
||||
| 5.1–5.6 | Terminal page manager | `pages/TerminalPage.tsx` is ~100 lines; terminal UI already in `components/features/terminal/` |
|
||||
| 6.1–6.6 | Settings / SSH Keys navigation | `SSHKeyList` already extracted; settings nav simple enough |
|
||||
| 7.1–7.4 | Projects page list | `ProjectCard` already extracted; page is ~294 lines |
|
||||
| 7.5–7.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.
|
||||
|
||||
Reference in New Issue
Block a user