The web UI is a React app that talks to the daemon over WebSocket via
the mew-web-client TypeScript library.
Getting started
Section titled “Getting started”just dev-ui # starts Vite dev server with HMRjust dev-ui -- --open # also launches browserThis auto-spawns mew daemon in the background and starts the Vite dev
server. Hot module replacement works for all React components.
Architecture
Section titled “Architecture”Browser → mew-web (bridge) → mew daemon (Unix socket)Three packages in a pnpm workspace:
mew-web-bridge(Rust): TCP+WS listener that relays browser WebSocket connections to the daemon’s Unix socket. Serves the built React app frommew-web-ui/dist/as static assets viainclude_dir. SPA fallback for client-side routing. Auto-spawnsmew daemonif not running.mew-web-client(TypeScript): Typed client for the wire protocol. Builds to ESM with.d.tstypes. TheMewClientclass manages the WebSocket connection, dispatches events to typed listeners, and provides promise-based methods for request/response patterns.mew-web-ui(TypeScript/React): React app with TanStack Router. Vite build todist/. Uses Zustand for state management.
The TypeScript client
Section titled “The TypeScript client”MewClient (mew-web-client/src/index.ts):
const client = new MewClient();await client.connect("ws://localhost:9847");
// Promise-based methods (wait for the matching server response):const models = await client.listModels();const result = await client.switchModel("deepseek", "deepseek-v4-flash");const variant = await client.setThinkingVariant("high");
// Event-based (streaming):client.on("provider", (ev) => { /* handle PartStart/PartDelta/etc */ });client.on("tool-start", (callId) => { /* ... */ });client.on("model-switched", (data) => { /* ... */ });Request/response methods use a pattern: register a one-time listener for the response event, send the request, resolve the promise when the response arrives.
Permission requests use a callback pattern: client.onPermissionRequest
registers a handler that receives a respond(decision) callback. The
handler can be async and the response is forwarded automatically.
The Zustand store
Section titled “The Zustand store”State lives in src/stores/session.ts. The SessionState interface defines
all state fields and actions:
interface SessionState { // Connection connectionState: ConnectionState; sessionId: string | null;
// Messages messages: ChatMessage[]; streamingPartId: string | null; streamingText: string;
// Model management availableModels: ModelInfo[]; currentModel: string | null; currentProvider: string | null; currentThinkingVariant: string | null;
// Session titles sessionTitles: Map<string, string>;
// Actions setAvailableModels: (models: ModelInfo[]) => void; setCurrentModel: (provider: string, model: string) => void; setCurrentThinkingVariant: (variant: string | null) => void; onSessionTitleChanged: (sessionId: string, title: string) => void; // ... 30+ more actions}The bridge function
Section titled “The bridge function”bridgeClientToStore wires client events to store actions:
export function bridgeClientToStore(client: MewClient, store: typeof useSessionStore) { client.on("session-ready", (data) => store.getState().setSessionId(data.session_id)); client.on("model-list", (data) => store.getState().setAvailableModels(data.models)); client.on("model-switched", (data) => store.getState().setCurrentModel(data.provider, data.model)); client.on("thinking-variant-changed", (data) => store.getState().setCurrentThinkingVariant(data.variant)); client.on("session-title-changed", (data) => store.getState().onSessionTitleChanged(data.session_id, data.title)); // ... 20+ more event bindings}Permission and ask-user requests use side-channel maps
(permissionResponders, askUserResponders) to pair the UI’s response
with the client’s callback.
App.tsx
Section titled “App.tsx”App.tsx handles:
- Connection lifecycle: exponential backoff reconnection (2s, 4s, 8s, cap 30s). On unexpected WS close, retries automatically and re-attaches to the same session.
- Session persistence:
sessionIdsaved tolocalStorage. On reload, reconnects viaattachSession, falling back tonewSessionif the session is gone. - Layout tree:
ChatSurface → TodoPanel → SubagentPanel → AskUserCard → InputArea
Key components
Section titled “Key components”| Component | Purpose |
|---|---|
App.tsx |
Root layout, reconnect logic, session attach |
ChatSurface.tsx |
Message list + streaming text |
MessageItem.tsx |
Single message with markdown rendering |
InputArea.tsx |
Prompt input (Cmd/Ctrl+Enter to send) |
ModelPill.tsx |
Model picker with thinking variant selection |
TitleStrip.tsx |
Session title + connection status |
ToolCallCard.tsx |
Tool call display with inline output |
CodeBlock.tsx |
Shiki syntax-highlighted code blocks |
MarkdownBody.tsx |
Markdown renderer (streaming + finalized) |
SessionRail.tsx |
Session list sidebar |
StatusFooter.tsx |
Token count, model, connection status |
Building
Section titled “Building”just build-web # builds Rust + TS, embeds dist into mew-web-bridgeThe built mew-web binary serves the React app from embedded assets at
runtime. Vite hashes asset filenames, so files are served dynamically by
path lookup rather than hardcoded include_bytes!.
Adding a new wire event end-to-end
Section titled “Adding a new wire event end-to-end”- Protocol: Add
ClientMessageorServerMessagevariant + roundtrip test inmew-protocol/src/lib.rs - Daemon: Handle in
handle_connection(mew-daemon/src/lib.rs). Updatetranslate_server_messageinmew-daemon/src/client.rsif needed. - TS client: Add the event type to the
MewClientEventsinterface, add the dispatch case inhandleMessage, and add any request method. - Store: Add state field + action to
SessionState, initialize in the store, wire inbridgeClientToStore. - Component: Render the new state in a React component.