* feat(ui): call chat sessions threads and quiet the sidebar sections
Thread naming on every creation surface (new-thread page, draft row,
+ tooltips, untitled-thread fallback, New Chat guard toasts). The draft
placeholder now renders as the first item inside the Threads section and
genuinely expands a collapsed section so the header toggle stays honest.
Empty sidebar sections no longer render (Coding keeps its CLI-catalog
exception); expanded section headers reveal their chevron only on
hover/focus while collapsed headers keep chevron + count. The new-thread
button moves from the macOS titlebar strip into the sidebar brand row
next to the agent picker (bigger, offline-aware); the titlebar + renders
only while the rail is collapsed. Mobile drawer shows the new + as well.
* fix(ui): keep empty custom groups and the mid-drag Threads drop target
Empty-section hiding narrows to the Threads zone at rest: user-created
categories stay visible when empty (New group… creation and drag-into-group
flows depend on them), and an empty Threads section reappears while a
session drag is in flight so drag-to-unpin keeps its drop target.
* test(ui): drag-to-unpin targets the sessions surface
The empty Threads section only materializes mid-drag, so Playwright cannot
pre-resolve it as a drop target; the sessions container's drop handler owns
the unpin and sends {pinned:false} without a category reset.
* refactor(ui): split lobster-pet into look/plans/element modules
Move existing behavior along appearance and seeded visit-planning ownership seams without changing runtime behavior.
Extract only the bounded logo stand-in, WebAudio chirp, and pure render-template helpers while keeping the state machine in the element. Remove the grandfathered max-lines suppression and baseline entry.
* style(ui): format lobster-pet split modules
* fix(ui): keep lobster split internals unexported, refresh i18n raw-copy baseline
knip flags the split's newly exported internals (strangerLookFor,
renderCrabSvg, lobsterPetSpriteStyle, LobsterPasserKind) — they have no
external consumers; the html-text raw-copy baseline is file-keyed, so the
sprite z/Z markup moving into lobster-pet-look.ts needs a regen.
* fix(ui): satisfy lint for the lobster-pet split (param reassign, max-lines)
* fix(ui): keep moved act-profile internals unexported
* feat(ui): camera-off glyph and remembered camera state for talk calls
* test(ui): rename shadowed lit html binding in composer icon mock
* refactor(ui): split tool icons out of icons.ts to stay under max-lines
* test(ui): shim dead Node 25+ WebStorage localStorage in jsdom test setup
Node 25+ ships a default-on global localStorage that is dead without
--localstorage-file (undefined on 26.5; throwing or inert elsewhere). It
shadows jsdom's Storage during vitest global population, so storage-touching
UI tests crash on newer local Node while Linux CI (Node 24) passes — six
files failed locally on Node 26.5. The shared setup now capability-probes
(round-trip, throw-safe), prefers jsdom's own window Storage when only the
global alias is dead, and otherwise installs one in-memory Storage on both
window and globalThis.
Also documents the PR-open CI drop pattern in AGENTS.md: fresh PRs race
GitHub's merge-ref computation and the open-event CI run can drop or die
as startup_failure/BuildFailed (not rerunnable); verify attach and
close/reopen to re-fire.
* fix(test): drop unnecessary String() conversions in localStorage shim
* 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