Web Client
The Diminuendo web client (@igentai/dim-web) is a single-page application that connects the browser directly to the gateway over a raw WebSocket — no backend-for-frontend, no REST intermediary, no server-side rendering layer standing between the human and the wire protocol. The browser opens a socket, authenticates, and begins receiving the same 51-event stream that every SDK consumes. The entire frontend is a projection of that stream onto React components, mediated by Zustand stores and a single architectural abstraction — the Gateway Adapter — that makes the web and desktop clients structurally identical despite their radically different transport mechanisms.
Tech Stack
| Technology | Version | Role |
|---|---|---|
| Vite | 6 | Build tool and development server with sub-second HMR |
| React | 19 | Component model and rendering |
| TypeScript | 5.x | Compile-time type safety across the entire client surface |
| Tailwind CSS | v4 | Utility-first styling with zero runtime CSS overhead |
| Effect | ^3.12 | Typed effects for the Gateway Adapter abstraction |
| Zustand | 5 | Surgical selector-based state management |
| react-markdown | 10 | Markdown rendering for assistant message content |
| remark-gfm | 4 | GitHub-flavored markdown: tables, task lists, strikethrough |
| shiki | 3 | Syntax highlighting with language detection for code blocks |
| lucide-react | 0.470 | Iconography |
Architecture
The web client employs no intermediate server. The browser holds the WebSocket connection directly, and the TypeScript SDK manages the protocol lifecycle — authentication handshake, ping keepalive, request-response correlation, and auto-reconnect. TheWebGatewayAdapter wraps this SDK in Effect types so that the shared React layer remains transport-agnostic.
The Gateway Adapter Pattern
TheGatewayAdapter interface is the load-bearing abstraction that permits the web and desktop clients to share every component, hook, and store without a single platform conditional. It defines all gateway operations using Effect types: commands return Effect.Effect<T, GatewayError>, and the event stream is a Stream.Stream<GatewayEvent, GatewayError>. The rest of the application consumes this interface without knowledge of which transport is active underneath.
WebGatewayAdapter is a thin wrapper over the TypeScript SDK. Promise-based SDK methods are lifted into Effect via Effect.tryPromise, and events are delivered as an Effect Stream via Stream.async. The adapter adds no logic of its own — the SDK already handles connection management, authentication, reconnection, and request-response correlation. The adapter merely translates the SDK’s Promise idiom into the Effect idiom that the shared stores expect.
GatewayAdapter interface via hooks that delegate to the store. Swapping transports requires no changes to any UI component or business logic.
Data Flow
The complete path of a user interaction through the web client, from keystroke to re-render:User Action
The user types a message in the chat input and presses Enter. The form’s
onSubmit handler calls sendMessage(inputText) from the useChat() hook.Hook Dispatch
The
useChat() hook calls chatStore.runTurn(sessionId, text), which invokes adapter.runTurn() on the WebGatewayAdapter.SDK Transmission
The adapter delegates to
DiminuendoClient.runTurn(), which serializes the message as JSON and sends it over the WebSocket. This is a fire-and-forget operation — the response arrives as a stream of events.Gateway Processing
The gateway receives the message, validates it against the protocol schema, routes it through the
MessageRouter, and forwards it to Podium for agent orchestration.Agent Execution
Podium dispatches the message to the coding agent. The agent begins processing and streams events back through Podium to the gateway:
turn_started, text_delta, tool_call, tool_result, turn_complete, and the rest of the 51-event vocabulary.Event Streaming
The gateway maps Podium events to the wire protocol, assigns session-scoped sequence numbers and timestamps, and publishes each event to the session topic. The client’s WebSocket receives each event as it arrives — no buffering, no batching.
Store Update
The
DiminuendoClient’s event handlers fire. The WebGatewayAdapter forwards each event through the events stream. The useGatewayConnection hook routes each event to the appropriate Zustand store based on its type field: text_delta events go to the chat store, session_state events go to the session store, usage_update events update the usage indicators.React Re-render
Zustand’s selector-based subscriptions trigger re-renders only in the affected components. The assistant message component re-renders to show the new text delta. The tool call component appears when a
tool_call event arrives. The status indicator updates on session_state changes. Components that are not subscribed to the changed slice remain untouched — no reconciliation, no wasted cycles.Shared Package: @igentai/dim-shared
The@igentai/dim-shared package is the foundation upon which both clients are built. It contains everything that is platform-agnostic — which, by design, is everything except the adapter implementation itself.
Zustand Stores
Four Zustand stores manage the entirety of application state in a unidirectional flow:| Store | File | Responsibility |
|---|---|---|
| Connection | stores/connection.ts | Gateway connection status, authenticated identity, adapter reference |
| Sessions | stores/sessions.ts | Session list, active session selection, session metadata cache |
| Chat | stores/chat.ts | Message history, streaming text accumulation, tool call tracking, thinking blocks |
| Preferences | stores/preferences.ts | Theme, sidebar collapsed state, user preferences |
useGatewayConnection hook’s event subscription routes each event to the correct store based on its type field. The targeted store updates its state — the chat store appends a text_delta to the current message’s streaming buffer, the session store updates metadata on session_updated, and so on. Components subscribed to that store slice re-render; all others remain inert.
React Hooks
Five hooks provide the interface between components and the machinery beneath:| Hook | Purpose |
|---|---|
useGatewayConnection() | Manages the adapter lifecycle, connects and disconnects, feeds events into stores |
useSession() | Provides the active session, join/leave logic, and session-scoped event handling |
useChat() | Provides chat messages, streaming state, and turn interaction methods |
useAutoScroll() | Handles automatic scroll-to-bottom during streaming while preserving manual scroll position |
useTheme() | Manages light/dark theme detection and persistence |
UI Components
Over 70 shared components, styled with Tailwind CSS v4 and built on shadcn/ui primitives, are consumed by both clients without modification. The web client imports them directly from@igentai/dim-shared and renders them without platform-specific overrides.
Key Components
Chat Container
The top-level chat layout that composes the message list, input area, and session controls. Manages scroll behavior viauseAutoScroll() — new content triggers an automatic scroll-to-bottom during streaming responses, but the scroll position is preserved when the user scrolls up to review history. This is a deceptively subtle interaction: the component must distinguish between “the user scrolled up intentionally” and “the container grew because new content arrived.”
Message List
Renders the conversation as a sequence of user and assistant messages. Each assistant message can contain:- Text blocks — rendered as Markdown using
react-markdownwithremark-gfmfor tables, task lists, and strikethrough - Code blocks — syntax-highlighted using
shikiwith language detection - Tool call blocks — show the tool name, arguments (collapsible JSON), and result
- Thinking blocks — collapsible sections showing the agent’s reasoning process
- Terminal output — monospaced blocks for command execution output
Session Sidebar
Lists all sessions for the authenticated tenant. Supports creating new sessions, renaming, archiving/unarchiving, and deleting. Sessions are grouped by status (active, archived) and sorted by last activity. The sidebar state (collapsed/expanded) is persisted in the preferences store.State Management Architecture
The state management architecture follows the principle that state should flow in one direction and that stores should be the single source of truth for their respective domains. No component holds local state that duplicates or shadows store state.Event Router
The
useGatewayConnection hook’s event subscription routes each event to the appropriate store based on its type field.Store Update
The targeted Zustand store updates its state immutably. For example, the chat store appends a
text_delta to the current message’s streaming buffer, or the session store merges an updated SessionMeta into its cache.Development
Start the web client development server:http://localhost:5173 with hot module replacement. The dev server proxies WebSocket connections to the gateway (default: ws://localhost:8080/ws).
The web client expects a running Diminuendo gateway instance. Start the gateway with
DEV_MODE=true to bypass authentication and enable auto-authentication with a dev identity.Build for Production
clients/web/dist/ suitable for deployment to any static hosting service — Vercel, Netlify, S3 + CloudFront, or any server that can serve HTML, CSS, and JavaScript.