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 ✓