* fix(ui): constrain composer capability menus
* fix(ui): cap composer capability menu height
* test(ui): cover long capability menu states
* test(ui): capture capability menu interaction states
* fix(ui): keep dark capability states legible
* fix(ui): preserve archive state across stale refreshes
* fix(ui): retain archive state across navigation
* fix(ui): ignore stale archive events
* fix(ui): scope archive state to session identity
* fix(ui): require session identity for presentation metadata
* fix(ui): require exact archive identity
* feat(ui): expand session diff navigation
Release note: Make the session diff scope selectable from its header and let working-tree diffs reveal unchanged file context in bounded chunks.
* fix(ui): keep unchanged-line expander labels inside their row
Chromium's flex layout on button elements pins the label line box to the
button's vertical center, painting the gap count into the diff row above.
Grid centering avoids the quirk; skip rows also center-align their controls.
* fix(ui): revalidate diff snapshot before expanding unchanged lines
Addresses the ClawSweeper P2 stale collapsed-gap content finding by revalidating the target file patch before reading current file text.
Removing the Ask OpenClaw button from the chat workspace rail (#122507) left
CUSTODIAN_PANEL_TOGGLE_EVENT with no production dispatcher, so its event
constant, detail type, panel listener, handleToggleRequest, toggle(), the
shell's deferred-load handler and its forwarding delegate were all dead.
The floating panel keeps its real open path: app-shell-navigation raises
minimizeRequestId when the operator leaves the /custodian route mid-conversation
and the panel opens itself when the store has a real user turn. Preloading is
unchanged and still gated on openclaw.chat, so the panel mounts exactly when it
could be available.
Tests move onto that surviving path rather than being deleted; the unused
custodian.panel.toggle string goes with the contract.
Production LOC: -64, none added.
* feat(gateway): add gateway-host desktop source behind desktop.host lab
Introduce the host as a first-class desktop source so operators can view
the machine OpenClaw runs on, not just cloud-worker environments:
- protocol: desktop.observe / desktop.launch with a discriminated
DesktopSource union (host | environment) plus an additive auth hint;
EnvironmentSummary gains a top-level desktop flag
- config: desktop.host { enabled, port?, passwordFile? }, Labs-gated
- rfb-probe: pure RFB version/security-type parser used to detect an
already-running loopback VNC server and classify its auth
- host-source: attaches to 127.0.0.1:<port>, refuses unauthenticated
(None) and unsupported (VeNCrypt) servers, and refuses ARD with the
supported alternative until the macOS milestone
- host-guidance: per-OS enablement text so no path dead-ends
- doctor + status report host desktop availability and auth type only
worker.desktop.observe/launch stay as delegating aliases with identical
behavior. Also drops the now-unused WorkerDesktopTunnels type export.
Live-verified against macOS Screen Sharing: probe reads RFB 003.889,
returns security types [30,33,36,35], classifies ard-account.
* test(gateway): probe RFB handshakes through the socket boundary
The probe's banner and security-offer parsers were exported solely so
unit tests could call them, which the dead-export gate rejects and which
tests internals rather than behavior. Keep them module-local and drive
the probe through a scripted loopback server instead.
The boundary tests also cover what pure-function vectors could not:
handshakes split across packets, legacy RFB 3.3 single-word security,
server-rejected handshakes, early hangups, and connect timeouts.
* feat(ui): let the Desktop panel view this machine, not just cloud workers
The Desktop panel was gated on a cloud-worker session placement, so an
operator running OpenClaw locally had no way to see the machine hosting
their main session even with a VNC server running on it.
Availability now follows the advertised desktop.observe method plus
operator.admin instead of session placement, and the picker lists every
environment whose summary reports a desktop, with the gateway row shown
as "This machine". Sources are passed to the generic desktop.observe /
desktop.launch RPCs; the app launcher stays worker-only. When a host
attach needs a password the gateway did not supply, the panel prompts and
keeps the value in memory for that connection only.
Adds the hostDesktop Labs toggle for desktop.host.enabled.
* fix(scripts): keep the env-var ratchet usable in shallow checkouts
The env-var budget check resolved its base ref, then hard-failed when
`git merge-base` found no shared ancestor. Shallow clones and grafted
agent checkouts resolve origin/main but truncate the history behind it,
so an advisory growth ratchet took down the whole check:changed gate
with "Could not resolve env-var count merge base for: origin/main".
Only the growth comparison needs a baseline, and the script already has
a no-baseline path. Treat git's exit 1 with empty output (no shared
ancestor) as that case and say so on stderr; a genuine failure still
exits 128 and still throws, and the absolute count-vs-budget check runs
either way.
* test(ui): measure the inline-code chip against its line box
The inline-code spacing test compared the chip's height to a prose text
rect, so it silently measured the monospace font's default line spacing.
That is ~17px on macOS and several px shorter on Linux, so the assertion
passed on CI and failed locally at 4.5 against a 3.75 bound -- after the
bound had already been widened once to chase browser font metrics.
Compare the chip to the paragraph's CSS line box instead, which is what
"the chip must not disrupt the line" actually means and is platform
independent. The horizontal gap stays as-is: it is em-derived padding
plus border, and it is the assertion that catches detached punctuation.
Verified both directions on macOS: the file is fully green, and
restoring the pre-fix 0.15em/0.35em padding still fails the gap
assertion at 5.41.
* feat(gateway): view macOS Screen Sharing from the Desktop panel
Modern macOS only offers ARD account authentication for Screen Sharing,
so the host desktop source refused every Mac. The Gateway now performs
the ARD handshake itself against the loopback server and hands the
browser a plain RFB 003.008 no-auth handshake, so the operator's macOS
account password authenticates the desktop without ever reaching the
browser, the observe result, a URL, or a log.
- rfb-preauth: ARD (type 30) Diffie-Hellman with MD5-derived AES-128-ECB
credentials, and VncAuth (type 2) bit-reversed DES, both under a single
10s negotiation deadline; Apple's RFB 003.889 maps to 3.8
- observe-bridge: runs pre-auth before splicing and starts the view-only
filter at clientInit, since the browser handshake is consumed here;
worker tokens keep the original version start phase
- host-source: attaches ARD, requiring per-observation credentials that
live only in the one-shot observer token and are dropped after use
- doctor: offers an explicitly confirmed sudo launchctl repair when
Screen Sharing is off, and prints the System Settings path otherwise
Live-verified against this Mac's Screen Sharing: the DH exchange and
credential framing are accepted and the server returns SecurityResult.
The VncAuth DES vector is confirmed against OpenSSL independently.
* feat(ui): commit-scoped session diff viewer with file, sync, and view menus
The Control UI session diff panel becomes a dense Amp-style viewer: per-file
menus (copy path, open file, reveal in file tree, open in editor), a Sync
Locally popover with a copyable git fetch command, view options (collapse all,
wrapping, split/unified layout), and a sticky footer that reports how far the
branch is ahead of its merge base and switches between all changes,
uncommitted work, and individual commits.
sessions.diff gains an additive scope param (all | uncommitted | commit) plus
commits, aheadCount, and mergeBase metadata. Commit-scope diffs read only the
object database, skip untracked collection, and bypass session-start baseline
filtering; unknown commits surface a typed unavailableReason. Offscreen file
bodies use content-visibility so large diffs stay responsive.
* fix(ui): satisfy session diff CI gates
* fix(gateway): fence commit-scoped session diffs to the advertised branch history
ClawSweeper found that commit-scoped sessions.diff accepted any commit resolvable in the checkout. Fence operator.read commit reads to the advertised merge-base..HEAD history and cover sibling-branch and base-history commits.
* fix(plugins): own packed entry lifecycles by package
Persist package ownership per runtime child, route lifecycle actions through one closed resolver, reconcile removed child policy during updates, and retain rollback generations until durable config/index commit.
* fix(plugins): break uninstall policy import cycle
* test(plugins): model package ownership in lifecycle fixtures
* improve(ui): make Control UI feel native on mobile
* fix(ui): keep coarse-pointer input floor text-scale aware
* fix(ui): let self-sized controls opt out of the touch input floor
* fix(ui): fold per-control coarse-pointer font floors into the shared touch floor
* feat(gateway): projects.list groups known checkouts by repo identity
Implements docs/plan/runners.md milestone 4 derived projects read model.
* feat(ui): regroup the Where picker by gateway, devices, and cloud
* feat(ui): placement chip shows where a session runs with reclaim
Implements docs/plan/runners.md milestone 4 placement display and reclaim.
* test(ui): advertise terminal and browser panels in the mock harness
The mocked Control UI never advertised browser.request or terminal.open and
left terminalEnabled false, so the chat header's panel toggles were invisible
in the harness and could not be visually verified.
* fix(ui): give the chat pane header the only panel toggle row
The session workspace rail header rendered Terminal, Browser, Ask OpenClaw and
Changes alongside its own dock/refresh/collapse controls. Terminal and Changes
already lived in the chat pane header, so both rendered twice at once, while
Browser was reachable only from inside a files rail and Ask OpenClaw sat in a
per-session rail despite being a global surface that already owns a sidebar
entry (settings route 'custodian').
The rail header now owns workspace-file actions only. Browser moves up to the
pane header beside Terminal and into the narrow-header overflow menu; the
duplicated Terminal and Changes buttons and the Ask OpenClaw toggle are gone.
Production LOC: +21 -84.
* test(ui): stop the vite stub from shadowing the mock bootstrap config
ui/vite.config.ts registers a placeholder /control-ui-config.json middleware
and config-file plugins load before inline ones, so the mock gateway plugin's
bootstrap body never reached the app and every scenario bootstrap field was
silently dropped. Marking the mock plugin 'pre' lets it answer first.
* refactor(gateway): extract source-agnostic desktop relay core
Split the cloud-worker desktop observer into a reusable core under
src/gateway/desktop/ so upcoming desktop sources (gateway host, nodes)
plug into one relay pipeline:
- attachment.ts: RfbAttachment union (unix socket | loopback TCP)
- session-registry.ts: generic observer lifecycle (8-observer cap,
controller eviction, 60s linger, owner-epoch fencing) with typed
stale-owner/stopped errors
- observe-bridge.ts: single-use observe tokens + WS relay, path moves
to /desktop/observe (clients consume wsPath verbatim)
- rfb-view-only-filter.ts: pure move
desktop-tunnel.ts keeps only worker SSH acquisition and app launch,
plugged into the shared registry; external API unchanged. Behavior
is identical; worker desktop tests pass unmodified apart from import
and path renames. Drops the never-used now() option.
* fix(gateway): fence only superseded desktop sessions
The desktop core extraction replaced the owner fence's "stop strictly
older owners" check with an unconditional session stop. A launcher that
claims an owner epoch first, then reaches its async fencing pass after a
same-epoch observe has already created the session, tore that session
down and failed the observer with "stopped before connecting".
Restore the original invariant in the registry that owns it:
stopSuperseded() retires an entry only when its epoch is strictly lower
than the claimant's, so peers sharing a generation keep the session.
The regression test drives launch-then-observe at one epoch and fails on
the pre-fix code inside fenceReplacedOwners.
* refactor(gateway): drop unused WorkerDesktopTunnels type export
* feat(webui): auto-request notification permission on first chat send
The Control UI now asks for notification permission automatically on the first chat message you send (once per browser/origin; web push in browsers, native prompt in the macOS app; denied permission is never re-asked automatically — Settings -> Notifications stays the manual path).
* fix(webui): preserve notification prompt gesture boundary
Restrict the one-shot notification prompt to direct non-command composer sends, invoke browser permission synchronously, and keep Settings documented as the management and recovery path.
* fix(webui): exclude deferred notification sends
Require an actively dispatching browser input event before consuming the notification one-shot, so retained catalog handoffs and other deferred sends cannot suppress the first real composer prompt.
Require an account-bound confirmation before deleting WhatsApp credentials, and revalidate Gateway ownership plus account state after the modal resolves.
* fix(ui): resolve project icons from deterministic paths
Prepare project icon bytes during chat startup and serve only the process-stable snapshot. Keep all filesystem work asynchronous and lift the breadcrumb trail onto the topbar's optical axis.
* fix(ui): keep project icon paths web-focused
Drop the IDE-specific icon convention so every candidate remains an explicit web project path.
* fix(ui): preserve project icon compatibility
Restore existing web icon paths and refresh bounded session snapshots when they are served. Keep the IDE-specific path excluded per the final product decision.