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

TechnologyVersionRole
Vite6Build tool and development server with sub-second HMR
React19Component model and rendering
TypeScript5.xCompile-time type safety across the entire client surface
Tailwind CSSv4Utility-first styling with zero runtime CSS overhead
Effect^3.12Typed effects for the Gateway Adapter abstraction
Zustand5Surgical selector-based state management
react-markdown10Markdown rendering for assistant message content
remark-gfm4GitHub-flavored markdown: tables, task lists, strikethrough
shiki3Syntax highlighting with language detection for code blocks
lucide-react0.470Iconography

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. The WebGatewayAdapter wraps this SDK in Effect types so that the shared React layer remains transport-agnostic.
Browser
  +-- React Components (from @igentai/dim-shared)
  +-- Zustand Stores (from @igentai/dim-shared)
  +-- WebGatewayAdapter
  |     +-- DiminuendoClient (TypeScript SDK)
  |           +-- WebSocket --> Gateway
  +-- Vite dev server (development only)
The full data path, drawn without elision:
React Component --> useChat() hook --> chatStore.runTurn()
  --> WebGatewayAdapter.runTurn()
    --> DiminuendoClient.runTurn()
      --> WebSocket.send(JSON)
        --> Gateway (wire protocol)
And back:
Gateway --> WebSocket.onmessage
  --> DiminuendoClient event handlers
    --> WebGatewayAdapter.events (Effect Stream)
      --> useGatewayConnection() subscriber
        --> chatStore.appendTextDelta()
          --> React re-render (selector-scoped)

The Gateway Adapter Pattern

The GatewayAdapter 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.
export interface GatewayAdapter {
  connect(url: string, token?: string): Effect.Effect<void, GatewayError>
  disconnect(): Effect.Effect<void, never>
  readonly events: Stream.Stream<GatewayEvent, GatewayError>

  // Session management (8 methods)
  listSessions(includeArchived?: boolean): Effect.Effect<SessionMeta[], GatewayError>
  createSession(agentType: string, name?: string): Effect.Effect<SessionMeta, GatewayError>
  joinSession(sessionId: string): Effect.Effect<StateSnapshotEvent, GatewayError>
  leaveSession(sessionId: string): Effect.Effect<void, GatewayError>
  renameSession(sessionId: string, name: string): Effect.Effect<SessionMeta, GatewayError>
  archiveSession(sessionId: string): Effect.Effect<SessionMeta, GatewayError>
  unarchiveSession(sessionId: string): Effect.Effect<SessionMeta, GatewayError>
  deleteSession(sessionId: string): Effect.Effect<void, GatewayError>

  // Turn interaction (4 methods)
  runTurn(sessionId: string, text: string): Effect.Effect<void, GatewayError>
  stopTurn(sessionId: string): Effect.Effect<void, GatewayError>
  steer(sessionId: string, content: string): Effect.Effect<void, GatewayError>
  answerQuestion(
    sessionId: string,
    requestId: string,
    answers: Record<string, string>,
  ): Effect.Effect<void, GatewayError>
}
The 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.
connect(url: string, token?: string): Effect.Effect<void, GatewayError> {
  return Effect.tryPromise({
    try: () => {
      this.client = new DiminuendoClient({ url, token })
      return this.client.connect()
    },
    catch: (err) => new GatewayError(
      "CONNECTION_FAILED",
      err instanceof Error ? err.message : String(err),
    ),
  })
}
This is the Strategy pattern with dependency injection: the adapter is the strategy, the connection store is the context, and platform detection is the factory. At initialization, the connection store selects the correct adapter based on the runtime environment:
const adapter: GatewayAdapter = window.__TAURI__
  ? new TauriGatewayAdapter()
  : new WebGatewayAdapter()
From that point forward, no component, hook, or store ever references a concrete adapter. They interact exclusively through the 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:
1

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.
2

Hook Dispatch

The useChat() hook calls chatStore.runTurn(sessionId, text), which invokes adapter.runTurn() on the WebGatewayAdapter.
3

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.
4

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.
5

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.
6

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.
7

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.
8

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:
StoreFileResponsibility
Connectionstores/connection.tsGateway connection status, authenticated identity, adapter reference
Sessionsstores/sessions.tsSession list, active session selection, session metadata cache
Chatstores/chat.tsMessage history, streaming text accumulation, tool call tracking, thinking blocks
Preferencesstores/preferences.tsTheme, sidebar collapsed state, user preferences
Events flow from the gateway through the adapter and into these stores in a strict unidirectional pattern:
Gateway Event --> Adapter Stream --> Event Router --> Zustand Store --> React Re-render
The 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:
HookPurpose
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 via useAutoScroll() — 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-markdown with remark-gfm for tables, task lists, and strikethrough
  • Code blocks — syntax-highlighted using shiki with 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.
1

Gateway Event

The gateway sends a server event over WebSocket.
2

Adapter Stream

The GatewayAdapter.events stream emits the parsed event.
3

Event Router

The useGatewayConnection hook’s event subscription routes each event to the appropriate store based on its type field.
4

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.
5

React Re-render

Components subscribed to the affected store slice re-render with the new state. Zustand’s selector-based subscriptions ensure only affected components re-render — a text_delta event will not cause the session sidebar to reconcile.

Development

Start the web client development server:
cd clients
npm install
npm run dev:web
This starts Vite’s dev server on 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

cd clients
npm run build:web
Produces a static site in 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.
In production, the web client must connect to a gateway that enforces authentication. The ALLOWED_ORIGINS environment variable on the gateway must include the web client’s origin to pass CSRF checks. Omitting this configuration will cause the gateway to reject WebSocket upgrade requests from the deployed client.