* fix(test): run the shared Control UI lane on the cross-file cleanup runner
ui/vitest.config.ts drives CI's checks-ui job (pnpm --dir ui test). Its
unit project sets isolate:false but never wired
runner: nonIsolatedRunnerPath, so the per-file cleanup in
test/non-isolated-runner.ts — module-graph reset, repo-owned custom
element dropping, DOM body reset, timer and spy restoration — never ran
in the lane CI actually uses. Only the repo-root lane behind
scripts/run-vitest.mjs loaded it.
Files sharing a worker therefore kept the previous file's evaluated
modules, so whichever file imported a component first pinned it to the
real dependency and a later file's vi.mock factory never reached
production code, surfacing as "expected 0 to be 1" in whichever sibling
the size sequencer happened to pack alongside it. This is the class
PR #123512 diagnosed and fixed at the runner; the fix never reached this
lane, so the repo kept absorbing it one uiIsolatedTestFiles entry at a
time.
browser stays exempt (the runner imports node:fs and server modules that
cannot load in browser mode) and unit-node stays exempt (it carries the
Playwright-driven layout tests whose browser lives in module scope, which
per-file module resets churn). The config test asserted runner was
undefined for every project, pinning the broken wiring; it now asserts
the invariant and fails on the pre-fix config.
* fix(agents): stop passing an ignored resolver to instance-bound announce dispatch
check-prod-types is red on main: #126062 threaded resolveGatewayContext
into the announce dispatch call, but that call now goes through
dispatchGatewayLifecycleMethod, whose options type does not carry the
field.
The type checker is right that it does not belong there. That dispatcher
hands work to runtime.dispatchAgent, which resolves context from the
Gateway instance it is bound to and forwards a fixed option allowlist, so
a caller-supplied resolver was already being ignored. Dropping it is
behavior-preserving.
The delivery test asserted the resolver was forwarded, but production now
binds to the instance dispatcher while the test injects a mock, so that
assertion only proved the mock. It now asserts the resolver is
deliberately not forwarded.
Left for the owner of #126062: sendSubagentAnnounceDirectly and its
callers still accept and thread resolveGatewayContext, which is now
vestigial on this path. Deleting that chain or teaching the instance
runtime to honor the resolver is a design call on a just-landed change.
* improve(control-ui): continuous corner curvature with a radius scale
Route every step of the Control UI radius scale through one knob and, on
engines that can draw a superellipse, widen it by 1.25 while switching the
app's surface families to continuous corner curvature. A corner that keeps
curving into the edge reads as a shorter arc than a circular corner of the
same radius, so the scale gives that visual weight back.
The whole refinement sits behind @supports (corner-shape: superellipse(1.5)),
so engines without it keep today's radii and today's arcs unchanged. The shape
is opt-in per surface: a superellipse flattens the ends of anything fully
rounded, and pills, circles, avatars and status dots are a third of this app's
corners.
Menu item radius is now derived from the menu panel radius minus its padding,
which is the optical relation its comment already documented and which now
holds at any corner scale.
* test(control-ui): assert chat radii against the live corner scale
* fix(ui): scope the corner-curvature radius scale to opted-in surfaces
The @supports block scaled --openclaw-corner-radius-scale at :root, so
every --radius-* consumer inherited the 1.25 multiplier — including
surfaces that never opted into corner-shape (e.g. .run-inspector__panel,
which stayed `round` but drew an inflated 12.5px radius) and the radius
tokens mcp-app-theme.ts exports to embedded MCP apps.
The canonical --radius-sm/md/lg/xl/--radius tokens at :root are back to
their pre-refinement literal values. The scaled radii now live only on
the selector list that also receives corner-shape, redeclaring the
tokens locally so the widened radius stays contained to opted-in
surfaces and their descendants. Menu panels needed their derived
--menu-radius/--menu-item-radius redeclared too (a custom property's
nested var() resolves relative to where it's declared, not where it's
read, so redeclaring only --radius-md wouldn't reach them), and
wa-dropdown/wa-popover/wa-select scale on the host rather than the
::part() panel since slotted menu items inherit from the light-DOM host,
not from the shadow part they render inside.
Adds regression coverage for the excluded consumer and the exported MCP
theme tokens to corner-shape.browser.test.ts.
* fix(ui): stop redeclared corner-curvature radius tokens from leaking to nested descendants
Round 1 fixed the :root-level leak by redeclaring --radius-* as an
inheritable custom property on each opted-in container instead. That
still leaks: any non-opted descendant nested inside an opted surface
inherits the container's redeclared token too. Proof: .settings-segmented
nested inside .settings-group rendered 12.5px round instead of its
canonical 10px, and .agent-chat__search-bar's own --radius-lg-derived
corners doubled up incorrectly depending on ancestor nesting.
Every opted selector now sets border-radius as a direct property computed
from the scale, not an inheritable token — border-radius does not
inherit, so nested non-opted descendants keep reading the untouched
canonical --radius-* from :root. Each selector is specificity-doubled
(`.card.card`) rather than !important, so it reliably outranks its own
bare-class declaration in a later-loaded file without silently clobbering
a genuinely more specific existing override elsewhere in the app (e.g.
`.chat:has(> .agent-chat__search-bar)`, `.chat-task-detail__transcript >
.chat-thread`) — both surfaced as regressions during this fix and are
preserved by design now. The one exception is the generic
wa-dropdown/wa-popover/wa-select ::part() panel selector, which by design
must outrank dozens of concrete per-feature panel classes app-wide; a bare
type selector can never out-specificity a class selector, so that rule
keeps !important.
--menu-item-radius at :root also serializes back to the byte-identical
`var(--radius-sm)` from base f21d34d71d in the unscaled fallback path;
the derived panel-minus-padding formula now lives only in the scaled
wa-dropdown/wa-popover/wa-select host redeclaration where it's needed.
Adds a nested-inheritance regression case to
corner-shape.browser.test.ts (.settings-segmented inside .settings-group)
and a .settings-group entry to prove the container itself still scales.
* fix(ui): pair scale and corner-shape on every corner-curvature selector
ClawSweeper caught two more instances of scale and shape traveling apart:
1. .option-card__choice wasn't in the corner-curvature list at all, despite
being a real nested member of the .option-card family (a button inside
.option-card__choices inside .option-card, drawn with the same 14px
--radius-lg step as .card/.settings-group). It kept its canonical 14px
round corner even under the scale.
2. .slash-menu-item / .chat-reply-context-menu button / .chat-selection-popup
button picked up the scaled radius from round 2 but never got
corner-shape, so their corners grew without curving into a squircle —
scale without shape, the same incoherence class as (1) in reverse.
Decision on (2): items get corner-shape added (not scale removed). The
panel they sit in is already squircle; an item whose radius grows but stays
circular reads as an unrelated circle poking out of the panel's superellipse,
not a coherent nested row.
A final invariant pass over the full @supports block (every selector that
sets a live border-radius must also set corner-shape, and vice versa) found
one more forgotten member beyond the two reported: .agent-chat__search-bar
scaled its own (bottom-only) radius but never carried corner-shape, same gap
as the menu items. Fixed alongside the reported two. Every remaining
selector in the block already pairs the two; the wa-dropdown/wa-popover/
wa-select host rule is the one correct exception, since it only carries
custom properties for descendants and draws no border of its own.
Test coverage now exercises each surface's real nested DOM instead of a
synthetic flat selector: option-card.ts's actual button-inside-choices-
inside-card structure, chat-composer-slash-menu.ts's item three levels
below the panel, and chat-thread-interactions.ts's plain <button> item
matched via descendant tag selector. Added a corner field to CornerCase so
the harness can probe the correct physical corner (search-bar's bottom-left
is its only non-zero corner) instead of universally reading top-left. All
four new/changed cases are proven to fail on pre-fix base.css with the
exact "radius scaled, shape stuck at round" (or, for option-card__choice,
"not opted in at all") signature before the corresponding fix.
* fix(ui): keep the GitHub mark on its label's line when chat wraps
The mark is an in-flow atomic inline, which carries a soft wrap opportunity
after it, so a chat line could end on the octocat and start the next one with
the reference it labels. Painting the mark out of flow and reserving its space
with the anchor's own padding removes that opportunity: nothing inside the
anchor precedes the label any more.
The anchor stays inline, so a long bare URL still fills the line it starts on
instead of moving down whole -- the sibling file-link chip's atomic treatment
could not be reused here.
* test(ui): route GitHub link layout test through Node
Recognized workspace paths in chat now read as their basename behind a
file-type glyph, so a file reference is identifiable before it is read.
Markdown, package manifests, TypeScript and other code, TSX/JSX components,
config/data, shell scripts, and images each get their own mark; anything else
falls back to a plain document. Paths sharing a basename keep the smallest
trailing suffix that tells them apart, and Windows paths keep their own
separator.
Classification wins over authoring syntax: a path written in backticks is a
file link first, so it drops the inline-code chip and renders exactly like a
bare path beside it. Code spans that are not file links keep the chip.
The full path stays addressable: it drives the file panel, the new tooltip,
and the message Copy action (which returns the original Markdown).
Author-written labels in [label](path) links are never rewritten, and text
that is not a recognizable path stays plain prose.
The glyph is painted as a masked ::before like the sibling GitHub mark, so it
stays out of the accessibility tree and out of copied text and follows the
link color in every theme. Extension classification moves to one shared
resolver that the file preview modal now uses too, replacing its own
code/text extension set.
* fix(ui): scope the Control UI cursor convention to app-like display modes
The app-chrome cursor convention from #103357/#103411 was applied
unconditionally, so an ordinary browser tab lost the pointer hand on
buttons, menus, tabs, rails, selects and accordion summaries — the only
hover affordance a page owns. The convention is correct for the installed
window the manifest declares ("display": "standalone"), not for every
window the same bundle is served into.
base.css now owns one policy token, --cursor-action, selected by display
mode: pointer by default, the desktop arrow under standalone, minimal-ui
and window-controls-overlay. A low-specificity rule maps generic
actionable controls onto that token, restoring the affordance on the
surfaces #103411 stripped bare, while every component rule that owns a
semantic cursor (not-allowed, disabled, grab, resize, zoom-in, text,
wait) keeps winning without !important.
The 92 cursor: pointer declarations that had drifted back into ui/src
since July now consume the token instead of hardcoding the hand, so they
stop contradicting the policy in an installed window. Real hyperlinks
keep the pointer in every mode. The pre-boot mount fallback repeats the
policy locally because it must render when the bundle fails to load.
Closes#121242
* test(ui): run the cursor policy browser test in the node-driven project
ui/vitest.config.ts routes Playwright-from-Node .browser.test.ts files to the
unit-node project; without registering the new cursor policy test there, the
in-browser chromium project tried to import it and failed on node:fs/playwright.
* fix(ui): keep the desktop arrow in the native app hosts
The macOS dashboard embeds the Control UI in a plain web view, which
reports `display-mode: browser`, so the display-mode-only policy would
have handed it the browser-tab pointer. It already announces itself with
`openclaw-native-macos`/`-nav`/`-web-chrome` on `<html>`, the same markers
`ui/src/styles/layout.css` matches on, so the policy reads those too.
* feat(ui): path-based session and dashboard URLs
* docs(ui): document control UI URLs
* fix(ui): finalize session path routing
* feat(ui): anchor session URLs on stable keys
* docs(ui): clarify stable session URL identity
* fix(ui): resolve session prefixes with full prefix and pagination
Query sessions.list with the full supplied prefix instead of an eight-char
truncation, and paginate up to a bounded page count so longer disambiguation
links resolve instead of being reported ambiguous. Zero strict-prefix matches
now fall through to literal-key resolution rather than rendering an empty
ambiguity view.
Also document the ~dot/~dotdot segment escape: peer ids reach session keys
trimmed and lowercased only (src/routing/session-key.ts), so a literal '.' or
'..' segment is reachable and browsers would normalize it away.
* fix(ui): synchronize committed session routes
* test(ui): split native shell host coverage
* fix(ui): thread configured mainKey through session URL builders
Reserved-set disambiguation needs the operator-configured mainKey at runtime, so
thread it from agentsList through every session path builder and the ClickClack
control URL. Unambiguous non-hex single-segment rests now resolve literally while
short-id-shaped rests still fail closed, which restores ClickClack channel
compatibility detection and control-link reconciliation.
Also bound prefix-resolution retries, preserve catalog thread identity, and keep
draft state on ambiguous candidate links.
* fix(ui): repair session URL CI integration
* perf(ui): lazy-load session route resolution
* perf(ui): isolate session prefix resolution
* perf(ui): defer session path parsing
* perf(ui): defer session navigation startup
* fix(ui): preserve first-run and literal session navigation
* fix(ui): satisfy session routing type and export gates
* fix(clickclack): preserve unscoped control link agent
* style(ui): satisfy chat page line limit
* refactor(ui): move chat page helpers to owning modules
* fix(ui): preserve destination session route identity
* fix(ui): preserve agent identity in session routes
* fix(ui): escape dots in literal session path segments
encodeURIComponent leaves periods intact, so a literal key segment like
channel:release.js produced /chat/main/channel/release.js. In-app navigation is
intercepted by the SPA, but a refresh, an external link, or a ClickClack link
would be served as a static asset request and never reach the app.
pathForWorkboardBoard already escapes dots for this reason; mirror it in both the
session URL contract and the ClickClack encoder, route the agent id through the
same segment encoder, and pin the case in both shared vector tables.
* fix(ui): stop bootstrap after teardown race
* refactor(ui): centralize session navigation targets
* fix(ui): consume bootstrap teardown abort
* fix(ui): canonicalize configured main session routes
* fix(ui): preserve distinct session references
* fix(ui): redirect released session query links
* fix(ui): make bootstrap teardown abort-safe
* test: wire Control UI suite into CI, fix its broken tests, drop dead harness surface
- fix 7 Control UI tests broken on main: module-mock factories are unreliable
under isolate:false shared workers; use RealtimeTalkSession prototype spies
and real jsdom storage instead (chat-realtime, chat-pull-requests)
- add gated checks-ui CI job (runUiTests changed-scope output): chromium
provisioning, lint:ui:no-raw-window-open, pnpm --dir ui test; ~30s on a
4vcpu runner, runs only when ui-affecting paths change
- add weekly node22-compat workflow exercising the supported lower Node bound
with the same command set as the dispatch-only ci.yml job
- delete the unreachable channels entry in EXTENSION_TEST_CONFIG_ROUTES and
the never-populated extensionRoutedChannelTestFiles override machinery
(born empty in 2ccb5cff22); resolved globs are provably identical
- guard run-vitest.mjs hardcoded path maps with existence tests so renames
cannot silently drop extended stall watchdogs
- remove unenforced coverage thresholds; test:coverage is informational, docs
updated (reference/test, help/testing, plugins/sdk-testing)
Closes#104321
* test: pin OPENCLAW_CI_RUN_UI_TESTS in the manifest env expectation
* test(ui): capture the realtime start spy instead of referencing the unbound prototype method
* test(ui): raise ui vitest timeouts for real-browser layout tests on small CI runners
Keep moved Control UI browser layout tests routed in both UI Vitest configs and stabilize chat suite-order assertions. Verified with Testbox UI proof and changed gate; final PR CI remains the merge gate.
Refactor the Control UI around route-owned page lifecycle and state while preserving existing behavior and design.
Prepared head SHA: bd51b6fa76
Co-authored-by: Shakker <165377636+shakkernerd@users.noreply.github.com>
Reviewed-by: @shakkernerd
Harden Workboard modal and drawer accessibility.
Summary:
- Add Workboard dialog focus lifecycle handling for initial focus, Tab/Shift+Tab containment, Escape close, and opener restore.
- Mark Workboard background content inert/aria-hidden while modal or drawer dialogs are active.
- Add focused unit and Chromium browser smoke coverage for the audited modal/drawer accessibility requirements.
- Keep UI browser test aliases able to resolve shared workspace packages used by the Workboard view.
Verification:
- node scripts/run-vitest.mjs ui/src/ui/views/workboard.test.ts
- node scripts/run-vitest.mjs ui/src/ui/views/workboard.browser.test.ts
- (cd ui && pnpm exec vitest run --config vitest.config.ts --project browser src/ui/views/workboard.browser.test.ts)
- GitHub checks green at 6557012430
Extract shared normalization/coercion helpers into private @openclaw/normalization-core workspace package while preserving existing plugin SDK helper subpaths.\n\nAlso keeps direct normalization-core imports internal, wires UI/build/loader resolution, and replaces the slow PR network CodeQL lane with a fast added-line boundary scan while retaining full CodeQL for scheduled/manual runs.\n\nVerification: local moved tests, plugin SDK boundary tests, extension loader tests, agents-support shard, UI build/test, build artifacts, lint, workflow guards, autoreview, and GitHub CI passed on PR head 963d893715.