OpenClaw Studio is a gateway-first, single-user Next.js App Router UI for managing OpenClaw agents. It provides:
- A focused UI with fleet list, primary agent panel, and inspect sidebar.
- Local persistence for gateway connection + focused-view preferences via a JSON settings file.
- A same-origin WebSocket bridge (
/api/gateway/ws) from browser to the upstream OpenClaw gateway. - Gateway-backed edits for agent config and agent files.
Primary goals:
- Gateway-first: agents, sessions, and config live in the gateway; Studio stores only UI settings.
- Remote-friendly: tailnet/remote gateways are first-class.
- Clear boundaries: client UI vs server routes vs external gateway/config.
- Predictable state: gateway is source of truth; local settings only for focused preferences + connection.
- Maintainability: feature-focused modules, minimal abstractions.
Non-goals:
- Multi-tenant or multi-user concurrency.
- Server-side rendering of data from external services.
Layered + vertical slice (feature-first) within Next.js App Router:
- UI components + feature state in
src/features. - Shared utilities and adapters in
src/lib. - Server-side route handlers under
src/app/api.
This keeps feature cohesion high while preserving a clear client/server boundary.
- Focused agent UI (
src/features/agents): focused agent panel, fleet sidebar, inspect panel, and local in-memory state + actions. The fleet sidebar keeps the same New Agent entry point, now implemented as a one-stepAgentCreateModalflow (name + avatar + launch). Creation is create-only:src/app/page.tsxcallsrunCreateAgentMutationLifecycleandcreateGatewayAgent, then reloads fleet state and focuses chat for the new agent; no guided setup compilation, deferred setup persistence, or pending retry UI remains in runtime flows. Agents render a status-first summary and latest-update preview driven by gateway events. Per-agent runtime controls (model,thinking) live in the chat header (AgentChatPanel), active runs can be stopped from the chat composer viachat.abort, and pending exec approvals render in-chat action cards (Allow once,Always allow,Deny) while fleet rows showNeeds approval. Settings sidebar actions remain focused on rename, display toggles, execution role updates (updateExecutionRoleViaStudio), new session, cron list/run/delete/create, and delete (AgentSettingsPanel). The Skills section inAgentSettingsPanelis split intoAccess(per-agent allowlist mode + toggles) andLibrary(gateway-wide setup actions in modal flow). Cron creation continues to use a guided modal scoped to the selected settings agent. Gateway event classification (presence/heartbeatsummary refresh andchat/agentruntime streams) is centralized in bridge helpers (src/features/agents/state/runtimeEventBridge.ts), while runtime flow decisions are emitted from pure policy helpers (src/features/agents/state/runtimeEventPolicy.ts) and executed bysrc/features/agents/state/gatewayRuntimeEventHandler.ts; both are consumed from one gateway subscription path insrc/app/page.tsx, where exec approval events are handled in parallel. Higher-level orchestration is factored into operations undersrc/features/agents/operations/(fleet hydration snapshots inagentFleetHydration.ts, pure fleet hydration derivation inagentFleetHydrationDerivation.ts, chat send inchatSendOperation.ts, cron create incronCreateOperation.ts, mutation lifecycle policy (create/rename/delete) inmutationLifecycleWorkflow.ts, latest-update policy inlatestUpdateWorkflow.ts, fleet summary/reconcile policy infleetLifecycleWorkflow.ts, reconcile operation adapter inagentReconcileOperation.ts, history request/disposition policy inhistoryLifecycleWorkflow.ts, history sync operation adapter inhistorySyncOperation.ts, approval lifecycle policy insrc/features/agents/approvals/execApprovalLifecycleWorkflow.ts, manual exec approval resolve operation insrc/features/agents/approvals/execApprovalResolveOperation.ts, and execution primitives inuseConfigMutationQueue.tsanduseGatewayRestartBlock.ts). Rename/delete post-run UI side effects are emitted as typed mutation commands frommutationLifecycleWorkflow.tsand executed insrc/app/page.tsx. Session setting mutations (model/thinking) are centralized insrc/features/agents/state/sessionSettingsMutations.tsso optimistic state updates and sync/error behavior stay aligned. Transcript ownership is split intentionally: optimistic send appends local user transcript entries while canonical timestamps and final ordering come fromchat.historysync via the history workflow boundary (historyLifecycleWorkflow.ts) and operation adapter (historySyncOperation.ts); replayed terminal chat events and late deltas from recently closed runs are ignored ingatewayRuntimeEventHandler, which requests recovery history only through therequestHistoryRefreshboundary command. Studio fetches a capped amount of chat history by default (currently 200 messages) and exposes a “Load more” affordance when the transcript may be truncated. Disconnected startup now uses a status-firstGatewayConnectScreenwith a local command copy affordance and a collapsible remote form. - Studio settings (
src/lib/studio,src/app/api/studio): local settings store for gateway URL/token and focused preferences (src/lib/studio/settings.ts,src/lib/studio/settings-store.ts,src/app/api/studio/route.ts).src/lib/studio/coordinator.tsnow owns both the/api/studiotransport helpers and shared client-side load/patch scheduling for gateway and focused settings. - Gateway (
src/lib/gateway): WebSocket client for agent runtime (frames, connect, request/response). Session settings sync transport (sessions.patch) is centralized insrc/lib/gateway/GatewayClient.ts. Connect failures surfaced through the Studio WS proxy are preserved asGatewayResponseErrorcodes (parsed fromconnect failed: <CODE> ...) souseGatewayConnectioncan gate auto-retry viaresolveGatewayAutoRetryDelayMs. The OpenClaw control UI client is vendored insrc/lib/gateway/openclaw/GatewayBrowserClient.tswith a sync script atscripts/sync-openclaw-gateway-client.ts. - Studio gateway proxy server (
server/index.js,server/gateway-proxy.js,server/studio-settings.js): custom Next server that terminates browser WS at/api/gateway/ws, loads upstream gateway URL/token server-side, injects auth token when needed, and forwards frames to the upstream gateway. - Gateway SSH helpers (
src/lib/ssh/gateway-host.ts): shared SSH target resolution and JSON-over-SSH execution for server routes.runSshJsoncentralizesssh -o BatchMode=yesinvocation, JSON parsing, and actionable error extraction; callers with large payloads (for example base64 media reads) can opt into a highermaxBufferrather than duplicatingspawnSynccalls. - Gateway-backed config + agent-file edits (
src/lib/gateway/agentConfig.ts,src/lib/gateway/agentFiles.ts,src/lib/gateway/execApprovals.ts,src/features/agents/components/AgentInspectPanels.tsx): agent create/rename/heartbeat/delete and per-agent overrides viaconfig.get+config.patch, agent file read/write viaagents.files.getandagents.files.set, and per-agent exec approvals viaexec.approvals.get+exec.approvals.set. - Heartbeat helpers (
src/lib/gateway/agentConfig.ts): resolves per-agent heartbeat state (enabled + schedule) by combining gateway config (config.get) and status (status) for the settings panel, triggerswakefor “run now”, and owns the heartbeat type shapes and gateway config mutation helpers. - Session lifecycle actions (
src/features/agents/state/store.tsx,src/app/page.tsx): per-agent “New session” calls gatewaysessions.reseton the current session key and resets local runtime transcript state. - Local OpenClaw config + paths (
src/lib/clawdbot): state/config path resolution withOPENCLAW_*env overrides (src/lib/clawdbot/paths.ts). Gateway URL/token in Studio are sourced from studio settings. - Shared agent config-list helpers (
src/lib/gateway/agentConfig.ts): pureagents.listread/write/upsert helpers used by gateway config patching to keep list-shape semantics aligned. - Shared utilities (
src/lib/*): env, ids, names, avatars, message parsing/normalization (including tool-line formatting) insrc/lib/text/message-extract.ts, cron types + selector helpers + gateway call helpers insrc/lib/cron/types.ts, logging, filesystem helpers.
src/app: Next.js App Router pages, layouts, global styles, and API routes.src/features: feature-first UI modules (currently focused agent-management components underfeatures/agents).src/lib: domain utilities, adapters, API clients, and shared logic.src/components: shared UI components (minimal use today).src/styles: shared styling assets.server: custom Node server and WS proxy for gateway bridging + access gate.public: static assets.tests,playwright.config.ts,vitest.config.ts: automated testing.
- Source of truth: JSON settings file at
~/.openclaw/openclaw-studio/settings.json(resolved viaresolveStateDir, with legacy fallbacks insrc/lib/clawdbot/paths.ts). Settings store the gateway URL/token plus per-gateway focused preferences. - Server boundary:
src/app/api/studio/route.tsloads/saves settings by reading and writingopenclaw-studio/settings.jsonunder the resolved state dir. - Client boundary:
useGatewayConnectionand focused/session flows insrc/app/page.tsxuse a sharedStudioSettingsCoordinatorto load settings and coalesce debounced/api/studiopatch writes.
Flow:
- UI loads settings from
/api/studio. - Gateway URL/token seed the connection panel and auto-connect.
- Focused filter + selected agent are loaded for the current gateway.
- UI schedules focused and gateway patches through the coordinator; both paths converge on
/api/studio.
- Client-side boundary:
GatewayClientconnects to Studio-origin/api/gateway/wsviaresolveStudioProxyGatewayUrl()and wraps the vendoredGatewayBrowserClient. - Server-side boundary: custom server proxy (
server/gateway-proxy.js) is in the middle for upstream URL/token resolution and connect-frame token injection.
Flow:
- UI loads gateway URL/token from
/api/studio(defaulting toNEXT_PUBLIC_GATEWAY_URL, orws://localhost:18789when that env var is unset). - Browser opens WS to Studio
/api/gateway/ws(ws://onhttp,wss://onhttps). - Proxy loads upstream URL/token from Studio settings on the server and opens upstream WS.
- Proxy forwards
connectand subsequent frames; it injects auth token server-side if the connect frame has none. - If upstream connect fails, the proxy sends an error response for the
connectrequest (with astudio.*error code when possible) and closes the Studio-origin WS. The browser-side gateway client converts a failedconnectresponse into a WS close with code4008and a reason likeconnect failed: <CODE> ...;GatewayClient.connect()parses this intoGatewayResponseError, anduseGatewayConnectiondecides whether/when to auto-retry based onconnectErrorCode(throughresolveGatewayAutoRetryDelayMs). - UI requests
agents.listand builds session keys viabuildAgentMainSessionKey(agentId, mainKey). - A single gateway listener in
src/app/page.tsxclassifiespresence/heartbeat/chat/agentevents throughclassifyGatewayEventKindinsrc/features/agents/state/runtimeEventBridge.ts, then routes runtime payloads throughsrc/features/agents/state/runtimeEventPolicy.tsand executes intents viasrc/features/agents/state/gatewayRuntimeEventHandler.ts; it also independently tracksexec.approval.requested/exec.approval.resolvedfor in-chat approval cards. - Agent store updates agent output/state.
- Pending approval queues are pruned by expiry timestamp (with a short grace window), so stale cards and stale
awaitingUserInputbadges self-clear even when no resolved event arrives.
- Agent files:
AGENTS.md,SOUL.md,IDENTITY.md,USER.md,TOOLS.md,HEARTBEAT.md,MEMORY.md. - Create + heartbeat + rename + per-agent overrides: stored in gateway config and updated via
config.get+config.patch. - Exec approvals policy: managed after creation through settings/runtime approval flows.
Flow:
AgentCreateModalcaptures only create payload (name, optionalavatarSeed) in a one-step launch flow (src/features/agents/components/AgentCreateModal.tsx).runCreateAgentMutationLifecycleenqueues one create mutation, applies queue guardrails, and drives create block status (src/features/agents/operations/mutationLifecycleWorkflow.ts).createGatewayAgentcallsconfig.getto derive workspace, then callsagents.createwith{ name, workspace }(src/lib/gateway/agentConfig.ts).- Studio reloads fleet state and focuses the created agent chat in
src/app/page.tsx. - Any authority/runtime adjustments happen after creation through settings operations such as
updateExecutionRoleViaStudio(src/features/agents/operations/agentPermissionsOperation.ts).
- Cron: the UI calls gateway cron methods directly (
cron.list,cron.add,cron.run,cron.remove) for latest-update previews and agent settings controls. - Create flow:
AgentSettingsPanelcollects aCronCreateDraftin a modal wizard,buildCronJobCreateInputmaps it to a gateway-safe payload (src/lib/cron/createPayloadBuilder.ts), andperformCronCreateFlowexecutes create + scoped refresh (src/features/agents/operations/cronCreateOperation.ts).
- UI boundary:
AgentChatPanelemits model/thinking callbacks from the agent header;src/app/page.tsxdelegates both through one mutation helper. - Mutation boundary:
applySessionSettingMutationinsrc/features/agents/state/sessionSettingsMutations.tsowns optimistic store updates,sessionCreatedguard logic, sync success updates, and user-facing failure lines. - Transport boundary:
syncGatewaySessionSettingsinsrc/lib/gateway/GatewayClient.tsis the only client-side builder/invoker forsessions.patchpayloads.
- Configuration: environment variables are read directly from
process.env. The browser usesNEXT_PUBLIC_GATEWAY_URLonly as a default upstream URL when Studio settings are missing; the Studio server persists upstream URL/token in<state dir>/openclaw-studio/settings.jsonand the WS proxy loads them viaserver/studio-settings.js. State/config path resolution lives inlib/clawdbot/paths.ts, honoringOPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATHwith legacy fallbacks. When Studio token is missing, settings loaders can fall back to token/port from<state dir>/openclaw.json. Loopback-IP gateway URLs are normalized tolocalhostin Studio settings, and the WS proxy rewrites loopback upstream origins tolocalhostfor control-UI secure-context compatibility. The optional Studio access gate is enabled bySTUDIO_ACCESS_TOKEN(server/access-gate.js). - Testing: Playwright e2e runs Studio with an isolated
OPENCLAW_STATE_DIRso the Studio WS proxy does not read real upstream gateway settings from the developer machine. - Logging: API routes and the gateway client use built-in
console.*logging. - Error handling:
- API routes return JSON
{ error }with appropriate status. fetchJsonthrows when!res.ok, surfaces errors to UI state.StudioSettingsCoordinatorlogs failed async persistence writes (debounced flush or queued patch failures) so settings-save errors are observable.- Gateway connect failures with
INVALID_REQUEST: invalid configsurface a doctor hint in Studio (npx openclaw doctor --fix/pnpm openclaw doctor --fix). - Gateway connect failures that close with
connect failed: <CODE> ...are preserved asGatewayResponseErrorcodes so auto-retry gating can be code-driven (instead of message-driven). - Gateway browser client truncates close reasons to WebSocket protocol limits (123 UTF-8 bytes) to avoid client-side close exceptions on long error messages.
- API routes return JSON
- Filesystem helpers: server-only filesystem operations live at the API route boundaries. Home-scoped path autocomplete is implemented directly in
src/app/api/path-suggestions/route.ts. These helpers are used for local settings and path suggestions, not for agent file edits. - Remote gateway tools over SSH: some server routes execute small scripts on the gateway host (for example agent-state operations and remote media reads). Shared helpers in
src/lib/ssh/gateway-host.tsown SSH invocation and JSON parsing so routes do not hand-rollspawnSyncerror handling. - Tracing:
src/instrumentation.tsregisters@vercel/otelfor telemetry. - Validation: request payload validation in API routes and typed client/server helpers in
src/lib/*.
- Local settings file over DB: fast, local-first persistence for gateway connection + focused preferences; trade-off is no concurrency or multi-user support.
- Same-origin WS proxy instead of direct browser->gateway WS: allows server-side token custody/injection and easier local/remote switching; trade-off is one extra hop and custom-server ownership.
- Gateway-first agent records: records map 1:1 to
agents.listentries with main sessions; trade-off is no local-only agent concept. - Gateway-backed config + agent-file edits: create/rename/heartbeat/delete and per-agent overrides via
config.patch, agent files viaagents.files.get/agents.files.set, and per-agent exec approvals viaexec.approvals.set; trade-off is reliance on gateway availability. - Fleet hydration snapshot/derive split:
hydrateAgentFleetFromGatewayloads gateway/settings snapshots (I/O) and delegates all derived decisions (seeds, exec policy resolution, summary selection) to a pure derivation helper; trade-off is one extra module and a more explicit snapshot input, but the derivation becomes independently testable. - Extract page-level workflows into operations: keep
src/app/page.tsxas wiring by moving workflow policy into operation modules (for example reconcile insrc/features/agents/operations/agentReconcileOperation.tsand manual exec approval resolve insrc/features/agents/approvals/execApprovalResolveOperation.ts); trade-off is more modules, but the workflows become independently unit testable without React rendering. - Structured connect errors + retry policy helper:
GatewayClient.connect()preserves gateway connect-failure codes anduseGatewayConnectiongates auto-retry viaresolveGatewayAutoRetryDelayMs; trade-off is one more helper plus extra state (connectErrorCode), but the behavior is less brittle than string matching. - Narrow local config mutation boundary: Studio does not write
openclaw.jsondirectly today; if a local-only integration is introduced, keep any local writes narrowly scoped to that integration and reuse shared list helpers instead of ad-hoc mutation paths; trade-off is less flexibility for local-only experimentation, but clearer ownership and lower drift risk. - Shared
agents.listhelper layer: gateway and local config paths now consume one pure helper module for list parsing/writing/upsert behavior; trade-off is one more shared dependency, but it reduces semantic drift and duplicate bug surface. - Single gateway settings endpoint:
/api/studiois the sole Studio gateway URL/token source; trade-off is migration pressure on any older local-config-based callers, but it removes ambiguous ownership and dead paths. - Shared client settings coordinator module:
src/lib/studio/coordinator.tsnow owns/api/studiotransport plus load/schedule/flush behavior for gateway + focused state; trade-off is introducing a central client singleton, but it removes wrapper indirection and duplicate timers/fetch paths. - Single shared JSON-over-SSH helper: server routes that need to run a gateway-side script over SSH should use
runSshJsoninsrc/lib/ssh/gateway-host.ts(and opt into a largermaxBufferwhen expecting large payloads) rather than duplicatingspawnSync+ JSON parsing; trade-off is one shared dependency, but it reduces drift risk and keeps error surfacing consistent. - Vendored gateway client + sync script: reduces drift from upstream OpenClaw UI; trade-off is maintaining a sync path and local copies of upstream helpers.
- Feature-first organization: increases cohesion in UI; trade-off is more discipline to keep shared logic in
lib. - Node runtime for API routes: required for filesystem access and tool proxying; trade-off is Node-only server runtime.
- Event-driven summaries + on-demand history: keeps the dashboard lightweight; trade-off is history not being available until requested.
- Runtime policy/executor split for event handling: one listener path in
src/app/page.tsxclassifies frames throughsrc/features/agents/state/runtimeEventBridge.ts, derives side-effect-free decisions insrc/features/agents/state/runtimeEventPolicy.ts, and executes those intents insrc/features/agents/state/gatewayRuntimeEventHandler.ts; trade-off is additional intent-shape maintenance, but lower coupling between lifecycle policy and side effects. - Single gateway event intake subscription: one
client.onEventpath now handles both summary-refresh events (presence/heartbeat) and runtime stream events (chat/agent) using bridge classification helpers; trade-off is a larger callback surface, but fewer lifecycle and cleanup divergence points. - Shared session-setting mutation path: model and thinking-level updates now pass through one UI mutation helper plus one gateway sync helper (
src/features/agents/state/sessionSettingsMutations.ts+src/lib/gateway/GatewayClient.ts), reducing divergence between optimistic state and remote patch flows.
C4Context
title OpenClaw Studio - System Context
Person(user, "User", "Operates agents locally")
System(ui, "OpenClaw Studio", "Next.js App Router UI")
System(proxy, "Studio WS Proxy", "Custom server /api/gateway/ws")
System_Ext(gateway, "OpenClaw Gateway", "WebSocket runtime")
System_Ext(fs, "Local Filesystem", "settings.json and other local reads (e.g. path suggestions)")
Rel(user, ui, "Uses")
Rel(ui, proxy, "WebSocket frames")
Rel(proxy, gateway, "WebSocket frames")
Rel(ui, fs, "HTTP to API routes -> fs read/write")
C4Container
title OpenClaw Studio - Containers
Person(user, "User")
Container_Boundary(app, "Next.js App") {
Container(client, "Client UI", "React", "Focused agent-management UI, state, gateway client")
Container(api, "API Routes", "Next.js route handlers", "Studio settings, path suggestions, gateway-host state tools")
Container(proxy, "WS Proxy", "Custom Node server", "Bridges /api/gateway/ws to upstream gateway with token injection")
}
Container_Ext(gateway, "Gateway", "WebSocket", "Agent runtime")
Container_Ext(fs, "Filesystem", "Local", "settings.json and other local reads (e.g. path suggestions)")
Rel(user, client, "Uses")
Rel(client, api, "HTTP JSON")
Rel(client, proxy, "WebSocket /api/gateway/ws")
Rel(proxy, gateway, "WebSocket")
Rel(api, fs, "Read/Write")
Rel(proxy, fs, "Read settings/token")
- Do not read/write local files directly from client components.
- Do not reintroduce local projects/workspaces as a source of truth for agent records.
- Do not write agent rename/heartbeat/override data directly to
openclaw.json; use gatewayconfig.patch. - Do not read/write agent files on the local filesystem; use the gateway tools proxy.
- Do not add parallel gateway settings endpoints;
/api/studiois the only supported Studio gateway URL/token path. - Do not add new generic local
openclaw.jsonmutation wrappers for runtime agent-management flows; if a local-only integration is introduced, keep any local writes narrowly scoped and well tested. - Do not store gateway tokens or secrets in client-side persistent storage.
- Do not add new global mutable state outside
AgentStoreProviderfor agent UI data. - Do not silently swallow errors in API routes; always return actionable errors.
- Do not add heavy abstractions or frameworks unless there is clear evidence of need.
- If multi-user support becomes a goal, replace the settings file with a DB-backed service and introduce authentication at the API boundary.
- If gateway protocol evolves, isolate changes within
src/lib/gatewayand keep UI call sites stable.