Compare commits

...

18 Commits

Author SHA1 Message Date
Developer 896674195c Merge branch 'feat/tool-container-home-directory' into dev 2026-06-14 13:10:10 +00:00
Developer ddd92e3dd4 feat: implement configurable tool container home directory
- Add ToolType.home_directory column with default /home/user
- Add Alembic migration to add column, set existing rows, and rewrite
  /workspace to /home/user/{{WORKSPACE_NAME}} in legacy templates
- Add merge migration fc8f1a20cbf6 to resolve Alembic multiple heads
- Update manifest compiler to honor manifest.home_directory for HOME,
  WORKDIR, /workspace symlink, and default repo mount target
- Update legacy dockerfile/compose instance generation to use
  tool_type.home_directory
- Thread resolved home_dir through config profile and git mount expansion
- Generate entrypoint permission fixer to chown home/mounts at startup
- Update base.dockerfile with sudo/passwordless sudo for permission fixer
- Add unit tests for manifest compiler, instance service, and migrations
- Add placeholder integration test for container lifecycle
- Update openspec/tasks/home-path-expansion.md task checkboxes
- Update project maps for modified files

Quality gates: py_compile, ruff, mypy, pytest tests/unit (205 passed),
pytest tests/integration (110 passed, 35 skipped). Alembic round-trip
and container lifecycle integration tests require Docker/PostgreSQL.
2026-06-14 13:09:41 +00:00
Developer b4203a4a09 chore: update project maps for tool container home directory artifacts 2026-06-14 10:32:13 +00:00
Developer 5fc8e035e6 docs: tool container home directory design, plan, and test plan
- Add design doc / ADR for configurable /home/user home directory
   - Add implementation plan with phased rollout
   - Add test plan / QA checklist
   - Update OpenSpec task for home-path-expansion
2026-06-14 10:23:59 +00:00
Developer e6114ed18c Merge branch 'fix/terminal-container-overflow' into dev 2026-06-14 09:08:25 +00:00
Developer ac9f7a9299 fix: prevent terminal container from overflowing page on desktop
The desktop terminal page sometimes grew an outer scrollbar because the
terminal instance/wrapper/container chain lacked height constraints.
Without min/max-height enforcement, xterm.js's internal viewport could
expand its parent flex/grid track past the available space.

- Add overflow: hidden to .terminal-page.
- Add max-height: 100% and overflow: hidden to .terminal-instance.
- Add max-height: 100% to .terminal-wrapper.
- Add min-height: 0 to .terminal-container.
- Constrain .xterm-viewport to max-height/width 100% so it fills but
  never exceeds its container.

Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed).

Refs: openspec/changes/fix-terminal-container-overflow
2026-06-14 09:07:43 +00:00
Developer 84cf423684 Merge branch 'fix/tmux-mouse-config' into dev 2026-06-14 08:49:57 +00:00
Developer 4c14966ae3 fix: write valid multi-line tmux config in Pi Agent images
The Pi Agent dockerfile templates created ~/.tmux.conf with a literal
\n because the RUN command used single-quoted echo. Tmux never parsed
the malformed line, so mouse mode stayed off. Without tmux mouse mode,
mouse-wheel events in xterm.js fell back to Up/Down arrow keys and
cycled shell command history instead of scrolling the terminal buffer.

- Use printf '%s\n' to write real newlines in .tmux.conf.
- Apply the same fix to the ranger rc.conf where the same bug existed.
- Update tool-images/pi-agent.dockerfile and both affected alembic
  migration dockerfile strings.

Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed),
py_compile on changed migrations.

Refs: openspec/changes/fix-tmux-mouse-config
2026-06-14 08:48:39 +00:00
Developer 6c31ef9577 Merge branch 'feat/mobile-list-delete-button' into dev 2026-06-14 08:12:21 +00:00
Developer a12d6a8169 feat: restore delete buttons in mobile list views and fix edit action bar
- Wire MobileListView onItemDelete/onItemDuplicate callbacks to render
  action buttons in each list row.
- Pass onItemDelete in ToolWorkshopMobileView list view.
- Add CSS for mobile-list-item-action buttons.
- Fix MobileEditView sticky bottom action bar that was hidden behind
  the 64px mobile navigation bar; raise to bottom: 64px and z-index 110.

Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed)

Refs: openspec/changes/mobile-list-delete-button
2026-06-14 08:10:58 +00:00
Developer 13b3c60abc Merge branch 'feat/mobile-edit-default-bottom-actions' into dev 2026-06-13 22:34:48 +00:00
Developer 23fc0a6b82 feat: mobile edit-as-default with sticky save/delete actions
- Update MobileEditView to render Save and optional Delete in a
  sticky bottom action bar; header now shows Cancel + title only.
- Make ConfigProfilesMobileView open edit view on profile tap.
- Make ToolWorkshopMobileView open edit view on tool type tap.
- Wire delete into MobileEditView for existing profiles and tool
  types.
- Stay on edit view after saving an existing item; create flow
  returns to list as before.
- Update ToolWorkshopPage cancel to return to list.
- Add mobile-edit-actions and mobile-edit-delete CSS.

Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed)

Refs: openspec/changes/mobile-edit-default-bottom-actions
2026-06-13 22:29:15 +00:00
Developer 8b15689fb3 Merge branch 'feat/mobile-config-profiles-ui' into dev 2026-06-13 22:10:06 +00:00
Developer 98d4393387 feat: redesign mobile Config Profiles detail, preview, and edit pages
- Rewrite ConfigProfilesMobileView to match desktop functionality:
  full detail view with all fields, preview action showing resolved
  profile, and edit view with project/tool selects, includes,
  environment variables, runtime hints, files, mounts with nested
  files, and git mounts.
- Update useConfigProfiles.handleSubmit to return boolean success.
- Update ConfigProfilesPage to pass required state and callbacks.
- Add mobile-specific CSS for config profile forms, includes,
  mount/file cards, and preview panels.
- Allow MobileDetailView to render extra children.

Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed)

Refs: openspec/changes/mobile-config-profiles-ui
2026-06-13 22:04:17 +00:00
Developer 35f0a3ea2e Merge branch 'fix/mobile-profile-title' into dev 2026-06-13 21:45:33 +00:00
Developer 1ae8d0e45f fix: hide page title on mobile Profile view
The mobile profile form already renders its own header via
ProfileMobileView, so the desktop page title was redundant on small
viewports.

Quality gates: npm run typecheck, npm run lint

Refs: openspec/changes/mobile-tool-profile-ui
2026-06-13 21:45:33 +00:00
Developer 025ccc5817 Merge branch 'feat/mobile-tool-profile-ui' into dev 2026-06-13 21:41:51 +00:00
Developer 350b393457 feat: rework mobile UI for Tool Workshop and Profile pages
- Rework ToolWorkshopMobileView to support full desktop functionality:
  definition type selection (Compose/Dockerfile/Manifest), manifest editor,
  conditional port, startup command, readiness probe, required variables,
  and validation feedback.
- Add ProfileMobileView and wire ProfilePage to render it on mobile.
- Update useToolWorkshop hook to return boolean success from submit.
- Add responsive CSS for mobile forms, edit views, and manifest editor.
- Update project maps.

Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed)

Refs: openspec/changes/mobile-tool-profile-ui
2026-06-13 21:41:42 +00:00
111 changed files with 3616 additions and 675 deletions
+1 -1
View File
@@ -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 containerized orchestration, environment templates, and development tooling. A self-hosted web-based project management and Git repository platform with OAuth2 authentication, designed for deployment via Docker Compose with Traefik reverse proxy.
## parent ## parent
- -
## children ## children
+2 -2
View File
@@ -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 containerized orchestration, environment templates, and development tooling. A self-hosted web-based project management and Git repository platform with OAuth2/SSO authentication, containerized for Docker deployment.
## 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
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. Multi-service containerized architecture (PostgreSQL, Redis, API backend, web frontend) with reverse proxy integration (Traefik), environment-driven configuration, and AI-agent-guided development workflows via OpenSpec.
## tags ## tags
docker, redis, git, application, postgresql, compose, traefik, project docker, redis, git, application, postgresql, compose, traefik, project
## symbols ## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps dir: apps
## role ## role
Contains the main deployable application modules or entry points for the project. Contains the main application entry points and top-level configurations for different deployable targets.
## parent ## parent
index: ./.pi-map.index.md index: ./.pi-map.index.md
map: ./.pi-map.md map: ./.pi-map.md
+2 -2
View File
@@ -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 modules or entry points for the project. Contains the main deployable applications or entry points for the project.
## files ## files
## arch ## arch
Modular monolith or microservices architecture with separate application boundaries, each potentially having its own configuration, dependencies, and lifecycle. Typically follows a multi-app workspace pattern where each subdirectory is an independent executable or service sharing common libraries.
## tags ## tags
- -
## symbols ## symbols
+7 -1
View File
@@ -2,17 +2,23 @@
dir: apps/api dir: apps/api
## role ## role
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances. FastAPI backend API that manages projects, git repositories, and development tools through Docker containers with PostgreSQL persistence.
## parent ## parent
index: apps/.pi-map.index.md index: apps/.pi-map.index.md
map: apps/.pi-map.md map: apps/.pi-map.md
## children ## children
- apps/api/.mypy_cache
index: apps/api/.mypy_cache/.pi-map.index.md
map: apps/api/.mypy_cache/.pi-map.md
- apps/api/.pi-lens - apps/api/.pi-lens
index: apps/api/.pi-lens/.pi-map.index.md index: apps/api/.pi-lens/.pi-map.index.md
map: apps/api/.pi-lens/.pi-map.md map: apps/api/.pi-lens/.pi-map.md
- apps/api/.pytest_cache - apps/api/.pytest_cache
index: apps/api/.pytest_cache/.pi-map.index.md index: apps/api/.pytest_cache/.pi-map.index.md
map: apps/api/.pytest_cache/.pi-map.md map: apps/api/.pytest_cache/.pi-map.md
- apps/api/.ruff_cache
index: apps/api/.ruff_cache/.pi-map.index.md
map: apps/api/.ruff_cache/.pi-map.md
- apps/api/.venv-test - apps/api/.venv-test
index: apps/api/.venv-test/.pi-map.index.md index: apps/api/.venv-test/.pi-map.index.md
map: apps/api/.venv-test/.pi-map.md map: apps/api/.venv-test/.pi-map.md
+1 -1
View File
@@ -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/FastAPI with PostgreSQL (Alembic migrations), multi-stage Docker deployment with Cloudflare tunneling, uv package management, and containerized service orchestration. Multi-stage containerized architecture with async PostgreSQL database, Alembic migrations, Cloudflare tunneling, Docker socket access, and uv-based Python dependency management.
## tags ## tags
docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn
## symbols ## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/alembic dir: apps/api/alembic
## role ## role
Database migration infrastructure for the API application, providing version-controlled schema evolution with async SQLAlchemy support. Database migration infrastructure for the API application, enabling version-controlled schema changes 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
+2 -2
View File
@@ -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 the API application, providing version-controlled schema evolution with async SQLAlchemy support. Database migration infrastructure for the API application, enabling version-controlled schema changes 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 Mako templating for generating revision scripts, async SQLAlchemy engine configuration, and autogenerate capabilities for schema change tracking. Alembic migration framework with Mako templating for script generation, async SQLAlchemy engine configuration, and environment-based database connection management.
## 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
+3 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/alembic/versions dir: apps/api/alembic/versions
## role ## role
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. Database schema evolution and versioning management for the API application using Alembic migration scripts.
## 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
@@ -48,6 +48,7 @@ map: apps/api/alembic/.pi-map.md
- 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 - 2026_06_13_make_clone_mode_nullable.py
- 2026_06_14_104415_add_tool_type_home_directory.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
@@ -55,6 +56,7 @@ map: apps/api/alembic/.pi-map.md
- 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
- fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py
## links ## links
index: apps/api/alembic/versions/.pi-map.index.md index: apps/api/alembic/versions/.pi-map.index.md
map: apps/api/alembic/versions/.pi-map.md map: apps/api/alembic/versions/.pi-map.md
+9 -7
View File
@@ -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
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. Database schema evolution and versioning management for the API application using Alembic migration scripts.
## 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
@@ -24,7 +24,7 @@ Manages incremental database schema evolution for the API application using Alem
- 0014_merge_heads.py | Alembic merge migration that reconciles two divergent migration branches into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic - 0014_merge_heads.py | Alembic merge migration that reconciles two divergent migration branches into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
- 0015_single_interface.py | Alembic database migration that replaces a JSON array `interfaces` column with `interface_type` string and `requires_port` boolean columns in the `tool_types` table, with dialect-specific data migration for PostgreSQL and SQLite. | exp: func:_get_dialect() → str, call:op.get_bind, func:upgrade() → None, call:_get_dialect, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Boolean, call:op.execute, call:op.alter_column, call:op.drop_column, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:_get_dialect, call:op.drop_constraint, call:op.add_column, call:sa.Column, call:postgresql.JSONB, call:sa.Text, call:op.execute, call:sa.JSON, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql (dialect) - 0015_single_interface.py | Alembic database migration that replaces a JSON array `interfaces` column with `interface_type` string and `requires_port` boolean columns in the `tool_types` table, with dialect-specific data migration for PostgreSQL and SQLite. | exp: func:_get_dialect() → str, call:op.get_bind, func:upgrade() → None, call:_get_dialect, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Boolean, call:op.execute, call:op.alter_column, call:op.drop_column, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:_get_dialect, call:op.drop_constraint, call:op.add_column, call:sa.Column, call:postgresql.JSONB, call:sa.Text, call:op.execute, call:sa.JSON, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql (dialect)
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py | Alembic database migration that adds a nullable UUID foreign key column `ssh_key_id` to the `config_profiles` table referencing `ssh_keys.id` with SET NULL on delete | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy - 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py | Alembic database migration that adds a nullable UUID foreign key column `ssh_key_id` to the `config_profiles` table referencing `ssh_keys.id` with SET NULL on delete | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
- 20260527160017_add_pi_agent_tool_type.py | Alembic database migration that adds a "pi-agent" terminal-based coding tool type to a tool_types table with Docker configuration templates | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text("SELECT id FROM tool_types WHERE name = 'pi-agent'") ).fetchone, call:sa.text, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text | dep: json, typing, alembic, uuid, sqlalchemy - 20260527160017_add_pi_agent_tool_type.py | Alembic database migration that adds a new "pi-agent" tool type with terminal-based Docker environment for the Pi coding agent | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text("SELECT id FROM tool_types WHERE name = 'pi-agent'") ).fetchone, call:sa.text, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text | dep: json, typing, alembic, uuid, sqlalchemy
- 2026_05_22_add_clone_mode.py | Alembic database migration that adds ssh_key_id foreign key to git_repositories table and clone_mode/branch columns to tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:op.create_foreign_key, call:sa.String, func:downgrade() → None, call:op.drop_column, call:op.drop_constraint | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql - 2026_05_22_add_clone_mode.py | Alembic database migration that adds ssh_key_id foreign key to git_repositories table and clone_mode/branch columns to tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:op.create_foreign_key, call:sa.String, func:downgrade() → None, call:op.drop_column, call:op.drop_constraint | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
- 2026_05_23_remove_is_builtin.py | Alembic database migration to remove the `is_builtin` column from the `tool_types` table | exp: func:upgrade() → None, call:op.execute, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Boolean | dep: alembic, sqlalchemy - 2026_05_23_remove_is_builtin.py | Alembic database migration to remove the `is_builtin` column from the `tool_types` table | exp: func:upgrade() → None, call:op.execute, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Boolean | dep: alembic, sqlalchemy
- 2026_05_24_220141_add_startup_command.py | Alembic database migration that adds a nullable `startup_command` text column to the `tool_types` table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy - 2026_05_24_220141_add_startup_command.py | Alembic database migration that adds a nullable `startup_command` text column to the `tool_types` table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
@@ -33,7 +33,7 @@ Manages incremental database schema evolution for the API application using Alem
- 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 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_add_tool_definition_manifests.py | Alembic database migration that creates a tool_definition_manifests table, adds manifest support to existing tool_types and tool_instances tables, and seeds initial data with a base Ubuntu image and pi-agent manifest while migrating the legacy pi-agent from Dockerfile templates to the new manifest system. | 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,18 +44,20 @@ Manages incremental database schema evolution for the API application using Alem
- 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 - 2026_06_13_make_clone_mode_nullable.py | Alembic database migration that makes the `clone_mode` column in `tool_instances` table nullable to support workspace-first cleanup workflow. | exp: func:upgrade() → None, call:op.alter_column, call:sa.String, func:downgrade() → None, call:op.alter_column, call:sa.String | dep: alembic, sqlalchemy
- 2026_06_14_104415_add_tool_type_home_directory.py | Alembic database migration that adds a `home_directory` column to `tool_types` table and updates template strings to use a configurable workspace path instead of hardcoded `/workspace` | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, func:downgrade() → None, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, call:op.drop_column | dep: typing, alembic, sqlalchemy.sql, 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 - 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py | Alembic database migration that removes or restores pi_state and pi_config mounts from the pi-agent manifest stored in tool_definition_manifests table | 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
- fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py | Alembic database migration that merges two revision branches by declaring them as down revisions without performing any schema changes. | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic, sqlalchemy
## arch ## arch
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. Sequential and branched migration pattern with merge resolution, using Alembic's revision-based approach with upgrade/downgrade functions, including data migrations, conditional schema changes, and cross-dialect support (PostgreSQL/SQLite).
## tags ## tags
column, table, call:op.drop, downgrade, alembic, upgrade, key, call:sa.text column, table, call:op.drop, alembic, downgrade, upgrade, key, call:sa.text
## symbols ## symbols
- upgrade - upgrade
- downgrade - downgrade
@@ -104,11 +104,11 @@ RUN git config --global init.defaultBranch main \\
&& git config --global user.name "Developer" && git config --global user.name "Developer"
# Create default tmux config # Create default tmux config
RUN echo 'set -g mouse on\\nset -g default-terminal "screen-256color"' > /home/user/.tmux.conf RUN printf '%s\\n' 'set -g mouse on' 'set -g default-terminal "screen-256color"' > /home/user/.tmux.conf
# Create default ranger config # Create default ranger config
RUN mkdir -p /home/user/.config/ranger \\ RUN mkdir -p /home/user/.config/ranger \\
&& echo 'set preview_files true\\nset use_preview_script true' > /home/user/.config/ranger/rc.conf && printf '%s\\n' 'set preview_files true' 'set use_preview_script true' > /home/user/.config/ranger/rc.conf
# Set up Pi config directory # Set up Pi config directory
RUN mkdir -p /home/user/.pi/agent RUN mkdir -p /home/user/.pi/agent
@@ -308,10 +308,10 @@ RUN git config --global init.defaultBranch main \\
&& git config --global user.email "dev@headquarter.local" \\ && git config --global user.email "dev@headquarter.local" \\
&& git config --global user.name "Developer" && git config --global user.name "Developer"
RUN echo 'set -g mouse on\\nset -g default-terminal "screen-256color"' > /home/user/.tmux.conf RUN printf '%s\\n' 'set -g mouse on' 'set -g default-terminal "screen-256color"' > /home/user/.tmux.conf
RUN mkdir -p /home/user/.config/ranger \\ RUN mkdir -p /home/user/.config/ranger \\
&& echo 'set preview_files true\\nset use_preview_script true' > /home/user/.config/ranger/rc.conf && printf '%s\\n' 'set preview_files true' 'set use_preview_script true' > /home/user/.config/ranger/rc.conf
RUN mkdir -p /home/user/.pi/agent RUN mkdir -p /home/user/.pi/agent
@@ -0,0 +1,89 @@
"""add_tool_type_home_directory
Revision ID: 2026_06_14_104415
Revises: f3d2dc90ba3a
Create Date: 2026-06-14 10:44:15.000000
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql import column, table
# revision identifiers, used by Alembic.
revision: str = "2026_06_14_104415"
down_revision: Union[str, Sequence[str], None] = "f3d2dc90ba3a"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
tool_types = table(
"tool_types",
column("id", sa.UUID),
column("home_directory", sa.String),
column("compose_template", sa.Text),
column("dockerfile_template", sa.Text),
)
OLD_WORKSPACE = "/workspace"
NEW_WORKSPACE = "/home/user/{{WORKSPACE_NAME}}"
def upgrade() -> None:
# Add the new column with a default that applies to existing rows.
op.add_column(
"tool_types",
sa.Column(
"home_directory",
sa.String(255),
nullable=False,
server_default="/home/user",
),
)
# Rewrite legacy templates that mount the workspace at /workspace so they
# use the new configurable home directory and preserve the workspace name.
op.execute(
sa.update(tool_types)
.where(tool_types.c.compose_template.is_not(None))
.values(
compose_template=sa.func.replace(
tool_types.c.compose_template, OLD_WORKSPACE, NEW_WORKSPACE
)
)
)
op.execute(
sa.update(tool_types)
.where(tool_types.c.dockerfile_template.is_not(None))
.values(
dockerfile_template=sa.func.replace(
tool_types.c.dockerfile_template, OLD_WORKSPACE, NEW_WORKSPACE
)
)
)
def downgrade() -> None:
# Restore the original /workspace strings before dropping the column.
op.execute(
sa.update(tool_types)
.where(tool_types.c.compose_template.is_not(None))
.values(
compose_template=sa.func.replace(
tool_types.c.compose_template, NEW_WORKSPACE, OLD_WORKSPACE
)
)
)
op.execute(
sa.update(tool_types)
.where(tool_types.c.dockerfile_template.is_not(None))
.values(
dockerfile_template=sa.func.replace(
tool_types.c.dockerfile_template, NEW_WORKSPACE, OLD_WORKSPACE
)
)
)
op.drop_column("tool_types", "home_directory")
@@ -0,0 +1,23 @@
"""merge home directory and pi agent mount cleanup heads
Revision ID: fc8f1a20cbf6
Revises: 2026_06_14_104415, 8c6d1dbd4798
Create Date: 2026-06-14 11:08:41.273502
"""
# revision identifiers, used by Alembic.
revision = 'fc8f1a20cbf6'
down_revision = ('2026_06_14_104415', '8c6d1dbd4798')
branch_labels = None
depends_on = None
def upgrade() -> None:
pass
def downgrade() -> None:
pass
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/src dir: apps/api/src
## role ## role
Core API application package that initializes and configures the Headquarter FastAPI backend with database, authentication, logging, and middleware infrastructure. Core application bootstrap and infrastructure layer for the Headquarter API, providing configuration, database connectivity, logging, and FastAPI application initialization.
## 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
+2 -2
View File
@@ -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 API application package that initializes and configures the Headquarter FastAPI backend with database, authentication, logging, and middleware infrastructure. Core API application package for the Headquarter service, providing configuration, database connectivity, structured logging, and FastAPI application initialization.
## 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 API application package that initializes and configures the Headquarter Fas
- 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 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. Layered architecture with environment-based Pydantic configuration, async SQLAlchemy with Alembic migrations, structured JSON logging with correlation IDs, and FastAPI middleware/routing pattern with background services integration.
## tags ## tags
src, database, logging, call:logger.info, api, middleware, fastapi, filter src, database, logging, call:logger.info, api, middleware, fastapi, filter
## symbols ## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/src/models dir: apps/api/src/models
## role ## role
Centralized database model definitions and shared infrastructure for the API's data layer. Provides the SQLAlchemy ORM data models and database schema definitions for 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
+2 -2
View File
@@ -4,12 +4,12 @@ dir: apps/api/src/models
index: apps/api/src/models/.pi-map.index.md index: apps/api/src/models/.pi-map.index.md
## role ## role
Centralized database model definitions and shared infrastructure for the API's data layer. Provides the SQLAlchemy ORM data models and database schema definitions for the API application.
## files ## files
- __init__.py | Re-exports model classes from submodules to provide a centralized public API for the src.models package | dep: src.models.base, src.models.config.config_profile, src.models.project.git_repository, src.models.project.project, src.models.project.workspace, src.models.system.health_check, src.models.system.instance_event, src.models.system.notification, src.models.system.terminal_session, src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type, src.models.user.ssh_key, src.models.user.user, src.models.user.user_config - __init__.py | Re-exports model classes from submodules to provide a centralized public API for the src.models package | dep: src.models.base, src.models.config.config_profile, src.models.project.git_repository, src.models.project.project, src.models.project.workspace, src.models.system.health_check, src.models.system.instance_event, src.models.system.notification, src.models.system.terminal_session, src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type, src.models.user.ssh_key, src.models.user.user, src.models.user.user_config
- base.py | Defines SQLAlchemy base model and reusable mixins for UUID primary keys and automatic timestamp tracking in database models. | exp: class:Base, class:UUIDPrimaryKeyMixin, class:TimestampMixin | dep: uuid, datetime, sqlalchemy, sqlalchemy.orm - base.py | Defines SQLAlchemy base model and reusable mixins for UUID primary keys and automatic timestamp tracking in database models. | exp: class:Base, class:UUIDPrimaryKeyMixin, class:TimestampMixin | dep: uuid, datetime, sqlalchemy, sqlalchemy.orm
## arch ## arch
SQLAlchemy ORM with declarative base, mixin-based composition for cross-cutting concerns (UUIDs, timestamps), and explicit package-level re-exports for clean public API surface. Layered repository pattern with declarative SQLAlchemy base, UUID/timestamp mixins for reusable model traits, and package-level facade pattern via __init__.py re-exports to centralize model access.
## tags ## tags
models, src, base, project, system, user, mixin, tool models, src, base, project, system, user, mixin, tool
## symbols ## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/src/models/tool dir: apps/api/src/models/tool
## role ## role
Database models for containerized tool lifecycle management, covering tool definitions, deployment instances, and categorization types. Provides SQLAlchemy ORM models for managing containerized tool definitions, types, and deployed instances in the API.
## parent ## parent
index: apps/api/src/models/.pi-map.index.md index: apps/api/src/models/.pi-map.index.md
map: apps/api/src/models/.pi-map.md map: apps/api/src/models/.pi-map.md
+4 -4
View File
@@ -4,16 +4,16 @@ dir: apps/api/src/models/tool
index: apps/api/src/models/tool/.pi-map.index.md index: apps/api/src/models/tool/.pi-map.index.md
## role ## role
Database models for containerized tool lifecycle management, covering tool definitions, deployment instances, and categorization types. Provides SQLAlchemy ORM models for managing containerized tool definitions, types, and deployed instances in the API.
## files ## files
- __init__.py | Exports the public API for the tool models module by re-exporting three key classes. | dep: src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type - __init__.py | Exports the public API for the tool models module by re-exporting three key classes. | dep: src.models.tool.tool_definition_manifest, src.models.tool.tool_instance, src.models.tool.tool_type
- tool_definition_manifest.py | Defines a SQLAlchemy ORM model for storing tool definition manifests that compile to Dockerfiles and Compose files, supporting both base definitions and tool-specific definitions with inheritance. | exp: class:ToolDefinitionManifest | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.user - tool_definition_manifest.py | Defines a SQLAlchemy ORM model for storing tool definition manifests that compile to Dockerfiles and Compose files, supporting both base definitions and tool-specific definitions with inheritance. | exp: class:ToolDefinitionManifest | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.user
- tool_instance.py | Defines a SQLAlchemy ORM model for tool instances that represent deployed tools with container metadata, status tracking, and relationships to users, projects, workspaces, and other entities. | exp: class:ToolInstance | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models, src.models.project, src.models.user, src.models (ConfigProfile, GitRepository, Project, ToolType, User, Workspace) - tool_instance.py | Defines a SQLAlchemy ORM model for tool instances that represent deployed tools with container metadata, status tracking, and relationships to users, projects, workspaces, and other entities. | exp: class:ToolInstance | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models, src.models.project, src.models.user, src.models (ConfigProfile, GitRepository, Project, ToolType, User, Workspace)
- tool_type.py | Defines a SQLAlchemy ORM model for tool types that represent configurable categories of tools with deployment templates, manifest references, and metadata. | exp: class:ToolType | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.tool.tool_definition_manifest, src.models.user - tool_type.py | Defines a SQLAlchemy ORM model for tool types that specify configuration templates and metadata for deployable tools in a containerized environment. | exp: class:ToolType | dep: uuid, typing, sqlalchemy, sqlalchemy.orm, src.models.base, src.models.tool.tool_definition_manifest, src.models.user
## arch ## arch
SQLAlchemy ORM with declarative models using inheritance hierarchies, relationship mappings, and polymorphic manifest compilation for Docker/Compose deployment. Domain-driven data models using SQLAlchemy ORM with declarative base pattern, entity relationships, and inheritance support for tool manifest definitions.
## tags ## tags
tool, models, src, sqlalchemy, orm, definition, manifest, base tool, models, src, sqlalchemy, orm, definition, base, manifest
## symbols ## symbols
- ToolDefinitionManifest - ToolDefinitionManifest
- ToolInstance - ToolInstance
+3
View File
@@ -39,6 +39,9 @@ class ToolType(UUIDPrimaryKeyMixin, TimestampMixin, Base):
) )
readiness_probe: Mapped[dict | None] = mapped_column(JSON, nullable=True) readiness_probe: Mapped[dict | None] = mapped_column(JSON, nullable=True)
startup_command: Mapped[str | None] = mapped_column(Text, nullable=True) startup_command: Mapped[str | None] = mapped_column(Text, nullable=True)
home_directory: Mapped[str] = mapped_column(
String(255), nullable=False, default="/home/user"
)
required_variables: Mapped[list[str]] = mapped_column( required_variables: Mapped[list[str]] = mapped_column(
JSON, default=list, nullable=False JSON, default=list, nullable=False
) )
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/src/services dir: apps/api/src/services
## role ## role
Marks the services directory as a Python package for business logic layer components. Marks the services directory as a Python package for organizing business logic and service-layer abstractions.
## 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
+2 -2
View File
@@ -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
Provides a Python package namespace for organizing service-layer modules in the API application. This directory serves as 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
Standard Python package structure using __init__.py for directory-based module organization, following conventional layered architecture patterns. Standard Python package structure using __init__.py to define an importable module directory, following conventional layered architecture patterns.
## tags ## tags
init, empty, functionality init, empty, functionality
## symbols ## symbols
+129 -11
View File
@@ -8,6 +8,7 @@ from typing import Any
import yaml import yaml
from src.services.config.config_profile_resolver import expand_container_path
from src.services.docker import sort_volumes_by_specificity from src.services.docker import sort_volumes_by_specificity
@@ -158,6 +159,8 @@ def compile_dockerfile(manifest: dict) -> str:
# User creation # User creation
user = manifest.get("user") user = manifest.get("user")
home_dir = get_manifest_home_dir(manifest)
workspace_name = manifest.get("workspace_name", "{{WORKSPACE_NAME}}")
if user: if user:
name = user["name"] name = user["name"]
uid = user["uid"] uid = user["uid"]
@@ -168,22 +171,21 @@ def compile_dockerfile(manifest: dict) -> str:
lines.append(f" useradd -u {uid} -g {gid} {create_home}-s {shell} {name}") lines.append(f" useradd -u {uid} -g {gid} {create_home}-s {shell} {name}")
lines.append("") lines.append("")
# Set HOME and USER for runtime compatibility # Set HOME and USER for runtime compatibility
home = f"/home/{name}" lines.append(f"ENV HOME={home_dir}")
lines.append(f"ENV HOME={home}")
lines.append(f"ENV USER={name}") lines.append(f"ENV USER={name}")
lines.append("") lines.append("")
# Ensure home directory exists and is writable by the user. # Ensure home directory exists and is writable by the user.
# Recursively chown so any files copied from /etc/skel by useradd -m # Recursively chown so any files copied from /etc/skel by useradd -m
# (e.g. .bashrc, .config) are owned by the container user. # (e.g. .bashrc, .config) are owned by the container user.
lines.append( lines.append(
f"RUN mkdir -p {home} && chown -R {name}:{name} {home} && chmod 755 {home}" f"RUN mkdir -p {home_dir} && chown -R {name}:{name} {home_dir} && chmod 755 {home_dir}"
) )
# Pre-create common config directories so apps like ranger can write # Pre-create common config directories so apps like ranger can write
# their configs on first run without permission errors. # their configs on first run without permission errors.
common_dirs = [".config", ".local/share", ".cache"] common_dirs = [".config", ".local/share", ".cache"]
for d in common_dirs: for d in common_dirs:
lines.append( lines.append(
f"RUN mkdir -p {home}/{d} && chown -R {name}:{name} {home}/{d}" f"RUN mkdir -p {home_dir}/{d} && chown -R {name}:{name} {home_dir}/{d}"
) )
lines.append("") lines.append("")
@@ -208,10 +210,12 @@ def compile_dockerfile(manifest: dict) -> str:
# After build scripts, ensure everything in home is owned by the user # After build scripts, ensure everything in home is owned by the user
if user and build_scripts: if user and build_scripts:
lines.append(f"RUN chown -R {name}:{name} {home}") lines.append(f"RUN chown -R {name}:{name} {home_dir}")
lines.append("") lines.append("")
# Create mount target directories # Create mount target directories and /workspace compatibility symlink.
# The symlink target includes the workspace/repo name so legacy scripts
# that cd into /workspace still land on the right project.
mounts = manifest.get("mounts", []) mounts = manifest.get("mounts", [])
if mounts: if mounts:
dirs = [mount["target"] for mount in mounts] dirs = [mount["target"] for mount in mounts]
@@ -221,6 +225,16 @@ def compile_dockerfile(manifest: dict) -> str:
lines.append(f"RUN chown -R {user['name']}:{user['name']} {dir_str}") lines.append(f"RUN chown -R {user['name']}:{user['name']} {dir_str}")
lines.append("") lines.append("")
workspace_target = f"{home_dir}/{workspace_name}"
lines.append(f"RUN mkdir -p {workspace_target}")
if user:
lines.append(
f"RUN ln -sfn {workspace_target} /workspace && chown -R {user['name']}:{user['name']} {home_dir}"
)
else:
lines.append(f"RUN ln -sfn {workspace_target} /workspace")
lines.append("")
# Entrypoint for startup scripts # Entrypoint for startup scripts
startup_scripts = manifest.get("scripts", {}).get("startup", []) startup_scripts = manifest.get("scripts", {}).get("startup", [])
if startup_scripts: if startup_scripts:
@@ -233,11 +247,18 @@ def compile_dockerfile(manifest: dict) -> str:
# Switch to runtime user # Switch to runtime user
if user: if user:
lines.append(f"USER {user['name']}") lines.append(f"USER {user['name']}")
lines.append(f"WORKDIR /home/{user['name']}")
# Set WORKDIR to the configured home directory unless runtime.working_dir
# explicitly overrides it.
runtime = manifest.get("runtime", {})
working_dir = runtime.get("working_dir")
if working_dir:
lines.append(f"WORKDIR {expand_container_path(working_dir, home_dir)}")
else:
lines.append(f"WORKDIR {home_dir}")
lines.append("") lines.append("")
# Entrypoint and CMD # Entrypoint and CMD
runtime = manifest.get("runtime", {})
if startup_scripts: if startup_scripts:
lines.append('ENTRYPOINT ["/usr/local/bin/headquarter-entrypoint"]') lines.append('ENTRYPOINT ["/usr/local/bin/headquarter-entrypoint"]')
@@ -251,6 +272,11 @@ def compile_dockerfile(manifest: dict) -> str:
def compile_entrypoint(manifest: dict) -> str: def compile_entrypoint(manifest: dict) -> str:
"""Generate the startup entrypoint script from startup scripts. """Generate the startup entrypoint script from startup scripts.
Injects a permission-fixer preamble that runs as root (or via sudo) before
any user-defined startup script. It chowns the home directory and a safe
subset of mount parents to the container user, creates the /workspace
compatibility symlink, and avoids recursive chown of large repo subtrees.
Args: Args:
manifest: Fully resolved manifest JSON. manifest: Fully resolved manifest JSON.
@@ -259,6 +285,70 @@ def compile_entrypoint(manifest: dict) -> str:
""" """
lines = ["#!/bin/bash", "set -e", ""] lines = ["#!/bin/bash", "set -e", ""]
user = manifest.get("user")
home_dir = get_manifest_home_dir(manifest)
workspace_name = manifest.get("workspace_name", "{{WORKSPACE_NAME}}")
workspace_target = f"{home_dir}/{workspace_name}"
# Permission fixer preamble: run as root when possible, else fall back to
# passwordless sudo configured in the Dockerfile.
lines.append("# Permission fixer preamble")
lines.append("CONTAINER_USER=''")
lines.append('if [ "$(id -u)" = '"'"'0'"'"' ]; then')
if user:
lines.append(f" CONTAINER_USER='{user['name']}'")
lines.append("else")
lines.append(" # Try passwordless sudo; ignore failure so the container still starts")
lines.append(" if sudo -n true 2>/dev/null; then")
lines.append(" SUDO='sudo'")
lines.append(" else")
lines.append(" SUDO=''")
lines.append(" fi")
lines.append("fi")
lines.append("")
if user:
name = user["name"]
uid = user["uid"]
gid = user["gid"]
lines.append(f"USER_NAME='{name}'")
lines.append(f"USER_UID='{uid}'")
lines.append(f"USER_GID='{gid}'")
lines.append(f"HOME_DIR='{home_dir}'")
lines.append(f"WORKSPACE_TARGET='{workspace_target}'")
lines.append("")
lines.append("fix_owner() {")
lines.append(" local path=\"$1\"")
lines.append(' [ -e "$path" ] || return 0')
lines.append(' if [ -n "$SUDO" ]; then')
lines.append(' sudo chown "$USER_UID:$USER_GID" "$path" 2>/dev/null || true')
lines.append(' elif [ "$(id -u)" = "0" ]; then')
lines.append(' chown "$USER_UID:$USER_GID" "$path" 2>/dev/null || true')
lines.append(' fi')
lines.append("}")
lines.append("")
lines.append("# Ensure home directory exists and is owned by the container user")
lines.append('mkdir -p "$HOME_DIR"')
lines.append('fix_owner "$HOME_DIR"')
lines.append("")
lines.append("# Ensure workspace target exists and is owned by the container user")
lines.append('mkdir -p "$WORKSPACE_TARGET"')
lines.append('fix_owner "$WORKSPACE_TARGET"')
lines.append("")
lines.append("# Create /workspace compatibility symlink")
lines.append('ln -sfn "$WORKSPACE_TARGET" /workspace')
lines.append("")
lines.append("# Fix ownership of declared mount targets (top-level only)")
for mount in manifest.get("mounts", []):
target = mount.get("target")
if not target:
continue
# Expand any ~/$HOME placeholders in the mount target.
expanded = target.replace("~", home_dir).replace("$HOME", home_dir)
if expanded.startswith(home_dir) and not mount.get("readonly", False):
lines.append(f'fix_owner "{expanded}"')
lines.append("")
startup_scripts = manifest.get("scripts", {}).get("startup", []) startup_scripts = manifest.get("scripts", {}).get("startup", [])
for script in startup_scripts: for script in startup_scripts:
lines.append(script) lines.append(script)
@@ -281,6 +371,13 @@ def compile_compose(manifest: dict, variables: dict[str, Any]) -> str:
runtime = manifest.get("runtime", {}) runtime = manifest.get("runtime", {})
user = manifest.get("user") user = manifest.get("user")
interface_type = manifest["interface_type"] interface_type = manifest["interface_type"]
home_dir = get_manifest_home_dir(manifest)
# Determine the workspace/repo name from variables when available.
workspace_name = variables.get(
"WORKSPACE_NAME",
variables.get("REPO_NAME", "workspace"),
)
service: dict[str, Any] = { service: dict[str, Any] = {
"image": variables["IMAGE_TAG"], "image": variables["IMAGE_TAG"],
@@ -294,7 +391,9 @@ def compile_compose(manifest: dict, variables: dict[str, Any]) -> str:
if runtime.get("tty", False): if runtime.get("tty", False):
service["tty"] = True service["tty"] = True
if runtime.get("working_dir"): if runtime.get("working_dir"):
service["working_dir"] = runtime["working_dir"] service["working_dir"] = expand_container_path(
runtime["working_dir"], home_dir
)
# User override # User override
if user: if user:
@@ -319,14 +418,24 @@ def compile_compose(manifest: dict, variables: dict[str, Any]) -> str:
# Volumes from mount schema # Volumes from mount schema
volumes = [] volumes = []
has_explicit_repo_mount = False
for mount in manifest.get("mounts", []): for mount in manifest.get("mounts", []):
source = resolve_mount_source(mount, variables) source = resolve_mount_source(mount, variables)
if not source: if not source:
continue continue
target = mount["target"] if mount.get("source_type") == "repo":
has_explicit_repo_mount = True
target = expand_container_path(mount["target"], home_dir)
readonly = ":ro" if mount.get("readonly", False) else "" readonly = ":ro" if mount.get("readonly", False) else ""
volumes.append(f"{source}:{target}{readonly}") volumes.append(f"{source}:{target}{readonly}")
# Synthesize a default repo/workspace mount when the manifest does not
# declare an explicit repo mount. This preserves the repo root directory
# name under the configured home directory.
if not has_explicit_repo_mount and variables.get("REPO_PATH"):
target = f"{home_dir}/{workspace_name}"
volumes.append(f"{variables['REPO_PATH']}:{target}")
# Append extra volumes from tool config / config profile # Append extra volumes from tool config / config profile
for vol in variables.get("EXTRA_VOLUMES", []): for vol in variables.get("EXTRA_VOLUMES", []):
vol_str = f"{vol['source']}:{vol['target']}" vol_str = f"{vol['source']}:{vol['target']}"
@@ -385,7 +494,12 @@ def resolve_mount_source(mount: dict, variables: dict[str, Any]) -> str:
def get_manifest_home_dir(manifest: dict) -> str: def get_manifest_home_dir(manifest: dict) -> str:
"""Get the home directory for a container based on manifest user config. """Get the home directory for a container based on manifest config.
Precedence:
1. manifest["home_directory"] if present and non-empty.
2. /home/{user.name} if manifest.user.name is present.
3. /root otherwise.
Args: Args:
manifest: Fully resolved manifest JSON. manifest: Fully resolved manifest JSON.
@@ -393,6 +507,10 @@ def get_manifest_home_dir(manifest: dict) -> str:
Returns: Returns:
Home directory path (e.g., /home/user or /root). Home directory path (e.g., /home/user or /root).
""" """
home_directory = manifest.get("home_directory")
if home_directory and isinstance(home_directory, str) and home_directory.strip():
return home_directory.strip()
user = manifest.get("user") user = manifest.get("user")
if user and user.get("name"): if user and user.get("name"):
return f"/home/{user['name']}" return f"/home/{user['name']}"
@@ -2,7 +2,7 @@
dir: apps/api/src/services/docker dir: apps/api/src/services/docker
## role ## role
Provides Docker infrastructure services for container lifecycle management, compose orchestration, configuration deployment, and secure tunneling to expose internal services. Provides Docker infrastructure services for container lifecycle management, compose orchestration, secure configuration deployment, and external tunnel exposure.
## 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
+3 -3
View File
@@ -4,15 +4,15 @@ dir: apps/api/src/services/docker
index: apps/api/src/services/docker/.pi-map.index.md index: apps/api/src/services/docker/.pi-map.index.md
## role ## role
Provides Docker infrastructure services for container lifecycle management, compose orchestration, configuration deployment, and secure tunneling to expose internal services. Provides Docker infrastructure services for container lifecycle management, compose orchestration, secure configuration deployment, and external tunnel exposure.
## files ## files
- __init__.py | Package initialization file that exposes Docker-related service functions for container operations, compose management, configuration staging, and tunnel management. | dep: src.services.docker.compose, src.services.docker.config_staging, src.services.docker.container, src.services.docker.tunnel - __init__.py | Package initialization file that exposes Docker-related service functions for container operations, compose management, configuration staging, and tunnel management. | dep: src.services.docker.compose, src.services.docker.config_staging, src.services.docker.container, src.services.docker.tunnel
- compose.py | Generates, renders, and executes Docker Compose commands for container orchestration with volume sorting and template substitution. | exp: func:sort_volumes_by_specificity(volumes: list[str]) → list[str], call:vol.split, call:len, call:parts[1].rstrip, call:target.count, call:targets.append, call:Counter(targets).items, call:logger.warning, call:sorted, func:_target_depth(vol: str) → int, call:vol.split, call:len, call:parts[1].rstrip, call:target.count, func:render_compose_template(template: str, variables: dict[str, Any]) → str, call:variables.items, call:result.replace, call:str, func:write_compose_file(instance_dir: str, content: str) → str, call:Path, call:compose_path.write_text, call:str, func:execute_compose_command(compose_path: str, action: str, timeout, env_file) → tuple[int, str, str], call:Path, call:cmd.extend, call:cmd.append, call:subprocess.run, call:str, raise:ValueError | dep: logging, subprocess, collections, pathlib, typing, collections.Counter, pathlib.Path, typing.Any - compose.py | Generates, renders, and executes Docker Compose files with volume sorting and template variable substitution. | exp: func:sort_volumes_by_specificity(volumes: list[str]) → list[str], call:vol.split, call:len, call:parts[1].rstrip, call:target.count, call:targets.append, call:Counter(targets).items, call:logger.warning, call:sorted, func:_target_depth(vol: str) → int, call:vol.split, call:len, call:parts[1].rstrip, call:target.count, func:render_compose_template(template: str, variables: dict[str, Any]) → str, call:variables.items, call:result.replace, call:str, call:aliases.items, call:variables.get, func:write_compose_file(instance_dir: str, content: str) → str, call:Path, call:compose_path.write_text, call:str, func:execute_compose_command(compose_path: str, action: str, timeout, env_file) → tuple[int, str, str], call:Path, call:cmd.extend, call:cmd.append, call:subprocess.run, call:str, raise:ValueError | dep: logging, subprocess, collections, pathlib, typing, collections.Counter, pathlib.Path, typing.Any
- config_staging.py | Stages configuration files into instance directories with security checks for path traversal. | exp: func:ensure_instance_directory(instance_id: str, base_path) → str, call:Settings, call:Path, call:instance_dir.mkdir, call:str, call:instance_dir.absolute, func:write_env_file(instance_dir: str, env_vars: dict[str, str]) → str, call:Path, call:env_vars.items, call:env_path.write_text, call:"\n".join, call:str, func:write_config_files(instance_dir: str, files: dict[str, str]) → None, call:Path, call:files.items, call:full_path.resolve().relative_to, call:instance_path.resolve, call:full_path.parent.mkdir, call:full_path.write_text, raise:ValueError | dep: logging, pathlib, src.config, src.config.Settings - config_staging.py | Stages configuration files into instance directories with security checks for path traversal. | exp: func:ensure_instance_directory(instance_id: str, base_path) → str, call:Settings, call:Path, call:instance_dir.mkdir, call:str, call:instance_dir.absolute, func:write_env_file(instance_dir: str, env_vars: dict[str, str]) → str, call:Path, call:env_vars.items, call:env_path.write_text, call:"\n".join, call:str, func:write_config_files(instance_dir: str, files: dict[str, str]) → None, call:Path, call:files.items, call:full_path.resolve().relative_to, call:instance_path.resolve, call:full_path.parent.mkdir, call:full_path.write_text, raise:ValueError | dep: logging, pathlib, src.config, src.config.Settings
- container.py | Provides Docker container runtime queries and network management utilities via subprocess calls to the Docker CLI. | exp: func:get_container_id(instance_name: str) → str | None, call:instance_name.lower, call:subprocess.run, call:result.stdout.strip, call:ps_result.stdout.strip().splitlines, call:line.split, call:len, call:name.lower, func:get_container_name(instance_name: str) → str | None, call:subprocess.run, call:instance_name.lower, call:result.stdout.strip().lstrip, func:get_backend_network_name() → str, call:subprocess.run, call:result.stdout.strip().split, call:net.lower, func:connect_container_to_network(container_name: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_ip_on_network(container_id: str, network_name) → str | None, call:get_backend_network_name, call:subprocess.run, call:result.stdout.strip, func:is_container_on_network(container_id: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_status(container_id: str) → dict[str, Any], call:subprocess.run, call:result.stdout.strip().split, call:int, call:len, call:parts[1].isdigit, func:wait_for_container_running(container_id: str, timeout, interval) → dict[str, Any], call:time.time, call:get_container_status, call:time.sleep, func:get_container_logs(container_id: str, tail) → str, call:subprocess.run, call:str, func:find_free_port(start, end) → int, call:range, call:socket.socket, call:s.connect_ex, raise:RuntimeError | dep: logging, subprocess, time, typing, socket - container.py | Provides Docker container runtime queries and network management utilities via subprocess calls to the Docker CLI. | exp: func:get_container_id(instance_name: str) → str | None, call:instance_name.lower, call:subprocess.run, call:result.stdout.strip, call:ps_result.stdout.strip().splitlines, call:line.split, call:len, call:name.lower, func:get_container_name(instance_name: str) → str | None, call:subprocess.run, call:instance_name.lower, call:result.stdout.strip().lstrip, func:get_backend_network_name() → str, call:subprocess.run, call:result.stdout.strip().split, call:net.lower, func:connect_container_to_network(container_name: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_ip_on_network(container_id: str, network_name) → str | None, call:get_backend_network_name, call:subprocess.run, call:result.stdout.strip, func:is_container_on_network(container_id: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_status(container_id: str) → dict[str, Any], call:subprocess.run, call:result.stdout.strip().split, call:int, call:len, call:parts[1].isdigit, func:wait_for_container_running(container_id: str, timeout, interval) → dict[str, Any], call:time.time, call:get_container_status, call:time.sleep, func:get_container_logs(container_id: str, tail) → str, call:subprocess.run, call:str, func:find_free_port(start, end) → int, call:range, call:socket.socket, call:s.connect_ex, raise:RuntimeError | dep: logging, subprocess, time, typing, socket
- tunnel.py | Manages Cloudflare tunnels by orchestrating cloudflared Docker containers to expose internal services via temporary public URLs. | exp: func:_tunnel_container_name(instance_name: str) → str, call:instance_name.lower, func:_ensure_image() → None, call:subprocess.run, call:result.stdout.strip, call:logger.info, call:logger.warning, func:_cleanup_stale_tunnel(tunnel_name: str) → None, call:subprocess.run, func:_get_tunnel_logs(tunnel_name: str) → tuple[str, str], call:subprocess.run, func:_get_tunnel_exit_code(tunnel_name: str) → int | None, call:subprocess.run, call:int, call:result.stdout.strip, func:start_tunnel(instance_name: str, container_port: int, timeout, target_url) → dict[str, str], call:_ensure_image, call:_tunnel_container_name, call:_cleanup_stale_tunnel, call:instance_name.lower, call:get_backend_network_name, call:logger.debug, call:" ".join, call:subprocess.run, call:proc.stdout.strip, call:re.compile, call:__import__("time").time, call:_get_tunnel_logs, call:url_pattern.search, call:match.group, call:_get_tunnel_exit_code, call:__import__("time").sleep, call:logger.info, raise:RuntimeError, func:stop_tunnel(instance_name: str) → None, call:_tunnel_container_name, call:_cleanup_stale_tunnel, call:logger.debug, func:recreate_tunnel(instance_name: str, container_port: int, target_url) → dict[str, str], call:stop_tunnel, call:start_tunnel, func:check_tunnel_health(url: str, timeout) → dict[str, Any], call:subprocess.run, call:int, call:result.stdout.strip, call:str(exc).lower, call:any | dep: logging, re, subprocess, typing, src.services.docker.container - tunnel.py | Manages Cloudflare tunnels by orchestrating cloudflared Docker containers to expose internal services via temporary public URLs. | exp: func:_tunnel_container_name(instance_name: str) → str, call:instance_name.lower, func:_ensure_image() → None, call:subprocess.run, call:result.stdout.strip, call:logger.info, call:logger.warning, func:_cleanup_stale_tunnel(tunnel_name: str) → None, call:subprocess.run, func:_get_tunnel_logs(tunnel_name: str) → tuple[str, str], call:subprocess.run, func:_get_tunnel_exit_code(tunnel_name: str) → int | None, call:subprocess.run, call:int, call:result.stdout.strip, func:start_tunnel(instance_name: str, container_port: int, timeout, target_url) → dict[str, str], call:_ensure_image, call:_tunnel_container_name, call:_cleanup_stale_tunnel, call:instance_name.lower, call:get_backend_network_name, call:logger.debug, call:" ".join, call:subprocess.run, call:proc.stdout.strip, call:re.compile, call:__import__("time").time, call:_get_tunnel_logs, call:url_pattern.search, call:match.group, call:_get_tunnel_exit_code, call:__import__("time").sleep, call:logger.info, raise:RuntimeError, func:stop_tunnel(instance_name: str) → None, call:_tunnel_container_name, call:_cleanup_stale_tunnel, call:logger.debug, func:recreate_tunnel(instance_name: str, container_port: int, target_url) → dict[str, str], call:stop_tunnel, call:start_tunnel, func:check_tunnel_health(url: str, timeout) → dict[str, Any], call:subprocess.run, call:int, call:result.stdout.strip, call:str(exc).lower, call:any | dep: logging, re, subprocess, typing, src.services.docker.container
## arch ## arch
Subprocess-based CLI wrapper architecture around Docker/cloudflared tools with template rendering, path-traversal-safe file staging, and functional decomposition into single-responsibility modules. Service-oriented utility modules with subprocess-based Docker CLI integration, Jinja2 templating for compose generation, and security-hardened file operations with path traversal validation.
## tags ## tags
tunnel, container, call:subprocess.run, get, name, call:, network, call:result.stdout.strip tunnel, container, call:subprocess.run, get, name, call:, network, call:result.stdout.strip
## symbols ## symbols
+13
View File
@@ -61,6 +61,19 @@ def render_compose_template(template: str, variables: dict[str, Any]) -> str:
for key, value in variables.items(): for key, value in variables.items():
placeholder = f"{{{{{key}}}}}" placeholder = f"{{{{{key}}}}}"
result = result.replace(placeholder, str(value)) result = result.replace(placeholder, str(value))
# Convenience aliases so legacy and migrated templates can use lowercase
# placeholders without changing every stored template.
aliases = {
"{{workspace_name}}": "WORKSPACE_NAME",
"{{home_directory}}": "HOME_DIRECTORY",
}
for alias_placeholder, key in aliases.items():
if alias_placeholder in result:
result = result.replace(
alias_placeholder, str(variables.get(key, "workspace"))
)
return result return result
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/src/services/tool dir: apps/api/src/services/tool
## role ## role
Provides backend infrastructure for provisioning and managing isolated development tool instances with their dependencies and network access. Orchestrates end-to-end deployment and runtime management of development tool instances via containerized environments with remote access capabilities.
## 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
@@ -9,7 +9,6 @@ import subprocess
import uuid import uuid
from datetime import datetime from datetime import datetime
import httpx
from fastapi import HTTPException, status from fastapi import HTTPException, status
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -33,7 +32,6 @@ from src.services.docker import (
get_container_id, get_container_id,
get_container_ip_on_network, get_container_ip_on_network,
get_container_logs, get_container_logs,
get_container_status,
is_container_on_network, is_container_on_network,
render_compose_template, render_compose_template,
sort_volumes_by_specificity, sort_volumes_by_specificity,
@@ -64,10 +62,9 @@ from src.services.shared.permission_fixer import (
apply_ssh_permissions, apply_ssh_permissions,
) )
from src.services.shared.readiness_probe import execute_probe from src.services.shared.readiness_probe import execute_probe
from src.services.shared.ssh_keys import cleanup_ssh_key_files, prepare_ssh_key_files from src.services.shared.ssh_keys import prepare_ssh_key_files
from src.services.instance.event_bus import InstanceEventBus from src.services.instance.event_bus import InstanceEventBus
from src.services.instance.lifecycle_hooks import publish_lifecycle_event from src.services.instance.lifecycle_hooks import publish_lifecycle_event
from src.auth.dependencies import _get_owned_project, _get_user
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_event_bus = InstanceEventBus() _event_bus = InstanceEventBus()
@@ -139,7 +136,7 @@ async def resolve_git_mounts(
if isinstance(result, Exception): if isinstance(result, Exception):
logger.warning("Git mount failed: %s", result) logger.warning("Git mount failed: %s", result)
continue continue
if result: if isinstance(result, list):
volume_mounts.extend(result) volume_mounts.extend(result)
return volume_mounts return volume_mounts
@@ -1043,7 +1040,11 @@ async def create_tool_instance(
else "" else ""
) )
compose_content = f"""version: "3.8"\nservices:\n app:\n image: {image_tag}\n container_name: {instance_name.lower()}\n stdin_open: true\n tty: true\n{ports_section} volumes:\n - {repo_path}:/workspace\n restart: unless-stopped\n""" home_dir = tool_type.home_directory or "/home/user"
repo_name = os.path.basename(os.path.normpath(repo_path))
workspace_target = f"{home_dir}/{repo_name}"
compose_content = f"""version: "3.8"\nservices:\n app:\n image: {image_tag}\n container_name: {instance_name.lower()}\n stdin_open: true\n tty: true\n{ports_section} environment:\n - HOME={home_dir}\n volumes:\n - {repo_path}:{workspace_target}\n working_dir: {workspace_target}\n restart: unless-stopped\n"""
write_compose_file(instance_dir, compose_content) write_compose_file(instance_dir, compose_content)
elif tool_type.definition_type == "manifest": elif tool_type.definition_type == "manifest":
@@ -1095,6 +1096,8 @@ async def create_tool_instance(
"TOOL_PORT": tool_port, "TOOL_PORT": tool_port,
"USER_ID": str(user_id), "USER_ID": str(user_id),
"PROJECT_ID": str(project_id), "PROJECT_ID": str(project_id),
"WORKSPACE_NAME": os.path.basename(os.path.normpath(repo_path)),
"HOME_DIRECTORY": tool_type.home_directory or "/home/user",
} }
compose_content = render_compose_template( compose_content = render_compose_template(
tool_type.compose_template, variables tool_type.compose_template, variables
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/tests dir: apps/api/tests
## role ## role
Provides shared test infrastructure and fixtures for the FastAPI API application. Provides shared test infrastructure and fixtures for the API application's test suite.
## 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
+2 -2
View File
@@ -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 the FastAPI API application. Provides shared test infrastructure and fixtures for the API application's test suite.
## 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 using async SQLite in-memory database, dependency injection overrides, and async HTTP client setup for isolated integration tests. Pytest plugin architecture with dependency-injected async fixtures for database, HTTP client, and authentication state management.
## 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 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/tests/integration dir: apps/api/tests/integration
## role ## role
Integration test suite for the API backend, covering authentication, workspaces, projects, notifications, configuration, and database models with real PostgreSQL and git repositories. Integration and unit test suite for the API backend, covering authentication, CRUD APIs, workspace operations, git integration, notifications, and database model validation against real PostgreSQL and mocked dependencies.
## 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
@@ -21,6 +21,7 @@ map: apps/api/tests/.pi-map.md
- test_projects_api.py - test_projects_api.py
- test_seed.py - test_seed.py
- test_ssh_keys_api.py - test_ssh_keys_api.py
- test_tool_instance_lifecycle.py
- test_tool_types_api.py - test_tool_types_api.py
- test_tool_types_api_extended.py - test_tool_types_api_extended.py
- test_users_api.py - test_users_api.py
+3 -2
View File
@@ -4,7 +4,7 @@ dir: apps/api/tests/integration
index: apps/api/tests/integration/.pi-map.index.md index: apps/api/tests/integration/.pi-map.index.md
## role ## role
Integration test suite for the API backend, covering authentication, workspaces, projects, notifications, configuration, and database models with real PostgreSQL and git repositories. Integration and unit test suite for the API backend, covering authentication, CRUD APIs, workspace operations, git integration, notifications, and database model validation against real PostgreSQL and mocked dependencies.
## files ## files
- __init__.py | Empty file with no functionality - __init__.py | Empty file with no functionality
- test_auth_api.py | Integration tests for authentication API endpoints using a real PostgreSQL database | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_auth_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:importlib.reload, func:_insert_test_user(user_id: str) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_login_redirects_to_authentik_authorize_endpoint() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.get, func:test_me_returns_401_without_session_cookie() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.get, func:test_me_returns_user_with_valid_session() → None, call:_prepare_auth_test_db, call:_insert_test_user, call:_load_app, call:Settings, call:create_session_cookie, call:TestClient, call:client.get, call:response.json, func:test_logout_clears_session_cookie() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.post, call:response.headers.get | dep: uuid, asyncio, importlib, fastapi.testclient, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.user.user, asyncpg, fastapi - test_auth_api.py | Integration tests for authentication API endpoints using a real PostgreSQL database | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_auth_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:importlib.reload, func:_insert_test_user(user_id: str) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_login_redirects_to_authentik_authorize_endpoint() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.get, func:test_me_returns_401_without_session_cookie() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.get, func:test_me_returns_user_with_valid_session() → None, call:_prepare_auth_test_db, call:_insert_test_user, call:_load_app, call:Settings, call:create_session_cookie, call:TestClient, call:client.get, call:response.json, func:test_logout_clears_session_cookie() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.post, call:response.headers.get | dep: uuid, asyncio, importlib, fastapi.testclient, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.user.user, asyncpg, fastapi
@@ -18,6 +18,7 @@ Integration test suite for the API backend, covering authentication, workspaces,
- test_projects_api.py | Integration tests for a FastAPI projects API endpoint, verifying authentication, CRUD operations, and ownership-based authorization against a real PostgreSQL database. | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:hasattr, call:asyncio.run, call:database_module.engine.dispose, call:importlib.reload, func:_mint_token(user_id: str) → str, call:Settings, call:create_session_cookie, func:_insert_user(user_id: str, email) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_insert_project(project_id: str, owner_id: str, name) → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:Project, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:Project, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_create_project_requires_authentication() → None, call:_prepare_test_db, call:_load_app, call:TestClient, call:client.post, func:test_create_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, call:response.json, func:test_list_projects_returns_only_owned_projects() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, call:len, func:test_update_project_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch, func:test_update_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch, call:response.json, func:test_delete_project_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, func:test_delete_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, func:test_set_default_ssh_key_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch | dep: uuid, datetime, asyncio, pytest, fastapi.testclient, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.project.project, src.models.user.user, asyncpg, importlib, fastapi, src.database, src.api.user.auth, src.api.project.projects, src.main - test_projects_api.py | Integration tests for a FastAPI projects API endpoint, verifying authentication, CRUD operations, and ownership-based authorization against a real PostgreSQL database. | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:hasattr, call:asyncio.run, call:database_module.engine.dispose, call:importlib.reload, func:_mint_token(user_id: str) → str, call:Settings, call:create_session_cookie, func:_insert_user(user_id: str, email) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_insert_project(project_id: str, owner_id: str, name) → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:Project, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:Project, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_create_project_requires_authentication() → None, call:_prepare_test_db, call:_load_app, call:TestClient, call:client.post, func:test_create_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, call:response.json, func:test_list_projects_returns_only_owned_projects() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, call:len, func:test_update_project_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch, func:test_update_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch, call:response.json, func:test_delete_project_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, func:test_delete_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, func:test_set_default_ssh_key_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch | dep: uuid, datetime, asyncio, pytest, fastapi.testclient, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.project.project, src.models.user.user, asyncpg, importlib, fastapi, src.database, src.api.user.auth, src.api.project.projects, src.main
- test_seed.py | Tests deterministic seed user generation and database seeding functionality for development environments. | exp: func:test_build_seed_user_returns_deterministic_payload() → None, call:build_seed_user, func:test_seed_database_creates_development_user(db_session: AsyncSession) → None, call:seed_database, call:db_session.scalar, call:select(User).where | dep: pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.user, src.scripts.seed - test_seed.py | Tests deterministic seed user generation and database seeding functionality for development environments. | exp: func:test_build_seed_user_returns_deterministic_payload() → None, call:build_seed_user, func:test_seed_database_creates_development_user(db_session: AsyncSession) → None, call:seed_database, call:db_session.scalar, call:select(User).where | dep: pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.user, src.scripts.seed
- test_ssh_keys_api.py | Integration tests verifying SSH key API endpoints require authentication | exp: func:test_create_ssh_key_requires_authentication(test_client: TestClient) → None, call:test_client.post, func:test_list_ssh_keys_requires_authentication(test_client: TestClient) → None, call:test_client.get | dep: pytest, fastapi.testclient - test_ssh_keys_api.py | Integration tests verifying SSH key API endpoints require authentication | exp: func:test_create_ssh_key_requires_authentication(test_client: TestClient) → None, call:test_client.post, func:test_list_ssh_keys_requires_authentication(test_client: TestClient) → None, call:test_client.get | dep: pytest, fastapi.testclient
- test_tool_instance_lifecycle.py | Integration test placeholder for verifying Docker availability and container home directory lifecycle behavior | exp: func:_docker_available() → bool, call:subprocess.run, func:test_docker_available_placeholder() → None, call:_docker_available | dep: subprocess, pytest
- test_tool_types_api.py | Integration tests for a FastAPI tool types REST API endpoint using a real PostgreSQL database. | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:hasattr, call:asyncio.run, call:database_module.engine.dispose, call:importlib.reload, func:_mint_token(user_id: str) → str, call:Settings, call:create_session_cookie, call:datetime.now, call:timedelta, func:_insert_user(user_id: str, email) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_insert_tool_type(tool_type_id: str, name: str, display_name: str, compose_template: str, created_by_id) → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:ToolType, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:ToolType, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_list_tool_types_requires_authentication() → None, call:_prepare_test_db, call:_load_app, call:TestClient, call:client.get, func:test_list_tool_types_returns_all_types() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, call:len, call:next, func:test_get_tool_type_by_id() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, func:test_get_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, func:test_create_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, call:response.json, func:test_create_tool_type_duplicate_name() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_invalid_yaml() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_missing_services() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_missing_required_variable() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_update_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.put, call:response.json, func:test_update_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.put, func:test_delete_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, call:client.get, func:test_delete_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete | dep: uuid, datetime, asyncio, pytest, fastapi.testclient, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.tool.tool_type, src.models.user.user, asyncpg, importlib, fastapi - test_tool_types_api.py | Integration tests for a FastAPI tool types REST API endpoint using a real PostgreSQL database. | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:hasattr, call:asyncio.run, call:database_module.engine.dispose, call:importlib.reload, func:_mint_token(user_id: str) → str, call:Settings, call:create_session_cookie, call:datetime.now, call:timedelta, func:_insert_user(user_id: str, email) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_insert_tool_type(tool_type_id: str, name: str, display_name: str, compose_template: str, created_by_id) → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:ToolType, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:ToolType, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_list_tool_types_requires_authentication() → None, call:_prepare_test_db, call:_load_app, call:TestClient, call:client.get, func:test_list_tool_types_returns_all_types() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, call:len, call:next, func:test_get_tool_type_by_id() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, func:test_get_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, func:test_create_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, call:response.json, func:test_create_tool_type_duplicate_name() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_invalid_yaml() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_missing_services() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_missing_required_variable() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_update_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.put, call:response.json, func:test_update_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.put, func:test_delete_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, call:client.get, func:test_delete_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete | dep: uuid, datetime, asyncio, pytest, fastapi.testclient, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.tool.tool_type, src.models.user.user, asyncpg, importlib, fastapi
- test_tool_types_api_extended.py | Integration tests for a FastAPI tool types API endpoint covering CRUD operations with extended fields like dockerfile templates, readiness probes, startup commands, and validation. | exp: class:TestToolTypesAPIExtended, method:test_create_tool_type_with_dockerfile(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_with_readiness_probe(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_invalid_definition_type(self, authenticated_client: TestClient) → None, call:authenticated_client.post, method:test_create_tool_type_dockerfile_without_template(self, authenticated_client: TestClient) → None, call:authenticated_client.post, method:test_update_tool_type_with_new_fields(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.put, call:response.json, method:test_validate_tool_type_compose(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_validate_tool_type_invalid_compose(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_validate_tool_type_dockerfile(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_get_tool_type_returns_new_fields(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.get, call:response.json, method:test_create_tool_type_without_port_fails(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, call:str, method:test_create_tool_type_with_port_mismatch_fails(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_with_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_update_tool_type_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.put, call:response.json, method:test_get_tool_type_returns_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.get, call:response.json | dep: pytest, fastapi.testclient, fastapi.testclient.TestClient - test_tool_types_api_extended.py | Integration tests for a FastAPI tool types API endpoint covering CRUD operations with extended fields like dockerfile templates, readiness probes, startup commands, and validation. | exp: class:TestToolTypesAPIExtended, method:test_create_tool_type_with_dockerfile(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_with_readiness_probe(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_invalid_definition_type(self, authenticated_client: TestClient) → None, call:authenticated_client.post, method:test_create_tool_type_dockerfile_without_template(self, authenticated_client: TestClient) → None, call:authenticated_client.post, method:test_update_tool_type_with_new_fields(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.put, call:response.json, method:test_validate_tool_type_compose(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_validate_tool_type_invalid_compose(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_validate_tool_type_dockerfile(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_get_tool_type_returns_new_fields(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.get, call:response.json, method:test_create_tool_type_without_port_fails(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, call:str, method:test_create_tool_type_with_port_mismatch_fails(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_with_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_update_tool_type_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.put, call:response.json, method:test_get_tool_type_returns_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.get, call:response.json | dep: pytest, fastapi.testclient, fastapi.testclient.TestClient
- test_users_api.py | Integration tests for user profile API endpoints using a real PostgreSQL database | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_users_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:importlib.reload, func:_insert_test_user(user_id: str) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_create_auth_cookie(user_id: str) → str, call:Settings, call:create_session_cookie, func:test_get_profile_returns_401_without_cookie() → None, call:_prepare_users_test_db, call:_load_app, call:TestClient, call:client.get, func:test_get_profile_returns_user_data() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.get, call:response.json, func:test_update_profile_changes_name_and_email() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, call:response.json, func:test_update_profile_rejects_empty_name() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, func:test_update_profile_rejects_invalid_email() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, func:test_upload_avatar_updates_avatar_url() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO, call:response.json, call:data["avatar_url"].startswith, func:test_upload_avatar_rejects_invalid_file_type() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO, func:test_upload_avatar_rejects_oversized_file() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO | dep: uuid, datetime, asyncio, io, fastapi.testclient, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.user.user, asyncpg, importlib, fastapi, src.database, src.api.user.users, src.main - test_users_api.py | Integration tests for user profile API endpoints using a real PostgreSQL database | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, func:_prepare_users_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:importlib.reload, func:_insert_test_user(user_id: str) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_create_auth_cookie(user_id: str) → str, call:Settings, call:create_session_cookie, func:test_get_profile_returns_401_without_cookie() → None, call:_prepare_users_test_db, call:_load_app, call:TestClient, call:client.get, func:test_get_profile_returns_user_data() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.get, call:response.json, func:test_update_profile_changes_name_and_email() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, call:response.json, func:test_update_profile_rejects_empty_name() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, func:test_update_profile_rejects_invalid_email() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, func:test_upload_avatar_updates_avatar_url() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO, call:response.json, call:data["avatar_url"].startswith, func:test_upload_avatar_rejects_invalid_file_type() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO, func:test_upload_avatar_rejects_oversized_file() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO | dep: uuid, datetime, asyncio, io, fastapi.testclient, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.user.user, asyncpg, importlib, fastapi, src.database, src.api.user.users, src.main
@@ -26,7 +27,7 @@ Integration test suite for the API backend, covering authentication, workspaces,
- test_workspace_instances.py | Integration tests for FastAPI workspace instance endpoints (list and create) using an authenticated test client and mocked database fixtures. | exp: class:TestListWorkspaceInstances, method:test_list_empty(self, authenticated_client: TestClient, test_workspace_with_tool_type), call:authenticated_client.get, call:response.json, method:test_list_instances(self, authenticated_client: TestClient, db_session: AsyncSession, test_workspace_with_tool_type), call:ToolInstance, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_create_instance, call:authenticated_client.get, call:response.json, call:len, class:TestCreateWorkspaceInstance, method:test_create_instance(self, authenticated_client: TestClient, test_workspace_with_tool_type), call:ToolInstance, call:uuid.uuid4, call:datetime.now, call:patch, call:authenticated_client.post, call:str, call:response.json, func:_get_user_id(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_workspace_with_tool_type(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:ToolType, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:ToolType, call:db_session.commit, call:db_session.refresh | dep: asyncio, tempfile, uuid, datetime, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.auth.session, src.config, src.api.workspace.workspace_instances - test_workspace_instances.py | Integration tests for FastAPI workspace instance endpoints (list and create) using an authenticated test client and mocked database fixtures. | exp: class:TestListWorkspaceInstances, method:test_list_empty(self, authenticated_client: TestClient, test_workspace_with_tool_type), call:authenticated_client.get, call:response.json, method:test_list_instances(self, authenticated_client: TestClient, db_session: AsyncSession, test_workspace_with_tool_type), call:ToolInstance, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_create_instance, call:authenticated_client.get, call:response.json, call:len, class:TestCreateWorkspaceInstance, method:test_create_instance(self, authenticated_client: TestClient, test_workspace_with_tool_type), call:ToolInstance, call:uuid.uuid4, call:datetime.now, call:patch, call:authenticated_client.post, call:str, call:response.json, func:_get_user_id(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_workspace_with_tool_type(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:ToolType, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:ToolType, call:db_session.commit, call:db_session.refresh | dep: asyncio, tempfile, uuid, datetime, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.auth.session, src.config, src.api.workspace.workspace_instances
- test_workspaces_api.py | Integration tests for workspace API endpoints including list, create, delete, and sync operations with repository/project scoping | exp: class:TestListWorkspaces, method:test_list_empty(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.get, call:response.json, method:test_list_with_workspaces(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:authenticated_client.get, call:response.json, call:len, class:TestCreateWorkspace, method:test_create_success(self, authenticated_client: TestClient, test_repo: GitRepository), call:Workspace, call:uuid.uuid4, call:patch.object, call:authenticated_client.post, call:response.json, call:mock_create.assert_called_once, method:test_create_missing_name(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.post, call:response.json, method:test_create_duplicate_name(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:patch.object, call:Exception, call:authenticated_client.post, class:TestDeleteWorkspace, method:test_delete_without_instances(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, call:response.json, method:test_delete_with_instances_force(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, class:TestSyncWorkspace, method:test_sync_success(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, method:test_sync_branch_deleted(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, func:_get_user_id_from_client(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_repo(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id_from_client, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh | dep: asyncio, uuid, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.services.shared.workspace_manager, src.auth.session, src.config, fastapi, sqlalchemy - test_workspaces_api.py | Integration tests for workspace API endpoints including list, create, delete, and sync operations with repository/project scoping | exp: class:TestListWorkspaces, method:test_list_empty(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.get, call:response.json, method:test_list_with_workspaces(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:authenticated_client.get, call:response.json, call:len, class:TestCreateWorkspace, method:test_create_success(self, authenticated_client: TestClient, test_repo: GitRepository), call:Workspace, call:uuid.uuid4, call:patch.object, call:authenticated_client.post, call:response.json, call:mock_create.assert_called_once, method:test_create_missing_name(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.post, call:response.json, method:test_create_duplicate_name(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:patch.object, call:Exception, call:authenticated_client.post, class:TestDeleteWorkspace, method:test_delete_without_instances(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, call:response.json, method:test_delete_with_instances_force(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, class:TestSyncWorkspace, method:test_sync_success(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, method:test_sync_branch_deleted(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, func:_get_user_id_from_client(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_repo(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id_from_client, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh | dep: asyncio, uuid, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.services.shared.workspace_manager, src.auth.session, src.config, fastapi, sqlalchemy
## arch ## arch
Pytest-based integration testing using real external dependencies (PostgreSQL, git repos), FastAPI TestClient with authenticated fixtures, mocked database fixtures for isolation, and security validation patterns (cookie/ OIDC/ SSE auth). Pytest-based integration testing with FastAPI TestClient, SQLAlchemy async sessions, real PostgreSQL database fixtures, and selective mocking; follows pattern of endpoint-per-test-file with authenticated client fixtures and seeded database state.
## tags ## tags
test, call:, call:db, call:authenticated, user, create, call:response.json, call:create test, call:, call:db, call:authenticated, user, create, call:response.json, call:create
## symbols ## symbols
@@ -0,0 +1,34 @@
"""Integration tests for tool container home directory behavior.
These tests exercise container lifecycle behavior and require Docker and a
running PostgreSQL database. They are skipped when Docker is unavailable.
"""
import subprocess
import pytest
def _docker_available() -> bool:
try:
result = subprocess.run(
["docker", "info"],
capture_output=True,
text=True,
timeout=10,
)
return result.returncode == 0
except Exception:
return False
@pytest.mark.integration
@pytest.mark.skipif(not _docker_available(), reason="Docker not available")
def test_docker_available_placeholder() -> None:
"""Placeholder to keep the test file valid when Docker is present.
Real lifecycle tests (container starts with HOME=/home/user, /workspace
symlink works, git mounts are writable) should be added here once the
test harness can start the API and Docker services.
"""
assert _docker_available()
+4 -2
View File
@@ -2,7 +2,7 @@
dir: apps/api/tests/unit dir: apps/api/tests/unit
## role ## role
Contains comprehensive unit tests for the API backend services covering configuration, Docker operations, Git integration, file management, health monitoring, notifications, and SSH key handling. Unit test suite for the API application covering core services, utilities, and infrastructure components.
## 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
@@ -10,6 +10,7 @@ map: apps/api/tests/.pi-map.md
- -
## files ## files
- __init__.py - __init__.py
- test_alembic_migrations.py
- test_config.py - test_config.py
- test_config_profile_resolver.py - test_config_profile_resolver.py
- test_docker_build.py - test_docker_build.py
@@ -21,6 +22,7 @@ map: apps/api/tests/.pi-map.md
- test_git_url_parser.py - test_git_url_parser.py
- test_health_monitor.py - test_health_monitor.py
- test_home_path_expansion.py - test_home_path_expansion.py
- test_instance_service.py
- test_lifecycle_hooks.py - test_lifecycle_hooks.py
- test_manifest_compiler.py - test_manifest_compiler.py
- test_migration_metadata.py - test_migration_metadata.py
@@ -35,7 +37,7 @@ index: apps/api/tests/unit/.pi-map.index.md
map: apps/api/tests/unit/.pi-map.md map: apps/api/tests/unit/.pi-map.md
## workflows ## workflows
- change unit behavior - change unit behavior
read: __init__.py, test_config.py, test_config_profile_resolver.py read: __init__.py, test_alembic_migrations.py, test_config.py
- change unit config - change unit config
read: test_config.py, test_config_profile_resolver.py read: test_config.py, test_config_profile_resolver.py
## dirty ## dirty
+7 -5
View File
@@ -4,9 +4,10 @@ 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 backend services covering configuration, Docker operations, Git integration, file management, health monitoring, notifications, and SSH key handling. Unit test suite for the API application covering core services, utilities, and infrastructure components.
## files ## files
- __init__.py | Empty file with no functionality - __init__.py | Empty file with no functionality
- test_alembic_migrations.py | Unit tests that verify Alembic database migrations are importable, have correct identifiers, and declare expected dependencies without requiring a live database. | exp: func:test_home_directory_migration_imports_and_rewrites() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_merge_migration_resolves_heads() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable | dep: importlib.util, pathlib, pytest, importlib
- 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 | 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_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
@@ -18,8 +19,9 @@ Contains comprehensive unit tests for the API backend services covering configur
- test_git_url_parser.py | Tests for git URL parsing utilities that extract base repository URLs, validate clone URLs, and parse various git URL formats across GitHub, GitLab, and Bitbucket. | exp: class:TestExtractBaseRepoUrl, method:test_github_tree_url(self), call:extract_base_repo_url, method:test_github_blob_url(self), call:extract_base_repo_url, method:test_github_pull_url(self), call:extract_base_repo_url, method:test_github_issues_url(self), call:extract_base_repo_url, method:test_github_valid_url(self), call:extract_base_repo_url, method:test_github_url_with_query_params(self), call:extract_base_repo_url, method:test_gitlab_tree_url(self), call:extract_base_repo_url, method:test_gitlab_blob_url(self), call:extract_base_repo_url, method:test_gitlab_merge_request_url(self), call:extract_base_repo_url, method:test_gitlab_valid_url(self), call:extract_base_repo_url, method:test_bitbucket_src_url(self), call:extract_base_repo_url, method:test_bitbucket_valid_url(self), call:extract_base_repo_url, method:test_ssh_url(self), call:extract_base_repo_url, method:test_ssh_url_without_git_suffix(self), call:extract_base_repo_url, method:test_invalid_url(self), call:extract_base_repo_url, method:test_empty_url(self), call:extract_base_repo_url, class:TestIsValidCloneUrl, method:test_valid_ssh_url(self), call:is_valid_clone_url, method:test_valid_https_url(self), call:is_valid_clone_url, method:test_browser_url(self), call:is_valid_clone_url, method:test_url_without_git_suffix(self), call:is_valid_clone_url, method:test_invalid_url(self), call:is_valid_clone_url, class:TestParseGitUrl, method:test_valid_git_url(self), call:parse_git_url, method:test_browser_url(self), call:parse_git_url, method:test_invalid_url(self), call:parse_git_url, method:test_empty_url(self), call:parse_git_url, method:test_ssh_url(self), call:parse_git_url | dep: src.utils.git_url_parser, pytest - test_git_url_parser.py | Tests for git URL parsing utilities that extract base repository URLs, validate clone URLs, and parse various git URL formats across GitHub, GitLab, and Bitbucket. | exp: class:TestExtractBaseRepoUrl, method:test_github_tree_url(self), call:extract_base_repo_url, method:test_github_blob_url(self), call:extract_base_repo_url, method:test_github_pull_url(self), call:extract_base_repo_url, method:test_github_issues_url(self), call:extract_base_repo_url, method:test_github_valid_url(self), call:extract_base_repo_url, method:test_github_url_with_query_params(self), call:extract_base_repo_url, method:test_gitlab_tree_url(self), call:extract_base_repo_url, method:test_gitlab_blob_url(self), call:extract_base_repo_url, method:test_gitlab_merge_request_url(self), call:extract_base_repo_url, method:test_gitlab_valid_url(self), call:extract_base_repo_url, method:test_bitbucket_src_url(self), call:extract_base_repo_url, method:test_bitbucket_valid_url(self), call:extract_base_repo_url, method:test_ssh_url(self), call:extract_base_repo_url, method:test_ssh_url_without_git_suffix(self), call:extract_base_repo_url, method:test_invalid_url(self), call:extract_base_repo_url, method:test_empty_url(self), call:extract_base_repo_url, class:TestIsValidCloneUrl, method:test_valid_ssh_url(self), call:is_valid_clone_url, method:test_valid_https_url(self), call:is_valid_clone_url, method:test_browser_url(self), call:is_valid_clone_url, method:test_url_without_git_suffix(self), call:is_valid_clone_url, method:test_invalid_url(self), call:is_valid_clone_url, class:TestParseGitUrl, method:test_valid_git_url(self), call:parse_git_url, method:test_browser_url(self), call:parse_git_url, method:test_invalid_url(self), call:parse_git_url, method:test_empty_url(self), call:parse_git_url, method:test_ssh_url(self), call:parse_git_url | dep: src.utils.git_url_parser, pytest
- test_health_monitor.py | Unit tests for HealthMonitor state-transition logic covering container crash detection, tunnel failure detection, recovery detection, write deduplication, exception resilience, and start/stop lifecycle. | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:health_monitor(event_bus: InstanceEventBus) → HealthMonitor, call:HealthMonitor, func:_create_running_instance(db_session) → ToolInstance, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, func:test_detects_container_crash(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_tunnel_failure(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_recovery(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:db_session.commit, call:HealthSnapshot, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_skips_writes_when_no_state_change(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:HealthSnapshot, call:patch, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:len, call:result.scalars().all, func:test_docker_exception_resilience(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:RuntimeError, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one_or_none, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_monitor_start_stop(health_monitor: HealthMonitor) → None, call:health_monitor.start, call:task.done, call:health_monitor.stop, call:suppress, call:task.cancelled | dep: asyncio, uuid, contextlib, unittest.mock, pytest, sqlalchemy, src.models.system.health_check, src.models.tool.tool_instance, src.models.user.user, src.services.instance.event_bus, src.services.instance.health_monitor - test_health_monitor.py | Unit tests for HealthMonitor state-transition logic covering container crash detection, tunnel failure detection, recovery detection, write deduplication, exception resilience, and start/stop lifecycle. | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:health_monitor(event_bus: InstanceEventBus) → HealthMonitor, call:HealthMonitor, func:_create_running_instance(db_session) → ToolInstance, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, func:test_detects_container_crash(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_tunnel_failure(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_recovery(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:db_session.commit, call:HealthSnapshot, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_skips_writes_when_no_state_change(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:HealthSnapshot, call:patch, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:len, call:result.scalars().all, func:test_docker_exception_resilience(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:RuntimeError, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one_or_none, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_monitor_start_stop(health_monitor: HealthMonitor) → None, call:health_monitor.start, call:task.done, call:health_monitor.stop, call:suppress, call:task.cancelled | dep: asyncio, uuid, contextlib, unittest.mock, pytest, sqlalchemy, src.models.system.health_check, src.models.tool.tool_instance, src.models.user.user, src.services.instance.event_bus, src.services.instance.health_monitor
- test_home_path_expansion.py | Unit tests for home directory path expansion (~ and $HOME) in container paths and manifest home directory resolution. | exp: class:TestExpandContainerPath, method:test_tilde_slash_expands(self) → None, call:expand_container_path, method:test_tilde_alone_expands(self) → None, call:expand_container_path, method:test_dollar_home_slash_expands(self) → None, call:expand_container_path, method:test_dollar_home_alone_expands(self) → None, call:expand_container_path, method:test_absolute_path_unchanged(self) → None, call:expand_container_path, method:test_relative_path_unchanged(self) → None, call:expand_container_path, method:test_tilde_in_middle_unchanged(self) → None, call:expand_container_path, method:test_dollar_home_in_middle_unchanged(self) → None, call:expand_container_path, method:test_root_home(self) → None, call:expand_container_path, class:TestGetManifestHomeDir, method:test_with_user_block(self) → None, call:get_manifest_home_dir, method:test_without_user_block(self) → None, call:get_manifest_home_dir, method:test_with_empty_user_name(self) → None, call:get_manifest_home_dir, method:test_with_none_user_name(self) → None, call:get_manifest_home_dir | dep: pytest, src.services.config.config_profile_resolver, src.services.build.manifest_compiler - test_home_path_expansion.py | Unit tests for home directory path expansion (~ and $HOME) in container paths and manifest home directory resolution. | exp: class:TestExpandContainerPath, method:test_tilde_slash_expands(self) → None, call:expand_container_path, method:test_tilde_alone_expands(self) → None, call:expand_container_path, method:test_dollar_home_slash_expands(self) → None, call:expand_container_path, method:test_dollar_home_alone_expands(self) → None, call:expand_container_path, method:test_absolute_path_unchanged(self) → None, call:expand_container_path, method:test_relative_path_unchanged(self) → None, call:expand_container_path, method:test_tilde_in_middle_unchanged(self) → None, call:expand_container_path, method:test_dollar_home_in_middle_unchanged(self) → None, call:expand_container_path, method:test_root_home(self) → None, call:expand_container_path, class:TestGetManifestHomeDir, method:test_with_user_block(self) → None, call:get_manifest_home_dir, method:test_without_user_block(self) → None, call:get_manifest_home_dir, method:test_with_empty_user_name(self) → None, call:get_manifest_home_dir, method:test_with_none_user_name(self) → None, call:get_manifest_home_dir | dep: pytest, src.services.config.config_profile_resolver, src.services.build.manifest_compiler
- test_instance_service.py | Unit tests for home directory expansion in docker-compose file modifications. | exp: class:TestModifyComposeFile, method:test_extra_volumes_expand_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text, method:test_working_directory_expands_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text | dep: pytest, src.services.tool.instance_service
- test_lifecycle_hooks.py | Unit tests for lifecycle hook helper functions that derive notification titles and determine whether events should trigger notifications. | exp: class:TestDeriveTitle, method:test_known_event_types(self) → None, call:_derive_title, method:test_unknown_event_type(self) → None, call:_derive_title, class:TestShouldNotify, method:test_error_events_are_notified(self) → None, call:_should_notify, method:test_health_changed_running_is_notified(self) → None, call:_should_notify, method:test_created_started_stopped_restarted_deleted_filtered(self) → None, call:_should_notify, method:test_health_changed_non_running_filtered(self) → None, call:_should_notify | dep: pytest, src.services.instance.lifecycle_hooks - test_lifecycle_hooks.py | Unit tests for lifecycle hook helper functions that derive notification titles and determine whether events should trigger notifications. | exp: class:TestDeriveTitle, method:test_known_event_types(self) → None, call:_derive_title, method:test_unknown_event_type(self) → None, call:_derive_title, class:TestShouldNotify, method:test_error_events_are_notified(self) → None, call:_should_notify, method:test_health_changed_running_is_notified(self) → None, call:_should_notify, method:test_created_started_stopped_restarted_deleted_filtered(self) → None, call:_should_notify, method:test_health_changed_non_running_filtered(self) → None, call:_should_notify | dep: pytest, src.services.instance.lifecycle_hooks
- test_manifest_compiler.py | Unit tests for a manifest compiler that generates Dockerfiles with user configuration and home directory setup | exp: func:test_compile_dockerfile_creates_config_dirs_for_user() → None, call:compile_dockerfile, func:test_compile_dockerfile_no_user_does_not_create_home() → None, call:compile_dockerfile | dep: pytest, src.services.build.manifest_compiler - test_manifest_compiler.py | Unit tests for a manifest compiler that generates Docker configurations (Dockerfile, compose, entrypoint) based on manifest specifications. | exp: class:TestGetManifestHomeDir, method:test_home_directory_in_manifest_wins(self) → None, call:get_manifest_home_dir, method:test_user_name_derives_home(self) → None, call:get_manifest_home_dir, method:test_root_fallback(self) → None, call:get_manifest_home_dir, method:test_empty_home_directory_falls_back(self) → None, call:get_manifest_home_dir, class:TestCompileDockerfileHomeDirectory, method:test_env_home_and_workdir_use_home_directory(self) → None, call:compile_dockerfile, method:test_workspace_symlink_created(self) → None, call:compile_dockerfile, method:test_runtime_working_dir_overrides_home_workdir(self) → None, call:compile_dockerfile, method:test_working_dir_expands_tilde(self) → None, call:compile_dockerfile, class:TestCompileComposeHomeDirectory, method:test_default_repo_mount_synthesized(self) → None, call:compile_compose, method:test_explicit_repo_mount_preserved(self) → None, call:compile_compose, method:test_working_dir_expands_home(self) → None, call:compile_compose, class:TestCompileEntrypoint, method:test_entrypoint_creates_home_and_workspace(self) → None, call:compile_entrypoint, method:test_entrypoint_fixes_mount_owners(self) → None, call:compile_entrypoint, func:test_compile_dockerfile_creates_config_dirs_for_user() → None, call:compile_dockerfile, func:test_compile_dockerfile_no_user_does_not_create_home() → None, call:compile_dockerfile | dep: pytest, src.services.build.manifest_compiler
- test_migration_metadata.py | Tests Alembic database migration files for correct table definitions and revision chain metadata | exp: func:test_initial_migration_defines_all_core_tables() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module, func:test_refresh_tokens_migration_has_expected_revision_chain() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module | dep: pytest, importlib.util, pathlib, pathlib.Path - test_migration_metadata.py | Tests Alembic database migration files for correct table definitions and revision chain metadata | exp: func:test_initial_migration_defines_all_core_tables() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module, func:test_refresh_tokens_migration_has_expected_revision_chain() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module | dep: pytest, importlib.util, pathlib, pathlib.Path
- test_monitoring_models.py | Unit tests verifying creation, persistence, and querying of monitoring models (InstanceEvent and HealthCheck) with database migration compatibility. | exp: func:test_instance_event_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.refresh, call:isinstance, func:test_health_check_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:HealthCheck, call:db_session.refresh, call:isinstance, func:test_instance_event_query_by_instance(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.execute, call:select(InstanceEvent).where, call:result.scalar_one | dep: uuid, datetime, pytest, sqlalchemy, src.models.system.health_check, src.models.system.instance_event, src.models.tool.tool_instance, src.models.user.user - test_monitoring_models.py | Unit tests verifying creation, persistence, and querying of monitoring models (InstanceEvent and HealthCheck) with database migration compatibility. | exp: func:test_instance_event_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.refresh, call:isinstance, func:test_health_check_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:HealthCheck, call:db_session.refresh, call:isinstance, func:test_instance_event_query_by_instance(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.execute, call:select(InstanceEvent).where, call:result.scalar_one | dep: uuid, datetime, pytest, sqlalchemy, src.models.system.health_check, src.models.system.instance_event, src.models.tool.tool_instance, src.models.user.user
- test_notification_service.py | Unit tests for NotificationService covering CRUD operations, filtering, sorting, and ownership isolation. | exp: func:notification_service() → NotificationService, call:NotificationService, func:user_a(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:user_b(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:test_create_notification(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:uuid.uuid4, func:test_list_notifications_orders_by_created_at_desc(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:datetime.now, call:timedelta, call:db_session.commit, call:db_session.refresh, call:notification_service.list_notifications, func:test_list_notifications_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.list_notifications, func:test_list_notifications_unread_only(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.list_notifications, func:test_get_unread_count(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.get_unread_count, func:test_mark_read_sets_read_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, func:test_mark_all_read_affects_all_unread(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count, func:test_dismiss_sets_dismissed_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:db_session.execute, call:select(Notification).where, call:result.scalar_one, func:test_mark_read_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.mark_read, func:test_dismiss_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.dismiss, func:test_list_notifications_mute_categories(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.list_notifications, func:test_get_unread_count_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.get_unread_count, func:test_dismiss_all_affects_all_non_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_dismiss_all_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_mark_all_read_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count | dep: uuid, datetime, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.system.notification, src.models.user.user, src.services.shared.notification_service, NotificationService, Notification, User, AsyncSession - test_notification_service.py | Unit tests for NotificationService covering CRUD operations, filtering, sorting, and ownership isolation. | exp: func:notification_service() → NotificationService, call:NotificationService, func:user_a(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:user_b(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:test_create_notification(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:uuid.uuid4, func:test_list_notifications_orders_by_created_at_desc(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:datetime.now, call:timedelta, call:db_session.commit, call:db_session.refresh, call:notification_service.list_notifications, func:test_list_notifications_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.list_notifications, func:test_list_notifications_unread_only(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.list_notifications, func:test_get_unread_count(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.get_unread_count, func:test_mark_read_sets_read_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, func:test_mark_all_read_affects_all_unread(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count, func:test_dismiss_sets_dismissed_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:db_session.execute, call:select(Notification).where, call:result.scalar_one, func:test_mark_read_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.mark_read, func:test_dismiss_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.dismiss, func:test_list_notifications_mute_categories(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.list_notifications, func:test_get_unread_count_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.get_unread_count, func:test_dismiss_all_affects_all_non_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_dismiss_all_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_mark_all_read_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count | dep: uuid, datetime, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.system.notification, src.models.user.user, src.services.shared.notification_service, NotificationService, Notification, User, AsyncSession
@@ -28,9 +30,9 @@ Contains comprehensive unit tests for the API backend services covering configur
- 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 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. pytest-based unit testing with heavy mocking of external dependencies (Docker, Git, subprocess, database) to test components in isolation without live services.
## tags ## tags
test, url, call:notification, git, call:, call:db, merge, src test, url, call:notification, git, home, call:, merge, call:db
## symbols ## symbols
- TestMergeFunctions - TestMergeFunctions
- TestResolveProfile - TestResolveProfile
@@ -42,7 +44,7 @@ test, url, call:notification, git, call:, call:db, merge, src
- TestSortVolumesBySpecificity - TestSortVolumesBySpecificity
## workflows ## workflows
- change unit behavior - change unit behavior
read: __init__.py, test_config.py, test_config_profile_resolver.py read: __init__.py, test_alembic_migrations.py, test_config.py
- change unit config - change unit config
read: test_config.py, test_config_profile_resolver.py read: test_config.py, test_config_profile_resolver.py
## dirty ## dirty
@@ -0,0 +1,49 @@
"""Unit tests for Alembic migration structure/import.
Actual upgrade/downgrade round-trips require a PostgreSQL database, so these
tests verify that migrations are importable, have the expected identifiers,
and declare the expected dependencies.
"""
import importlib.util
from pathlib import Path
import pytest
@pytest.mark.unit
def test_home_directory_migration_imports_and_rewrites() -> None:
migration_path = Path(__file__).parent.parent.parent / (
"alembic/versions/2026_06_14_104415_add_tool_type_home_directory.py"
)
assert migration_path.exists()
spec = importlib.util.spec_from_file_location("home_dir_migration", migration_path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
assert module.revision == "2026_06_14_104415"
assert module.down_revision == "f3d2dc90ba3a"
assert callable(module.upgrade)
assert callable(module.downgrade)
assert module.OLD_WORKSPACE == "/workspace"
assert module.NEW_WORKSPACE == "/home/user/{{WORKSPACE_NAME}}"
@pytest.mark.unit
def test_merge_migration_resolves_heads() -> None:
migration_path = Path(__file__).parent.parent.parent / (
"alembic/versions/fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py"
)
assert migration_path.exists()
spec = importlib.util.spec_from_file_location("merge_migration", migration_path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
assert module.revision == "fc8f1a20cbf6"
assert "2026_06_14_104415" in module.down_revision
assert "8c6d1dbd4798" in module.down_revision
assert callable(module.upgrade)
@@ -0,0 +1,44 @@
"""Unit tests for the tool instance service."""
import pytest
from src.services.tool.instance_service import modify_compose_file
@pytest.mark.unit
class TestModifyComposeFile:
"""Tests for modify_compose_file home-directory expansion."""
def test_extra_volumes_expand_home_dir(self, tmp_path):
compose_path = tmp_path / "docker-compose.yml"
compose_path.write_text(
'services:\n app:\n image: test:latest\n volumes: []\n'
)
modify_compose_file(
str(compose_path),
extra_volumes=[
{"source": "/host/config", "target": "~/.config", "type": "bind"},
{"source": "/host/code", "target": "$HOME/code", "type": "bind"},
],
home_dir="/home/user",
)
content = compose_path.read_text()
assert "/host/config:/home/user/.config" in content
assert "/host/code:/home/user/code" in content
def test_working_directory_expands_home_dir(self, tmp_path):
compose_path = tmp_path / "docker-compose.yml"
compose_path.write_text(
'services:\n app:\n image: test:latest\n'
)
modify_compose_file(
str(compose_path),
working_directory="~/workspace",
home_dir="/home/user",
)
content = compose_path.read_text()
assert "working_dir: /home/user/workspace" in content
+188 -1
View File
@@ -2,7 +2,12 @@
import pytest import pytest
from src.services.build.manifest_compiler import compile_dockerfile from src.services.build.manifest_compiler import (
compile_compose,
compile_dockerfile,
compile_entrypoint,
get_manifest_home_dir,
)
@pytest.mark.unit @pytest.mark.unit
@@ -36,3 +41,185 @@ def test_compile_dockerfile_no_user_does_not_create_home() -> None:
assert "useradd" not in dockerfile assert "useradd" not in dockerfile
assert "/home/" not in dockerfile assert "/home/" not in dockerfile
@pytest.mark.unit
class TestGetManifestHomeDir:
"""Tests for get_manifest_home_dir precedence."""
def test_home_directory_in_manifest_wins(self) -> None:
manifest = {
"home_directory": "/home/custom",
"user": {"name": "dev"},
}
assert get_manifest_home_dir(manifest) == "/home/custom"
def test_user_name_derives_home(self) -> None:
manifest = {"user": {"name": "dev", "uid": 1000, "gid": 1000}}
assert get_manifest_home_dir(manifest) == "/home/dev"
def test_root_fallback(self) -> None:
manifest = {"base_image": "ubuntu:24.04"}
assert get_manifest_home_dir(manifest) == "/root"
def test_empty_home_directory_falls_back(self) -> None:
manifest = {"home_directory": "", "user": {"name": "dev"}}
assert get_manifest_home_dir(manifest) == "/home/dev"
@pytest.mark.unit
class TestCompileDockerfileHomeDirectory:
"""Tests that compile_dockerfile honors manifest.home_directory."""
def test_env_home_and_workdir_use_home_directory(self) -> None:
manifest = {
"base_image": "ubuntu:24.04",
"interface_type": "terminal",
"home_directory": "/home/custom",
"user": {"name": "dev", "uid": 1000, "gid": 1000},
}
dockerfile = compile_dockerfile(manifest)
assert "ENV HOME=/home/custom" in dockerfile
assert "WORKDIR /home/custom" in dockerfile
def test_workspace_symlink_created(self) -> None:
manifest = {
"base_image": "ubuntu:24.04",
"interface_type": "terminal",
"home_directory": "/home/custom",
"user": {"name": "dev", "uid": 1000, "gid": 1000},
}
dockerfile = compile_dockerfile(manifest)
assert "ln -sfn /home/custom/{{WORKSPACE_NAME}} /workspace" in dockerfile
def test_runtime_working_dir_overrides_home_workdir(self) -> None:
manifest = {
"base_image": "ubuntu:24.04",
"interface_type": "terminal",
"home_directory": "/home/custom",
"user": {"name": "dev", "uid": 1000, "gid": 1000},
"runtime": {"working_dir": "/app/code"},
}
dockerfile = compile_dockerfile(manifest)
assert "WORKDIR /app/code" in dockerfile
assert "WORKDIR /home/custom" not in dockerfile
def test_working_dir_expands_tilde(self) -> None:
manifest = {
"base_image": "ubuntu:24.04",
"interface_type": "terminal",
"home_directory": "/home/custom",
"user": {"name": "dev", "uid": 1000, "gid": 1000},
"runtime": {"working_dir": "~/code"},
}
dockerfile = compile_dockerfile(manifest)
assert "WORKDIR /home/custom/code" in dockerfile
@pytest.mark.unit
class TestCompileComposeHomeDirectory:
"""Tests that compile_compose uses home_directory for volumes and working_dir."""
def test_default_repo_mount_synthesized(self) -> None:
manifest = {
"base_image": "ubuntu:24.04",
"interface_type": "terminal",
"home_directory": "/home/custom",
"user": {"name": "dev", "uid": 1000, "gid": 1000},
}
variables = {
"IMAGE_TAG": "test:latest",
"INSTANCE_NAME": "test-instance",
"REPO_PATH": "/host/repos/my-app",
"WORKSPACE_NAME": "my-app",
"TOOL_PORT": 0,
"EXTRA_ENV": {},
"EXTRA_VOLUMES": [],
}
compose = compile_compose(manifest, variables)
assert "/host/repos/my-app:/home/custom/my-app" in compose
def test_explicit_repo_mount_preserved(self) -> None:
manifest = {
"base_image": "ubuntu:24.04",
"interface_type": "terminal",
"home_directory": "/home/custom",
"user": {"name": "dev", "uid": 1000, "gid": 1000},
"mounts": [
{"source_type": "repo", "target": "/opt/code", "readonly": True}
],
}
variables = {
"IMAGE_TAG": "test:latest",
"INSTANCE_NAME": "test-instance",
"REPO_PATH": "/host/repos/my-app",
"WORKSPACE_NAME": "my-app",
"TOOL_PORT": 0,
"EXTRA_ENV": {},
"EXTRA_VOLUMES": [],
}
compose = compile_compose(manifest, variables)
assert "/host/repos/my-app:/opt/code:ro" in compose
assert "/home/custom/my-app" not in compose
def test_working_dir_expands_home(self) -> None:
manifest = {
"base_image": "ubuntu:24.04",
"interface_type": "terminal",
"home_directory": "/home/custom",
"user": {"name": "dev", "uid": 1000, "gid": 1000},
"runtime": {"working_dir": "$HOME/code"},
}
variables = {
"IMAGE_TAG": "test:latest",
"INSTANCE_NAME": "test-instance",
"REPO_PATH": "/host/repos/my-app",
"WORKSPACE_NAME": "my-app",
"TOOL_PORT": 0,
"EXTRA_ENV": {},
"EXTRA_VOLUMES": [],
}
compose = compile_compose(manifest, variables)
assert "working_dir: /home/custom/code" in compose
@pytest.mark.unit
class TestCompileEntrypoint:
"""Tests for the generated permission-fixing entrypoint."""
def test_entrypoint_creates_home_and_workspace(self) -> None:
manifest = {
"base_image": "ubuntu:24.04",
"interface_type": "terminal",
"home_directory": "/home/custom",
"user": {"name": "dev", "uid": 1000, "gid": 1000},
}
entrypoint = compile_entrypoint(manifest)
assert 'mkdir -p "$HOME_DIR"' in entrypoint
assert 'mkdir -p "$WORKSPACE_TARGET"' in entrypoint
assert 'ln -sfn "$WORKSPACE_TARGET" /workspace' in entrypoint
def test_entrypoint_fixes_mount_owners(self) -> None:
manifest = {
"base_image": "ubuntu:24.04",
"interface_type": "terminal",
"home_directory": "/home/custom",
"user": {"name": "dev", "uid": 1000, "gid": 1000},
"mounts": [
{"target": "~/.config", "readonly": False},
{"target": "/opt/readonly", "readonly": True},
],
}
entrypoint = compile_entrypoint(manifest)
assert 'fix_owner "/home/custom/.config"' in entrypoint
assert 'fix_owner "/opt/readonly"' not in entrypoint
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web dir: apps/web
## role ## role
Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project. Frontend web application providing a browser-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
## parent ## parent
index: apps/.pi-map.index.md index: apps/.pi-map.index.md
map: apps/.pi-map.md map: apps/.pi-map.md
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/web
index: apps/web/.pi-map.index.md index: apps/web/.pi-map.index.md
## role ## role
Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project. Frontend web application providing a browser-based UI with code editing, terminal, and routing capabilities for the "headquarter" project.
## files ## files
- .env.example | Template file defining example environment variables for frontend API and application URL configuration - .env.example | Template file defining example environment variables for frontend API and application URL configuration
- .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ECMAScript module support | dep: @typescript-eslint/parser, @typescript-eslint/eslint-plugin, eslint - .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ECMAScript module support | dep: @typescript-eslint/parser, @typescript-eslint/eslint-plugin, eslint
@@ -16,7 +16,7 @@ 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, using nginx for production serving with client-side routing, multi-stage Docker deployment, and Vitest for testing. Modern React SPA built with Vite and TypeScript, containerized via multi-stage Docker with nginx serving, featuring client-side routing, optimized static asset delivery, and development tooling (ESLint, Vitest).
## tags ## tags
react, eslint, vite, typescript, dom, application, nginx, web react, eslint, vite, typescript, dom, application, nginx, web
## symbols ## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src dir: apps/web/src
## role ## role
Frontend web application entry point and core infrastructure for a React-based collaborative development platform. Provides the core web application entry point, routing infrastructure, and shared domain type definitions for a React-based frontend.
## 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
+2 -2
View File
@@ -4,13 +4,13 @@ dir: apps/web/src
index: apps/web/src/.pi-map.index.md index: apps/web/src/.pi-map.index.md
## role ## role
Frontend web application entry point and core infrastructure for a React-based collaborative development platform. Provides the core web application entry point, routing infrastructure, and shared domain type definitions for a React-based frontend.
## 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
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. Modular React SPA with provider-based dependency injection, declarative routing with nested layouts and route guards, and centralized TypeScript type definitions.
## tags ## tags
pages, styles, css, router, react, session, dom, project pages, styles, css, router, react, session, dom, project
## symbols ## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src/components dir: apps/web/src/components
## role ## role
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. Provides reusable, accessible UI primitives and layout components for a React web application, including shell layout, data states, icons, code editing, routing guards, and toast notifications.
## 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
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/web/src/components
index: apps/web/src/components/.pi-map.index.md index: apps/web/src/components/.pi-map.index.md
## role ## role
Provides reusable, accessible UI components and utilities for rendering the application shell, data states, icons, code display, notifications, and route protection in a React web application. Provides reusable, accessible UI primitives and layout components for a React web application, including shell layout, data states, icons, code editing, routing guards, and toast notifications.
## files ## files
- 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 - 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
@@ -17,7 +17,7 @@ Provides reusable, accessible UI components and utilities for rendering the appl
- 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 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. Component-based React architecture with functional components, composition patterns, and separation of concerns between presentation (UI rendering), logic (rules/hooks), and testing; integrates third-party libraries (react-simple-code-editor, Phosphor icons) and React Router for SPA navigation.
## tags ## tags
toast, icon, react, state, code, loading, event, editor toast, icon, react, state, code, loading, event, editor
## symbols ## symbols
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features dir: apps/web/src/components/features
## role ## role
Contains reusable React components that implement specific user-facing features and functionality across the web application. Contains reusable React components that implement specific product features and user-facing 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
@@ -19,6 +19,9 @@ map: apps/web/src/components/.pi-map.md
- apps/web/src/components/features/notification - apps/web/src/components/features/notification
index: apps/web/src/components/features/notification/.pi-map.index.md index: apps/web/src/components/features/notification/.pi-map.index.md
map: apps/web/src/components/features/notification/.pi-map.md map: apps/web/src/components/features/notification/.pi-map.md
- apps/web/src/components/features/profile
index: apps/web/src/components/features/profile/.pi-map.index.md
map: apps/web/src/components/features/profile/.pi-map.md
- apps/web/src/components/features/project - apps/web/src/components/features/project
index: apps/web/src/components/features/project/.pi-map.index.md index: apps/web/src/components/features/project/.pi-map.index.md
map: apps/web/src/components/features/project/.pi-map.md map: apps/web/src/components/features/project/.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
index: apps/web/src/components/features/.pi-map.index.md index: apps/web/src/components/features/.pi-map.index.md
## role ## role
Contains reusable React components that implement specific user-facing features and functionality across the web application. Contains reusable React components that implement specific product features and user-facing functionality across the web application.
## files ## files
## arch ## arch
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. Feature-based component organization with domain-specific groupings, likely combining presentational and container patterns with hooks for state management, following a modular architecture where each feature encapsulates its own UI, logic, and data fetching concerns.
## tags ## tags
- -
## symbols ## symbols
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features/config-profiles dir: apps/web/src/components/features/config-profiles
## role ## role
Provides a complete UI subsystem for managing configuration profiles with desktop and mobile views, supporting CRUD operations, profile relationships, and advanced configuration options. Provides UI components for managing Docker configuration profiles including creation, editing, listing, and mobile-responsive views.
## 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,15 +4,15 @@ dir: apps/web/src/components/features/config-profiles
index: apps/web/src/components/features/config-profiles/.pi-map.index.md index: apps/web/src/components/features/config-profiles/.pi-map.index.md
## role ## role
Provides a complete UI subsystem for managing configuration profiles with desktop and mobile views, supporting CRUD operations, profile relationships, and advanced configuration options. Provides UI components for managing Docker configuration profiles including creation, editing, listing, and mobile-responsive views.
## files ## files
- ConfigProfileEditorPanel.tsx | React component that renders a form-based editor panel for creating and editing configuration profiles with support for includes, environment variables, runtime hints, files, and mounts. | exp: ConfigProfileEditorPanel | dep: ../../icon, ../git/git-mount-editor, ../../../api/config-profiles, ../../../types, ../../../api/tool-types, React, Icon, GitMountEditor, ConfigProfile, CreateConfigProfileRequest, ResolvedProfile, ProjectWithRepos, ToolType - ConfigProfileEditorPanel.tsx | React component that renders a form-based editor panel for creating and editing configuration profiles with support for includes, environment variables, runtime hints, files, and mounts. | exp: ConfigProfileEditorPanel | dep: ../../icon, ../git/git-mount-editor, ../../../api/config-profiles, ../../../types, ../../../api/tool-types, React, Icon, GitMountEditor, ConfigProfile, CreateConfigProfileRequest, ResolvedProfile, ProjectWithRepos, ToolType
- ConfigProfileListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting configuration profiles with visual indicators for default status, scope, and includes. | exp: ConfigProfileListSidebar | dep: ../../icon, ../../../api/config-profiles, Icon, ConfigProfile - ConfigProfileListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting configuration profiles with visual indicators for default status, scope, and includes. | exp: ConfigProfileListSidebar | dep: ../../icon, ../../../api/config-profiles, Icon, ConfigProfile
- ConfigProfilesMobileView.tsx | Renders a mobile-responsive CRUD interface for managing configuration profiles with list, detail, and edit views. | exp: ConfigProfilesMobileView | dep: ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../../icon, ../../../api/config-profiles, MobileListView, MobileDetailView, MobileEditView, MobileFAB, Icon, ConfigProfile, CreateConfigProfileRequest - ConfigProfilesMobileView.tsx | Renders a mobile-responsive view for managing configuration profiles with list, detail, edit, and preview states. | exp: ConfigProfilesMobileView | dep: react, ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../git/git-mount-editor, ../../icon, ../../../api/config-profiles, ../../../types, ../../../api/tool-types, mobile-list-view, mobile-detail-view, mobile-edit-view, mobile-fab, git-mount-editor, icon, config-profiles API types, types
## arch ## arch
Compound component architecture with panel/sidebar split for desktop, dedicated mobile view with state-driven routing, shared state management across list/editor views, and form-based configuration with nested array/object handling for includes, env vars, files, and mounts. Feature-based component composition with state-driven panels (list/detail/edit/preview), form-based editing, and responsive layout adaptation for mobile/desktop.
## tags ## tags
config, mobile, profiles, profile, view, editor, icon, list mobile, config, view, profiles, editor, profile, list, icon
## symbols ## symbols
- ConfigProfileEditorPanel - ConfigProfileEditorPanel
- ConfigProfileListSidebar - ConfigProfileListSidebar
@@ -1,35 +1,86 @@
import { useState } from "react";
import { MobileListView } from "../mobile/mobile-list-view"; import { MobileListView } from "../mobile/mobile-list-view";
import { MobileDetailView } from "../mobile/mobile-detail-view"; import { MobileDetailView } from "../mobile/mobile-detail-view";
import { MobileEditView } from "../mobile/mobile-edit-view"; import { MobileEditView } from "../mobile/mobile-edit-view";
import { MobileFAB } from "../mobile/mobile-fab"; import { MobileFAB } from "../mobile/mobile-fab";
import { GitMountEditor } from "../git/git-mount-editor";
import { Icon } from "../../icon"; import { Icon } from "../../icon";
import type { ConfigProfile, CreateConfigProfileRequest } from "../../../api/config-profiles"; import type {
ConfigProfile,
CreateConfigProfileRequest,
ResolvedProfile,
} from "../../../api/config-profiles";
import type { ProjectWithRepos } from "../../../types";
import type { ToolType } from "../../../api/tool-types";
type MobileView = "list" | "detail" | "edit"; type MobileView = "list" | "detail" | "edit" | "preview";
interface Props { interface Props {
profiles: ConfigProfile[]; profiles: ConfigProfile[];
projects: ProjectWithRepos[];
toolTypes: ToolType[];
selectedProfile: ConfigProfile | null; selectedProfile: ConfigProfile | null;
mobileView: MobileView; mobileView: MobileView;
isCreating: boolean; isCreating: boolean;
formData: CreateConfigProfileRequest; formData: CreateConfigProfileRequest;
includedProfileIds: string[];
availableProfiles: ConfigProfile[];
previewData: ResolvedProfile | null;
previewingId: string | null;
saveStatus: "idle" | "saving" | "saved" | "error"; saveStatus: "idle" | "saving" | "saved" | "error";
error: string | null;
onViewChange: (view: MobileView) => void; onViewChange: (view: MobileView) => void;
onSelect: (profile: ConfigProfile) => void; onSelect: (profile: ConfigProfile) => void;
onCreate: () => void; onCreate: () => void;
onDelete: (id: string) => void; onDelete: (id: string) => void;
onFormChange: <K extends keyof CreateConfigProfileRequest>(key: K, value: CreateConfigProfileRequest[K]) => void; onFormChange: <K extends keyof CreateConfigProfileRequest>(
onSubmit: () => void; key: K,
value: CreateConfigProfileRequest[K],
) => void;
onSubmit: (e?: React.FormEvent) => Promise<boolean>;
getScopeLabel: (profile: ConfigProfile) => string; getScopeLabel: (profile: ConfigProfile) => string;
getIncludedProfile: (id: string) => ConfigProfile | undefined;
onAddInclude: (id: string) => void;
onRemoveInclude: (index: number) => void;
onAddEnvVar: () => void;
onUpdateEnvVar: (oldKey: string, newKey: string, value: string) => void;
onRemoveEnvVar: (key: string) => void;
onAddFile: () => void;
onUpdateFile: (oldPath: string, newPath: string, content: string) => void;
onRemoveFile: (path: string) => void;
onAddMount: () => void;
onUpdateMount: (
index: number,
updates: Partial<ConfigProfile["mounts"][0]>,
) => void;
onRemoveMount: (index: number) => void;
onAddMountFile: (mountIndex: number) => void;
onUpdateMountFile: (
mountIndex: number,
oldPath: string,
newPath: string,
content: string,
) => void;
onRemoveMountFile: (mountIndex: number, path: string) => void;
onPreview: (id: string) => void;
onClosePreview: () => void;
} }
export const ConfigProfilesMobileView = ({ export const ConfigProfilesMobileView = ({
profiles, profiles,
projects,
toolTypes,
selectedProfile, selectedProfile,
mobileView, mobileView,
isCreating, isCreating,
formData, formData,
includedProfileIds,
availableProfiles,
previewData,
previewingId,
saveStatus, saveStatus,
error,
onViewChange, onViewChange,
onSelect, onSelect,
onCreate, onCreate,
@@ -37,12 +88,57 @@ export const ConfigProfilesMobileView = ({
onFormChange, onFormChange,
onSubmit, onSubmit,
getScopeLabel, getScopeLabel,
getIncludedProfile,
onAddInclude,
onRemoveInclude,
onAddEnvVar,
onUpdateEnvVar,
onRemoveEnvVar,
onAddFile,
onUpdateFile,
onRemoveFile,
onAddMount,
onUpdateMount,
onRemoveMount,
onAddMountFile,
onUpdateMountFile,
onRemoveMountFile,
onPreview,
onClosePreview,
}: Props) => { }: Props) => {
const [isSaving, setIsSaving] = useState(false);
const handleSave = async () => {
setIsSaving(true);
try {
const ok = await onSubmit();
if (ok) {
onViewChange(isCreating ? "list" : "edit");
}
} finally {
setIsSaving(false);
}
};
const handleDeleteClick = () => {
if (selectedProfile) {
onDelete(selectedProfile.id);
onViewChange("list");
}
};
const projectName = (id?: string) =>
projects.find((p) => p.id === id)?.name || id || "None";
const toolTypeName = (id?: string) =>
toolTypes.find((t) => t.id === id)?.display_name || id || "None";
if (mobileView === "list") { if (mobileView === "list") {
return ( return (
<div className="mobile-page"> <div className="mobile-page">
<div className="mobile-page-header"> <div className="mobile-page-header">
<h1>Config Profiles</h1> <h1>Config Profiles</h1>
<span className="muted">{profiles.length} profiles</span>
</div> </div>
<MobileListView <MobileListView
items={profiles.map((profile) => ({ items={profiles.map((profile) => ({
@@ -54,7 +150,7 @@ export const ConfigProfilesMobileView = ({
const profile = profiles.find((p) => p.id === id); const profile = profiles.find((p) => p.id === id);
if (profile) { if (profile) {
onSelect(profile); onSelect(profile);
onViewChange("detail"); onViewChange("edit");
} }
}} }}
onItemDelete={(id: string) => onDelete(id)} onItemDelete={(id: string) => onDelete(id)}
@@ -70,12 +166,146 @@ export const ConfigProfilesMobileView = ({
); );
} }
if (mobileView === "preview" && selectedProfile && previewData) {
return (
<div className="mobile-edit-view">
<header className="mobile-edit-header">
<button
className="mobile-edit-cancel"
onClick={() => onViewChange("detail")}
type="button"
>
Back
</button>
<h1 className="mobile-edit-title">Resolved Preview</h1>
<div style={{ width: "4rem" }} />
</header>
<div className="mobile-edit-form">
<div className="mobile-form-group">
<label className="mobile-form-label">Profile</label>
<p>{previewData.profile_name}</p>
</div>
{previewData.included_profiles.length > 0 && (
<div className="mobile-form-group">
<label className="mobile-form-label">Included Profiles</label>
<ul className="mobile-list-plain">
{previewData.included_profiles.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
)}
{Object.keys(previewData.env_vars).length > 0 && (
<div className="mobile-form-group">
<label className="mobile-form-label">Environment Variables</label>
<pre className="mobile-preview-code">
{JSON.stringify(previewData.env_vars, null, 2)}
</pre>
</div>
)}
{Object.keys(previewData.runtime_hints).length > 0 && (
<div className="mobile-form-group">
<label className="mobile-form-label">Runtime Hints</label>
<pre className="mobile-preview-code">
{JSON.stringify(previewData.runtime_hints, null, 2)}
</pre>
</div>
)}
{previewData.mounts.length > 0 && (
<div className="mobile-form-group">
<label className="mobile-form-label">Mounts</label>
{previewData.mounts.map((m, idx) => (
<div key={idx} className="mobile-preview-item">
<strong>
{m.target} ({m.mode})
</strong>
{Object.keys(m.files).length > 0 && (
<pre className="mobile-preview-code">
{JSON.stringify(m.files, null, 2)}
</pre>
)}
</div>
))}
</div>
)}
{previewData.git_mounts.length > 0 && (
<div className="mobile-form-group">
<label className="mobile-form-label">Git Mounts</label>
{previewData.git_mounts.map((m, idx) => (
<div key={idx} className="mobile-preview-item">
<strong>
{m.remote_url}
{m.branch ? ` @${m.branch}` : ""}
</strong>
{m.mappings.map((mapping, mi) => (
<div
key={mi}
className="mobile-preview-mapping"
>
{mapping.source_path || "."} {" "}
{mapping.target_path}
</div>
))}
</div>
))}
</div>
)}
{Object.keys(previewData.files).length > 0 && (
<div className="mobile-form-group">
<label className="mobile-form-label">Files</label>
<pre className="mobile-preview-code">
{JSON.stringify(previewData.files, null, 2)}
</pre>
</div>
)}
{Object.keys(previewData.overrides.env_vars).length > 0 && (
<div className="mobile-form-group">
<label className="mobile-form-label">Overrides</label>
<pre className="mobile-preview-code">
{JSON.stringify(previewData.overrides, null, 2)}
</pre>
</div>
)}
</div>
</div>
);
}
if (mobileView === "detail" && selectedProfile) { if (mobileView === "detail" && selectedProfile) {
const fields = [ const fields = [
{ label: "Name", value: selectedProfile.name }, { label: "Name", value: selectedProfile.name },
{ label: "Description", value: selectedProfile.description || "-" }, {
label: "Description",
value: selectedProfile.description || null,
},
{ label: "Scope", value: getScopeLabel(selectedProfile) }, { label: "Scope", value: getScopeLabel(selectedProfile) },
{ label: "Default", value: selectedProfile.is_default ? "Yes" : "No" }, { label: "Project", value: projectName(selectedProfile.project_id || undefined) },
{ label: "Tool Type", value: toolTypeName(selectedProfile.tool_type_id || undefined) },
{
label: "Default",
value: selectedProfile.is_default,
type: "boolean" as const,
},
{
label: "Includes",
value:
selectedProfile.includes.length > 0
? selectedProfile.includes
.map(
(inc) =>
getIncludedProfile(inc.included_profile_id)?.name ||
inc.included_profile_id,
)
.join(", ")
: null,
},
{ {
label: "Environment Variables", label: "Environment Variables",
value: value:
@@ -83,21 +313,40 @@ export const ConfigProfilesMobileView = ({
? Object.entries(selectedProfile.env_vars) ? Object.entries(selectedProfile.env_vars)
.map(([k, v]) => `${k}=${v}`) .map(([k, v]) => `${k}=${v}`)
.join(", ") .join(", ")
: "-", : null,
},
{
label: "Runtime Hints",
value:
Object.keys(selectedProfile.runtime_hints).length > 0
? JSON.stringify(selectedProfile.runtime_hints, null, 2)
: null,
type: "code" as const,
},
{
label: "Files",
value:
Object.keys(selectedProfile.files).length > 0
? Object.keys(selectedProfile.files).join(", ")
: null,
}, },
{ {
label: "Mounts", label: "Mounts",
value: value:
selectedProfile.mounts.length > 0 selectedProfile.mounts.length > 0
? selectedProfile.mounts.map((m) => `${m.target} (${m.mode})`).join(", ") ? selectedProfile.mounts
: "-", .map((m) => `${m.target} (${m.mode})`)
.join(", ")
: null,
}, },
{ {
label: "Includes", label: "Git Mounts",
value: value:
selectedProfile.includes.length > 0 selectedProfile.git_mounts.length > 0
? `${selectedProfile.includes.length} profile(s)` ? selectedProfile.git_mounts
: "-", .map((m) => m.remote_url)
.join(", ")
: null,
}, },
]; ];
@@ -111,8 +360,33 @@ export const ConfigProfilesMobileView = ({
onSelect(selectedProfile); onSelect(selectedProfile);
onViewChange("edit"); onViewChange("edit");
}} }}
onDelete={() => onDelete(selectedProfile.id)} onDelete={handleDeleteClick}
/> >
<div className="mobile-detail-actions-extra">
<button
type="button"
className="secondary-button"
disabled={previewingId === selectedProfile.id}
onClick={() => {
onClosePreview();
void onPreview(selectedProfile.id);
onViewChange("preview");
}}
>
{previewingId === selectedProfile.id ? (
<>
<Icon name="loading" size="sm" />
Previewing...
</>
) : (
<>
<Icon name="info" size="sm" />
Preview
</>
)}
</button>
</div>
</MobileDetailView>
); );
} }
@@ -121,98 +395,182 @@ export const ConfigProfilesMobileView = ({
<MobileEditView <MobileEditView
title={isCreating ? "Create Profile" : "Edit Profile"} title={isCreating ? "Create Profile" : "Edit Profile"}
onCancel={() => { onCancel={() => {
onViewChange(isCreating ? "list" : "detail"); onViewChange("list");
}} }}
onSave={onSubmit} onSave={handleSave}
isSaving={saveStatus === "saving"} onDelete={isCreating ? undefined : handleDeleteClick}
deleteLabel="Delete Profile"
isSaving={isSaving || saveStatus === "saving"}
> >
<div className="form-group"> <div className="mobile-form-group">
<label>Name *</label> <label className="mobile-form-label" htmlFor="cp-name">
Name *
</label>
<input <input
id="cp-name"
type="text" type="text"
value={formData.name} value={formData.name}
onChange={(e) => onFormChange("name", e.target.value)} onChange={(e) => onFormChange("name", e.target.value)}
placeholder="Profile name" placeholder="e.g., Development Environment"
className="mobile-form-input"
required required
/> />
</div> </div>
<div className="form-group"> <div className="mobile-form-group">
<label>Description</label> <label className="mobile-form-label" htmlFor="cp-description">
<textarea Description
</label>
<input
id="cp-description"
type="text"
value={formData.description || ""} value={formData.description || ""}
onChange={(e) => onFormChange("description", e.target.value || undefined)} onChange={(e) =>
onFormChange("description", e.target.value || undefined)
}
placeholder="Optional description" placeholder="Optional description"
rows={3} className="mobile-form-input"
/> />
</div> </div>
<div className="form-group"> <div className="mobile-form-group">
<label>Project</label> <label className="mobile-form-label" htmlFor="cp-project">
Project
</label>
<select <select
id="cp-project"
value={formData.project_id || ""} value={formData.project_id || ""}
onChange={(e) => onFormChange("project_id", e.target.value || undefined)} onChange={(e) =>
onFormChange("project_id", e.target.value || undefined)
}
className="mobile-form-select"
> >
<option value="">Global (all projects)</option> <option value="">None (Global)</option>
{projects.map((project) => (
<option key={project.id} value={project.id}>
{project.name}
</option>
))}
</select> </select>
</div> </div>
<div className="form-group"> <div className="mobile-form-group">
<label>Tool Type</label> <label className="mobile-form-label" htmlFor="cp-tool">
Tool Type
</label>
<select <select
id="cp-tool"
value={formData.tool_type_id || ""} value={formData.tool_type_id || ""}
onChange={(e) => onFormChange("tool_type_id", e.target.value || undefined)} onChange={(e) =>
onFormChange("tool_type_id", e.target.value || undefined)
}
className="mobile-form-select"
> >
<option value="">Any tool type</option> <option value="">None</option>
{toolTypes.map((toolType) => (
<option key={toolType.id} value={toolType.id}>
{toolType.display_name}
</option>
))}
</select> </select>
</div> </div>
<div className="form-group"> <div className="mobile-form-group mobile-form-row">
<label> <label className="mobile-form-label mobile-form-checkbox-label">
<input <input
type="checkbox" type="checkbox"
checked={formData.is_default || false} checked={formData.is_default || false}
onChange={(e) => onFormChange("is_default", e.target.checked)} onChange={(e) =>
onFormChange("is_default", e.target.checked)
}
className="mobile-form-checkbox"
/> />
Default Profile Set as default for this scope
</label> </label>
</div> </div>
<div className="form-group"> <div className="mobile-form-section">
<label>Environment Variables</label> <div className="mobile-form-section-header">
{Object.entries(formData.env_vars || {}).map(([key, value], index) => ( <h4>Includes</h4>
<div key={index} style={{ display: "flex", gap: "0.5rem", marginBottom: "0.5rem" }}> <span className="muted">{includedProfileIds.length} included</span>
</div>
{includedProfileIds.length === 0 ? (
<p className="muted mobile-form-help">
No profiles included. Add profiles to compose configurations.
</p>
) : (
<div className="mobile-includes-list">
{includedProfileIds.map((profileId, index) => {
const profile = getIncludedProfile(profileId);
if (!profile) return null;
return (
<div key={`${profileId}-${index}`} className="mobile-include-item">
<span className="mobile-include-name">{profile.name}</span>
<span className="mobile-include-scope">
{getScopeLabel(profile)}
</span>
<button
type="button"
className="ghost-button small"
onClick={() => onRemoveInclude(index)}
title="Remove include"
>
<Icon name="delete" size="sm" />
</button>
</div>
);
})}
</div>
)}
{availableProfiles.length > 0 && (
<select
value=""
onChange={(e) => {
if (e.target.value) {
onAddInclude(e.target.value);
e.target.value = "";
}
}}
className="mobile-form-select"
>
<option value="">+ Add Include...</option>
{availableProfiles.map((p) => (
<option key={p.id} value={p.id}>
{p.name} ({getScopeLabel(p)})
</option>
))}
</select>
)}
</div>
<div className="mobile-form-section">
<div className="mobile-form-section-header">
<h4>Environment Variables</h4>
</div>
{Object.entries(formData.env_vars || {}).map(([key, value], idx) => (
<div key={idx} className="mobile-key-value-row">
<input <input
type="text" type="text"
value={key} value={key}
onChange={(e) => { onChange={(e) =>
const newEnvVars = { ...formData.env_vars }; onUpdateEnvVar(key, e.target.value, value)
delete newEnvVars[key]; }
newEnvVars[e.target.value] = value; placeholder="VAR_NAME"
onFormChange("env_vars", newEnvVars); className="mobile-form-input"
}}
placeholder="KEY"
style={{ flex: 1 }}
/> />
<input <input
type="text" type="text"
value={value} value={value}
onChange={(e) => { onChange={(e) =>
const newEnvVars = { ...formData.env_vars }; onUpdateEnvVar(key, key, e.target.value)
newEnvVars[key] = e.target.value; }
onFormChange("env_vars", newEnvVars);
}}
placeholder="value" placeholder="value"
style={{ flex: 1 }} className="mobile-form-input"
/> />
<button <button
type="button" type="button"
onClick={() => { className="ghost-button small"
const newEnvVars = { ...formData.env_vars }; onClick={() => onRemoveEnvVar(key)}
delete newEnvVars[key];
onFormChange("env_vars", newEnvVars);
}}
className="secondary-button"
> >
<Icon name="delete" size="sm" /> <Icon name="delete" size="sm" />
</button> </button>
@@ -220,49 +578,158 @@ export const ConfigProfilesMobileView = ({
))} ))}
<button <button
type="button" type="button"
className="secondary-button" className="secondary-button mobile-add-button"
onClick={() => { onClick={onAddEnvVar}
onFormChange("env_vars", { ...formData.env_vars, "": "" });
}}
> >
<Icon name="add" size="sm" /> Add Variable <Icon name="add" size="sm" /> Add Variable
</button> </button>
</div> </div>
<div className="form-group"> <div className="mobile-form-section">
<label>Mounts</label> <div className="mobile-form-section-header">
<h4>Runtime Hints</h4>
</div>
<textarea
value={JSON.stringify(formData.runtime_hints || {}, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
onFormChange("runtime_hints", parsed);
} catch {
/* ignore invalid JSON while typing */
}
}}
placeholder='{"start_command": "npm start"}'
rows={4}
className="mobile-form-textarea mobile-form-code"
/>
</div>
<div className="mobile-form-section">
<div className="mobile-form-section-header">
<h4>Files</h4>
</div>
<p className="muted mobile-form-help">
Relative paths written to the instance directory. Use Mounts below
for absolute container paths.
</p>
{Object.entries(formData.files || {}).map(([path, content], idx) => (
<div key={idx} className="mobile-file-card">
<div className="mobile-file-header">
<input
type="text"
value={path}
onChange={(e) =>
onUpdateFile(path, e.target.value, content)
}
placeholder="relative/path/to/file"
className="mobile-form-input"
/>
<button
type="button"
className="ghost-button small"
onClick={() => onRemoveFile(path)}
>
<Icon name="delete" size="sm" />
</button>
</div>
<textarea
value={content}
onChange={(e) =>
onUpdateFile(path, path, e.target.value)
}
placeholder="File content"
rows={3}
className="mobile-form-textarea mobile-form-code"
/>
</div>
))}
<button
type="button"
className="secondary-button mobile-add-button"
onClick={onAddFile}
>
<Icon name="add" size="sm" /> Add File
</button>
</div>
<div className="mobile-form-section">
<div className="mobile-form-section-header">
<h4>Mounts</h4>
</div>
<p className="muted mobile-form-help">
Bind directories into the container at absolute paths. Files are
relative to the mount target.
</p>
{(formData.mounts || []).map((mount, index) => ( {(formData.mounts || []).map((mount, index) => (
<div key={index} style={{ display: "flex", gap: "0.5rem", marginBottom: "0.5rem" }}> <div key={index} className="mobile-mount-card">
<div className="mobile-mount-header">
<input <input
type="text" type="text"
value={mount.target} value={mount.target}
onChange={(e) => { onChange={(e) =>
const newMounts = [...(formData.mounts || [])]; onUpdateMount(index, { target: e.target.value })
newMounts[index] = { ...mount, target: e.target.value }; }
onFormChange("mounts", newMounts); placeholder="/target/path"
}} className="mobile-form-input"
placeholder="Target path"
style={{ flex: 1 }}
/> />
<select <select
value={mount.mode} value={mount.mode}
onChange={(e) => { onChange={(e) =>
const newMounts = [...(formData.mounts || [])]; onUpdateMount(index, {
newMounts[index] = { ...mount, mode: e.target.value as "ro" | "rw" }; mode: e.target.value as "ro" | "rw",
onFormChange("mounts", newMounts); })
}} }
style={{ width: "80px" }} className="mobile-form-select"
> >
<option value="ro">Read</option> <option value="rw">Read/Write</option>
<option value="rw">Write</option> <option value="ro">Read-Only</option>
</select> </select>
<button <button
type="button" type="button"
onClick={() => { className="ghost-button small"
const newMounts = (formData.mounts || []).filter((_, i) => i !== index); onClick={() => onRemoveMount(index)}
onFormChange("mounts", newMounts); >
}} <Icon name="delete" size="sm" />
className="secondary-button" </button>
</div>
<div className="mobile-mount-files">
{Object.entries(mount.files).map(([path, content], idx) => (
<div key={idx} className="mobile-mount-file-row">
<input
type="text"
value={path}
onChange={(e) =>
onUpdateMountFile(
index,
path,
e.target.value,
content,
)
}
placeholder="relative/path"
className="mobile-form-input"
/>
<textarea
value={content}
onChange={(e) =>
onUpdateMountFile(
index,
path,
path,
e.target.value,
)
}
placeholder="File content"
rows={2}
className="mobile-form-textarea mobile-form-code"
/>
<button
type="button"
className="ghost-button small"
onClick={() =>
onRemoveMountFile(index, path)
}
> >
<Icon name="delete" size="sm" /> <Icon name="delete" size="sm" />
</button> </button>
@@ -270,14 +737,44 @@ export const ConfigProfilesMobileView = ({
))} ))}
<button <button
type="button" type="button"
className="secondary-button" className="secondary-button small mobile-add-button"
onClick={() => { onClick={() => onAddMountFile(index)}
onFormChange("mounts", [...(formData.mounts || []), { target: "/", mode: "rw", files: {} }]); >
}} <Icon name="add" size="sm" /> Add File to Mount
</button>
</div>
</div>
))}
<button
type="button"
className="secondary-button mobile-add-button"
onClick={onAddMount}
> >
<Icon name="add" size="sm" /> Add Mount <Icon name="add" size="sm" /> Add Mount
</button> </button>
</div> </div>
<div className="mobile-form-section">
<GitMountEditor
mounts={formData.git_mounts || []}
onChange={(git_mounts) =>
onFormChange("git_mounts", git_mounts)
}
/>
</div>
{(error || saveStatus === "error") && (
<p className="mobile-form-error">
<Icon name="warning" size="sm" /> {error || "Failed to save"}
</p>
)}
{saveStatus === "saved" && (
<div className="mobile-form-success">
<Icon name="success" size="sm" />
Profile saved successfully
</div>
)}
</MobileEditView> </MobileEditView>
); );
} }
@@ -286,6 +783,7 @@ export const ConfigProfilesMobileView = ({
<div className="mobile-page"> <div className="mobile-page">
<div className="mobile-page-header"> <div className="mobile-page-header">
<h1>Config Profiles</h1> <h1>Config Profiles</h1>
<span className="muted">{profiles.length} profiles</span>
</div> </div>
<MobileListView <MobileListView
items={profiles.map((profile) => ({ items={profiles.map((profile) => ({
@@ -297,7 +795,7 @@ export const ConfigProfilesMobileView = ({
const profile = profiles.find((p) => p.id === id); const profile = profiles.find((p) => p.id === id);
if (profile) { if (profile) {
onSelect(profile); onSelect(profile);
onViewChange("detail"); onViewChange("edit");
} }
}} }}
onItemDelete={(id: string) => onDelete(id)} onItemDelete={(id: string) => onDelete(id)}
@@ -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 core application features including navigation, data views, forms, modals, and specialized terminal interfaces. Provides a complete set of mobile-optimized UI components for CRUD operations, navigation, and terminal interfaces in a responsive web application.
## 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,22 +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 core application features including navigation, data views, forms, modals, and specialized terminal interfaces. Provides a complete set of mobile-optimized UI components for CRUD operations, navigation, and terminal interfaces in a responsive web application.
## 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/edit/delete actions, and a configurable list of typed fields. | 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 records with configurable field types and save/cancel/delete 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 mobile-optimized list view with optional search, empty state, and customizable item rendering | exp: MobileListView | dep: react, ../../icon, ../../../utils/icons, icon - mobile-list-view.tsx | A reusable React component that renders a mobile-optimized list view with optional search, custom item rendering, and action buttons (duplicate/delete). | exp: MobileListView | dep: react, ../../icon, ../../../utils/icons, icon
- 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-nav.tsx | Renders a mobile navigation bar with grouped items that trigger bottom sheets and standard links with active state highlighting | 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 - 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
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. Feature-based component architecture using compound mobile patterns (bottom sheets, action sheets, FABs) with typed configurable props, consistent mobile-first design system, and specialized terminal wrapper with native-like behaviors (auto-hiding headers, virtual keyboard handling).
## tags ## tags
mobile, terminal, sheet, react, icon, view, header, renders mobile, terminal, sheet, react, icon, view, header, action
## symbols ## symbols
- MobileActionSheet - MobileActionSheet
- MobilePageHeader - MobilePageHeader
@@ -13,6 +13,7 @@ interface MobileDetailViewProps {
onEdit: () => void; onEdit: () => void;
onDelete: () => void; onDelete: () => void;
onBack: () => void; onBack: () => void;
children?: React.ReactNode;
} }
export const MobileDetailView: React.FC<MobileDetailViewProps> = ({ export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
@@ -22,6 +23,7 @@ export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
onEdit, onEdit,
onDelete, onDelete,
onBack, onBack,
children,
}) => { }) => {
const renderValue = (field: Field) => { const renderValue = (field: Field) => {
if (field.value === null || field.value === undefined) { if (field.value === null || field.value === undefined) {
@@ -90,6 +92,7 @@ export const MobileDetailView: React.FC<MobileDetailViewProps> = ({
</div> </div>
))} ))}
</div> </div>
{children}
</div> </div>
); );
}; };
@@ -16,6 +16,8 @@ interface MobileEditViewProps {
fields?: FormField[]; fields?: FormField[];
onSave: (data: Record<string, string | number | boolean>) => void; onSave: (data: Record<string, string | number | boolean>) => void;
onCancel: () => void; onCancel: () => void;
onDelete?: () => void;
deleteLabel?: string;
isSaving?: boolean; isSaving?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
} }
@@ -25,6 +27,8 @@ export const MobileEditView: React.FC<MobileEditViewProps> = ({
fields, fields,
onSave, onSave,
onCancel, onCancel,
onDelete,
deleteLabel = "Delete",
isSaving = false, isSaving = false,
children, children,
}) => { }) => {
@@ -59,14 +63,7 @@ export const MobileEditView: React.FC<MobileEditViewProps> = ({
Cancel Cancel
</button> </button>
<h1 className="mobile-edit-title">{title}</h1> <h1 className="mobile-edit-title">{title}</h1>
<button <div style={{ width: "4rem" }} />
className="mobile-edit-save"
onClick={() => onSave(formData)}
type="button"
disabled={isSaving}
>
{isSaving ? "Saving..." : "Save"}
</button>
</header> </header>
<form className="mobile-edit-form" onSubmit={handleSubmit}> <form className="mobile-edit-form" onSubmit={handleSubmit}>
@@ -156,6 +153,27 @@ export const MobileEditView: React.FC<MobileEditViewProps> = ({
</div> </div>
))} ))}
</form> </form>
<div className="mobile-edit-actions">
<button
className="mobile-edit-save"
onClick={() => onSave(formData)}
type="button"
disabled={isSaving}
>
{isSaving ? "Saving..." : "Save"}
</button>
{onDelete && (
<button
className="mobile-edit-delete"
onClick={onDelete}
type="button"
disabled={isSaving}
>
{deleteLabel}
</button>
)}
</div>
</div> </div>
); );
}; };
@@ -24,6 +24,8 @@ interface MobileListViewProps {
export const MobileListView: React.FC<MobileListViewProps> = ({ export const MobileListView: React.FC<MobileListViewProps> = ({
items, items,
onItemClick, onItemClick,
onItemDelete,
onItemDuplicate,
emptyMessage = "No items found", emptyMessage = "No items found",
searchPlaceholder = "Search...", searchPlaceholder = "Search...",
onSearch, onSearch,
@@ -73,11 +75,38 @@ export const MobileListView: React.FC<MobileListViewProps> = ({
)} )}
</div> </div>
)} )}
<div <div className="mobile-list-item-actions">
className="mobile-list-item-actions" {onItemDuplicate && (
style={{ transform: "rotate(180deg)" }} <button
type="button"
className="mobile-list-item-action mobile-list-item-action-duplicate"
onClick={(e) => {
e.stopPropagation();
onItemDuplicate(item.id);
}}
aria-label="Duplicate"
> >
<Icon name="copy" size="sm" />
</button>
)}
{onItemDelete && (
<button
type="button"
className="mobile-list-item-action mobile-list-item-action-delete"
onClick={(e) => {
e.stopPropagation();
onItemDelete(item.id);
}}
aria-label="Delete"
>
<Icon name="delete" size="sm" />
</button>
)}
{!onItemDelete && !onItemDuplicate && (
<span style={{ transform: "rotate(180deg)" }}>
<Icon name="arrow-left" size="sm" /> <Icon name="arrow-left" size="sm" />
</span>
)}
</div> </div>
</button> </button>
))} ))}
@@ -0,0 +1,20 @@
# apps/web/src/components/features/profile (index)
dir: apps/web/src/components/features/profile
## role
Provides a mobile-specific UI for users to view and edit their profile information.
## parent
index: apps/web/src/components/features/.pi-map.index.md
map: apps/web/src/components/features/.pi-map.md
## children
-
## files
- ProfileMobileView.tsx
## links
index: apps/web/src/components/features/profile/.pi-map.index.md
map: apps/web/src/components/features/profile/.pi-map.md
## workflows
- change profile behavior
read: ProfileMobileView.tsx
## dirty
-
@@ -0,0 +1,20 @@
# apps/web/src/components/features/profile
dir: apps/web/src/components/features/profile
index: apps/web/src/components/features/profile/.pi-map.index.md
## role
Provides a mobile-specific UI for users to view and edit their profile information.
## files
- ProfileMobileView.tsx | Renders a mobile-optimized profile editing form with avatar upload, name/email fields, and save/cancel functionality. | exp: ProfileMobileView | dep: react-router-dom, ../mobile/mobile-edit-view, ../../icon, ../../../api/profile
## arch
Single feature-focused component with form handling and file upload, likely using controlled inputs and local state management.
## tags
mobile, profile, view, renders, optimized, editing, form, avatar
## symbols
- ProfileMobileView
## workflows
- change profile behavior
read: ProfileMobileView.tsx
## dirty
-
@@ -0,0 +1,119 @@
import { useNavigate } from "react-router-dom";
import { MobileEditView } from "../mobile/mobile-edit-view";
import { Icon } from "../../icon";
import type { UserProfile } from "../../../api/profile";
interface ProfileMobileViewProps {
profile: UserProfile;
name: string;
email: string;
error: string | null;
isSaving: boolean;
avatarUrl: string | null;
fileInputRef: React.RefObject<HTMLInputElement>;
onNameChange: (value: string) => void;
onEmailChange: (value: string) => void;
onAvatarButtonClick: () => void;
onAvatarChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onSave: () => void;
}
export const ProfileMobileView = ({
profile,
name,
email,
error,
isSaving,
avatarUrl,
fileInputRef,
onNameChange,
onEmailChange,
onAvatarButtonClick,
onAvatarChange,
onSave,
}: ProfileMobileViewProps) => {
const navigate = useNavigate();
return (
<MobileEditView
title="Profile"
onCancel={() => navigate(-1)}
onSave={onSave}
isSaving={isSaving}
>
<div className="profile-mobile-avatar-section">
<div className="profile-mobile-avatar">
{avatarUrl ? (
<img alt="Avatar" className="profile-mobile-avatar-image" src={avatarUrl} />
) : (
<div className="profile-mobile-avatar-placeholder">
{profile.name.charAt(0).toUpperCase()}
</div>
)}
</div>
<button
className="secondary-button"
disabled={isSaving}
onClick={onAvatarButtonClick}
type="button"
>
{isSaving ? (
<>
<Icon name="loading" size="sm" />
Uploading...
</>
) : (
<>
<Icon name="edit" size="sm" />
Change Avatar
</>
)}
</button>
<input
accept="image/png,image/jpeg"
onChange={onAvatarChange}
ref={fileInputRef}
style={{ display: "none" }}
type="file"
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label" htmlFor="profile-mobile-name">
Name *
</label>
<input
className="mobile-form-input"
disabled={isSaving}
id="profile-mobile-name"
onChange={(e) => onNameChange(e.target.value)}
placeholder="Your name"
type="text"
value={name}
/>
</div>
<div className="mobile-form-group">
<label className="mobile-form-label" htmlFor="profile-mobile-email">
Email *
</label>
<input
className="mobile-form-input"
disabled={isSaving}
id="profile-mobile-email"
onChange={(e) => onEmailChange(e.target.value)}
placeholder="your.email@example.com"
type="email"
value={email}
/>
</div>
{error && (
<p className="mobile-form-error">
<Icon name="warning" size="sm" />
{error}
</p>
)}
</MobileEditView>
);
};
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features/tool-workshop dir: apps/web/src/components/features/tool-workshop
## role ## role
Provides a complete UI for managing custom tool types in a "Tool Workshop" interface with list, edit, and mobile-responsive views. Provides a complete UI for managing custom tool types (compose, dockerfile, manifest) in a workshop interface with list, edit, and mobile-responsive views.
## 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,15 +4,15 @@ dir: apps/web/src/components/features/tool-workshop
index: apps/web/src/components/features/tool-workshop/.pi-map.index.md index: apps/web/src/components/features/tool-workshop/.pi-map.index.md
## role ## role
Provides a complete UI for managing custom tool types in a "Tool Workshop" interface with list, edit, and mobile-responsive views. Provides a complete UI for managing custom tool types (compose, dockerfile, manifest) in a workshop interface with list, edit, and mobile-responsive views.
## files ## files
- ToolTypeEditorPanel.tsx | Renders a form panel for creating or editing tool types with support for compose, dockerfile, and manifest definition types | exp: ToolTypeFormState, ToolTypeEditorPanel | dep: ../../icon, ../tool/manifest-editor, ../../../api/tool-types, ../../../api/tool-definitions, React, Icon, ManifestEditor - ToolTypeEditorPanel.tsx | Renders a form panel for creating or editing tool types with support for compose, dockerfile, and manifest definition types | exp: ToolTypeFormState, ToolTypeEditorPanel | dep: ../../icon, ../tool/manifest-editor, ../../../api/tool-types, ../../../api/tool-definitions, React, Icon, ManifestEditor
- ToolTypeListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting tool types in a "Tool Workshop" interface. | exp: ToolTypeListSidebar | dep: ../../icon, ../../../api/tool-types, React, Icon component, ToolType type - ToolTypeListSidebar.tsx | Renders a sidebar component for listing, selecting, creating, and deleting tool types in a "Tool Workshop" interface. | exp: ToolTypeListSidebar | dep: ../../icon, ../../../api/tool-types, React, Icon component, ToolType type
- ToolWorkshopMobileView.tsx | Renders a mobile-responsive view for managing tool types with list, detail, and edit modes | exp: MobileView, ToolWorkshopMobileView | dep: ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../../../api/tool-types, ./ToolTypeEditorPanel, MobileListView, MobileDetailView, MobileEditView, MobileFAB, ToolType, ToolTypeFormState - ToolWorkshopMobileView.tsx | Renders a mobile-responsive workshop interface for managing tool types with list, detail, and edit views. | exp: MobileView, ToolWorkshopMobileView | dep: react, ../mobile/mobile-list-view, ../mobile/mobile-detail-view, ../mobile/mobile-edit-view, ../mobile/mobile-fab, ../tool/manifest-editor, ../../icon, ../../../api/tool-types, ./ToolTypeEditorPanel, ../../../api/tool-definitions, mobile-list-view, mobile-detail-view, mobile-edit-view, mobile-fab, manifest-editor, icon, tool-types, tool-definitions
## arch ## arch
Uses a split-pane sidebar/detail panel pattern with dedicated mobile breakpoint handling, separating list navigation from form editing concerns across three specialized view components. Feature-based component composition with split-pane layout (sidebar + editor panel), responsive mobile adaptation, and CRUD operations for tool type definitions.
## tags ## tags
tool, mobile, type, view, types, list, panel, editor tool, mobile, view, type, types, editor, list, workshop
## symbols ## symbols
- ToolTypeFormState - ToolTypeFormState
- ToolTypeEditorPanel - ToolTypeEditorPanel
@@ -1,9 +1,14 @@
import { useState } from "react";
import { MobileListView } from "../mobile/mobile-list-view"; import { MobileListView } from "../mobile/mobile-list-view";
import { MobileDetailView } from "../mobile/mobile-detail-view"; import { MobileDetailView } from "../mobile/mobile-detail-view";
import { MobileEditView } from "../mobile/mobile-edit-view"; import { MobileEditView } from "../mobile/mobile-edit-view";
import { MobileFAB } from "../mobile/mobile-fab"; import { MobileFAB } from "../mobile/mobile-fab";
import { ManifestEditor } from "../tool/manifest-editor";
import { Icon } from "../../icon";
import type { ToolType } from "../../../api/tool-types"; import type { ToolType } from "../../../api/tool-types";
import type { ToolTypeFormState } from "./ToolTypeEditorPanel"; import type { ToolTypeFormState } from "./ToolTypeEditorPanel";
import type { ToolDefinitionManifest } from "../../../api/tool-definitions";
export type MobileView = "list" | "detail" | "edit"; export type MobileView = "list" | "detail" | "edit";
@@ -13,13 +18,18 @@ interface ToolWorkshopMobileViewProps {
mobileView: MobileView; mobileView: MobileView;
isCreating: boolean; isCreating: boolean;
toolTypeForm: ToolTypeFormState; toolTypeForm: ToolTypeFormState;
manifestData: Record<string, unknown> | null;
manifestDefinitionId: string | null;
baseDefinitions: ToolDefinitionManifest[];
toolTypeError: string | null; toolTypeError: string | null;
toolTypeDirty: boolean;
onViewChange: (view: MobileView) => void; onViewChange: (view: MobileView) => void;
onSelect: (toolType: ToolType) => void; onSelect: (toolType: ToolType) => void;
onCreate: () => void; onCreate: () => void;
onDelete: (id: string) => void; onDelete: (id: string) => void;
onFormChange: (changes: Partial<ToolTypeFormState>) => void; onFormChange: (changes: Partial<ToolTypeFormState>) => void;
onSubmit: () => void; onManifestChange: (manifest: Record<string, unknown> | null) => void;
onSubmit: (e?: React.FormEvent) => Promise<boolean>;
onCancel: () => void; onCancel: () => void;
} }
@@ -29,15 +39,41 @@ export const ToolWorkshopMobileView = ({
mobileView, mobileView,
isCreating, isCreating,
toolTypeForm, toolTypeForm,
manifestData,
manifestDefinitionId,
baseDefinitions,
toolTypeError, toolTypeError,
toolTypeDirty,
onViewChange, onViewChange,
onSelect, onSelect,
onCreate, onCreate,
onDelete, onDelete,
onFormChange, onFormChange,
onManifestChange,
onSubmit, onSubmit,
onCancel, onCancel,
}: ToolWorkshopMobileViewProps) => { }: ToolWorkshopMobileViewProps) => {
const [isSaving, setIsSaving] = useState(false);
const handleSave = async () => {
setIsSaving(true);
try {
const ok = await onSubmit();
if (ok) {
onViewChange(isCreating ? "list" : "edit");
}
} finally {
setIsSaving(false);
}
};
const handleDelete = () => {
if (selectedToolType) {
void onDelete(selectedToolType.id);
onViewChange("list");
}
};
if (mobileView === "list") { if (mobileView === "list") {
return ( return (
<div className="mobile-page"> <div className="mobile-page">
@@ -49,15 +85,16 @@ export const ToolWorkshopMobileView = ({
items={toolTypes.map((t) => ({ items={toolTypes.map((t) => ({
id: t.id, id: t.id,
title: t.display_name, title: t.display_name,
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`, subtitle: `${t.category || "Uncategorized"} · ${t.definition_type} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
}))} }))}
onItemClick={(id) => { onItemClick={(id) => {
const toolType = toolTypes.find((t) => t.id === id); const toolType = toolTypes.find((t) => t.id === id);
if (toolType) { if (toolType) {
onSelect(toolType); onSelect(toolType);
onViewChange("detail"); onViewChange("edit");
} }
}} }}
onItemDelete={(id) => onDelete(id)}
emptyMessage="No tool types yet" emptyMessage="No tool types yet"
/> />
<MobileFAB <MobileFAB
@@ -71,11 +108,7 @@ export const ToolWorkshopMobileView = ({
} }
if (mobileView === "detail" && selectedToolType) { if (mobileView === "detail" && selectedToolType) {
return ( const fields = [
<MobileDetailView
title={selectedToolType.display_name}
subtitle={`${selectedToolType.name} · ${selectedToolType.definition_type} · ${selectedToolType.interface_type === "web" ? `Port ${selectedToolType.default_port}` : "Terminal"}`}
fields={[
{ label: "Name", value: selectedToolType.name }, { label: "Name", value: selectedToolType.name },
{ label: "Display Name", value: selectedToolType.display_name }, { label: "Display Name", value: selectedToolType.display_name },
{ label: "Description", value: selectedToolType.description }, { label: "Description", value: selectedToolType.description },
@@ -84,9 +117,11 @@ export const ToolWorkshopMobileView = ({
{ {
label: "Requires Port", label: "Requires Port",
value: selectedToolType.requires_port, value: selectedToolType.requires_port,
type: "boolean", type: "boolean" as const,
}, },
{ label: "Default Port", value: selectedToolType.default_port }, ...(selectedToolType.requires_port
? [{ label: "Default Port", value: selectedToolType.default_port }]
: []),
{ {
label: "Definition Type", label: "Definition Type",
value: selectedToolType.definition_type, value: selectedToolType.definition_type,
@@ -111,24 +146,32 @@ export const ToolWorkshopMobileView = ({
label: "Required Variables", label: "Required Variables",
value: selectedToolType.required_variables?.join(", ") ?? null, value: selectedToolType.required_variables?.join(", ") ?? null,
}, },
...(selectedToolType.definition_type !== "manifest"
? [
{ {
label: "Compose Template", label:
value: selectedToolType.compose_template, selectedToolType.definition_type === "compose"
type: "code", ? "Compose Template"
: "Dockerfile Template",
value:
selectedToolType.definition_type === "compose"
? selectedToolType.compose_template
: selectedToolType.dockerfile_template,
type: "code" as const,
}, },
{ ]
label: "Dockerfile Template", : []),
value: selectedToolType.dockerfile_template, ];
type: "code",
}, return (
]} <MobileDetailView
title={selectedToolType.display_name}
subtitle={`${selectedToolType.name} · ${selectedToolType.definition_type}`}
fields={fields}
onEdit={() => { onEdit={() => {
onViewChange("edit"); onViewChange("edit");
}} }}
onDelete={() => { onDelete={handleDelete}
void onDelete(selectedToolType.id);
onViewChange("list");
}}
onBack={() => { onBack={() => {
onViewChange("list"); onViewChange("list");
}} }}
@@ -137,18 +180,41 @@ export const ToolWorkshopMobileView = ({
} }
if (mobileView === "edit") { if (mobileView === "edit") {
const templateValue =
toolTypeForm.definition_type === "compose"
? toolTypeForm.compose_template
: toolTypeForm.dockerfile_template;
return ( return (
<MobileEditView <MobileEditView
title={isCreating ? "Create Tool Type" : "Edit Tool Type"} title={isCreating ? "Create Tool Type" : "Edit Tool Type"}
onCancel={onCancel} onCancel={onCancel}
onSave={() => { onSave={handleSave}
onSubmit(); onDelete={isCreating ? undefined : handleDelete}
if (!toolTypeError) { deleteLabel="Delete Tool Type"
onViewChange("list"); isSaving={isSaving}
}
}}
isSaving={false}
> >
<div className="mobile-form-group">
<label className="mobile-form-label">Definition Type</label>
<select
value={toolTypeForm.definition_type}
onChange={(e) =>
onFormChange({
definition_type: e.target.value as
| "compose"
| "dockerfile"
| "manifest",
})
}
className="mobile-form-select"
disabled={!isCreating}
>
<option value="compose">Docker Compose</option>
<option value="dockerfile">Dockerfile</option>
<option value="manifest">Manifest (Declarative)</option>
</select>
</div>
<div className="mobile-form-group"> <div className="mobile-form-group">
<label className="mobile-form-label">Name *</label> <label className="mobile-form-label">Name *</label>
<input <input
@@ -157,8 +223,11 @@ export const ToolWorkshopMobileView = ({
onChange={(e) => onFormChange({ name: e.target.value })} onChange={(e) => onFormChange({ name: e.target.value })}
className="mobile-form-input" className="mobile-form-input"
placeholder="e.g., my-tool" placeholder="e.g., my-tool"
disabled={!isCreating}
required
/> />
</div> </div>
<div className="mobile-form-group"> <div className="mobile-form-group">
<label className="mobile-form-label">Display Name *</label> <label className="mobile-form-label">Display Name *</label>
<input <input
@@ -167,8 +236,10 @@ export const ToolWorkshopMobileView = ({
onChange={(e) => onFormChange({ display_name: e.target.value })} onChange={(e) => onFormChange({ display_name: e.target.value })}
className="mobile-form-input" className="mobile-form-input"
placeholder="e.g., My Tool" placeholder="e.g., My Tool"
required
/> />
</div> </div>
<div className="mobile-form-group"> <div className="mobile-form-group">
<label className="mobile-form-label">Description</label> <label className="mobile-form-label">Description</label>
<textarea <textarea
@@ -179,6 +250,7 @@ export const ToolWorkshopMobileView = ({
rows={3} rows={3}
/> />
</div> </div>
<div className="mobile-form-group"> <div className="mobile-form-group">
<label className="mobile-form-label">Category</label> <label className="mobile-form-label">Category</label>
<input <input
@@ -189,130 +261,179 @@ export const ToolWorkshopMobileView = ({
placeholder="e.g., development" placeholder="e.g., development"
/> />
</div> </div>
<div className="mobile-form-group"> <div className="mobile-form-group">
<label className="mobile-form-label">Interface Type</label> <label className="mobile-form-label">Interface Type</label>
<select <select
value={toolTypeForm.interface_type} value={toolTypeForm.interface_type}
onChange={(e) => onChange={(e) => {
const value = e.target.value as "web" | "terminal";
onFormChange({ onFormChange({
interface_type: e.target.value as "web" | "terminal", interface_type: value,
}) requires_port: value === "web",
} default_port: value === "web" ? toolTypeForm.default_port : "",
});
}}
className="mobile-form-select" className="mobile-form-select"
> >
<option value="web">Web</option> <option value="web">Web</option>
<option value="terminal">Terminal</option> <option value="terminal">Terminal</option>
</select> </select>
</div> </div>
<div className="mobile-form-group">
<label className="mobile-form-label">Requires Port</label> <div className="mobile-form-group mobile-form-row">
<label className="mobile-form-label mobile-form-checkbox-label">
<input <input
type="checkbox" type="checkbox"
checked={toolTypeForm.requires_port} checked={toolTypeForm.requires_port}
onChange={(e) => onFormChange({ requires_port: e.target.checked })} onChange={(e) =>
onFormChange({ requires_port: e.target.checked })
}
className="mobile-form-checkbox" className="mobile-form-checkbox"
/> />
Requires Port
</label>
</div> </div>
{toolTypeForm.requires_port && (
<div className="mobile-form-group"> <div className="mobile-form-group">
<label className="mobile-form-label">Default Port</label> <label className="mobile-form-label">Default Port *</label>
<input <input
type="text" type="number"
value={toolTypeForm.default_port} value={toolTypeForm.default_port}
onChange={(e) => onFormChange({ default_port: e.target.value })} onChange={(e) =>
onFormChange({ default_port: e.target.value })
}
className="mobile-form-input" className="mobile-form-input"
placeholder="e.g., 8080" placeholder="e.g., 8080"
required
/> />
</div> </div>
<div className="mobile-form-group"> )}
<label className="mobile-form-label">Definition Type</label>
<select {toolTypeForm.interface_type === "terminal" && (
value={toolTypeForm.definition_type}
onChange={(e) =>
onFormChange({
definition_type: e.target.value as "compose" | "dockerfile",
})
}
className="mobile-form-select"
>
<option value="compose">Compose</option>
<option value="dockerfile">Dockerfile</option>
</select>
</div>
<div className="mobile-form-group"> <div className="mobile-form-group">
<label className="mobile-form-label">Startup Command</label> <label className="mobile-form-label">Startup Command</label>
<input <input
type="text" type="text"
value={toolTypeForm.startup_command} value={toolTypeForm.startup_command}
onChange={(e) => onFormChange({ startup_command: e.target.value })} onChange={(e) =>
onFormChange({ startup_command: e.target.value })
}
className="mobile-form-input" className="mobile-form-input"
placeholder="Command to run on startup" placeholder="Command to run on startup"
/> />
<small className="mobile-form-help">
Command to run before the interactive shell for each new
terminal session.
</small>
</div> </div>
)}
{toolTypeForm.definition_type === "manifest" ? (
<div className="mobile-manifest-editor-wrapper">
<ManifestEditor
manifest={manifestData}
baseDefinitions={baseDefinitions}
onChange={(m) => onManifestChange(m)}
definitionId={manifestDefinitionId}
/>
</div>
) : (
<div className="mobile-form-group">
<label className="mobile-form-label">
{toolTypeForm.definition_type === "compose"
? "Compose Template *"
: "Dockerfile Template *"}
</label>
<textarea
value={templateValue}
onChange={(e) => {
if (toolTypeForm.definition_type === "compose") {
onFormChange({ compose_template: e.target.value });
} else {
onFormChange({ dockerfile_template: e.target.value });
}
}}
className="mobile-form-textarea mobile-form-code"
placeholder={
toolTypeForm.definition_type === "compose"
? "version: '3'"
: "FROM ubuntu:22.04"
}
rows={10}
required
/>
</div>
)}
<div className="mobile-form-group"> <div className="mobile-form-group">
<label className="mobile-form-label">Readiness Command</label> <label className="mobile-form-label">Readiness Command</label>
<input <input
type="text" type="text"
value={toolTypeForm.readiness_command} value={toolTypeForm.readiness_command}
onChange={(e) => onFormChange({ readiness_command: e.target.value })} onChange={(e) =>
onFormChange({ readiness_command: e.target.value })
}
className="mobile-form-input" className="mobile-form-input"
placeholder="e.g., curl -f http://localhost:8080/health" placeholder="e.g., curl -f http://localhost:8080/health"
/> />
</div> </div>
<div className="mobile-form-group"> <div className="mobile-form-group">
<label className="mobile-form-label">Readiness Timeout</label> <label className="mobile-form-label">Readiness Timeout (seconds)</label>
<input <input
type="text" type="number"
value={toolTypeForm.readiness_timeout} value={toolTypeForm.readiness_timeout}
onChange={(e) => onFormChange({ readiness_timeout: e.target.value })} onChange={(e) =>
onFormChange({ readiness_timeout: e.target.value })
}
className="mobile-form-input" className="mobile-form-input"
placeholder="30" placeholder="30"
/> />
</div> </div>
<div className="mobile-form-group"> <div className="mobile-form-group">
<label className="mobile-form-label">Readiness Interval</label> <label className="mobile-form-label">Readiness Interval (seconds)</label>
<input <input
type="text" type="number"
value={toolTypeForm.readiness_interval} value={toolTypeForm.readiness_interval}
onChange={(e) => onFormChange({ readiness_interval: e.target.value })} onChange={(e) =>
onFormChange({ readiness_interval: e.target.value })
}
className="mobile-form-input" className="mobile-form-input"
placeholder="2" placeholder="2"
/> />
</div> </div>
<div className="mobile-form-group"> <div className="mobile-form-group">
<label className="mobile-form-label">Required Variables</label> <label className="mobile-form-label">Required Variables</label>
<input <input
type="text" type="text"
value={toolTypeForm.required_variables} value={toolTypeForm.required_variables}
onChange={(e) => onFormChange({ required_variables: e.target.value })} onChange={(e) =>
onFormChange({ required_variables: e.target.value })
}
className="mobile-form-input" className="mobile-form-input"
placeholder="VAR1, VAR2, VAR3" placeholder="VAR1, VAR2, VAR3"
/> />
</div> </div>
{toolTypeForm.definition_type === "compose" && (
<div className="mobile-form-group"> {toolTypeError && (
<label className="mobile-form-label">Compose Template</label> <p className="mobile-form-error">
<textarea <Icon name="warning" size="sm" /> {toolTypeError}
value={toolTypeForm.compose_template} </p>
onChange={(e) => onFormChange({ compose_template: e.target.value })}
className="mobile-form-textarea mobile-form-code"
placeholder="version: '3'"
rows={10}
/>
</div>
)} )}
{toolTypeForm.definition_type === "dockerfile" && (
<div className="mobile-form-group"> {toolTypeDirty && (
<label className="mobile-form-label">Dockerfile Template</label> <button
<textarea type="button"
value={toolTypeForm.dockerfile_template} className="secondary-button mobile-discard-button"
onChange={(e) => onClick={onCancel}
onFormChange({ dockerfile_template: e.target.value }) disabled={isSaving}
} >
className="mobile-form-textarea mobile-form-code" Discard Changes
placeholder="FROM ubuntu:22.04" </button>
rows={10}
/>
</div>
)} )}
</MobileEditView> </MobileEditView>
); );
@@ -328,15 +449,16 @@ export const ToolWorkshopMobileView = ({
items={toolTypes.map((t) => ({ items={toolTypes.map((t) => ({
id: t.id, id: t.id,
title: t.display_name, title: t.display_name,
subtitle: `${t.category || "Uncategorized"} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`, subtitle: `${t.category || "Uncategorized"} · ${t.definition_type} · ${t.interface_type === "web" ? `Port ${t.default_port}` : "Terminal"}`,
}))} }))}
onItemClick={(id) => { onItemClick={(id) => {
const toolType = toolTypes.find((t) => t.id === id); const toolType = toolTypes.find((t) => t.id === id);
if (toolType) { if (toolType) {
onSelect(toolType); onSelect(toolType);
onViewChange("detail"); onViewChange("edit");
} }
}} }}
onItemDelete={(id) => onDelete(id)}
emptyMessage="No tool types yet" emptyMessage="No tool types yet"
/> />
<MobileFAB <MobileFAB
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src/hooks dir: apps/web/src/hooks
## role ## role
A collection of custom React hooks providing reusable state management, API integration, and UI behavior logic for the web application. Provides reusable React custom hooks that encapsulate stateful logic, side effects, and API interactions for the web application's UI components, covering data fetching, terminal management, workspace operations, notifications, and mobile-specific behaviors.
## 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
+7 -7
View File
@@ -4,15 +4,15 @@ dir: apps/web/src/hooks
index: apps/web/src/hooks/.pi-map.index.md index: apps/web/src/hooks/.pi-map.index.md
## role ## role
A collection of custom React hooks providing reusable state management, API integration, and UI behavior logic for the web application. Provides reusable React custom hooks that encapsulate stateful logic, side effects, and API interactions for the web application's UI components, covering data fetching, terminal management, workspace operations, notifications, and mobile-specific behaviors.
## files ## files
- use-async-data.ts | A custom React hook that manages asynchronous data fetching with loading, error, and ready states, plus a manual reload capability. | exp: func:useAsyncData(fetcher: () => Promise<T>, deps: React.DependencyList) → UseAsyncDataResult<T>, call:useState, call:useCallback, call:setStatus, call:setError, call:fetcher, call:setData, call:load, call:useEffect | dep: react - use-async-data.ts | A custom React hook that manages asynchronous data fetching with loading, error, and ready states, plus a manual reload capability. | exp: func:useAsyncData(fetcher: () => Promise<T>, deps: React.DependencyList) → UseAsyncDataResult<T>, call:useState, call:useCallback, call:setStatus, call:setError, call:fetcher, call:setData, call:load, call:useEffect | dep: react
- use-auto-hide.ts | A React custom hook that automatically hides an element after a specified timeout and provides manual controls for showing, hiding, and toggling visibility. | exp: func:useAutoHide(options: AutoHideOptions), call:useState, call:useRef, call:Date.now, call:useCallback, call:setIsVisible, call:clearTimeout, call:setTimeout, call:hide, call:show, call:useEffect | dep: react - use-auto-hide.ts | A React custom hook that automatically hides an element after a specified timeout and provides manual controls for showing, hiding, and toggling visibility. | exp: func:useAutoHide(options: AutoHideOptions), call:useState, call:useRef, call:Date.now, call:useCallback, call:setIsVisible, call:clearTimeout, call:setTimeout, call:hide, call:show, call:useEffect | dep: react
- use-config-profiles.ts | A React custom hook that manages config profile CRUD operations, form state, profile inclusion with cycle detection, and drag-and-drop reordering. | exp: useConfigProfiles | dep: react, ../utils/errors, ../api/config-profiles, ../api/projects, ../api/tool-types, ../types - use-config-profiles.ts | A React custom hook that manages CRUD operations, form state, drag-and-drop ordering, and cyclic dependency prevention for configuration profiles. | exp: useConfigProfiles | dep: react, ../utils/errors, ../api/config-profiles, ../api/projects, ../api/tool-types, ../types
- use-events.test.ts | Tests a React custom hook that manages Server-Sent Events connections with automatic reconnection, backoff strategies, and error handling. | dep: vitest, @testing-library/react, ./use-events, ../api/events, use-events hook, ../api/events module - use-events.test.ts | Tests a React hook that manages Server-Sent Events (SSE) connections with automatic reconnection, backoff, and error handling | dep: vitest, @testing-library/react, ./use-events, ../api/events, use-events hook
- use-events.ts | React hook that manages a Server-Sent Events connection with exponential backoff reconnection, event buffering, and auth/rate-limit handling. | exp: UseEventsReturn, func:useEvents() → UseEventsReturn, call:useState, call:useRef, call:useCallback, call:clearTimeout, call:createEventSource, call:setConnected, call:setError, call:setReconnectCount, call:JSON.parse, call:setEvents, call:es.close, call:Math.min, call:Math.pow, call:Math.random, call:setTimeout, call:probeEventStreamStatus, call:window.location.assign, call:connect, call:useEffect, call:esRef.current.close | dep: react, ../api/events, ../types/events - use-events.ts | React hook that manages a Server-Sent Events connection with automatic exponential backoff reconnection, lifecycle event handling, and authentication redirect on 401 errors. | exp: UseEventsReturn, func:useEvents() → UseEventsReturn, call:useState, call:useRef, call:useCallback, call:clearTimeout, call:createEventSource, call:setConnected, call:setError, call:setReconnectCount, call:JSON.parse, call:setEvents, call:es.addEventListener, call:es.close, call:Math.min, call:Math.pow, call:Math.random, call:setTimeout, call:probeEventStreamStatus, call:window.location.assign, call:connect, call:useEffect, call:esRef.current.close | dep: react, ../api/events, ../types/events
- use-git-repo.ts | A custom React hook that centralizes all git repository operations (branch management, status tracking, commit history, and git actions) into a reusable interface for components. | exp: GitStatus, UseGitRepoResult, func:useGitRepo(projectId: string | undefined, repoId: string | undefined) → UseGitRepoResult, call:useState, call:useCallback, call:setLoading, call:setError, call:fn, call:extractError, call:withLoading, call:listRepositoryBranches, call:setBranches, call:data.branches.map, call:setDefaultBranch, call:getRepositoryStatus, call:setStatus, call:getRepositoryHistory, call:setHistory, call:getCommitDetail, call:setCommitDetail, call:commitChanges, call:refreshStatus, call:pushRepository, call:pullRepository, call:fetchRepository, call:checkoutBranch, call:refreshBranches, call:createBranch, call:deleteBranch, call:mergeBranches, call:refreshHistory, call:useEffect, raise:err | dep: react, ../api/git-repositories - use-git-repo.ts | A custom React hook that centralizes all git repository operations (branch management, status tracking, commit history, and git actions) into a reusable interface for components. | exp: GitStatus, UseGitRepoResult, func:useGitRepo(projectId: string | undefined, repoId: string | undefined) → UseGitRepoResult, call:useState, call:useCallback, call:setLoading, call:setError, call:fn, call:extractError, call:withLoading, call:listRepositoryBranches, call:setBranches, call:data.branches.map, call:setDefaultBranch, call:getRepositoryStatus, call:setStatus, call:getRepositoryHistory, call:setHistory, call:getCommitDetail, call:setCommitDetail, call:commitChanges, call:refreshStatus, call:pushRepository, call:pullRepository, call:fetchRepository, call:checkoutBranch, call:refreshBranches, call:createBranch, call:deleteBranch, call:mergeBranches, call:refreshHistory, call:useEffect, raise:err | dep: react, ../api/git-repositories
- use-instance-actions.ts | A custom React hook that provides a centralized interface for managing instance/session actions including opening, starting, stopping, deleting, force-deleting, recreating tunnels, and renaming, with loading states and dirty delete handling for conflict resolution. | exp: func:useInstanceActions(options: UseInstanceActionsOptions) → UseInstanceActionsReturn, call:useSessions, call:useSessionOperations, call:useState, call:useRef, call:useCallback, call:tabRefs.current.get, call:existing.focus, call:session.tool_type_interfaces?.includes, call:window.open, call:tabRefs.current.set, call:setLoadingSessionId, call:startOperation, call:startInstance, call:onRefresh, call:completeOperation, call:stopInstance, call:deleteInstance, call:setDirtyDeleteSession, call:setDirtyDeleteFiles, call:removeSession, call:recreateInstanceTunnel, call:alert, call:newName.trim, call:renameInstance | dep: react, ../api/sessions, ../state/sessions, ../state/session-operations - use-instance-actions.ts | A React custom hook that manages instance/session actions including opening, starting, stopping, deleting, force-deleting, recreating tunnels, and renaming with loading states and dirty delete handling. | exp: func:useInstanceActions(options: UseInstanceActionsOptions) → UseInstanceActionsReturn, call:useSessions, call:useState, call:useRef, call:useCallback, call:tabRefs.current.get, call:existing.focus, call:session.tool_type_interfaces?.includes, call:window.open, call:tabRefs.current.set, call:setLoadingSessionId, call:startInstance, call:onRefresh, call:stopInstance, call:deleteInstance, call:setDirtyDeleteSession, call:setDirtyDeleteFiles, call:removeSession, call:recreateInstanceTunnel, call:alert, call:newName.trim, call:renameInstance | dep: react, ../api/sessions, ../state/sessions
- use-mobile-viewport.ts | A React hook that tracks whether the viewport width is below a mobile breakpoint (768px) | exp: func:useMobileViewport(), call:useState, call:useEffect, call:setIsMobile, call:window.addEventListener, call:window.removeEventListener | dep: react - use-mobile-viewport.ts | A React hook that tracks whether the viewport width is below a mobile breakpoint (768px) | exp: func:useMobileViewport(), call:useState, call:useEffect, call:setIsMobile, call:window.addEventListener, call:window.removeEventListener | dep: react
- use-notifications.test.tsx | Tests the useNotifications custom React hook with mocked API calls, covering optimistic updates, polling behavior, and error handling. | dep: vitest, @testing-library/react, ./use-notifications, ../state/notifications, ../api/notifications, use-notifications - use-notifications.test.tsx | Tests the useNotifications custom React hook with mocked API calls, covering optimistic updates, polling behavior, and error handling. | dep: vitest, @testing-library/react, ./use-notifications, ../state/notifications, ../api/notifications, use-notifications
- use-notifications.ts | Custom React hook that provides access to notification state and ensures it's used within a NotificationProvider | exp: func:useNotifications(), call:useContext, raise:Error | dep: react, ../state/notifications - use-notifications.ts | Custom React hook that provides access to notification state and ensures it's used within a NotificationProvider | exp: func:useNotifications(), call:useContext, raise:Error | dep: react, ../state/notifications
@@ -23,7 +23,7 @@ A collection of custom React hooks providing reusable state management, API inte
- use-terminal-page.ts | Manages terminal page state including sessions, keyboard shortcuts, fullscreen mode, mobile viewport handling, and terminal lifecycle operations. | exp: useTerminalPage | dep: react, react-router-dom, ../components/features/terminal/terminal, ../components/features/terminal/terminal-session-tabs, ./use-mobile-viewport, ./use-auto-hide, ./use-virtual-keyboard, ./use-terminal-sessions, ../api/terminal, ./use-special-keys, use-mobile-viewport, use-auto-hide, use-virtual-keyboard, use-terminal-sessions, terminal, terminal-session-tabs, api/terminal, api/sessions, use-special-keys - use-terminal-page.ts | Manages terminal page state including sessions, keyboard shortcuts, fullscreen mode, mobile viewport handling, and terminal lifecycle operations. | exp: useTerminalPage | dep: react, react-router-dom, ../components/features/terminal/terminal, ../components/features/terminal/terminal-session-tabs, ./use-mobile-viewport, ./use-auto-hide, ./use-virtual-keyboard, ./use-terminal-sessions, ../api/terminal, ./use-special-keys, use-mobile-viewport, use-auto-hide, use-virtual-keyboard, use-terminal-sessions, terminal, terminal-session-tabs, api/terminal, api/sessions, use-special-keys
- use-terminal-sessions.ts | React custom hook that manages terminal session state (CRUD operations, active session tracking) for a given instance | exp: UseTerminalSessionsResult, func:useTerminalSessions(instanceId: string) → UseTerminalSessionsResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listTerminalSessions, call:setSessions, call:setActiveSessionId, call:createTerminalSession, call:closeTerminalSession, call:prev.filter, call:renameTerminalSession, call:prev.map, call:resetTerminalSession, call:loadSessions, call:useEffect | dep: react, ../api/terminal - use-terminal-sessions.ts | React custom hook that manages terminal session state (CRUD operations, active session tracking) for a given instance | exp: UseTerminalSessionsResult, func:useTerminalSessions(instanceId: string) → UseTerminalSessionsResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listTerminalSessions, call:setSessions, call:setActiveSessionId, call:createTerminalSession, call:closeTerminalSession, call:prev.filter, call:renameTerminalSession, call:prev.map, call:resetTerminalSession, call:loadSessions, call:useEffect | dep: react, ../api/terminal
- use-theme.ts | React hook that fetches user theme preference on mount and applies it to the document root element via data-theme attribute | exp: func:useTheme(), call:useEffect, call:getUserConfig, call:document.documentElement.removeAttribute, call:document.documentElement.setAttribute | dep: react, ../api/settings - use-theme.ts | React hook that fetches user theme preference on mount and applies it to the document root element via data-theme attribute | exp: func:useTheme(), call:useEffect, call:getUserConfig, call:document.documentElement.removeAttribute, call:document.documentElement.setAttribute | dep: react, ../api/settings
- use-tool-workshop.ts | React custom hook that manages state and operations for a tool workshop UI, including CRUD operations for tool types and tool definitions with form handling and validation. | exp: useToolWorkshop | dep: react, ../utils/errors, ../api/tool-types, ../api/tool-definitions, ../components/features/tool-workshop/ToolTypeEditorPanel - use-tool-workshop.ts | Custom React hook that manages state and operations for a tool workshop UI, including CRUD operations for tool types and tool definitions with form handling and validation. | exp: useToolWorkshop | dep: react, ../utils/errors, ../api/tool-types, ../api/tool-definitions, ../components/features/tool-workshop/ToolTypeEditorPanel
- use-virtual-keyboard.ts | React hook that detects virtual keyboard open/close state and measures its height on mobile devices | exp: func:useVirtualKeyboard(), call:useState, call:useCallback, call:setState, call:useEffect, call:visualViewport.addEventListener, call:window.addEventListener, call:updateKeyboardState, call:visualViewport.removeEventListener, call:window.removeEventListener | dep: react - use-virtual-keyboard.ts | React hook that detects virtual keyboard open/close state and measures its height on mobile devices | exp: func:useVirtualKeyboard(), call:useState, call:useCallback, call:setState, call:useEffect, call:visualViewport.addEventListener, call:window.addEventListener, call:updateKeyboardState, call:visualViewport.removeEventListener, call:window.removeEventListener | dep: react
- use-workspace-actions.ts | Provides a React hook that encapsulates workspace CRUD operations with loading state management and user confirmation dialogs for destructive actions. | exp: UseWorkspaceActionsResult, func:useWorkspaceActions() → UseWorkspaceActionsResult, call:useState, call:useCallback, call:createWorkspace, call:setLoadingId, call:deleteWorkspace, call:onRefresh, call:window.confirm, call:instances.map((i) => `- ${i.name}`).join, call:syncWorkspace, call:updateWorkspace, raise:err | dep: react, ../api/workspaces, ../types/workspace - use-workspace-actions.ts | Provides a React hook that encapsulates workspace CRUD operations with loading state management and user confirmation dialogs for destructive actions. | exp: UseWorkspaceActionsResult, func:useWorkspaceActions() → UseWorkspaceActionsResult, call:useState, call:useCallback, call:createWorkspace, call:setLoadingId, call:deleteWorkspace, call:onRefresh, call:window.confirm, call:instances.map((i) => `- ${i.name}`).join, call:syncWorkspace, call:updateWorkspace, raise:err | dep: react, ../api/workspaces, ../types/workspace
- use-workspace-files.ts | Custom React hook for managing workspace file operations including listing, loading, saving, and navigating files. | exp: UseWorkspaceFilesResult, func:useWorkspaceFiles(workspaceId: string) → UseWorkspaceFilesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceFiles, call:setEntries, call:setCurrentPath, call:setContent, call:getWorkspaceFileContent, call:saveWorkspaceFile, call:refresh, call:useEffect | dep: react, ../api/workspace-files - use-workspace-files.ts | Custom React hook for managing workspace file operations including listing, loading, saving, and navigating files. | exp: UseWorkspaceFilesResult, func:useWorkspaceFiles(workspaceId: string) → UseWorkspaceFilesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceFiles, call:setEntries, call:setCurrentPath, call:setContent, call:getWorkspaceFileContent, call:saveWorkspaceFile, call:refresh, call:useEffect | dep: react, ../api/workspace-files
@@ -31,7 +31,7 @@ A collection of custom React hooks providing reusable state management, API inte
- use-workspace-instances.ts | Custom React hook for managing workspace instances with CRUD operations, loading states, and error handling. | exp: UseWorkspaceInstancesResult, func:useWorkspaceInstances(workspaceId: string) → UseWorkspaceInstancesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceInstances, call:setInstances, call:createWorkspaceInstance, call:refresh, call:useEffect | dep: react, ../api/workspace-instances, ../api/sessions - use-workspace-instances.ts | Custom React hook for managing workspace instances with CRUD operations, loading states, and error handling. | exp: UseWorkspaceInstancesResult, func:useWorkspaceInstances(workspaceId: string) → UseWorkspaceInstancesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaceInstances, call:setInstances, call:createWorkspaceInstance, call:refresh, call:useEffect | dep: react, ../api/workspace-instances, ../api/sessions
- use-workspaces.ts | Custom React hook that fetches and manages workspace data with loading and error states. | exp: UseWorkspacesResult, func:useWorkspaces(projectId: string, repoId: string) → UseWorkspacesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaces, call:listAllWorkspaces, call:setWorkspaces, call:useEffect, call:refresh | dep: react, ../api/workspaces, ../types/workspace - use-workspaces.ts | Custom React hook that fetches and manages workspace data with loading and error states. | exp: UseWorkspacesResult, func:useWorkspaces(projectId: string, repoId: string) → UseWorkspacesResult, call:useState, call:useCallback, call:setLoading, call:setError, call:listWorkspaces, call:listAllWorkspaces, call:setWorkspaces, call:useEffect, call:refresh | dep: react, ../api/workspaces, ../types/workspace
## arch ## arch
Follows a feature-based composition pattern where each hook encapsulates a specific domain concern (data fetching, CRUD operations, terminal/session management, UI interactions), often combining React state with API calls, side effects, and provider context integration. Follows a custom hook composition pattern where each hook encapsulates a single domain concern (data fetching, CRUD operations, UI state, device detection), often combining useState/useEffect/useCallback with SWR-style async data management, and layering higher-level hooks (use-terminal-page) from lower-level ones (use-mobile-viewport, use-virtual-keyboard).
## tags ## tags
call:set, call:use, workspace, react, state, terminal, api, callback call:set, call:use, workspace, react, state, terminal, api, callback
## symbols ## symbols
+4 -2
View File
@@ -193,7 +193,7 @@ export const useConfigProfiles = () => {
setDragOverIndex(null); setDragOverIndex(null);
}; };
const handleSubmit = async (e?: React.FormEvent) => { const handleSubmit = async (e?: React.FormEvent): Promise<boolean> => {
e?.preventDefault(); e?.preventDefault();
setError(null); setError(null);
setSaveStatus("saving"); setSaveStatus("saving");
@@ -201,7 +201,7 @@ export const useConfigProfiles = () => {
if (!formData.name?.trim()) { if (!formData.name?.trim()) {
setError("Name is required"); setError("Name is required");
setSaveStatus("error"); setSaveStatus("error");
return; return false;
} }
try { try {
@@ -224,9 +224,11 @@ export const useConfigProfiles = () => {
const refreshed = (await listConfigProfiles()).find((p) => p.id === selectedProfile.id); const refreshed = (await listConfigProfiles()).find((p) => p.id === selectedProfile.id);
if (refreshed) populateForm(refreshed); if (refreshed) populateForm(refreshed);
} }
return true;
} catch (err) { } catch (err) {
setError(extractErrorMessage(err)); setError(extractErrorMessage(err));
setSaveStatus("error"); setSaveStatus("error");
return false;
} }
}; };
+7 -5
View File
@@ -151,13 +151,13 @@ export const useToolWorkshop = () => {
resetToolTypeForm(); resetToolTypeForm();
}; };
const handleToolTypeSubmit = async (e?: React.FormEvent) => { const handleToolTypeSubmit = async (e?: React.FormEvent): Promise<boolean> => {
e?.preventDefault(); e?.preventDefault();
setToolTypeError(null); setToolTypeError(null);
if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) { if (!toolTypeForm.name.trim() || !toolTypeForm.display_name.trim()) {
setToolTypeError("Name and display name are required"); setToolTypeError("Name and display name are required");
return; return false;
} }
if ( if (
@@ -166,7 +166,7 @@ export const useToolWorkshop = () => {
isNaN(Number(toolTypeForm.default_port))) isNaN(Number(toolTypeForm.default_port)))
) { ) {
setToolTypeError("Default port is required and must be a number"); setToolTypeError("Default port is required and must be a number");
return; return false;
} }
if (toolTypeForm.definition_type !== "manifest") { if (toolTypeForm.definition_type !== "manifest") {
@@ -179,13 +179,13 @@ export const useToolWorkshop = () => {
setToolTypeError( setToolTypeError(
`${toolTypeForm.definition_type === "compose" ? "Compose" : "Dockerfile"} template is required`, `${toolTypeForm.definition_type === "compose" ? "Compose" : "Dockerfile"} template is required`,
); );
return; return false;
} }
} else if (!manifestData) { } else if (!manifestData) {
setToolTypeError( setToolTypeError(
"Manifest data is required for manifest definition type", "Manifest data is required for manifest definition type",
); );
return; return false;
} }
const variables = toolTypeForm.required_variables const variables = toolTypeForm.required_variables
@@ -307,8 +307,10 @@ export const useToolWorkshop = () => {
setToolTypeDirty(false); setToolTypeDirty(false);
} }
await loadData(); await loadData();
return true;
} catch (err) { } catch (err) {
setToolTypeError(extractErrorMessage(err)); setToolTypeError(extractErrorMessage(err));
return false;
} }
}; };
+1 -1
View File
@@ -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's primary UI surfaces, each handling a specific domain area (dashboard, projects, workspaces, git, settings, etc.) with responsive layouts and CRUD operations. Contains top-level page components that render the main UI views for the web application, each corresponding to a distinct route and feature area.
## 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
+8 -8
View File
@@ -4,30 +4,30 @@ 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's primary UI surfaces, each handling a specific domain area (dashboard, projects, workspaces, git, settings, etc.) with responsive layouts and CRUD operations. Contains top-level page components that render the main UI views for the web application, each corresponding to a distinct route and feature area.
## 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 configuration profiles management page with responsive desktop/mobile layouts for creating, editing, previewing, and organizing 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, use-mobile-viewport, use-config-profiles, data-states, ConfigProfileListSidebar, ConfigProfileEditorPanel, ConfigProfilesMobileView
- 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.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
- PlaceholderPage.tsx | Exports three simple React page components (PlaceholderPage, NotFoundPage, LoginRedirectPage) for a frontend scaffold. | exp: PlaceholderPage, NotFoundPage, LoginRedirectPage | dep: ../components/icon, React - PlaceholderPage.tsx | Exports three simple React page components (PlaceholderPage, NotFoundPage, LoginRedirectPage) for a frontend scaffold. | exp: PlaceholderPage, NotFoundPage, LoginRedirectPage | dep: ../components/icon, React
- 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 renders a responsive profile page with form editing, avatar upload, and save functionality for both mobile and desktop viewports. | exp: ProfilePage | dep: react, ../api/profile, ../components/data-states, ../components/icon, ../components/features/profile/ProfileMobileView, ../state/auth, ../hooks/use-async-data, ../hooks/use-mobile-viewport
- 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 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 - 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, use-mobile-viewport, use-projects, data-states, icon, ProjectCard, ProjectDialog, RepositoryCreateDialog, MobileListView, MobileFAB
- 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
- TerminalPage.tsx | Renders a responsive terminal page that switches between mobile and desktop views based on device type, managing terminal sessions and their interactions. | exp: TerminalPage | dep: react, ../hooks/use-terminal-page, ../components/features/terminal/MobileTerminalView, ../components/features/terminal/DesktopTerminalView, useTerminalPage hook, MobileTerminalView, DesktopTerminalView - TerminalPage.tsx | Renders a responsive terminal page that switches between mobile and desktop views based on device type, managing terminal sessions and their interactions. | exp: TerminalPage | dep: react, ../hooks/use-terminal-page, ../components/features/terminal/MobileTerminalView, ../components/features/terminal/DesktopTerminalView, useTerminalPage hook, MobileTerminalView, DesktopTerminalView
- ToolWorkshopPage.tsx | Renders a responsive tool workshop page with sidebar/editor layout for desktop and tabbed mobile view for managing tool types | exp: ToolWorkshopPage | dep: ../components/data-states, ../hooks/use-mobile-viewport, ../hooks/use-tool-workshop, ../components/features/tool-workshop/ToolTypeListSidebar, ../components/features/tool-workshop/ToolTypeEditorPanel, ../components/features/tool-workshop/ToolWorkshopMobileView, react, use-mobile-viewport, use-tool-workshop, data-states, ToolTypeListSidebar, ToolTypeEditorPanel, ToolWorkshopMobileView - ToolWorkshopPage.tsx | Renders a responsive tool workshop page with list and editor views for managing tool types, handling loading/error states and mobile/desktop layouts | 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 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 - WorkspacesPage.tsx | Renders a responsive workspaces management page with mobile and desktop views supporting listing, creating, viewing details, syncing, deleting, and starting tools for workspaces. | 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 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. Route-based page organization with responsive mobile/desktop layout patterns, tabbed navigation for complex pages, polling/health-check mechanisms for live data, and CRUD operations with loading/error/empty states throughout.
## tags ## tags
page, components, workspace, features, react, mobile, settings, hooks page, components, workspace, features, mobile, react, hooks, settings
## symbols ## symbols
- WorkspaceDetailPage - WorkspaceDetailPage
- WorkspacesPage - WorkspacesPage
+25 -1
View File
@@ -6,7 +6,7 @@ import { ConfigProfileListSidebar } from "../components/features/config-profiles
import { ConfigProfileEditorPanel } from "../components/features/config-profiles/ConfigProfileEditorPanel"; import { ConfigProfileEditorPanel } from "../components/features/config-profiles/ConfigProfileEditorPanel";
import { ConfigProfilesMobileView } from "../components/features/config-profiles/ConfigProfilesMobileView"; import { ConfigProfilesMobileView } from "../components/features/config-profiles/ConfigProfilesMobileView";
type MobileView = "list" | "detail" | "edit"; type MobileView = "list" | "detail" | "edit" | "preview";
export const ConfigProfilesPage = () => { export const ConfigProfilesPage = () => {
const isMobile = useMobileViewport(); const isMobile = useMobileViewport();
@@ -100,11 +100,18 @@ export const ConfigProfilesPage = () => {
return ( return (
<ConfigProfilesMobileView <ConfigProfilesMobileView
profiles={profiles} profiles={profiles}
projects={projects}
toolTypes={toolTypes}
selectedProfile={selectedProfile} selectedProfile={selectedProfile}
mobileView={mobileView} mobileView={mobileView}
isCreating={isCreating} isCreating={isCreating}
formData={formData} formData={formData}
includedProfileIds={includedProfileIds}
availableProfiles={availableProfilesForInclude()}
previewData={previewData}
previewingId={previewingId}
saveStatus={saveStatus} saveStatus={saveStatus}
error={error}
onViewChange={setMobileView} onViewChange={setMobileView}
onSelect={handleSelectProfile} onSelect={handleSelectProfile}
onCreate={handleCreateNew} onCreate={handleCreateNew}
@@ -112,6 +119,23 @@ export const ConfigProfilesPage = () => {
onFormChange={updateFormField} onFormChange={updateFormField}
onSubmit={handleSubmit} onSubmit={handleSubmit}
getScopeLabel={getScopeLabel} getScopeLabel={getScopeLabel}
getIncludedProfile={getIncludedProfile}
onAddInclude={addInclude}
onRemoveInclude={removeInclude}
onAddEnvVar={addEnvVar}
onUpdateEnvVar={updateEnvVar}
onRemoveEnvVar={removeEnvVar}
onAddFile={addFile}
onUpdateFile={updateFile}
onRemoveFile={removeFile}
onAddMount={addMount}
onUpdateMount={updateMount}
onRemoveMount={removeMount}
onAddMountFile={addMountFile}
onUpdateMountFile={updateMountFile}
onRemoveMountFile={removeMountFile}
onPreview={handlePreview}
onClosePreview={() => setPreviewData(null)}
/> />
); );
} }
+21 -2
View File
@@ -3,13 +3,16 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { getProfile, updateProfile, uploadAvatar } from "../api/profile"; import { getProfile, updateProfile, uploadAvatar } from "../api/profile";
import { ErrorState, LoadingState } from "../components/data-states"; import { ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon"; import { Icon } from "../components/icon";
import { ProfileMobileView } from "../components/features/profile/ProfileMobileView";
import { useAuth } from "../state/auth"; import { useAuth } from "../state/auth";
import { useAsyncData } from "../hooks/use-async-data"; import { useAsyncData } from "../hooks/use-async-data";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
import type { UserProfile } from "../api/profile"; import type { UserProfile } from "../api/profile";
type ProfileStatus = "loading" | "ready" | "error" | "saving"; type ProfileStatus = "loading" | "ready" | "error" | "saving";
export const ProfilePage = () => { export const ProfilePage = () => {
const isMobile = useMobileViewport();
const { refreshSession } = useAuth(); const { refreshSession } = useAuth();
const { data: profile, status: loadStatus, reload } = useAsyncData<UserProfile>(getProfile, []); const { data: profile, status: loadStatus, reload } = useAsyncData<UserProfile>(getProfile, []);
const [displayStatus, setDisplayStatus] = useState<ProfileStatus>("loading"); const [displayStatus, setDisplayStatus] = useState<ProfileStatus>("loading");
@@ -90,13 +93,29 @@ export const ProfilePage = () => {
return ( return (
<section className="stack"> <section className="stack">
<h1>Profile</h1> {!isMobile && <h1>Profile</h1>}
{displayStatus === "loading" && <LoadingState message="Loading profile..." />} {displayStatus === "loading" && <LoadingState message="Loading profile..." />}
{displayStatus === "error" && <ErrorState message="Failed to load profile" onRetry={reload} />} {displayStatus === "error" && <ErrorState message="Failed to load profile" onRetry={reload} />}
{(displayStatus === "ready" || displayStatus === "saving") && profile && ( {(displayStatus === "ready" || displayStatus === "saving") && profile && (
isMobile ? (
<ProfileMobileView
profile={profile}
name={name}
email={email}
error={error}
isSaving={displayStatus === "saving"}
avatarUrl={avatarUrl}
fileInputRef={fileInputRef}
onNameChange={setName}
onEmailChange={setEmail}
onAvatarButtonClick={() => fileInputRef.current?.click()}
onAvatarChange={handleAvatarChange}
onSave={() => void handleSave()}
/>
) : (
<div className="card stack"> <div className="card stack">
<div className="profile-avatar-section"> <div className="profile-avatar-section">
<div className="avatar-preview"> <div className="avatar-preview">
@@ -178,7 +197,7 @@ export const ProfilePage = () => {
</button> </button>
</div> </div>
</div> </div>
)} ))}
</section> </section>
); );
}; };
+9 -1
View File
@@ -57,12 +57,20 @@ export const ToolWorkshopPage = () => {
mobileView={mobileView} mobileView={mobileView}
isCreating={isCreating} isCreating={isCreating}
toolTypeForm={toolTypeForm} toolTypeForm={toolTypeForm}
manifestData={manifestData}
manifestDefinitionId={manifestDefinitionId}
baseDefinitions={baseDefinitions}
toolTypeError={toolTypeError} toolTypeError={toolTypeError}
toolTypeDirty={toolTypeDirty}
onViewChange={setMobileView} onViewChange={setMobileView}
onSelect={handleSelectToolType} onSelect={handleSelectToolType}
onCreate={handleCreateNew} onCreate={handleCreateNew}
onDelete={handleDeleteToolType} onDelete={handleDeleteToolType}
onFormChange={handleFormChange} onFormChange={handleFormChange}
onManifestChange={(m) => {
setManifestData(m);
setToolTypeDirty(true);
}}
onSubmit={handleToolTypeSubmit} onSubmit={handleToolTypeSubmit}
onCancel={() => { onCancel={() => {
if (toolTypeDirty) { if (toolTypeDirty) {
@@ -70,7 +78,7 @@ export const ToolWorkshopPage = () => {
return; return;
} }
} }
setMobileView(isCreating ? "list" : "detail"); setMobileView("list");
}} }}
/> />
); );
+1 -1
View File
@@ -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, design tokens, utility classes, and component-specific styles. Provides the complete visual design system and styling foundation for the web application, including themes, tokens, utilities, 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 -4
View File
@@ -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, design tokens, utility classes, and component-specific styles. Provides the complete visual design system and styling foundation for the web application, including themes, tokens, utilities, 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 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) - utilities.css | Provides responsive CSS utility classes and component styles for a web application including layout, terminal, session navigation, and mobile-specific adaptations. | dep: CSS custom properties (--space-*, --border, --bg, --muted, --brand, --danger, --danger-light, --success, --text, --text-xs, --text-sm), xterm.js (terminal library)
## arch ## arch
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. Token-driven CSS architecture using CSS custom properties for light/dark theming, utility-first responsive patterns, and modular separation of concerns across global base styles, design tokens, syntax highlighting, and adaptive utilities.
## tags ## tags
space, global, css, web, application, syntax, defines, styles space, global, css, web, application, danger, syntax, text
## symbols ## symbols
- -
## workflows ## workflows
+563 -1
View File
@@ -439,6 +439,7 @@ a.nav-item,
min-height: 0; min-height: 0;
padding: var(--space-4); padding: var(--space-4);
gap: var(--space-4); gap: var(--space-4);
overflow: hidden;
} }
.terminal-page-header { .terminal-page-header {
@@ -457,6 +458,7 @@ a.nav-item,
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
max-height: 100%;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
@@ -549,6 +551,7 @@ a.nav-item,
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
} }
@@ -557,6 +560,8 @@ a.nav-item,
.terminal-container .xterm-viewport { .terminal-container .xterm-viewport {
touch-action: auto; touch-action: auto;
overscroll-behavior: auto; overscroll-behavior: auto;
max-height: 100% !important;
width: 100% !important;
} }
/* xterm.js manages its own positioning and sizing */ /* xterm.js manages its own positioning and sizing */
@@ -767,7 +772,9 @@ a.nav-item,
.terminal-instance { .terminal-instance {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
max-height: 100%;
display: none; display: none;
overflow: hidden;
} }
.terminal-instance.active { .terminal-instance.active {
@@ -2053,10 +2060,38 @@ a.nav-item,
.mobile-list-item-actions { .mobile-list-item-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-1);
flex-shrink: 0; flex-shrink: 0;
color: var(--muted); color: var(--muted);
} }
.mobile-list-item-action {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
background: transparent;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--muted);
cursor: pointer;
}
.mobile-list-item-action:active {
background: var(--bg);
}
.mobile-list-item-action-delete {
color: var(--danger);
border-color: var(--danger);
}
.mobile-list-item-action-delete:active {
background: var(--danger-light);
}
.mobile-list-empty { .mobile-list-empty {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -2109,6 +2144,198 @@ a.nav-item,
background: transparent; background: transparent;
} }
/* Mobile Page */
.mobile-page {
padding: var(--space-2);
padding-bottom: calc(var(--space-4) + 64px);
min-height: 100%;
}
/* Mobile Edit View */
.mobile-edit-view {
display: flex;
flex-direction: column;
min-height: 100%;
background: var(--bg);
}
.mobile-edit-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--panel);
border-bottom: 1px solid var(--border);
}
.mobile-edit-cancel,
.mobile-edit-save,
.mobile-edit-delete {
min-height: 44px;
padding: var(--space-2) var(--space-3);
border-radius: 10px;
font: inherit;
font-size: 0.9375rem;
cursor: pointer;
}
.mobile-edit-cancel {
background: transparent;
border: 1px solid var(--border);
color: var(--text);
}
.mobile-edit-save {
background: var(--brand);
border: 1px solid var(--brand);
color: white;
}
.mobile-edit-delete {
background: var(--danger-light);
border: 1px solid var(--danger);
color: var(--danger);
}
.mobile-edit-save:disabled,
.mobile-edit-cancel:disabled,
.mobile-edit-delete:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.mobile-edit-title {
font-size: 1rem;
font-weight: 600;
margin: 0;
flex: 1;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mobile-edit-form {
flex: 1;
overflow-y: auto;
padding: var(--space-3);
padding-bottom: calc(var(--space-3) + 148px);
}
/* Mobile Edit Actions (sticky bottom bar) */
.mobile-edit-actions {
position: fixed;
left: 0;
right: 0;
bottom: 64px;
z-index: 110;
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-3);
padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
background: var(--panel);
border-top: 1px solid var(--border);
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
}
.mobile-edit-actions .mobile-edit-save,
.mobile-edit-actions .mobile-edit-delete {
width: 100%;
justify-content: center;
min-height: 48px;
}
.mobile-edit-field {
margin-bottom: var(--space-3);
}
.mobile-edit-field-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: var(--space-2);
}
.mobile-edit-input,
.mobile-edit-textarea,
.mobile-edit-select {
width: 100%;
padding: var(--space-3);
border: 1px solid var(--border);
border-radius: 10px;
background: var(--panel);
color: var(--text);
font: inherit;
min-height: 44px;
}
.mobile-edit-textarea {
resize: vertical;
min-height: 96px;
}
.mobile-edit-code {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, monospace;
font-size: 0.8125rem;
}
.mobile-edit-checkbox {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
}
.mobile-edit-checkbox input {
width: 22px;
height: 22px;
accent-color: var(--brand);
}
/* Mobile Profile */
.profile-mobile-avatar-section {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
padding: var(--space-6) var(--space-3);
margin-bottom: var(--space-2);
}
.profile-mobile-avatar {
width: 96px;
height: 96px;
border-radius: 50%;
overflow: hidden;
background: var(--bg);
border: 2px solid var(--border);
flex-shrink: 0;
}
.profile-mobile-avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.profile-mobile-avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: 600;
color: var(--muted);
background: var(--bg);
}
/* Mobile Page Header */ /* Mobile Page Header */
.mobile-page-header { .mobile-page-header {
display: flex; display: flex;
@@ -2179,13 +2406,348 @@ a.nav-item,
/* Mobile form group (inline fields) */ /* Mobile form group (inline fields) */
.mobile-form-group { .mobile-form-group {
padding: var(--space-2); padding: var(--space-3);
background: var(--panel); background: var(--panel);
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--border); border: 1px solid var(--border);
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
} }
.mobile-form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: var(--space-2);
color: var(--text);
}
.mobile-form-input,
.mobile-form-select,
.mobile-form-textarea {
width: 100%;
padding: var(--space-3);
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg);
color: var(--text);
font: inherit;
min-height: 44px;
}
.mobile-form-textarea {
resize: vertical;
min-height: 96px;
}
.mobile-form-code {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, monospace;
font-size: 0.8125rem;
}
.mobile-form-select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
padding-right: 2.5rem;
}
.mobile-form-checkbox-label {
display: flex;
align-items: center;
gap: var(--space-2);
font-weight: 500;
cursor: pointer;
}
.mobile-form-checkbox {
width: 22px;
height: 22px;
min-width: 22px;
min-height: 22px;
accent-color: var(--brand);
cursor: pointer;
}
.mobile-form-help {
display: block;
font-size: 0.8125rem;
color: var(--muted);
margin-top: var(--space-1);
}
.mobile-form-error {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3);
background: var(--danger-light);
color: var(--danger);
border-radius: 10px;
font-size: 0.875rem;
margin: var(--space-2) 0;
}
.mobile-discard-button {
width: 100%;
justify-content: center;
margin-top: var(--space-2);
}
/* Manifest editor responsive overrides inside mobile forms */
.mobile-manifest-editor-wrapper .card {
padding: var(--space-3);
margin-bottom: var(--space-3);
}
.mobile-manifest-editor-wrapper .row {
flex-direction: column;
align-items: stretch;
gap: var(--space-2);
}
.mobile-manifest-editor-wrapper .form-group,
.mobile-manifest-editor-wrapper input,
.mobile-manifest-editor-wrapper select,
.mobile-manifest-editor-wrapper textarea {
width: 100%;
min-width: 0;
}
.mobile-manifest-editor-wrapper input[style*="width:"] {
width: 100% !important;
}
/* Mobile config profile form extras */
.mobile-form-group p {
margin: 0;
}
.mobile-form-row {
display: flex;
align-items: center;
}
.mobile-form-section {
padding: var(--space-3);
background: var(--panel);
border-radius: 8px;
border: 1px solid var(--border);
margin-bottom: var(--space-2);
}
.mobile-form-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-3);
}
.mobile-form-section-header h4 {
margin: 0;
font-size: 1rem;
}
.mobile-add-button {
width: 100%;
justify-content: center;
margin-top: var(--space-2);
}
.mobile-form-success {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3);
background: var(--success-light);
color: var(--success);
border-radius: 10px;
font-size: 0.875rem;
margin: var(--space-2) 0;
}
.mobile-list-plain {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.mobile-list-plain li {
padding: var(--space-2);
background: var(--bg);
border-radius: 6px;
font-size: 0.875rem;
}
/* Mobile detail extra actions */
.mobile-detail-actions-extra {
padding: var(--space-3);
padding-top: 0;
margin-top: calc(var(--space-2) * -1);
}
.mobile-detail-actions-extra .secondary-button {
width: 100%;
justify-content: center;
}
/* Mobile includes */
.mobile-includes-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.mobile-include-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
}
.mobile-include-name {
flex: 1;
font-weight: 500;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mobile-include-scope {
font-size: 0.75rem;
padding: 0.125rem 0.5rem;
background: var(--bg);
color: var(--muted);
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.025em;
}
/* Mobile key/value rows */
.mobile-key-value-row {
display: flex;
gap: var(--space-2);
align-items: center;
margin-bottom: var(--space-2);
}
.mobile-key-value-row .mobile-form-input {
flex: 1;
}
/* Mobile file and mount cards */
.mobile-file-card,
.mobile-mount-card {
padding: var(--space-3);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: var(--space-2);
}
.mobile-file-header,
.mobile-mount-header {
display: flex;
gap: var(--space-2);
align-items: center;
margin-bottom: var(--space-2);
}
.mobile-file-header .mobile-form-input,
.mobile-mount-header .mobile-form-input {
flex: 1;
}
.mobile-mount-files {
padding-left: var(--space-3);
border-left: 2px solid var(--border);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.mobile-mount-file-row {
display: flex;
gap: var(--space-2);
align-items: flex-start;
}
.mobile-mount-file-row .mobile-form-input,
.mobile-mount-file-row .mobile-form-textarea {
flex: 1;
}
/* Mobile preview */
.mobile-preview-code {
background: var(--code-bg, #1e1e1e);
color: var(--code-fg, #d4d4d4);
padding: var(--space-3);
border-radius: 8px;
overflow-x: auto;
font-size: 0.8125rem;
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, monospace;
margin: var(--space-2) 0 0 0;
}
.mobile-preview-item {
padding: var(--space-3);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: var(--space-2);
font-size: 0.875rem;
}
.mobile-preview-item strong {
display: block;
margin-bottom: var(--space-2);
word-break: break-all;
}
.mobile-preview-mapping {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, monospace;
font-size: 0.8125rem;
color: var(--muted);
margin-top: var(--space-1);
}
/* Mobile git mount editor overrides */
.mobile-form-section .git-mount-editor .card {
padding: var(--space-3);
margin-bottom: var(--space-2);
}
.mobile-form-section .git-mount-editor .form-row {
flex-direction: column;
align-items: stretch;
gap: var(--space-2);
}
.mobile-form-section .git-mount-editor input,
.mobile-form-section .git-mount-editor select,
.mobile-form-section .git-mount-editor textarea {
width: 100%;
}
.mobile-form-section .git-mount-editor .form-actions {
flex-direction: row;
}
.mobile-form-section .git-mount-editor button {
min-height: 44px;
}
/* Ensure minimum touch targets on mobile */ /* Ensure minimum touch targets on mobile */
button, button,
a, a,
+1 -1
View File
@@ -2,7 +2,7 @@
dir: docs dir: docs
## role ## role
Provides user-facing documentation and knowledge base for the Headquarter platform. Central documentation hub and navigation entry point for the Headquarter platform.
## parent ## parent
index: ./.pi-map.index.md index: ./.pi-map.index.md
map: ./.pi-map.md map: ./.pi-map.md
+2 -2
View File
@@ -4,11 +4,11 @@ dir: docs
index: docs/.pi-map.index.md index: docs/.pi-map.index.md
## role ## role
Provides user-facing documentation and knowledge base for the Headquarter platform. Central documentation hub and knowledge base for the Headquarter platform.
## files ## files
- README.md | Serves as the main index and navigation hub for the Headquarter platform documentation, organizing links to feature guides, API docs, architecture, deployment, and development resources. - README.md | Serves as the main index and navigation hub for the Headquarter platform documentation, organizing links to feature guides, API docs, architecture, deployment, and development resources.
## arch ## arch
Static documentation site using markdown files organized hierarchically with a central README index. Static documentation organization using hierarchical markdown files with cross-referenced navigation structure.
## tags ## tags
readme, serves, main, index, navigation, hub, headquarter, platform readme, serves, main, index, navigation, hub, headquarter, platform
## symbols ## symbols
+4 -1
View File
@@ -2,11 +2,14 @@
dir: docs/superpowers dir: docs/superpowers
## role ## role
Documentation directory for specialized or advanced capabilities/features of the project. Documentation for specialized capabilities or advanced features of the project.
## parent ## parent
index: docs/.pi-map.index.md index: docs/.pi-map.index.md
map: docs/.pi-map.md map: docs/.pi-map.md
## children ## children
- docs/superpowers/handoffs
index: docs/superpowers/handoffs/.pi-map.index.md
map: docs/superpowers/handoffs/.pi-map.md
- docs/superpowers/plans - docs/superpowers/plans
index: docs/superpowers/plans/.pi-map.index.md index: docs/superpowers/plans/.pi-map.index.md
map: docs/superpowers/plans/.pi-map.md map: docs/superpowers/plans/.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: docs/superpowers
index: docs/superpowers/.pi-map.index.md index: docs/superpowers/.pi-map.index.md
## role ## role
Documentation directory for specialized or advanced capabilities/features of the project. Documentation for advanced or specialized features and capabilities of the project.
## files ## files
## arch ## arch
Static documentation storage with no executable code architecture; likely contains markdown or text files describing enhanced functionality. Flat documentation structure with topic-based markdown files describing extended functionality beyond core features.
## tags ## tags
- -
## symbols ## symbols
@@ -0,0 +1,19 @@
# docs/superpowers/handoffs (index)
dir: docs/superpowers/handoffs
## role
Documents infrastructure migration procedures for development environment tooling.
## parent
index: docs/superpowers/.pi-map.index.md
map: docs/superpowers/.pi-map.md
## children
-
## files
- tool-container-home-directory.md
## links
index: docs/superpowers/handoffs/.pi-map.index.md
map: docs/superpowers/handoffs/.pi-map.md
## workflows
-
## dirty
-
+19
View File
@@ -0,0 +1,19 @@
# docs/superpowers/handoffs
dir: docs/superpowers/handoffs
index: docs/superpowers/handoffs/.pi-map.index.md
## role
Documents infrastructure migration procedures for development environment tooling.
## files
- tool-container-home-directory.md | Documents a multi-phase implementation plan for changing tool container home directories from `/workspace` to `/home/user` while preserving repo root directory names in mount targets and maintaining backward compatibility. | dep: alembic, docker, python, pydantic/sqlalchemy models, manifest compiler, instance service, config profile resolver, permission fixer
## arch
Technical documentation following phased implementation pattern with backward compatibility considerations.
## tags
home, tool, container, directory, documents, multi, phase, implementation
## symbols
-
## workflows
-
## dirty
-
@@ -0,0 +1,91 @@
# Handoff: Tool Container Home Directory
## Session context
- Original request: make tool container workspace/home directory default to `/home/user`, keep it user-writable, and preserve repo root directory name in the mount target.
- Completed a Grill Me session that resolved design decisions.
- Phase 1 implementation was started but blocked by persistent Grill Me read-only guard.
## Current branch
- `feat/tool-container-home-directory` (created from `dev`)
## Committed artifacts on `dev`
- `docs: tool container home directory design, plan, and test plan`
- `chore: update project maps for tool container home directory artifacts`
## Files already created/edited on `feat/tool-container-home-directory`
- `apps/api/src/models/tool/tool_type.py`
- Added `home_directory: Mapped[str]` column, default `"/home/user"`.
- `apps/api/alembic/versions/2026_06_14_104415_add_tool_type_home_directory.py`
- Adds column, sets existing rows to `/home/user`.
- Rewrites `/workspace` to `/home/user/{{WORKSPACE_NAME}}` in `compose_template` and `dockerfile_template`.
- Provides reversible downgrade.
## Design decisions resolved
| Area | Decision |
|------|----------|
| Config surface | `home_directory` field on ToolType / manifest schema |
| Default | `/home/user` |
| Runtime user | Still `manifest.user.name` |
| Mount target | `{home_directory}/{repo_name}` or `{home_directory}/{workspace_name}` |
| Precedence | Explicit manifest repo mount target wins; `home_directory` fills in default |
| Migration | Alembic data migration for legacy ToolType templates |
| Compatibility | `/workspace` symlink to actual repo/workspace target |
| Permissions | Entrypoint permission fixer at container startup |
| Tests | Unit + integration tests |
## Pending work
### Phase 1 verification
Run in `apps/api`:
```bash
python -m alembic current
python -m alembic upgrade head
python -m alembic downgrade -1
python -m alembic upgrade head
```
### Phase 2: Manifest compiler
File: `apps/api/src/services/build/manifest_compiler.py`
- Honor `manifest.home_directory` in `get_manifest_home_dir()`.
- Set `ENV HOME={home_directory}`, `ENV USER={user.name}`, `WORKDIR {home_directory}`.
- Create `/workspace` symlink step.
- Use `{home_directory}/{repo_name}` as default repo mount target when no explicit repo mount exists.
### Phase 3: Legacy instance generation
File: `apps/api/src/services/tool/instance_service.py`
- Use `tool_type.home_directory` for `dockerfile` tool mounts.
- Add `WORKSPACE_NAME`/`HOME_DIRECTORY` template variables for compose tools.
### Phase 4: Config-profile / git mount expansion
Files: `apps/api/src/services/tool/instance_service.py`, `apps/api/src/services/config/config_profile_resolver.py`
- Thread resolved `home_dir` through startup.
### Phase 5: Entrypoint permission fixer
Files: `apps/api/src/services/build/manifest_compiler.py`, `apps/api/src/services/shared/permission_fixer.py`, `tool-images/base.dockerfile`
- Generate startup script that chowns `{home_directory}` and key mounts to container user.
### Phase 6: Tests
Create/expand:
- `apps/api/tests/unit/test_home_path_expansion.py`
- `apps/api/tests/unit/test_manifest_compiler.py`
- `apps/api/tests/unit/test_instance_service.py`
- `apps/api/tests/unit/test_alembic_migrations.py`
- `apps/api/tests/integration/test_tool_instance_lifecycle.py`
## Reference docs
- `openspec/designs/tool-container-home-directory.md`
- `docs/superpowers/plans/tool-container-home-directory.md`
- `docs/superpowers/specs/tool-container-home-directory-test-plan.md`
- `openspec/tasks/home-path-expansion.md`
## Next action for fresh session
1. Ensure you are on `feat/tool-container-home-directory`.
2. Verify Phase 1 migration round-trip.
3. Continue with Phase 2 (manifest compiler).
+2 -1
View File
@@ -2,7 +2,7 @@
dir: docs/superpowers/plans dir: docs/superpowers/plans
## role ## role
Contains implementation planning documents for core infrastructure, authentication, database modeling, and UI feature development. Contains dated implementation planning documents that specify step-by-step technical approaches for major infrastructure, authentication, database, and UI feature development.
## parent ## parent
index: docs/superpowers/.pi-map.index.md index: docs/superpowers/.pi-map.index.md
map: docs/superpowers/.pi-map.md map: docs/superpowers/.pi-map.md
@@ -13,6 +13,7 @@ map: docs/superpowers/.pi-map.md
- 2026-05-17-auth-oauth.md - 2026-05-17-auth-oauth.md
- 2026-05-17-database-models.md - 2026-05-17-database-models.md
- 2026-05-24-session-branch-selection.md - 2026-05-24-session-branch-selection.md
- tool-container-home-directory.md
## links ## links
index: docs/superpowers/plans/.pi-map.index.md index: docs/superpowers/plans/.pi-map.index.md
map: docs/superpowers/plans/.pi-map.md map: docs/superpowers/plans/.pi-map.md
+4 -3
View File
@@ -4,16 +4,17 @@ dir: docs/superpowers/plans
index: docs/superpowers/plans/.pi-map.index.md index: docs/superpowers/plans/.pi-map.index.md
## role ## role
Contains implementation planning documents for core infrastructure, authentication, database modeling, and UI feature development. Contains dated implementation planning documents that specify step-by-step technical approaches for major infrastructure, authentication, database, and UI feature development.
## files ## files
- 2024-05-16-docker-infrastructure.md | Defines a step-by-step implementation plan for setting up a complete Docker-based development infrastructure with multi-service orchestration including PostgreSQL, Redis, Traefik, Authentik, FastAPI backend, and React frontend. | dep: Docker, Docker Compose, Make, Python 3.11, Node.js 20, PostgreSQL, Redis, Traefik, Authentik, FastAPI, React, Vite, nginx, alembic, uvicorn, SQLAlchemy, pydantic, structlog - 2024-05-16-docker-infrastructure.md | Defines a step-by-step implementation plan for setting up a complete Docker-based development infrastructure with multi-service orchestration including PostgreSQL, Redis, Traefik, Authentik, FastAPI backend, and React frontend. | dep: Docker, Docker Compose, Make, Python 3.11, Node.js 20, PostgreSQL, Redis, Traefik, Authentik, FastAPI, React, Vite, nginx, alembic, uvicorn, SQLAlchemy, pydantic, structlog
- 2026-05-17-auth-oauth.md | Implementation plan for adding Authentik-backed OIDC authentication with JWT access tokens and DB-backed refresh tokens to a FastAPI application | dep: FastAPI, SQLAlchemy, Alembic, python-jose, httpx, pytest, Authentik, PostgreSQL - 2026-05-17-auth-oauth.md | Implementation plan for adding Authentik-backed OIDC authentication with JWT access tokens and DB-backed refresh tokens to a FastAPI application | dep: FastAPI, SQLAlchemy, Alembic, python-jose, httpx, pytest, Authentik, PostgreSQL
- 2026-05-17-database-models.md | Define an implementation plan for building SQLAlchemy 2.0 async database models, Alembic migrations, and seed data for a FastAPI backend. | dep: SQLAlchemy 2.x, asyncpg, Alembic, pytest, pytest-asyncio, Pydantic Settings, PostgreSQL, FastAPI - 2026-05-17-database-models.md | Define an implementation plan for building SQLAlchemy 2.0 async database models, Alembic migrations, and seed data for a FastAPI backend. | dep: SQLAlchemy 2.x, asyncpg, Alembic, pytest, pytest-asyncio, Pydantic Settings, PostgreSQL, FastAPI
- 2026-05-24-session-branch-selection.md | Implementation plan for replacing free-text branch input with a dropdown of available branches and adding "Create new branch" functionality in a session creation form | dep: React, TypeScript, FastAPI, Python, Git subprocess, Pydantic, API client - 2026-05-24-session-branch-selection.md | Implementation plan for replacing free-text branch input with a dropdown of available branches and adding "Create new branch" functionality in a session creation form | dep: React, TypeScript, FastAPI, Python, Git subprocess, Pydantic, API client
- tool-container-home-directory.md | Implementation plan to make `/home/user` the configurable default home directory for tool containers while preserving repository name in mount targets, migrating legacy tool types, and maintaining `/workspace` backward compatibility symlink. | dep: Alembic, Docker, Compose, manifest compiler, instance service, config profile resolver, permission fixer, base dockerfile, pytest
## arch ## arch
Markdown-based technical specification documents using date-prefixed naming convention, each detailing phased implementation steps for specific system capabilities with technology stack integration points. Document-driven planning architecture using chronological Markdown specifications with precise technical scope, dependencies, and migration strategies for incremental system evolution.
## tags ## tags
05, fastapi, 2026, implementation, plan, docker, postgresql, authentik 05, fastapi, 2026, implementation, plan, docker, alembic, postgresql
## symbols ## symbols
- -
## workflows ## workflows
@@ -0,0 +1,171 @@
# Implementation Plan: Tool Container Home Directory
## Overview
Make `/home/user` the configurable, default workspace/home directory for all tool containers, preserve the repository/workspace directory name in the mount target, migrate legacy tool types, and keep `/workspace` as a compatibility symlink.
## Goals
- Add a `home_directory` field to `ToolType` and the manifest schema.
- Use that field to control workspace mount target, `HOME`, `WORKDIR`, and `~`/`$HOME` expansion.
- Preserve `{repo_name}` / `{workspace_name}` in the mount target.
- Migrate legacy Dockerfile/Compose templates via Alembic.
- Keep `/workspace` symlink for backward compatibility.
- Add an entrypoint permission fixer for runtime-mounted paths.
- Validate with unit and integration tests.
## Phases
### Phase 1: Database and model changes
**Files:**
- `apps/api/src/models/tool/tool_type.py`
- `apps/api/alembic/versions/<new>_add_tool_type_home_directory.py`
**Tasks:**
1. Add `home_directory: Mapped[str]` column to `ToolType`, non-nullable with server default `"/home/user"`.
2. Create Alembic migration that:
- Adds the column.
- Updates existing rows to `/home/user`.
- Rewrites `compose_template` and `dockerfile_template` to replace `/workspace` with `/home/user/{{WORKSPACE_NAME}}` (or a compatible template variable).
3. Provide downgrade that reverses template rewrites and drops the column.
**Quality gate:**
- `cd apps/api && alembic upgrade head` succeeds.
- `alembic downgrade -1` succeeds and restores `/workspace` strings.
- Existing tests still pass.
### Phase 2: Manifest schema and compiler
**Files:**
- `apps/api/src/services/build/manifest_compiler.py`
- `apps/api/src/services/tool/instance_service.py`
- `apps/api/src/api/tool/tool_types_validation.py` (if schema validation is added)
**Tasks:**
1. Accept `home_directory` in the manifest schema (optional; fallback to ToolType value).
2. In `compile_dockerfile()`:
- Set `ENV HOME={home_directory}` and `ENV USER={user.name}`.
- Set `WORKDIR {home_directory}` (unless `runtime.working_dir` is present).
- Create the home directory and pre-create `.config`, `.local/share`, `.cache` under it.
- Add a step to create `/workspace` as a symlink to `{home_directory}/{repo_name}` (placeholder or startup-time).
3. In `compile_compose()`:
- Use `{home_directory}/{repo_name}` as the default repo mount target when the manifest has no explicit repo mount.
- Keep `~`/`$HOME` expansion base equal to `home_directory`.
4. Update `get_manifest_home_dir()` to honor `manifest.home_directory` before deriving from `user.name`.
5. Pass `home_directory` through the manifest-based instance lifecycle.
**Quality gate:**
- `compile_dockerfile()` output contains `ENV HOME=/home/user` and `WORKDIR /home/user` for default manifests.
- `compile_compose()` output mounts repo at `/home/user/{repo_name}` when no explicit repo mount exists.
### Phase 3: Legacy instance generation
**Files:**
- `apps/api/src/services/tool/instance_service.py`
- `apps/api/src/services/docker/compose.py`
**Tasks:**
1. In `create_tool_instance()` for `definition_type == "dockerfile"`:
- Read `tool_type.home_directory` (default `/home/user`).
- Mount `{repo_path}:{home_directory}/{repo_name}` instead of `{repo_path}:/workspace`.
- Generate or adjust Dockerfile/Compose to create `/workspace` symlink.
2. For `definition_type == "compose"`:
- Render `{home_directory}` and `{WORKSPACE_NAME}` into the template.
- Validate that `{WORKSPACE_NAME}` is available as a template variable.
3. Add `WORKSPACE_NAME` to the render variables in `render_compose_template()`.
**Quality gate:**
- Legacy `dockerfile` instance compose mounts repo at `/home/user/{repo_name}`.
- Legacy `compose` template with `/home/user/{{WORKSPACE_NAME}}` renders correctly.
### Phase 4: Config-profile and git mount expansion
**Files:**
- `apps/api/src/services/config/config_profile_resolver.py`
- `apps/api/src/services/tool/instance_service.py`
**Tasks:**
1. Ensure `expand_container_path()` uses the resolved `home_directory` (already present; verify it is threaded through).
2. In `start_tool_instance()`, compute `home_dir` from ToolType/manifest and pass it to:
- `apply_resolved_profile()`
- `resolve_git_mounts()` / `resolve_git_mount_mappings()`
3. Confirm workspace/repo name is used as the mount target, not a generic `workspace` string.
**Quality gate:**
- Config profile mount target `~/config` expands to `/home/user/config`.
- Git mount target `~/repo` expands to `/home/user/repo`.
### Phase 5: Entrypoint permission fixer
**Files:**
- `apps/api/src/services/build/manifest_compiler.py`
- `apps/api/src/services/shared/permission_fixer.py`
- `tool-images/base.dockerfile` or generated entrypoint
**Tasks:**
1. Generate an entrypoint script that, before switching to the runtime user:
- Detects the container user name/uid.
- Runs `chown` on `{home_directory}` and key mount points.
- Creates `/workspace` symlink if it does not yet exist.
- Avoids recursive chown of large subtrees; target top-level dirs and runtime-created files.
2. Ensure manifest-generated Dockerfiles install `sudo` and configure passwordless sudo for the runtime user (already partially done).
3. Consider updating `tool-images/base.dockerfile` to include `sudo` and an entrypoint hook, or keep the fixer entirely in generated images.
**Quality gate:**
- Container starts successfully.
- Container user can write to `{home_directory}` and mounted config/git directories.
- `/workspace` symlink resolves to the repo/workspace directory.
### Phase 6: Tests
**Files:**
- `apps/api/tests/unit/test_home_path_expansion.py`
- `apps/api/tests/unit/test_manifest_compiler.py`
- `apps/api/tests/unit/test_instance_service.py` (new or expanded)
- `apps/api/tests/integration/test_tool_instance_lifecycle.py` (new or expanded)
**Tasks:**
1. Unit tests:
- `expand_container_path` with `~`, `$HOME`, absolute, and relative paths.
- `get_manifest_home_dir` with and without `home_directory`, with and without `user.name`.
- `compile_dockerfile` includes correct `ENV HOME`, `WORKDIR`, and `/workspace` symlink step.
- `compile_compose` defaults repo mount to `/home/user/{repo_name}` when no explicit repo mount exists.
- Explicit manifest repo mount target is preserved.
2. Integration tests:
- Create a manifest tool instance and verify the running container has `HOME=/home/user`, repo at `/home/user/{repo_name}`, and `/workspace` symlink.
- Verify a config-profile mount under `~` is writable by the container user.
- Verify git mount under `~` is writable.
- Verify legacy dockerfile tool uses `/home/user/{repo_name}` after migration.
**Quality gate:**
- `cd apps/api && pytest tests/unit/... tests/integration/... -xvs` passes.
- `cd apps/web && npm run typecheck` passes.
## Dependencies
- The existing `home-path-expansion` helpers (`expand_container_path`) must already be in place.
- Alembic migration infrastructure must be working.
- Manifest compiler must support generated entrypoints (already partially implemented).
## Risks and Mitigations
| Risk | Mitigation |
|------|-----------|
| Alembic migration rewrites commands/env vars that contain `/workspace` | Scope the replacement to volume mount lines only; add tests for edge cases. |
| `/workspace` symlink target missing at build time | Create placeholder directory in Dockerfile; finalize symlink in entrypoint at startup. |
| Entrypoint chown slow on large repos | Chown top-level directories only; rely on runtime user for new files. |
| Base image `USER user` cannot run sudo | Either install `sudo` in base image or keep entrypoint running as root before `su`/`gosu` to runtime user. |
| Legacy templates without `{{WORKSPACE_NAME}}` break | Migration injects the variable; validation rejects missing required variables. |
## OpenSpec Task Update
After this plan is accepted, update or replace `openspec/tasks/home-path-expansion.md` to reflect the expanded scope (configurable `home_directory`, repo-name mount target, migration, compatibility symlink, permission fixer, tests).
## Rollout
1. Merge Phase 1 (schema + migration) first so the column exists.
2. Merge Phase 2 and 3 (compiler + legacy generation) next.
3. Merge Phase 4 and 5 (profile/git mounts + permission fixer).
4. Merge Phase 6 (tests) with the previous phases as appropriate.
5. Run migration in production during a maintenance window; validate a few existing tool types before full rollout.
+2 -1
View File
@@ -2,7 +2,7 @@
dir: docs/superpowers/specs dir: docs/superpowers/specs
## role ## role
Contains design specification documents for planned system features, serving as architectural decision records for authentication and session management capabilities. Contains design specifications and test plans for upcoming features ("superpowers") being planned for the project.
## parent ## parent
index: docs/superpowers/.pi-map.index.md index: docs/superpowers/.pi-map.index.md
map: docs/superpowers/.pi-map.md map: docs/superpowers/.pi-map.md
@@ -11,6 +11,7 @@ map: docs/superpowers/.pi-map.md
## files ## files
- 2026-05-17-auth-oauth-design.md - 2026-05-17-auth-oauth-design.md
- 2026-05-24-session-branch-selection-design.md - 2026-05-24-session-branch-selection-design.md
- tool-container-home-directory-test-plan.md
## links ## links
index: docs/superpowers/specs/.pi-map.index.md index: docs/superpowers/specs/.pi-map.index.md
map: docs/superpowers/specs/.pi-map.md map: docs/superpowers/specs/.pi-map.md
+4 -3
View File
@@ -4,14 +4,15 @@ dir: docs/superpowers/specs
index: docs/superpowers/specs/.pi-map.index.md index: docs/superpowers/specs/.pi-map.index.md
## role ## role
Contains design specification documents for planned system features, serving as architectural decision records for authentication and session management capabilities. Contains design specifications and test plans for upcoming features ("superpowers") being planned for the project.
## files ## files
- 2026-05-17-auth-oauth-design.md | Design document for implementing OAuth2/OIDC authentication with Authentik, internal JWT access tokens, and database-backed refresh token rotation. | dep: Authentik, JWKS, JWT, database/PostgreSQL, cookies, pytest, mypy, ruff - 2026-05-17-auth-oauth-design.md | Design document for implementing OAuth2/OIDC authentication with Authentik, internal JWT access tokens, and database-backed refresh token rotation. | dep: Authentik, JWKS, JWT, database/PostgreSQL, cookies, pytest, mypy, ruff
- 2026-05-24-session-branch-selection-design.md | Design document for replacing free-text branch input with a dropdown of available branches and adding the ability to create a new local branch at clone time in a session creation UI. | dep: React/TypeScript frontend, Python/FastAPI backend, Git, API client, ToolInstance database model - 2026-05-24-session-branch-selection-design.md | Design document for replacing free-text branch input with a dropdown of available branches and adding the ability to create a new local branch at clone time in a session creation UI. | dep: React/TypeScript frontend, Python/FastAPI backend, Git, API client, ToolInstance database model
- tool-container-home-directory-test-plan.md | Test plan and QA checklist for validating configurable home directory behavior in tool containers, covering manifest compilation, path expansion, database migrations, and mount target resolution. | dep: pytest, alembic, docker, compose, manifest compiler, instance service, ToolType, config profiles, git mounts
## arch ## arch
Markdown-based technical design documentation following a chronological naming convention (YYYY-MM-DD-feature-name-design.md) with no executable code, using a lightweight specification format for capturing requirements, decisions, and implementation plans before development. Document-driven architecture using dated Markdown design documents for feature specifications, plus standalone test plans for QA validation of complex features.
## tags ## tags
design, 2026, 05, branch, session, document, authentik, jwt design, 2026, 05, branch, database, session, test, plan
## symbols ## symbols
- -
## workflows ## workflows
@@ -0,0 +1,114 @@
# Test Plan / QA Checklist: Tool Container Home Directory
## Scope
This plan covers validation of the configurable `home_directory` feature for tool containers, including:
- `home_directory` field on `ToolType` and manifest schema.
- Repository/workspace mount target using `{home_directory}/{repo_name}` or `{home_directory}/{workspace_name}`.
- Manifest compiler behavior (`HOME`, `WORKDIR`, `/workspace` symlink).
- Legacy tool type migration via Alembic.
- Config-profile and git-mount `~`/`$HOME` expansion.
- Entrypoint permission fixer.
## Unit Tests
### `apps/api/tests/unit/test_home_path_expansion.py`
| ID | Test | Expected Result |
|----|------|-----------------|
| U1 | `expand_container_path("~/config", "/home/user")` | returns `/home/user/config` |
| U2 | `expand_container_path("~", "/home/user")` | returns `/home/user` |
| U3 | `expand_container_path("$HOME/config", "/home/user")` | returns `/home/user/config` |
| U4 | `expand_container_path("$HOME", "/home/user")` | returns `/home/user` |
| U5 | `expand_container_path("/opt/data", "/home/user")` | returns `/opt/data` unchanged |
| U6 | `expand_container_path("relative/path", "/home/user")` | returns `relative/path` unchanged |
| U7 | `expand_container_path("path/$HOME/other", "/home/user")` | returns `path/$HOME/other` unchanged (mid-string not expanded) |
### `apps/api/tests/unit/test_manifest_compiler.py`
| ID | Test | Expected Result |
|----|------|-----------------|
| U10 | `get_manifest_home_dir({"home_directory": "/home/dev"})` | returns `/home/dev` |
| U11 | `get_manifest_home_dir({"user": {"name": "user"}})` | returns `/home/user` |
| U12 | `get_manifest_home_dir({"user": {"name": "dev"}})` | returns `/home/dev` |
| U13 | `get_manifest_home_dir({})` | returns `/root` |
| U14 | `compile_dockerfile()` for manifest with `home_directory: /home/user` and `user.name: user` | Dockerfile contains `ENV HOME=/home/user`, `ENV USER=user`, `WORKDIR /home/user`, and creates `/workspace` symlink |
| U15 | `compile_dockerfile()` for manifest without user block | Dockerfile does not create user and defaults `HOME=/root` |
| U16 | `compile_compose()` with no explicit repo mount and `home_directory: /home/user` | Compose mounts `{repo_path}:/home/user/{repo_name}` |
| U17 | `compile_compose()` with explicit repo mount target `/custom/path` | Compose preserves `/custom/path`; `/home/user` default is not injected |
### `apps/api/tests/unit/test_instance_service.py` (new or expanded)
| ID | Test | Expected Result |
|----|------|-----------------|
| U20 | `create_tool_instance()` for `definition_type == "dockerfile"` | Compose mounts `{repo_path}:/home/user/{repo_name}` |
| U21 | `create_tool_instance()` for `definition_type == "compose"` with migrated template | Template renders `/home/user/{{WORKSPACE_NAME}}` correctly |
| U22 | `start_tool_instance()` passes `home_dir` to `apply_resolved_profile()` | Config-profile `~/config` expands to `/home/user/config` |
| U23 | `start_tool_instance()` passes `home_dir` to `resolve_git_mounts()` | Git mount `~/repo` expands to `/home/user/repo` |
### `apps/api/tests/unit/test_alembic_migrations.py` (new or expanded)
| ID | Test | Expected Result |
|----|------|-----------------|
| U30 | Upgrade adds `home_directory` column with default `/home/user` | Column exists and legacy rows have value `/home/user` |
| U31 | Upgrade rewrites `compose_template` containing `/workspace` | Template now uses `/home/user/{{WORKSPACE_NAME}}` |
| U32 | Downgrade restores `/workspace` strings | Original templates are restored |
| U33 | Downgrade drops `home_directory` column | Column no longer exists |
## Integration Tests
### `apps/api/tests/integration/test_tool_instance_lifecycle.py` (new or expanded)
| ID | Test | Steps | Expected Result |
|----|------|-------|-----------------|
| I1 | Manifest tool starts with `/home/user` home | Create manifest tool type with default `home_directory`; create and start instance | Container has `HOME=/home/user`, repo at `/home/user/{repo_name}`, `/workspace` symlink resolves correctly |
| I2 | Manifest tool with overridden `home_directory` | Set `home_directory: /home/dev` in manifest; create and start instance | Container has `HOME=/home/dev`, repo at `/home/dev/{repo_name}` |
| I3 | Container user can write under `/home/user` | Start instance; exec `touch /home/user/test-file` as container user | Command succeeds and file is owned by container user |
| I4 | Config-profile mount under `~` is writable | Create profile with mount `{"target": "~/config", "files": {"settings.json": "{}"}}`; start instance with profile | File appears at `/home/user/config/settings.json` and is writable by container user |
| I5 | Git mount under `~` is writable | Create profile with git mount mapping `{"target_path": "~/dotfiles"}`; start instance | Repo files appear at `/home/user/dotfiles` and are writable by container user |
| I6 | Legacy dockerfile tool uses new mount target after migration | Run Alembic migration; create legacy dockerfile tool instance | Repo mounts at `/home/user/{repo_name}` and `/workspace` symlink works |
| I7 | Explicit manifest repo mount target is preserved | Create manifest with explicit repo mount target `/opt/repo`; start instance | Repo mounts at `/opt/repo`; no `/home/user/{repo_name}` mount injected |
## Manual QA Checklist
- [ ] Create a new manifest tool type without specifying `home_directory`; verify default `/home/user` is used.
- [ ] Create a manifest tool type with `home_directory: /home/custom`; verify container reflects the override.
- [ ] Verify repository root directory name appears in the mount target (e.g., `/home/user/my-app`).
- [ ] Verify workspace-based instance uses workspace name in the mount target.
- [ ] Verify `/workspace` symlink points to the actual repo/workspace directory and `cd /workspace && pwd` works.
- [ ] Verify legacy dockerfile tool, after migration, mounts repo under `/home/user/{repo_name}`.
- [ ] Verify config-profile file mount under `~/config` lands at `/home/user/config`.
- [ ] Verify git mount under `~/dotfiles` lands at `/home/user/dotfiles`.
- [ ] Verify container user can create files under `/home/user` and mounted config directories.
- [ ] Verify `alembic downgrade -1` restores `/workspace` strings and removes the column.
## Regression Coverage
- Existing manifest tools without `home_directory` continue to work (default `/home/user`).
- Existing config profiles without `~`/`$HOME` continue to work.
- Existing legacy tools that did not use `/workspace` are unaffected by the migration rewrite.
- Existing `home-path-expansion` tests still pass.
## Test Commands
```bash
cd apps/api
pytest tests/unit/test_home_path_expansion.py tests/unit/test_manifest_compiler.py tests/unit/test_instance_service.py tests/unit/test_alembic_migrations.py -xvs
pytest tests/integration/test_tool_instance_lifecycle.py -xvs
# Alembic round-trip
alembic upgrade head
alembic downgrade -1
alembic upgrade head
cd apps/web
npm run typecheck
```
## Exit Criteria
- All listed unit and integration tests pass.
- Manual QA checklist is completed or equivalent automated coverage is added.
- Alembic migration round-trip passes without errors.
- No regressions in existing `home-path-expansion` or tool instance tests.
+1 -1
View File
@@ -2,7 +2,7 @@
dir: openspec dir: openspec
## role ## role
Defines the OpenSpec methodology and configuration for managing living documentation, requirements, and task tracking within a Docker-based coding agent platform. Defines the OpenSpec methodology and configuration for managing software requirements, specifications, and task tracking as living documentation within a Docker-based coding agent management platform.
## parent ## parent
index: ./.pi-map.index.md index: ./.pi-map.index.md
map: ./.pi-map.md map: ./.pi-map.md
+2 -2
View File
@@ -4,12 +4,12 @@ dir: openspec
index: openspec/.pi-map.index.md index: openspec/.pi-map.index.md
## role ## role
Defines the OpenSpec methodology and configuration for managing living documentation, requirements, and task tracking within a Docker-based coding agent platform. Defines the OpenSpec methodology and configuration for managing software requirements, specifications, and task tracking as living documentation within a Docker-based coding agent management platform.
## files ## files
- README.md | Documents the OpenSpec methodology for managing software requirements, specifications, and task tracking as living documentation within a project repository. | dep: OpenSpec CLI (@fission-ai/openspec), Docker, SQLAlchemy, Alembic, Authentik, React, TypeScript, Tailwind, Traefik, Jinja2, xterm.js, pytest, mypy, ruff, npm - README.md | Documents the OpenSpec methodology for managing software requirements, specifications, and task tracking as living documentation within a project repository. | dep: OpenSpec CLI (@fission-ai/openspec), Docker, SQLAlchemy, Alembic, Authentik, React, TypeScript, Tailwind, Traefik, Jinja2, xterm.js, pytest, mypy, ruff, npm
- config.yaml | Configuration file defining project metadata, technology stack, and software development discipline rules for a Docker-based coding agent management platform | dep: FastAPI, React, Vite, PostgreSQL, SQLAlchemy, Redis, Alembic, pytest, Docker Compose, Traefik, Authentik - config.yaml | Configuration file defining project metadata, technology stack, and software development discipline rules for a Docker-based coding agent management platform | dep: FastAPI, React, Vite, PostgreSQL, SQLAlchemy, Redis, Alembic, pytest, Docker Compose, Traefik, Authentik
## arch ## arch
Documentation-as-code pattern with YAML-driven configuration, embedding specifications directly in the repository rather than external tools, supporting automated agent workflows. Documentation-as-code pattern with YAML-based configuration management, structured metadata definitions, and rule-based discipline enforcement for AI-assisted software development workflows.
## tags ## tags
software, project, docker, sqlalchemy, alembic, authentik, react, traefik software, project, docker, sqlalchemy, alembic, authentik, react, traefik
## symbols ## symbols
@@ -0,0 +1,38 @@
# Fix Terminal Container Overflow on Desktop
## Summary
On the desktop Terminal page, the terminal container sometimes grows taller than its allocated flex/grid track and acquires its own scrollbar, which in turn causes the outer page to scroll. This happens because the flex container chain from `.terminal-page` down to `.terminal-container` is missing `min-height: 0`, allowing a child with intrinsic content height to expand its parent.
## Scope
- `apps/web/src/styles/utilities.css`
- `.terminal-page`
- `.terminal-page-content`
- `.terminal-instance`
- `.terminal-wrapper`
- `.terminal-container`
- `.xterm-viewport`
## Root Cause
`.terminal-page-content` is a flex item that receives its height from the flex parent. In CSS flexbox, a flex item's `min-height` defaults to `auto`, which is based on its content. If the xterm.js viewport/canvas renders taller than the available track, the flex item expands to fit it, pushing the page beyond `100vh` and creating a page-level scrollbar.
## Fix
1. Add `min-height: 0` to every flex/grid item in the terminal layout chain so each level respects the constrained height from its parent.
2. Keep `overflow: hidden` on the wrapper/container so xterm.js's internal viewport is the only scroll surface.
3. Ensure `.terminal-container .xterm-viewport` fills the container and does not expand past it.
## Acceptance Criteria
- [x] Terminal page never shows an outer vertical scrollbar due to terminal content.
- [x] Terminal wrapper and container stay within their allocated track.
- [x] xterm.js internal viewport still scrolls normally.
- [x] Mobile terminal layout is unaffected.
- [x] Quality gates pass: `npm run typecheck`, `npm run lint`, `npm test -- --run`.
## Related
- `openspec/specs/tool-terminal`
- `openspec/changes/fix-tmux-mouse-config`
@@ -0,0 +1,13 @@
# Fix Terminal Container Overflow on Desktop — Tasks
- [x] Discard debug `terminal.tsx` changes from `debug/mobile-terminal-scroll`.
- [x] Create focused branch `fix/terminal-container-overflow` from `dev`.
- [x] Update `utilities.css` to prevent terminal container from overflowing its flex/grid track:
- [x] `.terminal-page`: add `overflow: hidden`.
- [x] `.terminal-instance`: add `max-height: 100%` and `overflow: hidden`.
- [x] `.terminal-wrapper`: add `max-height: 100%`.
- [x] `.terminal-container`: add `min-height: 0`.
- [x] `.terminal-container .xterm-viewport`: add `max-height: 100% !important` and `width: 100% !important`.
- [x] Run `npm run typecheck` and `npm run lint` in `apps/web`.
- [x] Run `npm test -- --run` in `apps/web` (87 passed).
- [x] Update project maps for changed files.
@@ -0,0 +1,30 @@
# Fix tmux Mouse Mode Configuration in Pi Agent Images
## Summary
The Pi Agent dockerfile templates create `/home/user/.tmux.conf` using `echo '... \n ...'` with single quotes. In both Dockerfile `RUN` commands and Python-embedded dockerfile strings, single quotes prevent `\n` from being interpreted as a newline, so the resulting `.tmux.conf` contains a single malformed line such as:
```
set -g mouse on\nset -g default-terminal "screen-256color"
```
Tmux fails to parse this, so `set -g mouse on` is never applied. Without tmux mouse mode, xterm.js sends mouse-wheel events as Up/Down arrow keys to the shell, causing wheel scroll to cycle command history instead of scrolling the terminal/tmux buffer.
## Scope
- `tool-images/pi-agent.dockerfile`
- `apps/api/alembic/versions/20260527160017_add_pi_agent_tool_type.py`
- `apps/api/alembic/versions/2026_05_28_add_tool_definition_manifests.py`
## Acceptance Criteria
- [x] Pi Agent image builds write a valid multi-line `.tmux.conf`.
- [x] `.tmux.conf` contains `set -g mouse on` on its own line.
- [x] `.tmux.conf` contains `set -g default-terminal "screen-256color"` on its own line.
- [x] Ranger config is also fixed where the same literal `\n` bug exists.
- [x] Alembic migration dockerfile strings produce the same valid commands as the standalone Dockerfile.
## Related
- `openspec/specs/tool-terminal`
- `tool-images/pi-agent.dockerfile`
@@ -0,0 +1,11 @@
# Fix tmux Mouse Mode Configuration in Pi Agent Images — Tasks
- [x] Identify the literal `\n` bug in `.tmux.conf` creation commands across Pi Agent dockerfiles and migrations.
- [x] Update `tool-images/pi-agent.dockerfile` to use `printf '%s\n'` for `.tmux.conf` and ranger `rc.conf`.
- [x] Update `apps/api/alembic/versions/20260527160017_add_pi_agent_tool_type.py` dockerfile string with the same fix.
- [x] Update `apps/api/alembic/versions/2026_05_28_add_tool_definition_manifests.py` dockerfile string with the same fix.
- [x] Verify resulting Dockerfile commands produce valid multi-line config files.
- [x] Run `npm run typecheck` and `npm run lint` in `apps/web`.
- [x] Run `npm test -- --run` in `apps/web` (87 passed).
- [x] Verify Python migration files compile with `py_compile`.
- [x] Update project maps for changed files.
@@ -0,0 +1,34 @@
# Mobile Config Profiles UI Rework
## Goal
Redesign the mobile **Config Profiles** detail ("preview") and edit pages to match the full desktop functionality, following the same mobile UX pattern used for the Tool Workshop.
## Scope
- Rewrite `ConfigProfilesMobileView` so the detail view displays all profile fields and supports resolving/previewing the profile.
- Rewrite the mobile edit view to expose every field available on the desktop editor:
- name, description, project, tool type, default flag
- includes with add/remove/reorder
- environment variables
- runtime hints
- files
- mounts with nested files
- git mounts
- Update `ConfigProfilesPage` to pass the required state and callbacks.
- Update `useConfigProfiles` so `handleSubmit` returns success status for cleaner mobile navigation.
- Add mobile-specific CSS for config profile forms.
## Acceptance Criteria
- On viewports narrower than 768 px, the Config Profiles page renders a mobile-friendly list/detail/edit flow.
- The mobile detail view shows all profile fields and offers a Preview action that displays the resolved profile.
- The mobile edit view supports all desktop fields, including includes, runtime hints, files, mounts, nested mount files, and git mounts.
- All existing desktop functionality remains intact.
- `npm run typecheck` and `npm run lint` pass in `apps/web`.
## Out of Scope
- Backend API changes.
- Changes to other pages.
- New features beyond existing desktop capabilities.
@@ -0,0 +1,10 @@
# Tasks: Mobile Config Profiles UI Rework
- [x] Inspect existing ConfigProfilesMobileView, desktop editor, hook, and page.
- [x] Update `useConfigProfiles.handleSubmit` to return boolean success.
- [x] Rewrite `ConfigProfilesMobileView` detail view with full field display and preview action.
- [x] Rewrite `ConfigProfilesMobileView` edit view with all desktop fields (includes, env vars, runtime hints, files, mounts, git mounts).
- [x] Update `ConfigProfilesPage` to pass required props to mobile view.
- [x] Add responsive CSS for config profile mobile forms.
- [x] Run `npm run typecheck` and `npm run lint` in `apps/web` and fix issues.
- [x] Update project maps for changed files.
@@ -0,0 +1,37 @@
# Mobile Edit-as-Default with Sticky Bottom Actions
## Summary
On mobile, tapping a config profile or tool type now opens the edit view directly instead of the read-only detail view. Save and Delete actions are anchored to a sticky bottom action bar so they are always reachable while scrolling long forms.
## Motivation
The previous mobile flow required an extra tap (list → detail → edit) for every change. Power users on phones primarily want to edit, so the default view should be edit. The Save and Delete buttons were also at the top of long forms, forcing users to scroll back up to commit changes.
## Scope
- Config Profiles mobile view (`ConfigProfilesMobileView`).
- Tool Workshop mobile view (`ToolWorkshopMobileView`).
- Shared `MobileEditView` component.
- Mobile edit-view CSS (`utilities.css`).
## Acceptance Criteria
- [x] Tapping a config profile opens the edit form.
- [x] Tapping a tool type opens the edit form.
- [x] Create flows still open the edit form and return to the list after save.
- [x] Edit flows stay on the edit form after a successful save and show a success message.
- [x] Delete is available in the edit view for existing items.
- [x] Save and Delete buttons are sticky at the bottom of the viewport on mobile.
- [x] Detail and preview views remain reachable from edit where already wired.
## Non-Goals
- Changing desktop behavior or layout.
- Removing detail/preview views entirely.
- Altering the user profile page flow (it already uses `MobileEditView` and benefits from the sticky bar automatically).
## Related
- `openspec/changes/mobile-config-profiles-ui`
- `openspec/changes/mobile-tool-profile-ui`
@@ -0,0 +1,14 @@
# Mobile Edit-as-Default with Sticky Bottom Actions — Tasks
- [x] Update `MobileEditView` to accept an optional `onDelete` callback and render Save/Delete in a sticky bottom action bar.
- [x] Move the Save button out of the `MobileEditView` header; keep Cancel + title in the header.
- [x] Update `ConfigProfilesMobileView` so list item clicks open edit view.
- [x] Update `ConfigProfilesMobileView` to pass `onDelete` to `MobileEditView` when editing existing profiles.
- [x] Update `ConfigProfilesMobileView` cancel/save navigation to return to list on create and stay on edit on save.
- [x] Update `ToolWorkshopMobileView` so list item clicks open edit view.
- [x] Update `ToolWorkshopMobileView` to pass `onDelete` to `MobileEditView` when editing existing tool types.
- [x] Update `ToolWorkshopMobileView` cancel/save navigation to return to list on create and stay on edit on save.
- [x] Add CSS for `.mobile-edit-actions` sticky bottom bar and `.mobile-edit-delete` button.
- [x] Increase `.mobile-edit-form` bottom padding so content is not hidden behind the sticky bar.
- [x] Run `npm run typecheck`, `npm run lint`, and `npm test -- --run` in `apps/web`.
- [x] Update project maps for changed files.
@@ -0,0 +1,38 @@
# Restore Delete Button in Mobile List Views and Fix Edit Action Bar
## Summary
The `MobileListView` component accepted `onItemDelete` and `onItemDuplicate` callbacks but never rendered the corresponding action buttons. This change restores the delete button (and adds duplicate support) in each mobile list row for any view that supplies the callback.
In addition, the sticky bottom action bar added to `MobileEditView` in the previous change was positioned behind the 64px mobile navigation bar, making the Save and Delete buttons impossible to see on mobile. The bar is now raised above the navigation and given a higher z-index.
## Motivation
Users need a quick way to remove config profiles and tool types directly from the mobile list without first opening the edit view. The callback props already existed, so wiring them up restores intended behavior.
Users also reported that the Save/Delete buttons in the mobile edit view were not visible. Investigation showed the fixed-position action bar was rendering underneath the persistent mobile bottom navigation.
## Scope
- `MobileListView` shared component.
- `ToolWorkshopMobileView` (passes `onItemDelete`).
- `ConfigProfilesMobileView` already passed `onItemDelete`; it now actually renders.
- `MobileEditView` sticky action bar CSS (`utilities.css`).
- Mobile list item action CSS (`utilities.css`).
## Acceptance Criteria
- [x] `MobileListView` renders a delete button when `onItemDelete` is provided.
- [x] `MobileListView` renders a duplicate button when `onItemDuplicate` is provided.
- [x] Tapping delete/duplicate does not also trigger the row click.
- [x] `ToolWorkshopMobileView` list rows show the delete button.
- [x] `ConfigProfilesMobileView` list rows show the delete button.
- [x] Rows without delete/duplicate still show the chevron.
- [x] The `MobileEditView` sticky bottom action bar sits above the mobile navigation bar.
- [x] Save and Delete buttons in the edit view are visible and reachable.
## Related
- `openspec/changes/mobile-edit-default-bottom-actions`
- `openspec/changes/mobile-config-profiles-ui`
- `openspec/changes/mobile-tool-profile-ui`
@@ -0,0 +1,12 @@
# Restore Delete Button in Mobile List Views and Fix Edit Action Bar — Tasks
- [x] Read `MobileListView` source and confirm `onItemDelete`/`onItemDuplicate` were unused.
- [x] Update `MobileListView` to render delete/duplicate action buttons when callbacks are provided.
- [x] Stop propagation on action buttons so row click does not fire.
- [x] Update `ToolWorkshopMobileView` to pass `onItemDelete` to both list view usages.
- [x] Verify `ConfigProfilesMobileView` still passes `onItemDelete`.
- [x] Add `.mobile-list-item-action` and variant CSS to `utilities.css`.
- [x] Identify that `MobileEditView` sticky action bar was hidden behind the 64px mobile nav.
- [x] Update `.mobile-edit-actions` to `bottom: 64px` and `z-index: 110`.
- [x] Run `npm run typecheck`, `npm run lint`, and `npm test -- --run` in `apps/web`.
- [x] Update project maps for changed files.
@@ -0,0 +1,26 @@
# Mobile Tool Workshop & Profile UI Rework
## Goal
Completely rework the mobile experience for the **Tool Workshop** (tool configuration) and **Profile** (user profile configuration) pages so they are usable on small viewports while preserving all existing desktop functionality.
## Scope
- Rework `ToolWorkshopMobileView` to expose every field and action available on the desktop editor, including manifest definition support.
- Add a new `ProfileMobileView` and update `ProfilePage` to render it on small viewports.
- Keep desktop layouts and behavior unchanged.
- No backend API changes.
## Acceptance Criteria
- On viewports narrower than 768 px, the Tool Workshop page renders a mobile-friendly list/detail/edit flow.
- Tool type creation and editing on mobile support: definition type selection (Compose/Dockerfile/Manifest), manifest editor, interface type, conditional port, startup command, readiness probe, required variables, and all existing validation.
- On viewports narrower than 768 px, the Profile page renders a mobile-friendly form with avatar upload, name/email editing, validation, and save feedback.
- All existing desktop functionality remains intact.
- `npm run typecheck` and `npm run lint` pass in `apps/web`.
## Out of Scope
- Backend changes.
- Changes to other pages (sessions, settings, etc.).
- New features beyond the existing desktop capabilities.

Some files were not shown because too many files have changed in this diff Show More