* feat(update): scheduled update campaigns with countdown, dev-channel auto-update, and Updates settings page
* fix(ui): satisfy update campaign deadcode checks
* feat(update): extend scheduled update campaigns
* test(ui): include Updates page in settings sidebar search expectations
* fix(update): end campaigns when apply fails
* fix(update): pin dev campaign applies to the announced commit
* test(ui): align update e2e fixtures
* fix(update): pin adopted dev updates to the announced commit
* docs(update): document automatic dev update campaigns
* fix(ui): gate automatic dev updates on git installs
* fix(gateway): scope detailed update metadata to read-capable clients
* fix(gateway): break post-attach madge cycle with a leaf client-lookup type
* chore(protocol): regenerate clients after rebase
* test(gateway): update method tail after rebase
* fix(update): preserve campaign target and hold state
* test(update): align adoption mocks and split handoff suite
* test(update): route split handoff suites on Windows
* feat(ui): finish cron -> Automations rename in visible strings
* fix(ui): strip persisted pre-rename Cron labels before applying the Automation prefix
Codex review finding: gateway rows persist labels as 'Cron: ...' (and keep
writing them until the strings PR lands), so the renamed typed prefix
double-rendered as 'Automation: Cron: ...'. Legacy label prefixes are now
stripped before prefixing; regression tests cover both stored shapes.
* fix(ui): catalog typed-session prefixes and refresh locale catalogs for the rename
Review follow-ups: typed-session prefixes come from the i18n catalog and
display branching uses machine identity (session kind), not translated text;
remaining English feature labels (config group, settings nav, dreaming intro)
renamed; all 20 locale catalogs refreshed through the owner sync (translated,
strict ui:i18n:check green) with a non-English regression protecting the
renamed keys.
* chore(ui): reconcile locale catalogs after rebase onto main locale refresh
* fix(ui): keep locale bundles workflow-owned; pin the rename at the English source
CI isolates generated locale artifacts from source PRs (preflight changed-scope
gate); the post-merge control-ui-locale-refresh workflow owns retranslation.
This PR ships the English source, catalog prefix keys, and machine-identity
display branching; the regression pins the renamed keys at the source of truth.
* fix(ui): move rename catalog test out of workflow-owned locales dir
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
* refactor(ui): dissolve the General settings page into Appearance
* test(ui): drop dissolved config route expectation
* test(ui): cover legacy General model redirect
* style(ui): format legacy redirect test
* feat(talk): emit realtime models and voices in talk.catalog and mirror create-time readiness
* feat(ui): add curated Talk settings page with catalog-driven pickers
* docs(talk): correct stale claims and add one-page GPT-Live setup path
* fix(ui): refresh Talk catalog on config-hash advance and neutralize GPT-Live badge
* fix(ui): provider-aware Talk selection, atomic provider switch, focus refresh
* fix(ui): resolve Talk provider fallbacks via catalog and make Default a true reset
* fix(ui): provider-compatible transports and race-free Talk catalog loads
* fix(ui): never resolve an unknown explicit Talk provider to the active one
* docs(talk): note Android relay readiness caveat for browser-only models
* fix(ui): keep the relay transport when switching Talk provider to Auto
* fix(ui): align section-ownership test and drop unused export after rebase
* refactor(ui): consolidate model defaults onto the Models page
* docs(ui): note busy-gate equivalence on the Models page controls
* fix(ui): render model behavior controls while providers load
* fix(ui): redirect the legacy General model scroll target to Models
* fix(ui): surface config autosave and apply workflow on the Models page
* refactor(ui): move gateway host panel and connection status to the Gateway page
* test(ui): update e2e expectations for the Gateway settings page move
* feat(ui): let a link's slug settle a shared short-id prefix
Two sessions can share the first block of their uuid, which sent an otherwise
exact link to the disambiguation view even though the slug in that link already
said which session was meant. Capture the slug on short references and use it to
break the tie.
It can only narrow: a slug matching none or several of the tied sessions leaves
the chooser alone, and a reference that already resolves uniquely is untouched.
Generated links stay at their normal length instead of growing a full uuid.
* fix(ui): never settle a slug tie from an incomplete search
A truncated candidate set is an unfinished search, not a tie: an unexamined page
could hold the same short-id prefix under the same slug. Settling there would be
the guess the bounded search exists to avoid, so keep the chooser.
* feat(ui): durable session board face and dashboards index
Board face lived only in client-side boardSessionViews, capped at 50 entries,
so the preference never followed the user to another device, evicted as
sessions accumulated, and could not be seen as a set.
Persist it as SessionEntry.boardFace, which rides the existing entry_json blob
and so needs no SQLite schema change or version bump. Expose it on the session
list row and add it to the sessions.patch write-scope allowlist alongside label,
pinned, and archived: setting your own view preference is user-level chat
organization, not policy. Unknown patch fields still fail closed to
operator.admin.
Generic navigation now reads the stored face, so the sidebar and session list
open a thread on the face you left it on. boardSessionViews keeps only
activeTabId and reopenDockByTab, which are genuinely per-device.
Add /dashboards listing threads whose preferred face is dashboard. Filtering
runs server-side in filterSessionEntries before pagination, because the client
holds only a capped page and a client-side filter would silently omit
dashboards.
* test(protocol): assert the pre-rename face param is rejected
The gateway-protocol validator test still passed the pre-rename 'face' key,
which the closed schema rejects. Use boardFace, and pin the old name as a
negative case so it cannot silently return.
* chore(protocol): regenerate Swift bindings and docs map for boardFace
Adding boardFace to the sessions schema changes two committed generated
artifacts: the Swift gateway models (pnpm protocol:gen:swift) and the docs map
(pnpm docs:map:gen), which now lists the dashboards index section.
* feat(ui): add a Memory settings page with Dreaming as a tab
Memory config was scattered across five surfaces: the memory.* schema section
lived on AI & Agents with 43 of 51 keys behind the Advanced tier, the memory
slot owner was only visible on Plugins, dreaming's knobs were JSON-only, its
status UI sat under Agents, and Memory Import was a separate route.
/settings/memory now owns that surface, following the MCP page shape (curated
rows above an embedded schema editor):
- Overview: the exclusive memory slot rendered as a segmented control over
installed memory-kind plugins, memory.backend promoted out of Advanced with
the qmd sub-config revealed only when qmd is selected, additive add-on rows,
and a Memory Import link.
- Search: the memory.search surface via the embedded editor.
- Dreaming: the global frequency/model/timezone/storage/phase knobs, which
previously required hand-editing openclaw.json, plus an agent picker feeding
the existing dream scene/diary/advanced panel for the agent-scoped reads.
Engine selection calls plugins.setEnabled so the gateway's exclusive slot
policy stays the single owner instead of being duplicated in the UI.
* fix(ui): redirect stale ai-agents memory deep links to the memory page
* fix(ui): report memory runtime defaults on the Memory page
The Dreaming tab rendered its own defaults instead of the ones
resolveMemoryDreamingConfig applies, so a config carrying only
dreaming.enabled showed all three phases off while they were running, and
an unset storage mode read as inline instead of separate. Toggle specs now
carry the runtime fallback and the storage default is stated once, both
pointing at src/memory-host-sdk/dreaming.ts.
Three more surfaces asserted things the runtime does not do:
- plugins.slots.memory "none" is the explicit-off sentinel, not an engine
id, so the segmented control selected nothing. The slot now resolves to a
closed auto/off/pinned selection with its own hint.
- memory.backend is resolved by the memory runtime the slot owner
registers, which only memory-core ships, so the row is hidden for any
other engine instead of saving a value nothing reads.
- The Dreaming tab wrote config.dreaming for whichever plugin owns the
slot even when that plugin's schema cannot hold it. It now reuses the
enablement flow's schema check (resolveDreamingConfigPathSupport, shared
with updateDreamingEnabled) and renders an unsupported state instead.
Also key the plugin-catalog sync on the connected phase: the connecting ->
connected transition keeps the same client object, so a page mounted
during the handshake never loaded the catalog and never showed the engine
picker.
The tab keeps the autosave status line and restart banner the embedded
editor renders on the other tabs; these knobs autosave, but nothing
reported it. The pure view moved to memory-dreaming.ts with the element in
memory-dreaming-page.ts, matching memory.ts/memory-page.ts.
* fix(ui): resolve the memory slot through the canonical policy
The Memory page re-derived plugins.slots.memory instead of using the rule the
runtime applies, which broke both directions of the engine control:
- An unset slot was reported as "the first enabled memory-kind plugin in the
catalog". The runtime resolves it to the slot's default owner
(DEFAULT_SLOT_BY_KEY.memory), so the page could show one engine as active
while another was loaded, reveal or hide the backend row for the wrong
plugin, and target the wrong plugin when switching memory off.
- Off called plugins.setEnabled(false), which writes enablement only. The slot
stayed pinned, so the choice did not survive a refresh and re-enabling that
plugin from the Plugins page silently switched memory back on.
resolveSlotSelection now lives next to defaultSlotIdForKey in
src/plugins/slots.ts and owns the rule once; config normalization consumes it
and the page imports it instead of restating it. Off writes the explicit "none"
sentinel through the config form, so it round-trips; picking an engine still
goes through plugins.setEnabled, which is where the exclusive slot policy
lives. The dreaming controller's own copy of the rule is gone too.
Four smaller fixes on the same surface:
- A failed engine change is reported next to the control instead of being
swallowed, so the selector no longer just snaps back.
- Dreaming's numeric inputs carry the memory-core manifest's integer/min/max
bounds and refuse out-of-range edits at the field, rather than patching a
value autosave then fails to write.
- Settings search destinations carry the Memory tab that renders the matched
child, so a memory.search hit no longer lands on Overview, whose narrowed
editor omits it.
- The Dreaming tab caches only a definitive schema-capability answer. An
offline or failed lookup now reports "unknown" and is retried on reconnect
instead of permanently suppressing the recheck.
* fix(ui): model unknown memory state instead of collapsing it
The Memory page reported unknowns as decided values. An empty catalog meant
loading, disconnected, or a failed plugins.list, yet add-on rows rendered
"Disabled"; catalog completions were keyed on client identity, which survives a
phase flip, so a stale load could repopulate a disconnected page or overwrite a
newer read; and `?tab=` was adopted once per distinct value, so a repeat
navigation to a tab the user had left was ignored.
Replace the ad-hoc nullable fields with closed shapes. MemoryCatalog is a
loading/unavailable/ready union, so absence of an entry only decides anything
inside `ready`, and MemoryAddonRow carries a four-state enablement the view
renders without ever inventing an "off". CatalogConnection is one object per
(client, connected) transition and doubles as the request generation an
in-flight load carries, so obsolete completions are dropped by identity. The tab
is no longer page state at all: the URL owns it, tab clicks navigate, and every
arrival is honored.
Settings search now resolves the engine/backend through the same
resolveMemoryBackend the page uses and matches only the `memory.*` children the
page can surface, so a `memory.qmd` hit under the built-in backend no longer
routes to an Overview whose editor omits it.
* fix(ui): surface a disabled memory owner and anchor curated backend search
The slot and plugin enablement are independent config surfaces, so
`plugins.slots.memory` can name a plugin the catalog reports as disabled.
The engine control showed that plugin as selected, and because re-picking an
already-selected radio fires no change event, there was no way back on. Add an
explicit enable row for that state and let the same-id write through when the
owner is not running; picking Off stays a no-op.
`memory.backend` is curated out of the schema editor, so the generic
`#config-section-memory` anchor scrolled past it. Fold the memory tab and hash
choice into one `memoryDestination` owner that routes a curated-only match to
the new anchor above the editor.
* fix(ui): scope the dreaming capability probe to its connection
The probe was deduplicated by plugin id alone, which cannot tell a current
answer from a stale one. A disconnect and reconnect on the same slot owner left
the token armed, so the reconnect read as "already in flight" and swallowed the
retry that an `unknown` answer requires — leaving an unsupported engine's knobs
editable until some unrelated config notification arrived. An A -> B -> A switch
had the mirror problem: the old A response was accepted for the new A probe.
Make the in-flight probe an object whose identity is the generation, drop it
whenever the owner or the connection changes, and accept only the completion
that still owns the slot. Same shape as the catalog guard on the Memory page.
* fix(ui): satisfy the lint and dead-export gates on the memory page
Exhaustive switches need a terminal `default:` to satisfy
typescript/consistent-return, matching the existing view-status.ts shape.
Seven symbols were exported with no production consumer outside their own
module, which the hard-zero Knip production scan rejects. Tests alone do not
make internals contracts, so drop the exports and reach the behavior through
each module's public surface instead: the view props type comes from
`Parameters<typeof renderMemory>`, the tab panel is found by its ARIA role, and
the dreaming number/storage helpers are proven through `renderDreamingSettings`.
Folding those helper unit tests into the render path also corrected one of them:
a `type="number"` input coerces unparseable text to empty, so the "reject
garbage" case was unreachable through the real control. Replaced with the
inclusive-bound and clear-the-field cases, which are reachable.
* refactor(ui): keep the memory schema facts out of the startup bundle
Settings pages are already lazy — the config route is `import("./config-page.ts")`
— but settings search runs from app-host at startup, and it needed the same
answers about which `memory.*` children are reachable and where a match lives.
Importing those from the view module dragged lit, hub-tabs, and settings-ui into
the startup chunk with it, blowing the Control UI startup budget.
Move the rendering-free facts (slot/backend resolution, tab and curated key
lists, schema narrowing, the anchor id) into memory-schema.ts, which imports
only record-coerce and the shared slot policy. The view keeps the templates and
now consumes the same module, so there is still one owner per fact.
* chore(ui): record the memory settings surface in the startup budget baseline
Routing settings search through memory-schema.ts instead of the view module
recovered 10,872 B of the startup chunk (334,992 -> 324,120 B), which is back
under the 324,608 B ceiling. The remaining 2,795 B over the old baseline is the
honest cost of the new surface: its i18n strings, plus the slot/backend facts
the startup search index has to read.
Measured by hosted CI (run 30189972795); this worktree cannot build locally
because pnpm wants to purge a node_modules shared with other running agents.
* 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
* feat(ui): repair settings titles, scope form-unsafe banner, consolidate advanced tier
- Advanced/Notifications settings headers rendered raw i18n keys
(tabs.advanced/tabs.notifications); configPageTitle now resolves through
the nav registry titleForRoute, deleting the drifting key map.
- Form-unsafe banner is value-aware (only when the user's config has a value
at a form-unsupported path in the active scope), names the paths, offers an
Open Raw editor action, and sits in the content column instead of full-bleed.
- Advanced tier collapses to one synced toggle: hidden advanced fields render
a ghost row that enables the toggle; the Advanced page always reveals and
hides the toggle; per-section details state and the controlled-open dance
are deleted; toggle visibility mirrors the renderer's tier split.
- schema.tags no longer tags facet-less paths as "advanced" (common fields
like update.channel wore a lying chip); the chip is no longer rendered in
form rows while tag:advanced search keeps working.
* fix(config): dev bootstrap writes canonical agents.entries; finish stale agents.list sweep
- openclaw gateway run --dev failed on a fresh state dir: the bootstrap wrote
the retired agents.list array and validation rejected its own config
(Unrecognized key: list). It now writes the keyed agents.entries record;
new dev.test.ts validates the written shape against the zod schema.
- Revive the dead gateway hot-reload rule: prefix agents.list never matched
canonical config diffs, so per-agent heartbeat edits fell through to the
agents:none tail rule; the rule now matches agents.entries.
- config set replacement protection moves from the dead agents.list array
to the agents.entries map (joins plugins.entries/auth.profiles family).
- Sweep remaining stale agents.list config-path strings in hints, fix-it
messages, elevated gates, doctor hints, audit text, and type docs to
agents.entries.*; RPC ids, the internal list projection, and doctor
legacy-migration references intentionally keep the old name.
* test(ui): derive nav i18n audit from route registry; fix stale bootstrap hint assertion
- Knip flagged navigationCopyEntries (test-only export); the audit now walks
ALL_ROUTES through prod titleForRoute/subtitleForRoute and rejects raw
dotted-key output, so no export exists solely for the test.
- commands-context-report expected the retired agents.list[] hint wording.
* fix(config): align rebase resolution with main's roster-aware diagnostic
- tool-policy-diagnostic: keep main's test expectations (dotted
agents.entries.<id> paths from the roster-aware implementation); our
pre-rebase bracket-style assertions no longer match any code.
- Reapply the placeholder sweep the conflict resolution dropped:
generic agents.list[] fallbacks in tool-policy-diagnostic and the audit
sandbox-mode hint now say agents.entries.*.
* feat(ui): mirror active destination and connection state in the dashboard title
* feat(ui): title agent main chats with the agent name
* test(ui): split document-title shell tests to satisfy max-lines
* feat(ui): finish the chat session → thread rename across the Control UI
Completes the #110933 creation-surface rename mechanically: Threads page
(title, filters, archive/delete/rename/pin menus, transcript search),
sidebar labels and menus, welcome suggestion, palette category, tools
availability copy, workboard lifecycle/card strings, dreaming history,
board/diff/workspace-rail chrome, and the untitled main/channel fallback
names. Technical identifiers stay: Session ID, session keys, terminal
sessions, protocol wording. Living docs updated (control-ui.md);
historical release notes left as shipped.
* style(ui): format session-management e2e
* fix(ui): finish thread rename in navigation labels and stale test literals
Codex review caught the palette Threads entry and the Worktrees owner
column still saying Session; CI caught test literals asserting the old
copy (Threads page overview tile, filter tooltips, group counts, catalog
terminal notice, create-rejection error, workboard lifecycle hint). Also
completes the usage-stat cluster (Threads + hints) and the tool-catalog
group label. Native app strings (iOS/Android/macOS) keep session wording
and reconcile through their own i18n pipelines as a follow-up.
* fix(gateway): emit config.changed from the reload committer for every accepted write
Agent config_set, CLI, and doctor writes reach the gateway through the file
watcher's reload path, which never broadcast config.changed - only direct RPC
writes did. Move the broadcast to a single onConfigCandidateCommitted hook in
the reload committer so all writers notify connected UIs, including
runtime-skipped commits (writer-intent echo suppression, reload mode off),
and delete the duplicate RPC-side broadcasts.
* feat(ui): sync chat follow-up and commentary prefs across devices
Add chatPersistCommentary and chatFollowUpMode to the ui.prefs sync surface
(schema, types, docs) with clearable null removal for the follow-up override.
Rewrite server-prefs around a descriptor table so a synced pref is one entry
instead of five hand-written code sites. Caption synced rows/sections
(chat prefs, theme, language) and refresh the stale Settings subtitle.
* refactor(ui): dedupe settings constants, drop dead composer branch, refresh copy and docs
Derive the Security tool-profile options from PROFILE_OPTIONS and General's
thinking levels from BASE_THINKING_LEVELS instead of parallel literals.
Remove the unused composer mode:"model" branch, the orphaned
.config-view-toggle CSS family, and stale quick-settings wording. Add static
settings-search entries for the Sessions and Managed Worktrees pages plus a
provenance-row test, and update docs/cli/openclaw.md to describe the current
system-agent config-write policy (denylist roots, route-backing plugin guard,
per-agent routing escalations).
* chore: refresh plugin SDK API baseline after rebase onto current main
- gateway broadcasts a hash-only config.changed event after every
persisted config write (operator.read scoped); the Control UI refreshes
its snapshot on the event so agent-approved ui.prefs changes apply live
on every connected client (skipped while a local draft is dirty)
- the chat composer's model picker shows provenance — 'Using default
from Settings' vs 'Session override' with an icon reset back to the
default — mirroring the existing reasoning-row anatomy
- the AI & Agents settings page is renamed Agent Defaults to separate it
from the per-agent Agents manager
- Memory Import renders through the settings design language (sections,
rows, toggle, status badges); only the collection review list, apply
report, skeleton, and confirm dialog keep custom markup, and the dead
bespoke CSS is pruned
* refactor(ui): restructure Settings IA around user-facing pages
Settings sidebar shrinks to settings-only destinations grouped by attention:
General/Appearance/Notifications up top, Connections, Agents & Tools,
Privacy & Security, System. Sessions, Worktrees, Activity, and Memory
Import leave the takeover: Sessions/Activity return to the workspace
sidebar, Worktrees becomes a Sessions hub tab, Memory Import is reached
from the Agents memory panel. Model Setup becomes a Models subpage.
New config pages: Notifications (web push), Privacy & Security (curated
policy rows + security/approvals schema sections), Advanced (uncurated
schema sections + raw editor, replacing the General quick/advanced
toggle). General slims to model defaults, language, and gateway host;
identity moves to Profile, lobster appearance rows to Appearance.
* feat(agents): widen system-agent config writes to operator parity
Every config surface the Control UI can edit is now agent-writable behind
the exact-operation human approval gate, except a small documented
denylist ($include, auth, env, models, secrets). tools.* and
plugins.entries.* writes are allowed; plugin_uninstall works for plugins
that provably do not back the active default inference route (fail closed
otherwise); set_default_model takes an optional agentId and live-tests a
per-agent model with the same rigor as the default route. Per-agent
routing fields on non-default agents are direct-writable; the default
agent's route keeps the verified set_default_model/onboard paths. A new
config-write-parity contract test forces explicit classification of every
top-level config key.
* feat(ui): sync operator display prefs through config ui.prefs
Theme, theme mode, text scale, locale, and chat display prefs get a
canonical server-side home in config ui.prefs. The Control UI mirrors
them into localStorage for instant boot, writes local changes through to
the gateway (best-effort; viewer-scope or offline clients stay
device-local), and applies server-side deltas on connect and on every
config snapshot reload — so an agent changing the theme through the
approval gate takes effect in the UI, and prefs follow the operator
across devices.
* chore: prune quick.ts from max-lines baseline after slim-down
* style: oxfmt changed files
* chore: regenerate plugin-sdk api baseline for ui.prefs config surface
* fix: dedupe runPluginUninstall dep type and narrow locale extraction
* docs: update Control UI settings structure and ui.prefs sync
* fix: address autoreview findings on parity guards and prefs sync
- plugins.entries.* config writes get the same active-route ownership
check as plugin_uninstall (fail closed when unprovable)
- system-agent prompt now describes the guarded write policy instead of
the old blanket refusals
- per-agent set_default_model logs the agent-scoped label
- legacy /config?section=... deep links redirect to the Advanced page
- prefs sync: pushes coalesce and drain serially (no CAS races dropping
updates), replaced config hashes mark pre-patch snapshots as stale, and
the last-seen server value persists per gateway scope so reloads cannot
revert unpushable viewer-scope edits
- lint: theme membership checks use Sets
* fix: harden uninstall route guard and scope prefs queue per gateway
- plugin uninstall ownership now covers execution owners (runtime policy,
embedded harness override) in addition to the provider, and re-verifies
inside the commit boundary so a concurrent route switch cannot slip a
destructive uninstall past the pre-approval check
- prefs push queue binds to one gateway client; switching gateways drops
the old queue (device-local fallback) instead of cross-writing prefs
* style: top-level type import in config-page test
* fix: close review races in config-write guards and prefs reconciliation
- config_set/config_set_ref re-verify the inference-route guard inside
the commit boundary so conditional verdicts (per-agent routing, plugin
entries) cannot be flipped by a concurrent edit after approval
- server prefs apply per changed field, so a server change to one pref
no longer reverts unpushable local edits of the others
- replaced-hash staleness retires once post-patch state is observed, so
a genuine restore of the pre-patch config becomes authoritative again
- moved-section deep links (communications notifications, automation
approvals) redirect to their new pages
* style: satisfy no-useless-assignment and loop-condition lints in prefs sync
* fix(ci): resolve unhandled profile-test rejections and knip unused exports
The profile page's new identity section reads context.config; the test
context mock now provides it, eliminating the unhandled rejections that
failed checks-ui and the compact-large shard. Exports flagged by the
dependency gate (extract/patch helpers, section-key and prop types) go
module-private; extraction coverage moves behind applyServerUiPrefs.
* refactor(agents): move config-write policy to its own module
The static path classifier and documented denylist move to
config-write-policy.ts, shared by the execution guard and the parity
contract test — no test-only exports left for the dependency gate to
flag. Blocked-root refusals now cite the denylist's documented
escalation for that root.
* feat(ui): add permanent OpenClaw navigation
* refactor(ui): drive Exit setup visibility with a typed onboarding property
The route view previously hid the button with an injected style tag (the page
file was owned by a parallel lane); render it conditionally instead.
* test(ui): use the navigation route-load cause literal
* fix(ui): request the caretaker greeting for permanent custodian visits
The onboarding welcome variant seeds the first-run setup proposal; the pinned
sidebar and Settings entries now omit it so post-setup visits get the normal
caretaker greeting, driven by the same route onboarding flag as the chrome.
* fix(ui): restart custodian session on mode change
* feat(ui): add custodian onboarding surface
* fix(ui): reset custodian session when credentials change on the same gateway
* fix(ui): include bootstrap token in custodian session ownership key
* fix(ui): no replay for ambiguous user turns; send sensitive input verbatim
* fix(ui): harden custodian session and route state
* fix(ui): keep device-token session scope stable across transient drops
* fix(ui): detect gateway scope errors structurally, not by class identity
Under isolate:false a custom element registered by an earlier test file keeps
its own module registry, so instanceof GatewayRequestError diverges while the
error shape stays stable; the branch's new custodian test files reshuffled
worker packing and exposed this in usage-page.test.ts.
Adds the sidebar approval attention chip and a dedicated approval history
page backed by the new approval.history gateway RPC (30-day retention
window). Extracts buildSidebarAttentionItems into its own module so the Lit
component consumes it as a real cross-module dependency, and wires the strict
i18n catalog for the new strings (fallbacks=0).
* feat(ui): add guided Model Setup page for inference onboarding
* fix(ui): regenerate i18n locale bundles and narrow model-setup exports for CI gates
* chore(ui): regenerate i18n baselines after rebase
* chore(ui): translate model-setup strings for all locales
* feat(ui): move Devices page into settings and redesign device inventory
- Devices now lives at /settings/devices (System group); /nodes stays as alias
- per-device form-factor icons (phone/browser/terminal/machine) with status dot
- equal-height header actions; stale cleanup and pairing only
- drop manual Refresh button; page already auto-refreshes via presence events + 30s poll
- dynamic connected/pending summary replaces static card subtitle
* style(ui): oxfmt pass + loading empty state for devices inventory
* chore(ui): sync locale bundles for devices settings copy; docs point at Settings → Devices
* refactor(ui): split device tile + pending rows out of view-inventory; drop unused icons
Keeps the TypeScript LOC ratchet green: view-inventory.ts returns below its
baseline and icons.ts sheds unused folderOpen/micOff/volumeOff glyphs.
* fix(ui): ratchet LOC baseline, drop unused DeviceIconSource export, settle devices settings copy
- baseline: icons.ts 693, view-inventory.ts back under the 500 ceiling
- subtitles.nodes now describes the settings page; nav tests updated
- locale bundles resynced (fallbacks=0)
* refactor(ui): rename token row param for reviewer-tool clarity
* fix(ui): collapse device rows in narrow containers and classify the TUI as a terminal client
Review findings: the nodes-entry grid override beat the shared 560px
list-item collapse, overflowing phone-width rows; openclaw-tui connects
with mode ui so only its client id marks it as a terminal.
* chore(ui): translate pending locale keys after rebase (fallbacks=0)
Sessions is session management, not a day-to-day workspace destination.
It now lives in Settings under Agents & Tools at /settings/sessions,
with the old /sessions path kept as a route alias; stale pinned-route
prefs drop automatically via normalizeSidebarPinnedRoutes. Also repairs
the sidebar-customization e2e that #105777 broke on main by opening
Settings through the agent-chip menu.
Rebuilds the Control UI cron page as a sidebar-first Automations master-detail workspace: a task list pane (All/Active/Paused tabs, search, filter popover, curated emoji suggestion cards, scheduler footer) beside a detail pane that edits the selected task inline (prompt, row-style Details/Frequency groups, collapsible Advanced, per-task run history) and shows all-task recent activity when nothing is selected. Create & run now kicks a new task immediately after creation. The modal editor and quick-create wizard are retired; the automation-ideas catalog from #104761 is ported into the new suggestions list. Nav item renamed to Automations and pinned by default; route (/cron) and cron.* RPC contracts unchanged.
Closes#104217
Co-authored-by: Claude <noreply@anthropic.com>
* feat(ui): fold Skills and Skill Workshop into a Plugins hub
One sidebar entry (Plugins) now covers plugins, skills, and skill-workshop
routes via a shared hub tab strip (Installed / Discover / Skills / Workshop).
Routes and URLs are unchanged; /settings/plugins gains ?tab= deep links and
the sidebar highlights Plugins for all hub routes.
Also latches Skill Workshop proposal loading so updated() no longer re-kicks
loads while one is pending or after one fails - the early-return finally
used to spin the page at 100% CPU whenever skills.proposals.list stalled.
* feat(mock): plugin catalog and skill-workshop fixtures for the mock harness
* docs(web): describe the Plugins hub tabs in the Control UI guide
* test(ui): backfill initialTab in plugins route-data fixtures
* fix(ui): review fixes for the plugins hub
- plugins route declares loaderDeps on the ?tab= param so query-only
navigation and back/forward re-run the loader instead of reusing the
cached match with the previous tab
- catalog tab clicks navigate so the URL and history stay in sync with
the documented ?tab=discover deep link (local switch stays instant)
- skill-workshop error banner gains a Try again button so a transient
proposals-list failure is recoverable without remounting the route
* fix(ui): restore the Installed tab on bare plugins URLs in history navigation
* fix(ui): manual-activation hub tablist with cross-route focus hand-off
Arrow keys only move focus now; activation stays on click/Enter so arrowing
can never unmount the strip under the user's focus. Keyboard activation of a
cross-route tab hands focus to the destination strip's active tab, and the
hub-route set uses a Set per lint.
* fix(ui): defer hub-tab focus reclaim until the strip is connected
* fix(ui): skip focus recovery for same-tab hub activation
* docs(css): note the hub-panel flex ownership for the workshop board
* fix(ui): adapt hub test to retired Overview route
* chore(i18n): sync locale bundles for the plugins hub keys
* docs(map): regenerate for the plugins hub section
* feat(ui): replace Overview page with Connection settings and sidebar attention chips
* feat(ui): open new-session drafts on the chat start screen hero
* refactor(ui): drop old overview-hints paths after rename
* chore(i18n): sync control-ui locale bundles for connection/palette/attention keys
* test(ui): expect attention slot above the sidebar update card
* chore(i18n): re-sync locale bundles after rebase onto main
* test(ui): stub sidebar-attention RPCs in app-sidebar unit tests
* test(ui): use a non-secret-shaped token fixture in connection view test
* refactor(ui): destructure gateway connection in connection settings draft
* refactor(ui): keep connection settings code out of secret-scanner shapes
* test(ui): expect Connection in the settings Connections group
* improve(ui): idle-refresh sidebar attention chips for always-visible windows
* chore(i18n): refresh raw-copy baseline after rebase
* docs(ui): note the unknown-route chat fallback covers retired paths
* refactor(ui): move dreams page modules to agents/memory (verbatim)
* refactor(ui): sidebar IA cleanup: Activity to Settings, Dreams into Agents Memory tab, single MCP mutation owner, Usage icon
Folds the per-agent Dreams page into the Agents page as a Memory panel and
deletes the dreams route; moves the session-scoped Activity page out of the
sidebar into Settings > System next to Logs; removes the MCP enable/disable
toggle from Settings > MCP so the Plugins page is the single mutation surface
for config.mcp.servers; gives Usage a coins icon so it no longer collides
with Overview.
Closes#104590
* docs: update Control UI docs for Activity/Memory/MCP sidebar changes
* fix(scripts): fail control-ui i18n sync closed on new untranslated keys without provider auth
An unauthenticated ui:i18n:sync silently recorded English fallbacks, which the
shipped-fallback CI gate then rejects; post-merge translation is owned by the
control-ui-locale-refresh workflow. Sync now errors on new untranslated keys
without a provider (OPENCLAW_CONTROL_UI_I18N_AUTH_OPTIONAL=1 opts back in),
and the ui AGENTS guide documents the commit-en.ts-only contributor flow.
* chore(ui): translate new sidebar keys, refresh i18n baseline, localize Memory tab expectation