The Inference Gateway CLI includes a browser-based terminal interface that provides remote access to the chat interface through any web browser. This enables multiple parallel sessions, remote access, and team collaboration workflows.
- Overview
- Quick Start
- Architecture
- Configuration
- Session Management
- Use Cases
- Security Considerations
- Troubleshooting
The web terminal feature wraps the standard infer chat TUI in a PTY (pseudo-terminal) and exposes it through a
WebSocket-based web interface using xterm.js. Each browser tab creates an independent terminal session with its own
infer chat process, containers, and resource isolation.
Key Features:
- Browser-based access: Access the CLI from any device with a web browser
- Multiple sessions: Create unlimited independent chat sessions in separate browser tabs
- Automatic cleanup: Sessions automatically terminate after configurable inactivity period
- Container isolation: Each tab manages its own Docker containers (agents, MCP servers)
- Graceful shutdown: Proper cleanup of all sessions and containers on server shutdown
- Responsive UI: Terminal automatically resizes to fit browser window
# Start with default settings (localhost:3000)
infer chat --web
# Custom port
infer chat --web --port 8080
# Bind to all interfaces for remote access (SECURITY WARNING: see below)
infer chat --web --host 0.0.0.0 --port 8080The server will output:
🌐 Web terminal available at: http://localhost:3000
Open this URL in your browser to access the terminal.
- Open the URL in your browser (e.g., http://localhost:3000)
- Create sessions by clicking the "+" button in the tab bar
- Switch sessions by clicking on tab titles
- Close sessions by clicking the "×" button on tabs
- Shutdown server with Ctrl+C in the terminal
Each tab provides a full infer chat session with:
- Model selection
- Real-time streaming responses
- Tool execution with approval modals
- Agent mode switching (Standard/Plan/Auto-Accept)
- All shortcuts and commands available
┌──────────────────┐ WebSocket ┌──────────────────┐ PTY I/O ┌─────────────────┐
│ Browser │ ◄───────────────────► │ HTTP Server │ ◄──────────────────► │ infer chat │
│ (xterm.js) │ Bidirectional │ + Session Mgr │ stdin/stdout/resize │ (BubbleTea TUI) │
└──────────────────┘ └──────────────────┘ └─────────────────┘
HTTP Server (internal/web/server.go):
- Serves HTML template and static assets (xterm.js, CSS, JavaScript)
- Handles WebSocket upgrade requests
- Manages graceful shutdown with signal handling
Session Manager (internal/web/session_manager.go):
- Tracks all active sessions with last activity timestamps
- Spawns cleanup goroutine for inactive session removal
- Coordinates shutdown of all sessions
- Thread-safe with mutex-protected session map
PTY Manager (internal/web/pty_manager.go):
- Spawns
infer chatsubprocess in PTY - Bridges WebSocket ↔ PTY bidirectional I/O
- Handles terminal resize events
- Graceful process shutdown (PTY close → SIGTERM → SIGKILL)
Frontend (internal/web/static/app.js, internal/web/templates/index.html):
- Tab management for multiple sessions
- xterm.js terminal emulator with Tokyo Night theme
- Automatic terminal resizing with FitAddon
- WebSocket connection management
- Connection: Browser opens WebSocket to
/ws - Session Creation: Server creates new session ID and spawns PTY with
infer chat - Activity Tracking: SessionHandler updates last activity timestamp every 10 seconds
- I/O Bridging: Bidirectional data flow between WebSocket and PTY
- Cleanup: On connection close or inactivity timeout, PTY process is shut down gracefully
Each session spawns its own infer chat process, which may create Docker containers for:
- A2A agents (e.g., browser-agent, code-reviewer)
- MCP servers with
run: trueconfiguration
When a session closes:
- ServiceContainer.Shutdown() is called via signal handling
- All containers are stopped with 20-second timeout
- Ports are released from global port registry
- Resources are freed
infer chat --web [--port PORT] [--host HOST]Flags:
--web: Enable web terminal mode--port: Port number (default: 3000)--host: Host to bind to (default: localhost)
Add to .infer/config.yaml:
web:
enabled: true # Enable web terminal
port: 3000 # Server port
host: "localhost" # Host binding
session_inactivity_mins: 5 # Session timeout in minutesexport INFER_WEB_ENABLED=true
export INFER_WEB_PORT=3000
export INFER_WEB_HOST="localhost"
export INFER_WEB_SESSION_INACTIVITY_MINS=10- Command-line flags (highest priority)
- Environment variables
- Config file
- Built-in defaults
Example:
# Config file sets port to 3000
# Command-line overrides to 8080
infer chat --web --port 8080 # Uses 8080Sessions are automatically cleaned up after the configured inactivity period (default: 5 minutes).
Activity tracking:
- SessionHandler updates last activity every 10 seconds while connection is open
- Cleanup goroutine runs every 1 minute
- Sessions inactive beyond threshold are stopped and removed
Logs:
INFO Cleaning up inactive session id=abc-123 inactive_duration=5m2s threshold=5m0s
INFO Inactive sessions cleaned up count=2 remaining=3 threshold=5m0s
Close a tab by clicking the "×" button. This:
- Closes the WebSocket connection
- Triggers graceful shutdown of the PTY process
- Stops all associated containers
- Removes session from tracking
The server logs active session count:
INFO Session created id=session-1 total=1
INFO Session created id=session-2 total=2
INFO Session removed id=session-1 total=1
Press Ctrl+C in the terminal running the web server:
Shutting down web terminal server...
Stopping 3 active session(s)...
All sessions stopped
Stopping HTTP server...
Web terminal server stopped gracefully
Access the CLI from any device on your network:
# Start server on your workstation
infer chat --web --host 0.0.0.0 --port 8080
# Access from laptop, tablet, or phone
# Navigate to: http://workstation-ip:80800.0.0.0 exposes the server to your entire network. Only do this on trusted networks.
Work on multiple tasks simultaneously without terminal multiplexer:
- Tab 1: Chat with
claude-4.1-haikufor quick questions - Tab 2: Long-running agent task with
deepseek-v4-pro - Tab 3: Testing with
gpt-4o
Each tab is completely independent with its own model, conversation history, and containers.
Share terminal access for pair programming or troubleshooting:
# Start server accessible on LAN
infer chat --web --host 0.0.0.0
# Team members connect to http://your-ip:3000
# Each person gets their own tab/sessionKeep sessions alive in background:
- Start web server
- Open browser, create session
- Minimize browser (session keeps running)
- Return hours later, tab still active
- Or wait for auto-cleanup after inactivity timeout
Access from mobile devices where installing the CLI is impractical:
- iPhone/iPad: Safari or Chrome
- Android: Chrome or Firefox
- Terminal emulation with touch keyboard support
By default, the web terminal is secure for local development:
- Binds to
localhostonly (not accessible from network) - No authentication required (assumes trusted local user)
- Single-user assumed
When binding to 0.0.0.0 or non-localhost addresses:
- No Authentication: Anyone who can reach the server can execute commands
- Full CLI Access: Users have same permissions as the CLI process
- Tool Execution: Can run bash commands, modify files, spawn containers
- API Key Access: Can use configured API keys for LLM providers
- Network Exposure: Port scanning may discover the service
Recommendations for remote access:
-
Use SSH tunnel instead of binding to 0.0.0.0:
# On server infer chat --web # On client ssh -L 3000:localhost:3000 user@server # Access at http://localhost:3000
-
Restrict host binding to specific interface:
# Bind to private network interface only infer chat --web --host 192.168.1.100 -
Firewall rules: Block port from untrusted networks
-
VPN: Only expose on VPN interface
-
Reverse proxy with auth: Use nginx/Caddy with authentication
Example nginx config with basic auth:
server {
listen 80;
server_name terminal.example.com;
auth_basic "CLI Access";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Authentication and authorization features are planned for future releases. For now, use network-level controls for security.
Error:
Error: listen tcp :3000: bind: address already in use
Solutions:
# Use different port
infer chat --web --port 8080
# Or kill process using port
lsof -ti:3000 | xargs kill -9
# Or find and choose available port
netstat -an | grep LISTEN | grep 3000Symptom: Browser console shows "WebSocket error" or "Connection closed"
Solutions:
-
Check server is running:
curl http://localhost:3000 # Should return HTML -
Check browser console for detailed error
-
Verify WebSocket endpoint:
// Should connect to ws://localhost:3000/ws -
Check for proxy/firewall blocking WebSocket connections
Symptom: Docker containers remain after closing tabs
Debugging:
# List all containers
docker ps -a
# Check if containers have CLI labels
docker ps -a --filter "label=created_by=infer"Solution: Ensure signal handling is working:
- ServiceContainer.Shutdown() must complete
- Check logs for "Shutting down session manager"
- Increase shutdown timeout in config if needed
Symptom: Tab close button doesn't respond, or server hangs on shutdown
Debugging:
# Check logs for shutdown sequence
# Should see:
# - "Stopping PTY session..."
# - "Initiating graceful shutdown of PTY process"
# - "PTY process exited gracefully after PTY close"Solutions:
-
Force kill stuck process:
# Find PID from logs kill -9 <pid>
-
Restart server if cleanup is stuck
-
Check for deadlock in logs (should not happen with current implementation)
Symptom: Server memory grows with many sessions
Explanation: Each session spawns:
- PTY process (BubbleTea TUI)
- Potential Docker containers (agents/MCP)
- WebSocket connection buffers
Solutions:
-
Lower inactivity timeout:
web: session_inactivity_mins: 2 # Aggressive cleanup
-
Limit concurrent sessions: Close unused tabs
-
Monitor with:
# Session count in logs grep "total" server.log # Container count docker ps | wc -l # Memory usage docker stats
Symptom: Browser shows blank page or "404 Not Found" for CSS/JS
Debugging:
# Check embedded files
grep "Embedded file found" server.log
# Should show:
# static/xterm.js
# static/xterm.css
# static/xterm-addon-fit.js
# static/app.jsSolution: Rebuild binary to re-embed static files:
task build
# Or
go build -o infer .Symptom: Text cut off or excessive padding
Solutions:
- Browser zoom: Reset to 100%
- Window resize: Terminal auto-resizes with window
- Manual resize: Trigger browser window resize event
- Check CSS: Padding configured in
index.htmltemplate
Current padding:
.terminal-tab {
top: 5px; /* Top margin */
padding: 0 25px; /* Horizontal padding only */
}Symptom: Connection refused from phone/tablet
Solutions:
-
Check host binding:
# Must bind to 0.0.0.0 or LAN IP infer chat --web --host 0.0.0.0 -
Check firewall: Allow incoming on port 3000
-
Verify network: Devices on same network/VLAN
-
Use IP address: http://192.168.1.100:3000 instead of hostname
-
Check router: Some routers block client-to-client traffic
For long-running tasks:
web:
session_inactivity_mins: 60 # 1 hour timeoutFor aggressive cleanup:
web:
session_inactivity_mins: 1 # 1 minute timeoutDisable timeout (not recommended):
web:
session_inactivity_mins: 0 # Never cleanup (requires manual close)Run multiple servers on different ports:
# Terminal 1: Personal use
infer chat --web --port 3000
# Terminal 2: Team access
infer chat --web --port 8080 --host 0.0.0.0Each server maintains independent sessions.
Run the web terminal in a Docker container:
FROM ghcr.io/inference-gateway/cli:latest
EXPOSE 3000
CMD ["chat", "--web", "--host", "0.0.0.0"]Build and run:
docker build -t infer-web .
docker run -d -p 3000:3000 --env-file .env infer-webNote: Requires Docker-in-Docker or Docker socket mount for container management:
docker run -d -p 3000:3000 \
-v /var/run/docker.sock:/var/run/docker.sock \
--env-file .env \
infer-web- xterm.js: Browser terminal emulator (v5.3.0)
- xterm-addon-fit: Auto-sizing terminal to container
- gorilla/websocket: WebSocket server implementation
- creack/pty: PTY management for subprocess spawning
cli/
├── cmd/
│ └── chat.go # --web flag handling
├── config/
│ ├── config.go # WebConfig struct
│ └── defaults.go # Default web config
└── internal/
└── web/
├── server.go # HTTP + WebSocket server
├── session_manager.go # Session lifecycle
├── pty_manager.go # PTY subprocess control
├── static/ # Embedded assets
│ ├── xterm.js # Terminal emulator
│ ├── xterm.css # Terminal styles
│ ├── xterm-addon-fit.js # Fit addon
│ └── app.js # Tab management
└── templates/
└── index.html # HTML template
Global port registry prevents race conditions:
// internal/config/config.go
var (
allocatedPorts = make(map[int]bool)
portMutex sync.Mutex
)
func FindAvailablePort(basePort int) int {
portMutex.Lock()
defer portMutex.Unlock()
// Check port availability and register atomically
// ...
}Used by Docker containers spawned by agents/MCP servers.
Signal handling ensures cleanup:
// cmd/chat.go
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
services.Shutdown(ctx)
}()Shutdown sequence:
- Ctrl+C sends SIGINT
- Signal handler calls ServiceContainer.Shutdown()
- Session manager stops all sessions
- Each session shuts down PTY process gracefully
- ServiceContainer stops all containers
- HTTP server shuts down with 5-second timeout
Planned features for future releases:
- Authentication: Basic auth, OAuth, API keys
- Authorization: Per-user session limits, tool restrictions
- SSH Sessions: Connect to remote servers via SSH
- Session persistence: Save/restore conversations across restarts
- Shared sessions: Multiple users in same terminal (read-only viewers)
- Recording: Save terminal output for later review
- Themes: Configurable color schemes
- Keybindings: Browser-compatible keyboard shortcuts
- File uploads: Drag-and-drop files into terminal
- Mobile improvements: Better touch keyboard support