DisPatch: A Self-Hosted Chat App for Local AI Agents

Posted
July 11, 2026
Updated
July 11, 2026
By
Jacob Lloyd β€” written with AI assistance, post-project
Read time
11 min read

In plain terms: DisPatch is three things in one, hosted entirely on your own computer: a chat app for talking to AI assistants that also live there, a file server, and a terminal β€” available on all your devices at all times, with nothing going to a cloud service. Its file server moves files from any device onto the computer the assistants run on, so they can work with what you upload. On shared devices, a safe subset of assistants is available without a PIN; entering the PIN unlocks the full-power ones.

DisPatch is the app I built so I could stop running a 30-container Rocket.Chat install just to text my own AI agents. It ended up as three things in one β€” a locally hosted LLM chat app, a file server, and a terminal β€” available on every device I own, at all times. It's a ~720KB zip, it's free, and it's the only chat front end I use now.

TL;DR

  • What it is: Three things in one, self-hosted: a web chat for a stable of local AI agents (an OpenClaw gateway), a drag-drop file server, and a real terminal β€” on every device on your LAN or Tailscale network, at all times.
  • What it costs: $0. No SaaS seat, no account.
  • What you need: Linux, Python 3.12+, and uv. An OpenClaw gateway if you want typed replies β€” everything else works without one.
  • What you end up with: per-bot threads, streaming markdown, a drag-drop File Server, an in-app coding-agent terminal, a PIN-lockable "Safe Mode", a push API for cron jobs, and 187 tests that pass straight out of the zip.

What you end up with

Before any setup talk, this is what it looks like doing its job. Shots are from a sandboxed copy with dummy data β€” nobody needs to see my actual bot logs.

DisPatch desktop layout at 1440px: a bot rail of colored letter-block avatars down the left, a thread list, and a chat pane showing a markdown morning briefing with a syntax-highlighted code block and a model badge.
Desktop, 1440px: bot rail, thread list, chat pane. The briefing and code block are rendered markdown, not pasted screenshots.
DisPatch mobile layout at 390px: the same conversation in a single panel with a bottom tab bar for Bots, Chats, and Messages, and per-message Copy, Regenerate, and Delete actions.
Same conversation at 390px. The rail collapses into a bottom tab bar; each message gets Copy / Regenerate / Delete.
DisPatch in Safe Mode after a PIN is set: the bot rail is cut down to three approved bots, with an amber padlock button at the bottom to unlock the rest.
Safe Mode with a PIN set: only the bots flagged "safe," shown with the same avatars as the unlocked side. Not a CSS filter β€” the server never sends the rest.

What it actually does

  • Per-bot threads, everywhere. Open it on your phone mid-conversation and it's the same thread, same history, no re-login dance.
  • A File Server β€” drag-drop files between every device and the box. The feature I use most; it gets its own section below.
  • A terminal (Terminex) β€” a real shell in the browser running a coding agent. Also its own section below.
  • Streaming markdown replies with syntax-highlighted code (copy button included), images/video with a zoom lightbox, installable as a PWA.
  • A live "working" panel. Click the typing dots to watch the model's tool calls and thinking live β€” it's tailing the agent's transcript, not faking a spinner.
  • Two-tier lock. A PIN separates the full-power agents from a safe, general-info subset β€” details in the two-tier lock section below.
  • A push API β€” POST /api/inject β€” so a cron job or another agent can drop a message (or an image) into a thread. That's how a daily briefing lands on your phone without you asking.
  • Local image generation in the thread. Ask an agent for a picture and it can call a local ComfyUI instance; the result lands in the chat like any other media, lightbox and all. No image API, no upload to anyone.
  • Optional, off by default: a ComfyUI panel for starting or stopping that image-gen service from chat.

The File Server

Of everything in DisPatch, the File Server is the piece I use the most. It's a drag-drop file area served from the same box: open DisPatch on any device, drop a file on it, and the file is on the server β€” grouped by day, with thumbnails for images and video.

DisPatch File Server view: an upload drop zone at the top, then files grouped by day with image thumbnails and file-type cards.
The File Server: drop zone up top, uploads grouped by day below. From a phone, this is the whole workflow.

What makes it more than a convenience is where the files land: on the same machine the agents live on. A photo uploaded from my phone can be read off disk by any agent seconds later β€” described, filed, put on a website, fed to the image tools. It's the bridge between "a file on whatever device I'm holding" and "a file my agents can work with," with no cloud drive in the middle and no cables.

The unglamorous parts are handled: uploads are size-checked before they're buffered (a phone can't OOM the box), there's an overall storage cap, writes are atomic so an interrupted upload never leaves a half-written file, and the file store is mirrored into the same rotated backup scheme as the chat history. A config flag can also flip the whole thing into a one-way, upload-only drop box β€” devices can add files but not browse them β€” for setups where more people can reach the app than should be able to read its archive.

Terminex: a terminal in the chat

The other feature that earns its keep daily: DisPatch can put a real terminal in the browser. It appears in the bot rail like any other bot, but opening it attaches you to a server-side PTY streamed over a WebSocket into xterm.js β€” an actual shell session on the box, not a mock.

DisPatch terminal view: an xterm.js terminal pane inside the chat app with Start, Restart, and Stop controls, running a coding agent CLI.
Terminex: a server-side PTY in the chat app, with Start / Restart / Stop controls.

Mine launches straight into Reasonix, the Claude-Code-style coding agent I run on DeepSeek β€” the setup the assistant hub page calls Terminex. That turns a chat tab into a full coding-agent console: hand it a job, watch it work, from any device that can open the app. Paired with the File Server it makes the box fully driveable from a phone β€” drop a file in from wherever you are, open the terminal, tell the agent what to do with it.

  • Unlocked mode only. The terminal doesn't exist in Safe Mode, and it stays off entirely unless you enable it (LOCAL_CHAT_TERMINAL).
  • The session is in-memory. Start / Restart / Stop from the app; a server restart ends it. There's no persistent shell hiding in the background.

Why it exists

It replaced a full Rocket.Chat deployment β€” a database and a pile of containers so one person could talk to some local models. It worked, but that's a lot of infrastructure for "send text, get text back, sync it everywhere."

And the deeper answer to "why self-host a chat app at all": once your models run locally, the chat UI is the last piece still tempting you toward a cloud service. Self-hosting it means your prompts, your agents' output, and your family's messages never leave the house, and the whole loop β€” UI, gateway, models, image generation β€” keeps working with the internet unplugged. If you've already committed to local LLMs, a hosted chat front end is the odd one out.

One design choice worth calling out: DisPatch has no build step, on purpose. The frontend is vanilla JS (ES modules) and plain CSS/HTML. No bundler, no npm run build. My actual requirement going in was "easy for an AI agent to modify later." Edit a JS file, hit refresh, done. I'd make the same call again.

The stack

LayerWhat it is
BackendPython 3.12+, FastAPI + uvicorn, aiosqlite (one SQLite file, WAL mode), Pillow, managed with uv
Backend size~7,200 lines in backend/app/ (main.py alone is ~3,600), plus ~2,700 lines of tests
FrontendNo-build vanilla JS + HTML/CSS β€” ~4,900 lines of JS, ~2,100 of CSS/HTML/service-worker
Vendored libsmarked, highlight.js, DOMPurify, xterm.js β€” no CDN calls
DataOutside the code at ~/.local/share/local-chat/: DB, config.yaml, security.yaml, media/, files/, rotated DB backups

Setting it up

Condensed from README-SETUP.md inside the zip (download box below).

unzip dispatch-2026-07.zip
cd dispatch
./install.sh
./run.sh

That starts it on http://0.0.0.0:8765 and prints the LAN and Tailscale URLs, so you can tap one from your phone. From there:

  • Edit ~/.local/share/local-chat/config.yaml (auto-created, documented in config.yaml.example) so the bot ids match your OpenClaw agent ids. It hot-reloads.
  • Optional: ./install.sh --systemd renders a user systemd unit.
  • Optional: set a PIN from the gear icon β†’ Security. security.yaml.example documents the file, including api_token for anyone hitting /api/inject from off-box.
  • Env knobs: LOCAL_CHAT_HOST/PORT/DATA_DIR/AGENT_TIMEOUT/MAX_CONCURRENCY, plus LOCAL_CHAT_COMFY=0 / LOCAL_CHAT_TERMINAL=0 to keep the optional panels off.

The openclaw CLI itself isn't included β€” DisPatch expects an OpenClaw gateway already running somewhere it can reach. No gateway yet? The app still starts, warns you in the logs, and everything except typed replies works: threads, the inject API, the file server, PIN and Safe Mode. That's how I smoke-tested this zip β€” fresh unzip, uv sync, spare port, no gateway β€” and the packaged test suite ran 187 passed in 8.4s.

How a typed message actually happens

You type something and hit send. Here's the path that takes:

That's the happy path. The engineering went into the unhappy ones: the CLI times out, crashes, or a subagent answers ten minutes late. Four independent sources β€” live watcher, the CLI's JSON payload, a post-turn reconciler that re-scans the transcript, and a "follower" that tails for another ~30 minutes β€” feed one delivery funnel with turn-scoped dedup (canonicalized text plus media fingerprint). Any one path can fail and the reply still lands exactly once. Even on a CLI error the reconciler runs first, because the model often flushed its answer to the transcript before dying.

The two-tier lock: normal vs. Safe Mode

The distinction matters because the unlocked side isn't a toy. Those agents can actually do things: edit source code, push changes to my live website, run tools, manage the machine they live on. The locked side is general info only β€” still genuinely usable, you can ask questions, chat, and get real help β€” but deliberately fenced off from all of that power. The reason is simple: I have a three-year-old, and the family phone is not always in adult hands. Imagine what a three-year-old could do with full access from my phone β€” the site would be entirely Paw Patrol videos. So Safe Mode is child-proofing, enforced on the server. One small quality-of-life touch: the locked home screen now shows the same bot avatars as the unlocked side, instead of the old letter-block placeholders. Here's what happens when a request arrives with no PIN entered yet:

Lock yourself out and there's a recovery code file plus an editable YAML, not a support ticket. And if security.yaml gets corrupted mid-write, the failure mode is "stays locked," not "serves everything to anyone on the LAN." That was a deliberate call.

Gotchas

  • The API field is content, not text. POST /api/inject wants {"bot_id": "...", "content": "..."}. The single most common integration mistake, including ones I've made myself.
  • Don't pass long messages as a CLI argument. The kernel caps argv at 128KB (MAX_ARG_STRLEN); a long reply crashes the spawn with E2BIG. The code uses --message-file <tmpfile> β€” don't "simplify" it back to an argument.
  • Bot ids are case-sensitive in one specific spot. OpenClaw's gateway lowercases session keys in its index, but your ids are mixed-case (Doxy, DS_Brain). A case-sensitive lookup silently kills the working panel and the late-answer follower while everything else keeps working β€” nasty to notice. Exact match first, lowercase fallback.
  • The CLI only returns the final block. Anything an agent says between tool calls exists only in its transcript, not the CLI's return value β€” hence the watcher and reconciler, and the special-casing for an agent's "here's what I found so far" while it waits on a subagent.
  • Cross-site WebSocket hijacking is blocked by an Origin check. Both WS routes reject a handshake whose Origin host doesn't match Host; non-browser clients send no Origin and pass fine.
  • SVGs are deliberately not servable as media. A browser treats inline SVG as same-origin script β€” serving one back as an image is a stored-XSS hole. Media responses get a sandboxing CSP and nosniff instead.
  • Service-worker updates leave old tabs stuck on old JS. Looks exactly like a regression until you realize the tab never picked up the new worker. Fixed with an auto-reload on controllerchange.

Replicating the idea

Even if you never run my zip, the architecture is worth stealing. The whole thing is four independent pieces, each replaceable:

  • A chat backend that owns the history: one small web server (here FastAPI + a single SQLite file in WAL mode) with a WebSocket for live updates and a plain HTTP endpoint for pushing messages in. That's the entire "database and pile of containers" replacement.
  • An agent gateway the backend shells out to or calls over HTTP β€” here OpenClaw, but anything that takes "session id + message in, text out" works. Keeping it a separate process means the chat app survives the agent stack crashing, being upgraded, or being swapped entirely.
  • Local model servers behind the gateway (LM Studio, vLLM, Ollama β€” the gateway's problem, not the chat app's).
  • Optional media services, like a local ComfyUI for image generation, that agents call as tools and whose output the backend just stores as media.

The lessons that transfer regardless of stack: enforce your safe/locked mode on the server, never in CSS; treat "the model's reply reached the user exactly once" as a real distributed-systems problem, not a happy-path assumption; and keep the frontend build-free if you expect AI agents to be the ones maintaining it.

Downloads

Free for personal use. If it saves you an afternoon, the coffee button's nearby.


← More AI & Local LLM