- get_container_id() and get_container_name() now lowercase the
instance name before passing to docker ps --filter, because
Docker container names are lowercase internally and the filter
is case-sensitive. This caused container_id to never be captured
when instance.name contained uppercase chars (e.g. 'Headquarter'),
breaking terminal WebSocket connections.
- Also guard proc.stdout being None in start_cloudflared_tunnel().
- Add unit tests for get_container_id and get_container_name.
Quality gates: pytest (14 passed), python clean
- Add stdin_open: true and tty: true to dockerfile-based compose generation.
Without these, bash (PID 1) exits immediately, causing a container restart
loop that makes the instance invisible to docker ps and triggers 4004.
- Treat WebSocket close codes 4001/4003/4004 as permanent errors in the
frontend. Stop retrying and show the server reason to the user.
- Prevent visibilitychange handler from resetting retry attempts after a
permanent error has occurred.
- Use docker ps -a in get_container_id/get_container_name to find
stopped/exited containers for diagnostics.
Quality gates: tsc --noEmit (pass), pytest (98 passed, 4 pre-existing failures)
- Change git mount schema from repo_id to remote_url
- Remove database lookups for git mount resolution
- Clone directly from URL at instance startup
- Simplify frontend UI to text input for Git URL
- Fix route ordering in git_repositories.py to prevent 422 errors
- Update all tests to use remote_url field
Breaking change: Git mounts now use remote_url instead of repo_id
- Add startup_command field to ToolType model and API
- Execute startup command before interactive shell in terminal sessions
- Add tmux and ranger to OpenCode container spec
- Update Tool Workshop UI with startup_command input for terminal types
- Add backend tests for startup_command CRUD operations
- Sync specs: tool-terminal, tool-types-definition, opencode-web-server
- New spec: tool-terminal-startup-command
Quality gates: Frontend typecheck/lint passed. Backend tests blocked by environment (Python/Docker not available).
OpenSpec: terminal-startup-and-container-tools
Instead of sending stty commands through the user's terminal session
(which causes 'inappropriate ioctl' errors), send SIGWINCH signal to
the docker exec process on the host. Docker exec should forward this
to the container process, causing the shell to re-read its terminal size.
This avoids:
- Visible stty commands in the terminal
- ioctl errors from stty
- Interference with user's shell session
Reverted docker exec back to -it (required for interactive bash).
Instead, sends stty command with \r to hide it from the terminal display:
- \r moves cursor to start of line (overwrites prompt)
- stty command executes silently (no output on success)
- \r moves cursor back to start, hiding echoed command
This sends stty on EVERY resize so the container shell always matches
frontend dimensions.
Removes -t flag from docker exec so it uses our PTY slave directly instead
of creating its own PTY inside the container. This allows TIOCSWINSZ on the
host PTY master to propagate naturally to the container shell via SIGWINCH.
Also removes all stty command injection logic since resize now works natively.
Backend:
- Remove _stty_sent guard to send stty on EVERY resize
- Use stty -echo to hide command, then delete the command line with ANSI escapes
- Change log level from info to debug
Frontend:
- Add window resize listener as fallback to ResizeObserver
- 250ms debounce to avoid excessive refits
- Proper cleanup on unmount
Reverted terminal.tsx, terminal_session.py, and terminal.py to clean state
from before the resize debugging saga. Removed:
- Debug console.log statements
- Explicit term.resize() calls that broke xterm.js
- position: relative CSS overrides on .xterm
- stty -echo wrapper and asyncio.sleep delay
- Extra requestAnimationFrame refresh calls
Kept:
- Mobile terminal features (special keys, modifiers, font size)
- ResizeObserver for container resize detection
- Basic fit() and WebSocket resize messaging
- Add ConfigProfile and ConfigProfileInclude data models with migrations
- Implement profile resolver service with ordered includes and merge rules
- Add profile CRUD API with validation, compatibility, and cycle detection
- Add instance API plumbing for profile selection on create/start/restart
- Add resolved profile preview and default resolution APIs
- Add frontend config profile API client and management UI
- Add launch/restart profile selection UI
- Add backend integration and unit tests (31 passing)
OpenSpec: add-config-profiles
Quality gates: ruff, TypeScript compile, 31 tests passing
The _stty_sent guard prevented the container shell from updating its
terminal size after the first resize. This caused visual mismatches
where xterm.js displayed at the new size but the shell still wrapped
output at the old size.
Remove the guard so stty is sent on every resize event.
- Frontend: Add ResizeObserver with dimension tracking for accurate resize detection
- Frontend: Fix cleanup function to properly disconnect ResizeObserver
- Frontend: Use CSS grid for terminal wrapper layout
- Backend: Add duplicate dimension check to avoid unnecessary resizes
- Backend: Ensure stty command is sent correctly to container shell
The ResizeObserver detected size changes caused by the stty command
output appearing in the terminal, creating an infinite resize loop:
1. Resize detected -> fit() -> send resize to backend
2. Backend sends stty command through PTY
3. stty text appears in terminal output
4. ResizeObserver detects content height change
5. fit() calculates new rows -> send resize
6. Loop continues forever
Reverted to:
- Window resize event instead of ResizeObserver
- stty command only sent once on first resize
This means the container shell stays at the initial size and won't
dynamically resize when the browser window changes, but prevents
the infinite loop.
Previously the stty command was only sent on the first resize. Now it
is sent every time the terminal dimensions change, so resizing the
browser window or rotating the device properly updates the container
shell size. Added a check to skip when dimensions haven't changed.
Docker exec doesn't forward PTY resize to the container process,
so the container bash stays at 80x24 regardless of frontend resize.
Work around this by sending a stty command through the terminal
on first resize to set the correct dimensions inside the container.
Docker exec doesn't forward PTY resize to the container process,
so the container bash stays at 80x24 regardless of frontend resize.
Work around this by sending a stty command through the terminal
on first resize to set the correct dimensions inside the container.
Sending SIGWINCH to the docker exec process was crashing/killing it,
which closed the PTY and caused WebSocket 1006 abnormal closure loops.
Reverting to the original TIOCSWINSZ-only approach.
When resizing the PTY, docker exec needs to be notified so it can
re-read the terminal size and propagate it to the container's PTY.
Without this, the container shell stays at 80x24 regardless of what
the frontend sends.
TerminalManager was trying to create an asyncio task at module import time,
but no event loop exists yet during import. This caused RuntimeError on startup.
Changes:
- _start_idle_check() now checks if event loop is running before creating task
- If no loop exists, silently skips (will be started lazily)
- Added lazy start call in get_or_create_session() when websocket connects
- Terminal sessions now persist across WebSocket disconnections
- Added circular output buffer (10KB) for replay on reconnect
- Added idle timeout cleanup (30 minutes)
- Added reset functionality via WebSocket message and HTTP endpoint
- Concurrent connections close old WebSocket when new one connects
- Frontend: Added reset button with confirmation dialog
- Frontend: Handle resetting status and reconnection
Refs: persistent-terminal-sessions
- Add clone_mode and branch fields to tool_instances
- Add ssh_key_id to git_repositories for per-repo SSH key assignment
- Implement host-side git cloning with branch selection (default: main)
- Mount SSH keys into containers for git operations in clone mode
- Add dirty state check on clone-mode instance deletion with confirmation
- Update SessionsPage with mount/clone selector, branch input, SSH key display
- Add SSH key selector to repository creation form
- Add dirty delete confirmation modal with changed files list
- Update API schemas and endpoints for new fields
- Sync delta specs to main specs (git-repo, tool-instances, repo-clone-mode)
- Archive completed OpenSpec change: repo-clone-mode-with-ssh
- Document git requirement for custom tool types
Quality gates: Frontend typecheck and build passed
OpenSpec: repo-clone-mode-with-ssh archived with all tasks complete
- Add new status badges: starting, probing, unhealthy
- Show tunnel error only when tunnel_status is unreachable
- Show app error badge with status code for error_response
- Add collapsible probe output section for diagnostics
- Update health polling to check all active instances
- Only show Recreate Tunnel button for unreachable tunnels
- Use Python pty module to create pseudo-terminal
- Pass slave fd to docker exec for real TTY allocation
- Fixes ioctl errors and job control warnings
- Supports terminal resizing via TIOCSWINSZ
Quality gates: local testing
- Change docker exec -i to -it for real TTY allocation
- Fixes ioctl errors and job control warnings in terminal
- Gives bash a proper terminal for interactive use
Quality gates: manually tested
- Add inline confirmation dialog before stopping instances
- Delete instances from state immediately without page reload
- Add health check polling every 30s for running instances
- Show tunnel error badge when tunnel is unreachable
- Add 'Fix Tunnel' button to recreate broken tunnels
- Update API client with health check and tunnel recreation endpoints
Replace persistent Cloudflare tunnels (API-based) with temporary tunnels using
'cloudflared tunnel --url'. This removes the need for Cloudflare API tokens,
DNS records, and persistent tunnel management.
Changes:
- Install cloudflared binary in API Dockerfile
- Add start_cloudflared_tunnel() and stop_cloudflared_tunnel() to docker.py
- Update instance start/stop/restart/delete to use temporary tunnels
- Store tunnel PID in tunnel_id field, temporary URL in url/public_url
- Remove Cloudflare API service (cloudflare_tunnel.py)
- Remove cloudflared container from docker-compose
- Remove Cloudflare env vars (CLOUDFLARE_API_TOKEN, ZONE_ID, etc.)
- Remove Cloudflare configuration from config.py
- Remove Cloudflare startup check from main.py
- Remove /health/cloudflare endpoint
- Add 'backend' external network to all compose templates so cloudflared can reach tool containers
- Add better error handling and logging to create_tunnel() with specific error messages for auth failures
- Add check_cloudflare_config() diagnostic function
- Add /health/cloudflare endpoint to verify Cloudflare configuration
- Import Any type for type hints
Add comprehensive logging to trace 500 error:
- Log each step of docker compose up (returncode, stdout, stderr)
- Log container ID and name after start
- Log tool type and port being used
- Log each step of Cloudflare tunnel creation with API responses
- Log cloudflared config updates
This will help identify exactly where the failure occurs.
Cloudflared was hardcoded to route to port 8080, but containers
listen on different ports (8443 for code-server, 8888 for jupyter).
- Add instance_port parameter to create_tunnel and update_cloudflared_config
- Fetch tool type default_port when creating tunnels
- Route to correct internal port instead of hardcoded 8080
Backend:
- Add cloudflare_tunnel.py service for creating/deleting tunnels via Cloudflare API
- Add public_url and tunnel_id fields to ToolInstance model
- Update start_instance to create Cloudflare tunnel after container starts
- Update stop_instance to delete tunnel before stopping container
- Update delete_instance to cleanup tunnel before deletion
- Update restart_instance to recreate tunnel on restart
- Create Alembic migration 0011 for tunnel fields
- Add Cloudflare config settings (API token, zone ID, account ID, base domain)
Infrastructure:
- Add cloudflared service to docker-compose.traefik.yml
- Mount shared cloudflared_config volume between API and cloudflared containers
- Add Cloudflare env vars to API service
Frontend:
- Update instance Open button to handle both full URLs and proxy paths
The instance URL is now set to the Cloudflare tunnel public URL when available,
falling back to the API proxy path if tunnel creation fails.
- Create ToolConfigsPage with tool type selector, config list, and add/edit form
- Support both env and file config types
- Add route /tool-configs and navigation item
- Update API client with tool config endpoints
- Build passes successfully
Add API proxy endpoint so users can access running tool instances
through the backend API instead of internal Docker network.
Backend:
- Add container_name field to ToolInstance model
- Create /instances/{id}/proxy/{path:path} endpoint with ownership checks
- Proxy HTTP requests to containers via docker network using container names
- Support all HTTP methods (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
- Store proxy URL in instance.url instead of localhost
- Add Alembic migration 0007 for container_name column
- Add get_container_name() utility to docker.py
Frontend:
- Update Open button to use full proxy URL (API_BASE_URL + instance.url)
Closes instance-proxy OpenSpec change.
- Add INSTANCE_BASE_PATH config option (defaults to /data/instances)
- Update docker.py to use configured path instead of hardcoded 'data/instances'
- Update Dockerfile to create /data/instances and chown to appuser
- Add instance_data volume to docker-compose.traefik.yml and docker-compose.yml
- Set INSTANCE_BASE_PATH env var in both compose files
This fixes the PermissionError when creating tool instances because
appuser can now write to /data/instances.
Backend:
- Create ToolInstance model with status tracking
- Add Alembic migration for tool_instances table
- Create Docker service for compose template rendering and container execution
- Add CRUD API endpoints for tool instances
- Add lifecycle endpoints (start/stop/restart)
- Add user sessions endpoint for navigation
- Register routers in main.py
Frontend:
- Create SessionsProvider with React context
- Create sessions API client
- Update AppShell with sessions section in navigation
- Add session status indicators and polling
- Add CSS for session navigation
Quality gates: typecheck ✓, lint ✓, build ✓