* feat(ui): custom session icons with agent-drawable SVG and nav-parity pinned rows
* wip: slice-2 interleaved sidebar zone (pre-review)
* fix(ui): trim icon exports, bypass protocol barrel in startup bundle, regen Swift protocol client
* fix(ui): preserve unknown-agent zone entries on writes, prune entries on any unpin
* fix(ui): reset keeps unloaded session slots; enforce SVG byte cap on canonical form
* fix(ui): archiving a pinned session retires its sidebar zone slot
* fix(ui): persist drag-pinned zone slot only after the pin patch lands
* fix(ui): recompute drop insertion against freshest zone order on pin ack
* fix(ui): consume self-drop events before the zone bailout
* docs(ui): note the non-sidebar unpin pruning contract on pruneSidebarSessionEntry
* refactor(ui): extract pure session-tree projection; drop unused zone type export
* feat(chat): rewind and fork a session from a message bubble
* docs(web): document chat rewind and fork bubble actions
* fix(chat): lint cleanups, protocol regen, and test splits for rewind/fork CI
* fix(ui): fit chat rewind disabled styles inside the startup CSS budget
* feat(mac): show native notification permission in dashboard Notifications settings
The Mac app's embedded Control UI showed the web-push section as
Unsupported/Not subscribed even though the app delivers notifications
natively. The dashboard now installs an openclawNotifications WebKit
bridge (status / request-permission / send-test) and the Notifications
settings page renders native permission state with Enable, Open System
Settings, and Send test actions when hosted in the Mac app. Browsers
keep the existing web-push UI. The notifications section renderer moved
out of the oversized config view module.
* fix(ui): break config view import cycle and satisfy lint/deadcode/i18n gates
Move the notifications section props to a leaf contract (no view.ts
type import), unexport the status-event constant, use bracket access
for the injected dunder global, default-case the status switch, and
refresh the native i18n inventory line offset.
The entry stylesheet aggregated approval, config, config-quick, and
lobster-pet styles even though every consumer is a lazy route chunk or
lazily defined element, and two small UI landings tipped startup CSS to
exactly the 42.0 KiB budget, failing all QA Smoke profiles on main (run
29643357786). Move each stylesheet to its owning lazy module — pages
already own their CSS this way (chat, agents, cron) — and keep only the
shell's pre-hydration approval booting subset in the entry via the new
approval-boot.css. Startup CSS drops 42.0 -> 36.0 KiB gzip; tighten the
budget 42 -> 38 KiB to lock in the recovery, matching the startup-JS
precedent from #110528.
* feat(ui): manage MCP servers directly on the Settings MCP page
* refactor(ui): mcp config section owned by the MCP settings page only
* chore(ui): drop test-only McpServersCard export for knip
* feat(ui): session unarchive flows with undo toast
Archiving from the web UI was silent and one-way. Adds a shared single-slot
toast with Undo (restores archive state, pin, and active selection), a sidebar
'View archived' entry into Settings -> Sessions, an Active|Archived segment
replacing the buried archived-only chip, an inline Restore button on archived
chats' disabled composer, and a guarded 'Delete all archived' bulk action that
fully re-enumerates archived sessions (paginated, aborts on any abnormal page)
and passes the protocol archivedOnly guard. No gateway/protocol changes.
* test(ui): adapt archive undo sidebar harness
* fix(ui): unexport ToastOptions and widen test mock casts for CI gates
* perf(ui): drop zod and lazy-load json5 out of Control UI startup
zod's only UI consumer was the custom-theme shape layer, whose deep CSS
validators already re-check every token; replace the two shallow schemas
with a plain record reader and delete the zod jitless CSP shim + test.
Startup keeps one schema library total (typebox, protocol-owned, already
lazy via the approval page).
json5 now loads through a lazy runtime boundary: strict JSON.parse is the
fast path, the parser warms with the config editor and whenever a config
snapshot or raw draft actually needs JSON5 (comments, trailing commas).
The redaction sanitize path parses the authoritative raw once at snapshot
ingestion (async-safe) and submits against the carried parsed fact, so
secret-placeholder handling never races the lazy parser.
Startup JS: 321.8 -> 295.8 KiB gzip, 13 -> 12 requests; budgets ratchet
to 310 KiB / 18 requests.
* fix(ui): gate config submits on pending JSON5 original parse
A JSON5 config racing the first parser load could reach the sanitize step
with a null parsed original and pass redaction placeholders through.
setConfigRawOriginal now parses synchronously whenever the parser is warm
and tracks a pending promise otherwise; submit, auto-save, and teardown
flush paths defer to that promise (teardown keeps its synchronous prefix
when no parse is pending).
* fix(ui): keep teardown config flush synchronous and JSON5 diff cache non-sticky
Teardown flush must dispatch before unload destroys the context; the
gateway's restore-or-reject sentinel contract backs the rare unsanitized
window. Raw-diff parse failures no longer cache while the lazy JSON5
parser is still loading, so a transient cold-parser miss retries on the
next render instead of pinning an empty diff.
* fix(ui): harden lazy JSON5 boundary against double-submit and failed loads
Claim the config busy flag before awaiting a pending JSON5 original parse
so a second click cannot slip past the busy state (autosave overlap is
already serialized by the in-flight registry and drain discipline). A
rejected json5 chunk import now resets the loader for retry, the per-state
pending promise is never-rejecting and self-clearing, and fire-and-forget
warms swallow rejections.
* docs(ui): note autosave entry serialization at the JSON5 parse await
* fix(ui): fill raw pending-changes diff once the lazy JSON5 parser lands
First diff open could race the parser chunk and render an empty list with
nothing scheduling a retry; renderConfig now re-renders when the warm
completes, and the browser test warms the parser in setup to assert the
steady state the view module guarantees in prod.
* perf(ui): lazy-load the lobster pet out of the Control UI startup graph
Keep the lightweight lobster contract on the sidebar's static path while idle-loading the heavy pet implementation. Move the logo stand-in custom element into the pet module so both decorative elements upgrade together after the lazy import.
* style(ui): format lobster-pet lazy boundary
* fix(ui): clear the lobster-pet chunk cache on load failure
A rejected import stayed cached forever and surfaced as an unhandled
rejection; clearing it lets a later scheduling call retry.
* fix(ui): retry the lobster-pet chunk once connectivity returns
* fix(ui): settings language picker matches the settings design language
* fix(ui): keep Web Awesome available-height bound in language listbox cap
* fix(ui): restore forced-colors focus outlines for the language picker
* 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
* fix(ui): cap sidebar child sessions at four with show-more, drop Subagent prefix in tree
* test(ui): split child-session cap cases under the max-lines budget
* feat(ui): merge voice and video talk into one button with in-call camera toggle
* fix(ui): avoid stale TS narrowing on talk status re-check in camera toggle
* feat(ui): drop the model-auth-expiring sidebar chip
The warning-severity expiring chip nagged for OAuth tokens that roll
continuously; the error-severity expired chip stays and still surfaces
actually broken auth.
* feat(ui): let ledge visits sometimes scare the sidebar logo away
Some idle lobster visits now spook the brand mark: a beat after the crab
settles in, the logo fades out (no stand-in), and it pops back when the
visit ends. Seeded 30% roll per arrival; perch visits, offline summons,
and reduced-motion users are exempt.
* perf(ui): drop typebox from Control UI startup by lazy-loading the approval page
The approval document page was statically registered in app-host, pulling
the protocol validators and the full typebox runtime (schemas, value
checks, formats, locales) into every Control UI boot even though the page
only renders for /approve document URLs.
- register openclaw-approval-page through the existing OptionalCustomElement
seam, preloaded only when bootstrap resolves an approval document mode;
the approval document's booting splash covers the load gap
- ratchet startup budgets to the new baseline (340 KiB gzip, 20 requests)
so the win cannot silently regress
Startup JS: 369.1 -> 321.8 KiB gzip (-13%), 22 -> 13 requests.
* chore(ui): record approval-page label in i18n raw-copy baseline
* perf(ci): drop compact bin cap to 190s to split straggler pairings
Fresh census from green run 29633628994: every fan-out job queues 65s,
the pack sits at ~160s, but compact-large-3 ran 195s because the 235s
cap let core-runtime-media-ui (hint 124) and core-unit-src-security
(hint 95) share one bin. A 190s cap forbids such pairings; the plan goes
from 21 to 25 compact bins and the hinted max drops accordingly, so the
run wall tracks the pack instead of one straggler. Costs ~4 extra
runners per run; wall is the optimization target.
* perf(test): run DOM-free Control UI tests in the node environment
35 ui/src test files whose transitive import closure never touches DOM
APIs or the lit runtime carried the ui config's jsdom default anyway; on
CI the per-file jsdom construction dominates the shard (environment
95.2s vs tests 59.4s in run 29633628994). Annotate them with the
repo-standard @vitest-environment node docblock (29 files in ui/ already
use it). Verified: full ui config run passes with the annotations; the
only failing files are three storage tests that fail identically on a
clean checkout under local Node 26 and are untouched here.
* chore: kick dropped push event for PR head ingest
A gateway in-place update can leave a document whose hashed entry CSS
404s while the app JS still boots, rendering a fully unstyled page that
no existing safety net catches (vite:preloadError only covers dynamic
imports; the index.html mount fallback only checks the custom element).
Add a CSS sentinel (--openclaw-css-ok) checked at document load plus a
capture-phase stylesheet error listener; on detection reuse the existing
stale-chunk auto-reload (one per build id, gateway probe), and fall back
to an inline-styled banner with a Reload button when auto-reload is
unavailable.
* feat(system-agent): hatch into the agent after a fresh setup applies
The hatch is a ceremony: a fresh-install setup apply now announces the hatch
and hands straight off (open-tui in the terminal, open-agent for gateway
clients); the web custodian page lands in agent chat with the birth-sequence
opener prefilled as a draft so one Send wakes the seeded BOOTSTRAP. Non-setup
persistent operations keep their in-place replies.
* fix(system-agent): hatch only on clean post-write verification
* fix(ui): scope hatch draft to setup handoffs
* fix(onboarding): seed hatch handoffs across clients
* fix(i18n): refresh native hatch inventory
* fix(onboarding): seed remote gateway hatch handoff
* fix(macos): localize hatch draft
* fix(onboarding): gate hatch on pending bootstrap
* fix(i18n): sync hatch source to ios catalog
* fix(onboarding): gate hatch on canonical pending state
- 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
* perf(ui): cut forced reflows in chat render path
Profiled the Control UI with Chrome DevTools tracing against a real-data
gateway: session-switch INP was 367ms with ForcedReflow insights on every
load and interaction trace.
- chat-thread: per-row stable Lit ref callbacks (keyed by row key) so the
virtualizer stops cache-sweeping and re-measuring every visible row on
every render; prune callbacks when rows leave the list
- chat-composer: stable textarea ref on per-pane state instead of an
inline arrow, so the textarea is re-measured only on attach or when the
draft changes programmatically, not on every chat render
- app-sidebar: coalesce scrollHeight/scrollTop reads from updated() into
one rAF per frame instead of a forced layout flush per render
After: INP 133-143ms on the same interaction sequence, no ForcedReflow
insight in load or interaction traces.
* perf(ui): keep Control UI startup under budget after reflow fixes
The reflow fixes shifted rolldown's chunk partition: the 400 KiB core
maxSize boundary split one core chunk in two (~1.4 KiB gzip compression
loss) and re-balancing minted a tiny build-info startup chunk, pushing
startup JS to 370.8 KiB over the 370 KiB budget.
- pin build-info.ts + build-info-normalizers.ts into control-ui-shared so
partition noise stops minting extra startup preload requests
- raise core maxSize 400 -> 448 KiB so the core graph packs into fewer,
better-compressing chunks
Startup JS: 22 requests, 369.1 KiB gzip (main: 23 requests, 369.3 KiB).
Web Awesome's wa-dropdown-item ships cursor: pointer on its shadow host,
so every context/dropdown menu showed the hyperlink hand. Document-level
override restores the app chrome convention (arrow for controls, hand
reserved for real links) and re-adds the disabled not-allowed cue.