Web Interface
Browser-based dashboard for clhorde — submit prompts, monitor workers, and interact with Claude sessions from any device.
- Quick Start
- Dashboard
- Prompt Detail & Terminal
- REST API Reference
- WebSocket API
- Store Management
- Authentication
- CLI Options
- Systemd Service
Quick Start
The web interface is provided by clhorde-web, a thin HTTP/WebSocket bridge that connects to the running daemon. It serves a vanilla JS single-page application with no build step required.
# 1. Start the daemon (if not already running)
$ clhorded &
# 2. Start the web server
$ clhorde-web
# 3. Open in your browser
$ open http://localhost:3120
The web dashboard connects via WebSocket and reflects the same state as the TUI and CLI clients. All three can run simultaneously.
clhorde-web binary at compile time via include_dir!. No external files needed.
Dashboard
The main view shows a split layout: prompt list on the left, detail view on the right.
Prompt List
- Color-coded status badges: pending running completed failed
- Mode indicator: IA (interactive) or 1S (one-shot)
- Worktree indicator:
[WT]when worktree isolation is enabled - Sorted: running first, then pending, then completed/failed (newest first)
- Click a prompt to view its detail in the right panel
Submit a Prompt
The submission form at the top of the sidebar provides:
- Text area — multi-line prompt input
- Mode selector — interactive or one-shot
- Worktree toggle — enable per-prompt git worktree isolation
- CWD field — optional working directory override (collapsed by default)
- Submit — click the button or press Ctrl+Enter
Filter & Search
Use the search input and status filter chips above the prompt list to narrow down prompts. Text search is case-insensitive substring matching. Status chips and text search combine (both must match). Press Esc to clear.
Worker Controls
The footer shows active and max worker counts, plus the default mode. Click +/- to adjust max workers (1–20) or click the mode label to toggle between interactive and one-shot.
Prompt Detail & Terminal
One-shot Prompts
Selecting a one-shot prompt shows an ANSI output viewer with full color support. Live output streams in real time via WebSocket OutputChunk events. Auto-scroll follows new output.
Running one-shot prompts show a follow-up input bar at the bottom — type a message and press Enter to send.
Interactive Prompts (xterm.js)
Selecting an interactive prompt opens a full terminal emulator powered by xterm.js with WebGL rendering. The complete Claude Code TUI renders in the browser — colors, cursor, scrollback, and all.
- PTY bytes are base64-encoded and streamed over WebSocket
- Keyboard input is forwarded back to the daemon PTY
- Terminal resizes automatically with the browser window
- Per-prompt PTY subscription avoids flooding all clients with all PTY data
Action Buttons
The detail header shows context-sensitive action buttons:
| Status | Available Actions |
|---|---|
| running | Kill, Delete |
| completed / failed | Retry, Resume, Delete |
| pending | Move Up, Move Down, Delete |
Destructive actions (Kill, Delete) require a confirmation dialog.
REST API Reference
All endpoints are prefixed with /api/ and return JSON responses with Content-Type: application/json.
Health & State
| Method | Path | Description |
|---|---|---|
GET | /api/health | Health check — returns { "status": "ok" } |
GET | /api/state | Full daemon state snapshot (prompts, workers, config) |
Prompts
| Method | Path | Description |
|---|---|---|
GET | /api/prompts | List all prompts |
GET | /api/prompts/:id | Get single prompt (404 if not found) |
GET | /api/prompts/:id/output | Full output text for a prompt |
POST | /api/prompts | Submit a new prompt |
POST | /api/prompts/:id/input | Send follow-up input to running worker |
POST | /api/prompts/:id/kill | Kill running worker |
POST | /api/prompts/:id/retry | Retry completed/failed prompt |
POST | /api/prompts/:id/resume | Resume with --resume |
DELETE | /api/prompts/:id | Delete a prompt (returns 204) |
POST | /api/prompts/:id/move-up | Move pending prompt up in queue |
POST | /api/prompts/:id/move-down | Move pending prompt down in queue |
Submit Prompt Body
{
"text": "Review the auth module", // required
"mode": "interactive", // default: "interactive"
"worktree": false, // default: false
"cwd": "/path/to/project", // optional
"tags": ["review", "auth"] // optional
}
Valid modes: "interactive", "one-shot", "one_shot", "oneshot"
Send Input Body
{
"text": "Continue with the refactor" // required, non-empty
}
Configuration
| Method | Path | Description |
|---|---|---|
PUT | /api/config/max-workers | Set max worker count (1–20) |
PUT | /api/config/default-mode | Set default prompt mode |
PUT | /api/prompts/:id/mode | Set mode for a specific prompt |
Set Max Workers Body
{ "count": 5 } // range: 1–20
Set Mode Body
{ "mode": "one-shot" }
Store
| Method | Path | Description |
|---|---|---|
GET | /api/store | List persisted prompts |
GET | /api/store/count | Counts by status |
GET | /api/store/path | Storage directory path |
POST | /api/store/drop | Drop prompts by filter |
POST | /api/store/keep | Keep prompts by filter, drop the rest |
POST | /api/store/clean-worktrees | Remove lingering git worktrees |
Store Filter Body
// For /api/store/drop:
{ "filter": "all" } // "all", "completed", "failed", "pending"
// For /api/store/keep:
{ "filter": "completed" } // "completed", "failed", "pending" (no "all")
Error Responses
Errors return JSON with an error field:
| Status | Meaning |
|---|---|
400 | Invalid request body (validation error) |
401 | Authentication required (when token is configured) |
404 | Prompt not found |
502 | Daemon unavailable |
{ "error": "prompt 99 not found" }
WebSocket API
Connect to ws://localhost:3120/api/ws for real-time event streaming. On connection, the server sends a StateSnapshot event to bootstrap the client.
Server → Client
Daemon Events
{
"type": "DaemonEvent",
"event": {
"type": "WorkerStarted",
"prompt_id": 1
}
}
Event types: StateSnapshot, PromptAdded, PromptUpdated, PromptRemoved, WorkerStarted, WorkerFinished, WorkerError, OutputChunk, MaxWorkersChanged, and more.
PTY Bytes
{
"type": "PtyBytes",
"prompt_id": 1,
"data": "G1szMW0=" // base64-encoded raw PTY bytes
}
Client → Server
Client Requests
{
"type": "ClientRequest",
"request": {
"type": "Ping"
}
}
PTY Subscription
// Subscribe to PTY bytes for a specific prompt
{ "type": "SubscribePty", "prompt_id": 1 }
// Unsubscribe
{ "type": "UnsubscribePty", "prompt_id": 1 }
Store Management
Click the Store tab in the navigation to access the store management view. This mirrors the clhorde-cli store functionality:
- Status counts — summary cards showing pending, running, completed, and failed counts
- Prompt list — all persisted prompts with ID, status, mode, and text
- Drop actions — drop all, completed, or failed prompts (with confirmation)
- Keep actions — keep only completed, failed, or pending prompts
- Clean worktrees — remove lingering git worktrees from completed prompts
- Storage path — shows the directory where prompt files are stored
Authentication
By default, clhorde-web binds to 127.0.0.1 (localhost only) and requires no authentication. When exposing on a network interface, you should configure a token:
# Via CLI flag
$ clhorde-web --host 0.0.0.0 --auth-token my-secret-token
# Or via environment variable
$ CLHORDE_WEB_AUTH_TOKEN=my-secret-token clhorde-web --host 0.0.0.0
When a token is configured:
- All
/api/*requests requireAuthorization: Bearer <token> - WebSocket upgrades accept the token as
?token=<token>query parameter - Static files (the web UI itself) are served without authentication
- The frontend shows a login overlay that stores the token in
localStorage - Unauthenticated requests return
401with{ "error": "Authentication required" }
clhorde-web prints a warning: "Warning: serving without authentication on a network interface".
CLI Options
$ clhorde-web --help
| Flag | Default | Description |
|---|---|---|
--port | 3120 | Port to listen on |
--host | 127.0.0.1 | Host address to bind to |
--static-dir | (embedded) | Override static file directory (for development) |
--daemon-socket | (auto) | Override path to daemon Unix socket |
--auth-token | (none) | Require this token for API access |
--cors-origin | (disabled) | Allow CORS requests from this origin |
-v / -vv | warn | Verbosity: -v = info, -vv = debug |
Environment variables: CLHORDE_WEB_AUTH_TOKEN, CLHORDE_WEB_CORS_ORIGIN
Systemd Service
Run clhorde-web as a systemd user service alongside the daemon:
[Unit]
Description=clhorde web interface
After=clhorded.service
Requires=clhorded.service
[Service]
ExecStart=/path/to/clhorde-web
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
$ systemctl --user enable clhorde-web.service
$ systemctl --user start clhorde-web.service
To expose on the network with authentication, override the command:
[Service]
ExecStart=/path/to/clhorde-web --host 0.0.0.0 --auth-token %H/.config/clhorde/web-token
Environment=CLHORDE_WEB_AUTH_TOKEN=your-token-here