Compare commits

...

31 Commits

Author SHA1 Message Date
Developer 346b32236a debug: synthesize wheel events for mobile terminal touch scrolling
Replace manual scrollLines/SGR injection with synthetic WheelEvent
instances dispatched into xterm.js's .xterm-viewport. Keep the debug
overlay so we can verify whether the gesture is recognized and whether
the wheel event reaches xterm.js.

Quality gates: typecheck, lint clean.
2026-06-13 21:04:50 +00:00
Developer 8f7f682a92 fix: merge mobile terminal scroll detection improvements 2026-06-13 20:42:52 +00:00
Developer 2ece7074f6 fix: improve mobile terminal alternate-screen detection and scrolling
- Revert touch-action: none on .xterm-viewport so xterm.js can fall back
  to its own viewport scrolling when the custom handler doesn't take over.
- Detect alternate screen via reference equality
  (term.buffer.active === term.buffer.alternate) instead of the
  string, which could report normal buffer incorrectly.
- Lower vertical-scroll activation threshold from 4px to 2px and only
  prevent default once a vertical gesture is recognized.
- In normal buffer use term.scrollLines() so xterm.js handles the buffer
  scroll consistently; in alternate screen continue sending SGR 1006
  mouse-wheel sequences to tmux/vim.

Quality gates: typecheck, lint clean, npm test -- --run 87 passed.
2026-06-13 20:42:52 +00:00
Developer c7ab2de25e fix: merge mobile terminal scrolling improvements 2026-06-13 20:30:52 +00:00
Developer c9b0259993 fix: route mobile terminal swipes to the terminal, not the page layout
- Add touch-action: none and overscroll-behavior: none to mobile terminal
  page, content, wrapper, container, and xterm viewport so the browser
  never treats swipes as page/address-bar scrolling or pull-to-refresh.
- Make .terminal-page.mobile fixed-position to prevent viewport layout
  scroll; keep the overlay toolbar as absolute with pointer-events only
  on interactive parts.
- Rework the mobile touch handler in terminal.tsx:
  * Detect normal vs alternate buffer via term.buffer.active.type instead
    of measuring the DOM viewport, which was unreliable in tmux/vim.
  * Accumulate swipe distance and emit SGR 1006 mouse-wheel sequences in
    steps, so tmux pane scrolling tracks the gesture correctly.
  * Prevent default as soon as the swipe is recognized so the page does
    not start a competing scroll gesture.

Quality gates: npm run typecheck, npm run lint clean, npm test -- --run 87 passed.
2026-06-13 20:30:52 +00:00
Developer 1f2c3dbe2a fix: merge workspace project_name population 2026-06-13 19:59:13 +00:00
Developer d247e44985 fix: include project_name in /workspaces list response
- The top-level GET /workspaces endpoint was returning project_name as
  an empty string because the GitRepository.project relationship was not
  eager-loaded.
- Select the full GitRepository entity and apply selectinload(project)
  so project_name is populated for every workspace row.

Backend quality gates: python3 -m pytest 313 passed, 34 skipped.
2026-06-13 19:59:13 +00:00
Developer d60dec9d62 fix: merge workspace project name visibility 2026-06-13 19:53:43 +00:00
Developer 4913cc7297 fix: make workspace project name visible on mobile and desktop cards
- Move the project name below the workspace title row so it reads as a
  distinct line with a project icon.
- Move the status badge into the title row next to the workspace name,
  preventing it from crowding the project label.
- Add .workspace-title-row flex styles and update .workspace-project-name
  to display inline-flex with a brand-colored project icon.

Quality gates: npm run typecheck, npm run lint clean,
npm test -- --run 87 passed.
2026-06-13 19:53:43 +00:00
Developer 86360661e9 fix: merge nav sessions middle 2026-06-13 19:44:47 +00:00
Developer 1d09193652 fix: put Sessions back in the middle of nav on desktop and mobile
- Reorder desktop sidebar so Sessions sits between spaces/tools groups:
  Home, Projects, Workspaces, Sessions, Tool Workshop, Config Profiles, Settings.
- Reorder mobile bottom nav so Sessions is the center item:
  Home, Spaces, Sessions, Tools, Settings.
- Workspace cards already display the owning project name; no extra change needed.

Quality gates: npm run typecheck, npm run lint clean,
npm test -- --run 87 passed.
2026-06-13 19:44:47 +00:00
Developer 4ced6141ae feat: merge mobile Spaces nav and list views 2026-06-13 19:38:03 +00:00
Developer 530225d37c feat: mobile Spaces nav and list-based project/workspace views
- Combine Projects and Workspaces into a single 'Spaces' grouped mobile
  nav item that opens a bottom-sheet menu.
- Add SpacesBottomSheet component with Projects/Workspaces options.
- Extend MobileListView with optional renderItem prop for rich rows.
- Redesign mobile ProjectsPage rows to show project description and
  repository chips.
- Replace mobile WorkspacesPage list with compact WorkspaceCard grid,
  matching desktop card content.
- Add mobile-list-* CSS and mobile-workspaces-list spacing.

Quality gates: npm run typecheck, npm run lint clean,
npm test -- --run 87 passed.
2026-06-13 19:32:16 +00:00
Developer e6ab77d123 feat: merge workspace card redesign 2026-06-13 18:54:59 +00:00
Developer 4e1477c4be feat: redesign workspace card header and layout
- Swap workspace card header order: project name is now the primary eyebrow,
  workspace name is the bold title below it.
- Restructure workspace card into clean top/body/actions sections with more
  whitespace and clearer hierarchy.
- Replace cramped meta paragraphs with an inline meta row (repo, branch,
  instance count) and dedicated instance chip area.
- Use icon-only ghost buttons for sync/delete to reduce visual noise; keep
  prominent Start Tool button.
- Add top divider for actions, improve hover states, and make long names
  truncate gracefully.
- Update mobile workspace list subtitle to project · workspace name.
- Refresh workspaces.css with new card layout and responsive mobile rules.

Quality gates: npm run typecheck clean, npm run lint clean,
npm test -- --run 87 passed
2026-06-13 17:00:16 +00:00
Developer 2b5b8363ca chore: merge completed OpenSpec archive 2026-06-13 14:39:33 +00:00
Developer 1e0f95f8bd chore: archive completed OpenSpec changes
Move the following audited-and-implemented changes into
openspec/changes/archive/2026-06-12-completed-changes-archive/:

- tool-config-mount-cleanup
- config-profile-directory-mounts

Update archive README count and project map index/files accordingly.
openspec/changes/ now contains only the archive directory.

Quality gates: pytest 313 passed/34 skipped, npm run typecheck/lint clean,
npm test -- --run 87 passed
2026-06-13 14:24:30 +00:00
Developer 6a20c54da7 chore: merge project map patches 2026-06-13 14:15:30 +00:00
Developer db6ae38568 chore: patch project maps for loading-overlay and orphaned session entries
- Add loading-overlay.tsx to component maps
- Remove orphaned session-progress-panel.tsx and session-operations.tsx entries
- Refresh parent indexes after manual map edits

Quality gates: pytest 313 passed/34 skipped, npm run typecheck/lint clean,
npm test -- --run 87 passed
2026-06-13 12:31:35 +00:00
Developer 1e43ea48aa Merge branch 'fix/compose-readonly-mounts' into dev 2026-06-13 12:12:12 +00:00
Developer efe5e4ae50 fix: respect readonly flag on config-profile mounts in compose tool types
- modify_compose_file now appends :ro when a config-profile volume entry
  has readonly=true, matching the manifest compiler behavior.
- Add a guard for missing tool_type in prepare_manifest_instance.

Quality gates: python3 -m pytest (313 passed, 34 skipped)
2026-06-13 12:12:12 +00:00
Developer 31f1ce00d3 Merge branch 'feat/config-profile-directory-mounts' into dev 2026-06-13 11:56:48 +00:00
Developer d395aaf574 fix: stage config-profile mounts as directories so ~/.config stays writable
- Switch apply_resolved_profile from per-file bind mounts to one
  directory-level bind mount per ResolvedMount target.
- Stage all configured files under instance_dir/mounts/<sanitized_target>
  and bind-mount that directory, so Docker no longer creates a root-owned
  parent directory such as ~/.config.
- Propagate read-only mode ('ro') as the 'readonly' flag on volume entries.
- Update unit tests to expect directory-level mounts and add coverage for
  readonly/writable flags.

Quality gates: python3 -m py_compile, pytest (313 passed, 34 skipped),
npm run typecheck, npm run lint.
2026-06-13 11:56:48 +00:00
Developer d78ca8a9d5 Merge branch 'chore/patch-project-maps' into dev 2026-06-13 11:40:03 +00:00
Developer 19291d6be9 chore: patch project maps after tool-config mount cleanup 2026-06-13 11:40:03 +00:00
Developer 5409ba2b13 Merge branch 'feat/tool-config-mount-cleanup' into dev 2026-06-13 11:35:55 +00:00
Developer 8bde9a213c feat: remove config/state mounts from built-in tool configs
- Remove pi_state and pi_config mounts from the pi-agent manifest.
- Add Alembic data migration to strip those mounts from existing DB rows.
- Remove opencode_home:/tmp volume and HOME=/tmp override from the opencode
  built-in compose template; config/state now belongs in config profiles.
- Workspace and SSH key mounts remain unchanged.

Quality gates: python3 -m py_compile, pytest (311 passed, 34 skipped),
npm run typecheck, npm run lint
2026-06-13 11:35:55 +00:00
Developer 315cb33e3f Merge branch 'chore/format-busy-overlay-followup' into dev 2026-06-13 11:19:22 +00:00
Developer 474843ffa6 chore: apply formatter to busy overlay files 2026-06-13 11:19:22 +00:00
Developer 766f7ff0d1 Merge branch 'feat/busy-container-overlays' into dev 2026-06-13 10:10:28 +00:00
Developer f8b162ec5f feat: busy container overlays for lifecycle actions
Add a reusable LoadingOverlay component that dims and disables the
container owning an in-flight action, with a spinning indicator and
label. Apply it to:

- SessionCard (when actionBusyId matches)
- InstanceList cards (per busyInstanceId with action-specific labels)
- CreateSessionForm (while submitting)
- ToolStarter (while starting)

Also add .icon-spin animation and position:relative to the relevant
containers.

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