/web opens ChatCLI in your browser, next to the terminal, on the same session. What you type in the browser continues in the terminal, in MCP, in ACP and in your channels, and the other way round, because the browser drives the very same engine and the same saved sessions the other surfaces drive. chatcli web serves it without a terminal.
It is local by construction: the server binds the loopback interface on a free port, every run mints its own token, the token travels in a header the page reads from the address once, and nothing is sent anywhere but your own machine.

A tour of the page: the panel with status, the tool catalog with its filter, skills and commands; / and @ open palettes in the composer; the mode tabs; the light theme. (Synthetic data.)
Opening it
/web works mid-run, like /dash. A terminal that is not bound to a saved session gets bound to a fresh one first (web-<date>), so the conversation has a name both surfaces can follow; a bound terminal shares its session as is. The browser opens with exactly what that session holds: run /web mid-conversation and the page starts with those turns and syncs from there; run it on a terminal that has not typed yet and the page starts empty. It never refills from the rolling mcp-web mirror of a previous run. Closing the terminal stops the web UI: the address carried a token only that process handed out.
chatcli web on its own does the same: without --session it binds the browser to a fresh web-<date> session, created on the first completed turn, so each standalone run keeps its own conversation instead of overwriting the rolling mcp-web mirror. Nothing is saved “at the end”: every completed turn is written through as it finishes, so Ctrl+C or closing the tab loses at most a turn that was still streaming. A terminal bound to a named session, including the web-<date> one /web creates, is flushed once more on exit and is not duplicated as an autosave- file.
The browser only opens from an interactive terminal.
chatcli web --no-browser, or a pipe, prints the address instead.Continuity across surfaces
The web UI is not a second engine. It runs on the shared RPC backend that the MCP server, the ACP server and the chat gateway run on, and it keeps a session the way they do:- the browser’s live history is bound to a saved session and written through after every turn;
- before each turn the bound session is re-read when another surface changed it (the terminal, an IDE over ACP, a WhatsApp or Telegram conversation through the gateway), so a reply given elsewhere is in the transcript before you continue;
- the Conversation Hub thread of the principal is resumed, exactly as
chatcli mcp-serverdoes.
web-<date> session of its own (the file appears on the first turn), so the new conversation is saved and listed like any other; the terminal keeps the session it had, and the header names the session the page is bound to. Deleting is the same /session delete the terminal runs.
What you see
Switching MCP servers and skills
Panel → MCP has a switch per configured server. Turning it on or off does exactly what/mcp start <name> and /mcp stop <name> do: the server shows starting… while it connects, and the tool list refreshes when it is up. Panel → Skills has a pin switch per skill, the same as /skill pin <name> and /skill unpin <name>: a pinned skill joins every turn of the session, and pinned skills are listed first with a count. Skills marked manual-only (disable-model-invocation) cannot be pinned; their switch is disabled and they still run when you call them as /<name>.
Like their terminal commands, the switches last while the web process runs: MCP servers come back as configured on the next start, and pins belong to the session. /web serves the page from its own chatcli web process, so a switch changes the servers and pins of the browser’s turns, not the terminal’s.
Modes and permissions
Chat turns stream token by token through the provider’s streaming path. Coder and agent turns stream the loop’s structured events: thoughts, messages, every tool call as it starts and ends, and the plan. When a loop reaches a policy-gated action (anask rule, a dangerous command), the page shows the same permission dialog the IDE gets over ACP: allow once, allow always, deny, deny always. No answer within the timeout (CHATCLI_MCP_PERMISSION_TIMEOUT, default 10 minutes) denies the action once, so a walked-away browser never leaves a loop hanging.
One turn at a time, as in the terminal: while a turn runs, sending another is refused with a clear notice instead of queueing silently. Closing the tab cancels the run.
Provider and model
The selectors show what the engine can reach: providers with credentials, and per provider the models the catalog knows plus what the provider’s API lists live. Picking one sets the route for the turns that follow, without touching the terminal’s own selection. Skills that pin a model still win for the turns they fire on.Slash commands and tools in the composer
A line that starts with/ or @ is routed the way the terminal routes it, before it reaches the model. Commands work typed in the composer, picked from the / autocomplete, or run from Panel → Commands. After a command and a space the autocomplete offers its subcommands, flags and values from the terminal’s own completer, so /mcp lists status, start, stop and the rest with their descriptions, and /skill pin lists skills. Arrows move, Tab or Enter accepts, Esc closes:
/chat,/agent,/runand/coderswitch the page’s mode;/coder fix the testswitches and runs the rest of the line in that mode.- Everything the ACP surface runs headless runs here too, listed by
GET /api/commands:/help,/session,/memory,/model,/switch,/cost,/policy,/config,/storage,/mcp,/skill,/dashand the others. They answer in the transcript./session load,attach,detachandforkalso re-render the page with the session they leave it bound to. - Custom command templates (
~/.chatcli/commands, the project’s.chatcli/commands,.claude/commands,~/.codex/promptsand the other sources) and user-invocable skills (/<skill> args) run as a turn in the page’s current mode, the way ACP runs them. - In chat mode a leading
@toolruns that tool directly, aschatcli tooldoes, and shows its output as a tool block;@file,@git,@envand@historystay context injectors for the model. - Any other
/textthat is not a known command goes to the model as user text, so a path or a typo is never hijacked.
/wait is not available, because it holds the conversation until a condition is met and the page runs one turn at a time: schedule the wait with /schedule <name> --wait <condition> and follow it with /jobs. The terminal’s large-file commands /retryall, /nextchunk and /skipchunk are refused and point to /retry.
Commands that only make sense in a terminal answer with the reason and what to do instead:
Any other known REPL command outside these lists answers that it is not available in the web app.
Images
Attach images with the file picker or paste them into the composer. A caption is optional: an image sent on its own asks the model to analyze it and describe what it shows.- Attached images travel with the turn and stay in the conversation for the turns that follow. A private copy of each is also saved under
~/.chatcli/attachments/(directory0700, files0600, sealed at rest whenCHATCLI_ENCRYPTION_KEYis set), and the turn names the saved path, so a coder or agent run can look at it again with@view <path>and the reference survives compaction and the terminal continuing the session. The copies expire with the session TTL (CHATCLI_SESSION_TTL, 90 days);/storagelists and prunes them as theattachmentsstore, and/config retentionshows the policy. - A line sent with attachments always goes to the model, even when it starts with an
@tool. In chat,@view shot.png what is wrong?attaches that local image the way@filedoes;@viewnever runs on its own in chat, because there the turn itself carries the image. - Vision is judged against the model the page routes the turn to: a vision model receives the image itself; a model without vision gets the describe fallback (the image is described by a vision model first).
- Limits: up to 32 MB per request; a larger one is refused with a clear
413naming the limit instead of a decoder error. Saved copies are capped at 20 MB per image.
Voice
Voice input works out of the box. The page prefers the embedded offline engines — Whisper for speech in, Kokoro for speech out, no API key and no account — for every direction thatCHATCLI_TRANSCRIPTION_* and CHATCLI_TTS_* leave unconfigured; a cloud API key alone is not a choice of speech engine (it is there for the chat models). CHATCLI_TRANSCRIPTION_PROVIDER, _CMD or _URL, and their CHATCLI_TTS_* counterparts, pin another engine and win.
- The first click on the microphone or the speak button asks before downloading the embedded engine (about 233 MB for Whisper base, about 158 MB for Kokoro; less when parts are already cached), with a progress bar and a cancel button, or lets you keep the engine that serves now (
Use <engine> for now) or postpone. The download runs in the background, survives closing the dialog, and the engine takes over as soon as it is ready. - Recordings are converted in the browser to 16 kHz mono WAV, so no ffmpeg is needed on the machine; a browser that cannot decode its own recording sends it as recorded and the server says why when it cannot decode it.
- The microphone shows its state — recording, transcribing — and errors with their reason (permission denied, no speech detected, unsupported browser). Click to talk, click again to stop; Esc discards a recording, or stops a reply being read.
- Talk starts a voice conversation: what you say is transcribed and sent on its own, the reply is spoken back without code or Markdown symbols, and the page listens again once the reply ends. A pause of about 1.3 s ends your turn; a listen that hears nothing ends the loop quietly; starting to record while a reply is being read stops it.
- The speak button on every reply uses the same engine choice. The macOS
sayfallback is converted to WAV on the way out, so it plays in every browser. - The status panel shows the engine serving each direction (
Voice in,Voice out) and offers the download when the offline engine is not installed yet;/config webprints the same rows in the terminal.
Only the web page prefers the embedded engines and asks before downloading them. The gateway and
@speak keep the process defaults: local and keyless engines first, the embedded engine from cache or as the last resort.Image generation
With an image provider configured (CHATCLI_IMAGE_*), the image button turns the composer text into an image. Features that are not configured are hidden, not broken.
The API behind the page
The page talks to a small JSON API on the same origin. It is documented here because a script can drive it as well as the page does, with the token from the address:
Every call carries
X-Web-Token: <token>; the token is never accepted from the query, the Host must match the bound address exactly (a DNS-rebinding guard), and the page ships with a strict Content-Security-Policy that allows no external resource.
Privacy and safety
- Loopback only. A request to bind another interface is refused.
- One token per run, 128 bits, in memory. Stop the process and the address is dead.
- The web UI runs the engine unattended like MCP and ACP: dangerous commands follow
CHATCLI_MCP_DANGER(blockturns them into in-band refusals) and the permission dialog covers theaskrules./policyin the terminal changes the rules for both. - The page is one embedded file. No CDN, no fonts, no analytics, works offline.
Configuration
/config web shows whether the web UI runs, its address, the child process, the bound session and the log file (~/.chatcli/web.log), plus the voice rows: the engine the page uses for each direction and the state of the offline engine it prefers (installed, or its download size).
Relationship to the other surfaces
Live Dashboard
What every process is doing, as a live graph. Open it from the web UI’s terminal with
/dash: the web UI shows up as its own web window, with its turns, model calls, tools and skills.Session Continuity
How a saved session follows you across the terminal, IDEs and channels.