openclaw/docs/tools/theme.md
Peter Steinberger bf3a182adb
fix: let people with the same permissions steer each other's turns (#159850)
* fix: let operators with equal permissions steer each other's turns

In shared Control UI sessions, a message sent while another person's turn
was running never steered: the Gateway rejected it as a tool-authority
mismatch and silently queued it as a follow-up. The steering fingerprint
hashed operator identity (profile id, plus the own-browser screen target
and own-profile theme target), so two different people could never match
even with identical roles.

The fingerprint now hashes only permissions. Own-browser screen and theme
targets become a profile-free marker; scopes, model policy, access grant
(including its identity fallback when unclassified), tool policy,
permission mode, caps, and bindings stay exact. Different permissions still
queue as a follow-up. Steering never transfers authority: the running turn
keeps its owner's authority, browser/screen/theme target, tool bindings,
and approval destination.

Release note: people with the same permissions can now steer each other's
active turns in shared sessions instead of having messages queued.

* fix(agents): disambiguate personal tools in steered turns

Require an explicit sender-id user selection for screen and theme after accepted cross-profile input. Keep participants with the existing turn authority owner, revalidate their authority at Gateway effects, and release retained authority when the turn ends. Preserve steering admission and single-owner browser bindings.

* fix(agents): preserve participants across steering and runtime calls

Register participants after accepted inbound input, including automatic-fallback and route-only pending-question acceptance. Carry host participant facts without changing admission. Resolve identity-only personal-tool calls through the exact live run registration, and make participant resolution safe to call unbound.

* fix: keep participant input type module-local

* fix: expose verified Control UI requester profiles

Bind live authenticated requester profiles to complete chat.send contexts without adding SenderId. Ordinary and steered user-role prompts expose requester_profile.id, which screen/theme descriptions and ambiguity guidance identify as the user target. Control UI participants already select the authenticated profile ID; authority, fingerprint, and participant lifecycle remain unchanged.

New metadata follows existing per-input context persistence: visible transcript text stays literal, while append-only backends may retain new context without rewriting history or stable prompts. Updated steering and personal-tool documentation.

Regression proof: original ingress producer fails the profile and ordinary model-context assertions; the fixed Gateway flow exposes A and B separately, rejects unnamed screen calls listing both profile IDs, and routes the rendered B ID only to B. Final focused files: custody 11/11 (126.94s wall, cross-profile case 2.215s); user-turn 25/25 (41.42s wall); identity 23/23 (38.96s wall); inbound metadata 67/67 (54.18s wall). Core and all 25 selected test type graphs, targeted lint, oxfmt, and diff whitespace checks pass. Tests use existing fixtures without new Gateway boots, timers, or polling.

* fix(agents): compare role permissions when admitting steering

Prepare the role session access cap, sandbox requirement, and agent allowlist with operator authority. Queue messages with different role permissions as followups while preserving steering for equivalent roles and roles-disabled gateways.

Gateway regression: 15 passed; authority unit tests: 38 passed. Restoring all three product files to 7eddf5f484 makes the three mismatched-role cases fail because their input incorrectly steers. Core and core-test typechecks, targeted formatting/lint, and independent review passed.

* test: align steering and heartbeat fixtures with current contracts

Accept equal-permission steering from another operator profile in the queued
turn regression, retaining rejection for missing authority, changed scopes,
and disabled tools.

Carry main's fixture fix from 6d1826f1ad: wait for database-claim release
before creating the replacement heartbeat run. Preserve the existing outcome
assertion without adding timers, retries, or production changes.

Validation: queued steering 4/4 (114.47s command, 0.894s test execution),
heartbeat 40/40 (73.82s command, 69.89s Vitest), and 216 additional steering
and authority tests across 11 files. Core typecheck, targeted lint, formatting,
and git diff --check pass.

CI 36397829641 also reported two inherited Gateway failures. Completion
exhaustion remains pending/retryable on both the branch and main 7510c4bd52;
fixture-order and registry-identity diagnostics did not establish a safe fix.
A later diagnostic pass is not evidence of repair. Managed-worktree skill
refresh misses the first edited skill event on both heads (expected 2 to be 3).
Initial concurrent local runs timed out; isolated runs reproduced the CI
assertion unchanged. Leave both unrelated owners unchanged after bounded
investigation.

Full-candidate independent review against the merge base is scoped-clean at
P0/P1, with no accepted or actionable findings.

* fix: compare channel role permissions and canonical profile ids in steering

Linked-channel operator authority now carries the same prepared role permissions (session access cap, sandbox requirement, agent allowlist) as Control UI ingress through one shared projection, so channel operators with different role limits no longer match. Personal-tool participant selection and its error choices use the canonical profile id that the agent sees as requester_profile.id instead of the transport sender id.

* fix(gateway): reject ambiguous personal effects after steering

Resolve browser participants at UI dispatch and reject ambiguous Crabbox presentation before reservation. Preserve cancellation if a steer makes presentation ambiguous after reservation. Reject personal instructions and preference reads or writes from mixed-person turns, including at asynchronous effect boundaries.

Regression tests fail on 8ffd8dd and pass with this change. Focused suites pass (176 tests); core typecheck, formatting, targeted lint, and full-candidate review pass. Changed test file wall times: environments.session 46.21s, users-personal-file 13.82s, users-preferences 27.92s.

* test: type the users.prefs synthetic-call wrapper against typed handlers
2026-09-28 13:52:08 +00:00

8.5 KiB
Raw Blame History

summary title sidebarTitle read_when
Let an agent select plugin themes or create a personal OpenClaw theme Theme Theme
You want an agent to change your OpenClaw theme
You want to create a custom theme and apply it in one call
You need the theme catalog and profile selection contract

The theme tool lets an agent list, inspect, select, and create OpenClaw appearance themes. Settings and the agent use the same catalog of built-in, plugin, and personal themes. Theme descriptions explain their palette, typography, and character so the agent can choose a theme from a request such as "make this look like an alien spacecraft."

The tool is available in the coding and messaging profiles and group:ui. It does not require a connected browser. Personal changes require a trusted participant profile.

Select a theme

Ask the agent to list available themes or choose one for you. list includes the current selection, so selecting a theme usually takes two calls:

{ "action": "list" }
{ "action": "set", "id": "space-pack/xenovessel", "mode": "dark" }

Use an ID returned by list. Plugin IDs are qualified as <pluginId>/<themeId>; personal themes use user/<slug>.

Actions

Action Inputs Result
list None Available themes, descriptions, supported modes, sources, and the current selection.
get Optional id Current selection and the requested theme, including its editable definition when available. Without id, inspects the current theme.
set id and/or mode Saves profile overrides and returns the resulting selection.
import id, definition; optional apply, mode Saves a personal theme. apply: true selects it in the same call.

Every action accepts optional user, the person's verified requester_profile.id from the Control UI message's conversation context. When several people have steered the turn, user is required; the agent chooses the person who asked or asks them if it is unclear. Only the turn's owner or an accepted participant can be selected. Reads, including the current selection in list and get, use that person's profile; changes save only to that profile. If their access has changed, they must ask again.

mode is system, light, or dark. set accepts null for either id or mode to clear that profile override and inherit the Gateway setting. Setting only one field preserves the other override when it is compatible. Selecting or applying a single-mode theme also selects its supported mode if the previous explicit mode cannot render it. An explicitly requested incompatible mode is rejected; system follows the available palette:

{ "action": "set", "id": null, "mode": null }

set and import return application: "saved" after persistence succeeds. The response already includes the resulting state; an extra get is not necessary. Saving does not assert that a particular browser has rendered the theme.

The returned current.mode is the saved preference. current.effectiveMode reports the rendered variant when it can be determined without a browser; with two palettes and system mode, the device determines it. A plugin reload can change available variants without rewriting anyone's saved preferences.

Create and apply a personal theme

import accepts a lowercase slug of up to 64 characters using letters, numbers, hyphens, and underscores. Reimporting the same slug updates that personal theme. apply defaults to false.

A definition requires a name, a short description, and at least one complete light or dark palette. Each palette uses the semantic colors shown below and may include font-sans and font-mono. Use CSS color values such as hex, rgb(), hsl(), or oklch(). Font families describe locally available fonts; definitions cannot load external stylesheets or resources.

Definitions can also supply these optional presentation fields, shared by built-in, plugin, and personal themes:

  • mascot: "claw" (the default) or "none". "none" replaces lobster branding with a neutral prompt mark and hides the resident lobster and visiting lobster strangers. Ordinary critters can still cross the composer ledge when Lobster visits is enabled; the theme does not change that toggle.
  • workingPhrases: up to 24 literal status phrases, each trimmed to 1–24 characters with no control characters or duplicates after trimming. These authored strings are not translated. Omit the field to use the default whimsical vocabulary, or set it to [] to hide long-wait phrases.
  • critters: up to 8 unique IDs from the built-in "penguin" and "fedora" catalog. These add occasional visitors to ordinary composer ledge traffic while Lobster visits is enabled. Omit the field or use [] to add none; unknown IDs and duplicates are rejected.
  • avatarHat: "fedora", "crown", "santa", "party", or "pumpkin" adds an occasional decorative hat to agent avatars. Omit the field for no theme-supplied avatar hat.

Use consistent CSS separators: rgb(20 30 40 / 50%) or rgba(20, 30, 40, 0.5). Modern functions such as oklch() use spaces between components and / before opacity. Font lists use comma-separated family names; quote names containing punctuation or beginning with a digit, such as "123 Font", monospace. Also quote names containing CSS keywords, such as "Foo serif". Malformed colors and unbalanced font quotes are rejected before the theme is saved.

This example creates and activates a dark theme in one call:

{
  "action": "import",
  "id": "xenovessel",
  "apply": true,
  "mode": "dark",
  "definition": {
    "name": "Xenovessel",
    "description": "Indigo spacecraft surfaces, lime controls, cyan highlights, and monospace typography.",
    "mascot": "none",
    "workingPhrases": ["Navigating", "Calibrating", "Scanning"],
    "critters": ["penguin", "fedora"],
    "avatarHat": "fedora",
    "dark": {
      "background": "#090818",
      "foreground": "#e8f2ff",
      "card": "#12112b",
      "card-foreground": "#e8f2ff",
      "popover": "#171533",
      "popover-foreground": "#e8f2ff",
      "primary": "#c7ff3d",
      "primary-foreground": "#172300",
      "secondary": "#28234a",
      "secondary-foreground": "#e8f2ff",
      "muted": "#211e39",
      "muted-foreground": "#aca6cc",
      "accent": "#4ce9ef",
      "accent-foreground": "#042b30",
      "destructive": "#ff698b",
      "destructive-foreground": "#290711",
      "border": "#40385e",
      "input": "#40385e",
      "ring": "#c7ff3d",
      "font-sans": "ui-monospace, monospace",
      "font-mono": "ui-monospace, monospace"
    }
  }
}

Names are limited to 80 characters, descriptions to 320 characters, and the normalized definition to 4096 UTF-8 bytes. The Gateway validates definitions before saving them. A personal theme does not require installing a plugin or publishing the definition elsewhere.

Plugin themes and hot reload

Plugins contribute theme definitions declaratively through their manifest. Personal themes use only the built-in hat and critter catalog IDs; plugin themes may also reference their own SVG artwork IDs declared in the plugin manifest. Definitions never contain artwork markup or external URLs. The shared catalog updates when the plugin is enabled, disabled, or reloaded; a Gateway restart is not required. The agent continues using the same theme tool rather than receiving a new tool for each plugin.

If a selected plugin theme becomes unavailable, the result includes current.requestedId while current.id identifies the fallback that can be rendered. Re-enable the plugin or choose another theme. Listing the catalog does not execute plugin theme code.