
A sleek, dark-themed webchat UI for OpenClaw — monitor sessions, stream responses, and inspect tool calls in real-time.

prefers-reduced-motion support, ARIA live regionsdocker run -p 3000:80 ghcr.io/marlburrow/pinchchat:latest
Open http://localhost:3000 and enter your OpenClaw gateway URL + token on the login screen.
Or use Docker Compose:
curl -O https://raw.githubusercontent.com/MarlBurroW/pinchchat/main/docker-compose.yml
docker compose up -d
Prerequisites: Node.js 20+, an OpenClaw gateway running and accessible.
git clone https://github.com/MarlBurroW/pinchchat.git
cd pinchchat
npm install
cp .env.example .env
npm run dev
Optionally edit .env to pre-fill the gateway URL:
VITE_GATEWAY_WS_URL=ws://localhost:18789
VITE_LOCALE=en # or "fr" for French UI
npm run build
npx vite preview
Or serve the dist/ folder with nginx, Caddy, or any static file server.
All configuration is optional — credentials are entered at runtime via the login screen.
| Variable | Description | Default |
|---|---|---|
VITE_GATEWAY_WS_URL |
Pre-fill the gateway URL on the login screen | ws://<hostname>:18789 |
VITE_LOCALE |
UI language (en or fr) |
en |
Note: The gateway token is entered at runtime and stored in
localStorage— it is never baked into the build.
PinchChat stores all preferences in localStorage — no server-side config needed.
Click the palette icon in the header to switch between:
| Theme | Description |
|---|---|
| Dark (default) | Zinc-based dark theme, easy on the eyes |
| Light | Clean light mode with white backgrounds |
| OLED | Pure black backgrounds for OLED screens |
| System | Follows your OS dark/light preference |
Six accent colors are available: Cyan (default), Violet, Emerald, Amber, Rose, and Blue. The accent tints buttons, links, progress bars, and user message bubbles.
These settings persist across sessions:
</> button in the input areagraph TD
subgraph Browser["🌐 PinchChat (Browser)"]
Login["LoginScreen
<i>credentials</i>"]
App["App.tsx
<i>router</i>"]
UI["Chat + Sidebar
<i>main UI</i>"]
Hook["useGateway
<i>WebSocket state machine</i>
auth · sessions · messages"]
Login --> App --> UI
App & UI --> Hook
end
Hook <-->|"WebSocket (JSON frames)"| Gateway["🔌 OpenClaw Gateway
<code>ws://host:18789</code>"]
Gateway <-->|API| LLM["🤖 LLM Provider
<i>Anthropic, OpenAI, etc.</i>"]
| File | Role |
|---|---|
src/hooks/useGateway.ts |
WebSocket connection, auth, message streaming, session management |
src/components/LoginScreen.tsx |
Runtime credential entry (stored in localStorage) |
src/components/Chat.tsx |
Message list with auto-scroll and streaming display |
src/components/ChatInput.tsx |
Input with file upload, paste, drag & drop, image compression |
src/components/ChatMessage.tsx |
Markdown rendering, tool calls, thinking blocks |
src/components/Sidebar.tsx |
Session list with token usage bars and activity indicators |
src/components/Header.tsx |
Connection status, token progress bar, logout |
src/lib/i18n.ts |
Lightweight i18n (English + French) |
src/lib/gateway.ts |
WebSocket protocol helpers and message types |
localStorageuseGateway opens a WebSocket and authenticates with the token📖 For a deeper dive into the codebase structure, see ARCHITECTURE.md.
PinchChat uses a zero-dependency i18n system. Adding a new language takes ~5 minutes:
src/lib/i18n.ts and duplicate the en object with your locale code:const de: typeof en = {
'login.title': 'PinchChat',
'login.subtitle': 'Verbinde dich mit deinem OpenClaw-Gateway',
// ... translate all keys
};
messages record (same file):const messages: Record<string, typeof en> = { en, fr, de };
export const localeLabels: Record<string, string> = {
en: 'EN',
fr: 'FR',
de: 'DE',
};
VITE_LOCALE, and browser auto-detection all pick it up automatically.Tip: TypeScript enforces that your new locale object has the same keys as
en— missing translations won't compile.
Press ? anywhere to open the shortcuts panel.
| Shortcut | Action |
|---|---|
Enter |
Send message |
Shift + Enter |
New line |
Esc |
Stop generation / close sidebar |
Ctrl/⌘ + F |
Search messages in current session |
Ctrl/⌘ + K |
Focus session search |
Alt + ↑ / Alt + ↓ |
Switch between sessions |
? |
Show shortcuts help |
ws://host:18789 (or wss:// if behind TLS)wss:// — browsers block mixed contentlocalStorage (Application tab → Storage → Clear site data) and logging in again# Check what's using port 3000
lsof -i :3000
# Use a different port
docker run -p 8080:80 ghcr.io/marlburrow/pinchchat:latest
PinchChat includes built-in debugging tools:
WebSocket debug logging — open the browser console (F12) and run:
localStorage.setItem('pinchchat:debug', '1');
Reload the page. All WebSocket frames (sent and received) will be logged to the console with a [GW] prefix. Set to '0' to disable.
Raw JSON viewer — click the { } toggle on any message to see the full gateway payload as formatted JSON. Useful for understanding the protocol or reporting bugs.
Metadata inspector — hover over any message and click the ℹ️ icon to see message metadata (timestamp, ID, channel, sender info).
# Ensure Node.js 20+
node --version
# Clean install
rm -rf node_modules package-lock.json
npm install
npm run build
PinchChat is a static SPA — serve it behind any reverse proxy. The only special requirement is WebSocket support for the OpenClaw gateway connection.
Note: PinchChat itself only serves static files (HTML/JS/CSS). The WebSocket connection goes directly from the browser to your OpenClaw gateway, not through PinchChat's server. You only need to proxy WebSocket if you're also putting the gateway behind the same reverse proxy.
server {
listen 443 ssl;
server_name chat.example.com;
# PinchChat static files
location / {
proxy_pass http://127.0.0.1:3000;
# Or serve directly from the build output:
# root /path/to/pinchchat/dist;
# try_files $uri $uri/ /index.html;
}
}
# Optional: proxy the OpenClaw gateway too
server {
listen 443 ssl;
server_name gw.example.com;
location / {
proxy_pass http://127.0.0.1:18789;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400s; # Keep WebSocket alive
}
}
chat.example.com {
reverse_proxy 127.0.0.1:3000
}
# Optional: proxy the gateway
gw.example.com {
reverse_proxy 127.0.0.1:18789
}
Caddy handles WebSocket upgrades and TLS automatically.
services:
pinchchat:
image: ghcr.io/marlburrow/pinchchat:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.pinchchat.rule=Host(`chat.example.com`)"
- "traefik.http.routers.pinchchat.tls.certresolver=letsencrypt"
- "traefik.http.services.pinchchat.loadbalancer.server.port=80"
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: pinchchat
spec:
entryPoints: [websecure]
routes:
- match: Host(`chat.example.com`)
kind: Rule
services:
- name: pinchchat
port: 80
tls:
certResolver: letsencrypt
wss:// — browsers block mixed content (https page → ws:// connection)proxy_buffer_size, proxy_buffers in Nginx)GET / with the SPA — use it as a health check endpoint$ claude mcp add pinchchat \
-- python -m otcore.mcp_server <graph>