Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb38b37ceb | |||
| 6698c20f25 | |||
| 5e06a2a226 | |||
| 49180e4c6d | |||
| 7a5538b53f | |||
| 41d24beade | |||
| d1777b88ad | |||
| 691bbaa87b | |||
| 6191565e80 | |||
| 468f34202a | |||
| 996a858892 | |||
| ff7fcb6e8d | |||
| 3f06224b75 | |||
| a1a77c99a6 | |||
| 3c96c7b153 | |||
| 10955dfe8e |
+4
-14
@@ -16,7 +16,7 @@ dir: .
|
|||||||
Trust boundary: index routes, map orients, source decides.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Root configuration and documentation package for "Headquarter," a self-hosted project management platform with Git repository features, containing deployment configs, development tooling, and UI rework tracking documents.
|
Package .
|
||||||
## parent
|
## parent
|
||||||
-
|
-
|
||||||
## children
|
## children
|
||||||
@@ -32,15 +32,6 @@ Root configuration and documentation package for "Headquarter," a self-hosted pr
|
|||||||
- .pi
|
- .pi
|
||||||
index: .pi/.pi-map.index.md
|
index: .pi/.pi-map.index.md
|
||||||
map: .pi/.pi-map.md
|
map: .pi/.pi-map.md
|
||||||
- .pi-lens
|
|
||||||
index: .pi-lens/.pi-map.index.md
|
|
||||||
map: .pi-lens/.pi-map.md
|
|
||||||
- .pytest_cache
|
|
||||||
index: .pytest_cache/.pi-map.index.md
|
|
||||||
map: .pytest_cache/.pi-map.md
|
|
||||||
- .ruff_cache
|
|
||||||
index: .ruff_cache/.pi-map.index.md
|
|
||||||
map: .ruff_cache/.pi-map.md
|
|
||||||
- .sisyphus
|
- .sisyphus
|
||||||
index: .sisyphus/.pi-map.index.md
|
index: .sisyphus/.pi-map.index.md
|
||||||
map: .sisyphus/.pi-map.md
|
map: .sisyphus/.pi-map.md
|
||||||
@@ -62,12 +53,12 @@ Root configuration and documentation package for "Headquarter," a self-hosted pr
|
|||||||
- scripts
|
- scripts
|
||||||
index: scripts/.pi-map.index.md
|
index: scripts/.pi-map.index.md
|
||||||
map: scripts/.pi-map.md
|
map: scripts/.pi-map.md
|
||||||
- tests
|
|
||||||
index: tests/.pi-map.index.md
|
|
||||||
map: tests/.pi-map.md
|
|
||||||
- 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
|
||||||
@@ -77,7 +68,6 @@ Root configuration and documentation package for "Headquarter," a self-hosted pr
|
|||||||
- README.md
|
- README.md
|
||||||
- docker-compose.traefik.yml
|
- docker-compose.traefik.yml
|
||||||
- docker-compose.yml
|
- docker-compose.yml
|
||||||
- package-lock.json
|
|
||||||
- progress.md
|
- progress.md
|
||||||
- swap-pane
|
- swap-pane
|
||||||
- ui-audit-spacing-typography.md
|
- ui-audit-spacing-typography.md
|
||||||
|
|||||||
+16
-17
@@ -18,26 +18,25 @@ index: ./.pi-map.index.md
|
|||||||
Trust boundary: index routes, map orients, source decides.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Root configuration and documentation package for "Headquarter," a self-hosted project management platform with Git repository features, containing deployment configs, development tooling, and UI rework tracking documents.
|
Package .
|
||||||
## files
|
## files
|
||||||
- .env.example | Provides a template of environment variables for configuring database, Redis, sessions, domains, Authentik OAuth, frontend, and Docker settings for the application. | dep: Postgres, Redis, Authentik, Vite, Docker Compose, Traefik
|
- .env.example | Provides example environment variable configuration for a full-stack application with database, caching, authentication, and deployment settings
|
||||||
- .gitignore | Specifies intentionally untracked files and directories to exclude from version control across multiple technologies and environments.
|
- .gitignore | Specifies files and directories for Git to ignore across a project using Beads/Dolt, Python, Node, and various IDE/OS tooling. | dep: git, dolt, beads, python, node, npm, yarn, pnpm, pytest, mypy, ruff, coverage
|
||||||
- AGENTS.md | Defines rules, workflow conventions, and verification standards for AI agents operating on the project, using OpenSpec as the source of truth and Superpowers as the default workflow. | dep: OpenSpec, Superpowers, Git
|
- AGENTS.md | Defines operational guidelines and workflows for AI agents collaborating on a software project governed by OpenSpec | dep: OpenSpec, superpowers (brainstorming, writing-plans, test-driven-development, systematic-debugging, verification-before-completion, using-git-worktrees, dispatching-parallel-agents), git
|
||||||
- CHANGELOG.md | Documents notable changes and version history for a project management application with Git repository features.
|
- CHANGELOG.md | Documents version history and notable changes for a project management and Git repository application
|
||||||
- Makefile | Provides common development automation commands for a Dockerized full-stack application, including service lifecycle, testing, linting, migrations, and cleanup. | dep: docker compose, alembic, pytest, playwright, ruff, mypy, npm
|
- Makefile | Provides standardized development commands for managing a Docker-based full-stack application with API, web frontend, database, and testing infrastructure | dep: docker compose, alembic, pytest, ruff, mypy, playwright, npm, postgres, redis
|
||||||
- README.md | Provides documentation and onboarding instructions for "Headquarter," a self-hosted platform for managing projects, git repositories, and development tools.
|
- README.md | Documentation for a self-hosted development platform that manages projects, git repositories, and development tools with OAuth2 authentication | dep: FastAPI, SQLAlchemy, Pydantic, Alembic, python-jose, React, TypeScript, Vite, React Router, Docker, PostgreSQL, Traefik, Authentik
|
||||||
- docker-compose.traefik.yml | Defines a Docker Compose deployment configuration for a multi-service application (PostgreSQL, Redis, web frontend, API) behind an existing Traefik reverse proxy with TLS termination. | dep: postgres:15-alpine, redis:7-alpine, traefik, docker.sock, authentik
|
- docker-compose.traefik.yml | Deploys a multi-service application (PostgreSQL, Redis, web frontend, API) behind an existing Traefik reverse proxy with TLS termination | dep: docker, docker-compose, traefik, postgres, redis, node/vite, python/fastapi
|
||||||
- docker-compose.yml | Defines a multi-container Docker Compose configuration for a full-stack application with PostgreSQL, Redis, an API service, and a web frontend. | dep: postgres:15-alpine, redis:7-alpine, apps/api, apps/web
|
- docker-compose.yml | Defines a multi-service Docker Compose stack for a web application with PostgreSQL, Redis, API backend, and web frontend services | dep: Docker, Docker Compose, PostgreSQL, Redis, Vite, asyncpg, Python/FastAPI (implied), Node.js (implied)
|
||||||
- package-lock.json | Locks dependency versions for the "headquarter" npm package.
|
- progress.md | Documents the progress and remaining tasks for a backend-frontend refactoring project involving modularization, code reorganization, and verification.
|
||||||
- progress.md | Tracks progress of a backend-frontend refactoring project across completed phases, pending tasks, and remaining work.
|
- swap-pane | Provides a command to swap the position of two tmux panes within a window or between windows | dep: tmux, client, window, layout, cmd-find, cmd-parse, options
|
||||||
- swap-pane | Empty package initialization file that marks a directory as a Python package.
|
- ui-audit-spacing-typography.md | A detailed audit report identifying critical CSS styling gaps, mobile layout failures, and design system inconsistencies in a web application's UI components and stylesheets. | dep: React/TSX components, CSS stylesheets (global.css, utilities.css, tokens.css, page-specific CSS), JSX/TSX files in apps/web/src/components and apps/web/src/pages
|
||||||
- ui-audit-spacing-typography.md | Audit report documenting spacing, typography, and visual rhythm issues in the web UI, including missing CSS classes, inconsistent units, incomplete design tokens, and recommendations for fixes.
|
- ui-rework-foundations-apply.md | Documents the implementation of Pass 1 (Foundations) of a web UI spacing/typography/visual-rhythm rework, including design token expansion, primitive CSS class additions, component refactors, and OpenSpec documentation. | dep: CSS design tokens, React/TSX components, OpenSpec documentation system, Git/SSH, npm build toolchain
|
||||||
- ui-rework-foundations-apply.md | Documenting the implementation of Pass 1 (Foundations) of a web UI spacing/typography/visual-rhythm rework, summarizing changed files, commits, validation results, and next steps. | dep: tokens.css, global.css, utilities.css, ConfigProfileListSidebar.tsx, ConfigProfileEditorPanel.tsx, instance-list.tsx, openspec artifacts
|
- ui-rework-pass2-apply.md | Documents the implementation and verification of Pass 2 of a web UI spacing/typography rework, refactoring inline styles into utility classes and design tokens across six components. | dep: CSS custom properties, BEM methodology, utility-first CSS, React/TSX components, OpenSpec documentation system
|
||||||
- ui-rework-pass2-apply.md | Implementation report documenting a UI component cleanup pass that refactored inline styles into CSS utility classes and design tokens.
|
|
||||||
## arch
|
## arch
|
||||||
Dockerized microservices architecture with PostgreSQL/Redis backend, separate API and web frontend services, Traefik reverse proxy option, npm-based frontend build, and Makefile-driven development workflow with phased UI refactoring tracked via markdown documents.
|
Contains 13 files.
|
||||||
## tags
|
## tags
|
||||||
docker, ui, application, web, package, css, rework, redis
|
docker, web, git, application, ui, rework, python, redis
|
||||||
## symbols
|
## symbols
|
||||||
-
|
-
|
||||||
## workflows
|
## workflows
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps
|
dir: apps
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains the main application entry points and executable components for the project
|
Package apps
|
||||||
## parent
|
## parent
|
||||||
index: ./.pi-map.index.md
|
index: ./.pi-map.index.md
|
||||||
map: ./.pi-map.md
|
map: ./.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,10 +4,10 @@ dir: apps
|
|||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains the main application entry points and executable components for the project
|
Package apps
|
||||||
## files
|
## files
|
||||||
## arch
|
## arch
|
||||||
Modular application structure with separate deployable units, typically following framework-specific patterns (e.g., MVC, serverless handlers, or microservices)
|
Contains 0 files.
|
||||||
## tags
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api
|
dir: apps/api
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Backend API server for the Headquarter platform, providing self-hosted project management, git repository orchestration, and development tool integration.
|
Backend API server for the Headquarter platform, providing self-hosted project management, git repository management, and development tool orchestration services.
|
||||||
## parent
|
## parent
|
||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
map: apps/.pi-map.md
|
map: apps/.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ dir: apps/api
|
|||||||
index: apps/api/.pi-map.index.md
|
index: apps/api/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Backend API server for the Headquarter platform, providing self-hosted project management, git repository orchestration, and development tool integration.
|
Backend API server for the Headquarter platform, providing self-hosted project management, git repository management, and development tool orchestration services.
|
||||||
## files
|
## files
|
||||||
- .dockerignore | Specifies files and directories to exclude from the Docker build context to optimize image build times and prevent sensitive or unnecessary files from being included.
|
- .dockerignore | Specifies files and directories to exclude from the Docker build context to optimize image build times and prevent sensitive or unnecessary files from being included.
|
||||||
- Dockerfile | Multi-stage Dockerfile that builds and runs a Python application with Docker CLI access, cloudflared, and database readiness checks. | dep: python:3.11-slim, libpq5, git, openssh-client, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, netcat-openbsd
|
- Dockerfile | Multi-stage Dockerfile that builds and runs a Python application with Docker CLI access, cloudflared, and database readiness checks. | dep: python:3.11-slim, libpq5, git, openssh-client, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, netcat-openbsd
|
||||||
@@ -14,7 +14,7 @@ Backend API server for the Headquarter platform, providing self-hosted project m
|
|||||||
- uv.lock | This file is a UV lockfile that pins exact versions, hashes, and metadata for all Python project dependencies to ensure reproducible environments. | dep: uv, aiosqlite, alembic, annotated-types, anyio, asyncpg, sqlalchemy, mako
|
- uv.lock | This file is a UV lockfile that pins exact versions, hashes, and metadata for all Python project dependencies to ensure reproducible environments. | dep: uv, aiosqlite, alembic, annotated-types, anyio, asyncpg, sqlalchemy, mako
|
||||||
- wait-for-db.sh | Polls a PostgreSQL host/port until it is available or a retry limit is reached, then executes the passed command. | dep: nc, sleep
|
- wait-for-db.sh | Polls a PostgreSQL host/port until it is available or a retry limit is reached, then executes the passed command. | dep: nc, sleep
|
||||||
## arch
|
## arch
|
||||||
Python-based service using FastAPI/ASGI with Alembic for database migrations, multi-stage Docker containerization with cloudflared tunneling, UV for dependency management, and PostgreSQL as the primary datastore with explicit readiness-gated startup orchestration.
|
Modern Python async API using FastAPI/Starlette with SQLAlchemy ORM, Alembic migrations, multi-stage Docker containerization with cloudflared tunneling, UV package management, and PostgreSQL database with health-checked startup orchestration.
|
||||||
## tags
|
## tags
|
||||||
alembic, docker, sqlalchemy, asyncpg, dockerfile, database, postgresql, pydantic
|
alembic, docker, sqlalchemy, asyncpg, dockerfile, database, postgresql, pydantic
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/alembic
|
dir: apps/api/alembic
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database migration infrastructure for the API application, enabling version-controlled schema changes with async database support.
|
Database migration tooling for the API service, enabling version-controlled schema changes with async SQLAlchemy support.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/.pi-map.index.md
|
index: apps/api/.pi-map.index.md
|
||||||
map: apps/api/.pi-map.md
|
map: apps/api/.pi-map.md
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ dir: apps/api/alembic
|
|||||||
index: apps/api/alembic/.pi-map.index.md
|
index: apps/api/alembic/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Database migration infrastructure for the API application, enabling version-controlled schema changes with async database support.
|
Database migration tooling for the API service, enabling version-controlled schema changes with async SQLAlchemy support.
|
||||||
## files
|
## files
|
||||||
- env.py | Configures Alembic database migration environment with async SQLAlchemy support | 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. | 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 SQL schema revisions | dep: alembic, sqlalchemy
|
- script.py.mako | Alembic database migration script template that generates Python migration files for SQLAlchemy database schema changes | dep: alembic, sqlalchemy, mako
|
||||||
## arch
|
## arch
|
||||||
Template-based migration generation using Alembic with async SQLAlchemy engine configuration and environment setup.
|
Template-based migration generation using Alembic's standard env.py configuration pattern with async SQLAlchemy engine integration and Mako templating for migration script scaffolding.
|
||||||
## tags
|
## tags
|
||||||
migrations, run, async, sqlalchemy, alembic, call:context.configure, call:context.begin, transaction
|
migrations, run, sqlalchemy, async, alembic, call:context.configure, call:context.begin, transaction
|
||||||
## symbols
|
## symbols
|
||||||
- run_migrations_offline
|
- run_migrations_offline
|
||||||
- do_run_migrations
|
- do_run_migrations
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
dir: apps/api/alembic/versions
|
dir: apps/api/alembic/versions
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Manages database schema evolution and version control for the API application using Alembic migration scripts.
|
Database schema versioning and incremental migration management for the API's relational data model using Alembic.
|
||||||
## 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
|
||||||
## children
|
## children
|
||||||
-
|
- apps/api/alembic/versions/.ruff_cache
|
||||||
|
index: apps/api/alembic/versions/.ruff_cache/.pi-map.index.md
|
||||||
|
map: apps/api/alembic/versions/.ruff_cache/.pi-map.md
|
||||||
## files
|
## files
|
||||||
- 0001_initial_schema.py
|
- 0001_initial_schema.py
|
||||||
- 0002_refresh_tokens.py
|
- 0002_refresh_tokens.py
|
||||||
@@ -51,6 +53,7 @@ map: apps/api/alembic/.pi-map.md
|
|||||||
- 2026_06_14_104415_add_tool_type_home_directory.py
|
- 2026_06_14_104415_add_tool_type_home_directory.py
|
||||||
- 2026_06_14_182955_fix_pi_agent_home_directory_mount.py
|
- 2026_06_14_182955_fix_pi_agent_home_directory_mount.py
|
||||||
- 2026_06_15_090500_remove_pi_agent_explicit_repo_mount.py
|
- 2026_06_15_090500_remove_pi_agent_explicit_repo_mount.py
|
||||||
|
- 2026_06_19_113000_remove_pi_agent_workspace_symlink.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
|
||||||
@@ -69,5 +72,7 @@ map: apps/api/alembic/versions/.pi-map.md
|
|||||||
read: 2026_05_24_220141_add_startup_command.py, 2026_05_29_remove_lsio_command_override.py
|
read: 2026_05_24_220141_add_startup_command.py, 2026_05_29_remove_lsio_command_override.py
|
||||||
- change versions config
|
- change versions config
|
||||||
read: 0003_user_configs.py, 0009_tool_configs.py, 0013_add_config_profiles.py
|
read: 0003_user_configs.py, 0009_tool_configs.py, 0013_add_config_profiles.py
|
||||||
|
- explore versions subdirectories
|
||||||
|
index: apps/api/alembic/versions/.ruff_cache/.pi-map.index.md
|
||||||
## dirty
|
## dirty
|
||||||
-
|
-
|
||||||
|
|||||||
@@ -4,60 +4,61 @@ dir: apps/api/alembic/versions
|
|||||||
index: apps/api/alembic/versions/.pi-map.index.md
|
index: apps/api/alembic/versions/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Manages database schema evolution and version control for the API application using Alembic migration scripts.
|
Database schema versioning and incremental migration management for the API's relational data model using Alembic.
|
||||||
## files
|
## files
|
||||||
- 0001_initial_schema.py | Creates the initial database schema with five tables (users, ssh_keys, projects, git_repositories, user_configs) using Alembic migrations | 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, sqlalchemy.dialects.postgresql
|
- 0001_initial_schema.py | Alembic database migration that creates the initial schema with five tables (users, ssh_keys, projects, git_repositories, user_configs) for a Git/SSH management application. | 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, sqlalchemy.dialects.postgresql
|
||||||
- 0002_refresh_tokens.py | Alembic database migration that creates a refresh_tokens table with indexes for user authentication/session 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, postgresql dialect
|
- 0002_refresh_tokens.py | Alembic database migration that creates a refresh_tokens table with indexes for secure session 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
|
||||||
- 0003_user_configs.py | Alembic database migration that creates a user_configs table with JSON configuration storage linked to users via foreign key | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.JSON, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, func:downgrade() → None, call:op.drop_table | dep: typing, alembic, sqlalchemy
|
- 0003_user_configs.py | Alembic database migration that creates a user_configs table with JSON configuration storage per user | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.JSON, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, func:downgrade() → None, call:op.drop_table | dep: typing, alembic, sqlalchemy
|
||||||
- 0004_tool_types.py | Alembic database migration that creates a tool_types table with metadata, templates, and user relationship columns | 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.Boolean, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, func:downgrade() → None, call:op.drop_table | dep: typing, alembic, sqlalchemy
|
- 0004_tool_types.py | Alembic database migration that creates a tool_types table with metadata, templates, and user tracking fields | 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.Boolean, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, func:downgrade() → None, call:op.drop_table | dep: typing, alembic, sqlalchemy
|
||||||
- 0005_ssh_keys_timestamps.py | Alembic database migration that adds created_at and updated_at timestamp columns to the ssh_keys table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.DateTime, call:sa.text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
- 0005_ssh_keys_timestamps.py | Alembic database migration that adds created_at and updated_at timestamp columns to the ssh_keys table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.DateTime, call:sa.text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||||
- 0006_tool_instances.py | Alembic database migration that creates a tool_instances table with UUID primary key, foreign key relationships, timestamps, and indexes for tracking deployed tool instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.String, call:sa.Integer, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
|
- 0006_tool_instances.py | Alembic database migration that creates a tool_instances table with foreign key relationships to tool_types, git_repositories, projects, and users tables, plus indexes for common query patterns. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.String, call:sa.Integer, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- 0007_instance_container_name.py | Alembic database migration that adds a nullable container_name column to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
- 0007_instance_container_name.py | Alembic database migration that adds a nullable container_name column to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||||
- 0008_tool_type_category.py | Alembic database migration that adds 'category' and 'interfaces' columns to the 'tool_types' table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
- 0008_tool_type_category.py | Alembic database migration that adds `category` and `interfaces` columns to the `tool_types` table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||||
- 0009_tool_configs.py | Alembic database migration that creates a tool_configs table for storing user tool configuration key-value pairs with optional project scoping | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.String, call:sa.Text, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 0009_tool_configs.py | Alembic database migration that creates a tool_configs table with UUID primary keys, foreign keys to users/tool_types/projects, key-value configuration storage, and supporting indexes. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.String, call:sa.Text, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- 0010_tool_type_default_port.py | Alembic database migration that adds a nullable default_port column to the tool_types table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
- 0010_tool_type_default_port.py | Alembic database migration that adds a nullable default_port column to the tool_types table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||||
- 0011_tool_instance_tunnel_fields.py | Alembic database migration that adds public_url and tunnel_id columns to the tool_instances table for tunnel functionality. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
- 0011_tool_instance_tunnel_fields.py | Alembic database migration that adds tunnel-related fields (public_url and tunnel_id) to the tool_instances table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||||
- 0012_default_port_req.py | Alembic database migration that backfills default_port values for existing tool types and makes the column non-nullable | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.Integer, func:downgrade() → None, call:op.alter_column, call:sa.Integer | dep: typing, alembic, sqlalchemy
|
- 0012_default_port_req.py | Alembic database migration that populates default_port values for existing tool types and makes the column non-nullable | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.Integer, func:downgrade() → None, call:op.alter_column, call:sa.Integer | dep: typing, alembic, sqlalchemy
|
||||||
- 0013_add_config_profiles.py | Database migration that adds config profiles, profile includes, profile mounts, and tool instance profile selection tables with defensive idempotent checks | exp: func:_table_exists(table_name: str) → bool, call:sa.inspect(op.get_bind()).has_table, call:op.get_bind, func:_column_exists(table_name: str, column_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_columns, call:op.get_bind, func:_index_exists(table_name: str, index_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_indexes, call:op.get_bind, func:_foreign_key_exists(table_name: str, constrained_columns: list[str], referred_table: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, func:upgrade() → None, call:_table_exists, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.Text, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:_index_exists, call:op.create_index, call:sa.Integer, call:_column_exists, call:op.add_column, call:_foreign_key_exists, call:op.create_foreign_key, func:downgrade() → None, call:op.drop_index, call:op.drop_constraint, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 0013_add_config_profiles.py | Alembic database migration that adds config_profiles, config_includes, config_mounts tables and a selected_profile_id column to tool_instances with defensive checks for idempotent execution | exp: func:_table_exists(table_name: str) → bool, call:sa.inspect(op.get_bind()).has_table, call:op.get_bind, func:_column_exists(table_name: str, column_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_columns, call:op.get_bind, func:_index_exists(table_name: str, index_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_indexes, call:op.get_bind, func:_foreign_key_exists(table_name: str, constrained_columns: list[str], referred_table: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, func:upgrade() → None, call:_table_exists, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.Text, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:_index_exists, call:op.create_index, call:sa.Integer, call:_column_exists, call:op.add_column, call:_foreign_key_exists, call:op.create_foreign_key, func:downgrade() → None, call:op.drop_index, call:op.drop_constraint, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- 0013_add_probe_result.py | Alembic database migration that adds a JSON probe_result column to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 0013_add_probe_result.py | Database migration to add a JSON probe_result column to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- 0014_add_profile_resolver_fields.py | Alembic database migration that adds profile resolver fields (project_id, tool_type_id, environment_variables, start_command, working_directory, port, is_default) to config_profiles table, renames mount_path to target_path and adds mode/files columns to config_mounts table while removing source_profile_id and content columns | exp: func:_table_exists(table_name: str) → bool, call:sa.inspect(op.get_bind()).has_table, call:op.get_bind, func:_column_exists(table_name: str, column_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_columns, call:op.get_bind, func:_index_exists(table_name: str, index_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_indexes, call:op.get_bind, func:_foreign_key_exists(table_name: str, constrained_columns: list[str], referred_table: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, func:_foreign_key_names_for_column(table_name: str, column_name: str) → list[str], call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, call:names.append, func:upgrade() → None, call:_column_exists, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:sa.JSON, call:sa.Text, call:sa.Integer, call:sa.Boolean, call:_foreign_key_exists, call:op.create_foreign_key, call:_index_exists, call:op.create_index, call:op.alter_column, call:sa.String, call:_foreign_key_names_for_column, call:op.drop_constraint, call:op.drop_column, func:downgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:sa.Text, call:op.drop_column, call:op.alter_column, call:op.drop_index, call:op.drop_constraint | dep: collections.abc, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 0014_add_profile_resolver_fields.py | Alembic database migration that adds profile resolver fields to config_profiles and config_mounts tables with idempotent schema checks | exp: func:_table_exists(table_name: str) → bool, call:sa.inspect(op.get_bind()).has_table, call:op.get_bind, func:_column_exists(table_name: str, column_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_columns, call:op.get_bind, func:_index_exists(table_name: str, index_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_indexes, call:op.get_bind, func:_foreign_key_exists(table_name: str, constrained_columns: list[str], referred_table: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, func:_foreign_key_names_for_column(table_name: str, column_name: str) → list[str], call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, call:names.append, func:upgrade() → None, call:_column_exists, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:sa.JSON, call:sa.Text, call:sa.Integer, call:sa.Boolean, call:_foreign_key_exists, call:op.create_foreign_key, call:_index_exists, call:op.create_index, call:op.alter_column, call:sa.String, call:_foreign_key_names_for_column, call:op.drop_constraint, call:op.drop_column, func:downgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:sa.Text, call:op.drop_column, call:op.alter_column, call:op.drop_index, call:op.drop_constraint | dep: collections.abc, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- 0014_merge_heads.py | Alembic merge migration that combines two divergent migration branches into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
- 0014_merge_heads.py | Alembic merge migration that reconciles two divergent migration branches without applying schema changes | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
||||||
- 0015_single_interface.py | Alembic database migration that replaces a plural JSON interfaces column with singular interface_type and requires_port columns on tool_types table | exp: func:_get_dialect() → str, call:op.get_bind, func:upgrade() → None, call:_get_dialect, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Boolean, call:op.execute, call:op.alter_column, call:op.drop_column, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:_get_dialect, call:op.drop_constraint, call:op.add_column, call:sa.Column, call:postgresql.JSONB, call:sa.Text, call:op.execute, call:sa.JSON, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 0015_single_interface.py | Alembic database migration that replaces a JSON array `interfaces` column with a single `interface_type` string column and adds a `requires_port` boolean column, with dialect-specific SQL for PostgreSQL and SQLite data migration. | exp: func:_get_dialect() → str, call:op.get_bind, func:upgrade() → None, call:_get_dialect, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Boolean, call:op.execute, call:op.alter_column, call:op.drop_column, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:_get_dialect, call:op.drop_constraint, call:op.add_column, call:sa.Column, call:postgresql.JSONB, call:sa.Text, call:op.execute, call:sa.JSON, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py | Alembic database migration that adds an ssh_key_id foreign key column to the config_profiles table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py | Alembic database migration that adds an ssh_key_id foreign key column to the config_profiles table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
||||||
- 20260527160017_add_pi_agent_tool_type.py | Alembic database migration that adds a new "pi-agent" tool type with terminal-based Docker development environment configuration | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text("SELECT id FROM tool_types WHERE name = 'pi-agent'") ).fetchone, call:sa.text, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text | dep: json, typing, alembic, uuid, sqlalchemy
|
- 20260527160017_add_pi_agent_tool_type.py | Adds a database migration that inserts a new "pi-agent" tool type into a tool_types table, defining a terminal-based Docker development environment with nvim, ranger, and tmux. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text("SELECT id FROM tool_types WHERE name = 'pi-agent'") ).fetchone, call:sa.text, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text | dep: json, typing, alembic, uuid, sqlalchemy
|
||||||
- 2026_05_22_add_clone_mode.py | Alembic database migration that adds ssh_key_id foreign key to git_repositories and clone_mode/branch columns to tool_instances | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:op.create_foreign_key, call:sa.String, func:downgrade() → None, call:op.drop_column, call:op.drop_constraint | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 2026_05_22_add_clone_mode.py | Alembic database migration that adds ssh_key_id foreign key to git_repositories table and clone_mode/branch columns to tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:op.create_foreign_key, call:sa.String, func:downgrade() → None, call:op.drop_column, call:op.drop_constraint | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- 2026_05_23_remove_is_builtin.py | Alembic database migration that removes the `is_builtin` column from the `tool_types` table with downgrade support to restore it. | exp: func:upgrade() → None, call:op.execute, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Boolean | dep: alembic, sqlalchemy
|
- 2026_05_23_remove_is_builtin.py | Alembic database migration that removes the `is_builtin` column from the `tool_types` table with a downgrade path to restore it. | exp: func:upgrade() → None, call:op.execute, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Boolean | dep: alembic, sqlalchemy
|
||||||
- 2026_05_24_220141_add_startup_command.py | Alembic database migration that adds a nullable `startup_command` text column to the `tool_types` table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
- 2026_05_24_220141_add_startup_command.py | Alembic database migration that adds a nullable startup_command column to the tool_types table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||||
- 2026_05_24_add_config_profiles.py | Alembic database migration that creates config_profiles and config_profile_includes tables with indexes, and adds a selected_config_profile_id foreign key to tool_instances | 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.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:sa.Integer, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
- 2026_05_24_add_config_profiles.py | Alembic database migration that creates config_profiles and config_profile_includes tables with indexes, and adds a selected_config_profile_id foreign key to tool_instances. | 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.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:sa.Integer, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
||||||
- 2026_05_26_add_git_mounts.py | Alembic database migration that adds a git_mounts JSON column to the config_profiles table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
- 2026_05_26_add_git_mounts.py | Database migration to add a `git_mounts` JSON column to the `config_profiles` table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
||||||
- 2026_05_27_external_repos.py | Alembic database migration that makes project_id nullable in git_repositories to support external repositories and expands alembic_version version_num column to prevent truncation. | 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 VARCHAR(64). | 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 check status. | 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 | Creates two database tables (instance_events and health_checks) for monitoring tool instances via an Alembic migration. | 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 foreign key to tool_instances for tracking terminal session lifecycle | 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 foreign key to tool_instances, including indexes and audit timestamps | 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 support to tool_types and tool_instances, and migrates the pi-agent tool from Dockerfile/compose templates to a JSON-based manifest system with a base Ubuntu image definition. | exp: func:upgrade() → None, call:op.get_bind, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:sa.ForeignKeyConstraint, call:sa.CheckConstraint, call:conn.execute, call:sa.text, call:result.fetchone, call:op.add_column, call:op.create_foreign_key, call:op.drop_constraint, call:op.execute, call:json.dumps, call:str, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_column, call:op.drop_constraint, call:op.drop_table | dep: json, uuid, typing, alembic, sqlalchemy
|
- 2026_05_28_add_tool_definition_manifests.py | Alembic database migration that creates a tool_definition_manifests table, adds manifest support to tool_types and tool_instances, and migrates the pi-agent tool to a manifest-based definition with a base Ubuntu image. | 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 | Database migration that drops the `tool_configs` and `config_folders` tables with conditional existence checks, and provides downgrade to recreate them. | 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 the tool_configs and config_folders tables with conditional existence checks, and provides downgrade to recreate them | 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, and severity-based notification records including read/dismissed tracking and optimized 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 | Creates a notifications table with user-linked, categorized, severity-graded messages supporting read/dismissed states and optimized querying via partial 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 ssh_key_ids column 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 ssh_key_ids column 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_drop_ssh_key_id_from_config_profiles.py | Alembic database migration that removes the ssh_key_id column from the config_profiles table | exp: func:upgrade() → None, call:op.drop_column, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey | dep: alembic, sqlalchemy
|
- 2026_05_29_drop_ssh_key_id_from_config_profiles.py | Alembic database migration that removes the ssh_key_id column from the config_profiles table | exp: func:upgrade() → None, call:op.drop_column, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey | dep: alembic, sqlalchemy
|
||||||
- 2026_05_29_fix_code_server_bind_addr.py | Alembic database migration that fixes code-server tool type compose templates by replacing deprecated --bind-addr flag with --host flag | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' AND compose_template LIKE '%--bind-addr%' """) ).fetchall, call:sa.text, call:compose_template.replace( "--bind-addr 0.0.0.0:8443", "--host 0.0.0.0" ).replace, call:print, func:downgrade() → None | dep: typing, alembic, sqlalchemy
|
- 2026_05_29_fix_code_server_bind_addr.py | Alembic database migration that fixes code-server tool type compose templates by replacing deprecated --bind-addr flag with --host flag | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' AND compose_template LIKE '%--bind-addr%' """) ).fetchall, call:sa.text, call:compose_template.replace( "--bind-addr 0.0.0.0:8443", "--host 0.0.0.0" ).replace, call:print, func:downgrade() → None | dep: typing, alembic, sqlalchemy
|
||||||
- 2026_05_29_fix_code_server_bind_addr_port.py | Alembic database migration that fixes code-server Docker compose templates by replacing broken `--host` flags with correct `--bind-addr 0.0.0.0:{port}` configurations in both database tool_type templates and on-disk instance compose files. | exp: func:_fix_tool_type_templates(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, default_port FROM tool_types WHERE name = 'code-server' AND compose_template LIKE '%--host%' """) ).fetchall, call:sa.text, call:compose_template.split, call:len, call:line.lstrip, call:new_lines.append, call:"\n".join, call:print, func:_fix_instance_compose_files(conn) → None, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:sa.text, call:print, call:conn.execute( sa.text(""" SELECT id, compose_path, tool_type_id FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:conn.execute( sa.text(""" SELECT default_port FROM tool_types WHERE id = :id """), {"id": tool_type_id}, ).fetchone, call:yaml.safe_load, call:data["services"].values, call:path.write_text, call:yaml.dump, func:upgrade() → None, call:op.get_bind, call:_fix_tool_type_templates, call:_fix_instance_compose_files, func:downgrade() → None | dep: typing, alembic, yaml, pathlib, sqlalchemy
|
- 2026_05_29_fix_code_server_bind_addr_port.py | Alembic database migration that fixes code-server Docker compose templates by replacing incorrect `--host` flags with proper `--bind-addr 0.0.0.0:PORT` configurations in both database-stored tool type templates and on-disk instance compose files. | exp: func:_fix_tool_type_templates(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, default_port FROM tool_types WHERE name = 'code-server' AND compose_template LIKE '%--host%' """) ).fetchall, call:sa.text, call:compose_template.split, call:len, call:line.lstrip, call:new_lines.append, call:"\n".join, call:print, func:_fix_instance_compose_files(conn) → None, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:sa.text, call:print, call:conn.execute( sa.text(""" SELECT id, compose_path, tool_type_id FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:conn.execute( sa.text(""" SELECT default_port FROM tool_types WHERE id = :id """), {"id": tool_type_id}, ).fetchone, call:yaml.safe_load, call:data["services"].values, call:path.write_text, call:yaml.dump, func:upgrade() → None, call:op.get_bind, call:_fix_tool_type_templates, call:_fix_instance_compose_files, func:downgrade() → None | dep: typing, alembic, yaml, pathlib, sqlalchemy
|
||||||
- 2026_05_29_fix_web_tool_bind_address.py | Alembic database migration that fixes code-server and jupyter-notebook tool compose templates to bind to 0.0.0.0 | exp: func:_fix_code_server_compose(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, definition_type FROM tool_types WHERE name = 'code-server' """) ).fetchone, call:sa.text, call:compose_template.split, call:enumerate, call:len, call:line.lstrip, call:new_lines.append, call:image_line.lstrip, call:new_lines.index, call:new_lines.insert, call:"\n".join, call:print, func:_fix_jupyter_compose(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, definition_type FROM tool_types WHERE name = 'jupyter-notebook' """) ).fetchone, call:sa.text, call:compose_template.split, call:enumerate, call:new_lines.append, call:len, call:line.lstrip, call:"\n".join, call:print, func:upgrade() → None, call:op.get_bind, call:_fix_code_server_compose, call:_fix_jupyter_compose, func:downgrade() → None | dep: typing, alembic, sqlalchemy
|
- 2026_05_29_fix_web_tool_bind_address.py | Alembic database migration that fixes web tool (code-server and jupyter-notebook) compose templates to bind to 0.0.0.0 for network accessibility. | exp: func:_fix_code_server_compose(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, definition_type FROM tool_types WHERE name = 'code-server' """) ).fetchone, call:sa.text, call:compose_template.split, call:enumerate, call:len, call:line.lstrip, call:new_lines.append, call:image_line.lstrip, call:new_lines.index, call:new_lines.insert, call:"\n".join, call:print, func:_fix_jupyter_compose(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, definition_type FROM tool_types WHERE name = 'jupyter-notebook' """) ).fetchone, call:sa.text, call:compose_template.split, call:enumerate, call:new_lines.append, call:len, call:line.lstrip, call:"\n".join, call:print, func:upgrade() → None, call:op.get_bind, call:_fix_code_server_compose, call:_fix_jupyter_compose, func:downgrade() → None | dep: typing, alembic, sqlalchemy
|
||||||
- 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 records and on-disk 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
|
- 2026_05_29_remove_lsio_command_override.py | Alembic database migration that removes broken command overrides containing --bind-addr or --host from LinuxServer.io (LSIO) 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
|
||||||
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py | Alembic database migration that removes (and can restore) the ssh_keys mount from the pi-agent tool definition manifest stored as JSON | 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 the ssh_keys mount from the pi-agent manifest in the tool_definition_manifests table, with downgrade support to restore it. | 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 indexes and adds a workspace_id foreign key 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 to make the `clone_mode` column nullable in the `tool_instances` table | exp: func:upgrade() → None, call:op.alter_column, call:sa.String, func:downgrade() → None, call:op.alter_column, call:sa.String | dep: alembic, sqlalchemy
|
- 2026_06_13_make_clone_mode_nullable.py | Alembic database migration that makes the `clone_mode` column in the `tool_instances` table nullable to allow NULL values for new rows. | exp: func:upgrade() → None, call:op.alter_column, call:sa.String, func:downgrade() → None, call:op.alter_column, call:sa.String | dep: alembic, sqlalchemy
|
||||||
- 2026_06_14_104415_add_tool_type_home_directory.py | Alembic database migration that adds a home_directory column to tool_types table and updates template strings from /workspace to a configurable path | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, func:downgrade() → None, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, call:op.drop_column | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
|
- 2026_06_14_104415_add_tool_type_home_directory.py | An Alembic database migration that adds a `home_directory` column to `tool_types` table and updates template strings from `/workspace` to a configurable `/home/user/{{WORKSPACE_NAME}}` path. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, func:downgrade() → None, call:op.execute, call:sa.update(tool_types) .where(tool_types.c.compose_template.is_not(None)) .values, call:tool_types.c.compose_template.is_not, call:sa.func.replace, call:sa.update(tool_types) .where(tool_types.c.dockerfile_template.is_not(None)) .values, call:tool_types.c.dockerfile_template.is_not, call:op.drop_column | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
|
||||||
- 2026_06_14_182955_fix_pi_agent_home_directory_mount.py | Alembic database migration that updates the pi-agent tool definition manifest to mount repos under the home directory instead of /workspace | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[Union[str, None], Union[dict, None]], call:conn.execute( sa.select(tool_definition_manifests.c.id, tool_definition_manifests.c.manifest) .where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select(tool_definition_manifests.c.id, tool_definition_manifests.c.manifest) .where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:manifest.setdefault, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:manifest.setdefault, call:_update_manifest | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
|
- 2026_06_14_182955_fix_pi_agent_home_directory_mount.py | Alembic database migration that fixes the pi-agent tool definition manifest's home directory mount path from /workspace to ~/{{WORKSPACE_NAME}} with a compatibility symlink and updated startup script. | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[Union[str, None], Union[dict, None]], call:conn.execute( sa.select(tool_definition_manifests.c.id, tool_definition_manifests.c.manifest) .where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select(tool_definition_manifests.c.id, tool_definition_manifests.c.manifest) .where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:manifest.setdefault, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:manifest.setdefault, call:_update_manifest | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
|
||||||
- 2026_06_15_090500_remove_pi_agent_explicit_repo_mount.py | Alembic database migration that removes explicit repo mounts from pi-agent tool definition manifests since they are now synthesized by compile_compose | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[Union[str, None], Union[dict, None]], call:conn.execute( sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.setdefault, call:any, call:mount.get, call:mounts.append, call:_update_manifest | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
|
- 2026_06_15_090500_remove_pi_agent_explicit_repo_mount.py | Alembic database migration that removes explicit repo mounts from the pi-agent tool definition manifest and restores them on downgrade | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[Union[str, None], Union[dict, None]], call:conn.execute( sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.get, call:mount.get, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.setdefault, call:any, call:mount.get, call:mounts.append, call:_update_manifest | dep: typing, alembic, sqlalchemy.sql, sqlalchemy
|
||||||
- 398082499c30_add_tool_config_fields.py | Alembic database migration that adds configuration fields (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
|
- 2026_06_19_113000_remove_pi_agent_workspace_symlink.py | Alembic database migration that updates the pi-agent tool definition manifest to remove the /workspace symlink dependency and adjust working directory and startup script accordingly. | exp: func:_find_pi_agent_manifest(conn: sa.Connection) → tuple[str | None, dict | None], call:conn.execute( sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where(tool_definition_manifests.c.name == "pi-agent") ).fetchone, call:sa.select( tool_definition_manifests.c.id, tool_definition_manifests.c.manifest ).where, call:dict, func:_update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) → None, call:conn.execute, call:sa.update(tool_definition_manifests) .where(tool_definition_manifests.c.id == manifest_id) .values, func:upgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.setdefault, call:runtime.get, call:_update_manifest, func:downgrade() → None, call:op.get_bind, call:_find_pi_agent_manifest, call:manifest.setdefault, call:_update_manifest | dep: collections.abc, alembic, sqlalchemy.sql, sqlalchemy
|
||||||
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py | Alembic database migration that merges two branches (removing is_builtin and adding config_profiles) with no actual schema changes | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
- 398082499c30_add_tool_config_fields.py | Alembic database migration that adds configuration fields (port_override, start_command, working_directory, environment_variables, volumes) to the tool_configs table with a port range validation 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
|
||||||
- 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
|
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py | Alembic merge migration that combines two parallel migration branches (remove_is_builtin and add_config_profiles) into a single revision history | 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
|
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py | Merges two Alembic migration branches (profile resolver and workspaces) into a single migration head | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
||||||
- 8ed7dd80973d_create_config_folders_table.py | Creates a database migration that adds a config_folders table for storing user configuration folders with JSONB metadata, foreign key relationships, and indexing. | 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, postgresql dialect
|
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py | Alembic database migration that removes or restores pi_state and pi_config mounts from a JSON manifest stored in the tool_definition_manifests table for the 'pi-agent' tool. | 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
|
||||||
- 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
|
- 8ed7dd80973d_create_config_folders_table.py | Alembic database migration that creates a config_folders table with UUID primary key, user foreign key, JSONB fields for files and project overrides, and supporting indexes/constraints | 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, postgresql dialect
|
||||||
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py | Alembic database migration that merges two parent revisions (single_interface and clone_mode) into a single branch point with no actual schema changes | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
- af8512103d67_add_tool_type_fields.py | Alembic database migration that adds columns (definition_type, dockerfile_template, build_context, readiness_probe) and a check constraint to the tool_types table. | 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 previous migrations (single_interface and clone_mode) into a single revision point | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
||||||
- fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py | Alembic database migration that merges two branch heads (home directory and pi agent mount cleanup) into a single revision point | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
- fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py | Alembic database migration that merges two branch heads (home directory and pi agent mount cleanup) into a single revision point | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
||||||
## arch
|
## arch
|
||||||
Linear migration history with branching/merge resolution pattern; each migration is an immutable, timestamped or sequenced script containing declarative SQLAlchemy operations (create_table, add_column, drop_column) with bidirectional upgrade/downgrade functions; includes data migrations, idempotent guards, and cross-references to on-disk file mutations.
|
Linear migration history with occasional branch/merge patterns (using merge heads), sequential numbered and timestamped revision files, each containing declarative schema changes (CREATE TABLE/ALTER TABLE/DROP TABLE) with idempotent guards, downgrade paths, and occasional data migrations; supports PostgreSQL and SQLite dialects.
|
||||||
## tags
|
## tags
|
||||||
column, table, call:op.drop, downgrade, upgrade, alembic, key, call:sa.text
|
column, table, call:op.drop, downgrade, upgrade, alembic, key, call:sa.text
|
||||||
## symbols
|
## symbols
|
||||||
@@ -76,5 +77,7 @@ column, table, call:op.drop, downgrade, upgrade, alembic, key, call:sa.text
|
|||||||
read: 2026_05_24_220141_add_startup_command.py, 2026_05_29_remove_lsio_command_override.py
|
read: 2026_05_24_220141_add_startup_command.py, 2026_05_29_remove_lsio_command_override.py
|
||||||
- change versions config
|
- change versions config
|
||||||
read: 0003_user_configs.py, 0009_tool_configs.py, 0013_add_config_profiles.py
|
read: 0003_user_configs.py, 0009_tool_configs.py, 0013_add_config_profiles.py
|
||||||
|
- explore versions subdirectories
|
||||||
|
index: apps/api/alembic/versions/.ruff_cache/.pi-map.index.md
|
||||||
## dirty
|
## dirty
|
||||||
-
|
-
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""remove pi agent workspace symlink
|
||||||
|
|
||||||
|
Revision ID: 2026_06_19_113000
|
||||||
|
Revises: 2026_06_15_090500
|
||||||
|
Create Date: 2026-06-19 11:30:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy.sql import column, table
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "2026_06_19_113000"
|
||||||
|
down_revision: str | Sequence[str] | None = "2026_06_15_090500"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
tool_definition_manifests = table(
|
||||||
|
"tool_definition_manifests",
|
||||||
|
column("id", sa.UUID),
|
||||||
|
column("name", sa.String),
|
||||||
|
column("manifest", sa.JSON),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_pi_agent_manifest(
|
||||||
|
conn: sa.Connection,
|
||||||
|
) -> tuple[str | None, dict | None]:
|
||||||
|
result = conn.execute(
|
||||||
|
sa.select(
|
||||||
|
tool_definition_manifests.c.id, tool_definition_manifests.c.manifest
|
||||||
|
).where(tool_definition_manifests.c.name == "pi-agent")
|
||||||
|
).fetchone()
|
||||||
|
if result is None:
|
||||||
|
return None, None
|
||||||
|
return result.id, dict(result.manifest)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_manifest(conn: sa.Connection, manifest_id: str, manifest: dict) -> None:
|
||||||
|
conn.execute(
|
||||||
|
sa.update(tool_definition_manifests)
|
||||||
|
.where(tool_definition_manifests.c.id == manifest_id)
|
||||||
|
.values(manifest=manifest)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
manifest_id, manifest = _find_pi_agent_manifest(conn)
|
||||||
|
if not manifest_id or not manifest:
|
||||||
|
return
|
||||||
|
|
||||||
|
runtime = manifest.setdefault("runtime", {})
|
||||||
|
# The /workspace compatibility symlink is no longer created by the
|
||||||
|
# compiler/entrypoint. Leaving working_dir set to /workspace causes the
|
||||||
|
# container to start in an empty directory. Let compile_compose default
|
||||||
|
# to /home/user/{workspace_name} instead.
|
||||||
|
if runtime.get("working_dir") == "/workspace":
|
||||||
|
del runtime["working_dir"]
|
||||||
|
|
||||||
|
scripts = manifest.setdefault("scripts", {})
|
||||||
|
# Update the startup script to operate on the real repo-named directory.
|
||||||
|
scripts["startup"] = [
|
||||||
|
'if [ -n "$WORKSPACE_NAME" ]; then sudo chown -R user:user "$HOME/$WORKSPACE_NAME" 2>/dev/null || true; fi',
|
||||||
|
]
|
||||||
|
|
||||||
|
_update_manifest(conn, manifest_id, manifest)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
conn = op.get_bind()
|
||||||
|
manifest_id, manifest = _find_pi_agent_manifest(conn)
|
||||||
|
if not manifest_id or not manifest:
|
||||||
|
return
|
||||||
|
|
||||||
|
runtime = manifest.setdefault("runtime", {})
|
||||||
|
runtime["working_dir"] = "/workspace"
|
||||||
|
|
||||||
|
scripts = manifest.setdefault("scripts", {})
|
||||||
|
scripts["startup"] = [
|
||||||
|
"if [ -d /workspace ]; then sudo chown -R user:user /workspace 2>/dev/null || true; fi",
|
||||||
|
]
|
||||||
|
|
||||||
|
_update_manifest(conn, manifest_id, manifest)
|
||||||
@@ -86,9 +86,7 @@ async def _resolve_container_user(
|
|||||||
if not tool_type.manifest_id:
|
if not tool_type.manifest_id:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
manifest_def = await db_session.get(
|
manifest_def = await db_session.get(ToolDefinitionManifest, tool_type.manifest_id)
|
||||||
ToolDefinitionManifest, tool_type.manifest_id
|
|
||||||
)
|
|
||||||
if not manifest_def:
|
if not manifest_def:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -336,66 +334,73 @@ async def _write_loop(session_ref: SessionRef, websocket, instance_id: str) -> N
|
|||||||
await session.write_input(message["bytes"])
|
await session.write_input(message["bytes"])
|
||||||
elif "text" in message:
|
elif "text" in message:
|
||||||
text = message["text"]
|
text = message["text"]
|
||||||
|
# A text frame that parses to a JSON object with a
|
||||||
|
# "type" field is a control message and must NEVER be
|
||||||
|
# written to the PTY (e.g. the heartbeat {"type":"pong"}
|
||||||
|
# must be consumed, not typed into the shell/pi). Handle
|
||||||
|
# known types and ignore unknown ones. Everything else
|
||||||
|
# (keystrokes, bracketed-paste content, plain text) is
|
||||||
|
# forwarded as raw terminal input.
|
||||||
|
ctrl = None
|
||||||
if text.startswith("{"):
|
if text.startswith("{"):
|
||||||
# Control message (JSON)
|
|
||||||
try:
|
try:
|
||||||
ctrl = json.loads(text)
|
parsed = json.loads(text)
|
||||||
msg_type = ctrl.get("type")
|
|
||||||
|
|
||||||
if msg_type == "resize":
|
|
||||||
cols = ctrl.get("cols", 80)
|
|
||||||
rows = ctrl.get("rows", 24)
|
|
||||||
logger.debug(
|
|
||||||
"Received resize message for instance %s: %sx%s",
|
|
||||||
instance_id,
|
|
||||||
cols,
|
|
||||||
rows,
|
|
||||||
)
|
|
||||||
await session.resize(cols, rows)
|
|
||||||
elif msg_type == "ack":
|
|
||||||
char_count = ctrl.get("chars", 0)
|
|
||||||
if char_count > 0:
|
|
||||||
session.acknowledge_data(char_count)
|
|
||||||
elif msg_type == "reset":
|
|
||||||
# Reset terminal session (scoped to current slot)
|
|
||||||
logger.debug(
|
|
||||||
"Resetting terminal session for instance %s (slot=%s)",
|
|
||||||
session.instance_id,
|
|
||||||
session_ref.slot_session_id,
|
|
||||||
)
|
|
||||||
await websocket.send_json(
|
|
||||||
{"type": "status", "status": "resetting"}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Reset the session scoped to its slot
|
|
||||||
new_session = await terminal_manager.reset_session(
|
|
||||||
session.instance_id,
|
|
||||||
session.container_id,
|
|
||||||
startup_command=session.startup_command,
|
|
||||||
session_id=session_ref.slot_session_id,
|
|
||||||
name=session.name,
|
|
||||||
container_user=session.container_user,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update the mutable session reference
|
|
||||||
session_ref.session = new_session
|
|
||||||
|
|
||||||
# Attach to new session
|
|
||||||
await terminal_manager.attach_websocket(
|
|
||||||
new_session, websocket
|
|
||||||
)
|
|
||||||
await websocket.send_json(
|
|
||||||
{"type": "status", "status": "connected"}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Continue the loop with the new session
|
|
||||||
continue
|
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
# Not a valid JSON control message, treat as regular input
|
parsed = None
|
||||||
await session.write_input(text.encode("utf-8"))
|
if isinstance(parsed, dict) and "type" in parsed:
|
||||||
else:
|
ctrl = parsed
|
||||||
|
|
||||||
|
if ctrl is None:
|
||||||
await session.write_input(text.encode("utf-8"))
|
await session.write_input(text.encode("utf-8"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
msg_type = ctrl["type"]
|
||||||
|
if msg_type == "resize":
|
||||||
|
cols = ctrl.get("cols", 80)
|
||||||
|
rows = ctrl.get("rows", 24)
|
||||||
|
logger.debug(
|
||||||
|
"Received resize message for instance %s: %sx%s",
|
||||||
|
instance_id,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
)
|
||||||
|
await session.resize(cols, rows)
|
||||||
|
elif msg_type == "ack":
|
||||||
|
char_count = ctrl.get("chars", 0)
|
||||||
|
if char_count > 0:
|
||||||
|
session.acknowledge_data(char_count)
|
||||||
|
elif msg_type == "reset":
|
||||||
|
# Reset terminal session (scoped to current slot)
|
||||||
|
logger.debug(
|
||||||
|
"Resetting terminal session for instance %s (slot=%s)",
|
||||||
|
session.instance_id,
|
||||||
|
session_ref.slot_session_id,
|
||||||
|
)
|
||||||
|
await websocket.send_json(
|
||||||
|
{"type": "status", "status": "resetting"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reset the session scoped to its slot
|
||||||
|
new_session = await terminal_manager.reset_session(
|
||||||
|
session.instance_id,
|
||||||
|
session.container_id,
|
||||||
|
startup_command=session.startup_command,
|
||||||
|
session_id=session_ref.slot_session_id,
|
||||||
|
name=session.name,
|
||||||
|
container_user=session.container_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the mutable session reference
|
||||||
|
session_ref.session = new_session
|
||||||
|
|
||||||
|
# Attach to new session
|
||||||
|
await terminal_manager.attach_websocket(new_session, websocket)
|
||||||
|
await websocket.send_json(
|
||||||
|
{"type": "status", "status": "connected"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Continue the loop with the new session
|
||||||
|
continue
|
||||||
elif message["type"] == "websocket.disconnect":
|
elif message["type"] == "websocket.disconnect":
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -12,12 +12,15 @@ import signal
|
|||||||
import struct
|
import struct
|
||||||
import fcntl
|
import fcntl
|
||||||
import time
|
import time
|
||||||
|
import tty
|
||||||
import uuid
|
import uuid
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_session_counters_by_instance: dict[str, int] = {}
|
||||||
|
|
||||||
|
|
||||||
class TerminalSession:
|
class TerminalSession:
|
||||||
"""Manages a single terminal session with event-driven PTY I/O.
|
"""Manages a single terminal session with event-driven PTY I/O.
|
||||||
@@ -45,9 +48,6 @@ class TerminalSession:
|
|||||||
# Max WebSocket frame size
|
# Max WebSocket frame size
|
||||||
MAX_FRAME_SIZE = 64 * 1024
|
MAX_FRAME_SIZE = 64 * 1024
|
||||||
|
|
||||||
# Session number counter per instance_id for auto-naming
|
|
||||||
_instance_counters: dict[str, int] = {}
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session_id: str,
|
session_id: str,
|
||||||
@@ -98,11 +98,11 @@ class TerminalSession:
|
|||||||
# Ack timeout fallback
|
# Ack timeout fallback
|
||||||
self._ack_timeout_handle: asyncio.TimerHandle | None = None
|
self._ack_timeout_handle: asyncio.TimerHandle | None = None
|
||||||
|
|
||||||
@classmethod
|
@staticmethod
|
||||||
def _generate_name(cls, instance_id: str) -> str:
|
def _generate_name(instance_id: str) -> str:
|
||||||
"""Generate an auto-incremented session name for the instance."""
|
"""Generate an auto-incremented session name for the instance."""
|
||||||
count = cls._instance_counters.get(instance_id, 0) + 1
|
count = _session_counters_by_instance.get(instance_id, 0) + 1
|
||||||
cls._instance_counters[instance_id] = count
|
_session_counters_by_instance[instance_id] = count
|
||||||
return f"Session {count}"
|
return f"Session {count}"
|
||||||
|
|
||||||
async def start(self, startup_command: str | None = None) -> None:
|
async def start(self, startup_command: str | None = None) -> None:
|
||||||
@@ -120,8 +120,21 @@ class TerminalSession:
|
|||||||
if self.container_user:
|
if self.container_user:
|
||||||
exec_cmd.extend(["--user", self.container_user])
|
exec_cmd.extend(["--user", self.container_user])
|
||||||
|
|
||||||
# Create a pseudo-terminal on the host
|
# Create a pseudo-terminal on the host. The master must be
|
||||||
|
# non-blocking: a browser paste can be larger than the PTY input
|
||||||
|
# buffer, and write_input() drains it asynchronously without dropping
|
||||||
|
# the closing bracketed-paste marker.
|
||||||
self._master_fd, slave_fd = pty.openpty()
|
self._master_fd, slave_fd = pty.openpty()
|
||||||
|
os.set_blocking(self._master_fd, False)
|
||||||
|
|
||||||
|
# Put the host PTY into raw mode so it behaves as a pass-through
|
||||||
|
# pipe. openpty() leaves the slave in canonical mode by default,
|
||||||
|
# which line-buffers input, splits multiline pastes at newlines,
|
||||||
|
# and mangles bracketed-paste markers before docker exec / the
|
||||||
|
# foreground app (e.g. pi) ever see them. Real terminal discipline
|
||||||
|
# (echo, canonical editing for readline) is provided by the
|
||||||
|
# in-container PTY that `docker exec -t` allocates.
|
||||||
|
tty.setraw(slave_fd)
|
||||||
|
|
||||||
# Set the terminal size initially
|
# Set the terminal size initially
|
||||||
self._set_terminal_size(self._cols, self._rows)
|
self._set_terminal_size(self._cols, self._rows)
|
||||||
@@ -194,6 +207,9 @@ class TerminalSession:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
data = os.read(self._master_fd, 4096)
|
data = os.read(self._master_fd, 4096)
|
||||||
|
except BlockingIOError:
|
||||||
|
# The readiness notification raced with another callback.
|
||||||
|
return
|
||||||
except (OSError, IOError) as exc:
|
except (OSError, IOError) as exc:
|
||||||
logger.debug("PTY read error for session %s: %s", self.session_id, exc)
|
logger.debug("PTY read error for session %s: %s", self.session_id, exc)
|
||||||
self._handle_eof()
|
self._handle_eof()
|
||||||
@@ -333,12 +349,42 @@ class TerminalSession:
|
|||||||
pass
|
pass
|
||||||
logger.info("Session %s EOF handled, websockets closed", self.session_id)
|
logger.info("Session %s EOF handled, websockets closed", self.session_id)
|
||||||
|
|
||||||
|
async def _wait_for_write_ready(self, fd: int) -> None:
|
||||||
|
"""Wait until a non-blocking PTY master can accept more input."""
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
writable = loop.create_future()
|
||||||
|
|
||||||
|
def mark_writable() -> None:
|
||||||
|
if not writable.done():
|
||||||
|
writable.set_result(None)
|
||||||
|
|
||||||
|
loop.add_writer(fd, mark_writable)
|
||||||
|
try:
|
||||||
|
await writable
|
||||||
|
finally:
|
||||||
|
loop.remove_writer(fd)
|
||||||
|
|
||||||
async def write_input(self, data: bytes) -> None:
|
async def write_input(self, data: bytes) -> None:
|
||||||
"""Write input to the PTY master."""
|
"""Write all terminal input bytes to the PTY master in order."""
|
||||||
if self._master_fd is None or self._closed:
|
if self._master_fd is None or self._closed:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
fd = self._master_fd
|
||||||
|
remaining = memoryview(data)
|
||||||
try:
|
try:
|
||||||
os.write(self._master_fd, data)
|
while remaining and not self._closed and self._master_fd == fd:
|
||||||
|
try:
|
||||||
|
written = os.write(fd, remaining)
|
||||||
|
except BlockingIOError:
|
||||||
|
await self._wait_for_write_ready(fd)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if written == 0:
|
||||||
|
await self._wait_for_write_ready(fd)
|
||||||
|
continue
|
||||||
|
|
||||||
|
remaining = remaining[written:]
|
||||||
|
|
||||||
self.last_activity = time.time()
|
self.last_activity = time.time()
|
||||||
except (OSError, IOError) as exc:
|
except (OSError, IOError) as exc:
|
||||||
logger.debug("PTY write error for session %s: %s", self.session_id, exc)
|
logger.debug("PTY write error for session %s: %s", self.session_id, exc)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/tests
|
dir: apps/api/tests
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides shared pytest fixtures and test utilities for FastAPI application testing across the API test suite.
|
Provides shared pytest fixtures and test utilities for FastAPI application testing
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/.pi-map.index.md
|
index: apps/api/.pi-map.index.md
|
||||||
map: apps/api/.pi-map.md
|
map: apps/api/.pi-map.md
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ dir: apps/api/tests
|
|||||||
index: apps/api/tests/.pi-map.index.md
|
index: apps/api/tests/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides shared pytest fixtures and test utilities for FastAPI application testing across the API test suite.
|
Provides shared pytest fixtures and test utilities for FastAPI application testing
|
||||||
## files
|
## files
|
||||||
- conftest.py | Provides shared pytest fixtures including test clients, database sessions, authenticated clients, and test data for FastAPI application testing. | 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.testclient.TestClient
|
- conftest.py | Provides shared pytest fixtures including test clients, database sessions, authenticated clients, and test data for FastAPI application testing. | 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.testclient.TestClient
|
||||||
## arch
|
## arch
|
||||||
Standard pytest fixture pattern with dependency injection for test clients, database sessions, and authentication state management.
|
Pytest plugin pattern with dependency injection fixtures for database sessions, HTTP clients, and authentication state management
|
||||||
## tags
|
## tags
|
||||||
call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user
|
call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/api/tests/unit
|
dir: apps/api/tests/unit
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Unit test suite for the API backend, covering configuration, Docker services, Git operations, event bus, file/SSH/terminal utilities, monitoring, notifications, and manifest compilation.
|
Contains isolated unit tests for individual components of the API application, covering configuration, services, utilities, and core business logic without external dependencies.
|
||||||
## parent
|
## parent
|
||||||
index: apps/api/tests/.pi-map.index.md
|
index: apps/api/tests/.pi-map.index.md
|
||||||
map: apps/api/tests/.pi-map.md
|
map: apps/api/tests/.pi-map.md
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ dir: apps/api/tests/unit
|
|||||||
index: apps/api/tests/unit/.pi-map.index.md
|
index: apps/api/tests/unit/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Unit test suite for the API backend, covering configuration, Docker services, Git operations, event bus, file/SSH/terminal utilities, monitoring, notifications, and manifest compilation.
|
Contains isolated unit tests for individual components of the API application, covering configuration, services, utilities, and core business logic without external dependencies.
|
||||||
## files
|
## files
|
||||||
- __init__.py | Empty package initialization file that marks a directory as a Python package.
|
- __init__.py | Empty package initialization file that marks a directory as a Python package.
|
||||||
- test_alembic_migrations.py | Verifies the structure, importability, and metadata of specific Alembic migration files without requiring a database connection. | exp: func:test_home_directory_migration_imports_and_rewrites() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_merge_migration_resolves_heads() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_remove_pi_agent_repo_mount_migration_imports() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable | dep: importlib.util, pathlib, pytest, pathlib.Path
|
- test_alembic_migrations.py | Unit tests that verify Alembic database migration files are importable, have correct revision identifiers, and declare expected dependencies without requiring a live database. | exp: func:test_home_directory_migration_imports_and_rewrites() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_merge_migration_resolves_heads() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_remove_pi_agent_repo_mount_migration_imports() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_remove_pi_agent_workspace_symlink_migration_imports() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable | dep: importlib.util, pathlib, pytest, pathlib.Path
|
||||||
- test_config.py | Unit tests verifying configuration settings including database URLs, auth defaults, and environment-specific cookie policies. | 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, src.config.Settings, src.database.build_database_url
|
- test_config.py | Unit tests verifying configuration settings including database URLs, auth defaults, and environment-specific cookie policies. | 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, src.config.Settings, src.database.build_database_url
|
||||||
- test_config_profile_resolver.py | Tests config profile resolution logic 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_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, pathlib.Path, sqlalchemy.ext.asyncio.AsyncSession
|
- test_config_profile_resolver.py | Tests config profile resolution logic 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_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, pathlib.Path, sqlalchemy.ext.asyncio.AsyncSession
|
||||||
- test_docker_build.py | Unit tests for the `build_image` Docker build service function, verifying successful builds, error handling, file writing, and security. | exp: class:TestBuildImage | dep: subprocess, tempfile, pathlib, unittest.mock, pytest, src.services.build.docker_build, pathlib.Path
|
- test_docker_build.py | Unit tests for the `build_image` Docker build service function, verifying successful builds, error handling, file writing, and security. | exp: class:TestBuildImage | dep: subprocess, tempfile, pathlib, unittest.mock, pytest, src.services.build.docker_build, pathlib.Path
|
||||||
@@ -32,7 +32,7 @@ Unit test suite for the API backend, covering configuration, Docker services, Gi
|
|||||||
- test_terminal_container_user.py | Unit tests for resolving container user from tool type definitions, handling legacy tools, manifest-based tools, base definition inheritance, and missing definitions. | exp: func:test_resolve_container_user_legacy_tool_type(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, call:session.get.assert_awaited_once, func:test_resolve_container_user_manifest_no_manifest_id(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:test_resolve_container_user_manifest_with_user(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:session_get(model, obj_id), func:test_resolve_container_user_manifest_with_base_definition(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:session_get(model, obj_id), func:test_resolve_container_user_missing_manifest_definition(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:session_get(model, obj_id) | dep: unittest.mock, pytest, src.api.system.terminal, src.api.system.terminal._resolve_container_user
|
- test_terminal_container_user.py | Unit tests for resolving container user from tool type definitions, handling legacy tools, manifest-based tools, base definition inheritance, and missing definitions. | exp: func:test_resolve_container_user_legacy_tool_type(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, call:session.get.assert_awaited_once, func:test_resolve_container_user_manifest_no_manifest_id(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:test_resolve_container_user_manifest_with_user(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:session_get(model, obj_id), func:test_resolve_container_user_manifest_with_base_definition(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:session_get(model, obj_id), func:test_resolve_container_user_missing_manifest_definition(), call:MagicMock, call:AsyncMock, call:_resolve_container_user, func:session_get(model, obj_id) | dep: unittest.mock, pytest, src.api.system.terminal, src.api.system.terminal._resolve_container_user
|
||||||
- test_terminal_session.py | Unit tests verifying TerminalSession passes container_user to docker exec --user flag when set, and omits it when not configured. | exp: func:test_start_passes_container_user_to_docker_exec() → None, call:TerminalSession, call:str, call:uuid.uuid4, call:patch, call:AsyncMock, call:session.start, call:args.index, func:test_start_omits_user_when_not_configured() → None, call:TerminalSession, call:str, call:uuid.uuid4, call:patch, call:AsyncMock, call:session.start | dep: uuid, unittest.mock, pytest, src.services.terminal.terminal_session
|
- test_terminal_session.py | Unit tests verifying TerminalSession passes container_user to docker exec --user flag when set, and omits it when not configured. | exp: func:test_start_passes_container_user_to_docker_exec() → None, call:TerminalSession, call:str, call:uuid.uuid4, call:patch, call:AsyncMock, call:session.start, call:args.index, func:test_start_omits_user_when_not_configured() → None, call:TerminalSession, call:str, call:uuid.uuid4, call:patch, call:AsyncMock, call:session.start | dep: uuid, unittest.mock, pytest, src.services.terminal.terminal_session
|
||||||
## arch
|
## arch
|
||||||
pytest-based unit testing with fixtures for temporary resources (git repos, Docker mocks), heavy use of mocking for external dependencies (Docker, databases), and parameterized tests for cross-platform/git-hosting scenarios.
|
Standard Python unittest/pytest package structure with test files mirroring the source codebase; uses dependency injection, mocking (likely unittest.mock or pytest-mock), and temporary filesystem/git fixtures to test components in isolation.
|
||||||
## tags
|
## tags
|
||||||
test, url, call:, git, user, call:notification, container, mounts
|
test, url, call:, git, user, call:notification, container, mounts
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -67,3 +67,23 @@ def test_remove_pi_agent_repo_mount_migration_imports() -> None:
|
|||||||
assert module.down_revision == "2026_06_14_182955"
|
assert module.down_revision == "2026_06_14_182955"
|
||||||
assert callable(module.upgrade)
|
assert callable(module.upgrade)
|
||||||
assert callable(module.downgrade)
|
assert callable(module.downgrade)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_remove_pi_agent_workspace_symlink_migration_imports() -> None:
|
||||||
|
migration_path = Path(__file__).parent.parent.parent / (
|
||||||
|
"alembic/versions/2026_06_19_113000_remove_pi_agent_workspace_symlink.py"
|
||||||
|
)
|
||||||
|
assert migration_path.exists()
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"remove_workspace_symlink_migration", migration_path
|
||||||
|
)
|
||||||
|
assert spec is not None and spec.loader is not None
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
assert module.revision == "2026_06_19_113000"
|
||||||
|
assert module.down_revision == "2026_06_15_090500"
|
||||||
|
assert callable(module.upgrade)
|
||||||
|
assert callable(module.downgrade)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Unit tests for TerminalSession docker exec invocation."""
|
"""Unit tests for TerminalSession PTY handling."""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest # pyright: ignore[reportMissingImports]
|
||||||
|
|
||||||
from src.services.terminal.terminal_session import TerminalSession
|
from src.services.terminal.terminal_session import TerminalSession
|
||||||
|
|
||||||
@@ -19,16 +19,20 @@ async def test_start_passes_container_user_to_docker_exec() -> None:
|
|||||||
container_user="dev",
|
container_user="dev",
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with (
|
||||||
"src.services.terminal.terminal_session.pty.openpty",
|
patch(
|
||||||
return_value=(1, 2),
|
"src.services.terminal.terminal_session.pty.openpty",
|
||||||
):
|
return_value=(1, 2),
|
||||||
with patch(
|
),
|
||||||
|
patch("src.services.terminal.terminal_session.os.set_blocking") as set_blocking,
|
||||||
|
patch("src.services.terminal.terminal_session.tty.setraw"),
|
||||||
|
patch(
|
||||||
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
|
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
|
||||||
new=AsyncMock(),
|
new=AsyncMock(),
|
||||||
) as mock_exec:
|
) as mock_exec,
|
||||||
with patch("src.services.terminal.terminal_session.os.close"):
|
patch("src.services.terminal.terminal_session.os.close"),
|
||||||
await session.start()
|
):
|
||||||
|
await session.start()
|
||||||
|
|
||||||
args, _kwargs = mock_exec.call_args
|
args, _kwargs = mock_exec.call_args
|
||||||
assert "docker" in args
|
assert "docker" in args
|
||||||
@@ -37,6 +41,7 @@ async def test_start_passes_container_user_to_docker_exec() -> None:
|
|||||||
user_index = args.index("--user")
|
user_index = args.index("--user")
|
||||||
assert args[user_index + 1] == "dev"
|
assert args[user_index + 1] == "dev"
|
||||||
assert "container-123" in args
|
assert "container-123" in args
|
||||||
|
set_blocking.assert_called_once_with(1, False)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
@@ -49,16 +54,44 @@ async def test_start_omits_user_when_not_configured() -> None:
|
|||||||
container_id="container-123",
|
container_id="container-123",
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with (
|
||||||
"src.services.terminal.terminal_session.pty.openpty",
|
patch(
|
||||||
return_value=(1, 2),
|
"src.services.terminal.terminal_session.pty.openpty",
|
||||||
):
|
return_value=(1, 2),
|
||||||
with patch(
|
),
|
||||||
|
patch("src.services.terminal.terminal_session.os.set_blocking"),
|
||||||
|
patch("src.services.terminal.terminal_session.tty.setraw"),
|
||||||
|
patch(
|
||||||
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
|
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
|
||||||
new=AsyncMock(),
|
new=AsyncMock(),
|
||||||
) as mock_exec:
|
) as mock_exec,
|
||||||
with patch("src.services.terminal.terminal_session.os.close"):
|
patch("src.services.terminal.terminal_session.os.close"),
|
||||||
await session.start()
|
):
|
||||||
|
await session.start()
|
||||||
|
|
||||||
args, _kwargs = mock_exec.call_args
|
args, _kwargs = mock_exec.call_args
|
||||||
assert "--user" not in args
|
assert "--user" not in args
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_write_input_retries_partial_pty_writes() -> None:
|
||||||
|
"""A large paste is fully written even when the PTY accepts it in chunks."""
|
||||||
|
session = TerminalSession(
|
||||||
|
session_id=str(uuid.uuid4()),
|
||||||
|
instance_id=uuid.uuid4(),
|
||||||
|
container_id="container-123",
|
||||||
|
)
|
||||||
|
session._master_fd = 42
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"src.services.terminal.terminal_session.os.write",
|
||||||
|
side_effect=[2, 2, 1],
|
||||||
|
) as write:
|
||||||
|
await session.write_input(b"hello")
|
||||||
|
|
||||||
|
assert [bytes(call.args[1]) for call in write.call_args_list] == [
|
||||||
|
b"hello",
|
||||||
|
b"llo",
|
||||||
|
b"o",
|
||||||
|
]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web
|
dir: apps/web
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Frontend web application serving as the main user interface for the "Headquarter" project, built with React and Vite.
|
Frontend web application for a terminal/code-editor interface named "Headquarter," built with React/TypeScript using Vite and served as a static single-page app.
|
||||||
## parent
|
## parent
|
||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
map: apps/.pi-map.md
|
map: apps/.pi-map.md
|
||||||
|
|||||||
+10
-10
@@ -4,21 +4,21 @@ dir: apps/web
|
|||||||
index: apps/web/.pi-map.index.md
|
index: apps/web/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Frontend web application serving as the main user interface for the "Headquarter" project, built with React and Vite.
|
Frontend web application for a terminal/code-editor interface named "Headquarter," built with React/TypeScript using Vite and served as a static single-page app.
|
||||||
## files
|
## files
|
||||||
- .env.example | Template file showing required environment variables for a Vite-based frontend application | dep: Vite
|
- .env.example | Template/example file defining environment variables for a Vite-based application's API and OAuth configuration | dep: Vite
|
||||||
- .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ES2022 module support | dep: eslint, @typescript-eslint/parser, @typescript-eslint/eslint-plugin
|
- .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ES2022 module support | dep: eslint, @typescript-eslint/parser, @typescript-eslint/eslint-plugin
|
||||||
- Dockerfile | Multi-stage Docker build for a Vite/Node.js frontend application, compiling static assets and serving them via nginx as a non-root user | dep: node:20-alpine, nginx:alpine, npm, wget
|
- Dockerfile | Multi-stage Docker build for a Node.js/Vite frontend application compiled to static assets and served by nginx as a non-root user | dep: node:20-alpine, nginx:alpine, npm, wget
|
||||||
- index.html | Serves as the entry point HTML file for a React application named "Headquarter" that loads a TypeScript module and uses Google Fonts. | dep: Google Fonts (Inter, IBM Plex Mono), React (implied by root div and main.tsx)
|
- index.html | Entry point HTML file for a React application named "Headquarter" that loads a TypeScript module and Google Fonts | dep: Google Fonts (Inter, IBM Plex Mono), main.tsx module
|
||||||
- nginx.conf | Configures Nginx as a web server for a single-page application with gzip compression, client-side routing support, cache control for static assets, and a health check endpoint. | dep: nginx
|
- nginx.conf | Configures Nginx as a web server for a single-page application with gzip compression, client-side routing support, cache control for static assets, and a health check endpoint. | dep: nginx
|
||||||
- package-lock.json | Auto-generated dependency lock file that records exact versions and resolved URLs of all npm packages for reproducible installs in the "headquarter-web" React web application. | dep: npm, Node.js, React, Vite, TailwindCSS, xterm, PrismJS, axios, react-router-dom, TypeScript, ESLint, Vitest, @testing-library/react, @phosphor-icons/react, react-simple-code-editor, jsdom, postcss, autoprefixer
|
- package-lock.json | Records exact dependency versions for a React-based web application to ensure reproducible installs | dep: npm, React, React Router, Tailwind CSS, Vite, Vitest, TypeScript, ESLint, xterm, PrismJS, axios, phosphor-icons
|
||||||
- package.json | Defines a React-based web application project named "headquarter-web" with configuration for building, development, testing, and linting using Vite and TypeScript. | dep: react, react-dom, react-router-dom, axios, tailwindcss, prismjs, xterm, @phosphor-icons/react, react-simple-code-editor, vite, typescript, vitest, eslint, @testing-library/react
|
- package.json | Defines a React-based web application project named "headquarter-web" with build tooling, testing, and UI dependencies for a terminal/code editor interface. | dep: react, react-dom, react-router-dom, vite, typescript, tailwindcss, axios, prismjs, xterm, @phosphor-icons/react, react-simple-code-editor, vitest, eslint
|
||||||
- tsconfig.json | Configures TypeScript compiler settings for a modern React project using Vite with ES modules | dep: TypeScript, Vite, React, DOM
|
- tsconfig.json | Configures TypeScript compiler options for a modern React/Vite project with ES2020 target and strict type checking | dep: TypeScript, React, Vite
|
||||||
- vite.config.ts | Configures Vite build tool for a React project with custom dev server port and testing setup | dep: vite, @vitejs/plugin-react
|
- vite.config.ts | Configures Vite build tool with React support, dev server port, and Vitest testing settings | dep: vite, @vitejs/plugin-react
|
||||||
## arch
|
## arch
|
||||||
Modern SPA architecture using React with TypeScript, Vite for build tooling, Docker multi-stage builds with nginx static serving, and environment-based configuration.
|
Containerized single-page React/TypeScript app using Vite for build/dev, Vitest for testing, ESLint for linting, and nginx for static asset delivery with client-side routing support.
|
||||||
## tags
|
## tags
|
||||||
react, vite, eslint, typescript, application, nginx, web, configures
|
react, vite, eslint, application, typescript, nginx, configures, web
|
||||||
## symbols
|
## symbols
|
||||||
-
|
-
|
||||||
## workflows
|
## workflows
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src
|
dir: apps/web/src
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Frontend web application entry point and routing layer for a React-based collaborative development platform.
|
Frontend web client that bootstraps and renders a React single-page application with routing, authentication, and session-driven UI.
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/.pi-map.index.md
|
index: apps/web/.pi-map.index.md
|
||||||
map: apps/web/.pi-map.md
|
map: apps/web/.pi-map.md
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ dir: apps/web/src
|
|||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Frontend web application entry point and routing layer for a React-based collaborative development platform.
|
Frontend web client that bootstraps and renders a React single-page application with routing, authentication, and session-driven UI.
|
||||||
## files
|
## files
|
||||||
- main.tsx | Entry point that 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 | Entry point that bootstraps a React SPA with routing, authentication, and session state management | 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 client-side routing configuration for a React application using nested routes with authentication protection 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 multi-page application with nested routes, protected authentication, 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 declarations for session users, projects, workspaces, and repositories in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
|
- types.ts | Defines TypeScript type declarations for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
|
||||||
## arch
|
## arch
|
||||||
Modular React SPA with provider-based dependency injection, nested route guards for authentication/authorization, and centralized TypeScript type definitions.
|
React SPA built with a declarative React Router configuration, protected/nested routes, and centralized TypeScript types for users, sessions, projects, repositories, and workspaces.
|
||||||
## tags
|
## tags
|
||||||
pages, styles, css, react, router, session, dom, project
|
pages, styles, css, router, react, session, dom, project
|
||||||
## symbols
|
## symbols
|
||||||
- AppRouter
|
- AppRouter
|
||||||
- SessionUser
|
- SessionUser
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components
|
dir: apps/web/src/components
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Reusable React UI components providing the application's shared visual elements, layout shell, authentication guards, and notification infrastructure.
|
Package components
|
||||||
## 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,22 +4,22 @@ 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 providing the application's shared visual elements, layout shell, authentication guards, and notification infrastructure.
|
Package components
|
||||||
## files
|
## files
|
||||||
- app-shell.tsx | Provides the main application shell layout with navigation, session management, and responsive mobile/desktop rendering for a React application. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../utils/open-session, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
|
- app-shell.tsx | Main application layout shell with responsive navigation, session management, and global state providers for notifications and toasts. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../utils/open-session, ../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-highlighting 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-highlighting 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 UI components for rendering loading, error, and empty data states. | 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 properties. | 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 sizes, weights, and colors. | exp: IconName, IconProps, Icon | dep: react, @phosphor-icons/react
|
||||||
- loading-overlay.tsx | Renders an accessible loading overlay with optional label when visible prop is true | exp: func:LoadingOverlay({ visible, label }: LoadingOverlayProps) | dep: ./icon, icon
|
- loading-overlay.tsx | Renders an accessible loading overlay with optional text label when visible | exp: func:LoadingOverlay({ visible, label }: LoadingOverlayProps) | dep: ./icon, icon
|
||||||
- protected-route.test.tsx | Tests a ProtectedRoute component that guards private content based on authentication state | dep: @testing-library/react, react-router-dom, vitest, ./protected-route, ../state/auth
|
- protected-route.test.tsx | Tests a ProtectedRoute component that guards content based on authentication state, showing loading or redirecting to login | dep: @testing-library/react, react-router-dom, vitest, ./protected-route
|
||||||
- protected-route.tsx | A React component that conditionally renders children or redirects to login based on authentication state, preserving the intended destination URL. | 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, language badge, and copy-to-clipboard functionality. | exp: SyntaxHighlighter | dep: react, ./icon, ../utils/language, React, icon, language utilities
|
- syntax-highlighter.tsx | A React component that renders syntax-highlighted code with optional line numbers and a copy-to-clipboard button | 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 | 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, InstanceEventPayload
|
- toast-rules.ts | Maps instance events to toast notifications with deduplication logic | 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, InstanceEventPayload
|
||||||
## arch
|
## arch
|
||||||
Component-based architecture with functional React patterns, separation of presentational components (data states, icons, syntax highlighting) from behavioral logic (toast rules, protected routes), and centralized design abstractions (icon mapping, code editor wrapping).
|
Contains 10 files.
|
||||||
## tags
|
## tags
|
||||||
toast, react, state, icon, code, loading, event, editor
|
toast, state, icon, react, loading, code, event, editor
|
||||||
## symbols
|
## symbols
|
||||||
- LoadingOverlay
|
- LoadingOverlay
|
||||||
- mapEventToCategory
|
- mapEventToCategory
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features
|
dir: apps/web/src/components/features
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Reusable UI feature components that compose domain-specific functionality for the web application.
|
Feature-specific UI components organized by business domain/capability for the web
|
||||||
## parent
|
## parent
|
||||||
index: apps/web/src/components/.pi-map.index.md
|
index: apps/web/src/components/.pi-map.index.md
|
||||||
map: apps/web/src/components/.pi-map.md
|
map: apps/web/src/components/.pi-map.md
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
|
|||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Reusable UI feature components that compose domain-specific functionality for the web application.
|
Feature-specific UI components organized by business domain/capability for the web
|
||||||
## files
|
## files
|
||||||
## arch
|
## arch
|
||||||
Feature-based component organization with domain-driven design patterns, likely combining presentational components with localized state and business logic.
|
Contains 0 files.
|
||||||
## tags
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/terminal
|
dir: apps/web/src/components/features/terminal
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides responsive, cross-platform terminal UI components with session management, virtual special keys, and WebSocket-backed xterm.js integration for web-based terminal access.
|
Provides the UI components for a responsive, multi-session terminal interface in the web app, including desktop and mobile layouts, session tabs, and special-key input controls.
|
||||||
## 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,19 +4,19 @@ 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 responsive, cross-platform terminal UI components with session management, virtual special keys, and WebSocket-backed xterm.js integration for web-based terminal access.
|
Provides the UI components for a responsive, multi-session terminal interface in the web app, including desktop and mobile layouts, session tabs, and special-key input controls.
|
||||||
## files
|
## files
|
||||||
- DesktopTerminalView.tsx | Renders a desktop terminal view with session tabs, fullscreen controls, and multiple terminal instances | exp: DesktopTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../../api/terminal
|
- DesktopTerminalView.tsx | Renders the desktop layout for a multi-session terminal UI, managing tabs, fullscreen mode, font controls, and reset confirmation. | exp: DesktopTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../../api/terminal, React, TerminalComponent, TerminalSessionTabs, TerminalSession
|
||||||
- MobileTerminalView.tsx | Renders a mobile-optimized terminal interface with session tabs, toolbar controls, special keys input, and keyboard-aware layout adjustments. | exp: MobileTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../icon, ./special-keys-strip, ./special-keys-panel, ../../../hooks/use-special-keys, ../../../api/terminal, terminal, terminal-session-tabs, icon, special-keys-strip, special-keys-panel, use-special-keys, api/terminal
|
- MobileTerminalView.tsx | Renders a mobile-specific terminal interface with session tabs, toolbar controls, special key inputs, and multiple terminal instances. | exp: MobileTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../icon, ./special-keys-strip, ./special-keys-panel, ../../../hooks/use-special-keys, ../../../api/terminal, React, TerminalComponent, TerminalSessionTabs, Icon, SpecialKeysStrip, SpecialKeysPanel
|
||||||
- special-keys-panel.tsx | Renders an overlay panel of special keyboard keys (Home, End, F1-F12, Ctrl+C, etc.) that sends terminal sequences when clicked and supports modifier key combinations. | exp: SpecialKeysPanel | dep: react, ../../../hooks/use-special-keys, React, use-special-keys hook
|
- special-keys-panel.tsx | Renders a modal overlay panel of special keyboard keys (Home, End, F1-F12, Ctrl+C, etc.) that sends escape sequences to a terminal when clicked. | exp: SpecialKeysPanel | dep: react, ../../../hooks/use-special-keys
|
||||||
- special-keys-strip.tsx | Renders a strip of virtual special keys (Esc, Tab, Ctrl, Alt, arrows, etc.) for sending terminal key sequences with optional modifier support. | exp: SpecialKeysStrip | dep: react, ../../../hooks/use-special-keys, use-special-keys hook
|
- special-keys-strip.tsx | Renders a touch-accessible strip of special keyboard keys (Esc, Tab, arrows, modifiers) that sends terminal escape sequences with optional modifier support. | exp: SpecialKeysStrip | dep: react, ../../../hooks/use-special-keys, React, use-special-keys hook
|
||||||
- terminal-session-tabs.test.tsx | Tests a React terminal session tabs component for rendering, selection, closing with confirmation, renaming, session limits, and connection status display. | dep: @testing-library/react, vitest, ./terminal-session-tabs
|
- terminal-session-tabs.test.tsx | Unit tests for a TerminalSessionTabs React component covering tab rendering, selection, close confirmation, inline renaming, session limit enforcement, and connection status display. | dep: @testing-library/react, vitest, ./terminal-session-tabs
|
||||||
- terminal-session-tabs.tsx | Renders an interactive tab bar for managing multiple terminal sessions with selection, renaming, closing, and creation capabilities | exp: TerminalSessionInfo, TerminalSessionTabsProps, TerminalSessionTabs | dep: react, React, useState, useRef, useCallback
|
- terminal-session-tabs.tsx | Renders a tabbed interface for managing multiple terminal sessions with support for switching, renaming, closing with confirmation, and creating new sessions. | exp: TerminalSessionInfo, TerminalSessionTabsProps, TerminalSessionTabs | dep: react, React
|
||||||
- terminal.tsx | A React terminal component that provides an interactive xterm.js-based terminal with WebSocket connectivity, mobile touch support, and session management. | exp: TerminalProps, TerminalRef, TerminalComponent | dep: react, xterm, xterm-addon-fit, xterm-addon-web-links, xterm/css/xterm.css, ../../../hooks/use-special-keys, React
|
- terminal.tsx | | 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 layers (desktop/mobile), modular special keys subsystems (strip/overlay panel), tab-based session management with React state, and xterm.js integration via WebSocket abstraction with touch/mobile adaptations.
|
Modular React component architecture organized by feature, with separate presentational components for desktop/mobile viewports, shared tab/session management, and helper components for terminal input handling.
|
||||||
## tags
|
## tags
|
||||||
terminal, special, keys, session, tabs, react, view, strip
|
terminal, special, session, keys, react, tabs, view, strip
|
||||||
## symbols
|
## symbols
|
||||||
- DesktopTerminalView
|
- DesktopTerminalView
|
||||||
- MobileTerminalView
|
- MobileTerminalView
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { getTerminalScrollbackLimit } from "./terminal.tsx";
|
||||||
|
|
||||||
|
describe("getTerminalScrollbackLimit", () => {
|
||||||
|
it("retains normal-buffer history for custom mobile swipe scrolling", () => {
|
||||||
|
expect(getTerminalScrollbackLimit(true)).toBe(10_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps desktop scrollback disabled to prevent stale-frame wheel scrolling", () => {
|
||||||
|
expect(getTerminalScrollbackLimit(false)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/tool
|
dir: apps/web/src/components/features/tool
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides UI components for managing containerized development tools, including instance lifecycle management, manifest configuration, and workspace-integrated tool launching.
|
Provides UI components for managing containerized development tools, including starting, configuring, monitoring, and editing tool instances across workspaces.
|
||||||
## 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,18 +4,18 @@ dir: apps/web/src/components/features/tool
|
|||||||
index: apps/web/src/components/features/tool/.pi-map.index.md
|
index: apps/web/src/components/features/tool/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Provides UI components for managing containerized development tools, including instance lifecycle management, manifest configuration, and workspace-integrated tool launching.
|
Provides UI components for managing containerized development tools, including starting, configuring, monitoring, and editing tool instances across workspaces.
|
||||||
## files
|
## files
|
||||||
- instance-list.tsx | React component that displays and manages a list of tool instances with CRUD operations, real-time status updates, and configuration profile selection. | exp: InstanceList | dep: react, react-router-dom, ../../icon, ../../loading-overlay, ../../../api/sessions, ../../../api/tool-types, ../session/create-session-form, ../../../api/config-profiles, ../../../api/ssh-keys, ../../../state/events, ../../../state/sessions
|
- instance-list.tsx | Displays and manages tool instances for a project/repo with start/stop/restart/delete actions, config profile selection, SSH key selection, and real-time status updates. | exp: InstanceList | dep: react, react-router-dom, ../../icon, ../../loading-overlay, ../../../api/sessions, ../../../api/tool-types, ../session/create-session-form, ../../../api/config-profiles, ../../../api/ssh-keys, ../../../state/events, ../../../state/sessions, Icon, LoadingOverlay, sessions API, config-profiles API, ssh-keys API, events state, sessions state
|
||||||
- manifest-editor.tsx | A React component that provides a form-based UI for editing container manifest configurations including base images, packages, environment variables, scripts, mounts, and runtime settings, with live preview generation capability. | exp: ManifestEditor | dep: react, ../../icon, ../../../utils/errors, ../../../api/tool-definitions, icon, errors utils, tool-definitions API
|
- manifest-editor.tsx | A React component that provides a form-based UI for editing container manifest configurations including base images, packages, environment variables, scripts, mounts, and runtime settings, with live preview compilation. | exp: ManifestEditor | dep: react, ../../icon, ../../../utils/errors, ../../../api/tool-definitions, icon, errors utils, tool-definitions API
|
||||||
- start-tool-fab.tsx | Renders a floating action button that opens a modal to select a workspace and start a tool. | exp: func:StartToolFAB(), call:useState, call:setOpen, call:setWorkspacesLoading, call:listAllWorkspaces, call:setWorkspaces, call:setSelectedWorkspace, call:e.stopPropagation, call:workspaces.find, call:workspaces.map | dep: react, ../../icon, ./tool-starter, ../../../types/workspace, ../../../api/workspaces, icon, tool-starter, workspace types, workspaces api
|
- start-tool-fab.tsx | Renders a floating action button that opens a modal for selecting a workspace and starting a tool. | exp: func:StartToolFAB(), call:useState, call:setOpen, call:setWorkspacesLoading, call:listAllWorkspaces, call:setWorkspaces, call:setSelectedWorkspace, call:e.stopPropagation, call:workspaces.find, call:workspaces.map | dep: react, ../../icon, ./tool-starter, ../../../types/workspace, ../../../api/workspaces
|
||||||
- start-tool-modal.tsx | React modal component for selecting a tool type and optional config profile to start a tool on a workspace | exp: StartToolModalProps, func:StartToolModal({ workspace, onClose, onStart, }: StartToolModalProps), call:useState, call:useAsyncData, call:e.preventDefault, call:setError, call:setSubmitting, call:onStart, call:onClose, call:e.stopPropagation, call:setToolTypeId, call:toolTypes?.map, call:setConfigProfileId | dep: react, ../../icon, ../../../api/tool-types, ../../../hooks/use-async-data, ../../../types/workspace
|
- start-tool-modal.tsx | A React modal component that allows users to select a tool type and optional configuration profile to start a tool on a specific workspace. | exp: StartToolModalProps, func:StartToolModal({ workspace, onClose, onStart, }: StartToolModalProps), call:useState, call:useAsyncData, call:e.preventDefault, call:setError, call:setSubmitting, call:onStart, call:onClose, call:e.stopPropagation, call:setToolTypeId, call:toolTypes?.map, call:setConfigProfileId | dep: react, ../../icon, ../../../api/tool-types, ../../../hooks/use-async-data, ../../../types/workspace, icon, tool-types, use-async-data, workspace
|
||||||
- tool-starter.tsx | React component for starting a tool instance within a workspace with configurable tool types, profiles, and SSH keys | exp: ToolStarterProps, func:ToolStarter({ workspace, onStarted, onCancel, }: ToolStarterProps), call:useSessions, call:useState, call:useEffect, call:listToolTypes, call:setToolTypes, call:setToolTypesError, call:setToolTypesLoading, call:load, call:setProfiles, call:setSelectedProfileId, call:setProfilesLoading, call:listConfigProfiles, call:data.find, call:listSSHKeys, call:setSshKeys, call:setSelectedSshKeyIds, call:console.error, call:setSshKeysLoading, call:sshKeys.find, call:useCallback, call:setError, call:setStarting, call:createInstance, call:displayName.trim, call:startInstance, call:addOrUpdateSession, call:onStarted, call:setSelectedToolTypeId, call:toolTypes.find, call:setDisplayName, call:toolTypes.map, call:setNameEdited, call:profiles.map, call:sshKeys.map, call:selectedSshKeyIds.includes, call:prev.filter | dep: react, ../../icon, ../../loading-overlay, ../../../api/tool-types, ../../../api/config-profiles, ../../../api/ssh-keys, ../../../state/sessions, ../../../types/workspace, ../../../api/sessions, icon, loading-overlay, tool-types, config-profiles, ssh-keys, sessions, workspace types, sessions api
|
- tool-starter.tsx | React component for starting a tool instance within a workspace context, fetching and configuring tool types, profiles, and SSH keys. | exp: ToolStarterProps, func:ToolStarter({ workspace, onStarted, onCancel, }: ToolStarterProps), call:useSessions, call:useState, call:useEffect, call:listToolTypes, call:setToolTypes, call:setToolTypesError, call:setToolTypesLoading, call:load, call:setProfiles, call:setSelectedProfileId, call:setProfilesLoading, call:listConfigProfiles, call:data.find, call:listSSHKeys, call:setSshKeys, call:setSelectedSshKeyIds, call:console.error, call:setSshKeysLoading, call:sshKeys.find, call:useCallback, call:setError, call:setStarting, call:createInstance, call:displayName.trim, call:startInstance, call:addOrUpdateSession, call:onStarted, call:setSelectedToolTypeId, call:toolTypes.find, call:setDisplayName, call:toolTypes.map, call:setNameEdited, call:profiles.map, call:sshKeys.map, call:selectedSshKeyIds.includes, call:prev.filter | dep: react, ../../icon, ../../loading-overlay, ../../../api/tool-types, ../../../api/config-profiles, ../../../api/ssh-keys, ../../../state/sessions, ../../../types/workspace, ../../../api/sessions, icon, loading-overlay, tool-types, config-profiles, ssh-keys, sessions, workspace
|
||||||
- tools-bottom-sheet.tsx | Renders a mobile bottom sheet navigation menu for tools with active route highlighting | exp: ToolsBottomSheet | dep: react-router-dom, ../../icon, icon
|
- tools-bottom-sheet.tsx | Renders a mobile bottom sheet navigation menu for tools with active route highlighting | exp: ToolsBottomSheet | dep: react-router-dom, ../../icon, icon
|
||||||
## arch
|
## arch
|
||||||
Feature-based React component architecture with modal/bottom-sheet mobile-responsive patterns, form-driven configuration editing with live preview, and workspace-contextual tool orchestration.
|
React component-based architecture with feature-specific composition, using modal/bottom-sheet patterns for mobile-responsive navigation, form-based configuration editors with live preview, and real-time status polling for instance management.
|
||||||
## tags
|
## tags
|
||||||
call:set, tool, types, start, call:use, ssh, react, loading
|
call:set, tool, types, start, ssh, call:use, icon, loading
|
||||||
## symbols
|
## symbols
|
||||||
- StartToolFAB
|
- StartToolFAB
|
||||||
- StartToolModal
|
- StartToolModal
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const ManifestEditor = ({
|
|||||||
const [startupScripts, setStartupScripts] = useState<string[]>([""]);
|
const [startupScripts, setStartupScripts] = useState<string[]>([""]);
|
||||||
const [mounts, setMounts] = useState<MountEntry[]>([]);
|
const [mounts, setMounts] = useState<MountEntry[]>([]);
|
||||||
const [command, setCommand] = useState<string[]>([""]);
|
const [command, setCommand] = useState<string[]>([""]);
|
||||||
const [workingDir, setWorkingDir] = useState("/workspace");
|
const [workingDir, setWorkingDir] = useState("");
|
||||||
const [stdinOpen, setStdinOpen] = useState(true);
|
const [stdinOpen, setStdinOpen] = useState(true);
|
||||||
const [tty, setTty] = useState(true);
|
const [tty, setTty] = useState(true);
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ export const ManifestEditor = ({
|
|||||||
|
|
||||||
const runtime = (manifest.runtime as Record<string, unknown>) || {};
|
const runtime = (manifest.runtime as Record<string, unknown>) || {};
|
||||||
setCommand((runtime.command as string[]) || [""]);
|
setCommand((runtime.command as string[]) || [""]);
|
||||||
setWorkingDir((runtime.working_dir as string) || "/workspace");
|
setWorkingDir((runtime.working_dir as string) || "");
|
||||||
setStdinOpen((runtime.stdin_open as boolean) ?? true);
|
setStdinOpen((runtime.stdin_open as boolean) ?? true);
|
||||||
setTty((runtime.tty as boolean) ?? true);
|
setTty((runtime.tty as boolean) ?? true);
|
||||||
}, [manifest]);
|
}, [manifest]);
|
||||||
@@ -561,7 +561,8 @@ export const ManifestEditor = ({
|
|||||||
<textarea
|
<textarea
|
||||||
value={script}
|
value={script}
|
||||||
onChange={(e) => updateStartupScript(idx, e.target.value)}
|
onChange={(e) => updateStartupScript(idx, e.target.value)}
|
||||||
placeholder="chown -R user:user /workspace"
|
placeholder="chown -R user:user $HOME/$WORKSPACE_NAME"
|
||||||
|
|
||||||
className="form-input font-mono text-sm flex-1"
|
className="form-input font-mono text-sm flex-1"
|
||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
@@ -723,7 +724,7 @@ export const ManifestEditor = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={workingDir}
|
value={workingDir}
|
||||||
onChange={(e) => setWorkingDir(e.target.value)}
|
onChange={(e) => setWorkingDir(e.target.value)}
|
||||||
placeholder="/workspace"
|
placeholder="e.g., /workspace or ~/code"
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -169,6 +169,10 @@ export const useTerminalPage = () => {
|
|||||||
const isAltShift = e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey;
|
const isAltShift = e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey;
|
||||||
if (!isAltShift) return;
|
if (!isAltShift) return;
|
||||||
|
|
||||||
|
const activeSessionIndex = activeSessionId
|
||||||
|
? sessions.findIndex((session) => session.id === activeSessionId)
|
||||||
|
: -1;
|
||||||
|
|
||||||
switch (e.key.toLowerCase()) {
|
switch (e.key.toLowerCase()) {
|
||||||
case "n":
|
case "n":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -187,17 +191,14 @@ export const useTerminalPage = () => {
|
|||||||
break;
|
break;
|
||||||
case "arrowleft":
|
case "arrowleft":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (activeSessionId) {
|
if (activeSessionIndex > 0) {
|
||||||
const idx = sessions.findIndex((s) => s.id === activeSessionId);
|
setActiveSessionId(sessions[activeSessionIndex - 1].id);
|
||||||
if (idx > 0) setActiveSessionId(sessions[idx - 1].id);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "arrowright":
|
case "arrowright":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (activeSessionId) {
|
if (activeSessionIndex >= 0 && activeSessionIndex < sessions.length - 1) {
|
||||||
const idx = sessions.findIndex((s) => s.id === activeSessionId);
|
setActiveSessionId(sessions[activeSessionIndex + 1].id);
|
||||||
if (idx < sessions.length - 1)
|
|
||||||
setActiveSessionId(sessions[idx + 1].id);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "r":
|
case "r":
|
||||||
@@ -208,6 +209,8 @@ export const useTerminalPage = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsFullscreen((prev) => !prev);
|
setIsFullscreen((prev) => !prev);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { ProjectsPage } from "./ProjectsPage";
|
import { ProjectsPage } from "./ProjectsPage";
|
||||||
import * as projectsApi from "../api/projects";
|
import * as projectsApi from "../api/projects";
|
||||||
|
import { SessionsProvider } from "../state/sessions";
|
||||||
|
import type { ProjectWithRepos } from "../types";
|
||||||
|
|
||||||
const mockProjects = [
|
vi.mock("../api/sessions", () => ({
|
||||||
|
getUserSessions: vi.fn().mockResolvedValue([]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockProjects: ProjectWithRepos[] = [
|
||||||
{
|
{
|
||||||
id: "proj-1",
|
id: "proj-1",
|
||||||
name: "Alpha Project",
|
name: "Alpha Project",
|
||||||
description: "First project",
|
description: "First project",
|
||||||
owner_id: "user-1",
|
owner_id: "user-1",
|
||||||
default_ssh_key_id: null,
|
default_ssh_key_id: null,
|
||||||
|
repositories: [],
|
||||||
|
created_at: "2026-07-01T00:00:00Z",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "proj-2",
|
id: "proj-2",
|
||||||
@@ -19,6 +28,8 @@ const mockProjects = [
|
|||||||
description: null,
|
description: null,
|
||||||
owner_id: "user-1",
|
owner_id: "user-1",
|
||||||
default_ssh_key_id: null,
|
default_ssh_key_id: null,
|
||||||
|
repositories: [],
|
||||||
|
created_at: "2026-07-01T00:00:00Z",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -31,9 +42,7 @@ describe("ProjectsPage", () => {
|
|||||||
it("renders loading state initially", () => {
|
it("renders loading state initially", () => {
|
||||||
vi.spyOn(projectsApi, "listProjects").mockImplementation(() => new Promise(() => {}));
|
vi.spyOn(projectsApi, "listProjects").mockImplementation(() => new Promise(() => {}));
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
|
expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -41,9 +50,7 @@ describe("ProjectsPage", () => {
|
|||||||
it("renders project list after loading", async () => {
|
it("renders project list after loading", async () => {
|
||||||
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
|
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -56,9 +63,7 @@ describe("ProjectsPage", () => {
|
|||||||
it("renders empty state when no projects", async () => {
|
it("renders empty state when no projects", async () => {
|
||||||
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
|
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -69,9 +74,7 @@ describe("ProjectsPage", () => {
|
|||||||
it("renders error state with retry button", async () => {
|
it("renders error state with retry button", async () => {
|
||||||
vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail"));
|
vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail"));
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -85,9 +88,7 @@ describe("ProjectsPage", () => {
|
|||||||
const createMock = vi.spyOn(projectsApi, "createProject").mockResolvedValue(mockProjects[0]);
|
const createMock = vi.spyOn(projectsApi, "createProject").mockResolvedValue(mockProjects[0]);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -118,9 +119,7 @@ describe("ProjectsPage", () => {
|
|||||||
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
|
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -138,16 +137,14 @@ describe("ProjectsPage", () => {
|
|||||||
const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]);
|
const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
|
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
|
const alphaCard = screen.getByText("Alpha Project").closest(".project-list-item") as HTMLElement | null;
|
||||||
if (!alphaCard) throw new Error("Card not found");
|
if (!alphaCard) throw new Error("Card not found");
|
||||||
|
|
||||||
fireEvent.click(within(alphaCard).getByRole("button", { name: /edit/i }));
|
fireEvent.click(within(alphaCard).getByRole("button", { name: /edit/i }));
|
||||||
@@ -171,16 +168,14 @@ describe("ProjectsPage", () => {
|
|||||||
const deleteMock = vi.spyOn(projectsApi, "deleteProject").mockResolvedValue(undefined);
|
const deleteMock = vi.spyOn(projectsApi, "deleteProject").mockResolvedValue(undefined);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
|
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
|
const alphaCard = screen.getByText("Alpha Project").closest(".project-list-item") as HTMLElement | null;
|
||||||
if (!alphaCard) throw new Error("Card not found");
|
if (!alphaCard) throw new Error("Card not found");
|
||||||
|
|
||||||
fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));
|
fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));
|
||||||
|
|||||||
@@ -152,7 +152,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.font-mono {
|
.font-mono {
|
||||||
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
font-family:
|
||||||
|
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-medium {
|
.font-medium {
|
||||||
@@ -938,6 +940,14 @@ a.nav-item,
|
|||||||
overscroll-behavior: auto;
|
overscroll-behavior: auto;
|
||||||
max-height: 100% !important;
|
max-height: 100% !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
|
/* Full-screen TUI tools repaint in place; scrollback is 0 (see terminal.tsx).
|
||||||
|
Hide the viewport scrollbar so it never appears or captures input. */
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-container .xterm-viewport::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* On mobile the custom touch handler scrolls the buffer; disable native
|
/* On mobile the custom touch handler scrolls the buffer; disable native
|
||||||
@@ -3809,7 +3819,9 @@ a:active,
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
transition: background 0.12s ease, color 0.12s ease;
|
transition:
|
||||||
|
background 0.12s ease,
|
||||||
|
color 0.12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-btn:hover {
|
.delete-btn:hover {
|
||||||
@@ -3836,7 +3848,9 @@ a:active,
|
|||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: background 0.12s ease, border-color 0.12s ease;
|
transition:
|
||||||
|
background 0.12s ease,
|
||||||
|
border-color 0.12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-header-action-btn:hover {
|
.workspace-header-action-btn:hover {
|
||||||
@@ -3911,7 +3925,9 @@ a:active,
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.12s ease, color 0.12s ease;
|
transition:
|
||||||
|
background 0.12s ease,
|
||||||
|
color 0.12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-button:hover {
|
.copy-button:hover {
|
||||||
@@ -4157,7 +4173,10 @@ a:active,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.drag-item-active {
|
.drag-item-active {
|
||||||
background: var(--brand-light, color-mix(in srgb, var(--brand) 10%, transparent));
|
background: var(
|
||||||
|
--brand-light,
|
||||||
|
color-mix(in srgb, var(--brand) 10%, transparent)
|
||||||
|
);
|
||||||
border-color: var(--brand);
|
border-color: var(--brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4173,7 +4192,9 @@ a:active,
|
|||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
font-family:
|
||||||
|
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
monospace;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
line-height: var(--line-height-normal);
|
line-height: var(--line-height-normal);
|
||||||
}
|
}
|
||||||
@@ -4321,7 +4342,9 @@ a:active,
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: border-color 0.15s, color 0.15s;
|
transition:
|
||||||
|
border-color 0.15s,
|
||||||
|
color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-create-button:hover {
|
.sidebar-create-button:hover {
|
||||||
@@ -4346,7 +4369,9 @@ a:active,
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
transition:
|
||||||
|
transform 0.15s ease,
|
||||||
|
box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-fab:hover {
|
.mobile-fab:hover {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Restore Mobile Terminal Scrolling
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The recent terminal scrollback optimization disabled scrollback for every viewport. Mobile terminal swipe handling still scrolls xterm's normal buffer programmatically, so swipes in normal-buffer tools no longer have retained output to move through.
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
|
||||||
|
`468f342` changed the terminal configuration to `scrollback: 0` globally to prevent stale repaint frames and wheel scrolling on desktop. The mobile touch handler calls `term.scrollLines()` when the normal buffer is active. With zero scrollback, that call has no scrollable history and becomes a no-op.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- `apps/web/src/components/features/terminal/terminal.tsx`
|
||||||
|
- Focused terminal configuration test
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
|
||||||
|
Retain a bounded xterm scrollback buffer on mobile only (`10000` lines), while leaving desktop at zero scrollback and with wheel sensitivity disabled. Mobile's existing custom touch handler remains responsible for moving through normal-buffer history; alternate-screen swipes continue to send SGR wheel events to the active TUI.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] A mobile terminal with normal-buffer output exceeding one screen scrolls via a vertical swipe.
|
||||||
|
- [ ] Alternate-screen terminal scrolling continues to use the existing SGR wheel-event path.
|
||||||
|
- [ ] Desktop keeps zero xterm scrollback and disabled native wheel scrolling, so stale repaint frames do not return.
|
||||||
|
- [ ] Focused unit test and frontend quality gates pass.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- `468f342 fix(terminal): hide scrollbar and stop stale-frame wheel scroll for TUI tools`
|
||||||
|
- `openspec/changes/fix-terminal-container-overflow`
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Restore Mobile Terminal Scrolling — Tasks
|
||||||
|
|
||||||
|
- [x] Add a mobile-specific terminal scrollback limit while preserving zero scrollback on desktop.
|
||||||
|
- [x] Reinitialize the terminal when the responsive mobile classification changes so its scrollback and touch handler match the active viewport.
|
||||||
|
- [x] Add focused tests for the responsive scrollback configuration.
|
||||||
|
- [x] Run the full frontend test suite (89 tests passed after repairing the `ProjectsPage` test setup).
|
||||||
|
- [x] Run frontend typecheck, lint, focused tests, and production build.
|
||||||
|
- [ ] Perform mobile normal-buffer and alternate-screen manual QA.
|
||||||
|
- [ ] Update project maps for changed source files (the map patch tool currently fails with an unsupported `temperature` parameter).
|
||||||
Reference in New Issue
Block a user