Cherry-pick the still-relevant fixes from recent draft PRs without pulling in stale release/docs changes:
- serialize dashboard data loads to avoid concurrent config self-heal writes
- preserve valid per-model default blocks during dashboard model self-heal
- pass structured humanizeError results directly to toast for model import scan failures
- align frontend kernel isLatest with suffix-aware recommended version ordering
Verification:
- node --test tests/*.test.js
- npm run build
OpenClaw 2026.5.18/2026.5.19 still uses Gateway WS protocol v4
(PROTOCOL_VERSION=4, MIN_CLIENT_PROTOCOL_VERSION=4), so there is no v5
handshake to implement. The compatibility break is the hello payload
shape: current upstream sends the runtime version at `hello.server.version`
while ClawPanel only read the old flat `hello.serverVersion` field.
Read both shapes so latest kernels keep populating wsClient.serverVersion,
which in turn keeps Dashboard display, kernel snapshot feature gates and
isLatest checks working after the WebSocket handshake succeeds.
Also bump the recommended OpenClaw targets to the current npm latests:
- official: 2026.5.19
- chinese: 2026.5.18-zh.1
Verification:
- node --test tests/kernel.test.js
- npm run build
- manual module check: simulated both hello.server.version and legacy
hello.serverVersion payloads, both report serverVersion and protocol v4
Three follow-ups the user spotted in one round.
assistant.js — assistant did not know it was on Hermes
Both engines (OpenClaw and Hermes Agent) reuse the same /assistant
page (engines/hermes/index.js comments it as "共用页面/引擎无关"),
but getSystemPromptBase() hard-coded the OpenClaw self-introduction:
"你帮助用户管理和排障 OpenClaw AI Agent 平台 / 你精通 OpenClaw 的架
构…", followed by a CLI cheatsheet for `openclaw gateway start` and
`openclaw config apply`. Result: under the Hermes engine, the
assistant happily told users to run `openclaw doctor` and edit
`~/.openclaw/openclaw.json` — neither of which exists in the Hermes
world.
Split into a per-engine dispatcher:
getSystemPromptBase()
└ if hermes → getHermesSystemPromptBase() (new)
└ else → getOpenclawSystemPromptBase() (renamed, same body)
The new Hermes base prompt covers the facts that actually matter:
- dual-process layout: Gateway 8642 (chat API, what ClawPanel
mostly drives) vs Dashboard 9119 (admin/profiles/skills/oauth/
kanban — must be started separately)
- Profile system (independent workspaces, switchProfile restarts
dashboard, multi-gateway view)
- lazy_deps allowlist and why pre-installing matters
- paths: ~/.hermes (data) and ~/.hermes-venv (interpreter), with a
reminder that ~/.openclaw/clawpanel.json is the panel config
shared with the OpenClaw engine — not Hermes data
- Top-5 problem playbook (9119 not running, venv missing, channels
hanging on first launch, gateway crashing, profile drift)
- Explicit "do not give the user `openclaw …` commands"
Two more spots in buildSystemPrompt() are also engine-aware now:
- the "ClawPanel 工具能力" bullet list inside the soul-cache branch
- the "跨平台路径" reminder (Hermes points to .hermes / .hermes-venv)
lazy-deps.js — "请确认目标资源是否仍存在" was masking the real hint
When the user has not installed Hermes yet, Rust's
`hermes_lazy_deps_features` returns the very actionable string
"Hermes venv 未找到(~/.hermes-venv 不存在)。请先安装 Hermes。".
humanize-error.js then sees "未找到", classifies the error as
notFound, and replaces the message with the generic template
"请确认目标资源是否仍存在" — which tells the user nothing about
installing Hermes.
Take humanizeError() but render `message + raw` instead of
`message + hint`. The user now sees both the friendly title and the
exact Rust-side instruction. Drop the unused humanizeErrorText
import that this commit replaces.
config.rs — unblock CI (clippy too_many_arguments on existing code)
The clippy gate has been red on main since e1eda2d ("import external
client configs") because two helpers in commands/config.rs take >7
positional parameters:
- push_client_candidate (14 params)
- scan_json_client_file (10 params)
Both helpers exist purely to push a flat record into a Vec<Value>.
Wrapping them in a struct just to satisfy clippy would force every
caller to first build that struct, hurting readability. Suppress
clippy::too_many_arguments locally on these two functions with an
inline comment explaining why.
## Verification
- node --check + npm run build: clean
- cargo clippy --all-targets -- -D warnings: now compiles to
"Finished `dev` profile" with zero errors/warnings (previously
failed with two too_many_arguments)
- Playwright: import lazy-deps with api.hermesLazyDepsFeatures mocked
to throw "Hermes venv 未找到 … 请先安装 Hermes。", rendered content
contains "请先安装 Hermes" (hasRaw=true), does not contain the
generic "请确认目标资源是否仍存在" (hasGenericNotFound=false), and
does not contain "[object Object]"
Four independent UI fixes the user spotted in one screenshot tour:
services.js — desktop docker manager
ClawPanel is not a docker management tool. The "Docker 多实例管理"
block on the OpenClaw services page only makes sense for users who
deployed ClawPanel itself in Web mode (serve.js / dev-api) and want
to orchestrate multiple OpenClaw containers from one panel.
On desktop Tauri this block always degrades to either "未启用" with
a connect ENOENT error (no docker daemon on the user box) or to a
generic "unavailable" placeholder — pure visual noise. Skip the
whole config-section in render() and bail out of loadDockerManager()
when isTauriRuntime() is true. Web mode keeps the feature.
profiles.js — Hermes Profile manager could not load
Profile API only exists on the Hermes Dashboard process at 9119,
which the user has to start by hand. When it is offline, the page
showed a raw "由于目标计算机积极拒绝, 无法连接 (10061)" error which
is useless: users do not know they need to start a separate process.
load() now does the same probe → auto-start dance that
extensions.js / dashboard.js use for their 9119 links: probe first,
call hermesDashboardStart() if not running, only then issue the
/api/profiles request. If the start itself fails, we fall through to
the original catch and humanize-error renders a real reason.
lazy-deps.js — "[object Object]" on load failure
humanizeError() returns { message, hint, raw, action? }, not a
string. The catch branch passed the object straight into
escapeHtml(), so String(obj) coerced to "[object Object]". Switch
to humanizeErrorText() which is exactly the (message + hint)
one-liner string variant.
layout.css — header buttons crammed against the description
Several pages (hermes profiles / lazy-deps / files / gateways /
group-chat / kanban / oauth) put their header buttons inside
<div class="config-actions"> nested in <div class="page-header">,
but the existing flex layout rule only matched .page-actions. The
buttons therefore stacked directly under .page-desc with zero
visible gap. Add .config-actions to both the desktop flex selector
and the @media (max-width:768px) column-stack selector so all
these pages get the same title-left / actions-right layout.
## Verification
- npm run build (no warnings beyond the existing chunk size note)
- Playwright on /services in browser mode: docker section present;
same page after mocking window.__TAURI_INTERNALS__: section gone
- Playwright on /lazy-deps: rendered content does not contain the
string "[object Object]"
- Playwright dynamic-imports profiles / lazy-deps / files render():
computed style on .page-header is display:flex, flex-direction:row,
title and button share the same getBoundingClientRect().top, button
pushed to the right edge (justify-content:space-between effective)
Users have reported confusion about "when will ClawPanel update its
gateway protocol to v4". This is actually a misreading: ClawPanel v0.15+
already advertises `minProtocol=3, maxProtocol=4` in its connect frame,
and negotiates v4 transparently when the kernel is >= 2026.5.12. The
`v3|` prefix users were seeing in dev-api.js is the device signature
payload string schema version, which is a completely separate concept
from the handshake protocol version.
Make this visible and unambiguous:
UI
- Add a "Proto v4" badge next to the Gateway service name in
/services once the WS handshake succeeds, with a tooltip explaining
that this is the WS handshake protocol version (not the device
signature payload v3 format).
- Add the same protocol info to the WebSocket row in /chat-debug.
API
- WsClient now exposes `negotiatedProtocol` which prefers the explicit
field from the hello payload (`protocol` / `protocolVersion` /
`negotiatedProtocol`) and falls back to inferring from serverVersion:
kernels >= 2026.5.12 are reported as v4, older as v3. This matches
the panel's advertised range of [3, 4].
- KernelSnapshot grows a `protocol` field so feature gates and UIs that
already consume the snapshot can read it without touching wsClient.
Comments
- Expand the KERNEL_TARGET comment in feature-catalog.js to spell out
the two-distinct-version-numbers rule explicitly.
- Add matching clarifying comments next to the `v3|...` payload string
in both scripts/dev-api.js and src-tauri/src/commands/device.rs, so
the next reader does not confuse payload schema with handshake.
## Verification
- node --check on all touched JS files
- npm run build
- cargo fmt --check && cargo check (clippy errors that surface are
pre-existing debt in config.rs, untouched here)
- Playwright /services: mock wsClient state, observe `协议 v4` badge
rendered with `rgba(99, 102, 241, 0.1)` background and accent color,
for both the explicit-protocol path and the version-inferred path.
The fallback editor's "Add" buttons appeared to do nothing because
applyDefaultModel auto-populated defaults.model.fallbacks with every
non-primary model whenever the list was empty (and the same for
defaults.models). After any save with an empty chain, the chain got
filled with all 17+ candidates; the candidate pool became "No candidate
models available" and any subsequent Add click hit the early return
`if (modelConfig.fallbacks.includes(full)) return`.
Even worse, the auto-fill made it impossible for users to keep an empty
fallback chain on purpose: deleting all chips would silently get
replaced with every model on the next debounced autosave.
Remove the auto-fill in applyDefaultModel. An empty fallback chain is a
valid configuration that means "no implicit fallback"; Gateway already
surfaces a clear primary-model error in that case. normalizeDefaultModel
Selection still cleans up invalid/duplicate entries.
Also add a small "Clear All" button next to the active chain title so
users can drop the entire fallback list in one click instead of removing
chips one by one (especially useful for the existing bloated 17-fallback
state created by the old auto-fill path).
## Verification
- node --check src/pages/models.js
- node --check src/locales/modules/models.js
- npm run build
- Playwright repro: open /#/models → Clear All → fallbacks on disk
go from 17 → 0 → click Add on one candidate → fallbacks on disk are
exactly that one entry, no longer auto-expanded back to 17.
The previous implementation passed CREATE_NEW_CONSOLE to a Rust
StdCommand spawning cmd.exe directly, but Rust's default Stdio::inherit
copies the parent stdio handles into STARTUPINFO with
STARTF_USESTDHANDLES, which neutralizes CREATE_NEW_CONSOLE. The cmd
process then ran without a visible window (MainWindowHandle = 0), so
users only saw the OpenClaw node child started by runner.cmd in Task
Manager and got the impression that "the terminal does not pop up".
Wrap the launch in `cmd /c start "OpenClaw Gateway" /D <dir> cmd /D /K
runner.cmd` so the new console is created by the `start` builtin via a
fresh CreateProcess call without inherited stdio. The outer cmd /c
itself is short-lived and uses CREATE_NO_WINDOW so it does not flash a
window, and the inner cmd hosting runner.cmd reliably becomes a normal
visible terminal that the user can close to stop Gateway.
Because the visible terminal is now detached from our process tree,
spawn().id() can no longer be tracked. Instead, poll netstat after the
launch and record the listener PID as the active Gateway PID, which is
what the stop path actually needs to send a precise kill to.
## Verification
- cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check
- cargo check --manifest-path src-tauri/Cargo.toml
Add a model client import flow that scans local Codex, Claude Code, Gemini CLI, and common environment variable configurations without reading or copying OAuth tokens.
The new backend command returns safe import candidates with provider metadata, model IDs, and API key environment-variable references. Tauri and Web/dev-api both implement the scanner, and Web mode keeps the scan local even when a remote instance is active.
The Models page now offers an import wizard that lets users select importable candidates, adds providers without overwriting existing keys, preserves secrets as ${ENV_VAR} references, and leaves OAuth-only Codex entries as guidance rather than direct OpenClaw imports.
## Verification
- node --check src/pages/models.js
- node --check src/lib/tauri-api.js
- node --check src/locales/modules/models.js
- node --check scripts/dev-api.js
- cargo fmt --check
- cargo check
- npm run build
Treat an unavailable local Docker socket with no containers as an optional Docker management capability instead of showing a meaningless offline default node.
Improve the model configuration layout for large model/fallback lists by wrapping controls, truncating long IDs safely, capping collapsed fallback chips, and making the fallback editor responsive.
Guard chat message insertion against route changes so async history/hosted output cannot insert into an unloaded chat DOM.
## Verification
- node --check src/pages/models.js
- node --check src/pages/services.js
- node --check src/pages/chat.js
- node --check src/locales/modules/services.js
- npm run build
Delay heavy dashboard requests until after the first stat-card render so slow version checks, agent scans, MCP reads, backups, channel discovery, or log tail reads cannot occupy the backend before the initial paint.
Add a 1.2s first-paint fallback that replaces skeleton cards with safe unknown-state cards and logs a warning when dashboard APIs are still pending.
## Verification
- npm run build
Render the dashboard first wave without waiting for get_version_info, which may spawn CLI work and query the registry. Version data now updates the stat cards asynchronously after the core service/config data is shown.
Also shorten the desktop Gateway port probe before WebSocket connection from 20s/2s polling to 3s/300ms polling, relying on the WebSocket reconnect path instead of blocking startup for a long time.
## Verification
- npm run build