merge: integrate main restructuring into dev
- Resolve 57 merge conflicts from codebase restructure - Port dev feature code to new directory structure: * Update import paths to use @/ aliases * Add backward-compatible API signatures (createInstance, startInstance, deleteInstance) * Add missing type exports (ProjectWithRepos, InstanceHealth, Branch, BranchesResponse) * Extend Session and GitRepository types for dev features * Extend TerminalComponent props for mobile terminal wrapper * Add missing icon names (bell, drag, undo) Quality gates: tsc pass (0 errors), build pass, 127/131 tests pass (4 pre-existing failures unrelated to merge)
This commit is contained in:
@@ -26,6 +26,7 @@ User guides for each feature:
|
||||
- [Repositories](features/repositories.md) - Git repository management
|
||||
- [Workspace](features/workspace.md) - Repository workspace
|
||||
- [Git History](features/git-history.md) - History visualization
|
||||
- [Web Terminal](features/terminal.md) - Interactive terminal for tool instances
|
||||
- [Authentication](features/auth.md) - Login and user management
|
||||
- [Settings](features/settings.md) - User preferences
|
||||
- [Tool Types](features/tool-types.md) - Development tool management
|
||||
|
||||
@@ -36,6 +36,7 @@ All responses are JSON. Error responses follow this format:
|
||||
- [Config Profiles](config-profiles.md) - Config profile management with git mounts
|
||||
- [Users](users.md) - User management and settings
|
||||
- [Tool Types](tool-types.md) - Tool type management
|
||||
- [Config Profiles](config-profiles.md) - Config profile management for tool instances
|
||||
- [SSH Keys](ssh-keys.md) - SSH key management
|
||||
|
||||
## Testing
|
||||
|
||||
+402
-134
@@ -1,165 +1,433 @@
|
||||
# Config Profiles
|
||||
# Config Profiles API
|
||||
|
||||
## Overview
|
||||
Config profile management endpoints for customizing tool instances.
|
||||
|
||||
Config profiles allow users to define reusable configuration sets for tool instances. Profiles can include environment variables, files, mounts, and git repository mounts. They support profile includes for composition and can be scoped to specific projects or tool types.
|
||||
## Authentication
|
||||
|
||||
## Git Mounts
|
||||
All endpoints require authentication (session cookie).
|
||||
|
||||
Git mounts allow you to mount files or directories from git repositories into tool instances at startup.
|
||||
---
|
||||
|
||||
### Git Mount Object
|
||||
## GET /config-profiles
|
||||
|
||||
**Description:** List all config profiles for the current user.
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `tool_type_id` | `string` | No | Filter by tool type compatibility (currently returns all profiles) |
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"repo_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"source_path": ".",
|
||||
"target_path": "/app/config",
|
||||
"branch": "main"
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `repo_id` | string (UUID) | Yes | ID of the git repository to mount from |
|
||||
| `source_path` | string | No | Path within the repository (default: "."). Supports glob patterns like "*.json" or "configs/**" |
|
||||
| `target_path` | string | Yes | Absolute path inside the container where files will be mounted |
|
||||
| `branch` | string | No | Branch or tag to checkout before mounting (default: current branch) |
|
||||
|
||||
### Path Validation
|
||||
|
||||
- `source_path`: Must be relative (no leading `/`). Cannot contain `..` (path traversal)
|
||||
- `target_path`: Must be absolute (starts with `/`). Cannot contain `..`
|
||||
|
||||
### Glob Patterns
|
||||
|
||||
The `source_path` supports standard glob patterns:
|
||||
|
||||
- `*.json` - Match all JSON files in root
|
||||
- `configs/**` - Match all files in configs directory recursively
|
||||
- `src/*.py` - Match all Python files in src directory
|
||||
- `.` - Mount entire repository (default)
|
||||
|
||||
**Limits:**
|
||||
- Maximum 100 matches per glob pattern
|
||||
- Only matches within the repository boundary
|
||||
|
||||
### Branch Behavior
|
||||
|
||||
When a `branch` is specified:
|
||||
|
||||
1. System attempts to checkout the branch in the existing clone
|
||||
2. If branch doesn't exist locally, attempts to fetch from remote and checkout
|
||||
3. If checkout fails, logs warning and continues with current branch
|
||||
4. No branch specified: uses current checked-out branch
|
||||
|
||||
**Auto-clone:** If repository is not cloned locally, the system will automatically clone it using the repository's configured SSH key.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### List Config Profiles
|
||||
|
||||
```
|
||||
GET /config-profiles
|
||||
```
|
||||
|
||||
Query parameters:
|
||||
- `project_id` (optional): Filter by project compatibility
|
||||
- `tool_type_id` (optional): Filter by tool type compatibility
|
||||
|
||||
Response includes `git_mounts` array in each profile.
|
||||
|
||||
### Create Config Profile
|
||||
|
||||
```
|
||||
POST /config-profiles
|
||||
```
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"name": "My Profile",
|
||||
"git_mounts": [
|
||||
"profiles": [
|
||||
{
|
||||
"repo_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"source_path": "configs/*.json",
|
||||
"target_path": "/app/config",
|
||||
"branch": "main"
|
||||
"id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"name": "my-profile",
|
||||
"description": "My custom profile",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Validation:
|
||||
- All referenced repositories must exist
|
||||
- Repositories must belong to the same project (if profile has project_id)
|
||||
- source_path and target_path must pass path validation
|
||||
---
|
||||
|
||||
### Update Config Profile
|
||||
## POST /config-profiles
|
||||
|
||||
```
|
||||
PUT /config-profiles/{id}
|
||||
```
|
||||
**Description:** Create a new config profile.
|
||||
|
||||
Same request body as create. Partial updates supported (omit fields to keep current values).
|
||||
### Request
|
||||
|
||||
### Preview Resolved Profile
|
||||
#### Request Body
|
||||
|
||||
```
|
||||
GET /config-profiles/{id}/preview
|
||||
```
|
||||
|
||||
Returns the fully resolved profile with all includes merged. Git mounts from included profiles are merged with override rules (later profiles override earlier ones with same repo_id + target_path combo).
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"profile_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"profile_name": "My Profile",
|
||||
"env_vars": {},
|
||||
"runtime_hints": {},
|
||||
"mounts": [],
|
||||
"git_mounts": [
|
||||
{
|
||||
"repo_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"source_path": "configs/*.json",
|
||||
"target_path": "/app/config",
|
||||
"branch": "main"
|
||||
}
|
||||
],
|
||||
"files": {},
|
||||
"overrides": {
|
||||
"env_vars": {},
|
||||
"runtime_hints": {},
|
||||
"files": {},
|
||||
"mounts": {}
|
||||
},
|
||||
"included_profiles": []
|
||||
"name": "my-profile",
|
||||
"description": "My custom profile"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | `string` | Yes | Unique profile name (max 255 chars) |
|
||||
| `description` | `string` | No | Optional description |
|
||||
|
||||
Git mount errors during instance startup are non-blocking:
|
||||
- Missing repository: Mount skipped, warning logged
|
||||
- Clone failure: Mount skipped, warning logged
|
||||
- Invalid paths: Mount skipped, warning logged
|
||||
- Branch checkout failure: Falls back to current branch, warning logged
|
||||
### Response
|
||||
|
||||
Instance startup continues normally even if some git mounts fail.
|
||||
#### Success (201 Created)
|
||||
|
||||
## Profile Resolution
|
||||
Returns created profile.
|
||||
|
||||
When a profile includes other profiles, git mounts are merged:
|
||||
- Same `repo_id` + `target_path` combo: later profile overrides
|
||||
- Different combos: both are kept
|
||||
- Branch conflicts: later profile wins
|
||||
#### Error (409 Conflict)
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"detail": "config profile with name 'my-profile' already exists"
|
||||
}
|
||||
```
|
||||
Base Profile: git_mounts = [{repo_a, /app, main}]
|
||||
Included Profile: git_mounts = [{repo_a, /app, develop}, {repo_b, /data}]
|
||||
Resolved: git_mounts = [{repo_a, /app, develop}, {repo_b, /data}]
|
||||
|
||||
#### Error (422 Unprocessable Entity)
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Profile name cannot be empty"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET /config-profiles/{profile_id}
|
||||
|
||||
**Description:** Get a config profile with its includes and mounts.
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"name": "my-profile",
|
||||
"description": "My custom profile",
|
||||
"includes": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"profile_id": "uuid",
|
||||
"included_profile_id": "uuid",
|
||||
"included_profile_name": "base-profile",
|
||||
"order_index": 0,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"mounts": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"profile_id": "uuid",
|
||||
"target_path": "/etc/config",
|
||||
"mode": "rw",
|
||||
"files": {"test.txt": "hello"},
|
||||
"order_index": 0,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PUT /config-profiles/{profile_id}
|
||||
|
||||
**Description:** Update a config profile.
|
||||
|
||||
### Request
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "updated-name",
|
||||
"description": "Updated description"
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
Returns updated profile.
|
||||
|
||||
---
|
||||
|
||||
## DELETE /config-profiles/{profile_id}
|
||||
|
||||
**Description:** Delete a config profile and all its includes and mounts.
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (204 No Content)
|
||||
|
||||
---
|
||||
|
||||
## GET /config-profiles/defaults
|
||||
|
||||
**Description:** Get the current user's default profile assignments per tool type.
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"default_profiles": {
|
||||
"code-server": "profile-uuid-1",
|
||||
"jupyter-notebook": "profile-uuid-2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PUT /config-profiles/defaults
|
||||
|
||||
**Description:** Set the current user's default profile assignments per tool type.
|
||||
|
||||
### Request
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"default_profiles": {
|
||||
"code-server": "profile-uuid-1",
|
||||
"jupyter-notebook": "profile-uuid-2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `default_profiles` | `object` | Yes | Mapping of tool_type_id to profile_id |
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
Returns updated default profiles.
|
||||
|
||||
#### Error (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "profile {profile_id} not found"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET /config-profiles/defaults/{tool_type_id}
|
||||
|
||||
**Description:** Get the default profile ID for a specific tool type.
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"tool_type_id": "code-server",
|
||||
"profile_id": "profile-uuid-1"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET /config-profiles/{profile_id}/includes
|
||||
|
||||
**Description:** List all includes for a config profile.
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"includes": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"profile_id": "uuid",
|
||||
"included_profile_id": "uuid",
|
||||
"included_profile_name": "base-profile",
|
||||
"order_index": 0,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /config-profiles/{profile_id}/includes
|
||||
|
||||
**Description:** Add an include to a config profile.
|
||||
|
||||
### Request
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"included_profile_id": "uuid",
|
||||
"order_index": 0
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `included_profile_id` | `string` | Yes | UUID of the profile to include |
|
||||
| `order_index` | `integer` | No | Order for include resolution (default: 0) |
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (201 Created)
|
||||
|
||||
Returns created include.
|
||||
|
||||
#### Error (400 Bad Request)
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "a profile cannot include itself"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "adding this include would create a circular reference"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PUT /config-profiles/{profile_id}/includes/{include_id}
|
||||
|
||||
**Description:** Update the order index of a profile include.
|
||||
|
||||
### Request
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"order_index": 5
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
Returns updated include.
|
||||
|
||||
---
|
||||
|
||||
## DELETE /config-profiles/{profile_id}/includes/{include_id}
|
||||
|
||||
**Description:** Remove an include from a config profile.
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (204 No Content)
|
||||
|
||||
---
|
||||
|
||||
## GET /config-profiles/{profile_id}/mounts
|
||||
|
||||
**Description:** List all mounts for a config profile.
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"mounts": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"profile_id": "uuid",
|
||||
"target_path": "/etc/config",
|
||||
"mode": "rw",
|
||||
"files": {"test.txt": "hello"},
|
||||
"order_index": 0,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /config-profiles/{profile_id}/mounts
|
||||
|
||||
**Description:** Add a mount to a config profile.
|
||||
|
||||
### Request
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"target_path": "/etc/config",
|
||||
"mode": "rw",
|
||||
"files": {"test.txt": "hello"},
|
||||
"order_index": 0
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `target_path` | `string` | Yes | Absolute target path (must start with /) |
|
||||
| `mode` | `string` | No | Mount mode: "rw" or "ro" (default: "rw") |
|
||||
| `files` | `object` | No | Files as {path: content} |
|
||||
| `order_index` | `integer` | No | Order for mount resolution (default: 0) |
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (201 Created)
|
||||
|
||||
Returns created mount.
|
||||
|
||||
#### Error (422 Unprocessable Entity)
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Target path must be absolute (start with /)"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PUT /config-profiles/{profile_id}/mounts/{mount_id}
|
||||
|
||||
**Description:** Update a mount in a config profile.
|
||||
|
||||
### Request
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"target_path": "/new/path",
|
||||
"files": {"test.txt": "updated"},
|
||||
"order_index": 2
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (200 OK)
|
||||
|
||||
Returns updated mount.
|
||||
|
||||
---
|
||||
|
||||
## DELETE /config-profiles/{profile_id}/mounts/{mount_id}
|
||||
|
||||
**Description:** Remove a mount from a config profile.
|
||||
|
||||
### Response
|
||||
|
||||
#### Success (204 No Content)
|
||||
|
||||
@@ -13,20 +13,16 @@ The Headquarter backend is built with **FastAPI** and follows a layered architec
|
||||
│ Middleware: CORS → Request Logging → Exception Logging │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ API Layer (src/api/) │
|
||||
│ ┌─────────┐ ┌──────────┐ ┌────────┐ ┌──────────┐ │
|
||||
│ │ Auth │ │ Projects │ │ Users │ │ Git │ │
|
||||
│ │ Routes │ │ Routes │ │ Routes │ │ Repos │ │
|
||||
│ └────┬────┘ └────┬─────┘ └───┬────┘ └────┬─────┘ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ ToolInst │ │ Events │ │
|
||||
│ │ Routes │ │ Routes │ │
|
||||
│ └────┬─────┘ └────┬─────┘ │
|
||||
├───────┼───────────┼───────────┼───────────┼─────────────────┤
|
||||
│ │ │ │ │ │
|
||||
│ Auth │ Project │ User │ Git │ │
|
||||
│ Layer │ Service │ Service │ Service │ │
|
||||
│ │ │ │ │ │
|
||||
├───────┴───────────┴───────────┴───────────┴─────────────────┤
|
||||
│ ┌─────────┐ ┌─────────┐ ┌────────┐ ┌──────────┐ │
|
||||
│ │ Auth │ │Terminal │ │Projects│ │ Git │ │
|
||||
│ │ Routes │ │ WS │ │ Routes │ │ Repos │ │
|
||||
│ └────┬────┘ └────┬────┘ └───┬────┘ └────┬─────┘ │
|
||||
├───────┼───────────┼──────────┼───────────┼──────────────────┤
|
||||
│ │ │ │ │ │
|
||||
│ Auth │ Terminal │ Project │ Git │ │
|
||||
│ Layer │ Manager │ Service │ Service │ │
|
||||
│ │ + Session│ │ │ │
|
||||
├───────┴───────────┴──────────┴───────────┴──────────────────┤
|
||||
│ Data Layer │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Models │ │ Database │ │ Config │ │
|
||||
@@ -41,13 +37,12 @@ The Headquarter backend is built with **FastAPI** and follows a layered architec
|
||||
src/
|
||||
├── api/ # API Routes
|
||||
│ ├── auth.py # Authentication endpoints
|
||||
│ ├── terminal.py # WebSocket terminal endpoint
|
||||
│ ├── projects.py # Project endpoints
|
||||
│ ├── git_repositories.py # Repository endpoints
|
||||
│ ├── users.py # User endpoints
|
||||
│ ├── tool_types.py # Tool type endpoints
|
||||
│ ├── tool_instances.py # Tool instance endpoints
|
||||
│ ├── ssh_keys.py # SSH key endpoints
|
||||
│ ├── events.py # SSE streaming endpoint
|
||||
│ └── dashboard.py # Dashboard endpoints
|
||||
├── auth/ # Authentication
|
||||
│ ├── session.py # Session management
|
||||
@@ -60,15 +55,12 @@ src/
|
||||
│ ├── git_repository.py # Repository model
|
||||
│ ├── tool_type.py # Tool type model
|
||||
│ ├── ssh_key.py # SSH key model
|
||||
│ ├── instance_event.py # Instance event audit model
|
||||
│ ├── health_check.py # Health check snapshot model
|
||||
│ └── user_config.py # User config model
|
||||
├── services/ # Services
|
||||
│ ├── docker.py # Docker operations
|
||||
├── services/ # Business Logic
|
||||
│ ├── terminal_manager.py # Terminal session manager
|
||||
│ ├── event_bus.py # Instance event bus (pub/sub)
|
||||
│ ├── health_monitor.py # Background health monitoring
|
||||
│ └── lifecycle_hooks.py # Instance lifecycle events
|
||||
│ ├── terminal_session.py # PTY + docker exec session
|
||||
│ ├── docker.py # Docker operations
|
||||
│ └── profile_resolver.py # Profile resolution
|
||||
├── utils/ # Utilities
|
||||
│ ├── git_url_parser.py # URL parsing
|
||||
│ ├── git_files.py # Git file operations
|
||||
@@ -78,6 +70,65 @@ src/
|
||||
└── main.py # Application entry point
|
||||
```
|
||||
|
||||
## Terminal System
|
||||
|
||||
The terminal system provides interactive shell access to running tool instances via WebSocket.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Client (WebSocket)
|
||||
↕
|
||||
terminal.py (FastAPI WS endpoint)
|
||||
├─ Auth validation (session cookie)
|
||||
├─ Instance ownership check
|
||||
├─ Session lifecycle (create / monitor / cleanup)
|
||||
└─ Echo state detection (termios)
|
||||
↕
|
||||
TerminalManager
|
||||
├─ create_session() → spawns TerminalSession
|
||||
├─ _read_loop() → batches PTY output → WebSocket
|
||||
├─ _write_loop() → WebSocket input → PTY
|
||||
└─ _heartbeat_loop() → closes idle connections (60s)
|
||||
↕
|
||||
TerminalSession
|
||||
├─ start() → pty.openpty() + docker exec
|
||||
├─ read_output() → select.select() + os.read()
|
||||
├─ write_input() → os.write() to PTY master
|
||||
├─ resize() → TIOCSWINSZ ioctl
|
||||
└─ check_echo_state() → termios.ECHO flag
|
||||
```
|
||||
|
||||
### Protocol
|
||||
|
||||
**Binary frames**: Raw terminal I/O (hot path)
|
||||
**Text (JSON) frames**: Control messages
|
||||
|
||||
**Control messages:**
|
||||
|
||||
| Direction | Type | Purpose |
|
||||
|-----------|------|---------|
|
||||
| Client → Server | `ping` | Heartbeat (every 15s idle) |
|
||||
| Server → Client | `pong` | Heartbeat response |
|
||||
| Client → Server | `resize` | Terminal dimensions changed |
|
||||
| Server → Client | `set_echo_state` | Enable/disable local echo |
|
||||
| Server → Client | `session_ended` | Container process exited |
|
||||
|
||||
### Message Batching
|
||||
|
||||
The read loop batches small PTY reads into single WebSocket frames:
|
||||
- Buffer accumulates data for up to 16ms
|
||||
- Flushed immediately when no new data is available
|
||||
- Reduces WebSocket frame overhead for rapid output
|
||||
|
||||
### Reconnect Behavior
|
||||
|
||||
The server cannot resume a `docker exec` PTY across connections. On reconnect:
|
||||
1. Old session is terminated
|
||||
2. New `docker exec` is spawned
|
||||
3. Client restores scrollback from `sessionStorage`
|
||||
4. New shell appears seamlessly to the user
|
||||
|
||||
## Layers
|
||||
|
||||
### 1. API Layer (`src/api/`)
|
||||
@@ -207,33 +258,6 @@ Errors are handled at multiple levels:
|
||||
- **Integration tests**: PostgreSQL with transaction rollback
|
||||
- **Fixtures**: Shared in `conftest.py`
|
||||
|
||||
## Monitoring & Notifications
|
||||
|
||||
The backend includes a real-time monitoring system:
|
||||
|
||||
### Components
|
||||
|
||||
- **InstanceEventBus** (`services/event_bus.py`): Typed pub/sub singleton for instance lifecycle events
|
||||
- **HealthMonitor** (`services/health_monitor.py`): Asyncio background task polling container health every 15s
|
||||
- **SSE Endpoint** (`api/events.py`): Server-Sent Events streaming for real-time frontend updates
|
||||
- **Lifecycle Hooks** (`services/lifecycle_hooks.py`): Publishes events on create/start/stop/restart/delete
|
||||
|
||||
### Event Flow
|
||||
|
||||
```
|
||||
Container Action → Lifecycle Hook → EventBus → SSE Stream → Frontend Toast
|
||||
```
|
||||
|
||||
### Event Types
|
||||
|
||||
| Event | When Fired |
|
||||
|-------|-----------|
|
||||
| `instance.created` | After DB insert |
|
||||
| `instance.starting` | Before docker compose up |
|
||||
| `instance.running` | After readiness probe succeeds |
|
||||
| `instance.error` | Build fail, crash, or probe fail |
|
||||
| `instance.stopped` | After docker compose stop |
|
||||
|
||||
## Technology Stack
|
||||
|
||||
| Component | Technology | Version |
|
||||
|
||||
@@ -26,27 +26,26 @@ apps/web/src/
|
||||
│ ├── ssh_keys.ts # SSH key API
|
||||
│ ├── tool_types.ts # Tool type API
|
||||
│ ├── users.ts # User API
|
||||
│ ├── events.ts # SSE events API
|
||||
│ ├── sessions.ts # Tool instance sessions API
|
||||
│ └── settings.ts # Settings API
|
||||
├── components/ # Reusable components
|
||||
│ ├── app-shell.tsx # Main app layout
|
||||
│ ├── terminal.tsx # xterm.js terminal component
|
||||
│ ├── protected-route.tsx # Auth guard
|
||||
│ ├── event-toast-bridge.tsx # Events → toasts
|
||||
│ └── [more...]
|
||||
├── state/ # Global state
|
||||
│ ├── auth.tsx # Auth state management
|
||||
│ ├── events.tsx # Event provider (SSE)
|
||||
│ └── toast.tsx # Toast notifications
|
||||
├── context/ # React contexts
|
||||
│ └── auth.tsx # Auth state management
|
||||
├── hooks/ # Custom hooks
|
||||
│ ├── use-auth.ts # Auth hook
|
||||
│ ├── use-theme.ts # Theme hook
|
||||
│ └── use-events.ts # SSE events hook
|
||||
│ └── use-terminal-connection.ts # Terminal WebSocket lifecycle
|
||||
├── pages/ # Page components (routes)
|
||||
│ ├── dashboard.tsx # Dashboard
|
||||
│ ├── projects.tsx # Project list
|
||||
│ ├── repo-workspace.tsx # Repository workspace
|
||||
│ ├── git-history.tsx # Git history
|
||||
│ ├── git-repositories.tsx # Repository management
|
||||
│ ├── terminal.tsx # Web terminal
|
||||
│ ├── profile.tsx # User profile
|
||||
│ ├── settings.tsx # User settings
|
||||
│ ├── tool-types.tsx # Tool types
|
||||
@@ -160,28 +159,6 @@ interface AuthState {
|
||||
}
|
||||
```
|
||||
|
||||
### Real-Time Events (SSE)
|
||||
|
||||
The frontend receives real-time instance events via Server-Sent Events:
|
||||
|
||||
```
|
||||
EventSource → useEvents() hook → EventProvider → EventToastBridge → ToastContainer
|
||||
```
|
||||
|
||||
**Components:**
|
||||
- `useEvents()`: Manages SSE connection with auto-reconnect
|
||||
- `EventProvider`: Shares event stream across components
|
||||
- `EventToastBridge`: Maps events to toast notifications
|
||||
- `ToastContainer`: Displays and manages toast stack
|
||||
|
||||
**Event-to-Toast Mapping:**
|
||||
| Event | Toast Severity | Auto-dismiss |
|
||||
|-------|---------------|--------------|
|
||||
| `instance.starting` | Info | 3s |
|
||||
| `instance.running` | Success | 3s |
|
||||
| `instance.error` | Error | Persistent |
|
||||
| `instance.stopped` | Info | 3s |
|
||||
|
||||
### 5. Routing Structure
|
||||
|
||||
```typescript
|
||||
@@ -191,6 +168,7 @@ EventSource → useEvents() hook → EventProvider → EventToastBridge → Toas
|
||||
<Route path="/projects/:projectId" element={<RepoWorkspace />} />
|
||||
<Route path="/projects/:projectId/repositories" element={<GitRepositories />} />
|
||||
<Route path="/projects/:projectId/repositories/:repoId/history" element={<GitHistory />} />
|
||||
<Route path="/terminal/:instanceId" element={<TerminalPage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/ssh-keys" element={<SSHKeysPage />} />
|
||||
@@ -296,12 +274,64 @@ test('renders file list', () => {
|
||||
4. **Caching**: Browser caches API responses (ETags)
|
||||
5. **Optimistic UI**: Immediate feedback before API response
|
||||
|
||||
## Terminal Architecture
|
||||
|
||||
The web terminal is the most complex component in the frontend. It bridges a browser-based terminal emulator with a server-side PTY session.
|
||||
|
||||
### Component Stack
|
||||
|
||||
```
|
||||
TerminalPage (route)
|
||||
└── TerminalComponent
|
||||
├── Status bar (connection state, latency, actions)
|
||||
├── Session-ended overlay (reconnect / go back)
|
||||
├── Reconnect banner (spinner + countdown)
|
||||
└── xterm.js (terminal emulator)
|
||||
├── FitAddon (auto-resize to container)
|
||||
├── SerializeAddon (scrollback serialization)
|
||||
└── WebLinksAddon (clickable URLs)
|
||||
```
|
||||
|
||||
### Connection Hook
|
||||
|
||||
`useTerminalConnection` manages the full WebSocket lifecycle:
|
||||
|
||||
```
|
||||
CONNECTING
|
||||
→ onopen → CONNECTED → heartbeat every 15s
|
||||
→ onclose (unexpected) → RECONNECTING
|
||||
→ backoff: 1s → 2s → 4s → 8s → 16s → 30s max
|
||||
→ up to 10 attempts
|
||||
→ onopen → restore scrollback → CONNECTED
|
||||
→ onclose (expected) → DISCONNECTED
|
||||
```
|
||||
|
||||
**Key behaviors:**
|
||||
- **Local echo**: Printable ASCII chars appear instantly; server echo is deduplicated
|
||||
- **Resize**: Debounced 200ms, throttled to 1 message per 500ms
|
||||
- **Scrollback**: Serialized to `sessionStorage` on disconnect, restored on reconnect
|
||||
- **Keyboard**: `Ctrl+Shift+R` triggers manual reconnect
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User types 'a'
|
||||
→ xterm onData event
|
||||
→ useTerminalConnection.sendInput('a')
|
||||
→ local echo writes 'a' to xterm immediately
|
||||
→ WebSocket sends 'a' to server
|
||||
→ server PTY echoes 'a' back
|
||||
→ client receives 'a' via binary frame
|
||||
→ deduplicates against pending echo buffer
|
||||
→ (no-op if matched, or writes remaining chars)
|
||||
```
|
||||
|
||||
## Future Improvements
|
||||
|
||||
- [x] Implement real-time updates (SSE)
|
||||
- [ ] Add React Query for server state management
|
||||
- [ ] Implement virtual scrolling for large file trees
|
||||
- [ ] Add service worker for offline support
|
||||
- [x] Implement real-time updates (WebSocket) — Terminal done
|
||||
- [ ] Add error boundary components
|
||||
|
||||
## Development Workflow
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# Naming Conventions
|
||||
|
||||
This document defines the file and identifier naming conventions for the Headquarter codebase.
|
||||
|
||||
## Frontend (`apps/web/src/`)
|
||||
|
||||
### React Components
|
||||
|
||||
**File naming:** PascalCase, matching the exported component name exactly.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
components/features/git/FileBrowser.tsx
|
||||
components/features/dashboard/DashboardSummary.tsx
|
||||
pages/DashboardPage.tsx
|
||||
|
||||
❌ Bad:
|
||||
components/file-browser.tsx
|
||||
pages/dashboard.tsx
|
||||
```
|
||||
|
||||
**Component naming:** PascalCase. Page components end with `Page`.
|
||||
|
||||
```typescript
|
||||
// Component
|
||||
export const FileBrowser = () => { ... }
|
||||
|
||||
// Page
|
||||
export const DashboardPage = () => { ... }
|
||||
```
|
||||
|
||||
### Hooks
|
||||
|
||||
**File naming:** camelCase with `use` prefix.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
hooks/use-theme.ts
|
||||
hooks/use-dashboard-actions.ts
|
||||
```
|
||||
|
||||
### API Modules
|
||||
|
||||
**File naming:** kebab-case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
api/tool-types.ts
|
||||
api/git-repositories.ts
|
||||
api/config-folders.ts
|
||||
```
|
||||
|
||||
### Type Modules
|
||||
|
||||
**File naming:** kebab-case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
types/tool-type.ts
|
||||
types/git-repository.ts
|
||||
```
|
||||
|
||||
### Utilities
|
||||
|
||||
**File naming:** kebab-case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
utils/terminal-protocol.ts
|
||||
utils/language.ts
|
||||
```
|
||||
|
||||
### CSS Modules
|
||||
|
||||
**File naming:** kebab-case, matching the component file name with `.module.css` suffix.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
FileBrowser.tsx + FileBrowser.module.css
|
||||
```
|
||||
|
||||
## Backend (`apps/api/src/`)
|
||||
|
||||
### Routers
|
||||
|
||||
**File naming:** snake_case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
api/tool_instances.py
|
||||
api/git_repositories.py
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
**File naming:** snake_case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
services/docker/compose.py
|
||||
services/profile_resolver.py
|
||||
```
|
||||
|
||||
### Models
|
||||
|
||||
**File naming:** snake_case. Class names use PascalCase.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
models/tool_instance.py
|
||||
class ToolInstance(Base):
|
||||
```
|
||||
|
||||
### Schemas
|
||||
|
||||
**File naming:** snake_case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
schemas/tool_instance.py
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
**File naming:** Same as source file with `.test.tsx` suffix.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
FileBrowser.tsx + FileBrowser.test.tsx
|
||||
```
|
||||
|
||||
### Backend Tests
|
||||
|
||||
**File naming:** `test_` prefix + snake_case.
|
||||
|
||||
```
|
||||
✅ Good:
|
||||
test_tool_instances.py
|
||||
```
|
||||
|
||||
## Directory Structure Summary
|
||||
|
||||
```
|
||||
apps/web/src/
|
||||
├── api/ # kebab-case files
|
||||
├── components/
|
||||
│ ├── ui/ # PascalCase files
|
||||
│ ├── layout/ # PascalCase files
|
||||
│ └── features/ # PascalCase files, grouped by domain
|
||||
│ ├── git/
|
||||
│ ├── project/
|
||||
│ ├── session/
|
||||
│ └── ...
|
||||
├── hooks/ # camelCase files
|
||||
├── pages/ # PascalCase files ending with Page
|
||||
├── styles/ # kebab-case CSS files
|
||||
├── types/ # kebab-case files
|
||||
└── utils/ # kebab-case files
|
||||
|
||||
apps/api/src/
|
||||
├── api/ # snake_case files
|
||||
├── models/ # snake_case files
|
||||
├── schemas/ # snake_case files
|
||||
├── services/ # snake_case files
|
||||
└── auth/ # snake_case files
|
||||
```
|
||||
|
||||
## Migration Notes
|
||||
|
||||
Some legacy files may not yet follow these conventions. When touching a file for other work, rename it to match the convention in the same PR.
|
||||
@@ -22,25 +22,30 @@ The Projects page displays all your projects in a card layout showing:
|
||||
- Creation date
|
||||
- Associated repositories count
|
||||
|
||||
Each project card provides quick actions:
|
||||
- **Settings** — Navigate to the project settings page
|
||||
- **Delete** — Delete the project with confirmation
|
||||
- **Open Workspace** — Open the project's workspace (rightmost action)
|
||||
|
||||
### Opening a Project Workspace
|
||||
|
||||
Click on any project card to open its **workspace**. The workspace is the default view for a project and shows:
|
||||
Click the **"Open Workspace"** button on any project card to open its **workspace**. The workspace is the default view for a project and shows:
|
||||
- Repository file browser
|
||||
- Branch selector
|
||||
- File viewer
|
||||
|
||||
### Editing a Project
|
||||
|
||||
1. From the Projects page, click the **menu icon** (⋮) on a project card
|
||||
2. Select **"Edit"**
|
||||
3. Update the name or description
|
||||
4. Click **"Save"**
|
||||
1. From the Projects page, click the **"Settings"** link on a project card
|
||||
2. On the project settings page, update the **name** or **description**
|
||||
3. Click **"Save Changes"**
|
||||
|
||||
The settings page also provides access to repository management and member settings.
|
||||
|
||||
### Deleting a Project
|
||||
|
||||
1. From the Projects page, click the **menu icon** (⋮) on a project card
|
||||
2. Select **"Delete"**
|
||||
3. Confirm the deletion
|
||||
1. From the Projects page, click the **"Delete"** button on a project card
|
||||
2. Confirm the deletion
|
||||
|
||||
**Note:** Deleting a project also deletes all associated repositories and their data. This action cannot be undone.
|
||||
|
||||
|
||||
+180
-68
@@ -1,104 +1,216 @@
|
||||
# Terminal Sessions
|
||||
# Web Terminal
|
||||
|
||||
Terminal sessions provide interactive shell access to your running tool instances directly in the browser.
|
||||
## Overview
|
||||
|
||||
## Persistent Sessions
|
||||
The web terminal provides an interactive shell session inside running tool instances directly from your browser. It uses xterm.js to render a full terminal emulator connected via WebSocket to a PTY-backed docker exec session.
|
||||
|
||||
Terminal sessions are **persistent** - they survive browser refreshes, network interruptions, and tab switches.
|
||||
The terminal is designed to feel as close to a local terminal as possible, with features for network resilience, low-latency typing, and session continuity.
|
||||
|
||||
### How It Works
|
||||
## How to Use
|
||||
|
||||
- When you open a terminal, a shell session starts inside the tool instance container
|
||||
- If you close the browser or lose connection, the session keeps running
|
||||
- When you reconnect, you reattach to the same session with all previous output preserved
|
||||
- Sessions automatically clean up after 30 minutes of inactivity
|
||||
### Opening a Terminal
|
||||
|
||||
1. Navigate to a **project** and select a **repository**
|
||||
2. Go to the repository **workspace**
|
||||
3. Start or select a **tool instance** that supports the terminal interface
|
||||
4. Click the **"Open Terminal"** button
|
||||
|
||||
The terminal opens in full-page mode with a status bar at the top.
|
||||
|
||||
### Terminal Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ ● Connected [Reconnect] [×] │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ user@container:~$ ls -la │
|
||||
│ total 128 │
|
||||
│ drwxr-xr-x 5 user user 4096 May 27 10:00 │
|
||||
│ ... │
|
||||
│ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Status bar (top):**
|
||||
- **Connection dot** — color indicates connection health
|
||||
- **Status text** — shows current state and latency
|
||||
- **Reconnect button** — appears when disconnected
|
||||
- **Close button** — returns to the previous page
|
||||
|
||||
### Connection States
|
||||
|
||||
| Indicator | Meaning | Action |
|
||||
|-----------|---------|--------|
|
||||
| 🟡 **Yellow dot** + "Connecting..." | Opening WebSocket | Wait or check network |
|
||||
| 🟢 **Green dot** + "Connected" | Healthy connection (<100ms) | Ready to use |
|
||||
| 🟡 **Yellow dot** + "Slow (150ms)" | Elevated latency | Connection usable but laggy |
|
||||
| 🟡 **Yellow dot** + "Reconnecting (2)" | Connection lost, retrying | Wait for auto-reconnect |
|
||||
| ⚪ **Gray dot** + "Disconnected" | Max retries exceeded | Click Reconnect or refresh |
|
||||
|
||||
**Hover the status dot** to see the current round-trip latency in milliseconds.
|
||||
|
||||
### Typing
|
||||
|
||||
Type normally as you would in a local terminal. The terminal supports:
|
||||
|
||||
- **Printable characters** appear instantly (local echo)
|
||||
- **Special keys** (Tab, Enter, Ctrl+C, arrow keys) are sent to the server
|
||||
- **Password prompts** automatically suppress local echo
|
||||
- **Unicode** input and output
|
||||
|
||||
### Reconnecting
|
||||
|
||||
If your connection drops:
|
||||
1. The terminal shows "Reconnecting..." status
|
||||
2. The client automatically attempts to reconnect with exponential backoff
|
||||
3. On successful reconnection, buffered output is replayed
|
||||
4. You can continue working where you left off
|
||||
The terminal **automatically reconnects** if the WebSocket drops:
|
||||
|
||||
## Resetting the Terminal
|
||||
- Brief disconnects (WiFi hiccups, proxy timeouts) are recovered within 1–5 seconds
|
||||
- Up to **10 reconnection attempts** with exponential backoff
|
||||
- **Scrollback is preserved** across reconnects
|
||||
- A visual divider (`--- Reconnected ---`) separates old and new output
|
||||
|
||||
If your terminal becomes unresponsive or you want a fresh start:
|
||||
**Manual reconnect:**
|
||||
- Click the **Reconnect** button in the status bar
|
||||
- Or press **Ctrl+Shift+R** anywhere in the terminal page
|
||||
|
||||
1. Click the **Reset** button in the terminal header
|
||||
2. Confirm the reset action
|
||||
3. The current shell is killed and a new one starts
|
||||
4. All terminal history is cleared
|
||||
### Session Ended
|
||||
|
||||
**Note:** Resetting only affects the terminal session, not the tool instance itself. Any files you've created remain intact.
|
||||
When the container process exits (e.g., you run `exit` or the container stops), the terminal shows an overlay:
|
||||
|
||||
## Mobile Terminal
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ Session Ended │
|
||||
│ The container process │
|
||||
│ has exited. │
|
||||
│ │
|
||||
│ [Reconnect] [Go Back] │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
On mobile devices, the terminal includes:
|
||||
- Special keys panel (Ctrl, Alt, Tab, arrows, etc.)
|
||||
- Font size controls
|
||||
- Auto-hiding header for maximum screen space
|
||||
- Touch-friendly interface
|
||||
- **Reconnect** — spawns a new shell session in the same container
|
||||
- **Go Back** — returns to the workspace page
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
Standard terminal shortcuts work as expected:
|
||||
- `Ctrl+C`: Send interrupt signal
|
||||
- `Ctrl+D`: Send EOF (close shell if empty)
|
||||
- `Ctrl+L`: Clear screen
|
||||
- `Ctrl+Z`: Suspend process
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Ctrl+Shift+R` | Force reconnect (bypasses backoff) |
|
||||
| Standard terminal shortcuts | `Ctrl+C`, `Ctrl+D`, `Ctrl+L`, Tab completion, etc. |
|
||||
|
||||
Special keys can be accessed via the special keys panel on mobile or by using modifier combinations.
|
||||
## Technical Details
|
||||
|
||||
## Container Monitoring & Notifications
|
||||
### WebSocket Protocol
|
||||
|
||||
The platform monitors your tool instances in real-time and notifies you of important events:
|
||||
The terminal communicates over a binary WebSocket with mixed JSON control messages.
|
||||
|
||||
### What You'll See
|
||||
**Connection:**
|
||||
```
|
||||
ws://api.example.com/ws/tool-instances/{instance_id}/terminal
|
||||
```
|
||||
|
||||
- **Starting:** When a container begins starting
|
||||
- **Running:** When a container is ready
|
||||
- **Error:** When a build fails, container crashes, or tunnel fails
|
||||
- **Stopped:** When a container stops
|
||||
**Binary frames** carry raw terminal I/O. **Text (JSON) frames** carry control messages:
|
||||
|
||||
Notifications appear as toast messages at the top of the screen. Errors persist until dismissed; other notifications auto-dismiss after a few seconds.
|
||||
**Client → Server:**
|
||||
- `{"type":"ping","id":n}` — heartbeat ping
|
||||
- `{"type":"resize","cols":120,"rows":40}` — terminal resize
|
||||
- Raw bytes — keystroke input
|
||||
|
||||
### Real-Time Status
|
||||
**Server → Client:**
|
||||
- `{"type":"pong","id":n}` — heartbeat response
|
||||
- `{"type":"status","status":"connected"}` — session ready
|
||||
- `{"type":"set_echo_state","enabled":false}` — disable local echo
|
||||
- `{"type":"session_ended","reason":"process_exit"}` — session ended
|
||||
- Raw bytes — terminal output
|
||||
|
||||
Instance status badges update in real-time via Server-Sent Events (SSE) — no page refresh needed.
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Browser Backend
|
||||
┌──────────────────────┐ ┌─────────────────────────────┐
|
||||
│ TerminalComponent │ │ terminal.py (WS endpoint) │
|
||||
│ ├─ xterm.js │◄───────►│ ├─ auth + session mgmt │
|
||||
│ ├─ FitAddon │ WS │ └─ echo state detection │
|
||||
│ ├─ SerializeAddon │ │ │
|
||||
│ └─ useTerminalConn. │ │ TerminalManager │
|
||||
│ ├─ heartbeat │ │ ├─ read_loop (batching) │
|
||||
│ ├─ reconnect │ │ ├─ write_loop │
|
||||
│ ├─ local echo │ │ └─ heartbeat_loop │
|
||||
│ └─ resize throttle│ │ │
|
||||
│ │ │ TerminalSession │
|
||||
│ sessionStorage │ │ ├─ PTY + docker exec │
|
||||
│ (scrollback backup) │ │ └─ termios echo detection │
|
||||
└──────────────────────┘ └─────────────────────────────┘
|
||||
```
|
||||
|
||||
### Reconnect Behavior
|
||||
|
||||
On disconnect:
|
||||
1. The client serializes terminal scrollback to `sessionStorage`
|
||||
2. Backoff timer starts (1s, 2s, 4s, 8s, 16s, then caps at 30s)
|
||||
3. On reconnect, scrollback is restored + divider line
|
||||
4. A new `docker exec` session is spawned transparently
|
||||
|
||||
**Note:** The underlying docker exec PTY is not resumable. Reconnect creates a new shell, but scrollback continuity makes this transparent.
|
||||
|
||||
### Performance
|
||||
|
||||
- **Local echo** makes printable characters appear in <1ms
|
||||
- **Message batching** on the backend reduces WebSocket frame overhead
|
||||
- **Resize debouncing** (200ms) + throttling (500ms) prevents server spam
|
||||
- **Heartbeat interval** is 15s to balance detection speed with server load
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
### "Connecting..." stays yellow
|
||||
|
||||
**"Connection closed" error:**
|
||||
- The tool instance may have stopped - check the instance status
|
||||
- Network issues - the client will auto-reconnect
|
||||
- Session timeout - sessions expire after 30 minutes of inactivity
|
||||
**Issue:** WebSocket cannot open
|
||||
**Check:**
|
||||
1. Is the API server running?
|
||||
2. Is the tool instance in "running" status?
|
||||
3. Check browser console for connection errors
|
||||
4. Verify the `VITE_API_BASE_URL` points to the correct API
|
||||
|
||||
**"Container not found" error (4004):**
|
||||
- The Docker container no longer exists (e.g., after host restart)
|
||||
- Restart the tool instance to recreate the container
|
||||
### "Reconnecting" loops forever
|
||||
|
||||
**Terminal not responding:**
|
||||
- Try resetting the terminal using the Reset button
|
||||
- Check if the tool instance is still running
|
||||
- Refresh the page to force reconnection
|
||||
**Issue:** Max reconnection attempts exceeded
|
||||
**Check:**
|
||||
1. Is the container still running? (`docker ps`)
|
||||
2. Did the container crash or get stopped?
|
||||
3. Check server logs for `Terminal session error`
|
||||
|
||||
### Display Issues
|
||||
### Typing feels slow
|
||||
|
||||
**Text not visible:**
|
||||
- Adjust font size using +/- buttons
|
||||
- Check if the terminal has focus (click inside it)
|
||||
- Try resizing the browser window
|
||||
**Issue:** High latency or no local echo
|
||||
**Check:**
|
||||
1. Hover the status dot — latency >100ms is shown as "Slow"
|
||||
2. Local echo only works for printable ASCII characters
|
||||
3. Password prompts intentionally disable echo
|
||||
4. Very high latency may indicate a congested network
|
||||
|
||||
**Characters not appearing:**
|
||||
- Ensure the terminal has focus
|
||||
- Check if a modifier key is stuck (Ctrl, Alt)
|
||||
- Reset the terminal if stuck
|
||||
### Terminal is blank after reconnect
|
||||
|
||||
## Session Limits
|
||||
**Issue:** Scrollback not restored
|
||||
**Check:**
|
||||
1. `sessionStorage` may have been cleared (new browser session)
|
||||
2. The scrollback cap is 10,000 lines — very long sessions may truncate
|
||||
3. Browser privacy settings may block `sessionStorage`
|
||||
|
||||
- **One connection per terminal:** Only one browser tab can connect to a terminal session at a time. Opening a new connection closes the old one.
|
||||
- **30-minute idle timeout:** Sessions without activity are automatically cleaned up
|
||||
- **Buffer size:** Up to 10KB of output is buffered for replay on reconnection
|
||||
### "Session Ended" immediately
|
||||
|
||||
**Issue:** Container process exits right away
|
||||
**Check:**
|
||||
1. The container's default command may have finished
|
||||
2. Check the tool type's Docker Compose template
|
||||
3. Some tools (like one-off scripts) are not meant for persistent terminal sessions
|
||||
|
||||
## Configuration
|
||||
|
||||
No additional configuration is required. The terminal adapts automatically to:
|
||||
- Browser window size (via ResizeObserver)
|
||||
- System light/dark theme preference
|
||||
- Network conditions (reconnect backoff)
|
||||
|
||||
## Related Features
|
||||
|
||||
- [Workspace](workspace.md) — Open the terminal from the repository workspace
|
||||
- [Tool Types](tool-types.md) — Configure which tools expose a terminal interface
|
||||
- [SSH Keys](ssh-keys.md) — Manage SSH keys for repository access from within the terminal
|
||||
|
||||
Reference in New Issue
Block a user