* fix(mac): prerelease-exact launch resolution and policy-aware update ownership
* fix(ui): keep update affordance when nav is hidden and fall back on native decline
* chore(i18n): sync native inventory
* feat: sidebar update card (web + macOS) with app-first mac update flow and Sparkle beta track
Squashed from claude/update-notification-display-c6cfb9 after semantic merge
with #104178 (channel-aware CLI installs). See PR #104171 body for details.
* chore(i18n): resync generated inventories after rebase
* chore(i18n): resync locale metadata after rebase
* fix(ci): register local-audio-acceleration doctor contribution and wrap overlong MLX log line
* fix(ci): keep MLX log line under 120 cols and resync native i18n inventory
* chore: nudge PR head sync
* fix(mac): use throwing safe read in MLX transport readability handler
Adds a git-backed session diff panel to the Control UI, complementing the existing PR status chips.
New sessions.diff gateway method (operator.read): resolves the session checkout and returns structured per-file diffs (status, renames, +/- counts, capped unified patch) of branch + uncommitted + untracked work against the default-branch merge base. Hardened against git textconv execution and hardlinked out-of-tree content leaks; handles repos before their first commit via the empty-tree base.
Control UI renders a "Changes" panel in the chat detail sidebar with collapsible per-file diffs, hunk-gap markers, stat chips, and untracked/binary badges, gated on gateway method advertisement. Schemas additive (Swift models regenerated), 20 locales translated.
Closes#104182
* feat(android): durable offline chat with attachments and history-proof retirement
Every chat send is journaled to the per-gateway Room outbox before any
network attempt, so process death always has one durable recovery owner.
Rows survive gateway ACKs as 'accepted' and retire only once the turn is
proven in canonical chat.history by idempotency key; ambiguous outcomes
(lost ACK, kill mid-send, gateway restart before the transcript write)
park as delivery-unconfirmed for explicit retry, per the fail-closed
model #103273 landed. An unproven accepted head briefly holds only its
own session's queue, and retrying a parked head re-orders still-queued
successors behind it. Offline sends now accept picked images and voice
notes; attachment bytes persist as chunked BLOBs (512 KB chunks, 8 MB
per message, 48 MB per gateway) admitted and retired atomically with
their row. Pre-hello 'main' rows pin to the canonical session at first
dispatch so later default-agent changes cannot retarget captured input;
slash commands are connection-gated by a persisted epoch (legacy queued
command rows migrate to a never-matching sentinel) and never auto-replay
across reconnects. The Room store moves to schema v4 on top of v3's
data-parking migration, adding the epoch column and attachment tables
while preserving queued rows. The queued->sending claim is an atomic
compare-and-set shared by the direct dispatch and the flush loop, the
direct path waits for the startup recovery sweep before claiming, and
failed reconnect attempts republish outbox rows so queued sends stay
visible on offline cold starts.
Proof: 1151 Android unit tests and :app:ktlintCheck green (new
migration, storage, and controller coverage for admission, restarts,
claims, pinning, gating, ordering, and byte round-trips); a live
emulator scenario on the pre-integration build queued text+image
offline, survived force-stop plus a device reboot, resent exactly once
on reconnect (single <rowId>:user turn in the gateway transcript), and
drained the outbox after canonical-history confirmation.
Closes#104087.
* fix(android): rearm outbox recovery when direct-send state persistence fails
* fix(android): keep direct dispatch alive across caller cancellation
The direct send's network phase now runs in the controller scope, so a
cancelled UI scope (leaving the chat screen mid-send) can no longer
strand a claimed row in 'sending' with no user action available; the
dispatch completes and settles the row exactly once. Direct-path state
persistence failures also re-arm the startup recovery sweep, mirroring
the flush path's fail-closed handling.
* fix(android): hand claim-persistence failures to the flush lane
A direct-send claim that fails to persist no longer reads as a lost race:
the admitted row is handed to the flush lane so a healthy connection still
delivers it, and the flush path's fail-closed handling owns any repeated
storage failure instead of the UI reporting success with no active owner.
* fix(android): enforce connection gating and fail-closed parking on every send path
The direct dispatch now rechecks a slash command's connection epoch after
its durable claim, so a reconnect between admission and dispatch parks the
command instead of replaying it on the new connection. Parking a stale
gated row only counts once the write persists; a storage failure drops
health, re-arms recovery, and halts the flush pass instead of spinning or
dispatching the stale row.
* fix(android): recognize gateway-acknowledged run ids in outbox ownership checks
A chat.send ACK can return a run id that differs from the row's
idempotency key (the direct path transfers local run ownership to it).
Ownership, in-flight, backlog, and timeout-park checks now consider both
ids, so reconciliation can no longer park — and Retry can no longer
duplicate — a turn that is still running on the gateway.
* fix(android): close remaining ack-ownership and persistence-failure gaps in the outbox
Flushed sends acked under a divergent run id now transfer local run
ownership like the direct path, so live runs cannot time out and surface
spurious errors for delivered turns. A session pin that cannot persist
stops the dispatch while the row is still safely queued. Reconcile and
timeout parking only report changes their writes actually persisted,
failing closed on storage errors.
* docs(android): record why acknowledged run ids stay in-memory
* feat(webui): reintroduce opt-in AI purpose titles for tool calls
Restores the chat.toolTitles path removed in #103821, gated behind the new
gateway.controlUi.toolTitles opt-in (default false) so tool rendering stays
fully deterministic with no background model calls unless an operator enables
it. Disabled gateways answer { titles: {}, disabled: true } without loading
the completion runtime, and clients stop asking for the session.
When enabled, titles use canonical utility-model routing: an explicit
utilityModel (operator-chosen provider, like every utility task), else the
session provider's declared small-model default, honoring per-session model
overrides and auth profiles; utilityModel "" disables titles and malformed
refs fail closed — never the primary model. Tool inputs are redacted with the
tools-mode redactor before cache keys or prompts, caller ids are bounded and
never reach the model, and results cache in the per-agent SQLite
cache_entries so repeat views never re-bill.
Also completes two crestodian model-input mock factories that leaked into
sibling tests under shared-registry CI shards.
Fixes#103987
* fix(webui): redact tool-title inputs before truncation
* fix(macos): drag the window from split-view pane headers
In the macOS app, split-screen pane header rows (session title strip) now
start a native window drag instead of selecting the title text. The Control
UI posts an openclawWindowDrag script message on mousedown over passive
header chrome; the dashboard window validates the trusted main-frame source
and the in-flight left-mouse press before calling NSWindow.performDrag.
Pane headers are chrome, so they are also no longer text-selectable.
* refactor(macos): move the dashboard failure page out of the window controller
SwiftLint type_body_length: the drag-message handler pushed
DashboardWindowController past 800 lines; the failure-page HTML is
presentation-only and now lives in DashboardFailurePage.
Kind-aware tool rows (terminal-style commands with wrapper stripping and
display highlighting, file edits with inline numbered diffs and diffstat,
write previews, key-value args), aggregate group summaries with live run
status, and a new batched chat.toolTitles gateway RPC that titles complex
calls via the configured utilityModel or the OpenAI Luna default (gated to
OpenAI-primary agents, cached in the per-agent SQLite cache_entries).
Also fixes two transcript pairing bugs: result blocks now inherit call
id/name/details at merge time, and results pair with any open call in the
current tool run so parallel calls render as single rows.
Fixes#103554
* fix(apple): prevent stale model thinking state
* test(apple): complete attachment transport fixture
* fix(apple): scope model patch ordering by route identity
* chore(apple): sync native CI metadata
* feat(gateway): session worktree targeting, branch listing, and group catalog
- sessions.create accepts worktreeBaseRef/worktreeName (write scope) and
execNode (admin); worktree binding persists on the session entry as
worktree { id, branch, repoRoot } and projects onto session rows with
execNode so UIs can show checkout/branch/node state
- new worktrees.branches RPC lists local/remote branches (no fetch) for
base-ref pickers; worktrees.remove now reports snapshotError
- sessions.delete reports preserved dirty checkouts as worktreePreserved
- gateway-owned session group catalog in the shared state DB with
sessions.groups.list/put/rename/delete; rename/delete update member
categories server-side without bumping recency; sessions.patch absorbs
ad-hoc categories into the catalog
- group session display names prefer the human chat title (subject or
space #channel) over stored compact tokens
Part of #103431
* fix(gateway): route group category updates through the session accessor and regen protocol models
- bulk member-category rename/clear uses applySessionEntryReplacements
instead of legacy updateSessionStore call sites (session accessor
boundary guard)
- regenerate Swift GatewayModels for the worktrees.branches schemas,
snapshotError, and new sessions.create params
Part of #103431
* fix(gateway): resolvable remote branch refs and workspace-scoped branch listing
- worktrees.branches returns remote-only branches remote-qualified
(origin/feature-a) so every advertised name works as a worktree base ref
- write-scoped worktrees.branches callers are limited to configured agent
workspaces; other host paths require operator.admin, matching the
sessions.create cwd bar
Part of #103431
* fix(gateway): guard worktree name reuse by owner and env-scope group transactions
- managedWorktrees.create rejects a caller-chosen name whose live or
restorable record belongs to a different owner, so write-scoped
sessions.create cannot bind a session into another session's or a
manual checkout
- session group catalog writes run their SQLite transaction on the same
env-scoped handle as their statements, keeping OPENCLAW_STATE_DIR
overrides atomic and away from the default state DB
Part of #103431
* feat(ui): add plugin catalog management
* feat(gateway): add plugins.uninstall and richer plugin catalog metadata
Adds a plugins.uninstall gateway method (operator.admin, control-plane write)
backed by a lock-guarded uninstallManagedPlugin that mirrors the CLI flow:
config cleanup, install-record removal, managed file deletion, and registry
refresh. Bundled plugins stay disable-only. Catalog entries now carry a
manifest-derived category and a removable flag; ClawHub search results expose
download counts and verification tiers.
* feat(ui): redesign plugins page with inventory, store shelves, and cover art
Rebuilds /settings/plugins around three tabs: Installed (category-grouped
inventory with overview stats, state filters, uninstall for external plugins,
and inline MCP server management through the shared config seam), Discover
(featured/official shelves plus one-click MCP connectors and curated ClawHub
searches), and ClawHub (search with download counts and verification badges).
Every catalog entry renders bundled cover art or a deterministic gradient
monogram tile - no more empty boxes. Artwork generated with Codex CLI, shipped
as 512px WebP under ui/public/plugin-art.
* chore(ui): regenerate locale bundles for plugins manager strings
* docs: describe plugins manager tabs, uninstall, and MCP connectors
* fix(plugins): human catalog labels and un-pinned hosted fallback ids
listManagedPlugins now prefers manifest names over registry package-name
backfill, falls back to channel catalog labels and blurbs, and stops pinning
expectedPluginId when a hosted feed entry only exposes its package name
(which rejected every legitimate install of that package). Found via live
gateway testing against ClawHub.
* fix(ui): send minimal RFC 7396 merge patches for MCP server edits
config.patch merges rather than replaces, so key removal needs an explicit
null; sending the full config back made MCP server removal a no-op. Found
via live gateway testing.
* fix(ui): write explicit MCP transports for URL servers
The MCP runtime defaults URL-only servers to SSE, so streamable HTTP
endpoints saved by the add form or connector templates would fail at
connect time. Connector templates now declare their transport and the
add form infers streamable-http unless the URL follows the /sse
convention. Flagged by autoreview against the transport resolver.
* test(ui): wait for deferred plugin requests before resolving in e2e
* feat(ui): plugins detail view, action menus, and unified ClawHub search
Reworks the plugins page from PR #103176 feedback: merges the ClawHub tab into
Discover (typing searches ClawHub inline and appends a quiet From ClawHub
section, with Browse ClawHub demoted to a header text link), makes every row and
store card open a plugin detail overlay (hero art, primary enable/install
action, metadata table), and replaces enable/disable switches with a state chip
plus an overflow menu (Enable/Disable, Remove for external plugins, View
details) matching the ChatGPT-store install+menu pattern. MCP rows use the same
menu; refresh is now icon-only.
* chore(ui): regenerate locale bundles for plugins UI iteration
* feat(ui): vetted, grouped connector catalog for the plugins store
Expands Connect your world to 28 connectors organized into use-case shelves
(Work & productivity, Coding & infrastructure, Home & media, Everyday life).
Every entry passed a three-stage subagent review: official-docs verification
plus live endpoint probes for MCP servers, ClawHub result-quality and
malware/typosquat screening for curated searches, and an adversarial pass
that dynamically registered OAuth clients to prove one-click viability.
That review removed Figma (registration allowlisted, 403) and Atlassian
(OAuth issuer-mismatch bug upstream), downgraded GitHub to PAT-based setup
(no dynamic client registration upstream), fixed Linear (/sse retired) and
Home Assistant (/api/mcp, streamable HTTP) endpoints, retargeted poisoned or
dead searches (youtube, finance, hue dropped; calendar -> google calendar;
stocks replaces finance), and added Todoist, Airtable, Canva, Stripe,
Context7, DeepWiki, Hugging Face one-click MCP servers plus Jira, PDF,
transcription, Kubernetes, Reddit, maps, translation, and notes searches.
Keyless servers get a ready-to-use success message; new cover art included.
* chore(ui): regenerate locale bundles for connector groups
* fix(plugins): suppress hosted catalog rows once their package is installed
Hosted feed entries without a declared runtime id fall back to their package
name as catalog id, which never matches the installed runtime id, so the
Discover shelf kept offering an already-installed package. Installed package
names now also suppress official rows. Flagged by autoreview.
* fix(plugins): pin declared runtime ids and surface connector errors in place
The runtime-id pin now keys off explicitly declared catalog ids (plugin,
channel, or provider) instead of string-comparing against the package name,
so declared ids that equal their package name stay enforced while entry-id
fallbacks stay unpinned. Connector add failures on Discover now render on the
triggering card instead of the Installed tab's MCP section. Both flagged by
autoreview; regression tests included.
* feat(ui): full inventory artwork, pulse header, and two-column plugin list
Every bundled plugin now ships distinctive cover art (113 new Codex CLI
illustrations; 172 total, ~2.1MB WebP), so inventory rows and detail views
never fall back to monogram tiles. The four stat cards give way to a compact
inventory pulse: a segmented enabled/disabled/issues meter whose legend and
counts live inside the filter chips. Inventory, MCP, and search rows flow
into two columns when the panel is wide enough.
* chore(ui): regenerate locale bundles for pulse header
* fix(ui): omit stdio args from the MCP server row target
Stdio MCP args routinely carry tokens, and the inventory is visible to
read-only operators; mirror the config page and show only the command.
Flagged by autoreview; regression test included.
* fix(merge): point crestodian setup at relocated plugin commit/refresh modules
* fix(merge): add bootstrapToken to plugins page test gateway harness
* fix(plugins): name catalog install-action branches so Swift emits the union
* fix(ui): satisfy strict lint on plugins page form parsing and mocks
* chore(build): regen docs map, raise plugin-sdk declaration budget for new protocol surface
* fix(ui): type the plugins page patch mock with its real call signature