Building a Custom Chat UI for OpenClaw


When I first started experimenting with OpenClaw, I wanted a simple, fast way to talk to my assistant — no mobile app, no web framework, no dependencies. Just a chat interface.

The OpenClaw gateway exposes a REST API on 127.0.0.1:18789 with streaming responses. That’s enough to build a full chat interface with. So that’s what I did.

The whole thing is four files: 68 lines of HTML, 651 lines of CSS, 824 lines of JavaScript, and a 173-line Node.js proxy server. 1,736 lines total. No build step. No npm install. No frameworks. Just static files served by a simple Node.js proxy on port 8888.

The proxy does one thing: it forwards API requests to the gateway, handles authentication transparently, and serves static files from disk so edits take effect instantly without any restart. That last part turned out to be the most underrated feature — I could tweak CSS, fix a JavaScript bug, and see the result in the browser immediately. No reload. No hot-reload config. Just edit and refresh.

The chat UI itself uses localStorage to persist sessions. A sidebar shows your chat history, a main area renders messages as Markdown with syntax highlighting (via highlight.js), and the input bar handles streaming responses with a thinking animation. Sessions are saved to localStorage as JSON, keyed by session ID.

One of the more interesting challenges was the model selection. OpenClaw has multiple models available, and you can set a model per session. The UI includes a dropdown to pick a model, with a lock icon to indicate when a session’s model has been overridden from the default. That required fetching the model list from the gateway on load, storing the current model per session, and syncing with the gateway API.

The styling was another interesting problem. I wanted a clean, minimal design that worked well in both light and dark modes. The CSS uses custom properties for theming (--bg-*, --text-*, --border-*), and data-theme is set on the <html> element. The sidebar collapses on mobile, messages use a chat-bubble layout, and code blocks get syntax highlighting with copy buttons.

The proxy server itself is simple: it serves static files directly from disk (so edits are live), proxies API requests to the gateway with proper headers, and handles CORS. No session management, no authentication UI — just forwarding requests with the gateway token from the configuration.

One thing I found interesting about the architecture is the separation of concerns. The HTML is structure, the CSS is presentation, the JS is behavior, and the proxy is infrastructure. Each layer is thin and does one thing. That makes it easy to debug and modify.

The biggest bug I ran into was with the session list. When a chat was deleted, the UI removed it from localStorage but the sidebar still showed the old list because the in-memory cache wasn’t updated. The sidebar rendered from the stale cache instead of localStorage. The fix was straightforward — update the cache after filtering. But it highlighted a common pattern in small apps: state lives in multiple places, and keeping them in sync is the actual work.

The print functionality turned out to be one of the more surprising features. The original “Save as HTML” was replaced with a print button that opens a print-friendly window with all CSS inlined, auto-triggers the print dialog, and handles page breaks for code blocks while allowing chat bubbles to flow across pages. It was built with a single window.open() call and document.write() of an inlined document.

The project started on June 1 and is now on commit d3966e4 with 24 commits. It’s a small project, but it’s exactly what I needed — a fast, local chat interface with no dependencies, no cloud services, and zero setup beyond pointing a browser at localhost:8888.

I keep thinking about the separation between what people think building software involves and what it actually involves. On the surface, it’s just writing HTML and CSS. But the real work is the state management, the edge cases, the bugs that reveal themselves only after you’ve been using it for a while. The chat UI looks simple. It took more iterations than I expected to get it right.

The most valuable thing about it isn’t the features — it’s the feedback loop. Edit, refresh, done. That kind of speed changes how you build things. You stop being afraid to make changes. You start iterating instead of planning. You end up with something that works better than the plan would have suggested, simply because you had room to discover what worked as you went.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *