Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 346b32236a | |||
| 8f7f682a92 | |||
| 2ece7074f6 | |||
| c7ab2de25e | |||
| c9b0259993 | |||
| 1f2c3dbe2a | |||
| d247e44985 | |||
| d60dec9d62 | |||
| 4913cc7297 | |||
| 86360661e9 | |||
| 1d09193652 | |||
| 4ced6141ae | |||
| 530225d37c | |||
| e6ab77d123 | |||
| 4e1477c4be | |||
| 2b5b8363ca | |||
| 1e0f95f8bd | |||
| 6a20c54da7 | |||
| db6ae38568 | |||
| 1e43ea48aa | |||
| efe5e4ae50 | |||
| 31f1ce00d3 | |||
| d395aaf574 | |||
| d78ca8a9d5 | |||
| 19291d6be9 | |||
| 5409ba2b13 | |||
| 8bde9a213c | |||
| 315cb33e3f | |||
| 474843ffa6 | |||
| 766f7ff0d1 | |||
| f8b162ec5f | |||
| 8a8a9bc9e4 | |||
| 61f9c52455 |
+4
-1
@@ -16,7 +16,7 @@ dir: .
|
|||||||
Trust boundary: index routes, map orients, source decides.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing Docker Compose orchestration, environment templates, and development tooling.
|
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing containerized orchestration, environment templates, and development tooling.
|
||||||
## parent
|
## parent
|
||||||
-
|
-
|
||||||
## children
|
## children
|
||||||
@@ -59,6 +59,9 @@ Infrastructure and deployment configuration package for a self-hosted project ma
|
|||||||
- tool-images
|
- tool-images
|
||||||
index: tool-images/.pi-map.index.md
|
index: tool-images/.pi-map.index.md
|
||||||
map: tool-images/.pi-map.md
|
map: tool-images/.pi-map.md
|
||||||
|
- uploads
|
||||||
|
index: uploads/.pi-map.index.md
|
||||||
|
map: uploads/.pi-map.md
|
||||||
## files
|
## files
|
||||||
- .env.example
|
- .env.example
|
||||||
- .gitignore
|
- .gitignore
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ index: ./.pi-map.index.md
|
|||||||
Trust boundary: index routes, map orients, source decides.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing Docker Compose orchestration, environment templates, and development tooling.
|
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing containerized orchestration, environment templates, and development tooling.
|
||||||
## files
|
## files
|
||||||
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
|
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
|
||||||
- .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git
|
- .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git
|
||||||
@@ -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
|
- progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases
|
||||||
- swap-pane | Empty file with no functionality
|
- swap-pane | Empty file with no functionality
|
||||||
## arch
|
## arch
|
||||||
Containerized microservices architecture using Docker Compose with PostgreSQL and Redis backing services, Traefik reverse proxy for TLS termination, environment-driven configuration pattern, and Makefile-based development workflow automation.
|
Docker Compose-based microservices architecture with frontend/backend separation, PostgreSQL/Redis data layer, Traefik reverse proxy integration, and environment-driven configuration management following twelve-factor app principles.
|
||||||
## tags
|
## tags
|
||||||
docker, redis, git, application, postgresql, compose, traefik, project
|
docker, redis, git, application, postgresql, compose, traefik, project
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps
|
dir: apps
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains the main deployable application entry points and executable components of the project.
|
Contains the main deployable application modules or entry points for the project.
|
||||||
## parent
|
## parent
|
||||||
index: ./.pi-map.index.md
|
index: ./.pi-map.index.md
|
||||||
map: ./.pi-map.md
|
map: ./.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,10 +4,10 @@ dir: apps
|
|||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains the main deployable application entry points and executable components of the project.
|
Contains the main deployable application modules or entry points for the project.
|
||||||
## files
|
## files
|
||||||
## arch
|
## arch
|
||||||
Typically follows a multi-app monorepo structure where each subdirectory is an independent application sharing common libraries/modules.
|
Modular monolith or microservices architecture with separate application boundaries, each potentially having its own configuration, dependencies, and lifecycle.
|
||||||
## tags
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api
|
dir: apps/api
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools through Docker container orchestration.
|
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances.
|
||||||
## parent
|
## parent
|
||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
map: apps/.pi-map.md
|
map: apps/.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ dir: apps/api
|
|||||||
index: apps/api/.pi-map.index.md
|
index: apps/api/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools through Docker container orchestration.
|
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances.
|
||||||
## files
|
## 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
|
- .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
|
- 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
|
- 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
|
- 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
|
## arch
|
||||||
Async Python backend using FastAPI with PostgreSQL (asyncpg), Alembic migrations, multi-stage Docker deployment with Cloudflare tunneling, uv package management, and containerized service coordination with database dependency waiting.
|
Async Python/FastAPI with PostgreSQL (Alembic migrations), multi-stage Docker deployment with Cloudflare tunneling, uv package management, and containerized service orchestration.
|
||||||
## tags
|
## tags
|
||||||
docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn
|
docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/alembic
|
dir: apps/api/alembic
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database migration infrastructure for managing schema evolution with async SQLAlchemy support.
|
Database migration infrastructure for the API application, providing version-controlled schema evolution with async SQLAlchemy support.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/.pi-map.index.md
|
index: apps/api/.pi-map.index.md
|
||||||
map: apps/api/.pi-map.md
|
map: apps/api/.pi-map.md
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ dir: apps/api/alembic
|
|||||||
index: apps/api/alembic/.pi-map.index.md
|
index: apps/api/alembic/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database migration infrastructure for managing schema evolution with async SQLAlchemy support.
|
Database migration infrastructure for the API application, providing version-controlled schema evolution with async SQLAlchemy support.
|
||||||
## files
|
## files
|
||||||
- env.py | Configures Alembic database migration environment with async SQLAlchemy support for a project. | exp: func:run_migrations_offline() → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:do_run_migrations(connection: Connection) → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:run_async_migrations() → None, call:async_engine_from_config, call:config.get_section, call:connectable.connect, call:connection.run_sync, call:connectable.dispose, func:run_migrations_online() → None, call:asyncio.run, call:run_async_migrations | dep: logging.config, alembic, sqlalchemy, sqlalchemy.engine, sqlalchemy.ext.asyncio, src.config, src.models, asyncio
|
- env.py | Configures Alembic database migration environment with async SQLAlchemy support for a project. | exp: func:run_migrations_offline() → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:do_run_migrations(connection: Connection) → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:run_async_migrations() → None, call:async_engine_from_config, call:config.get_section, call:connectable.connect, call:connection.run_sync, call:connectable.dispose, func:run_migrations_online() → None, call:asyncio.run, call:run_async_migrations | dep: logging.config, alembic, sqlalchemy, sqlalchemy.engine, sqlalchemy.ext.asyncio, src.config, src.models, asyncio
|
||||||
- script.py.mako | Alembic database migration script template that generates upgrade/downgrade functions for SQLAlchemy schema migrations | dep: alembic, sqlalchemy
|
- script.py.mako | Alembic database migration script template that generates upgrade/downgrade functions for SQLAlchemy schema migrations | dep: alembic, sqlalchemy
|
||||||
## arch
|
## arch
|
||||||
Alembic migration framework with async SQLAlchemy engine configuration and templated revision scripts.
|
Alembic migration framework with Mako templating for generating revision scripts, async SQLAlchemy engine configuration, and autogenerate capabilities for schema change tracking.
|
||||||
## tags
|
## tags
|
||||||
migrations, run, sqlalchemy, async, alembic, call:context.configure, call:context.begin, transaction
|
migrations, run, sqlalchemy, async, alembic, call:context.configure, call:context.begin, transaction
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/alembic/versions
|
dir: apps/api/alembic/versions
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database schema versioning and incremental migration management for the API application's relational database, tracking evolutionary changes to tables, columns, relationships, and data transformations across the project lifecycle.
|
Manages incremental database schema evolution for the API application using Alembic migrations, tracking all table creations, column additions, relationship changes, and data transformations over the project's lifecycle.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/alembic/.pi-map.index.md
|
index: apps/api/alembic/.pi-map.index.md
|
||||||
map: apps/api/alembic/.pi-map.md
|
map: apps/api/alembic/.pi-map.md
|
||||||
@@ -47,9 +47,11 @@ map: apps/api/alembic/.pi-map.md
|
|||||||
- 2026_05_29_remove_lsio_command_override.py
|
- 2026_05_29_remove_lsio_command_override.py
|
||||||
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py
|
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py
|
||||||
- 2026_06_01_add_workspaces.py
|
- 2026_06_01_add_workspaces.py
|
||||||
|
- 2026_06_13_make_clone_mode_nullable.py
|
||||||
- 398082499c30_add_tool_config_fields.py
|
- 398082499c30_add_tool_config_fields.py
|
||||||
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py
|
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py
|
||||||
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py
|
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py
|
||||||
|
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py
|
||||||
- 8ed7dd80973d_create_config_folders_table.py
|
- 8ed7dd80973d_create_config_folders_table.py
|
||||||
- af8512103d67_add_tool_type_fields.py
|
- af8512103d67_add_tool_type_fields.py
|
||||||
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py
|
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ dir: apps/api/alembic/versions
|
|||||||
index: apps/api/alembic/versions/.pi-map.index.md
|
index: apps/api/alembic/versions/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database schema versioning and incremental migration management for the API application's relational database, tracking evolutionary changes to tables, columns, relationships, and data transformations across the project lifecycle.
|
Manages incremental database schema evolution for the API application using Alembic migrations, tracking all table creations, column additions, relationship changes, and data transformations over the project's lifecycle.
|
||||||
## files
|
## files
|
||||||
- 0001_initial_schema.py | Defines the initial database schema migration creating five tables (users, ssh_keys, projects, git_repositories, user_configs) with relationships, indexes, and constraints using Alembic. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.String, call:postgresql.UUID, call:sa.DateTime, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:op.f, call:sa.Text, call:sa.ForeignKeyConstraint, call:sa.Boolean, call:postgresql.JSONB, func:downgrade() → None, call:op.drop_table, call:op.drop_index, call:op.f | dep: alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
|
- 0001_initial_schema.py | Defines the initial database schema migration creating five tables (users, ssh_keys, projects, git_repositories, user_configs) with relationships, indexes, and constraints using Alembic. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.String, call:postgresql.UUID, call:sa.DateTime, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:op.f, call:sa.Text, call:sa.ForeignKeyConstraint, call:sa.Boolean, call:postgresql.JSONB, func:downgrade() → None, call:op.drop_table, call:op.drop_index, call:op.f | dep: alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
|
||||||
- 0002_refresh_tokens.py | Alembic database migration that creates a refresh_tokens table with indexes for user authentication token management | exp: func:upgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:inspector.get_indexes, call:op.f, call:op.create_index, func:downgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:inspector.get_indexes, call:op.f, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 0002_refresh_tokens.py | Alembic database migration that creates a refresh_tokens table with indexes for user authentication token management | exp: func:upgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:inspector.get_indexes, call:op.f, call:op.create_index, func:downgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:inspector.get_indexes, call:op.f, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
@@ -33,7 +33,7 @@ Database schema versioning and incremental migration management for the API appl
|
|||||||
- 2026_05_27_external_repos.py | Alembic database migration that makes project_id nullable in git_repositories table to support external repositories and expands alembic_version version_num column to 64 characters. | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.UUID, func:downgrade() → None, call:op.alter_column, call:sa.UUID, call:op.execute | dep: typing, alembic, sqlalchemy
|
- 2026_05_27_external_repos.py | Alembic database migration that makes project_id nullable in git_repositories table to support external repositories and expands alembic_version version_num column to 64 characters. | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.UUID, func:downgrade() → None, call:op.alter_column, call:sa.UUID, call:op.execute | dep: typing, alembic, sqlalchemy
|
||||||
- 2026_05_28_add_monitoring_tables.py | Alembic database migration that creates monitoring tables (instance_events and health_checks) with indexes for tracking tool instance events and health checks | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.Boolean, call:sa.Integer, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
- 2026_05_28_add_monitoring_tables.py | Alembic database migration that creates monitoring tables (instance_events and health_checks) with indexes for tracking tool instance events and health checks | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.Boolean, call:sa.Integer, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||||
- 2026_05_28_add_terminal_sessions_table.py | Alembic database migration that creates a terminal_sessions table with tracking columns and foreign key to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:op.f, func:downgrade() → None, call:op.drop_index, call:op.f, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
- 2026_05_28_add_terminal_sessions_table.py | Alembic database migration that creates a terminal_sessions table with tracking columns and foreign key to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:op.f, func:downgrade() → None, call:op.drop_index, call:op.f, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||||
- 2026_05_28_add_tool_definition_manifests.py | Alembic database migration that creates a tool_definition_manifests table, adds manifest-related columns to tool_types and tool_instances, and migrates existing pi-agent data from Dockerfile templates to JSON-based manifest definitions | exp: func:upgrade() → None, call:op.get_bind, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:sa.ForeignKeyConstraint, call:sa.CheckConstraint, call:conn.execute, call:sa.text, call:result.fetchone, call:op.add_column, call:op.create_foreign_key, call:op.drop_constraint, call:op.execute, call:json.dumps, call:str, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_column, call:op.drop_constraint, call:op.drop_table | dep: json, uuid, typing, alembic, sqlalchemy
|
- 2026_05_28_add_tool_definition_manifests.py | Alembic database migration that creates a tool_definition_manifests table, adds manifest-related columns to tool_types and tool_instances, and migrates the pi-agent tool from Dockerfile-based to manifest-based definitions with seed data. | exp: func:upgrade() → None, call:op.get_bind, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:sa.ForeignKeyConstraint, call:sa.CheckConstraint, call:conn.execute, call:sa.text, call:result.fetchone, call:op.add_column, call:op.create_foreign_key, call:op.drop_constraint, call:op.execute, call:json.dumps, call:str, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_column, call:op.drop_constraint, call:op.drop_table | dep: json, uuid, typing, alembic, sqlalchemy
|
||||||
- 2026_05_28_drop_tool_configs_and_config_folders.py | Alembic database migration that drops `tool_configs` and `config_folders` tables with conditional existence checks and full downgrade recreation | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_table, func:downgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.Integer | dep: typing, alembic, sqlalchemy
|
- 2026_05_28_drop_tool_configs_and_config_folders.py | Alembic database migration that drops `tool_configs` and `config_folders` tables with conditional existence checks and full downgrade recreation | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_table, func:downgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.Integer | dep: typing, alembic, sqlalchemy
|
||||||
- 2026_05_29_add_notifications_table.py | Alembic database migration that creates a notifications table with user-linked, categorized, severity-graded messages supporting read/dismissed tracking and optimized querying indexes. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.text, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
- 2026_05_29_add_notifications_table.py | Alembic database migration that creates a notifications table with user-linked, categorized, severity-graded messages supporting read/dismissed tracking and optimized querying indexes. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.text, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||||
- 2026_05_29_add_ssh_key_ids_to_tool_instances.py | Alembic database migration that adds a JSON column named ssh_key_ids to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
- 2026_05_29_add_ssh_key_ids_to_tool_instances.py | Alembic database migration that adds a JSON column named ssh_key_ids to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
||||||
@@ -44,16 +44,18 @@ Database schema versioning and incremental migration management for the API appl
|
|||||||
- 2026_05_29_remove_lsio_command_override.py | Alembic database migration that removes broken command overrides containing --bind-addr or --host flags from LinuxServer.io code-server Docker Compose templates in both database tool_types records and on-disk instance compose files. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' """) ).fetchall, call:sa.text, call:yaml.safe_load, call:data["services"].values, call:svc.get, call:yaml.dump, call:print, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:conn.execute( sa.text(""" SELECT id, compose_path FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:path.write_text, func:downgrade() → None | dep: collections.abc, alembic, yaml, pathlib, sqlalchemy, pathlib.Path, information_schema
|
- 2026_05_29_remove_lsio_command_override.py | Alembic database migration that removes broken command overrides containing --bind-addr or --host flags from LinuxServer.io code-server Docker Compose templates in both database tool_types records and on-disk instance compose files. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' """) ).fetchall, call:sa.text, call:yaml.safe_load, call:data["services"].values, call:svc.get, call:yaml.dump, call:print, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:conn.execute( sa.text(""" SELECT id, compose_path FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:path.write_text, func:downgrade() → None | dep: collections.abc, alembic, yaml, pathlib, sqlalchemy, pathlib.Path, information_schema
|
||||||
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py | Alembic database migration that removes (or restores) the ssh_keys mount from a JSON manifest stored in the tool_definition_manifests table for the pi-agent tool definition. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:any, call:m.get, call:mounts.append, call:json.dumps | dep: json, typing, alembic, sqlalchemy
|
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py | Alembic database migration that removes (or restores) the ssh_keys mount from a JSON manifest stored in the tool_definition_manifests table for the pi-agent tool definition. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:any, call:m.get, call:mounts.append, call:json.dumps | dep: json, typing, alembic, sqlalchemy
|
||||||
- 2026_06_01_add_workspaces.py | Alembic database migration that creates a workspaces table with foreign keys to git_repositories and users, adds indexes, and adds a workspace_id column to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, call:sa.UniqueConstraint, call:op.create_index, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
- 2026_06_01_add_workspaces.py | Alembic database migration that creates a workspaces table with foreign keys to git_repositories and users, adds indexes, and adds a workspace_id column to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, call:sa.UniqueConstraint, call:op.create_index, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
||||||
|
- 2026_06_13_make_clone_mode_nullable.py | Alembic database migration that makes the `clone_mode` column in `tool_instances` table nullable to allow NULL values for new rows | exp: func:upgrade() → None, call:op.alter_column, call:sa.String, func:downgrade() → None, call:op.alter_column, call:sa.String | dep: alembic, sqlalchemy
|
||||||
- 398082499c30_add_tool_config_fields.py | Alembic database migration that adds five new columns (port_override, start_command, working_directory, environment_variables, volumes) to the tool_configs table with a port range check constraint. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 398082499c30_add_tool_config_fields.py | Alembic database migration that adds five new columns (port_override, start_command, working_directory, environment_variables, volumes) to the tool_configs table with a port range check constraint. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py | Alembic database migration that merges two parallel revision branches (removing is_builtin and adding config_profiles) into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py | Alembic database migration that merges two parallel revision branches (removing is_builtin and adding config_profiles) into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
||||||
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py | Alembic database migration that merges two divergent migration branches (profile resolver and workspaces) into a single head | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py | Alembic database migration that merges two divergent migration branches (profile resolver and workspaces) into a single head | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
||||||
|
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py | Alembic database migration that removes pi_state and pi_config mounts from the pi-agent manifest in upgrade, and restores them in downgrade | exp: func:_load_manifest(manifest_json), call:isinstance, call:json.loads, func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, call:manifest.get, call:m.get, call:mounts.append, call:json.dumps | dep: json, alembic, sqlalchemy
|
||||||
- 8ed7dd80973d_create_config_folders_table.py | Alembic database migration that creates a config_folders table with user-owned configuration folders supporting JSONB file storage and project overrides | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.ForeignKey, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:sa.Boolean, call:sa.DateTime, call:sa.UniqueConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 8ed7dd80973d_create_config_folders_table.py | Alembic database migration that creates a config_folders table with user-owned configuration folders supporting JSONB file storage and project overrides | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.ForeignKey, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:sa.Boolean, call:sa.DateTime, call:sa.UniqueConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- af8512103d67_add_tool_type_fields.py | Alembic database migration that adds new columns (definition_type, dockerfile_template, build_context, readiness_probe) to the tool_types table with a CHECK constraint on definition_type. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- af8512103d67_add_tool_type_fields.py | Alembic database migration that adds new columns (definition_type, dockerfile_template, build_context, readiness_probe) to the tool_types table with a CHECK constraint on definition_type. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py | Alembic database migration that merges two prior revisions (single_interface and clone_mode) into a single migration path | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py | Alembic database migration that merges two prior revisions (single_interface and clone_mode) into a single migration path | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
||||||
## arch
|
## arch
|
||||||
Linear and branched migration history using Alembic (SQLAlchemy's migration tool) with sequential numeric revisions, merge heads to reconcile divergent branches, idempotent data migrations, dialect-specific SQL (PostgreSQL/SQLite), JSON/JSONB columns for flexible schemas, and defensive checks for conditional schema changes; follows additive-only evolution with occasional table drops and column restructures, embedding both schema and data migration logic in revision files.
|
Linear and branched migration pattern using Alembic's revision system with merge migrations to reconcile divergent branches; each migration is an imperative upgrade/downgrade script containing raw SQL/DDL operations, with some migrations including data seeding and dialect-specific logic (PostgreSQL/SQLite), but lacks consistent naming convention (mixed timestamp and numeric prefixes) indicating organic evolution rather than planned schema design.
|
||||||
## tags
|
## tags
|
||||||
column, table, call:op.drop, key, alembic, downgrade, upgrade, constraint
|
column, table, call:op.drop, downgrade, alembic, upgrade, key, call:sa.text
|
||||||
## symbols
|
## symbols
|
||||||
- upgrade
|
- upgrade
|
||||||
- downgrade
|
- downgrade
|
||||||
|
|||||||
@@ -232,20 +232,6 @@ def upgrade() -> None:
|
|||||||
"writable": True,
|
"writable": True,
|
||||||
"owner": "user",
|
"owner": "user",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "pi_state",
|
|
||||||
"target": "/tmp/.pi/agents",
|
|
||||||
"source_type": "instance",
|
|
||||||
"writable": True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "pi_config",
|
|
||||||
"target": "/home/user/.pi",
|
|
||||||
"source_type": "git_mount",
|
|
||||||
"git_mount_ref": "dotfiles",
|
|
||||||
"writable": True,
|
|
||||||
"owner": "user",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"command": ["/bin/bash"],
|
"command": ["/bin/bash"],
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""remove pi config and state mounts from pi-agent manifest
|
||||||
|
|
||||||
|
Revision ID: 8c6d1dbd4798
|
||||||
|
Revises: 2026_06_13_make_clone_mode_nullable
|
||||||
|
Create Date: 2026-06-13 11:21:25.983178
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '8c6d1dbd4798'
|
||||||
|
down_revision = '2026_06_13_make_clone_mode_nullable'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_manifest(manifest_json):
|
||||||
|
return manifest_json if isinstance(manifest_json, dict) else json.loads(manifest_json)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Remove pi_state and pi_config mounts from the pi-agent manifest."""
|
||||||
|
conn = op.get_bind()
|
||||||
|
|
||||||
|
result = conn.execute(
|
||||||
|
sa.text(
|
||||||
|
"SELECT id, manifest FROM tool_definition_manifests WHERE name = 'pi-agent'"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row = result.fetchone()
|
||||||
|
if not row:
|
||||||
|
return
|
||||||
|
|
||||||
|
manifest_id, manifest_json = row
|
||||||
|
manifest = _load_manifest(manifest_json)
|
||||||
|
|
||||||
|
mounts = manifest.get("mounts", [])
|
||||||
|
original_count = len(mounts)
|
||||||
|
|
||||||
|
filtered_mounts = [
|
||||||
|
m for m in mounts if m.get("name") not in ("pi_state", "pi_config")
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(filtered_mounts) < original_count:
|
||||||
|
manifest["mounts"] = filtered_mounts
|
||||||
|
conn.execute(
|
||||||
|
sa.text(
|
||||||
|
"UPDATE tool_definition_manifests SET manifest = :manifest WHERE id = :id"
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"manifest": json.dumps(manifest),
|
||||||
|
"id": manifest_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Restore pi_state and pi_config mounts to the pi-agent manifest."""
|
||||||
|
conn = op.get_bind()
|
||||||
|
|
||||||
|
result = conn.execute(
|
||||||
|
sa.text(
|
||||||
|
"SELECT id, manifest FROM tool_definition_manifests WHERE name = 'pi-agent'"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row = result.fetchone()
|
||||||
|
if not row:
|
||||||
|
return
|
||||||
|
|
||||||
|
manifest_id, manifest_json = row
|
||||||
|
manifest = _load_manifest(manifest_json)
|
||||||
|
|
||||||
|
mounts = manifest.get("mounts", [])
|
||||||
|
existing_names = {m.get("name") for m in mounts}
|
||||||
|
|
||||||
|
if "pi_state" not in existing_names:
|
||||||
|
mounts.append(
|
||||||
|
{
|
||||||
|
"name": "pi_state",
|
||||||
|
"target": "/tmp/.pi/agents",
|
||||||
|
"source_type": "instance",
|
||||||
|
"writable": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if "pi_config" not in existing_names:
|
||||||
|
mounts.append(
|
||||||
|
{
|
||||||
|
"name": "pi_config",
|
||||||
|
"target": "/home/user/.pi",
|
||||||
|
"source_type": "git_mount",
|
||||||
|
"git_mount_ref": "dotfiles",
|
||||||
|
"writable": True,
|
||||||
|
"owner": "user",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
manifest["mounts"] = mounts
|
||||||
|
conn.execute(
|
||||||
|
sa.text(
|
||||||
|
"UPDATE tool_definition_manifests SET manifest = :manifest WHERE id = :id"
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"manifest": json.dumps(manifest),
|
||||||
|
"id": manifest_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src
|
dir: apps/api/src
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Core application package for the Headquarter API, handling configuration, database connectivity, structured logging, and FastAPI application initialization.
|
Core API application package that initializes and configures the Headquarter FastAPI backend with database, authentication, logging, and middleware infrastructure.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/.pi-map.index.md
|
index: apps/api/.pi-map.index.md
|
||||||
map: apps/api/.pi-map.md
|
map: apps/api/.pi-map.md
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ dir: apps/api/src
|
|||||||
index: apps/api/src/.pi-map.index.md
|
index: apps/api/src/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Core application package for the Headquarter API, handling configuration, database connectivity, structured logging, and FastAPI application initialization.
|
Core API application package that initializes and configures the Headquarter FastAPI backend with database, authentication, logging, and middleware infrastructure.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Marks the directory as a Python package for the Headquarter API.
|
- __init__.py | Marks the directory as a Python package for the Headquarter API.
|
||||||
- config.py | Defines application configuration settings with environment-based overrides using Pydantic, including database URLs, service domains, OAuth/Authentik integration, JWT/session settings, and computed properties for environment-specific behavior. | exp: class:Settings, func:build_database_url(user: str, password: str, host: str, port: int, database: str) → str | dep: pydantic, pydantic_settings
|
- config.py | Defines application configuration settings with environment-based overrides using Pydantic, including database URLs, service domains, OAuth/Authentik integration, JWT/session settings, and computed properties for environment-specific behavior. | exp: class:Settings, func:build_database_url(user: str, password: str, host: str, port: int, database: str) → str | dep: pydantic, pydantic_settings
|
||||||
@@ -12,7 +12,7 @@ Core application package for the Headquarter API, handling configuration, databa
|
|||||||
- 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
|
- 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 | 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.*
|
- 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
|
## arch
|
||||||
Layered architecture with environment-based Pydantic config, async SQLAlchemy with retry patterns, structured JSON logging with correlation IDs, and modular FastAPI setup with middleware and background services.
|
Layered architecture using Pydantic for environment-based configuration, async SQLAlchemy with Alembic migrations, structured JSON logging with correlation IDs, and FastAPI middleware/routing pattern for a service-oriented backend.
|
||||||
## tags
|
## tags
|
||||||
src, database, logging, call:logger.info, api, middleware, fastapi, filter
|
src, database, logging, call:logger.info, api, middleware, fastapi, filter
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src/api
|
dir: apps/api/src/api
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Defines shared API infrastructure and reusable validation utilities for the API layer.
|
Defines shared API infrastructure including reusable Pydantic validators for consistent input validation across API endpoints.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/src/.pi-map.index.md
|
index: apps/api/src/.pi-map.index.md
|
||||||
map: apps/api/src/.pi-map.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
|
index: apps/api/src/api/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Defines shared API infrastructure and reusable validation utilities for the API layer.
|
Defines shared API infrastructure including reusable Pydantic validators for consistent input validation across API endpoints.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Marks the directory as a Python package for API routers.
|
- __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
|
- 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
|
## arch
|
||||||
Modular package structure with shared Pydantic validators for cross-cutting schema validation concerns.
|
Modular utility package with functional validation helpers using Pydantic for declarative schema enforcement.
|
||||||
## tags
|
## tags
|
||||||
validate, raise:value, error, call:isinstance, mount, api, init, path
|
validate, raise:value, error, call:isinstance, mount, api, init, path
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src/api/workspace
|
dir: apps/api/src/api/workspace
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides FastAPI REST API endpoints for workspace management, including CRUD operations, file access, git version control, and tool instance management within user-scoped workspaces.
|
Provides FastAPI REST endpoints for workspace management, file operations, git integration, and tool instance management within user workspaces.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/src/api/.pi-map.index.md
|
index: apps/api/src/api/.pi-map.index.md
|
||||||
map: apps/api/src/api/.pi-map.md
|
map: apps/api/src/api/.pi-map.md
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ dir: apps/api/src/api/workspace
|
|||||||
index: apps/api/src/api/workspace/.pi-map.index.md
|
index: apps/api/src/api/workspace/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides FastAPI REST API endpoints for workspace management, including CRUD operations, file access, git version control, and tool instance management within user-scoped workspaces.
|
Provides FastAPI REST endpoints for workspace management, file operations, git integration, and tool instance management within user workspaces.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Aggregates and re-exports workspace API router modules for centralized access | dep: src.api.workspace.workspace_files, src.api.workspace.workspace_git, src.api.workspace.workspace_instances, src.api.workspace.workspaces
|
- __init__.py | Aggregates and re-exports workspace API router modules for centralized access | dep: src.api.workspace.workspace_files, src.api.workspace.workspace_git, src.api.workspace.workspace_instances, src.api.workspace.workspaces
|
||||||
- workspace_files.py | Provides FastAPI endpoints for listing, reading, and writing files within user workspaces with optional git commit integration. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:list_files(workspace_id: uuid.UUID, path, user_id, session) → dict, call:_get_workspace, call:FileService, call:service.list_directory, raise:HTTPException, func:get_file_content(workspace_id: uuid.UUID, path: str, user_id, session) → dict, call:_get_workspace, call:FileService, call:service.read_file, raise:HTTPException, func:write_file(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:FileService, call:data.get("path", "").strip, call:data.get("message", "").strip, call:service.write_file, call:GitOperations, call:git.commit, raise:HTTPException | dep: uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.shared.file_service, sqlalchemy, src.services.git.git_operations
|
- workspace_files.py | Provides FastAPI endpoints for listing, reading, and writing files within user workspaces with optional git commit integration. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:list_files(workspace_id: uuid.UUID, path, user_id, session) → dict, call:_get_workspace, call:FileService, call:service.list_directory, raise:HTTPException, func:get_file_content(workspace_id: uuid.UUID, path: str, user_id, session) → dict, call:_get_workspace, call:FileService, call:service.read_file, raise:HTTPException, func:write_file(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:FileService, call:data.get("path", "").strip, call:data.get("message", "").strip, call:service.write_file, call:GitOperations, call:git.commit, raise:HTTPException | dep: uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.shared.file_service, sqlalchemy, src.services.git.git_operations
|
||||||
- workspace_git.py | Provides FastAPI REST endpoints for git operations (status, branches, commit, push, pull, fetch, checkout, history) scoped to user workspaces. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:git_status(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.status, raise:HTTPException, func:git_branches(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.branches, raise:HTTPException, func:git_commit(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:data.get("message", "").strip, call:GitOperations, call:git.commit, raise:HTTPException, func:git_push(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.push, raise:HTTPException, func:git_pull(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.pull, raise:HTTPException, func:git_fetch(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.fetch, raise:HTTPException, func:git_checkout(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:data.get("branch", "").strip, call:GitOperations, call:git.checkout, call:session.commit, raise:HTTPException, func:git_history(workspace_id: uuid.UUID, path, limit, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.history, raise:HTTPException | dep: uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.git.git_operations, sqlalchemy
|
- workspace_git.py | Provides FastAPI REST endpoints for git operations (status, branches, commit, push, pull, fetch, checkout, history) scoped to user workspaces. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:git_status(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.status, raise:HTTPException, func:git_branches(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.branches, raise:HTTPException, func:git_commit(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:data.get("message", "").strip, call:GitOperations, call:git.commit, raise:HTTPException, func:git_push(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.push, raise:HTTPException, func:git_pull(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.pull, raise:HTTPException, func:git_fetch(workspace_id: uuid.UUID, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.fetch, raise:HTTPException, func:git_checkout(workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_workspace, call:data.get("branch", "").strip, call:GitOperations, call:git.checkout, call:session.commit, raise:HTTPException, func:git_history(workspace_id: uuid.UUID, path, limit, user_id, session) → dict, call:_get_workspace, call:GitOperations, call:git.history, raise:HTTPException | dep: uuid, fastapi, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.services.git.git_operations, sqlalchemy
|
||||||
- workspace_instances.py | FastAPI router providing endpoints to create and list tool instances associated with a specific workspace. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:create_workspace_instance(workspace_id: uuid.UUID, data: CreateWorkspaceInstanceRequest, user_id, session) → dict, call:_get_workspace, call:session.get, call:CreateInstanceRequest, call:str, call:create_tool_instance, call:instance.created_at.isoformat, raise:HTTPException, func:list_workspace_instances(workspace_id: uuid.UUID, user_id, session) → list[dict], call:_get_workspace, call:session.execute, call:select(ToolInstance) .where(ToolInstance.workspace_id == workspace_id) .order_by, call:ToolInstance.created_at.desc, call:result.scalars().all, call:str, call:i.created_at.isoformat | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.schemas.tool, src.services.tool.instance_service
|
- workspace_instances.py | FastAPI router providing endpoints to create and list tool instances associated with a specific workspace. | exp: func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, user_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException, func:create_workspace_instance(workspace_id: uuid.UUID, data: CreateWorkspaceInstanceRequest, user_id, session) → dict, call:_get_workspace, call:session.get, call:CreateInstanceRequest, call:str, call:create_tool_instance, call:instance.created_at.isoformat, raise:HTTPException, func:list_workspace_instances(workspace_id: uuid.UUID, user_id, session) → list[dict], call:_get_workspace, call:session.execute, call:select(ToolInstance) .where(ToolInstance.workspace_id == workspace_id) .order_by, call:ToolInstance.created_at.desc, call:result.scalars().all, call:str, call:i.created_at.isoformat | dep: uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.dependencies, src.models, src.schemas.tool, src.services.tool.instance_service
|
||||||
- workspaces.py | Provides FastAPI CRUD endpoints for managing Git repository workspaces with nested and top-level URL structures, including listing, creating, updating, deleting, and syncing workspaces with instance count tracking. | exp: func:list_all_workspaces(user_id, session) → list[dict], call:select(func.count(ToolInstance.id)) .where(ToolInstance.workspace_id == Workspace.id) .correlate(Workspace) .scalar_subquery, call:func.count, call:session.execute, call:select( Workspace, GitRepository.name.label("repo_name"), GitRepository.project_id, GitRepository.ssh_key_id.label("repo_ssh_key_id"), instance_count.label("instance_count"), ) .join(GitRepository, Workspace.repo_id == GitRepository.id) .where(Workspace.user_id == user_id) .order_by, call:GitRepository.name.label, call:GitRepository.ssh_key_id.label, call:instance_count.label, call:Workspace.created_at.desc, call:result.all, call:str, call:ws.last_sync_at.isoformat, call:ws.created_at.isoformat, call:ws.updated_at.isoformat, func:delete_workspace_top_level(workspace_id: uuid.UUID, force, user_id, session) → dict, call:session.get, call:WorkspaceManager, call:manager.delete, call:session.commit, call:session.rollback, call:logger.error, raise:HTTPException, func:create_workspace_top_level(data: dict, user_id, session) → dict, call:data.get("repo_id", "").strip, call:uuid.UUID, call:session.get, call:data.get("name", "").strip, call:data.get("branch", "main").strip, call:WorkspaceManager, call:manager.create, call:session.add, call:session.commit, call:session.rollback, call:logger.error, call:session.refresh, call:str, call:workspace.created_at.isoformat, raise:HTTPException, func:list_workspaces(project_id: uuid.UUID, repo_id: uuid.UUID, user_id, session) → list[dict], call:_get_repo, call:select(func.count(ToolInstance.id)) .where(ToolInstance.workspace_id == Workspace.id) .correlate(Workspace) .scalar_subquery, call:func.count, call:session.execute, call:select( Workspace, instance_count.label("instance_count"), ) .where(Workspace.repo_id == repo_id) .order_by, call:instance_count.label, call:Workspace.created_at.desc, call:result.all, call:str, call:ws.last_sync_at.isoformat, call:ws.created_at.isoformat, call:ws.updated_at.isoformat, func:create_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_repo, call:data.get("name", "").strip, call:data.get("branch", "main").strip, call:WorkspaceManager, call:manager.create, call:session.add, call:session.commit, call:session.rollback, call:logger.error, call:session.refresh, call:str, call:workspace.created_at.isoformat, raise:HTTPException, func:get_workspace_detail(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:session.execute, call:select(func.count(ToolInstance.id)).where, call:func.count, call:result.scalar, call:str, call:workspace.last_sync_at.isoformat, call:workspace.created_at.isoformat, call:workspace.updated_at.isoformat, func:update_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:data.get("name", "").strip, call:data.get("branch", "").strip, call:session.commit, call:session.rollback, call:logger.error, call:str, raise:HTTPException, func:delete_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, force, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:WorkspaceManager, call:manager.delete, call:session.commit, call:session.rollback, call:logger.error, raise:HTTPException, func:sync_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:WorkspaceManager, call:manager.sync, call:session.commit, call:workspace.last_sync_at.isoformat, raise:HTTPException, func:_get_repo(session: AsyncSession, repo_id: uuid.UUID, project_id: uuid.UUID, user_id: uuid.UUID) → GitRepository, call:session.execute, call:select(GitRepository) .where( GitRepository.id == repo_id, GitRepository.project_id == project_id, ) .options, call:selectinload, call:result.scalar_one_or_none, raise:HTTPException, func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, repo_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException | dep: logging, uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, sqlalchemy.orm, src.auth.dependencies, src.models, src.services.shared.workspace_manager
|
- workspaces.py | FastAPI router providing CRUD endpoints for workspace management with nested and top-level URL structures, including listing, creating, updating, deleting, and syncing workspaces tied to Git repositories. | exp: func:list_all_workspaces(user_id, session) → list[dict], call:select(func.count(ToolInstance.id)) .where(ToolInstance.workspace_id == Workspace.id) .correlate(Workspace) .scalar_subquery, call:func.count, call:session.execute, call:select( Workspace, GitRepository, instance_count.label("instance_count"), ) .join(GitRepository, Workspace.repo_id == GitRepository.id) .options(selectinload(GitRepository.project)) .where(Workspace.user_id == user_id) .order_by, call:instance_count.label, call:selectinload, call:Workspace.created_at.desc, call:result.all, call:str, call:ws.last_sync_at.isoformat, call:ws.created_at.isoformat, call:ws.updated_at.isoformat, func:delete_workspace_top_level(workspace_id: uuid.UUID, force, user_id, session) → dict, call:session.get, call:WorkspaceManager, call:manager.delete, call:session.commit, call:session.rollback, call:logger.error, raise:HTTPException, func:create_workspace_top_level(data: dict, user_id, session) → dict, call:data.get("repo_id", "").strip, call:uuid.UUID, call:session.get, call:data.get("name", "").strip, call:data.get("branch", "main").strip, call:WorkspaceManager, call:manager.create, call:session.add, call:session.commit, call:session.rollback, call:logger.error, call:session.refresh, call:str, call:workspace.created_at.isoformat, raise:HTTPException, func:list_workspaces(project_id: uuid.UUID, repo_id: uuid.UUID, user_id, session) → list[dict], call:_get_repo, call:select(func.count(ToolInstance.id)) .where(ToolInstance.workspace_id == Workspace.id) .correlate(Workspace) .scalar_subquery, call:func.count, call:session.execute, call:select( Workspace, instance_count.label("instance_count"), ) .where(Workspace.repo_id == repo_id) .order_by, call:instance_count.label, call:Workspace.created_at.desc, call:result.all, call:str, call:ws.last_sync_at.isoformat, call:ws.created_at.isoformat, call:ws.updated_at.isoformat, func:create_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_repo, call:data.get("name", "").strip, call:data.get("branch", "main").strip, call:WorkspaceManager, call:manager.create, call:session.add, call:session.commit, call:session.rollback, call:logger.error, call:session.refresh, call:str, call:workspace.created_at.isoformat, raise:HTTPException, func:get_workspace_detail(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:session.execute, call:select(func.count(ToolInstance.id)).where, call:func.count, call:result.scalar, call:str, call:workspace.last_sync_at.isoformat, call:workspace.created_at.isoformat, call:workspace.updated_at.isoformat, func:update_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, data: dict, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:data.get("name", "").strip, call:data.get("branch", "").strip, call:session.commit, call:session.rollback, call:logger.error, call:str, raise:HTTPException, func:delete_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, force, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:WorkspaceManager, call:manager.delete, call:session.commit, call:session.rollback, call:logger.error, raise:HTTPException, func:sync_workspace(project_id: uuid.UUID, repo_id: uuid.UUID, workspace_id: uuid.UUID, user_id, session) → dict, call:_get_repo, call:_get_workspace, call:WorkspaceManager, call:manager.sync, call:session.commit, call:workspace.last_sync_at.isoformat, raise:HTTPException, func:_get_repo(session: AsyncSession, repo_id: uuid.UUID, project_id: uuid.UUID, user_id: uuid.UUID) → GitRepository, call:session.execute, call:select(GitRepository) .where( GitRepository.id == repo_id, GitRepository.project_id == project_id, ) .options, call:selectinload, call:result.scalar_one_or_none, raise:HTTPException, func:_get_workspace(session: AsyncSession, workspace_id: uuid.UUID, repo_id: uuid.UUID) → Workspace, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, raise:HTTPException | dep: logging, uuid, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, sqlalchemy.orm, src.auth.dependencies, src.models, src.services.shared.workspace_manager
|
||||||
## arch
|
## arch
|
||||||
Modular router-based architecture using FastAPI with domain-driven decomposition into separate modules (files, git, instances, workspaces), nested URL routing for hierarchical resources, and optional git integration for file operations.
|
Modular FastAPI router pattern with domain-driven separation (files, git, instances, workspaces) using nested URL structures and git-backed workspace synchronization.
|
||||||
## tags
|
## tags
|
||||||
workspace, get, raise:httpexception, call:, at.isoformat, git, call:select, call:data.get
|
workspace, get, raise:httpexception, call:, at.isoformat, git, call:select, call:data.get
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -36,12 +36,11 @@ async def list_all_workspaces(
|
|||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(
|
select(
|
||||||
Workspace,
|
Workspace,
|
||||||
GitRepository.name.label("repo_name"),
|
GitRepository,
|
||||||
GitRepository.project_id,
|
|
||||||
GitRepository.ssh_key_id.label("repo_ssh_key_id"),
|
|
||||||
instance_count.label("instance_count"),
|
instance_count.label("instance_count"),
|
||||||
)
|
)
|
||||||
.join(GitRepository, Workspace.repo_id == GitRepository.id)
|
.join(GitRepository, Workspace.repo_id == GitRepository.id)
|
||||||
|
.options(selectinload(GitRepository.project))
|
||||||
.where(Workspace.user_id == user_id)
|
.where(Workspace.user_id == user_id)
|
||||||
.order_by(Workspace.created_at.desc())
|
.order_by(Workspace.created_at.desc())
|
||||||
)
|
)
|
||||||
@@ -51,11 +50,11 @@ async def list_all_workspaces(
|
|||||||
{
|
{
|
||||||
"id": str(ws.id),
|
"id": str(ws.id),
|
||||||
"name": ws.name,
|
"name": ws.name,
|
||||||
"repo_id": str(ws.repo_id),
|
"repo_id": str(repo.id),
|
||||||
"repo_name": repo_name or "",
|
"repo_name": repo.name or "",
|
||||||
"repo_ssh_key_id": str(ssh_key_id) if ssh_key_id else None,
|
"repo_ssh_key_id": str(repo.ssh_key_id) if repo.ssh_key_id else None,
|
||||||
"project_id": str(project_id) if project_id else "",
|
"project_id": str(repo.project_id) if repo.project_id else "",
|
||||||
"project_name": "",
|
"project_name": repo.project.name if repo.project else "",
|
||||||
"user_id": str(ws.user_id),
|
"user_id": str(ws.user_id),
|
||||||
"branch": ws.branch,
|
"branch": ws.branch,
|
||||||
"path": ws.path,
|
"path": ws.path,
|
||||||
@@ -65,7 +64,7 @@ async def list_all_workspaces(
|
|||||||
"updated_at": ws.updated_at.isoformat() if ws.updated_at else None,
|
"updated_at": ws.updated_at.isoformat() if ws.updated_at else None,
|
||||||
"instance_count": count or 0,
|
"instance_count": count or 0,
|
||||||
}
|
}
|
||||||
for ws, repo_name, project_id, ssh_key_id, count in rows
|
for ws, repo, count in rows
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src/seeds
|
dir: apps/api/src/seeds
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database seeding utilities for populating initial data, specifically built-in tool types.
|
Provides database seeding utilities for initializing built-in tool type configurations in the API application.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/src/.pi-map.index.md
|
index: apps/api/src/.pi-map.index.md
|
||||||
map: apps/api/src/.pi-map.md
|
map: apps/api/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ dir: apps/api/src/seeds
|
|||||||
index: apps/api/src/seeds/.pi-map.index.md
|
index: apps/api/src/seeds/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database seeding utilities for populating initial data, specifically built-in tool types.
|
Provides database seeding utilities for initializing built-in tool type configurations in the API application.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Marks the directory as a Python package for database seeding utilities.
|
- __init__.py | Marks the directory as a Python package for database seeding utilities.
|
||||||
- builtin_tool_types.py | Seeds built-in Docker Compose-based tool types (VS Code Server, Jupyter Notebook, OpenCode) into a database with idempotent create-or-update logic. | exp: func:_table_exists(session, table_name: str) → bool, call:session.execute, call:text, call:result.scalar, func:seed_builtin_tool_types(), call:SessionLocal, call:_table_exists, call:logger.warning, call:session.scalar, call:select(ToolType).where, call:ToolType, call:tool_data.get, call:session.add, call:logger.info, call:session.commit | dep: logging, sqlalchemy, src.database, src.models
|
- builtin_tool_types.py | Seeds built-in tool types (code-server, jupyter-notebook, opencode) into a database with upsert logic, creating or updating Docker Compose-based development environment templates. | exp: func:_table_exists(session, table_name: str) → bool, call:session.execute, call:text, call:result.scalar, func:seed_builtin_tool_types(), call:SessionLocal, call:_table_exists, call:logger.warning, call:session.scalar, call:select(ToolType).where, call:ToolType, call:tool_data.get, call:session.add, call:logger.info, call:session.commit | dep: logging, sqlalchemy, src.database, src.models
|
||||||
## arch
|
## arch
|
||||||
Simple procedural seeding scripts with idempotent create-or-update logic for Docker Compose-based tool definitions.
|
Simple procedural seeding script using SQLAlchemy upsert operations to populate reference data for containerized development environment templates.
|
||||||
## tags
|
## tags
|
||||||
tool, types, table, exists, builtin, call:tool, database, init
|
tool, types, table, exists, builtin, call:tool, database, init
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -103,11 +103,8 @@ services:
|
|||||||
image: node:20-slim
|
image: node:20-slim
|
||||||
container_name: {{TOOL_NAME}}
|
container_name: {{TOOL_NAME}}
|
||||||
working_dir: /workspace
|
working_dir: /workspace
|
||||||
environment:
|
|
||||||
- HOME=/tmp
|
|
||||||
volumes:
|
volumes:
|
||||||
- {{REPO_PATH}}:/workspace
|
- {{REPO_PATH}}:/workspace
|
||||||
- opencode_home:/tmp
|
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
command: >
|
command: >
|
||||||
@@ -125,10 +122,7 @@ services:
|
|||||||
exec tail -f /dev/null"
|
exec tail -f /dev/null"
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped""",
|
||||||
|
|
||||||
volumes:
|
|
||||||
opencode_home:""",
|
|
||||||
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
"required_variables": ["REPO_PATH", "TOOL_NAME"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src/services
|
dir: apps/api/src/services
|
||||||
|
|
||||||
## role
|
## role
|
||||||
This directory is intended to contain service-layer business logic modules for the API application.
|
Marks the services directory as a Python package for business logic layer components.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/src/.pi-map.index.md
|
index: apps/api/src/.pi-map.index.md
|
||||||
map: apps/api/src/.pi-map.md
|
map: apps/api/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ dir: apps/api/src/services
|
|||||||
index: apps/api/src/services/.pi-map.index.md
|
index: apps/api/src/services/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
This directory is intended to contain service-layer business logic modules for the API application.
|
Provides a Python package namespace for organizing service-layer modules in the API application.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Empty file with no functionality
|
- __init__.py | Empty file with no functionality
|
||||||
## arch
|
## arch
|
||||||
Empty package placeholder following standard Python package structure with no implemented architecture yet.
|
Standard Python package structure using __init__.py for directory-based module organization, following conventional layered architecture patterns.
|
||||||
## tags
|
## tags
|
||||||
init, empty, functionality
|
init, empty, functionality
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src/services/config
|
dir: apps/api/src/services/config
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides configuration profile resolution and management services for the API, handling hierarchical config merging, validation, and access control.
|
Provides configuration profile management with recursive resolution, validation, and access control for container environment setup.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/src/services/.pi-map.index.md
|
index: apps/api/src/services/.pi-map.index.md
|
||||||
map: apps/api/src/services/.pi-map.md
|
map: apps/api/src/services/.pi-map.md
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ dir: apps/api/src/services/config
|
|||||||
index: apps/api/src/services/config/.pi-map.index.md
|
index: apps/api/src/services/config/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides configuration profile resolution and management services for the API, handling hierarchical config merging, validation, and access control.
|
Provides configuration profile management with recursive resolution, validation, and access control for container environment setup.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Re-exports configuration profile resolution components from a subpackage to provide a unified public API. | dep: src.services.config.config_profile_resolver
|
- __init__.py | Re-exports configuration profile resolution components from a subpackage to provide a unified public API. | dep: src.services.config.config_profile_resolver
|
||||||
- config_profile_resolver.py | Resolves configuration profiles recursively with ordered include merging, cycle detection, and deterministic override rules for environment variables, runtime hints, files, mounts, and git mounts. | exp: class:ConfigProfileCycleError, class:ConfigProfileNotFoundError, class:ResolvedMount, class:ResolvedProfile, func:_detect_cycle(profile_id: uuid.UUID, visited: set[uuid.UUID], path: list[uuid.UUID]) → bool, func:_merge_env_vars(base: dict[str, str], overlay: dict[str, str], overrides: dict[str, str], source_name: str) → dict[str, str], call:dict, call:overlay.items, func:_merge_runtime_hints(base: dict[str, Any], overlay: dict[str, Any], overrides: dict[str, str], source_name: str) → dict[str, Any], call:dict, call:overlay.items, func:_merge_files(base: dict[str, str], overlay: dict[str, str], overrides: dict[str, str], source_name: str) → dict[str, str], call:dict, call:overlay.items, func:_merge_mounts(base: dict[str, ResolvedMount], overlay: list[dict[str, Any]], overrides: dict[str, str], source_name: str) → dict[str, ResolvedMount], call:dict, call:mount_data.get, call:files.items, call:ResolvedMount, func:_merge_git_mounts(base: list[dict[str, Any]], overlay: list[dict[str, Any]], source_name: str) → list[dict[str, Any]], call:list, call:enumerate, call:_normalize_git_mount_entry, call:dict, call:m.get, call:mount.get, call:existing.get, call:existing["mappings"].append, call:existing_sources.add, call:len, call:result.append, func:_normalize_git_mount_entry(entry: dict[str, Any]) → dict[str, Any], call:dict, call:entry.get, call:entry.pop, func:_resolve_profile_recursive(session: AsyncSession, profile_id: uuid.UUID, visited: set[uuid.UUID], path: list[uuid.UUID]) → ResolvedProfile, call:_detect_cycle, call:" -> ".join, call:str, call:session.get, call:ResolvedProfile, call:select(ConfigProfileInclude) .where(ConfigProfileInclude.profile_id == profile_id) .order_by, call:session.execute, call:include_result.scalars().all, call:_resolve_profile_recursive, call:result.included_profiles.append, call:_merge_env_vars, call:_merge_runtime_hints, call:_merge_files, call:_merge_mounts, call:included.mounts.values, call:_merge_git_mounts, raise:ConfigProfileCycleError, raise:ConfigProfileNotFoundError, func:resolve_profile(session: AsyncSession, profile_id: uuid.UUID) → ResolvedProfile, call:_resolve_profile_recursive, call:set, func:check_include_cycle(session: AsyncSession, profile_id: uuid.UUID, new_include_id) → list[uuid.UUID] | None, call:select(ConfigProfileInclude) .where(ConfigProfileInclude.profile_id == current_id) .order_by, call:session.execute, call:include_result.scalars().all, call:_check_from, call:set, call:len, func:_check_from(current_id: uuid.UUID, target_id: uuid.UUID, visited: set[uuid.UUID], path: list[uuid.UUID]) → list[uuid.UUID] | None, call:select(ConfigProfileInclude) .where(ConfigProfileInclude.profile_id == current_id) .order_by, call:session.execute, call:include_result.scalars().all, call:_check_from, func:apply_resolved_profile(instance_dir: str, resolved: ResolvedProfile, home_dir) → tuple[dict[str, str], dict[str, str], list[dict], dict[str, Any]], call:Path, call:dict, call:files.items, call:full_path.resolve().relative_to, call:instance_path.resolve, call:logger.warning, call:full_path.parent.mkdir, call:full_path.write_text, call:resolved.mounts.values, call:expand_container_path, call:expanded_target.lstrip("/").replace, call:mount_dir.mkdir, call:mount.files.items, call:mount_dir.resolve, call:os.path.join, call:volume_mounts.append, call:str, func:expand_container_path(path: str, home_dir: str) → str, call:path.startswith, call:os.path.join, func:resolved_profile_to_dict(resolved: ResolvedProfile) → dict[str, Any], call:str, call:resolved.mounts.values | dep: logging, os, uuid, dataclasses, typing, sqlalchemy, sqlalchemy.ext.asyncio, src.models, pathlib
|
- config_profile_resolver.py | Resolves configuration profiles recursively with ordered includes, deterministic merge rules, and cycle protection for container environment setup. | exp: class:ConfigProfileCycleError, class:ConfigProfileNotFoundError, class:ResolvedMount, class:ResolvedProfile, func:_detect_cycle(profile_id: uuid.UUID, visited: set[uuid.UUID], path: list[uuid.UUID]) → bool, func:_merge_env_vars(base: dict[str, str], overlay: dict[str, str], overrides: dict[str, str], source_name: str) → dict[str, str], call:dict, call:overlay.items, func:_merge_runtime_hints(base: dict[str, Any], overlay: dict[str, Any], overrides: dict[str, str], source_name: str) → dict[str, Any], call:dict, call:overlay.items, func:_merge_files(base: dict[str, str], overlay: dict[str, str], overrides: dict[str, str], source_name: str) → dict[str, str], call:dict, call:overlay.items, func:_merge_mounts(base: dict[str, ResolvedMount], overlay: list[dict[str, Any]], overrides: dict[str, str], source_name: str) → dict[str, ResolvedMount], call:dict, call:mount_data.get, call:files.items, call:ResolvedMount, func:_merge_git_mounts(base: list[dict[str, Any]], overlay: list[dict[str, Any]], source_name: str) → list[dict[str, Any]], call:list, call:enumerate, call:_normalize_git_mount_entry, call:dict, call:m.get, call:mount.get, call:existing.get, call:existing["mappings"].append, call:existing_sources.add, call:len, call:result.append, func:_normalize_git_mount_entry(entry: dict[str, Any]) → dict[str, Any], call:dict, call:entry.get, call:entry.pop, func:_resolve_profile_recursive(session: AsyncSession, profile_id: uuid.UUID, visited: set[uuid.UUID], path: list[uuid.UUID]) → ResolvedProfile, call:_detect_cycle, call:" -> ".join, call:str, call:session.get, call:ResolvedProfile, call:select(ConfigProfileInclude) .where(ConfigProfileInclude.profile_id == profile_id) .order_by, call:session.execute, call:include_result.scalars().all, call:_resolve_profile_recursive, call:result.included_profiles.append, call:_merge_env_vars, call:_merge_runtime_hints, call:_merge_files, call:_merge_mounts, call:included.mounts.values, call:_merge_git_mounts, raise:ConfigProfileCycleError, raise:ConfigProfileNotFoundError, func:resolve_profile(session: AsyncSession, profile_id: uuid.UUID) → ResolvedProfile, call:_resolve_profile_recursive, call:set, func:check_include_cycle(session: AsyncSession, profile_id: uuid.UUID, new_include_id) → list[uuid.UUID] | None, call:select(ConfigProfileInclude) .where(ConfigProfileInclude.profile_id == current_id) .order_by, call:session.execute, call:include_result.scalars().all, call:_check_from, call:set, call:len, func:_check_from(current_id: uuid.UUID, target_id: uuid.UUID, visited: set[uuid.UUID], path: list[uuid.UUID]) → list[uuid.UUID] | None, call:select(ConfigProfileInclude) .where(ConfigProfileInclude.profile_id == current_id) .order_by, call:session.execute, call:include_result.scalars().all, call:_check_from, func:apply_resolved_profile(instance_dir: str, resolved: ResolvedProfile, home_dir) → tuple[dict[str, str], dict[str, str], list[dict], dict[str, Any]], call:Path, call:dict, call:files.items, call:full_path.resolve().relative_to, call:instance_path.resolve, call:logger.warning, call:full_path.parent.mkdir, call:full_path.write_text, call:resolved.mounts.values, call:os.path.normpath, call:expand_container_path, call:expanded_target.lstrip("/").replace, call:mount_dir.mkdir, call:mount.files.items, call:mount_dir.resolve, call:volume_mounts.append, call:str, func:expand_container_path(path: str, home_dir: str) → str, call:path.startswith, call:os.path.join, func:resolved_profile_to_dict(resolved: ResolvedProfile) → dict[str, Any], call:str, call:resolved.mounts.values | dep: logging, os, uuid, dataclasses, typing, sqlalchemy, sqlalchemy.ext.asyncio, src.models, pathlib
|
||||||
- crud_service.py | Provides CRUD operations for configuration profiles including validation, access control, size limits, and include cycle detection. | exp: func:calculate_profile_size(data: dict) → int, call:data.get("env_vars", {}).items, call:len, call:key.encode, call:str(value).encode, call:data.get("runtime_hints", {}).items, call:str(mount.get("target", "")).encode, call:str(mount.get("mode", "")).encode, call:mount.get("files", {}).items, call:path.encode, call:content.encode, call:data.get("files", {}).items, func:get_profile_with_includes(session: AsyncSession, profile_id: uuid.UUID) → ConfigProfile | None, call:session.execute, call:select(ConfigProfile) .where(ConfigProfile.id == profile_id) .options, call:selectinload, call:result.scalar_one_or_none, func:check_access(session: AsyncSession, user_id: uuid.UUID, project_id, tool_type_id) → None, call:session.get, raise:HTTPException, func:validate_git_mounts(session: AsyncSession, user_id: uuid.UUID, git_mounts: list[Any], project_id) → None, call:mount.get, call:remote_url.startswith, raise:HTTPException, func:profile_to_response(profile: ConfigProfile, includes) → dict, call:str, call:profile.created_at.isoformat, call:profile.updated_at.isoformat, func:get_or_create_user_config(session: AsyncSession, user_id: uuid.UUID) → UserConfig, call:session.execute, call:select(UserConfig).where, call:result.scalar_one_or_none, call:UserConfig, call:session.add, func:validate_default_profiles(session: AsyncSession, user_id: uuid.UUID, default_profiles: dict[str, str]) → None, call:default_profiles.items, call:uuid.UUID, call:session.get, raise:HTTPException, func:create_profile(session: AsyncSession, user_id: uuid.UUID, data: Any) → ConfigProfile, call:session.execute, call:select(ConfigProfile) .where( ConfigProfile.user_id == user_id, ConfigProfile.name == data.name, ) .options, call:selectinload, call:existing.scalar_one_or_none, call:uuid.UUID, call:check_access, call:m.model_dump, call:hasattr, call:validate_git_mounts, call:calculate_profile_size, call:data.model_dump, call:ConfigProfile, call:session.add, call:session.commit, call:select(ConfigProfile) .where(ConfigProfile.id == profile.id) .options, call:result.scalar_one, raise:HTTPException, func:update_profile(session: AsyncSession, profile: ConfigProfile, data: Any) → ConfigProfile, call:data.model_dump, call:session.execute, call:select(ConfigProfile).where, call:existing.scalar_one_or_none, call:uuid.UUID, call:check_access, call:m.model_dump, call:hasattr, call:validate_git_mounts, call:profile_to_response, call:calculate_profile_size, call:update_data.items, call:isinstance, call:setattr, call:session.commit, call:select(ConfigProfile) .where(ConfigProfile.id == profile.id) .options, call:selectinload, call:result.scalar_one, raise:HTTPException, func:update_includes(session: AsyncSession, profile: ConfigProfile, included_ids: list[uuid.UUID], user_id: uuid.UUID) → ConfigProfile, call:session.get, call:check_include_cycle, call:" -> ".join, call:str, call:session.execute, call:select(ConfigProfileInclude).where, call:result.scalars().all, call:session.delete, call:session.flush, call:enumerate, call:ConfigProfileInclude, call:session.add, call:session.commit, call:select(ConfigProfile).where, call:result.scalar_one, raise:HTTPException | dep: uuid, typing, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, sqlalchemy.orm, src.models, src.models.project, src.services.config.config_profile_resolver
|
- crud_service.py | Provides CRUD operations for configuration profiles including validation, access control, size limits, and include cycle detection. | exp: func:calculate_profile_size(data: dict) → int, call:data.get("env_vars", {}).items, call:len, call:key.encode, call:str(value).encode, call:data.get("runtime_hints", {}).items, call:str(mount.get("target", "")).encode, call:str(mount.get("mode", "")).encode, call:mount.get("files", {}).items, call:path.encode, call:content.encode, call:data.get("files", {}).items, func:get_profile_with_includes(session: AsyncSession, profile_id: uuid.UUID) → ConfigProfile | None, call:session.execute, call:select(ConfigProfile) .where(ConfigProfile.id == profile_id) .options, call:selectinload, call:result.scalar_one_or_none, func:check_access(session: AsyncSession, user_id: uuid.UUID, project_id, tool_type_id) → None, call:session.get, raise:HTTPException, func:validate_git_mounts(session: AsyncSession, user_id: uuid.UUID, git_mounts: list[Any], project_id) → None, call:mount.get, call:remote_url.startswith, raise:HTTPException, func:profile_to_response(profile: ConfigProfile, includes) → dict, call:str, call:profile.created_at.isoformat, call:profile.updated_at.isoformat, func:get_or_create_user_config(session: AsyncSession, user_id: uuid.UUID) → UserConfig, call:session.execute, call:select(UserConfig).where, call:result.scalar_one_or_none, call:UserConfig, call:session.add, func:validate_default_profiles(session: AsyncSession, user_id: uuid.UUID, default_profiles: dict[str, str]) → None, call:default_profiles.items, call:uuid.UUID, call:session.get, raise:HTTPException, func:create_profile(session: AsyncSession, user_id: uuid.UUID, data: Any) → ConfigProfile, call:session.execute, call:select(ConfigProfile) .where( ConfigProfile.user_id == user_id, ConfigProfile.name == data.name, ) .options, call:selectinload, call:existing.scalar_one_or_none, call:uuid.UUID, call:check_access, call:m.model_dump, call:hasattr, call:validate_git_mounts, call:calculate_profile_size, call:data.model_dump, call:ConfigProfile, call:session.add, call:session.commit, call:select(ConfigProfile) .where(ConfigProfile.id == profile.id) .options, call:result.scalar_one, raise:HTTPException, func:update_profile(session: AsyncSession, profile: ConfigProfile, data: Any) → ConfigProfile, call:data.model_dump, call:session.execute, call:select(ConfigProfile).where, call:existing.scalar_one_or_none, call:uuid.UUID, call:check_access, call:m.model_dump, call:hasattr, call:validate_git_mounts, call:profile_to_response, call:calculate_profile_size, call:update_data.items, call:isinstance, call:setattr, call:session.commit, call:select(ConfigProfile) .where(ConfigProfile.id == profile.id) .options, call:selectinload, call:result.scalar_one, raise:HTTPException, func:update_includes(session: AsyncSession, profile: ConfigProfile, included_ids: list[uuid.UUID], user_id: uuid.UUID) → ConfigProfile, call:session.get, call:check_include_cycle, call:" -> ".join, call:str, call:session.execute, call:select(ConfigProfileInclude).where, call:result.scalars().all, call:session.delete, call:session.flush, call:enumerate, call:ConfigProfileInclude, call:session.add, call:session.commit, call:select(ConfigProfile).where, call:result.scalar_one, raise:HTTPException | dep: uuid, typing, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, sqlalchemy.orm, src.models, src.models.project, src.services.config.config_profile_resolver
|
||||||
- resolver_service.py | Resolves default configuration profiles for users/projects/tools and validates Git URLs with SSH key support. | exp: func:resolve_default_profile(session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, tool_type_id: uuid.UUID) → dict, call:select(ConfigProfile) .where(ConfigProfile.user_id == user_id) .where( (ConfigProfile.project_id.is_(None) & ConfigProfile.tool_type_id.is_(None)) | (ConfigProfile.project_id == project_id) | (ConfigProfile.tool_type_id == tool_type_id) | ( (ConfigProfile.project_id == project_id) & (ConfigProfile.tool_type_id == tool_type_id) ) ) .order_by, call:ConfigProfile.project_id.is_, call:ConfigProfile.tool_type_id.is_, call:session.execute, call:result.scalars().all, call:str, func:validate_git_url(session: AsyncSession, current_user_id: uuid.UUID, url: str, ssh_key_id: str | None) → ValidateGitUrlResponse, call:parse_git_url, call:url.strip, call:parse_result.get, call:ValidateGitUrlResponse, call:uuid.UUID, call:session.get, call:_get_fernet, call:fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode, call:ssh_key.private_key_encrypted.encode, call:tempfile.mkstemp, call:os.write, call:private_key.encode, call:os.close, call:os.chmod, call:subprocess.run, call:os.path.exists, call:os.unlink, call:result.stderr.strip, call:stderr.lower, call:result.stdout.strip().split, call:line.strip, call:line.split, call:len, call:ref.startswith, call:branches.append | dep: logging, os, subprocess, uuid, typing, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.models, src.services.shared.ssh_keys, src.utils.git_url_parser, src.schemas.config, tempfile
|
- resolver_service.py | Resolves default configuration profiles for users/projects/tools and validates Git URLs with SSH key support. | exp: func:resolve_default_profile(session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, tool_type_id: uuid.UUID) → dict, call:select(ConfigProfile) .where(ConfigProfile.user_id == user_id) .where( (ConfigProfile.project_id.is_(None) & ConfigProfile.tool_type_id.is_(None)) | (ConfigProfile.project_id == project_id) | (ConfigProfile.tool_type_id == tool_type_id) | ( (ConfigProfile.project_id == project_id) & (ConfigProfile.tool_type_id == tool_type_id) ) ) .order_by, call:ConfigProfile.project_id.is_, call:ConfigProfile.tool_type_id.is_, call:session.execute, call:result.scalars().all, call:str, func:validate_git_url(session: AsyncSession, current_user_id: uuid.UUID, url: str, ssh_key_id: str | None) → ValidateGitUrlResponse, call:parse_git_url, call:url.strip, call:parse_result.get, call:ValidateGitUrlResponse, call:uuid.UUID, call:session.get, call:_get_fernet, call:fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode, call:ssh_key.private_key_encrypted.encode, call:tempfile.mkstemp, call:os.write, call:private_key.encode, call:os.close, call:os.chmod, call:subprocess.run, call:os.path.exists, call:os.unlink, call:result.stderr.strip, call:stderr.lower, call:result.stdout.strip().split, call:line.strip, call:line.split, call:len, call:ref.startswith, call:branches.append | dep: logging, os, subprocess, uuid, typing, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.models, src.services.shared.ssh_keys, src.utils.git_url_parser, src.schemas.config, tempfile
|
||||||
## arch
|
## arch
|
||||||
Service-layer pattern with separation of concerns: dedicated resolver (recursive merge with cycle detection), CRUD operations (validation/ACLs), and default resolution (user/project/tool scoping); supports multiple config sources with deterministic override precedence.
|
Layered service architecture with separation between resolution logic (recursive merge with cycle detection), CRUD operations (validation, access control, size limits), and default resolution (Git/SSH integration), exposed through a unified facade pattern via __init__.py.
|
||||||
## tags
|
## tags
|
||||||
profile, call:, call:select, git, merge, call:str, call:session.execute, mounts
|
profile, call:, call:select, merge, git, call:str, call:session.execute, cycle
|
||||||
## symbols
|
## symbols
|
||||||
- ConfigProfileCycleError
|
- ConfigProfileCycleError
|
||||||
- ConfigProfileNotFoundError
|
- ConfigProfileNotFoundError
|
||||||
|
|||||||
@@ -476,9 +476,19 @@ def apply_resolved_profile(
|
|||||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
full_path.write_text(content)
|
full_path.write_text(content)
|
||||||
|
|
||||||
# Stage mount files and prepare volume mounts
|
# Stage mount directories and prepare directory-level volume mounts.
|
||||||
|
# Each ResolvedMount targets a container directory; we stage all of its
|
||||||
|
# files under a single host directory and bind-mount that directory. This
|
||||||
|
# keeps the target directory writable by the container user, instead of
|
||||||
|
# having Docker create a root-owned parent directory when only individual
|
||||||
|
# files are mounted.
|
||||||
for mount in resolved.mounts.values():
|
for mount in resolved.mounts.values():
|
||||||
expanded_target = expand_container_path(mount.target, home_dir)
|
if not mount.files:
|
||||||
|
continue
|
||||||
|
|
||||||
|
expanded_target = os.path.normpath(
|
||||||
|
expand_container_path(mount.target, home_dir)
|
||||||
|
)
|
||||||
mount_dir = (
|
mount_dir = (
|
||||||
instance_path / "mounts" / expanded_target.lstrip("/").replace("/", "_")
|
instance_path / "mounts" / expanded_target.lstrip("/").replace("/", "_")
|
||||||
)
|
)
|
||||||
@@ -494,14 +504,12 @@ def apply_resolved_profile(
|
|||||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
full_path.write_text(content)
|
full_path.write_text(content)
|
||||||
|
|
||||||
# Mount each file individually so sibling files from other mounts
|
|
||||||
# (e.g. git repo directories) are preserved.
|
|
||||||
file_target = os.path.join(expanded_target, file_path)
|
|
||||||
volume_mounts.append(
|
volume_mounts.append(
|
||||||
{
|
{
|
||||||
"source": str(full_path),
|
"source": str(mount_dir),
|
||||||
"target": file_target,
|
"target": expanded_target,
|
||||||
"type": "bind",
|
"type": "bind",
|
||||||
|
"readonly": mount.mode in ("ro", "readonly"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/src/services/tool
|
dir: apps/api/src/services/tool
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides Docker container lifecycle management for tool instances with git repository integration and configuration validation.
|
Provides backend infrastructure for provisioning and managing isolated development tool instances with their dependencies and network access.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/src/services/.pi-map.index.md
|
index: apps/api/src/services/.pi-map.index.md
|
||||||
map: apps/api/src/services/.pi-map.md
|
map: apps/api/src/services/.pi-map.md
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -583,10 +583,13 @@ def modify_compose_file(
|
|||||||
source = vol.get("source", "")
|
source = vol.get("source", "")
|
||||||
target = expand_container_path(vol.get("target", ""), home_dir)
|
target = expand_container_path(vol.get("target", ""), home_dir)
|
||||||
vol_type = vol.get("type", "bind")
|
vol_type = vol.get("type", "bind")
|
||||||
|
readonly = ":ro" if vol.get("readonly") else ""
|
||||||
if vol_type == "bind":
|
if vol_type == "bind":
|
||||||
service_config["volumes"].append(f"{source}:{target}")
|
service_config["volumes"].append(f"{source}:{target}{readonly}")
|
||||||
else:
|
else:
|
||||||
service_config["volumes"].append(f"{source}:{target}:{vol_type}")
|
service_config["volumes"].append(
|
||||||
|
f"{source}:{target}:{vol_type}{readonly}"
|
||||||
|
)
|
||||||
|
|
||||||
# Sort volumes so parent paths come before child paths
|
# Sort volumes so parent paths come before child paths
|
||||||
if service_config.get("volumes"):
|
if service_config.get("volumes"):
|
||||||
@@ -792,8 +795,10 @@ async def prepare_manifest_instance(
|
|||||||
from src.models import ToolDefinitionManifest
|
from src.models import ToolDefinitionManifest
|
||||||
|
|
||||||
tool_type = await session.get(ToolType, instance.tool_type_id)
|
tool_type = await session.get(ToolType, instance.tool_type_id)
|
||||||
manifest_def = await session.get(ToolDefinitionManifest, tool_type.manifest_id)
|
if not tool_type:
|
||||||
|
raise RuntimeError(f"Tool type not found for instance {instance.id}")
|
||||||
|
|
||||||
|
manifest_def = await session.get(ToolDefinitionManifest, tool_type.manifest_id)
|
||||||
if not manifest_def:
|
if not manifest_def:
|
||||||
raise RuntimeError(f"Manifest not found for tool type {tool_type.id}")
|
raise RuntimeError(f"Manifest not found for tool type {tool_type.id}")
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/tests
|
dir: apps/api/tests
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides shared test infrastructure and fixtures for API integration testing.
|
Provides shared test infrastructure and fixtures for the FastAPI API application.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/.pi-map.index.md
|
index: apps/api/.pi-map.index.md
|
||||||
map: apps/api/.pi-map.md
|
map: apps/api/.pi-map.md
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ dir: apps/api/tests
|
|||||||
index: apps/api/tests/.pi-map.index.md
|
index: apps/api/tests/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides shared test infrastructure and fixtures for API integration testing.
|
Provides shared test infrastructure and fixtures for the FastAPI API application.
|
||||||
## files
|
## files
|
||||||
- conftest.py | Provides shared pytest fixtures for testing a FastAPI application with async SQLite database, authenticated clients, and test data setup. | exp: func:test_client() → Generator[TestClient, None, None], call:create_async_engine, call:engine.begin, call:conn.run_sync, call:asyncio.run, call:init_db, call:async_sessionmaker, call:patch, call:TestClient, call:app.dependency_overrides.pop, call:engine.dispose, func:init_db(), call:engine.begin, call:conn.run_sync, func:override_get_db_session() → AsyncGenerator[AsyncSession, None], call:async_sessionmaker, func:db_session(test_client) → AsyncGenerator[AsyncSession, None], call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:gen.aclose, call:create_async_engine, call:engine.begin, call:conn.run_sync, call:async_sessionmaker, call:engine.dispose, func:authenticated_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_test_user, call:create_session_cookie, call:test_client.cookies.set, func:create_test_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, func:test_project_and_repo(authenticated_client) → tuple[str, str], call:uuid.uuid4, call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, call:asyncio.run, call:get_user_id, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, call:create_project_and_repo, call:str, raise:RuntimeError, func:get_user_id(), call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, func:create_project_and_repo(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, func:admin_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_admin_user, call:create_session_cookie, call:test_client.cookies.set, func:create_admin_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose | dep: asyncio, os, typing, unittest.mock, pytest, pytest_asyncio, fastapi.testclient, sqlalchemy.ext.asyncio, src.config, src.models.base, src.main, src.auth.dependencies, uuid, src.auth.session, src.models.user.user, src.models.project.project, src.models.project.git_repository, fastapi, sqlalchemy, aiosqlite, src.models, src.auth
|
- conftest.py | Provides shared pytest fixtures for testing a FastAPI application with async SQLite database, authenticated clients, and test data setup. | exp: func:test_client() → Generator[TestClient, None, None], call:create_async_engine, call:engine.begin, call:conn.run_sync, call:asyncio.run, call:init_db, call:async_sessionmaker, call:patch, call:TestClient, call:app.dependency_overrides.pop, call:engine.dispose, func:init_db(), call:engine.begin, call:conn.run_sync, func:override_get_db_session() → AsyncGenerator[AsyncSession, None], call:async_sessionmaker, func:db_session(test_client) → AsyncGenerator[AsyncSession, None], call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:gen.aclose, call:create_async_engine, call:engine.begin, call:conn.run_sync, call:async_sessionmaker, call:engine.dispose, func:authenticated_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_test_user, call:create_session_cookie, call:test_client.cookies.set, func:create_test_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, func:test_project_and_repo(authenticated_client) → tuple[str, str], call:uuid.uuid4, call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, call:asyncio.run, call:get_user_id, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, call:create_project_and_repo, call:str, raise:RuntimeError, func:get_user_id(), call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, func:create_project_and_repo(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, func:admin_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_admin_user, call:create_session_cookie, call:test_client.cookies.set, func:create_admin_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose | dep: asyncio, os, typing, unittest.mock, pytest, pytest_asyncio, fastapi.testclient, sqlalchemy.ext.asyncio, src.config, src.models.base, src.main, src.auth.dependencies, uuid, src.auth.session, src.models.user.user, src.models.project.project, src.models.project.git_repository, fastapi, sqlalchemy, aiosqlite, src.models, src.auth
|
||||||
## arch
|
## arch
|
||||||
Pytest fixture-based testing architecture with async SQLite database lifecycle management, FastAPI TestClient dependency injection, and authenticated client wrappers for role-based testing.
|
Pytest fixture-based testing architecture using async SQLite in-memory database, dependency injection overrides, and async HTTP client setup for isolated integration tests.
|
||||||
## tags
|
## tags
|
||||||
call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user
|
call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/tests/unit
|
dir: apps/api/tests/unit
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains comprehensive unit tests for the API application's core services, utilities, and infrastructure components.
|
Contains comprehensive unit tests for the API backend services covering configuration, Docker operations, Git integration, file management, health monitoring, notifications, and SSH key handling.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/tests/.pi-map.index.md
|
index: apps/api/tests/.pi-map.index.md
|
||||||
map: apps/api/tests/.pi-map.md
|
map: apps/api/tests/.pi-map.md
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ dir: apps/api/tests/unit
|
|||||||
index: apps/api/tests/unit/.pi-map.index.md
|
index: apps/api/tests/unit/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains comprehensive unit tests for the API application's core services, utilities, and infrastructure components.
|
Contains comprehensive unit tests for the API backend services covering configuration, Docker operations, Git integration, file management, health monitoring, notifications, and SSH key handling.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Empty file with no functionality
|
- __init__.py | Empty file with no functionality
|
||||||
- test_config.py | Tests configuration settings and database URL building for an application, verifying defaults, environment variable overrides, and environment-specific behavior. | exp: func:test_settings_default_database_url_uses_asyncpg(monkeypatch) → None, call:monkeypatch.delenv, call:Settings, func:test_build_database_url_uses_explicit_values() → None, call:build_database_url, func:test_settings_prefers_explicit_database_url_env(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_auth_settings_have_secure_defaults() → None, call:Settings, call:settings.resolved_authentik_authorize_url.endswith, call:settings.resolved_authentik_token_url.endswith, call:settings.resolved_authentik_jwks_url.endswith, func:test_cookie_policy_is_strict_in_production(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_cookie_policy_is_relaxed_for_local_dev(monkeypatch) → None, call:monkeypatch.setenv, call:Settings | dep: pytest, src.config, src.database
|
- test_config.py | Tests configuration settings and database URL building for an application, verifying defaults, environment variable overrides, and environment-specific behavior. | exp: func:test_settings_default_database_url_uses_asyncpg(monkeypatch) → None, call:monkeypatch.delenv, call:Settings, func:test_build_database_url_uses_explicit_values() → None, call:build_database_url, func:test_settings_prefers_explicit_database_url_env(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_auth_settings_have_secure_defaults() → None, call:Settings, call:settings.resolved_authentik_authorize_url.endswith, call:settings.resolved_authentik_token_url.endswith, call:settings.resolved_authentik_jwks_url.endswith, func:test_cookie_policy_is_strict_in_production(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_cookie_policy_is_relaxed_for_local_dev(monkeypatch) → None, call:monkeypatch.setenv, call:Settings | dep: pytest, src.config, src.database
|
||||||
- test_config_profile_resolver.py | Unit tests for config profile resolution including merge functions, profile inheritance with includes, cycle detection, and git mount normalization | exp: class:TestMergeFunctions, method:test_merge_env_vars_basic(self) → None, call:_merge_env_vars, method:test_merge_env_vars_tracks_overrides(self) → None, call:_merge_env_vars, method:test_merge_runtime_hints_basic(self) → None, call:_merge_runtime_hints, method:test_merge_files_basic(self) → None, call:_merge_files, method:test_merge_mounts_basic(self) → None, call:_merge_mounts, method:test_merge_mounts_file_override(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_mounts_mode_conflict(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_git_mounts_basic(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_concatenate_same_repo_branch(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_dedup_same_mapping(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_repos(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_branches(self) → None, call:_merge_git_mounts, call:len, call:m.get, class:TestResolveProfile, class:TestApplyResolvedProfile, method:test_mounts_individual_files_not_directory(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, method:test_file_mount_preserves_sibling_files(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0]["source"].endswith, method:test_empty_mount_produces_no_volumes(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, method:test_home_expansion_in_file_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, class:TestCheckIncludeCycle | dep: uuid, pytest, sqlalchemy.ext.asyncio, src.models.config.config_profile, src.services.config.config_profile_resolver
|
- test_config_profile_resolver.py | Tests the config profile resolution system including merge helpers, profile inheritance with cycle detection, and git mount normalization | exp: class:TestMergeFunctions, method:test_merge_env_vars_basic(self) → None, call:_merge_env_vars, method:test_merge_env_vars_tracks_overrides(self) → None, call:_merge_env_vars, method:test_merge_runtime_hints_basic(self) → None, call:_merge_runtime_hints, method:test_merge_files_basic(self) → None, call:_merge_files, method:test_merge_mounts_basic(self) → None, call:_merge_mounts, method:test_merge_mounts_file_override(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_mounts_mode_conflict(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_git_mounts_basic(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_concatenate_same_repo_branch(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_dedup_same_mapping(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_repos(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_branches(self) → None, call:_merge_git_mounts, call:len, call:m.get, class:TestResolveProfile, class:TestApplyResolvedProfile, method:test_mounts_directory_not_individual_files(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path(volumes[0]["source"]).is_dir, call:(Path(volumes[0]["source"]) / "config.json").exists, call:(Path(volumes[0]["source"]) / "nested" / "file.txt").exists, method:test_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path, call:(Path(volumes[0]["source"]) / "z.json").exists, method:test_empty_mount_produces_no_volumes(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, method:test_home_expansion_in_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:(Path(volumes[0]["source"]) / "app.toml").exists, call:Path, method:test_readonly_mount_sets_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, method:test_writable_mount_does_not_set_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, class:TestCheckIncludeCycle | dep: uuid, pathlib, pytest, sqlalchemy.ext.asyncio, src.models.config.config_profile, src.services.config.config_profile_resolver
|
||||||
- test_docker_build.py | Unit tests for a Docker image build service that verifies Dockerfile creation, command structure, context file handling, path traversal prevention, and error handling. | exp: class:TestBuildImage | dep: subprocess, tempfile, pathlib, unittest.mock, pytest, src.services.build.docker_build
|
- test_docker_build.py | Unit tests for a Docker image build service that verifies Dockerfile creation, command structure, context file handling, path traversal prevention, and error handling. | exp: class:TestBuildImage | dep: subprocess, tempfile, pathlib, unittest.mock, pytest, src.services.build.docker_build
|
||||||
- test_docker_service.py | Unit tests for Docker service utilities including container ID/name retrieval and volume sorting by specificity. | exp: class:TestGetContainerId, class:TestGetContainerName, class:TestSortVolumesBySpecificity, method:test_parent_before_child(self) → None, call:sort_volumes_by_specificity, method:test_stable_sort_for_equal_depth(self) → None, call:sort_volumes_by_specificity, method:test_with_type_suffix(self) → None, call:sort_volumes_by_specificity, method:test_empty_list(self) → None, call:sort_volumes_by_specificity, method:test_single_volume(self) → None, call:sort_volumes_by_specificity, method:test_duplicate_target_warning(self, caplog) → None, call:caplog.at_level, call:sort_volumes_by_specificity | dep: unittest.mock, logging, src.services.docker.container, src.services.docker.compose, subprocess
|
- test_docker_service.py | Unit tests for Docker service utilities including container ID/name retrieval and volume sorting by specificity. | exp: class:TestGetContainerId, class:TestGetContainerName, class:TestSortVolumesBySpecificity, method:test_parent_before_child(self) → None, call:sort_volumes_by_specificity, method:test_stable_sort_for_equal_depth(self) → None, call:sort_volumes_by_specificity, method:test_with_type_suffix(self) → None, call:sort_volumes_by_specificity, method:test_empty_list(self) → None, call:sort_volumes_by_specificity, method:test_single_volume(self) → None, call:sort_volumes_by_specificity, method:test_duplicate_target_warning(self, caplog) → None, call:caplog.at_level, call:sort_volumes_by_specificity | dep: unittest.mock, logging, src.services.docker.container, src.services.docker.compose, subprocess
|
||||||
- test_event_bus.py | Unit tests for InstanceEventBus verifying publish/subscribe, exception isolation, unsubscribe, and async callback support. | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:sample_payload() → InstanceEventPayload, call:str, call:uuid.uuid4, func:test_publish_delivers_to_all_subscribers(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.publish, call:len, func:callback_1(payload: InstanceEventPayload) → None, call:received.append, func:callback_2(payload: InstanceEventPayload) → None, call:received.append, func:callback_3(payload: InstanceEventPayload) → None, call:received.append, func:test_subscriber_exception_isolation(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.publish, raise:RuntimeError, func:bad_callback(_payload: InstanceEventPayload) → None, raise:RuntimeError, func:good_callback(_payload: InstanceEventPayload) → None, call:received.append, func:test_unsubscribe_removes_callback(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:unsubscribe, call:event_bus.publish, func:callback(_payload: InstanceEventPayload) → None, call:received.append, func:test_publish_to_empty_subscriber_list(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:event_bus.publish, func:test_async_subscriber_supported(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:asyncio.sleep, call:received.append, call:event_bus.subscribe, call:event_bus.publish, func:async_callback(_payload: InstanceEventPayload) → None, call:asyncio.sleep, call:received.append, func:test_unsubscribe_all_clears_subscribers(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.unsubscribe_all, call:event_bus.publish, func:callback(_payload: InstanceEventPayload) → None, call:received.append | dep: asyncio, uuid, typing, pytest, src.services.instance.event_bus
|
- test_event_bus.py | Unit tests for InstanceEventBus verifying publish/subscribe, exception isolation, unsubscribe, and async callback support. | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:sample_payload() → InstanceEventPayload, call:str, call:uuid.uuid4, func:test_publish_delivers_to_all_subscribers(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.publish, call:len, func:callback_1(payload: InstanceEventPayload) → None, call:received.append, func:callback_2(payload: InstanceEventPayload) → None, call:received.append, func:callback_3(payload: InstanceEventPayload) → None, call:received.append, func:test_subscriber_exception_isolation(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.publish, raise:RuntimeError, func:bad_callback(_payload: InstanceEventPayload) → None, raise:RuntimeError, func:good_callback(_payload: InstanceEventPayload) → None, call:received.append, func:test_unsubscribe_removes_callback(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:unsubscribe, call:event_bus.publish, func:callback(_payload: InstanceEventPayload) → None, call:received.append, func:test_publish_to_empty_subscriber_list(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:event_bus.publish, func:test_async_subscriber_supported(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:asyncio.sleep, call:received.append, call:event_bus.subscribe, call:event_bus.publish, func:async_callback(_payload: InstanceEventPayload) → None, call:asyncio.sleep, call:received.append, func:test_unsubscribe_all_clears_subscribers(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.unsubscribe_all, call:event_bus.publish, func:callback(_payload: InstanceEventPayload) → None, call:received.append | dep: asyncio, uuid, typing, pytest, src.services.instance.event_bus
|
||||||
@@ -28,9 +28,9 @@ Contains comprehensive unit tests for the API application's core services, utili
|
|||||||
- test_readiness_probe.py | Unit tests for a Docker container readiness probe service that executes commands via docker exec with retry logic. | exp: class:TestExecuteProbe, class:TestIntegrationScenarios | dep: unittest.mock, src.services.shared.readiness_probe, subprocess
|
- test_readiness_probe.py | Unit tests for a Docker container readiness probe service that executes commands via docker exec with retry logic. | exp: class:TestExecuteProbe, class:TestIntegrationScenarios | dep: unittest.mock, src.services.shared.readiness_probe, subprocess
|
||||||
- test_ssh_keys.py | Unit tests for SSH key preparation functionality including file creation, permissions, ownership, and error handling | exp: class:TestPrepareSshKeyFiles | dep: os, pathlib, unittest.mock, pytest, src.services.shared.ssh_keys
|
- test_ssh_keys.py | Unit tests for SSH key preparation functionality including file creation, permissions, ownership, and error handling | exp: class:TestPrepareSshKeyFiles | dep: os, pathlib, unittest.mock, pytest, src.services.shared.ssh_keys
|
||||||
## arch
|
## arch
|
||||||
Standard Python unittest/pytest structure with heavy use of mocking for external dependencies (Docker, Git subprocess, database), testing business logic in isolation without integration requirements.
|
Standard Python unittest/pytest pattern with heavy mocking of external dependencies (subprocess, docker, filesystem) to test service layer logic in isolation, organized by functional domain with one test module per service component.
|
||||||
## tags
|
## tags
|
||||||
test, url, call:notification, git, call:db, merge, call:, src
|
test, url, call:notification, git, call:, call:db, merge, src
|
||||||
## symbols
|
## symbols
|
||||||
- TestMergeFunctions
|
- TestMergeFunctions
|
||||||
- TestResolveProfile
|
- TestResolveProfile
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -483,10 +485,10 @@ class TestResolveProfile:
|
|||||||
|
|
||||||
|
|
||||||
class TestApplyResolvedProfile:
|
class TestApplyResolvedProfile:
|
||||||
"""Unit tests for apply_resolved_profile file-level mount behavior."""
|
"""Unit tests for apply_resolved_profile directory-level mount behavior."""
|
||||||
|
|
||||||
def test_mounts_individual_files_not_directory(self, tmp_path) -> None:
|
def test_mounts_directory_not_individual_files(self, tmp_path) -> None:
|
||||||
"""Each file in a ResolvedMount should be mounted individually, not the staging dir."""
|
"""Each ResolvedMount should produce one directory-level bind mount."""
|
||||||
resolved = ResolvedProfile(
|
resolved = ResolvedProfile(
|
||||||
profile_id=uuid.uuid4(),
|
profile_id=uuid.uuid4(),
|
||||||
profile_name="test",
|
profile_name="test",
|
||||||
@@ -503,15 +505,15 @@ class TestApplyResolvedProfile:
|
|||||||
)
|
)
|
||||||
env, files, volumes, hints = apply_resolved_profile(str(tmp_path), resolved)
|
env, files, volumes, hints = apply_resolved_profile(str(tmp_path), resolved)
|
||||||
|
|
||||||
assert len(volumes) == 2
|
assert len(volumes) == 1
|
||||||
targets = {v["target"] for v in volumes}
|
assert volumes[0]["target"] == "/app"
|
||||||
assert "/app/config.json" in targets
|
assert Path(volumes[0]["source"]).name == "app"
|
||||||
assert "/app/nested/file.txt" in targets
|
assert Path(volumes[0]["source"]).is_dir()
|
||||||
# No directory-level mount
|
assert (Path(volumes[0]["source"]) / "config.json").exists()
|
||||||
assert "/app" not in targets
|
assert (Path(volumes[0]["source"]) / "nested" / "file.txt").exists()
|
||||||
|
|
||||||
def test_file_mount_preserves_sibling_files(self, tmp_path) -> None:
|
def test_directory_mount_target(self, tmp_path) -> None:
|
||||||
"""File-level mounts should not hide sibling files from other mounts."""
|
"""A directory-level mount targets the configured directory path."""
|
||||||
resolved = ResolvedProfile(
|
resolved = ResolvedProfile(
|
||||||
profile_id=uuid.uuid4(),
|
profile_id=uuid.uuid4(),
|
||||||
profile_name="test",
|
profile_name="test",
|
||||||
@@ -526,8 +528,9 @@ class TestApplyResolvedProfile:
|
|||||||
env, files, volumes, hints = apply_resolved_profile(str(tmp_path), resolved)
|
env, files, volumes, hints = apply_resolved_profile(str(tmp_path), resolved)
|
||||||
|
|
||||||
assert len(volumes) == 1
|
assert len(volumes) == 1
|
||||||
assert volumes[0]["target"] == "/workspace/x/y/z.json"
|
assert volumes[0]["target"] == "/workspace/x/y"
|
||||||
assert volumes[0]["source"].endswith("z.json")
|
assert Path(volumes[0]["source"]).name == "workspace_x_y"
|
||||||
|
assert (Path(volumes[0]["source"]) / "z.json").exists()
|
||||||
|
|
||||||
def test_empty_mount_produces_no_volumes(self, tmp_path) -> None:
|
def test_empty_mount_produces_no_volumes(self, tmp_path) -> None:
|
||||||
"""A mount with no files should not produce any volume entries."""
|
"""A mount with no files should not produce any volume entries."""
|
||||||
@@ -539,8 +542,8 @@ class TestApplyResolvedProfile:
|
|||||||
env, files, volumes, hints = apply_resolved_profile(str(tmp_path), resolved)
|
env, files, volumes, hints = apply_resolved_profile(str(tmp_path), resolved)
|
||||||
assert volumes == []
|
assert volumes == []
|
||||||
|
|
||||||
def test_home_expansion_in_file_mount_target(self, tmp_path) -> None:
|
def test_home_expansion_in_directory_mount_target(self, tmp_path) -> None:
|
||||||
"""~ in mount target should be expanded to home_dir for file mounts."""
|
"""~ in mount target should be expanded to home_dir for directory mounts."""
|
||||||
resolved = ResolvedProfile(
|
resolved = ResolvedProfile(
|
||||||
profile_id=uuid.uuid4(),
|
profile_id=uuid.uuid4(),
|
||||||
profile_name="test",
|
profile_name="test",
|
||||||
@@ -555,7 +558,47 @@ class TestApplyResolvedProfile:
|
|||||||
env, files, volumes, hints = apply_resolved_profile(
|
env, files, volumes, hints = apply_resolved_profile(
|
||||||
str(tmp_path), resolved, home_dir="/home/user"
|
str(tmp_path), resolved, home_dir="/home/user"
|
||||||
)
|
)
|
||||||
assert volumes[0]["target"] == "/home/user/.config/app.toml"
|
assert len(volumes) == 1
|
||||||
|
assert volumes[0]["target"] == "/home/user/.config"
|
||||||
|
assert (Path(volumes[0]["source"]) / "app.toml").exists()
|
||||||
|
|
||||||
|
def test_readonly_mount_sets_readonly_flag(self, tmp_path) -> None:
|
||||||
|
"""A mount with mode 'ro' should set readonly on the volume entry."""
|
||||||
|
resolved = ResolvedProfile(
|
||||||
|
profile_id=uuid.uuid4(),
|
||||||
|
profile_name="test",
|
||||||
|
mounts={
|
||||||
|
"/etc/app": ResolvedMount(
|
||||||
|
target="/etc/app",
|
||||||
|
mode="ro",
|
||||||
|
files={"config.cfg": "value"},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
env, files, volumes, hints = apply_resolved_profile(str(tmp_path), resolved)
|
||||||
|
|
||||||
|
assert len(volumes) == 1
|
||||||
|
assert volumes[0]["target"] == "/etc/app"
|
||||||
|
assert volumes[0].get("readonly") is True
|
||||||
|
|
||||||
|
def test_writable_mount_does_not_set_readonly_flag(self, tmp_path) -> None:
|
||||||
|
"""A mount with mode 'rw' should not set readonly on the volume entry."""
|
||||||
|
resolved = ResolvedProfile(
|
||||||
|
profile_id=uuid.uuid4(),
|
||||||
|
profile_name="test",
|
||||||
|
mounts={
|
||||||
|
"/app": ResolvedMount(
|
||||||
|
target="/app",
|
||||||
|
mode="rw",
|
||||||
|
files={"config.json": "{}"},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
env, files, volumes, hints = apply_resolved_profile(str(tmp_path), resolved)
|
||||||
|
|
||||||
|
assert len(volumes) == 1
|
||||||
|
assert volumes[0]["target"] == "/app"
|
||||||
|
assert volumes[0].get("readonly") is False
|
||||||
|
|
||||||
|
|
||||||
class TestCheckIncludeCycle:
|
class TestCheckIncludeCycle:
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ Frontend web application providing a React-based UI with code editing, terminal,
|
|||||||
- tsconfig.json | TypeScript configuration file for a React project using Vite with modern ES2020 target and bundler module resolution | dep: typescript, react, vite
|
- tsconfig.json | TypeScript configuration file for a React project using Vite with modern ES2020 target and bundler module resolution | dep: typescript, react, vite
|
||||||
- vite.config.ts | Configures Vite build tool for a React project with custom dev server port and Vitest test settings. | dep: vite, @vitejs/plugin-react
|
- vite.config.ts | Configures Vite build tool for a React project with custom dev server port and Vitest test settings. | dep: vite, @vitejs/plugin-react
|
||||||
## arch
|
## arch
|
||||||
Modern React SPA built with Vite and TypeScript, containerized via multi-stage Docker with nginx serving, featuring client-side routing and optimized static asset delivery.
|
Modern React SPA built with Vite and TypeScript, using nginx for production serving with client-side routing, multi-stage Docker deployment, and Vitest for testing.
|
||||||
## tags
|
## tags
|
||||||
react, eslint, vite, typescript, dom, application, nginx, web
|
react, eslint, vite, typescript, dom, application, nginx, web
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src
|
dir: apps/web/src
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Entry point and core infrastructure for a React web application handling routing, authentication, and shared type definitions.
|
Frontend web application entry point and core infrastructure for a React-based collaborative development platform.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/.pi-map.index.md
|
index: apps/web/.pi-map.index.md
|
||||||
map: apps/web/.pi-map.md
|
map: apps/web/.pi-map.md
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ dir: apps/web/src
|
|||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Entry point and core infrastructure for a React web application handling routing, authentication, and shared type definitions.
|
Frontend web application entry point and core infrastructure for a React-based collaborative development platform.
|
||||||
## files
|
## 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
|
- 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
|
- router.tsx | Defines the React Router configuration for a web application with protected routes, nested layouts, and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage
|
||||||
- types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
|
- types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
|
||||||
## arch
|
## arch
|
||||||
Provider pattern wrapping React Router with nested route layouts, protected route guards via authentication/session providers, and centralized TypeScript type definitions for domain models.
|
Modular React SPA using React Router v6 with nested route layouts, protected route guards via authentication context, and centralized TypeScript domain models for session/workspace/project entities.
|
||||||
## tags
|
## tags
|
||||||
pages, styles, css, router, react, session, dom, project
|
pages, styles, css, router, react, session, dom, project
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components
|
dir: apps/web/src/components
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Reusable React UI components and authentication guards for the web application's frontend interface.
|
Provides reusable, accessible UI components and utilities for rendering the application shell, data states, icons, code display, notifications, and route protection in a React web application.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.md
|
map: apps/web/src/.pi-map.md
|
||||||
@@ -15,6 +15,7 @@ map: apps/web/src/.pi-map.md
|
|||||||
- code-editor.tsx
|
- code-editor.tsx
|
||||||
- data-states.tsx
|
- data-states.tsx
|
||||||
- icon.tsx
|
- icon.tsx
|
||||||
|
- loading-overlay.tsx
|
||||||
- protected-route.test.tsx
|
- protected-route.test.tsx
|
||||||
- protected-route.tsx
|
- protected-route.tsx
|
||||||
- syntax-highlighter.tsx
|
- syntax-highlighter.tsx
|
||||||
|
|||||||
@@ -4,22 +4,24 @@ dir: apps/web/src/components
|
|||||||
index: apps/web/src/components/.pi-map.index.md
|
index: apps/web/src/components/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Reusable React UI components and authentication guards for the web application's frontend interface.
|
Provides reusable, accessible UI components and utilities for rendering the application shell, data states, icons, code display, notifications, and route protection in a React web application.
|
||||||
## files
|
## files
|
||||||
- app-shell.tsx | Renders the main application shell layout with navigation, session management, and responsive mobile/desktop views for a React Router-based app. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ../state/session-operations, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./features/session/session-progress-panel, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
|
- app-shell.tsx | Renders the main application shell layout with navigation, header, session management, and mobile-responsive behavior for a React Router-based SPA. | 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, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
|
||||||
- code-editor.tsx | A React component that renders a syntax-highlighted code editor with line numbers using react-simple-code-editor. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language
|
- code-editor.tsx | A React component that renders a syntax-highlighted code editor with line numbers using react-simple-code-editor. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language
|
||||||
- data-states.tsx | Provides reusable React components for displaying loading, error, and empty data states in a UI. | exp: LoadingState, ErrorState, EmptyState | dep: ./icon, icon
|
- data-states.tsx | Provides reusable React components for displaying loading, error, and empty data states in a UI. | exp: LoadingState, ErrorState, EmptyState | dep: ./icon, icon
|
||||||
- icon.tsx | Provides a centralized, type-safe React icon component that maps semantic names to Phosphor icons with configurable size, weight, color, and accessibility attributes. | exp: IconName, IconProps, Icon | dep: react, @phosphor-icons/react
|
- icon.tsx | Provides a centralized, type-safe React icon component that maps semantic names to Phosphor icons with configurable size, weight, color, and accessibility attributes. | exp: IconName, IconProps, Icon | dep: react, @phosphor-icons/react
|
||||||
|
- loading-overlay.tsx | Renders an accessible loading overlay with optional label that conditionally displays based on visibility prop | exp: func:LoadingOverlay({ visible, label }: LoadingOverlayProps) | dep: ./icon, icon
|
||||||
- protected-route.test.tsx | Tests a ProtectedRoute component that guards content based on authentication state | dep: @testing-library/react, react-router-dom, vitest, ./protected-route
|
- protected-route.test.tsx | Tests a ProtectedRoute component that guards content based on authentication state | dep: @testing-library/react, react-router-dom, vitest, ./protected-route
|
||||||
- protected-route.tsx | Conditionally renders children or redirects to login based on authentication state | exp: ProtectedRoute | dep: react-router-dom, ../state/auth
|
- protected-route.tsx | Conditionally renders children or redirects to login based on authentication state | exp: ProtectedRoute | dep: react-router-dom, ../state/auth
|
||||||
- syntax-highlighter.tsx | Renders syntax-highlighted code blocks with optional line numbers and copy-to-clipboard functionality | exp: SyntaxHighlighter | dep: react, ./icon, ../utils/language, React, icon, language (utils)
|
- syntax-highlighter.tsx | Renders syntax-highlighted code blocks with optional line numbers and copy-to-clipboard functionality | exp: SyntaxHighlighter | dep: react, ./icon, ../utils/language, React, icon, language (utils)
|
||||||
- toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events
|
- toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events
|
||||||
- toast-rules.ts | Maps instance events to toast notifications with deduplication logic to prevent spam | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast state module, InstanceEventPayload type
|
- toast-rules.ts | Maps instance events to toast notifications with deduplication logic to prevent spam | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast state module, InstanceEventPayload type
|
||||||
## arch
|
## arch
|
||||||
Component-based React architecture using functional components with hooks, centralized type-safe utilities (icons, data states), composition patterns for layout (app-shell), and higher-order route protection for auth gating.
|
Component-based React architecture with functional components, composition patterns, separation of concerns (presentation vs logic), and test coverage for critical utilities; integrates third-party libraries (Phosphor icons, react-simple-code-editor) and React Router for SPA navigation.
|
||||||
## tags
|
## tags
|
||||||
toast, state, react, icon, code, event, editor, protected
|
toast, icon, react, state, code, loading, event, editor
|
||||||
## symbols
|
## symbols
|
||||||
|
- LoadingOverlay
|
||||||
- mapEventToCategory
|
- mapEventToCategory
|
||||||
- mapEventToSeverity
|
- mapEventToSeverity
|
||||||
- handleEventToast
|
- handleEventToast
|
||||||
@@ -27,7 +29,6 @@ toast, state, react, icon, code, event, editor, protected
|
|||||||
- AppShell
|
- AppShell
|
||||||
- CodeEditor
|
- CodeEditor
|
||||||
- LoadingState
|
- LoadingState
|
||||||
- ErrorState
|
|
||||||
## workflows
|
## workflows
|
||||||
- change components behavior
|
- change components behavior
|
||||||
read: app-shell.tsx, code-editor.tsx, data-states.tsx
|
read: app-shell.tsx, code-editor.tsx, data-states.tsx
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ import { useMobileViewport } from "../hooks/use-mobile-viewport";
|
|||||||
import { EventProvider } from "../state/events";
|
import { EventProvider } from "../state/events";
|
||||||
import { ToastProvider } from "../state/toast";
|
import { ToastProvider } from "../state/toast";
|
||||||
import { NotificationProvider } from "../state/notifications";
|
import { NotificationProvider } from "../state/notifications";
|
||||||
import { SessionOperationsProvider } from "../state/session-operations";
|
|
||||||
import { EventToastBridge } from "./features/notification/event-toast-bridge";
|
import { EventToastBridge } from "./features/notification/event-toast-bridge";
|
||||||
import { NotificationCenter } from "./features/notification/notification-center";
|
import { NotificationCenter } from "./features/notification/notification-center";
|
||||||
import { SessionProgressPanel } from "./features/session/session-progress-panel";
|
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { MobileNav } from "./features/mobile/mobile-nav";
|
import { MobileNav } from "./features/mobile/mobile-nav";
|
||||||
import { StartToolFAB } from "./features/tool/start-tool-fab";
|
import { StartToolFAB } from "./features/tool/start-tool-fab";
|
||||||
@@ -24,9 +22,9 @@ const NAV_ITEMS: {
|
|||||||
badge?: "sessions";
|
badge?: "sessions";
|
||||||
}[] = [
|
}[] = [
|
||||||
{ to: "/", label: "Home", icon: "dashboard" },
|
{ to: "/", label: "Home", icon: "dashboard" },
|
||||||
{ to: "/sessions", label: "Sessions", icon: "terminal", badge: "sessions" },
|
|
||||||
{ to: "/workspaces", label: "Workspaces", icon: "folder" },
|
|
||||||
{ to: "/projects", label: "Projects", icon: "projects" },
|
{ to: "/projects", label: "Projects", icon: "projects" },
|
||||||
|
{ to: "/workspaces", label: "Workspaces", icon: "folder" },
|
||||||
|
{ to: "/sessions", label: "Sessions", icon: "terminal", badge: "sessions" },
|
||||||
{ to: "/tool-workshop", label: "Tool Workshop", icon: "settings" },
|
{ to: "/tool-workshop", label: "Tool Workshop", icon: "settings" },
|
||||||
{ to: "/config-profiles", label: "Config Profiles", icon: "folder" },
|
{ to: "/config-profiles", label: "Config Profiles", icon: "folder" },
|
||||||
{ to: "/settings", label: "Settings", icon: "settings" },
|
{ to: "/settings", label: "Settings", icon: "settings" },
|
||||||
@@ -87,12 +85,10 @@ export const AppShell = () => {
|
|||||||
<EventProvider>
|
<EventProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<SessionOperationsProvider>
|
|
||||||
<EventToastBridge />
|
<EventToastBridge />
|
||||||
<div className="shell mobile-terminal-shell">
|
<div className="shell mobile-terminal-shell">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</SessionOperationsProvider>
|
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</EventProvider>
|
</EventProvider>
|
||||||
@@ -103,9 +99,7 @@ export const AppShell = () => {
|
|||||||
<EventProvider>
|
<EventProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<SessionOperationsProvider>
|
|
||||||
<EventToastBridge />
|
<EventToastBridge />
|
||||||
<SessionProgressPanel />
|
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<header className="shell-header">
|
<header className="shell-header">
|
||||||
<Link className="brand" to="/">
|
<Link className="brand" to="/">
|
||||||
@@ -180,7 +174,6 @@ export const AppShell = () => {
|
|||||||
)}
|
)}
|
||||||
<StartToolFAB />
|
<StartToolFAB />
|
||||||
</div>
|
</div>
|
||||||
</SessionOperationsProvider>
|
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</EventProvider>
|
</EventProvider>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features
|
dir: apps/web/src/components/features
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains reusable UI components that implement specific business features and user-facing functionality across the web application.
|
Contains reusable React components that implement specific user-facing features and functionality across the web application.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/components/.pi-map.index.md
|
index: apps/web/src/components/.pi-map.index.md
|
||||||
map: apps/web/src/components/.pi-map.md
|
map: apps/web/src/components/.pi-map.md
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
|
|||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains reusable UI components that implement specific business features and user-facing functionality across the web application.
|
Contains reusable React components that implement specific user-facing features and functionality across the web application.
|
||||||
## files
|
## files
|
||||||
## arch
|
## arch
|
||||||
Feature-based component organization with domain-specific groupings, likely using composition patterns, container/presentational separation, and integration with hooks/state management for feature logic.
|
Feature-based component organization with domain-specific UI building blocks following React composition patterns, likely co-located with related hooks, utilities, or sub-components for each feature area.
|
||||||
## tags
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/mobile
|
dir: apps/web/src/components/features/mobile
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides mobile-optimized UI components for a responsive web application, covering navigation, lists, detail views, forms, modals, and specialized terminal interfaces.
|
Provides mobile-optimized UI components for core application features including navigation, data views, forms, modals, and specialized terminal interfaces.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.md
|
map: apps/web/src/components/features/.pi-map.md
|
||||||
@@ -18,6 +18,7 @@ map: apps/web/src/components/features/.pi-map.md
|
|||||||
- mobile-page-header.tsx
|
- mobile-page-header.tsx
|
||||||
- mobile-terminal-header.tsx
|
- mobile-terminal-header.tsx
|
||||||
- mobile-terminal-wrapper.tsx
|
- mobile-terminal-wrapper.tsx
|
||||||
|
- spaces-bottom-sheet.tsx
|
||||||
## links
|
## links
|
||||||
index: apps/web/src/components/features/mobile/.pi-map.index.md
|
index: apps/web/src/components/features/mobile/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/mobile/.pi-map.md
|
map: apps/web/src/components/features/mobile/.pi-map.md
|
||||||
|
|||||||
@@ -4,21 +4,22 @@ dir: apps/web/src/components/features/mobile
|
|||||||
index: apps/web/src/components/features/mobile/.pi-map.index.md
|
index: apps/web/src/components/features/mobile/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides mobile-optimized UI components for a responsive web application, covering navigation, lists, detail views, forms, modals, and specialized terminal interfaces.
|
Provides mobile-optimized UI components for core application features including navigation, data views, forms, modals, and specialized terminal interfaces.
|
||||||
## files
|
## files
|
||||||
- mobile-action-sheet.tsx | Renders a mobile-optimized action sheet modal with title, configurable action buttons, and cancel option. | exp: MobileActionSheetItem, func:MobileActionSheet({ isOpen, onClose, title, actions, }: MobileActionSheetProps), call:useRef, call:useEffect, call:onClose, call:document.addEventListener, call:document.removeEventListener, call:e.stopPropagation, call:actions.map, call:action.onClick | dep: react, ../../icon, icon
|
- mobile-action-sheet.tsx | Renders a mobile-optimized action sheet modal with title, configurable action buttons, and cancel option. | exp: MobileActionSheetItem, func:MobileActionSheet({ isOpen, onClose, title, actions, }: MobileActionSheetProps), call:useRef, call:useEffect, call:onClose, call:document.addEventListener, call:document.removeEventListener, call:e.stopPropagation, call:actions.map, call:action.onClick | dep: react, ../../icon, icon
|
||||||
- mobile-detail-view.tsx | A React component that renders a mobile-optimized detail view with a header (back button, title, edit/delete actions) and a field list supporting multiple value types (text, code, JSON, boolean). | exp: MobileDetailView | dep: ../../icon, react
|
- mobile-detail-view.tsx | A React component that renders a mobile-optimized detail view with a header (back button, title, edit/delete actions) and a field list supporting multiple value types (text, code, JSON, boolean). | exp: MobileDetailView | dep: ../../icon, react
|
||||||
- mobile-edit-view.tsx | A React component that renders a mobile-optimized form for editing data with configurable field types and save/cancel actions. | exp: MobileEditView | dep: react
|
- mobile-edit-view.tsx | A React component that renders a mobile-optimized form for editing data with configurable field types and save/cancel actions. | exp: MobileEditView | dep: react
|
||||||
- mobile-fab.tsx | Renders a floating action button component for mobile with an add icon and configurable click handler and label. | exp: MobileFAB | dep: ../../icon, react, icon
|
- mobile-fab.tsx | Renders a floating action button component for mobile with an add icon and configurable click handler and label. | exp: MobileFAB | dep: ../../icon, react, icon
|
||||||
- mobile-list-view.tsx | Renders a searchable mobile-friendly list view with optional icons, subtitles, and click handling | exp: MobileListView | dep: ../../icon, ../../../utils/icons, react, icon, icons
|
- mobile-list-view.tsx | Renders a mobile-optimized list view with optional search, empty state, and customizable item rendering | exp: MobileListView | dep: react, ../../icon, ../../../utils/icons, icon
|
||||||
- mobile-nav.tsx | Renders a mobile navigation bar with routing links, a tools group button that opens a bottom sheet, and session count badges | exp: MobileNav | dep: react, react-router-dom, ../../icon, ../tool/tools-bottom-sheet, ../../../utils/icons, icon, tools-bottom-sheet, icons utils
|
- mobile-nav.tsx | Renders a mobile navigation bar with grouped items that open bottom sheets and standard items that use React Router links, including active state indicators and session count badges. | exp: MobileNav | dep: react, react-router-dom, ../../icon, ../tool/tools-bottom-sheet, ./spaces-bottom-sheet, ../../../utils/icons
|
||||||
- mobile-page-header.tsx | Renders a mobile-only page header with optional back navigation and custom actions. | exp: func:MobilePageHeader({ title, showBack = true, actions }: MobilePageHeaderProps), call:useNavigate, call:useMobileViewport, call:navigate | dep: react-router-dom, ../../../hooks/use-mobile-viewport, ../../icon, use-mobile-viewport hook, Icon component
|
- mobile-page-header.tsx | Renders a mobile-only page header with optional back navigation and custom actions. | exp: func:MobilePageHeader({ title, showBack = true, actions }: MobilePageHeaderProps), call:useNavigate, call:useMobileViewport, call:navigate | dep: react-router-dom, ../../../hooks/use-mobile-viewport, ../../icon, use-mobile-viewport hook, Icon component
|
||||||
- mobile-terminal-header.tsx | Renders a mobile-responsive header for a terminal interface with navigation, title, connection status, font size controls, and close actions. | exp: MobileTerminalHeader | dep: react, ../../icon, icon
|
- mobile-terminal-header.tsx | Renders a mobile-responsive header for a terminal interface with navigation, title, connection status, font size controls, and close actions. | exp: MobileTerminalHeader | dep: react, ../../icon, icon
|
||||||
- mobile-terminal-wrapper.tsx | Wraps a terminal component with mobile-specific UI including auto-hiding header, virtual keyboard handling, and special keys interface. | exp: MobileTerminalWrapper | dep: react, ../terminal/terminal, ./mobile-terminal-header, ../terminal/special-keys-strip, ../terminal/special-keys-panel, ../../../hooks/use-mobile-viewport, ../../../hooks/use-virtual-keyboard, ../../../hooks/use-auto-hide, ../../../hooks/use-special-keys
|
- mobile-terminal-wrapper.tsx | Wraps a terminal component with mobile-specific UI including auto-hiding header, virtual keyboard handling, and special keys interface. | exp: MobileTerminalWrapper | dep: react, ../terminal/terminal, ./mobile-terminal-header, ../terminal/special-keys-strip, ../terminal/special-keys-panel, ../../../hooks/use-mobile-viewport, ../../../hooks/use-virtual-keyboard, ../../../hooks/use-auto-hide, ../../../hooks/use-special-keys
|
||||||
|
- spaces-bottom-sheet.tsx | Renders a mobile bottom sheet navigation menu for switching between "Projects" and "Workspaces" spaces with active state highlighting. | exp: SpacesBottomSheet | dep: react-router-dom, ../../icon, icon
|
||||||
## arch
|
## arch
|
||||||
Feature-based component organization with compound mobile UI patterns (action sheets, FABs, bottom sheets, auto-hiding headers) that mirror native mobile app paradigms, using configurable props for customization and likely React Context or hooks for keyboard/virtual terminal state management.
|
Compositional React component library with feature-specific mobile adaptations, using bottom sheets/action sheets for mobile-native UX patterns, and wrapper components that inject mobile-specific behaviors (auto-hiding headers, virtual keyboard handling) into existing features.
|
||||||
## tags
|
## tags
|
||||||
mobile, terminal, view, icon, header, react, sheet, action
|
mobile, terminal, sheet, react, icon, view, header, renders
|
||||||
## symbols
|
## symbols
|
||||||
- MobileActionSheet
|
- MobileActionSheet
|
||||||
- MobilePageHeader
|
- MobilePageHeader
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
import type { IconName } from "../../../utils/icons";
|
import type { IconName } from "../../../utils/icons";
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ interface MobileListViewProps {
|
|||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
onSearch?: (query: string) => void;
|
onSearch?: (query: string) => void;
|
||||||
|
renderItem?: (item: MobileListItem) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MobileListView: React.FC<MobileListViewProps> = ({
|
export const MobileListView: React.FC<MobileListViewProps> = ({
|
||||||
@@ -25,6 +27,7 @@ export const MobileListView: React.FC<MobileListViewProps> = ({
|
|||||||
emptyMessage = "No items found",
|
emptyMessage = "No items found",
|
||||||
searchPlaceholder = "Search...",
|
searchPlaceholder = "Search...",
|
||||||
onSearch,
|
onSearch,
|
||||||
|
renderItem,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="mobile-list-view">
|
<div className="mobile-list-view">
|
||||||
@@ -58,13 +61,22 @@ export const MobileListView: React.FC<MobileListViewProps> = ({
|
|||||||
<Icon name={item.icon as IconName} size="md" />
|
<Icon name={item.icon as IconName} size="md" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{renderItem ? (
|
||||||
|
renderItem(item)
|
||||||
|
) : (
|
||||||
<div className="mobile-list-item-content">
|
<div className="mobile-list-item-content">
|
||||||
<div className="mobile-list-item-title">{item.title}</div>
|
<div className="mobile-list-item-title">{item.title}</div>
|
||||||
{item.subtitle && (
|
{item.subtitle && (
|
||||||
<div className="mobile-list-item-subtitle">{item.subtitle}</div>
|
<div className="mobile-list-item-subtitle">
|
||||||
|
{item.subtitle}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mobile-list-item-actions" style={{ transform: "rotate(180deg)" }}>
|
)}
|
||||||
|
<div
|
||||||
|
className="mobile-list-item-actions"
|
||||||
|
style={{ transform: "rotate(180deg)" }}
|
||||||
|
>
|
||||||
<Icon name="arrow-left" size="sm" />
|
<Icon name="arrow-left" size="sm" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { NavLink, useLocation } from "react-router-dom";
|
import { NavLink, useLocation } from "react-router-dom";
|
||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
import { ToolsBottomSheet } from "../tool/tools-bottom-sheet";
|
import { ToolsBottomSheet } from "../tool/tools-bottom-sheet";
|
||||||
|
import { SpacesBottomSheet } from "./spaces-bottom-sheet";
|
||||||
import type { IconName } from "../../../utils/icons";
|
import type { IconName } from "../../../utils/icons";
|
||||||
|
|
||||||
interface MobileNavProps {
|
interface MobileNavProps {
|
||||||
@@ -13,39 +14,66 @@ interface NavItem {
|
|||||||
label: string;
|
label: string;
|
||||||
icon: IconName;
|
icon: IconName;
|
||||||
isGroup?: boolean;
|
isGroup?: boolean;
|
||||||
|
group?: "tools" | "spaces";
|
||||||
}
|
}
|
||||||
|
|
||||||
const MOBILE_NAV_ITEMS: NavItem[] = [
|
const MOBILE_NAV_ITEMS: NavItem[] = [
|
||||||
{ to: "/", label: "Home", icon: "dashboard" },
|
{ to: "/", label: "Home", icon: "dashboard" },
|
||||||
{ to: "/projects", label: "Projects", icon: "projects" },
|
{
|
||||||
|
to: "/spaces",
|
||||||
|
label: "Spaces",
|
||||||
|
icon: "projects",
|
||||||
|
isGroup: true,
|
||||||
|
group: "spaces",
|
||||||
|
},
|
||||||
{ to: "/sessions", label: "Sessions", icon: "terminal" },
|
{ to: "/sessions", label: "Sessions", icon: "terminal" },
|
||||||
{ to: "/tools", label: "Tools", icon: "settings", isGroup: true },
|
{
|
||||||
|
to: "/tools",
|
||||||
|
label: "Tools",
|
||||||
|
icon: "settings",
|
||||||
|
isGroup: true,
|
||||||
|
group: "tools",
|
||||||
|
},
|
||||||
{ to: "/settings", label: "Settings", icon: "settings" },
|
{ to: "/settings", label: "Settings", icon: "settings" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MobileNav: React.FC<MobileNavProps> = ({ sessionCount }) => {
|
export const MobileNav: React.FC<MobileNavProps> = ({ sessionCount }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [toolsSheetOpen, setToolsSheetOpen] = useState(false);
|
const [toolsSheetOpen, setToolsSheetOpen] = useState(false);
|
||||||
|
const [spacesSheetOpen, setSpacesSheetOpen] = useState(false);
|
||||||
|
|
||||||
const isToolsActive =
|
const isToolsActive =
|
||||||
location.pathname === "/tool-workshop" ||
|
location.pathname === "/tool-workshop" ||
|
||||||
location.pathname === "/config-profiles";
|
location.pathname === "/config-profiles";
|
||||||
|
|
||||||
|
const isSpacesActive =
|
||||||
|
location.pathname.startsWith("/projects") ||
|
||||||
|
location.pathname.startsWith("/workspaces");
|
||||||
|
|
||||||
const handleNavClick = (item: NavItem) => {
|
const handleNavClick = (item: NavItem) => {
|
||||||
if (item.isGroup) {
|
if (!item.isGroup) return;
|
||||||
|
if (item.group === "spaces") {
|
||||||
|
setSpacesSheetOpen(true);
|
||||||
|
} else {
|
||||||
setToolsSheetOpen(true);
|
setToolsSheetOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="mobile-nav" role="navigation" aria-label="Mobile navigation">
|
<nav
|
||||||
|
className="mobile-nav"
|
||||||
|
role="navigation"
|
||||||
|
aria-label="Mobile navigation"
|
||||||
|
>
|
||||||
{MOBILE_NAV_ITEMS.map((item) => {
|
{MOBILE_NAV_ITEMS.map((item) => {
|
||||||
if (item.isGroup) {
|
if (item.isGroup) {
|
||||||
|
const isActive =
|
||||||
|
item.group === "spaces" ? isSpacesActive : isToolsActive;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item.to}
|
key={item.to}
|
||||||
className={`mobile-nav-item ${isToolsActive ? "active" : ""}`}
|
className={`mobile-nav-item ${isActive ? "active" : ""}`}
|
||||||
onClick={() => handleNavClick(item)}
|
onClick={() => handleNavClick(item)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@@ -82,6 +110,10 @@ export const MobileNav: React.FC<MobileNavProps> = ({ sessionCount }) => {
|
|||||||
isOpen={toolsSheetOpen}
|
isOpen={toolsSheetOpen}
|
||||||
onClose={() => setToolsSheetOpen(false)}
|
onClose={() => setToolsSheetOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<SpacesBottomSheet
|
||||||
|
isOpen={spacesSheetOpen}
|
||||||
|
onClose={() => setSpacesSheetOpen(false)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { Icon } from "../../icon";
|
||||||
|
|
||||||
|
interface SpacesBottomSheetProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SPACES_ITEMS = [
|
||||||
|
{ to: "/projects", label: "Projects" },
|
||||||
|
{ to: "/workspaces", label: "Workspaces" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SpacesBottomSheet: React.FC<SpacesBottomSheetProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const handleSelect = (to: string) => {
|
||||||
|
onClose();
|
||||||
|
navigate(to);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mobile-bottom-sheet-overlay"
|
||||||
|
onClick={onClose}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mobile-bottom-sheet"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Spaces menu"
|
||||||
|
>
|
||||||
|
<div className="mobile-bottom-sheet-header">
|
||||||
|
<div className="mobile-bottom-sheet-handle" />
|
||||||
|
<h3 className="mobile-bottom-sheet-title">Spaces</h3>
|
||||||
|
</div>
|
||||||
|
<div className="mobile-bottom-sheet-content">
|
||||||
|
{SPACES_ITEMS.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.to}
|
||||||
|
className={`mobile-bottom-sheet-item ${
|
||||||
|
location.pathname === item.to ? "active" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => handleSelect(item.to)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="mobile-bottom-sheet-item-label">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
{location.pathname === item.to && (
|
||||||
|
<Icon name="success" size="sm" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/session
|
dir: apps/web/src/components/features/session
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides UI components for managing development sessions including creation, listing, monitoring progress, and interacting with individual sessions.
|
Provides UI components for managing development sessions including creation, listing, and interacting with individual sessions.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.md
|
map: apps/web/src/components/features/.pi-map.md
|
||||||
@@ -12,7 +12,6 @@ map: apps/web/src/components/features/.pi-map.md
|
|||||||
- create-session-form.tsx
|
- create-session-form.tsx
|
||||||
- session-card.tsx
|
- session-card.tsx
|
||||||
- session-list.tsx
|
- session-list.tsx
|
||||||
- session-progress-panel.tsx
|
|
||||||
## links
|
## links
|
||||||
index: apps/web/src/components/features/session/.pi-map.index.md
|
index: apps/web/src/components/features/session/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/session/.pi-map.md
|
map: apps/web/src/components/features/session/.pi-map.md
|
||||||
|
|||||||
@@ -4,20 +4,18 @@ dir: apps/web/src/components/features/session
|
|||||||
index: apps/web/src/components/features/session/.pi-map.index.md
|
index: apps/web/src/components/features/session/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides UI components for managing development sessions including creation, listing, monitoring progress, and interacting with individual sessions.
|
Provides UI components for managing development sessions including creation, listing, and interacting with individual sessions.
|
||||||
## files
|
## files
|
||||||
- create-session-form.tsx | A React form component for creating and starting a new development session with configurable project, repository, workspace, tool type, config profile, and SSH key options. | exp: CreateSessionForm | dep: react, ../../icon, ../../../api/sessions, ../../../types, ../../../api/git-repositories, ../../../api/tool-types, ../../../api/ssh-keys, ../../../api/config-profiles, ../../../api/workspaces, ../../../types/workspace, icon, sessions API, git-repositories API, tool-types API, ssh-keys API, config-profiles API, workspaces API, types
|
- create-session-form.tsx | A React form component for creating and starting a new development session with configurable project, repository, workspace, tool type, config profile, and SSH key options. | exp: CreateSessionForm | dep: react, ../../icon, ../../../api/sessions, ../../../types, ../../../api/git-repositories, ../../../api/tool-types, ../../../api/ssh-keys, ../../../api/config-profiles, ../../../api/workspaces, ../../../types/workspace, icon, sessions API, git-repositories API, tool-types API, ssh-keys API, config-profiles API, workspaces API, types
|
||||||
- session-card.tsx | Renders a card component displaying session information with status badges, inline rename editing, action buttons, and responsive mobile/desktop layouts including a dropdown options menu and mobile action sheet. | exp: SessionCardProps, func:SessionCard({ session, onOpen, onStart, onStop, onDelete, onRecreateTunnel, onRename, isBusy = false, tunnelHealth = null, }: SessionCardProps), call:useState, call:useRef, call:useMobileViewport, call:session.tool_type_interfaces?.includes, call:[ "running", "building", "starting", "probing", "pending", "unhealthy", ].includes, call:useEffect, call:optionsRef.current.contains, call:setOptionsOpen, call:document.addEventListener, call:document.removeEventListener, call:window.confirm, call:onDelete, call:setEditName, call:onRename, call:setIsEditingName, call:editName.trim, call:onOpen, call:new Date(session.created_at).toLocaleString, call:setShowActionSheet, call:onStart, call:onStop, call:onRecreateTunnel | dep: react, ../../../api/sessions, ../../icon, ../../../hooks/use-mobile-viewport, ../mobile/mobile-action-sheet
|
- session-card.tsx | Renders a card component displaying session information with status badges, inline rename editing, action buttons, and responsive mobile/desktop layouts including a dropdown options menu and mobile action sheet. | exp: SessionCardProps, func:SessionCard({ session, onOpen, onStart, onStop, onDelete, onRecreateTunnel, onRename, isBusy = false, tunnelHealth = null, }: SessionCardProps), call:useState, call:useRef, call:useMobileViewport, call:session.tool_type_interfaces?.includes, call:[ "running", "building", "starting", "probing", "pending", "unhealthy", ].includes, call:useEffect, call:optionsRef.current.contains, call:setOptionsOpen, call:document.addEventListener, call:document.removeEventListener, call:window.confirm, call:onDelete, call:setEditName, call:onRename, call:setIsEditingName, call:editName.trim, call:onOpen, call:new Date(session.created_at).toLocaleString, call:setShowActionSheet, call:onStart, call:onStop, call:onRecreateTunnel | dep: react, ../../../api/sessions, ../../icon, ../../../hooks/use-mobile-viewport, ../mobile/mobile-action-sheet
|
||||||
- session-list.tsx | Renders a list of sessions grouped by active/recent status or as a flat grid, delegating to SessionCard for individual session display. | exp: SessionListProps, func:SessionList({ sessions, onOpen, onStart, onStop, onDelete, onRecreateTunnel, onRename, actionBusyId = null, tunnelHealth = {}, showGrouping = true, activeTitle = "Active Sessions", recentTitle = "Recent Sessions", maxRecent = 5, emptyMessage = "No sessions", }: SessionListProps), call:sessions.filter, call:activeStatuses.includes, call:sessions .filter((s) => recentStatuses.includes(s.status)) .slice, call:recentStatuses.includes, call:sessions.map, call:activeSessions.map, call:recentSessions.map | dep: ../../../api/sessions, ./session-card, Session, SessionCard, InstanceHealth
|
- session-list.tsx | Renders a list of sessions grouped by active/recent status or as a flat grid, delegating to SessionCard for individual session display. | exp: SessionListProps, func:SessionList({ sessions, onOpen, onStart, onStop, onDelete, onRecreateTunnel, onRename, actionBusyId = null, tunnelHealth = {}, showGrouping = true, activeTitle = "Active Sessions", recentTitle = "Recent Sessions", maxRecent = 5, emptyMessage = "No sessions", }: SessionListProps), call:sessions.filter, call:activeStatuses.includes, call:sessions .filter((s) => recentStatuses.includes(s.status)) .slice, call:recentStatuses.includes, call:sessions.map, call:activeSessions.map, call:recentSessions.map | dep: ../../../api/sessions, ./session-card, Session, SessionCard, InstanceHealth
|
||||||
- session-progress-panel.tsx | Renders a panel displaying active and recently completed session operations with step-by-step progress indicators and dismissible notifications. | exp: func:SessionProgressPanel(), call:useSessionOperations, call:useEventContext, call:useEffect, call:updateOperationFromEvent, call:operations.filter, call:Date.now, call:visibleOperations.map, call:dismissOperation | dep: react, ../../../state/session-operations, ../../../state/events, ../../icon, useSessionOperations, useEventContext, Icon
|
|
||||||
## arch
|
## arch
|
||||||
Feature-based component architecture with compound component pattern (SessionCard/SessionList/SessionProgressPanel), controlled forms with validation, optimistic UI updates for inline editing, responsive design with mobile/desktop layout variants, and compound action menus using dropdowns/sheets.
|
Feature-based component architecture with compound component pattern (SessionCard/SessionList), controlled forms with validation, optimistic UI updates for inline editing, responsive design with mobile/desktop layout variants, and compound action menus using dropdowns/sheets.
|
||||||
## tags
|
## tags
|
||||||
session, call:use, call:on, api, card, call:set, event, mobile
|
session, call:use, call:on, api, card, call:set, event, mobile
|
||||||
## symbols
|
## symbols
|
||||||
- SessionCard
|
- SessionCard
|
||||||
- SessionList
|
- SessionList
|
||||||
- SessionProgressPanel
|
|
||||||
- CreateSessionForm
|
- CreateSessionForm
|
||||||
- SessionCardProps
|
- SessionCardProps
|
||||||
- call:useState
|
- call:useState
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
|
import { LoadingOverlay } from "../../loading-overlay";
|
||||||
import {
|
import {
|
||||||
createInstance,
|
createInstance,
|
||||||
startInstance,
|
startInstance,
|
||||||
@@ -215,6 +216,7 @@ export const CreateSessionForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`create-session-form-wrapper ${className}`}>
|
<div className={`create-session-form-wrapper ${className}`}>
|
||||||
|
<LoadingOverlay visible={isSubmitting} label="Creating session..." />
|
||||||
<form onSubmit={handleSubmit} className="stack create-session-form">
|
<form onSubmit={handleSubmit} className="stack create-session-form">
|
||||||
{/* Project */}
|
{/* Project */}
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Session } from "../../../api/sessions";
|
|||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
import { useMobileViewport } from "../../../hooks/use-mobile-viewport";
|
import { useMobileViewport } from "../../../hooks/use-mobile-viewport";
|
||||||
import { MobileActionSheet } from "../mobile/mobile-action-sheet";
|
import { MobileActionSheet } from "../mobile/mobile-action-sheet";
|
||||||
|
import { LoadingOverlay } from "../../loading-overlay";
|
||||||
import type { IconName } from "../../icon";
|
import type { IconName } from "../../icon";
|
||||||
|
|
||||||
export interface SessionCardProps {
|
export interface SessionCardProps {
|
||||||
@@ -14,6 +15,7 @@ export interface SessionCardProps {
|
|||||||
onRecreateTunnel?: (session: Session) => void;
|
onRecreateTunnel?: (session: Session) => void;
|
||||||
onRename?: (session: Session, newName: string) => void;
|
onRename?: (session: Session, newName: string) => void;
|
||||||
isBusy?: boolean;
|
isBusy?: boolean;
|
||||||
|
busyLabel?: string;
|
||||||
tunnelHealth?: {
|
tunnelHealth?: {
|
||||||
healthy: boolean;
|
healthy: boolean;
|
||||||
container_status: string;
|
container_status: string;
|
||||||
@@ -46,6 +48,7 @@ export function SessionCard({
|
|||||||
onRecreateTunnel,
|
onRecreateTunnel,
|
||||||
onRename,
|
onRename,
|
||||||
isBusy = false,
|
isBusy = false,
|
||||||
|
busyLabel = "Working...",
|
||||||
tunnelHealth = null,
|
tunnelHealth = null,
|
||||||
}: SessionCardProps) {
|
}: SessionCardProps) {
|
||||||
const [showActionSheet, setShowActionSheet] = useState(false);
|
const [showActionSheet, setShowActionSheet] = useState(false);
|
||||||
@@ -106,6 +109,7 @@ export function SessionCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="card session-card">
|
<article className="card session-card">
|
||||||
|
<LoadingOverlay visible={isBusy} label={busyLabel} />
|
||||||
<div className="session-card-content">
|
<div className="session-card-content">
|
||||||
<div className="session-card-header">
|
<div className="session-card-header">
|
||||||
<div className="session-card-title">
|
<div className="session-card-title">
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import {
|
|
||||||
useSessionOperations,
|
|
||||||
type Operation,
|
|
||||||
} from "../../../state/session-operations";
|
|
||||||
import { useEventContext } from "../../../state/events";
|
|
||||||
import { Icon } from "../../icon";
|
|
||||||
|
|
||||||
interface StepConfig {
|
|
||||||
label: string;
|
|
||||||
index: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const steps: StepConfig[] = [
|
|
||||||
{ label: "Created", index: 1 },
|
|
||||||
{ label: "Building", index: 2 },
|
|
||||||
{ label: "Starting", index: 3 },
|
|
||||||
{ label: "Ready", index: 4 },
|
|
||||||
];
|
|
||||||
|
|
||||||
function OperationItem({
|
|
||||||
operation,
|
|
||||||
onDismiss,
|
|
||||||
}: {
|
|
||||||
operation: Operation;
|
|
||||||
onDismiss: () => void;
|
|
||||||
}) {
|
|
||||||
const isDone = operation.status === "success" || operation.status === "error";
|
|
||||||
const isError = operation.status === "error";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`session-operation-item ${operation.status}`}
|
|
||||||
role="status"
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
<div className="session-operation-header">
|
|
||||||
<div className="session-operation-title">
|
|
||||||
{isError ? (
|
|
||||||
<Icon name="error" size="sm" />
|
|
||||||
) : isDone ? (
|
|
||||||
<Icon name="success" size="sm" />
|
|
||||||
) : (
|
|
||||||
<Icon name="loading" size="sm" />
|
|
||||||
)}
|
|
||||||
<span className="session-operation-name">{operation.displayName}</span>
|
|
||||||
</div>
|
|
||||||
{isDone && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="session-operation-dismiss"
|
|
||||||
onClick={onDismiss}
|
|
||||||
aria-label="Dismiss"
|
|
||||||
>
|
|
||||||
<Icon name="close" size="sm" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="session-operation-message">{operation.message}</p>
|
|
||||||
<div className="session-operation-steps">
|
|
||||||
{steps.map((step) => {
|
|
||||||
const active = operation.step >= step.index;
|
|
||||||
const current = operation.step === step.index && !isDone;
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={step.label}
|
|
||||||
className={`session-operation-step ${active ? "active" : ""} ${current ? "current" : ""}`}
|
|
||||||
>
|
|
||||||
{step.label}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SessionProgressPanel() {
|
|
||||||
const { operations, updateOperationFromEvent, dismissOperation } =
|
|
||||||
useSessionOperations();
|
|
||||||
const { events } = useEventContext();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (events.length === 0) return;
|
|
||||||
const latestEvent = events[events.length - 1];
|
|
||||||
updateOperationFromEvent(latestEvent);
|
|
||||||
}, [events, updateOperationFromEvent]);
|
|
||||||
|
|
||||||
const visibleOperations = operations.filter(
|
|
||||||
(op) =>
|
|
||||||
op.status === "pending" ||
|
|
||||||
op.status === "active" ||
|
|
||||||
(op.status === "success" && Date.now() - op.createdAt < 5000) ||
|
|
||||||
op.status === "error",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (visibleOperations.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="session-progress-panel" role="region" aria-label="Session operations">
|
|
||||||
<div className="session-progress-panel-header">
|
|
||||||
<span className="session-progress-panel-title">Operations</span>
|
|
||||||
</div>
|
|
||||||
<div className="session-progress-panel-list">
|
|
||||||
{visibleOperations.map((operation) => (
|
|
||||||
<OperationItem
|
|
||||||
key={operation.id}
|
|
||||||
operation={operation}
|
|
||||||
onDismiss={() => dismissOperation(operation.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/terminal
|
dir: apps/web/src/components/features/terminal
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides cross-platform terminal UI components for desktop and mobile with session management, special key input, and xterm.js WebSocket connectivity.
|
Provides a cross-platform terminal emulator interface with WebSocket backend connectivity, supporting multiple sessions, special key input, and responsive desktop/mobile layouts.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.md
|
map: apps/web/src/components/features/.pi-map.md
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ dir: apps/web/src/components/features/terminal
|
|||||||
index: apps/web/src/components/features/terminal/.pi-map.index.md
|
index: apps/web/src/components/features/terminal/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides cross-platform terminal UI components for desktop and mobile with session management, special key input, and xterm.js WebSocket connectivity.
|
Provides a cross-platform terminal emulator interface with WebSocket backend connectivity, supporting multiple sessions, special key input, and responsive desktop/mobile layouts.
|
||||||
## files
|
## files
|
||||||
- DesktopTerminalView.tsx | Renders a desktop-optimized terminal view with session tabs, fullscreen mode, font controls, and reset confirmation dialog | exp: DesktopTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../../api/terminal, React, TerminalComponent, TerminalSessionTabs, TerminalSession type
|
- DesktopTerminalView.tsx | Renders a desktop-optimized terminal view with session tabs, fullscreen mode, font controls, and reset confirmation dialog | exp: DesktopTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../../api/terminal, React, TerminalComponent, TerminalSessionTabs, TerminalSession type
|
||||||
- MobileTerminalView.tsx | Renders a mobile-optimized terminal interface with toolbar, session tabs, terminal output, and special keys input controls. | exp: MobileTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../icon, ./special-keys-strip, ./special-keys-panel, ../../../hooks/use-special-keys, ../../../api/terminal
|
- MobileTerminalView.tsx | Renders a mobile-optimized terminal interface with toolbar, session tabs, terminal output, and special keys input controls. | exp: MobileTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../icon, ./special-keys-strip, ./special-keys-panel, ../../../hooks/use-special-keys, ../../../api/terminal
|
||||||
@@ -12,9 +12,9 @@ Provides cross-platform terminal UI components for desktop and mobile with sessi
|
|||||||
- special-keys-strip.tsx | Renders a strip of buttons for sending special keyboard keys (Esc, Tab, arrows, etc.) with optional modifier support for a terminal interface. | exp: SpecialKeysStrip | dep: react, ../../../hooks/use-special-keys, React, use-special-keys hook
|
- special-keys-strip.tsx | Renders a strip of buttons for sending special keyboard keys (Esc, Tab, arrows, etc.) with optional modifier support for a terminal interface. | exp: SpecialKeysStrip | dep: react, ../../../hooks/use-special-keys, React, use-special-keys hook
|
||||||
- terminal-session-tabs.test.tsx | Tests a React component that renders interactive terminal session tabs with selection, close confirmation, renaming, creation limits, and status indicators. | dep: @testing-library/react, vitest, ./terminal-session-tabs
|
- terminal-session-tabs.test.tsx | Tests a React component that renders interactive terminal session tabs with selection, close confirmation, renaming, creation limits, and status indicators. | dep: @testing-library/react, vitest, ./terminal-session-tabs
|
||||||
- terminal-session-tabs.tsx | Renders a tabbed interface for managing multiple terminal sessions with selection, creation, renaming, and close confirmation features. | exp: TerminalSessionInfo, TerminalSessionTabsProps, TerminalSessionTabs | dep: react, React
|
- terminal-session-tabs.tsx | Renders a tabbed interface for managing multiple terminal sessions with selection, creation, renaming, and close confirmation features. | exp: TerminalSessionInfo, TerminalSessionTabsProps, TerminalSessionTabs | dep: react, React
|
||||||
- terminal.tsx | React component that renders an interactive xterm.js terminal with WebSocket connectivity, mobile touch support, flow control, heartbeat monitoring, and reconnection logic. | exp: TerminalProps, TerminalRef, TerminalComponent | dep: react, xterm, xterm-addon-fit, xterm-addon-web-links, xterm/css/xterm.css, ../../../hooks/use-special-keys, React, use-special-keys hook
|
- terminal.tsx | A React component that renders an interactive terminal emulator using xterm.js, connecting to a WebSocket backend with features like mobile touch scrolling, font resizing, flow control, heartbeat monitoring, and automatic reconnection. | exp: TerminalProps, TerminalRef, TerminalComponent | dep: react, xterm, xterm-addon-fit, xterm-addon-web-links, xterm/css/xterm.css, ../../../hooks/use-special-keys
|
||||||
## arch
|
## arch
|
||||||
Component-based architecture with platform-specific view variants (Desktop/Mobile), shared session tabs state management, modular special key input panels, and a core terminal component handling WebSocket lifecycle with heartbeat/reconnection logic.
|
Component-based architecture with platform-specific view variants (Desktop/Mobile), compound component pattern for terminal subsystems (tabs, special keys, xterm.js integration), and WebSocket-based real-time communication with reconnection and heartbeat monitoring.
|
||||||
## tags
|
## tags
|
||||||
terminal, special, keys, session, react, tabs, view, renders
|
terminal, special, keys, session, react, tabs, view, renders
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -355,13 +355,41 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
// In normal mode xterm.js has a scrollable viewport; in alternate
|
// In normal mode xterm.js has a scrollable viewport; in alternate
|
||||||
// screen (tmux/vim) there is no scrollback and the only way to
|
// screen (tmux/vim) there is no scrollback and the only way to
|
||||||
// scroll is to send mouse-wheel protocol sequences to the
|
// scroll is to send mouse-wheel protocol sequences to the
|
||||||
// application. We detect which situation we're in by checking
|
// application. We detect the active buffer by reference equality
|
||||||
// whether the viewport has scrollable height.
|
// (term.buffer.active === term.buffer.alternate) because the
|
||||||
|
// `type` string can be unreliable in some xterm.js versions.
|
||||||
let touchCleanup: (() => void) | undefined;
|
let touchCleanup: (() => void) | undefined;
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
let startY = 0;
|
let startY = 0;
|
||||||
let startX = 0;
|
let startX = 0;
|
||||||
let isScrolling = false;
|
let isScrolling = false;
|
||||||
|
let debugOverlay: HTMLDivElement | null = null;
|
||||||
|
const showDebug = (msg: string) => {
|
||||||
|
if (!debugOverlay) {
|
||||||
|
debugOverlay = document.createElement("div");
|
||||||
|
debugOverlay.style.position = "fixed";
|
||||||
|
debugOverlay.style.bottom = "120px";
|
||||||
|
debugOverlay.style.left = "8px";
|
||||||
|
debugOverlay.style.right = "8px";
|
||||||
|
debugOverlay.style.zIndex = "9999";
|
||||||
|
debugOverlay.style.background = "rgba(0,0,0,0.8)";
|
||||||
|
debugOverlay.style.color = "#0f0";
|
||||||
|
debugOverlay.style.fontFamily = "monospace";
|
||||||
|
debugOverlay.style.fontSize = "11px";
|
||||||
|
debugOverlay.style.padding = "8px";
|
||||||
|
debugOverlay.style.borderRadius = "6px";
|
||||||
|
debugOverlay.style.pointerEvents = "none";
|
||||||
|
debugOverlay.style.whiteSpace = "pre-wrap";
|
||||||
|
debugOverlay.style.maxHeight = "120px";
|
||||||
|
debugOverlay.style.overflow = "auto";
|
||||||
|
document.body.appendChild(debugOverlay);
|
||||||
|
}
|
||||||
|
const line = `${new Date().toLocaleTimeString()} ${msg}`;
|
||||||
|
debugOverlay.textContent =
|
||||||
|
line +
|
||||||
|
"\n" +
|
||||||
|
debugOverlay.textContent?.split("\n").slice(0, 8).join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
const onTouchStart = (e: TouchEvent) => {
|
const onTouchStart = (e: TouchEvent) => {
|
||||||
if (e.touches.length === 1) {
|
if (e.touches.length === 1) {
|
||||||
@@ -375,46 +403,49 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
const deltaY = startY - touch.clientY;
|
const deltaY = startY - touch.clientY;
|
||||||
const deltaX = Math.abs(startX - touch.clientX);
|
const deltaX = Math.abs(startX - touch.clientX);
|
||||||
|
showDebug(`move dy=${deltaY.toFixed(0)} dx=${deltaX.toFixed(0)} scrolling=${isScrolling}`);
|
||||||
|
|
||||||
|
// Decide early whether this is a vertical scroll gesture.
|
||||||
if (!isScrolling) {
|
if (!isScrolling) {
|
||||||
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 4) {
|
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 2) {
|
||||||
isScrolling = true;
|
isScrolling = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isScrolling) {
|
if (!isScrolling) return;
|
||||||
e.preventDefault();
|
|
||||||
const viewport = container.querySelector(
|
|
||||||
".xterm-viewport",
|
|
||||||
) as HTMLElement | null;
|
|
||||||
if (!viewport) return;
|
|
||||||
|
|
||||||
// If the viewport is scrollable, scroll it directly.
|
// Take over the gesture so the browser never scrolls the
|
||||||
// Otherwise we are in alternate screen (tmux/vim) and must
|
// page or the terminal canvas. We synthesize wheel events
|
||||||
// send SGR 1006 mouse-wheel protocol data.
|
// and dispatch them into xterm.js, which already handles
|
||||||
const hasScrollback = viewport.scrollHeight > viewport.clientHeight;
|
// both normal buffer scrolling and alternate-screen apps
|
||||||
if (hasScrollback) {
|
// (tmux/vim) correctly.
|
||||||
viewport.scrollTop += deltaY;
|
e.preventDefault();
|
||||||
} else {
|
|
||||||
const ws = wsRef.current;
|
const target = container.querySelector(".xterm-viewport") as
|
||||||
if (ws?.readyState === WebSocket.OPEN && termRef.current) {
|
| HTMLElement
|
||||||
// Use the cursor position as the wheel location so
|
| null;
|
||||||
// tmux knows which pane to scroll.
|
if (target) {
|
||||||
const buf = termRef.current.buffer.active;
|
showDebug(`dispatch wheel deltaY=${deltaY.toFixed(0)}`);
|
||||||
const col = buf.cursorX + 1;
|
const wheel = new WheelEvent("wheel", {
|
||||||
const row = buf.cursorY + 1;
|
deltaY,
|
||||||
// SGR 1006: 64 = wheel-up, 65 = wheel-down
|
deltaX: 0,
|
||||||
const btn = deltaY > 0 ? 64 : 65;
|
deltaZ: 0,
|
||||||
ws.send(`\x1b[<${btn};${col};${row}M`);
|
clientX: touch.clientX,
|
||||||
}
|
clientY: touch.clientY,
|
||||||
|
screenX: touch.screenX,
|
||||||
|
screenY: touch.screenY,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
target.dispatchEvent(wheel);
|
||||||
}
|
}
|
||||||
startY = touch.clientY;
|
startY = touch.clientY;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const onTouchEnd = () => {
|
const onTouchEnd = () => {
|
||||||
isScrolling = false;
|
isScrolling = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
container.addEventListener("touchstart", onTouchStart, {
|
container.addEventListener("touchstart", onTouchStart, {
|
||||||
passive: true,
|
passive: false,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
container.addEventListener("touchmove", onTouchMove, {
|
container.addEventListener("touchmove", onTouchMove, {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
|
import { LoadingOverlay } from "../../loading-overlay";
|
||||||
import type { ToolInstance } from "../../../api/sessions";
|
import type { ToolInstance } from "../../../api/sessions";
|
||||||
import {
|
import {
|
||||||
deleteInstance,
|
deleteInstance,
|
||||||
@@ -60,6 +61,7 @@ export const InstanceList = ({
|
|||||||
|
|
||||||
// Per-instance busy state for actions
|
// Per-instance busy state for actions
|
||||||
const [busyInstanceId, setBusyInstanceId] = useState<string | null>(null);
|
const [busyInstanceId, setBusyInstanceId] = useState<string | null>(null);
|
||||||
|
const [busyLabel, setBusyLabel] = useState("Working...");
|
||||||
|
|
||||||
const loadInstances = useCallback(async () => {
|
const loadInstances = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -135,6 +137,7 @@ export const InstanceList = ({
|
|||||||
sshKeyIds?: string[],
|
sshKeyIds?: string[],
|
||||||
) => {
|
) => {
|
||||||
setBusyInstanceId(instanceId);
|
setBusyInstanceId(instanceId);
|
||||||
|
setBusyLabel("Starting...");
|
||||||
try {
|
try {
|
||||||
await startInstance(
|
await startInstance(
|
||||||
projectId,
|
projectId,
|
||||||
@@ -156,6 +159,7 @@ export const InstanceList = ({
|
|||||||
|
|
||||||
const handleStop = async (instanceId: string) => {
|
const handleStop = async (instanceId: string) => {
|
||||||
setBusyInstanceId(instanceId);
|
setBusyInstanceId(instanceId);
|
||||||
|
setBusyLabel("Stopping...");
|
||||||
try {
|
try {
|
||||||
await stopInstance(projectId, repoId, instanceId);
|
await stopInstance(projectId, repoId, instanceId);
|
||||||
setStopConfirmId(null);
|
setStopConfirmId(null);
|
||||||
@@ -173,6 +177,7 @@ export const InstanceList = ({
|
|||||||
sshKeyIds?: string[],
|
sshKeyIds?: string[],
|
||||||
) => {
|
) => {
|
||||||
setBusyInstanceId(instanceId);
|
setBusyInstanceId(instanceId);
|
||||||
|
setBusyLabel("Restarting...");
|
||||||
try {
|
try {
|
||||||
await restartInstance(
|
await restartInstance(
|
||||||
projectId,
|
projectId,
|
||||||
@@ -195,6 +200,7 @@ export const InstanceList = ({
|
|||||||
const handleDelete = async (instanceId: string) => {
|
const handleDelete = async (instanceId: string) => {
|
||||||
if (!confirm("Are you sure you want to delete this instance?")) return;
|
if (!confirm("Are you sure you want to delete this instance?")) return;
|
||||||
setBusyInstanceId(instanceId);
|
setBusyInstanceId(instanceId);
|
||||||
|
setBusyLabel("Deleting...");
|
||||||
try {
|
try {
|
||||||
await deleteInstance(projectId, repoId, instanceId);
|
await deleteInstance(projectId, repoId, instanceId);
|
||||||
// Update state immediately instead of reloading
|
// Update state immediately instead of reloading
|
||||||
@@ -250,6 +256,10 @@ export const InstanceList = ({
|
|||||||
<div className="instance-grid">
|
<div className="instance-grid">
|
||||||
{instances.map((instance) => (
|
{instances.map((instance) => (
|
||||||
<div key={instance.id} className="instance-card">
|
<div key={instance.id} className="instance-card">
|
||||||
|
<LoadingOverlay
|
||||||
|
visible={busyInstanceId === instance.id}
|
||||||
|
label={busyLabel}
|
||||||
|
/>
|
||||||
<div className="instance-info">
|
<div className="instance-info">
|
||||||
<div className="instance-name">{instance.display_name}</div>
|
<div className="instance-name">{instance.display_name}</div>
|
||||||
<div className="instance-meta">
|
<div className="instance-meta">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
|
import { LoadingOverlay } from "../../loading-overlay";
|
||||||
import { listToolTypes, type ToolType } from "../../../api/tool-types";
|
import { listToolTypes, type ToolType } from "../../../api/tool-types";
|
||||||
import {
|
import {
|
||||||
listConfigProfiles,
|
listConfigProfiles,
|
||||||
@@ -9,7 +10,6 @@ import {
|
|||||||
} from "../../../api/config-profiles";
|
} from "../../../api/config-profiles";
|
||||||
import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys";
|
import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys";
|
||||||
import { useSessions } from "../../../state/sessions";
|
import { useSessions } from "../../../state/sessions";
|
||||||
import { useSessionOperations } from "../../../state/session-operations";
|
|
||||||
import type { Workspace } from "../../../types/workspace";
|
import type { Workspace } from "../../../types/workspace";
|
||||||
import type { ToolInstance } from "../../../api/sessions";
|
import type { ToolInstance } from "../../../api/sessions";
|
||||||
|
|
||||||
@@ -25,7 +25,6 @@ export function ToolStarter({
|
|||||||
onCancel,
|
onCancel,
|
||||||
}: ToolStarterProps) {
|
}: ToolStarterProps) {
|
||||||
const { addOrUpdateSession } = useSessions();
|
const { addOrUpdateSession } = useSessions();
|
||||||
const { startOperation } = useSessionOperations();
|
|
||||||
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
||||||
const [toolTypesLoading, setToolTypesLoading] = useState(true);
|
const [toolTypesLoading, setToolTypesLoading] = useState(true);
|
||||||
const [toolTypesError, setToolTypesError] = useState<string | null>(null);
|
const [toolTypesError, setToolTypesError] = useState<string | null>(null);
|
||||||
@@ -156,7 +155,6 @@ export function ToolStarter({
|
|||||||
status: instance.status || "pending",
|
status: instance.status || "pending",
|
||||||
url: instance.url || null,
|
url: instance.url || null,
|
||||||
});
|
});
|
||||||
startOperation("create", instance.id, instance.display_name);
|
|
||||||
onStarted(instance);
|
onStarted(instance);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to start tool");
|
setError(err instanceof Error ? err.message : "Failed to start tool");
|
||||||
@@ -171,11 +169,11 @@ export function ToolStarter({
|
|||||||
onStarted,
|
onStarted,
|
||||||
toolTypes,
|
toolTypes,
|
||||||
addOrUpdateSession,
|
addOrUpdateSession,
|
||||||
startOperation,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tool-starter">
|
<div className="tool-starter">
|
||||||
|
<LoadingOverlay visible={starting} label="Starting tool..." />
|
||||||
{/* Context header — read-only workspace info */}
|
{/* Context header — read-only workspace info */}
|
||||||
<div className="tool-starter-context">
|
<div className="tool-starter-context">
|
||||||
<div className="context-row">
|
<div className="context-row">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/workspace
|
dir: apps/web/src/components/features/workspace
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides React UI components for workspace management, navigation, and detail views in a web-based development environment.
|
Provides the complete UI component suite for workspace management, including creation, detail viewing, file editing, Git operations, tool management, and settings.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.md
|
map: apps/web/src/components/features/.pi-map.md
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ dir: apps/web/src/components/features/workspace
|
|||||||
index: apps/web/src/components/features/workspace/.pi-map.index.md
|
index: apps/web/src/components/features/workspace/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides React UI components for workspace management, navigation, and detail views in a web-based development environment.
|
Provides the complete UI component suite for workspace management, including creation, detail viewing, file editing, Git operations, tool management, and settings.
|
||||||
## files
|
## files
|
||||||
- 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-card.tsx | React component that renders a card displaying 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-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 showing breadcrumb navigation and branch information. | exp: WorkspaceDetailHeaderProps, func:WorkspaceDetailHeader({ workspace, }: WorkspaceDetailHeaderProps) | dep: ../../icon, react
|
- workspace-detail-header.tsx | Renders a header component for a workspace detail page showing breadcrumb navigation and branch information. | exp: WorkspaceDetailHeaderProps, func:WorkspaceDetailHeader({ workspace, }: WorkspaceDetailHeaderProps) | dep: ../../icon, react
|
||||||
- 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
|
- 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
|
||||||
@@ -17,7 +17,7 @@ Provides React UI components for workspace management, navigation, and detail vi
|
|||||||
- workspace-tab-bar.tsx | Renders desktop and mobile tab bar components for workspace navigation with files, git, tools, and settings tabs. | exp: WorkspaceTab, func:WorkspaceTabBar({ active, onChange }: WorkspaceTabBarProps), call:TABS.map, call:onChange, func:WorkspaceMobileTabBar({ active, onChange, }: WorkspaceTabBarProps), call:TABS.map, call:onChange | dep: ../../icon, react
|
- workspace-tab-bar.tsx | Renders desktop and mobile tab bar components for workspace navigation with files, git, tools, and settings tabs. | exp: WorkspaceTab, func:WorkspaceTabBar({ active, onChange }: WorkspaceTabBarProps), call:TABS.map, call:onChange, func:WorkspaceMobileTabBar({ active, onChange, }: WorkspaceTabBarProps), call:TABS.map, call:onChange | dep: ../../icon, react
|
||||||
- workspace-tools-panel.tsx | Renders a tools management panel for a workspace that displays running tool instances and allows starting new tools via a 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, workspace types
|
- workspace-tools-panel.tsx | Renders a tools management panel for a workspace that displays running tool instances and allows starting new tools via a 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, workspace types
|
||||||
## arch
|
## arch
|
||||||
Feature-based component organization with page-specific composite components (header/detail/settings) and reusable atomic pieces (cards, chips, forms), following a panel/tab architecture for workspace detail layout.
|
Feature-based component organization with compound page pattern (header/tab-bar/panels), form abstraction for creation, and modal-driven tool management with responsive mobile/desktop tab navigation.
|
||||||
## tags
|
## tags
|
||||||
workspace, call:set, call:use, panel, git, branch, react, call:on
|
workspace, call:set, call:use, panel, git, branch, react, call:on
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -29,47 +29,71 @@ export function WorkspaceCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={`card workspace-card ${loading ? "loading" : ""}`}>
|
<article className={`card workspace-card ${loading ? "loading" : ""}`}>
|
||||||
|
<div className="workspace-card-top">
|
||||||
<Link
|
<Link
|
||||||
to={`/workspaces/${workspace.id}`}
|
to={`/workspaces/${workspace.id}`}
|
||||||
className="workspace-header-link"
|
className="workspace-title-link"
|
||||||
>
|
>
|
||||||
<div className="workspace-header">
|
<div className="workspace-title-stack">
|
||||||
<h4>{workspace.name}</h4>
|
<div className="workspace-title-row">
|
||||||
|
<h4 className="workspace-name">{workspace.name}</h4>
|
||||||
<span className={`status-badge ${statusClass}`}>
|
<span className={`status-badge ${statusClass}`}>
|
||||||
{workspace.status}
|
{workspace.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="workspace-project-name">
|
||||||
|
<Icon name="projects" size="sm" />
|
||||||
|
{workspace.project_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="workspace-meta">
|
</div>
|
||||||
<p className="workspace-project">
|
|
||||||
{workspace.project_name} / {workspace.repo_name}
|
<div className="workspace-card-body">
|
||||||
</p>
|
<div className="workspace-meta-row">
|
||||||
<p className="workspace-branch">
|
<span className="workspace-repo">
|
||||||
<Icon name="branch" size="sm" /> {workspace.branch}
|
<Icon name="repositories" size="sm" />
|
||||||
</p>
|
{workspace.repo_name}
|
||||||
|
</span>
|
||||||
|
<span className="workspace-branch">
|
||||||
|
<Icon name="branch" size="sm" />
|
||||||
|
{workspace.branch}
|
||||||
|
</span>
|
||||||
|
{workspace.instance_count > 0 && (
|
||||||
|
<span className="workspace-instance-count">
|
||||||
|
{workspace.instance_count} tool
|
||||||
|
{workspace.instance_count > 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<WorkspaceInstanceChips workspaceId={workspace.id} />
|
<WorkspaceInstanceChips workspaceId={workspace.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="workspace-actions">
|
<div className="workspace-actions">
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={() => onStartTool(workspace)}
|
onClick={() => onStartTool(workspace)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
title="Start a tool in this workspace"
|
||||||
>
|
>
|
||||||
<Icon name="play" size="sm" /> Start Tool
|
<Icon name="play" size="sm" />
|
||||||
|
Start Tool
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary"
|
className="ghost-button"
|
||||||
onClick={() => onSync(workspace)}
|
onClick={() => onSync(workspace)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
title="Sync workspace"
|
||||||
>
|
>
|
||||||
<Icon name="refresh" size="sm" /> Sync
|
<Icon name="refresh" size="sm" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-danger"
|
className="ghost-button danger-text"
|
||||||
onClick={() => onDelete(workspace)}
|
onClick={() => onDelete(workspace)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
title="Delete workspace"
|
||||||
>
|
>
|
||||||
<Icon name="delete" size="sm" /> Delete
|
<Icon name="delete" size="sm" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Icon } from "./icon";
|
||||||
|
|
||||||
|
interface LoadingOverlayProps {
|
||||||
|
visible: boolean;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingOverlay({ visible, label }: LoadingOverlayProps) {
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="loading-overlay" aria-live="polite">
|
||||||
|
<div className="loading-overlay-content">
|
||||||
|
<Icon
|
||||||
|
name="loading"
|
||||||
|
size="lg"
|
||||||
|
className="icon-spin"
|
||||||
|
ariaLabel={label}
|
||||||
|
/>
|
||||||
|
{label && <span className="loading-overlay-label">{label}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -30,17 +30,25 @@ describe("useEvents", () => {
|
|||||||
return EventSource.OPEN;
|
return EventSource.OPEN;
|
||||||
},
|
},
|
||||||
url: "http://localhost:8000/events/stream",
|
url: "http://localhost:8000/events/stream",
|
||||||
addEventListener: vi.fn((type: string, handler: (e: MessageEvent) => void) => {
|
addEventListener: vi.fn(
|
||||||
|
(type: string, handler: (e: MessageEvent) => void) => {
|
||||||
if (!listeners.has(type)) listeners.set(type, new Set());
|
if (!listeners.has(type)) listeners.set(type, new Set());
|
||||||
listeners.get(type)!.add(handler);
|
listeners.get(type)!.add(handler);
|
||||||
}),
|
|
||||||
removeEventListener: vi.fn((type: string, handler: (e: MessageEvent) => void) => {
|
|
||||||
listeners.get(type)?.delete(handler);
|
|
||||||
}),
|
|
||||||
emit: (type: string, data: string) => {
|
|
||||||
listeners.get(type)?.forEach((handler) => handler({ data } as MessageEvent));
|
|
||||||
},
|
},
|
||||||
} as unknown as EventSource & { emit: (type: string, data: string) => void };
|
),
|
||||||
|
removeEventListener: vi.fn(
|
||||||
|
(type: string, handler: (e: MessageEvent) => void) => {
|
||||||
|
listeners.get(type)?.delete(handler);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
emit: (type: string, data: string) => {
|
||||||
|
listeners
|
||||||
|
.get(type)
|
||||||
|
?.forEach((handler) => handler({ data } as MessageEvent));
|
||||||
|
},
|
||||||
|
} as unknown as EventSource & {
|
||||||
|
emit: (type: string, data: string) => void;
|
||||||
|
};
|
||||||
mockedCreateEventSource.mockReturnValue(mockEs);
|
mockedCreateEventSource.mockReturnValue(mockEs);
|
||||||
mockedProbeEventStreamStatus.mockResolvedValue(null);
|
mockedProbeEventStreamStatus.mockResolvedValue(null);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from "../api/sessions";
|
} from "../api/sessions";
|
||||||
import type { Session } from "../api/sessions";
|
import type { Session } from "../api/sessions";
|
||||||
import { useSessions } from "../state/sessions";
|
import { useSessions } from "../state/sessions";
|
||||||
import { useSessionOperations } from "../state/session-operations";
|
|
||||||
|
|
||||||
interface UseInstanceActionsOptions {
|
interface UseInstanceActionsOptions {
|
||||||
onRefresh: () => Promise<void>;
|
onRefresh: () => Promise<void>;
|
||||||
@@ -33,7 +32,6 @@ export function useInstanceActions(
|
|||||||
): UseInstanceActionsReturn {
|
): UseInstanceActionsReturn {
|
||||||
const { onRefresh } = options;
|
const { onRefresh } = options;
|
||||||
const { removeSession } = useSessions();
|
const { removeSession } = useSessions();
|
||||||
const { startOperation, completeOperation } = useSessionOperations();
|
|
||||||
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
|
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
|
||||||
const [dirtyDeleteSession, setDirtyDeleteSession] = useState<Session | null>(
|
const [dirtyDeleteSession, setDirtyDeleteSession] = useState<Session | null>(
|
||||||
null,
|
null,
|
||||||
@@ -66,7 +64,6 @@ export function useInstanceActions(
|
|||||||
async (session: Session) => {
|
async (session: Session) => {
|
||||||
if (loadingSessionId === session.id) return;
|
if (loadingSessionId === session.id) return;
|
||||||
setLoadingSessionId(session.id);
|
setLoadingSessionId(session.id);
|
||||||
startOperation("start", session.id, session.display_name);
|
|
||||||
try {
|
try {
|
||||||
await startInstance(
|
await startInstance(
|
||||||
session.project_id,
|
session.project_id,
|
||||||
@@ -74,20 +71,17 @@ export function useInstanceActions(
|
|||||||
session.id,
|
session.id,
|
||||||
);
|
);
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
} catch {
|
|
||||||
completeOperation(session.id, "start", "error");
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSessionId(null);
|
setLoadingSessionId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[loadingSessionId, onRefresh, startOperation, completeOperation],
|
[loadingSessionId, onRefresh],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleStop = useCallback(
|
const handleStop = useCallback(
|
||||||
async (session: Session) => {
|
async (session: Session) => {
|
||||||
if (loadingSessionId === session.id) return;
|
if (loadingSessionId === session.id) return;
|
||||||
setLoadingSessionId(session.id);
|
setLoadingSessionId(session.id);
|
||||||
startOperation("stop", session.id, session.display_name);
|
|
||||||
try {
|
try {
|
||||||
await stopInstance(
|
await stopInstance(
|
||||||
session.project_id,
|
session.project_id,
|
||||||
@@ -95,20 +89,17 @@ export function useInstanceActions(
|
|||||||
session.id,
|
session.id,
|
||||||
);
|
);
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
} catch {
|
|
||||||
completeOperation(session.id, "stop", "error");
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSessionId(null);
|
setLoadingSessionId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[loadingSessionId, onRefresh, startOperation, completeOperation],
|
[loadingSessionId, onRefresh],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
async (session: Session) => {
|
async (session: Session) => {
|
||||||
if (loadingSessionId === session.id) return;
|
if (loadingSessionId === session.id) return;
|
||||||
setLoadingSessionId(session.id);
|
setLoadingSessionId(session.id);
|
||||||
startOperation("delete", session.id, session.display_name);
|
|
||||||
try {
|
try {
|
||||||
await deleteInstance(
|
await deleteInstance(
|
||||||
session.project_id,
|
session.project_id,
|
||||||
@@ -120,7 +111,6 @@ export function useInstanceActions(
|
|||||||
removeSession(session.id);
|
removeSession(session.id);
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
completeOperation(session.id, "delete", "error");
|
|
||||||
const axiosError = error as {
|
const axiosError = error as {
|
||||||
response?: {
|
response?: {
|
||||||
status?: number;
|
status?: number;
|
||||||
@@ -139,20 +129,13 @@ export function useInstanceActions(
|
|||||||
setLoadingSessionId(null);
|
setLoadingSessionId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[loadingSessionId, onRefresh, removeSession],
|
||||||
loadingSessionId,
|
|
||||||
onRefresh,
|
|
||||||
removeSession,
|
|
||||||
startOperation,
|
|
||||||
completeOperation,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleForceDelete = useCallback(
|
const handleForceDelete = useCallback(
|
||||||
async (session: Session) => {
|
async (session: Session) => {
|
||||||
if (loadingSessionId === session.id) return;
|
if (loadingSessionId === session.id) return;
|
||||||
setLoadingSessionId(session.id);
|
setLoadingSessionId(session.id);
|
||||||
startOperation("delete", session.id, session.display_name);
|
|
||||||
try {
|
try {
|
||||||
await deleteInstance(
|
await deleteInstance(
|
||||||
session.project_id,
|
session.project_id,
|
||||||
@@ -164,26 +147,17 @@ export function useInstanceActions(
|
|||||||
setDirtyDeleteFiles([]);
|
setDirtyDeleteFiles([]);
|
||||||
removeSession(session.id);
|
removeSession(session.id);
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
} catch {
|
|
||||||
completeOperation(session.id, "delete", "error");
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSessionId(null);
|
setLoadingSessionId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[loadingSessionId, onRefresh, removeSession],
|
||||||
loadingSessionId,
|
|
||||||
onRefresh,
|
|
||||||
removeSession,
|
|
||||||
startOperation,
|
|
||||||
completeOperation,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRecreateTunnel = useCallback(
|
const handleRecreateTunnel = useCallback(
|
||||||
async (session: Session) => {
|
async (session: Session) => {
|
||||||
if (loadingSessionId === session.id) return;
|
if (loadingSessionId === session.id) return;
|
||||||
setLoadingSessionId(session.id);
|
setLoadingSessionId(session.id);
|
||||||
startOperation("recreate-tunnel", session.id, session.display_name);
|
|
||||||
try {
|
try {
|
||||||
await recreateInstanceTunnel(
|
await recreateInstanceTunnel(
|
||||||
session.project_id,
|
session.project_id,
|
||||||
@@ -191,18 +165,16 @@ export function useInstanceActions(
|
|||||||
session.id,
|
session.id,
|
||||||
);
|
);
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
completeOperation(session.id, "recreate-tunnel", "success");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message =
|
const message =
|
||||||
(err as { response?: { data?: { detail?: string } } })?.response?.data
|
(err as { response?: { data?: { detail?: string } } })?.response?.data
|
||||||
?.detail || "Failed to recreate tunnel";
|
?.detail || "Failed to recreate tunnel";
|
||||||
completeOperation(session.id, "recreate-tunnel", "error", message);
|
|
||||||
alert(message);
|
alert(message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSessionId(null);
|
setLoadingSessionId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[loadingSessionId, onRefresh, startOperation, completeOperation],
|
[loadingSessionId, onRefresh],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRename = useCallback(
|
const handleRename = useCallback(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/pages
|
dir: apps/web/src/pages
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains top-level page components that serve as route endpoints for the web application, each rendering distinct feature areas of the developer workspace platform.
|
Contains top-level page components that serve as route endpoints for the web application's primary UI surfaces, each handling a specific domain area (dashboard, projects, workspaces, git, settings, etc.) with responsive layouts and CRUD operations.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.md
|
map: apps/web/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ dir: apps/web/src/pages
|
|||||||
index: apps/web/src/pages/.pi-map.index.md
|
index: apps/web/src/pages/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains top-level page components that serve as route endpoints for the web application, each rendering distinct feature areas of the developer workspace platform.
|
Contains top-level page components that serve as route endpoints for the web application's primary UI surfaces, each handling a specific domain area (dashboard, projects, workspaces, git, settings, etc.) with responsive layouts and CRUD operations.
|
||||||
## files
|
## 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
|
- 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 | 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.test.tsx | Unit tests for the DashboardPage component verifying overview loading and error retry behavior | dep: @testing-library/jest-dom/vitest, @testing-library/react, react-router-dom, vitest, ./DashboardPage, ../state/sessions, DashboardPage, SessionsProvider
|
||||||
- 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
|
- 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
|
- 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
|
- 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
|
||||||
@@ -15,7 +15,7 @@ Contains top-level page components that serve as route endpoints for the web app
|
|||||||
- ProfilePage.tsx | A React component that displays and allows editing of a user profile, including name, email, and avatar upload with validation. | exp: ProfilePage | dep: react, ../api/profile, ../components/data-states, ../components/icon, ../state/auth, ../hooks/use-async-data
|
- ProfilePage.tsx | A React component that displays and allows editing of a user profile, including name, email, and avatar upload with validation. | exp: ProfilePage | dep: react, ../api/profile, ../components/data-states, ../components/icon, ../state/auth, ../hooks/use-async-data
|
||||||
- ProjectSettingsPage.tsx | Renders a project settings page with tabbed navigation for general settings, repositories, and members, including project data fetching, editing, and deletion capabilities. | exp: ProjectSettingsPage | dep: react, react-router-dom, ../components/features/settings/settings-tab-layout, ../components/features/project/repositories-settings-tab, ../api/client, ../types
|
- ProjectSettingsPage.tsx | Renders a project settings page with tabbed navigation for general settings, repositories, and members, including project data fetching, editing, and deletion capabilities. | exp: ProjectSettingsPage | dep: react, react-router-dom, ../components/features/settings/settings-tab-layout, ../components/features/project/repositories-settings-tab, ../api/client, ../types
|
||||||
- ProjectsPage.test.tsx | Unit tests for the ProjectsPage component covering loading, empty, error, create, edit, and delete states with API mocking. | dep: @testing-library/react, react-router-dom, vitest, ./ProjectsPage, ../api/projects, ProjectsPage
|
- ProjectsPage.test.tsx | Unit tests for the ProjectsPage component covering loading, empty, error, create, edit, and delete states with API mocking. | dep: @testing-library/react, react-router-dom, vitest, ./ProjectsPage, ../api/projects, ProjectsPage
|
||||||
- ProjectsPage.tsx | Renders a responsive projects management page with separate mobile and desktop layouts, supporting project CRUD operations, repository creation, and workspace management. | exp: ProjectsPage | dep: react, ../components/data-states, ../components/icon, ../hooks/use-mobile-viewport, ../components/features/project/ProjectCard, ../components/features/project/ProjectDialog, ../components/features/project/repository-create-dialog, ../components/features/mobile/mobile-list-view, ../components/features/mobile/mobile-fab, ../hooks/use-projects, ../types
|
- ProjectsPage.tsx | Renders a responsive projects management page with separate mobile and desktop layouts, supporting project CRUD operations, repository management, and workspace actions. | exp: ProjectsPage | dep: react, ../components/data-states, ../components/icon, ../hooks/use-mobile-viewport, ../components/features/project/ProjectCard, ../components/features/project/ProjectDialog, ../components/features/project/repository-create-dialog, ../components/features/mobile/mobile-list-view, ../components/features/mobile/mobile-fab, ../hooks/use-projects, ../types
|
||||||
- SessionsPage.tsx | Renders a sessions management page that displays, polls health for, and handles CRUD operations on development environment sessions with dirty delete confirmation. | exp: SessionsPage | dep: react, ../api/sessions, ../api/settings, ../components/data-states, ../components/features/session/session-list, ../components/features/session/session-card, ../hooks/use-instance-actions, ../state/sessions
|
- SessionsPage.tsx | Renders a sessions management page that displays, polls health for, and handles CRUD operations on development environment sessions with dirty delete confirmation. | exp: SessionsPage | dep: react, ../api/sessions, ../api/settings, ../components/data-states, ../components/features/session/session-list, ../components/features/session/session-card, ../hooks/use-instance-actions, ../state/sessions
|
||||||
- SettingsPage.tsx | A React settings page component that loads, displays, and manages user configuration with tabbed navigation and nested outlet for child routes. | exp: SettingsPage | dep: react, react-router-dom, ../api/settings, ../components/data-states, ../hooks/use-async-data, ../components/features/settings/GeneralSettingsTab
|
- SettingsPage.tsx | A React settings page component that loads, displays, and manages user configuration with tabbed navigation and nested outlet for child routes. | exp: SettingsPage | dep: react, react-router-dom, ../api/settings, ../components/data-states, ../hooks/use-async-data, ../components/features/settings/GeneralSettingsTab
|
||||||
- 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
|
- 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
|
||||||
@@ -23,9 +23,9 @@ Contains top-level page components that serve as route endpoints for the web app
|
|||||||
- 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
|
- 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 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.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 tab-based navigation for files, git, tools, and settings panels, with mobile-responsive layout. | 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-file-panel, workspace-git-panel, workspace-tools-panel, workspace-settings-panel
|
- WorkspaceDetailPage.tsx | Renders a workspace detail page with tab-based navigation for files, git, tools, and settings panels, with mobile-responsive layout. | 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-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
|
- WorkspacesPage.tsx | Renders a responsive workspaces management page with separate mobile and desktop layouts, supporting workspace listing, creation, deletion, sync, 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: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-detail-view, ../components/features/mobile/mobile-fab, ../components/features/tool/tool-starter, ../types/workspace
|
||||||
## arch
|
## arch
|
||||||
Follows a React Router-based page-level architecture with responsive dual-layout patterns (mobile/desktop), tabbed navigation, CRUD operations with API integration, polling mechanisms, and nested outlet routing for complex settings/workspace detail views.
|
Follows a page-based routing architecture where each file maps to a URL route, using responsive design patterns with explicit mobile/desktop view branching, compound component layouts (sidebar/editor, tabbed panels), polling for real-time data, and direct API integration within page components rather than abstracted service layers.
|
||||||
## tags
|
## tags
|
||||||
page, components, workspace, features, react, mobile, settings, hooks
|
page, components, workspace, features, react, mobile, settings, hooks
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
import { HomePage } from "./DashboardPage";
|
import { HomePage } from "./DashboardPage";
|
||||||
import { SessionsProvider } from "../state/sessions";
|
import { SessionsProvider } from "../state/sessions";
|
||||||
import { SessionOperationsProvider } from "../state/session-operations";
|
|
||||||
|
|
||||||
const mockDashboard = vi.fn();
|
const mockDashboard = vi.fn();
|
||||||
const mockSessions = vi.fn();
|
const mockSessions = vi.fn();
|
||||||
@@ -59,9 +58,7 @@ describe("HomePage", () => {
|
|||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<SessionsProvider>
|
<SessionsProvider>
|
||||||
<SessionOperationsProvider>
|
|
||||||
<HomePage />
|
<HomePage />
|
||||||
</SessionOperationsProvider>
|
|
||||||
</SessionsProvider>
|
</SessionsProvider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
@@ -81,9 +78,7 @@ describe("HomePage", () => {
|
|||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<SessionsProvider>
|
<SessionsProvider>
|
||||||
<SessionOperationsProvider>
|
|
||||||
<HomePage />
|
<HomePage />
|
||||||
</SessionOperationsProvider>
|
|
||||||
</SessionsProvider>
|
</SessionsProvider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -168,8 +168,34 @@ export const ProjectsPage = () => {
|
|||||||
items={projects.map((p) => ({
|
items={projects.map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
title: p.name,
|
title: p.name,
|
||||||
subtitle: `${p.repositories?.length ?? 0} repo${(p.repositories?.length ?? 0) !== 1 ? "s" : ""}${p.description ? " · " + p.description : ""}`,
|
|
||||||
}))}
|
}))}
|
||||||
|
renderItem={(item) => {
|
||||||
|
const project = projects.find((p) => p.id === item.id);
|
||||||
|
if (!project) return null;
|
||||||
|
return (
|
||||||
|
<div className="mobile-list-item-content mobile-list-item-content-rich">
|
||||||
|
<div className="mobile-list-item-title">{project.name}</div>
|
||||||
|
{project.description && (
|
||||||
|
<p className="mobile-list-item-description">
|
||||||
|
{project.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="mobile-list-item-chips">
|
||||||
|
{(project.repositories?.length ?? 0) === 0 ? (
|
||||||
|
<span className="mobile-list-item-chip muted">
|
||||||
|
No repositories
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
project.repositories.map((repo) => (
|
||||||
|
<span key={repo.id} className="mobile-list-item-chip">
|
||||||
|
{repo.name}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
onItemClick={(id) => {
|
onItemClick={(id) => {
|
||||||
const project = projects.find((p) => p.id === id);
|
const project = projects.find((p) => p.id === id);
|
||||||
if (project) {
|
if (project) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useWorkspaces } from "../hooks/use-workspaces";
|
|||||||
import { useWorkspaceActions } from "../hooks/use-workspace-actions";
|
import { useWorkspaceActions } from "../hooks/use-workspace-actions";
|
||||||
import { WorkspaceCard } from "../components/features/workspace/workspace-card";
|
import { WorkspaceCard } from "../components/features/workspace/workspace-card";
|
||||||
import { WorkspaceCreateForm } from "../components/features/workspace/workspace-create-form";
|
import { WorkspaceCreateForm } from "../components/features/workspace/workspace-create-form";
|
||||||
import { MobileListView } from "../components/features/mobile/mobile-list-view";
|
|
||||||
import { MobileDetailView } from "../components/features/mobile/mobile-detail-view";
|
import { MobileDetailView } from "../components/features/mobile/mobile-detail-view";
|
||||||
import { MobileFAB } from "../components/features/mobile/mobile-fab";
|
import { MobileFAB } from "../components/features/mobile/mobile-fab";
|
||||||
import { ToolStarter } from "../components/features/tool/tool-starter";
|
import { ToolStarter } from "../components/features/tool/tool-starter";
|
||||||
@@ -100,22 +99,18 @@ export function WorkspacesPage() {
|
|||||||
{loading && workspaces.length === 0 ? (
|
{loading && workspaces.length === 0 ? (
|
||||||
<div className="loading-state">Loading workspaces...</div>
|
<div className="loading-state">Loading workspaces...</div>
|
||||||
) : (
|
) : (
|
||||||
<MobileListView
|
<div className="workspaces-grid mobile-workspaces-list">
|
||||||
items={workspaces.map((ws) => ({
|
{workspaces.map((ws) => (
|
||||||
id: ws.id,
|
<WorkspaceCard
|
||||||
title: ws.name,
|
key={ws.id}
|
||||||
subtitle: `${ws.project_name} · ${ws.repo_name} · ${ws.branch}`,
|
workspace={ws}
|
||||||
status: ws.status,
|
loading={actions.loadingId === ws.id}
|
||||||
}))}
|
onStartTool={setStartWorkspace}
|
||||||
onItemClick={(id) => {
|
onSync={handleSync}
|
||||||
const ws = workspaces.find((w) => w.id === id);
|
onDelete={handleDelete}
|
||||||
if (ws) {
|
|
||||||
setSelectedWorkspace(ws);
|
|
||||||
setMobileView("detail");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
emptyMessage="No workspaces yet"
|
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MobileFAB
|
<MobileFAB
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/state
|
dir: apps/web/src/state
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Centralized client-side state management layer providing React context providers for core application concerns including authentication, real-time data, notifications, background operations, and user feedback.
|
Centralized client-side state management layer providing React context providers for core application concerns including authentication, real-time data, notifications, and user feedback.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.md
|
map: apps/web/src/.pi-map.md
|
||||||
@@ -12,7 +12,6 @@ map: apps/web/src/.pi-map.md
|
|||||||
- auth.tsx
|
- auth.tsx
|
||||||
- events.tsx
|
- events.tsx
|
||||||
- notifications.tsx
|
- notifications.tsx
|
||||||
- session-operations.tsx
|
|
||||||
- sessions.tsx
|
- sessions.tsx
|
||||||
- toast.tsx
|
- toast.tsx
|
||||||
## links
|
## links
|
||||||
|
|||||||
@@ -4,18 +4,17 @@ dir: apps/web/src/state
|
|||||||
index: apps/web/src/state/.pi-map.index.md
|
index: apps/web/src/state/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Centralized client-side state management layer providing React context providers for core application concerns including authentication, real-time data, notifications, background operations, and user feedback.
|
Centralized client-side state management layer providing React context providers for core application concerns including authentication, real-time data, notifications, and user feedback.
|
||||||
## files
|
## files
|
||||||
- auth.tsx | Provides React authentication context for managing user session state with login status, user data, and session refresh/logout operations. | exp: AuthProvider, useAuth | dep: react, ../api/client, ../types
|
- auth.tsx | Provides React authentication context for managing user session state with login status, user data, and session refresh/logout operations. | exp: AuthProvider, useAuth | dep: react, ../api/client, ../types
|
||||||
- events.tsx | Creates a React context provider for real-time event data with connection status and reconnection tracking | exp: func:EventProvider({ children }: { children: React.ReactNode }), call:useEvents, call:useMemo, func:useEventContext(), call:useContext | dep: react, ../hooks/use-events, ../types/events
|
- events.tsx | Creates a React context provider for real-time event data with connection status and reconnection tracking | exp: func:EventProvider({ children }: { children: React.ReactNode }), call:useEvents, call:useMemo, func:useEventContext(), call:useContext | dep: react, ../hooks/use-events, ../types/events
|
||||||
- notifications.tsx | Provides a React context provider that manages notification state with optimistic updates, polling, and dropdown-aware fetching. | exp: NotificationContextValue, NotificationContext, func:NotificationProvider({ children, }: { children: React.ReactNode; }), call:useState, call:useRef, call:useCallback, call:getUnreadCount, call:setUnreadCount, call:clearInterval, call:console.error, call:setIsLoading, call:getNotifications, call:setNotifications, call:fetchUnreadCount, call:setInterval, call:fetchList, call:useEffect, call:stopPolling, call:startPolling, call:document.addEventListener, call:document.removeEventListener, call:currentNotifications.find, call:currentNotifications.map, call:new Date().toISOString, call:Math.max, call:setError, call:markNotificationRead, call:markAllNotificationsRead, call:currentNotifications.filter, call:dismissNotification, call:clearAllNotifications | dep: react, ../api/notifications, React
|
- notifications.tsx | Provides a React context provider that manages notification state with optimistic updates, polling, and dropdown-aware fetching. | exp: NotificationContextValue, NotificationContext, func:NotificationProvider({ children, }: { children: React.ReactNode; }), call:useState, call:useRef, call:useCallback, call:getUnreadCount, call:setUnreadCount, call:clearInterval, call:console.error, call:setIsLoading, call:getNotifications, call:setNotifications, call:fetchUnreadCount, call:setInterval, call:fetchList, call:useEffect, call:stopPolling, call:startPolling, call:document.addEventListener, call:document.removeEventListener, call:currentNotifications.find, call:currentNotifications.map, call:new Date().toISOString, call:Math.max, call:setError, call:markNotificationRead, call:markAllNotificationsRead, call:currentNotifications.filter, call:dismissNotification, call:clearAllNotifications | dep: react, ../api/notifications, React
|
||||||
- session-operations.tsx | Manages a React context provider for tracking asynchronous instance operations (create, start, stop, etc.) with status updates from event payloads. | exp: OperationType, OperationStatus, Operation, SessionOperationsContextType, SessionOperationsProvider, useSessionOperations | dep: react, ../types/events
|
|
||||||
- sessions.tsx | Provides a React context that manages user sessions state with server polling, local CRUD operations, and loading/error handling. | exp: SessionsContextType, SessionsProvider, useSessions | dep: react, ../api/sessions
|
- sessions.tsx | Provides a React context that manages user sessions state with server polling, local CRUD operations, and loading/error handling. | exp: SessionsContextType, SessionsProvider, useSessions | dep: react, ../api/sessions
|
||||||
- toast.tsx | Implements a React toast notification system with context-based state management, auto-dismiss timers, and imperative API shortcuts. | exp: ToastSeverity, ToastItem, toast, func:ToastProvider({ children }: { children: React.ReactNode }), call:useState, call:useRef, call:useCallback, call:setToasts, call:prev.filter, call:timersRef.current.get, call:clearTimeout, call:timersRef.current.delete, call:Date.now, call:setTimeout, call:removeToast, call:timersRef.current.set, call:useEffect, call:addToast, func:useToast(), call:useContext, raise:Error | dep: react, React
|
- toast.tsx | Implements a React toast notification system with context-based state management, auto-dismiss timers, and imperative API shortcuts. | exp: ToastSeverity, ToastItem, toast, func:ToastProvider({ children }: { children: React.ReactNode }), call:useState, call:useRef, call:useCallback, call:setToasts, call:prev.filter, call:timersRef.current.get, call:clearTimeout, call:timersRef.current.delete, call:Date.now, call:setTimeout, call:removeToast, call:timersRef.current.set, call:useEffect, call:addToast, func:useToast(), call:useContext, raise:Error | dep: react, React
|
||||||
## arch
|
## arch
|
||||||
React Context API pattern with separate providers per domain, combining optimistic updates, server polling, event-driven state synchronization, and imperative APIs for cross-cutting concerns like toast notifications.
|
React Context API pattern with separate providers per domain, combining optimistic updates, server polling, event-driven state synchronization, and imperative APIs for cross-cutting concerns.
|
||||||
## tags
|
## tags
|
||||||
call:use, context, toast, provider, react, call:set, notification, operations
|
call:use, context, toast, provider, react, call:set, notification
|
||||||
## symbols
|
## symbols
|
||||||
- EventProvider
|
- EventProvider
|
||||||
- useEventContext
|
- useEventContext
|
||||||
@@ -24,7 +23,6 @@ call:use, context, toast, provider, react, call:set, notification, operations
|
|||||||
- useToast
|
- useToast
|
||||||
- AuthProvider
|
- AuthProvider
|
||||||
- useAuth
|
- useAuth
|
||||||
- call:useEvents
|
|
||||||
## workflows
|
## workflows
|
||||||
- change state behavior
|
- change state behavior
|
||||||
read: auth.tsx, events.tsx, notifications.tsx
|
read: auth.tsx, events.tsx, notifications.tsx
|
||||||
|
|||||||
@@ -1,259 +0,0 @@
|
|||||||
import {
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
type ReactNode,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
import type { InstanceEventPayload } from "../types/events";
|
|
||||||
|
|
||||||
export type OperationType =
|
|
||||||
| "create"
|
|
||||||
| "start"
|
|
||||||
| "stop"
|
|
||||||
| "restart"
|
|
||||||
| "delete"
|
|
||||||
| "recreate-tunnel";
|
|
||||||
|
|
||||||
export type OperationStatus = "pending" | "active" | "success" | "error";
|
|
||||||
|
|
||||||
export interface Operation {
|
|
||||||
id: string;
|
|
||||||
type: OperationType;
|
|
||||||
instanceId: string;
|
|
||||||
displayName: string;
|
|
||||||
status: OperationStatus;
|
|
||||||
message: string;
|
|
||||||
step: number;
|
|
||||||
createdAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SessionOperationsContextType {
|
|
||||||
operations: Operation[];
|
|
||||||
startOperation: (
|
|
||||||
type: OperationType,
|
|
||||||
instanceId: string,
|
|
||||||
displayName: string,
|
|
||||||
) => string;
|
|
||||||
updateOperationFromEvent: (event: InstanceEventPayload) => void;
|
|
||||||
completeOperation: (
|
|
||||||
instanceId: string,
|
|
||||||
type: OperationType,
|
|
||||||
outcome: "success" | "error",
|
|
||||||
message?: string,
|
|
||||||
) => void;
|
|
||||||
dismissOperation: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SessionOperationsContext =
|
|
||||||
createContext<SessionOperationsContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
let operationIdCounter = 0;
|
|
||||||
|
|
||||||
function actionLabel(type: OperationType): string {
|
|
||||||
switch (type) {
|
|
||||||
case "create":
|
|
||||||
return "Creating";
|
|
||||||
case "start":
|
|
||||||
return "Starting";
|
|
||||||
case "stop":
|
|
||||||
return "Stopping";
|
|
||||||
case "restart":
|
|
||||||
return "Restarting";
|
|
||||||
case "delete":
|
|
||||||
return "Deleting";
|
|
||||||
case "recreate-tunnel":
|
|
||||||
return "Recreating tunnel";
|
|
||||||
default:
|
|
||||||
return "Working";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function messageForEvent(
|
|
||||||
type: OperationType,
|
|
||||||
event: InstanceEventPayload,
|
|
||||||
): string {
|
|
||||||
if (event.message) return event.message;
|
|
||||||
|
|
||||||
switch (event.event) {
|
|
||||||
case "instance.created":
|
|
||||||
return "Created";
|
|
||||||
case "instance.started":
|
|
||||||
return "Starting container";
|
|
||||||
case "instance.restarted":
|
|
||||||
return "Restarting container";
|
|
||||||
case "instance.stopped":
|
|
||||||
return "Stopped";
|
|
||||||
case "instance.deleted":
|
|
||||||
return "Deleted";
|
|
||||||
case "instance.health_changed":
|
|
||||||
if (event.status === "running") return "Running";
|
|
||||||
if (event.status === "unhealthy") return "Unhealthy";
|
|
||||||
return `Status: ${event.status ?? event.event}`;
|
|
||||||
case "instance.error":
|
|
||||||
return event.message ?? "Error";
|
|
||||||
default:
|
|
||||||
return event.message ?? actionLabel(type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stepForEvent(event: InstanceEventPayload): number {
|
|
||||||
switch (event.event) {
|
|
||||||
case "instance.created":
|
|
||||||
return 1;
|
|
||||||
case "instance.started":
|
|
||||||
case "instance.restarted":
|
|
||||||
return 2;
|
|
||||||
case "instance.health_changed":
|
|
||||||
if (event.status === "running") return 4;
|
|
||||||
if (event.status === "unhealthy") return 4;
|
|
||||||
return 3;
|
|
||||||
case "instance.error":
|
|
||||||
return 4;
|
|
||||||
case "instance.stopped":
|
|
||||||
return 4;
|
|
||||||
case "instance.deleted":
|
|
||||||
return 4;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SessionOperationsProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
}) => {
|
|
||||||
const [operations, setOperations] = useState<Operation[]>([]);
|
|
||||||
|
|
||||||
const startOperation = useCallback(
|
|
||||||
(type: OperationType, instanceId: string, displayName: string): string => {
|
|
||||||
const id = `op-${++operationIdCounter}`;
|
|
||||||
const operation: Operation = {
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
instanceId,
|
|
||||||
displayName,
|
|
||||||
status: "pending",
|
|
||||||
message: actionLabel(type),
|
|
||||||
step: 0,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
};
|
|
||||||
setOperations((prev) => [operation, ...prev].slice(0, 20));
|
|
||||||
return id;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateOperationFromEvent = useCallback(
|
|
||||||
(event: InstanceEventPayload) => {
|
|
||||||
setOperations((prev) => {
|
|
||||||
const matches = prev.filter(
|
|
||||||
(op) => op.instanceId === event.instance_id && op.status !== "success",
|
|
||||||
);
|
|
||||||
if (matches.length === 0) return prev;
|
|
||||||
|
|
||||||
const updated = new Map<string, Operation>();
|
|
||||||
for (const op of prev) updated.set(op.id, op);
|
|
||||||
|
|
||||||
for (const op of matches) {
|
|
||||||
const nextStep = stepForEvent(event);
|
|
||||||
const message = messageForEvent(op.type, event);
|
|
||||||
let nextStatus: OperationStatus = op.status;
|
|
||||||
|
|
||||||
if (event.event === "instance.error") {
|
|
||||||
nextStatus = "error";
|
|
||||||
} else if (
|
|
||||||
event.event === "instance.health_changed" &&
|
|
||||||
event.status === "running"
|
|
||||||
) {
|
|
||||||
nextStatus = "success";
|
|
||||||
} else if (event.event === "instance.deleted") {
|
|
||||||
nextStatus = "success";
|
|
||||||
} else if (event.event === "instance.stopped") {
|
|
||||||
nextStatus = "success";
|
|
||||||
} else if (nextStatus === "pending") {
|
|
||||||
nextStatus = "active";
|
|
||||||
}
|
|
||||||
|
|
||||||
updated.set(op.id, {
|
|
||||||
...op,
|
|
||||||
status: nextStatus,
|
|
||||||
message,
|
|
||||||
step: Math.max(op.step, nextStep),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(updated.values());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const completeOperation = useCallback(
|
|
||||||
(
|
|
||||||
instanceId: string,
|
|
||||||
type: OperationType,
|
|
||||||
outcome: "success" | "error",
|
|
||||||
message?: string,
|
|
||||||
) => {
|
|
||||||
setOperations((prev) => {
|
|
||||||
const match = prev.find(
|
|
||||||
(op) => op.instanceId === instanceId && op.type === type,
|
|
||||||
);
|
|
||||||
if (!match) return prev;
|
|
||||||
return prev.map((op) =>
|
|
||||||
op.id === match.id
|
|
||||||
? {
|
|
||||||
...op,
|
|
||||||
status: outcome,
|
|
||||||
message:
|
|
||||||
message ?? (outcome === "success" ? "Done" : "Failed"),
|
|
||||||
step: 4,
|
|
||||||
}
|
|
||||||
: op,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const dismissOperation = useCallback((id: string) => {
|
|
||||||
setOperations((prev) => prev.filter((op) => op.id !== id));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const value = useMemo(
|
|
||||||
() => ({
|
|
||||||
operations,
|
|
||||||
startOperation,
|
|
||||||
updateOperationFromEvent,
|
|
||||||
completeOperation,
|
|
||||||
dismissOperation,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
operations,
|
|
||||||
startOperation,
|
|
||||||
updateOperationFromEvent,
|
|
||||||
completeOperation,
|
|
||||||
dismissOperation,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SessionOperationsContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</SessionOperationsContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSessionOperations = (): SessionOperationsContextType => {
|
|
||||||
const context = useContext(SessionOperationsContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
"useSessionOperations must be used within a SessionOperationsProvider",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/styles
|
dir: apps/web/src/styles
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides the complete visual design system and styling foundation for the web application, encompassing global styles, theme tokens, syntax highlighting, and utility classes.
|
Provides the complete visual design system and styling foundation for the web application, encompassing global styles, design tokens, utility classes, and component-specific styles.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.md
|
map: apps/web/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ dir: apps/web/src/styles
|
|||||||
index: apps/web/src/styles/.pi-map.index.md
|
index: apps/web/src/styles/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides the complete visual design system and styling foundation for the web application, encompassing global styles, theme tokens, syntax highlighting, and utility classes.
|
Provides the complete visual design system and styling foundation for the web application, encompassing global styles, design tokens, utility classes, and component-specific styles.
|
||||||
## files
|
## files
|
||||||
- global.css | Defines global CSS styles for a web application shell layout, navigation, cards, forms, dialogs, settings pages, and responsive design patterns. | dep: CSS custom properties (CSS variables: --border, --panel, --brand, --muted, --ink, --bg, --danger, --success, --warning, --font-size-xs, --font-size-sm, --space-2, --space-3, --space-4, --space-5)
|
- global.css | Defines global CSS styles for a web application shell layout, navigation, cards, forms, dialogs, settings pages, and responsive design patterns. | dep: CSS custom properties (CSS variables: --border, --panel, --brand, --muted, --ink, --bg, --danger, --success, --warning, --font-size-xs, --font-size-sm, --space-2, --space-3, --space-4, --space-5)
|
||||||
- syntax-highlight.css | Stylesheet for a syntax highlighting component with toolbar, line numbers, code display, and Prism.js theme integration | dep: Prism.js
|
- syntax-highlight.css | Stylesheet for a syntax highlighting component with toolbar, line numbers, code display, and Prism.js theme integration | dep: Prism.js
|
||||||
- tokens.css | Defines a comprehensive CSS design token system with light/dark themes, spacing scales, breakpoints, and fluid typography for a web application.
|
- tokens.css | Defines a comprehensive CSS design token system with light/dark themes, spacing scales, breakpoints, and fluid typography for a web application.
|
||||||
- utilities.css | Provides a comprehensive responsive CSS utility system including layout primitives, mobile-first breakpoints, terminal styling, session management UI, and touch-target accessibility patterns for a web application. | dep: CSS custom properties (var(--space-*), var(--border), var(--bg), var(--primary), var(--danger), etc.), xterm.js (terminal rendering)
|
- utilities.css | Provides responsive CSS utility classes and component-specific styles for a web application featuring terminals, dialogs, tables, forms, and navigation with mobile-first breakpoints | dep: CSS custom properties (variables like --space-*, --border, --bg, --brand, --muted, --success, --danger, --text-xs, --text-sm), xterm.js (terminal library)
|
||||||
## arch
|
## arch
|
||||||
CSS custom properties-based design token architecture with light/dark theme support, mobile-first responsive breakpoint system, utility-first CSS patterns, and modular separation of concerns across global, component-specific, token, and utility layers.
|
CSS custom properties (variables) based theming system with light/dark mode support, mobile-first responsive breakpoints, fluid typography scales, utility-first class patterns, and modular separation of concerns across tokens, utilities, global layouts, and component-specific styles.
|
||||||
## tags
|
## tags
|
||||||
space, var(, global, css, web, application, syntax, defines
|
space, global, css, web, application, syntax, defines, styles
|
||||||
## symbols
|
## symbols
|
||||||
-
|
-
|
||||||
## workflows
|
## workflows
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/styles/pages
|
dir: apps/web/src/styles/pages
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains page-specific CSS stylesheets for individual application views in the web frontend.
|
Contains page-specific CSS stylesheets for individual application views in the web frontend, providing scoped styling for distinct feature areas.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/styles/.pi-map.index.md
|
index: apps/web/src/styles/.pi-map.index.md
|
||||||
map: apps/web/src/styles/.pi-map.md
|
map: apps/web/src/styles/.pi-map.md
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ dir: apps/web/src/styles/pages
|
|||||||
index: apps/web/src/styles/pages/.pi-map.index.md
|
index: apps/web/src/styles/pages/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains page-specific CSS stylesheets for individual application views in the web frontend.
|
Contains page-specific CSS stylesheets for individual application views in the web frontend, providing scoped styling for distinct feature areas.
|
||||||
## files
|
## files
|
||||||
- git-history.css | Stylesheet for a Git history visualization page with commit list, branch selector, and detail panel layout | dep: CSS custom properties (var(--border), var(--panel), var(--ink), var(--brand), var(--muted))
|
- git-history.css | Stylesheet for a Git history visualization page with commit list, branch selector, and detail panel layout | dep: CSS custom properties (var(--border), var(--panel), var(--ink), var(--brand), var(--muted))
|
||||||
- projects.css | Stylesheet for a projects page UI featuring expandable project toggles, repository listings, workspace grids, and repository creation radio controls. | dep: CSS custom properties (variables)
|
- projects.css | Stylesheet for a projects page UI featuring expandable project toggles, repository listings, workspace grids, and repository creation radio controls. | dep: CSS custom properties (variables)
|
||||||
- sessions.css | Stylesheet for session management UI components including cards, dropdowns, forms, and responsive mobile layouts. | dep: CSS custom properties/variables (var(--border), var(--panel), var(--text), var(--space-*), var(--danger), var(--color-border))
|
- sessions.css | Stylesheet for session management UI components including cards, dropdowns, forms, and responsive mobile layouts. | dep: CSS custom properties/variables (var(--border), var(--panel), var(--text), var(--space-*), var(--danger), var(--color-border))
|
||||||
- ssh-keys.css | Styles a responsive SSH key list component that stacks vertically on mobile and switches to horizontal layout on larger screens | dep: CSS custom properties (var(--space-3), var(--space-2), var(--space-4), var(--bg), var(--border))
|
- ssh-keys.css | Styles a responsive SSH key list component that stacks vertically on mobile and switches to horizontal layout on larger screens | dep: CSS custom properties (var(--space-3), var(--space-2), var(--space-4), var(--bg), var(--border))
|
||||||
- workspace-detail.css | Stylesheet for a workspace detail page with tabbed interface including files, git, tools, and settings views, plus a workspace creation form | dep: CSS custom properties (variables for colors, spacing, typography)
|
- workspace-detail.css | Stylesheet for a workspace detail page with tabbed interface including files, git, tools, and settings views, plus a workspace creation form | dep: CSS custom properties (variables for colors, spacing, typography)
|
||||||
- workspaces.css | Stylesheet for a workspaces page featuring a responsive grid of workspace cards with status badges, hover effects, and mobile adaptations. | dep: CSS custom properties (var(--space-*), var(--border), var(--panel), var(--muted), var(--success), var(--warning), var(--danger))
|
- workspaces.css | Stylesheet for a workspaces management page featuring responsive grid layout, workspace cards with status badges, instance chips, and mobile adaptations. | dep: CSS custom properties (design tokens: --space-*, --border, --panel, --brand, --ink, --muted, --bg, --success, --warning, --danger, --font-size-*)
|
||||||
## arch
|
## arch
|
||||||
Modular page-scoped styling using dedicated CSS files per route/page, with responsive design patterns (mobile-first breakpoints, grid/flex layouts) and component-oriented class naming.
|
Modular CSS architecture with page-level style separation, responsive design patterns using media queries for mobile adaptation, and component-scoped styling organized by feature domain rather than shared component library.
|
||||||
## tags
|
## tags
|
||||||
var(, workspace, stylesheet, space, git, page, detail, history
|
var(, workspace, stylesheet, space, git, page, detail, history
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -7,22 +7,28 @@
|
|||||||
|
|
||||||
.workspaces-grid {
|
.workspaces-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
margin-top: var(--space-4);
|
margin-top: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Workspace card */
|
/* Workspace card */
|
||||||
.workspace-card {
|
.workspace-card {
|
||||||
padding: var(--space-4);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-5);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
transition: box-shadow 0.15s ease;
|
transition:
|
||||||
|
box-shadow 0.15s ease,
|
||||||
|
border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-card:hover {
|
.workspace-card:hover {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
|
||||||
|
border-color: color-mix(in srgb, var(--border) 80%, var(--brand));
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-card.loading {
|
.workspace-card.loading {
|
||||||
@@ -30,46 +36,162 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-header-link {
|
/* Top row: title + status */
|
||||||
|
.workspace-card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-title-link {
|
||||||
display: block;
|
display: block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-header {
|
.workspace-title-link:hover .workspace-name {
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-title-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-title-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
margin-bottom: var(--space-2);
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-header h4 {
|
.workspace-name {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.1rem;
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-meta {
|
.workspace-project-name {
|
||||||
margin-bottom: var(--space-3);
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-meta p {
|
.workspace-project-name .icon {
|
||||||
margin: 0.15rem 0;
|
color: var(--brand);
|
||||||
font-size: 0.875rem;
|
}
|
||||||
|
|
||||||
|
/* Body: meta row + instance chips */
|
||||||
|
.workspace-card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-meta-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-meta-row > span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-repo {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-branch {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-instance-count {
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
background: var(--bg);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Instance chips */
|
||||||
|
.instance-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--ink);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-chip.running {
|
||||||
|
background: var(--success-light);
|
||||||
|
border-color: color-mix(in srgb, var(--success) 25%, transparent);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-chip a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-chip a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
.workspace-actions {
|
.workspace-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
flex-wrap: wrap;
|
margin-top: auto;
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-actions .btn-primary {
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status badges */
|
/* Status badges */
|
||||||
.status-badge {
|
.status-badge {
|
||||||
font-size: 0.75rem;
|
flex-shrink: 0;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0.15rem 0.5rem;
|
padding: var(--space-1) var(--space-2);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
@@ -103,7 +225,7 @@
|
|||||||
/* Mobile */
|
/* Mobile */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.workspaces-page {
|
.workspaces-page {
|
||||||
padding: var(--space-2);
|
padding: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspaces-grid {
|
.workspaces-grid {
|
||||||
@@ -112,15 +234,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace-card {
|
.workspace-card {
|
||||||
padding: var(--space-3);
|
padding: var(--space-4);
|
||||||
|
gap: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-actions {
|
.workspace-actions {
|
||||||
flex-direction: column;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-actions .btn {
|
.workspace-actions .btn-primary {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-workspaces-list {
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+190
-116
@@ -384,128 +384,50 @@ a.nav-item,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
Session Operations Progress Panel
|
Loading Overlay
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
.session-progress-panel {
|
.loading-overlay {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
bottom: var(--space-4);
|
inset: 0;
|
||||||
right: var(--space-4);
|
background: rgba(0, 0, 0, 0.12);
|
||||||
width: min(360px, calc(100vw - 2rem));
|
backdrop-filter: blur(1px);
|
||||||
max-height: min(480px, 60vh);
|
border-radius: inherit;
|
||||||
overflow-y: auto;
|
|
||||||
background: var(--panel);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--space-2);
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
||||||
z-index: 100;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-progress-panel-header {
|
|
||||||
padding: var(--space-3) var(--space-4);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-progress-panel-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-2);
|
|
||||||
padding: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-item {
|
|
||||||
padding: var(--space-3);
|
|
||||||
background: var(--bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-item.error {
|
|
||||||
border-color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-title .icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-name {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-message {
|
|
||||||
margin: var(--space-1) 0 0;
|
|
||||||
font-size: var(--text-xs);
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-steps {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-2);
|
|
||||||
margin-top: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-step {
|
|
||||||
font-size: 10px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
color: var(--muted);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--bg-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-step.active {
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--primary-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-step.current {
|
|
||||||
box-shadow: 0 0 0 1px var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-operation-dismiss {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--muted);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: var(--space-1);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
.loading-overlay-content {
|
||||||
.session-progress-panel {
|
display: flex;
|
||||||
left: var(--space-2);
|
flex-direction: column;
|
||||||
right: var(--space-2);
|
align-items: center;
|
||||||
bottom: calc(var(--space-2) + 64px);
|
gap: var(--space-2);
|
||||||
width: auto;
|
color: var(--text, currentColor);
|
||||||
max-height: 35vh;
|
}
|
||||||
|
|
||||||
|
.loading-overlay-label {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-spin {
|
||||||
|
animation: icon-spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes icon-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-card,
|
||||||
|
.tool-starter,
|
||||||
|
.create-session-form-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
Terminal Styles
|
Terminal Styles
|
||||||
============================================ */
|
============================================ */
|
||||||
@@ -631,6 +553,12 @@ a.nav-item,
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Let xterm.js's own viewport handle touch; we only lock the outer page */
|
||||||
|
.terminal-container .xterm-viewport {
|
||||||
|
touch-action: auto;
|
||||||
|
overscroll-behavior: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* xterm.js manages its own positioning and sizing */
|
/* xterm.js manages its own positioning and sizing */
|
||||||
|
|
||||||
.terminal-container canvas {
|
.terminal-container canvas {
|
||||||
@@ -644,6 +572,11 @@ a.nav-item,
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal-wrapper.mobile {
|
||||||
|
touch-action: none;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Ensure xterm viewport fills container properly */
|
/* Ensure xterm viewport fills container properly */
|
||||||
/* Responsive terminal */
|
/* Responsive terminal */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
@@ -933,17 +866,23 @@ a.nav-item,
|
|||||||
Mobile Terminal Overlay
|
Mobile Terminal Overlay
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
/* Mobile terminal page — no padding, terminal fills viewport */
|
/* Mobile terminal page — no padding, terminal fills viewport, locked in place */
|
||||||
.terminal-page.mobile {
|
.terminal-page.mobile {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
position: relative;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
touch-action: none;
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overlay status bar — floats over terminal, never resizes it */
|
/* Overlay status bar — floats over terminal; only its buttons consume pointer events */
|
||||||
.mobile-terminal-overlay {
|
.mobile-terminal-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -952,11 +891,21 @@ a.nav-item,
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
border-bottom: 1px solid #3e3e3e;
|
border-bottom: 1px solid #3e3e3e;
|
||||||
|
pointer-events: none;
|
||||||
transition:
|
transition:
|
||||||
transform 0.3s ease,
|
transform 0.3s ease,
|
||||||
opacity 0.3s ease;
|
opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-terminal-overlay.visible {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-terminal-overlay > *,
|
||||||
|
.mobile-terminal-overlay-tabs {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-terminal-overlay.hidden {
|
.mobile-terminal-overlay.hidden {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -1056,13 +1005,15 @@ a.nav-item,
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Terminal content — always fills full viewport on mobile */
|
/* Terminal content — always fills full viewport on mobile, locked against page scroll */
|
||||||
.terminal-page-content.mobile-full {
|
.terminal-page-content.mobile-full {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
touch-action: none;
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile fullscreen */
|
/* Mobile fullscreen */
|
||||||
@@ -2035,6 +1986,129 @@ a.nav-item,
|
|||||||
color: var(--brand);
|
color: var(--brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile List View */
|
||||||
|
.mobile-list-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--text);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item:active {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-subtitle {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-10) var(--space-4);
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-empty p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rich project rows */
|
||||||
|
.mobile-list-item-content-rich {
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--muted);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-1);
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-chip {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-list-item-chip.muted {
|
||||||
|
color: var(--muted);
|
||||||
|
font-style: italic;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile Page Header */
|
/* Mobile Page Header */
|
||||||
.mobile-page-header {
|
.mobile-page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ map: archive/.pi-map.md
|
|||||||
- archive/2026-06-12-completed-changes-archive/config-profile-multi-repo-mounts
|
- archive/2026-06-12-completed-changes-archive/config-profile-multi-repo-mounts
|
||||||
index: archive/2026-06-12-completed-changes-archive/config-profile-multi-repo-mounts/.pi-map.index.md
|
index: archive/2026-06-12-completed-changes-archive/config-profile-multi-repo-mounts/.pi-map.index.md
|
||||||
map: archive/2026-06-12-completed-changes-archive/config-profile-multi-repo-mounts/.pi-map.md
|
map: archive/2026-06-12-completed-changes-archive/config-profile-multi-repo-mounts/.pi-map.md
|
||||||
|
- archive/2026-06-12-completed-changes-archive/config-profile-directory-mounts
|
||||||
|
index: archive/2026-06-12-completed-changes-archive/config-profile-directory-mounts/.pi-map.index.md
|
||||||
|
map: archive/2026-06-12-completed-changes-archive/config-profile-directory-mounts/.pi-map.md
|
||||||
- archive/2026-06-12-completed-changes-archive/container-monitoring-notifications
|
- archive/2026-06-12-completed-changes-archive/container-monitoring-notifications
|
||||||
index: archive/2026-06-12-completed-changes-archive/container-monitoring-notifications/.pi-map.index.md
|
index: archive/2026-06-12-completed-changes-archive/container-monitoring-notifications/.pi-map.index.md
|
||||||
map: archive/2026-06-12-completed-changes-archive/container-monitoring-notifications/.pi-map.md
|
map: archive/2026-06-12-completed-changes-archive/container-monitoring-notifications/.pi-map.md
|
||||||
@@ -49,6 +52,9 @@ map: archive/.pi-map.md
|
|||||||
- archive/2026-06-12-completed-changes-archive/terminal-fullscreen-unified-header
|
- archive/2026-06-12-completed-changes-archive/terminal-fullscreen-unified-header
|
||||||
index: archive/2026-06-12-completed-changes-archive/terminal-fullscreen-unified-header/.pi-map.index.md
|
index: archive/2026-06-12-completed-changes-archive/terminal-fullscreen-unified-header/.pi-map.index.md
|
||||||
map: archive/2026-06-12-completed-changes-archive/terminal-fullscreen-unified-header/.pi-map.md
|
map: archive/2026-06-12-completed-changes-archive/terminal-fullscreen-unified-header/.pi-map.md
|
||||||
|
- archive/2026-06-12-completed-changes-archive/tool-config-mount-cleanup
|
||||||
|
index: archive/2026-06-12-completed-changes-archive/tool-config-mount-cleanup/.pi-map.index.md
|
||||||
|
map: archive/2026-06-12-completed-changes-archive/tool-config-mount-cleanup/.pi-map.md
|
||||||
- archive/2026-06-12-completed-changes-archive/tool-session-progress-and-updates
|
- archive/2026-06-12-completed-changes-archive/tool-session-progress-and-updates
|
||||||
index: archive/2026-06-12-completed-changes-archive/tool-session-progress-and-updates/.pi-map.index.md
|
index: archive/2026-06-12-completed-changes-archive/tool-session-progress-and-updates/.pi-map.index.md
|
||||||
map: archive/2026-06-12-completed-changes-archive/tool-session-progress-and-updates/.pi-map.md
|
map: archive/2026-06-12-completed-changes-archive/tool-session-progress-and-updates/.pi-map.md
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ index: archive/2026-06-12-completed-changes-archive/.pi-map.index.md
|
|||||||
Stores historical records of fully implemented and audited OpenSpec changes for reference and audit trail purposes.
|
Stores historical records of fully implemented and audited OpenSpec changes for reference and audit trail purposes.
|
||||||
## files
|
## files
|
||||||
- README.md | Documents archived completed OpenSpec changes that have been audited and confirmed as fully implemented
|
- README.md | Documents archived completed OpenSpec changes that have been audited and confirmed as fully implemented
|
||||||
|
- config-profile-directory-mounts | Completed OpenSpec change for directory-level config profile mounts
|
||||||
- multi-session-terminal-ux | Completed OpenSpec change for multi-session terminal UX
|
- multi-session-terminal-ux | Completed OpenSpec change for multi-session terminal UX
|
||||||
- reorganize-long-files | Completed OpenSpec change for reorganizing long files
|
- reorganize-long-files | Completed OpenSpec change for reorganizing long files
|
||||||
|
- tool-config-mount-cleanup | Completed OpenSpec change for cleaning up built-in tool config/state mounts
|
||||||
- working-copies | Completed OpenSpec change for workspace-first working copies
|
- working-copies | Completed OpenSpec change for workspace-first working copies
|
||||||
- workspace-first-ui | Completed OpenSpec change for workspace-first UI
|
- workspace-first-ui | Completed OpenSpec change for workspace-first UI
|
||||||
## arch
|
## arch
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ These OpenSpec changes have been audited against the current source tree and con
|
|||||||
- config-profile-git-mounts
|
- config-profile-git-mounts
|
||||||
- config-profile-includes-ui
|
- config-profile-includes-ui
|
||||||
- config-profile-multi-repo-mounts
|
- config-profile-multi-repo-mounts
|
||||||
|
- config-profile-directory-mounts
|
||||||
- container-monitoring-notifications
|
- container-monitoring-notifications
|
||||||
- git-mount-url-validation
|
- git-mount-url-validation
|
||||||
- home-path-expansion
|
- home-path-expansion
|
||||||
@@ -18,13 +19,14 @@ These OpenSpec changes have been audited against the current source tree and con
|
|||||||
- session-list-overhaul
|
- session-list-overhaul
|
||||||
- ssh-key-mounting
|
- ssh-key-mounting
|
||||||
- terminal-fullscreen-unified-header
|
- terminal-fullscreen-unified-header
|
||||||
|
- tool-config-mount-cleanup
|
||||||
- tool-session-progress-and-updates
|
- tool-session-progress-and-updates
|
||||||
|
|
||||||
## Audit summary
|
## Audit summary
|
||||||
|
|
||||||
| Status | Count |
|
| Status | Count |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| Fully implemented | 15 |
|
| Fully implemented | 17 |
|
||||||
| Partially implemented | 0 (in this archive) |
|
| Partially implemented | 0 (in this archive) |
|
||||||
|
|
||||||
Audit report: `/tmp/active-changes-implementation-audit.md` (generated before archiving).
|
Audit report: `/tmp/active-changes-implementation-audit.md` (generated before archiving).
|
||||||
|
|||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
name: config-profile-directory-mounts
|
||||||
|
status: completed
|
||||||
|
completed_at: 2026-06-13
|
||||||
|
started_at: 2026-06-13
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
# . (index)
|
||||||
|
dir: .
|
||||||
|
|
||||||
|
## Project Map Protocol
|
||||||
|
|
||||||
|
1. Read this protocol and the root `.pi-map.index.md` first.
|
||||||
|
2. Use `index:` / `map:` references to open relevant directory indexes and maps.
|
||||||
|
3. Load indexes before rich maps during task-start navigation.
|
||||||
|
4. Read the local rich map and actual source before editing.
|
||||||
|
5. Treat non-empty `## dirty` sections in either artifact as stale.
|
||||||
|
6. If source and generated artifacts disagree, trust source.
|
||||||
|
7. If map and index disagree, trust neither blindly; verify from source and regenerate the pair.
|
||||||
|
8. After editing source, run `project_map_patch` for each changed file.
|
||||||
|
9. Before broad architectural claims or final handoff, run `project_map_validate` when freshness matters.
|
||||||
|
|
||||||
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
|
## role
|
||||||
|
Provides configuration and documentation for a mount staging system that resolves file ownership issues in containerized environments.
|
||||||
|
## parent
|
||||||
|
-
|
||||||
|
## children
|
||||||
|
-
|
||||||
|
## files
|
||||||
|
- .openspec.yaml
|
||||||
|
- proposal.md
|
||||||
|
- tasks.md
|
||||||
|
## links
|
||||||
|
index: ./.pi-map.index.md
|
||||||
|
map: ./.pi-map.md
|
||||||
|
## workflows
|
||||||
|
-
|
||||||
|
## dirty
|
||||||
|
-
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
# .
|
||||||
|
dir: .
|
||||||
|
|
||||||
|
index: ./.pi-map.index.md
|
||||||
|
|
||||||
|
## Project Map Protocol
|
||||||
|
|
||||||
|
1. Read this protocol and the root `.pi-map.index.md` first.
|
||||||
|
2. Use `index:` / `map:` references to open relevant directory indexes and maps.
|
||||||
|
3. Load indexes before rich maps during task-start navigation.
|
||||||
|
4. Read the local rich map and actual source before editing.
|
||||||
|
5. Treat non-empty `## dirty` sections in either artifact as stale.
|
||||||
|
6. If source and generated artifacts disagree, trust source.
|
||||||
|
7. If map and index disagree, trust neither blindly; verify from source and regenerate the pair.
|
||||||
|
8. After editing source, run `project_map_patch` for each changed file.
|
||||||
|
9. Before broad architectural claims or final handoff, run `project_map_validate` when freshness matters.
|
||||||
|
|
||||||
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
|
## role
|
||||||
|
Provides configuration and documentation for a mount staging system that resolves file ownership issues in containerized environments.
|
||||||
|
## files
|
||||||
|
- .openspec.yaml | Defines an OpenSpec configuration profile for directory mounts with completion tracking metadata
|
||||||
|
- proposal.md | Proposes a design change to fix root-owned directory issues in config profile mounts by staging and bind-mounting entire directories instead of individual files. | dep: Docker, config_profile_resolver.py, test_config_profile_resolver.py
|
||||||
|
- tasks.md | Tracks completed development tasks for refactoring a mount staging system and running quality gates.
|
||||||
|
## arch
|
||||||
|
Configuration-driven design using YAML profiles for directory mounts, with a proposal-based development approach documenting a shift from file-level to directory-level bind-mount staging.
|
||||||
|
## tags
|
||||||
|
tasks, profile, directory, mounts, .openspec, staging, py, proposal
|
||||||
|
## symbols
|
||||||
|
-
|
||||||
|
## workflows
|
||||||
|
-
|
||||||
|
## dirty
|
||||||
|
-
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Config profile directory mounts
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`apply_resolved_profile` currently stages each config-profile mount file individually and bind-mounts each file into the container. Docker creates the parent directory (for example `~/.config`) as root-owned when only files are mounted, so applications running as a non-root container user cannot write new files inside that directory.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Stage a directory per mount target and bind-mount the whole directory. The staged directory is created under `instance_dir/mounts/` and is chowned to the resolved container user before the container starts, so the container user can create and modify files inside the target directory.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Change `apply_resolved_profile` in `apps/api/src/services/config/config_profile_resolver.py` to emit one directory-level volume mount per `ResolvedMount` target.
|
||||||
|
- Continue staging all configured files under that directory.
|
||||||
|
- Preserve read-only mode by setting `readonly: true` on the volume entry when `mode` is `ro`.
|
||||||
|
- Update unit tests in `apps/api/tests/unit/test_config_profile_resolver.py` to expect directory-level mounts.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- No changes to git-mount, SSH-key, workspace, or manifest mount handling.
|
||||||
|
- No changes to config profile storage or resolution semantics.
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
# Tasks
|
||||||
|
|
||||||
|
- [x] Identify the per-file mount staging in `apply_resolved_profile` as the root cause.
|
||||||
|
- [x] Refactor `apply_resolved_profile` to stage one directory per mount target and bind-mount the directory.
|
||||||
|
- [x] Propagate read-only mode to the volume entry.
|
||||||
|
- [x] Update `TestApplyResolvedProfile` unit tests to assert directory-level mounts.
|
||||||
|
- [x] Run backend quality gates (`py_compile`, `pytest`).
|
||||||
|
- [x] Commit, merge to `dev`, push.
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
name: tool-config-mount-cleanup
|
||||||
|
status: completed
|
||||||
|
completed_at: 2026-06-13
|
||||||
|
started_at: 2026-06-12
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
# . (index)
|
||||||
|
dir: .
|
||||||
|
|
||||||
|
## Project Map Protocol
|
||||||
|
|
||||||
|
1. Read this protocol and the root `.pi-map.index.md` first.
|
||||||
|
2. Use `index:` / `map:` references to open relevant directory indexes and maps.
|
||||||
|
3. Load indexes before rich maps during task-start navigation.
|
||||||
|
4. Read the local rich map and actual source before editing.
|
||||||
|
5. Treat non-empty `## dirty` sections in either artifact as stale.
|
||||||
|
6. If source and generated artifacts disagree, trust source.
|
||||||
|
7. If map and index disagree, trust neither blindly; verify from source and regenerate the pair.
|
||||||
|
8. After editing source, run `project_map_patch` for each changed file.
|
||||||
|
9. Before broad architectural claims or final handoff, run `project_map_validate` when freshness matters.
|
||||||
|
|
||||||
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
|
## role
|
||||||
|
Package containing completed specification, proposal, and task tracking for a tool configuration mount cleanup feature that separates built-in tool definitions from user-specific configuration.
|
||||||
|
## parent
|
||||||
|
-
|
||||||
|
## children
|
||||||
|
-
|
||||||
|
## files
|
||||||
|
- .openspec.yaml
|
||||||
|
- proposal.md
|
||||||
|
- tasks.md
|
||||||
|
## links
|
||||||
|
index: ./.pi-map.index.md
|
||||||
|
map: ./.pi-map.md
|
||||||
|
## workflows
|
||||||
|
-
|
||||||
|
## dirty
|
||||||
|
-
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
# .
|
||||||
|
dir: .
|
||||||
|
|
||||||
|
index: ./.pi-map.index.md
|
||||||
|
|
||||||
|
## Project Map Protocol
|
||||||
|
|
||||||
|
1. Read this protocol and the root `.pi-map.index.md` first.
|
||||||
|
2. Use `index:` / `map:` references to open relevant directory indexes and maps.
|
||||||
|
3. Load indexes before rich maps during task-start navigation.
|
||||||
|
4. Read the local rich map and actual source before editing.
|
||||||
|
5. Treat non-empty `## dirty` sections in either artifact as stale.
|
||||||
|
6. If source and generated artifacts disagree, trust source.
|
||||||
|
7. If map and index disagree, trust neither blindly; verify from source and regenerate the pair.
|
||||||
|
8. After editing source, run `project_map_patch` for each changed file.
|
||||||
|
9. Before broad architectural claims or final handoff, run `project_map_validate` when freshness matters.
|
||||||
|
|
||||||
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
|
## role
|
||||||
|
Package containing completed specification, proposal, and task tracking for a tool configuration mount cleanup feature that separates built-in tool definitions from user-specific configuration.
|
||||||
|
## files
|
||||||
|
- .openspec.yaml | Defines a completed OpenSpec configuration for a tool named "tool-config-mount-cleanup"
|
||||||
|
- proposal.md | Proposes removing configuration and state mounts from built-in tool definitions to separate tool setup from user-specific configuration handled by config profiles.
|
||||||
|
- tasks.md | This is a completed task checklist for modifying built-in tool configurations and database migrations in a codebase. | dep: alembic, pytest, opencode, pi-agent
|
||||||
|
## arch
|
||||||
|
Documentation-driven change management with OpenSpec configuration, markdown-based proposal/justification, and structured task checklist for tracking implementation of a configuration separation pattern.
|
||||||
|
## tags
|
||||||
|
tool, configuration, completed, config, .openspec, proposal, tasks, defines
|
||||||
|
## symbols
|
||||||
|
-
|
||||||
|
## workflows
|
||||||
|
-
|
||||||
|
## dirty
|
||||||
|
-
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
# Tool config mount cleanup
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The built-in tool definitions currently declare configuration and state mounts that belong in user-controlled config profiles:
|
||||||
|
|
||||||
|
- The `pi-agent` manifest mounts `pi_state` (`/tmp/.pi/agents`) and `pi_config` (`/home/user/.pi`) via a `git_mount` reference.
|
||||||
|
- The `opencode` built-in compose template mounts a named volume `opencode_home:/tmp` and sets `HOME=/tmp`.
|
||||||
|
|
||||||
|
These require manual configuration or implicit state persistence, which conflicts with the design that tool configs should only set up the actual tool, while config profiles handle user-specific configuration and state.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Remove all configuration/state mounts from built-in tool definitions. Tool configs will declare only:
|
||||||
|
|
||||||
|
- Runtime environment (packages, user, command, ports).
|
||||||
|
- The workspace/repository mount.
|
||||||
|
- SSH key mounts supplied by the platform.
|
||||||
|
|
||||||
|
User-specific configuration and state will be handled exclusively by config profiles.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Remove `pi_state` and `pi_config` mounts from the `pi-agent` manifest.
|
||||||
|
- Update the original migration that inserts the `pi-agent` manifest.
|
||||||
|
- Add a data migration to remove those mounts from existing `tool_definition_manifests` rows.
|
||||||
|
- Remove the `opencode_home:/tmp` volume and `HOME=/tmp` override from the `opencode` built-in compose template.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- No changes to config profile behavior.
|
||||||
|
- No changes to workspace or SSH key mounts.
|
||||||
|
- No changes to tool image Dockerfiles beyond what is required by mount removal.
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
# Tasks
|
||||||
|
|
||||||
|
- [x] Identify all built-in tool configs with configuration or state mounts.
|
||||||
|
- [x] Remove `pi_state` and `pi_config` mounts from the `pi-agent` manifest in `apps/api/alembic/versions/2026_05_28_add_tool_definition_manifests.py`.
|
||||||
|
- [x] Create an Alembic data migration to remove those mounts from existing `tool_definition_manifests` rows.
|
||||||
|
- [x] Remove the `opencode_home:/tmp` volume and `HOME=/tmp` environment variable from the `opencode` compose template in `apps/api/src/seeds/builtin_tool_types.py`.
|
||||||
|
- [x] Run backend quality gates (`py_compile`, `pytest`).
|
||||||
|
- [x] Commit, merge to `dev`, push.
|
||||||
Reference in New Issue
Block a user