* feat(ui): live agent activity subtitles on running sidebar sessions
* refactor(ui): lazy-load narration controller off the startup chunk, unexport test-only symbols
* chore(ui): raise startup JS budget to 312 KiB for the lazy narration feature
* test(ui): restore real timers after narration controller cases
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.
* 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): 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
* refactor(ui): add Web Awesome foundation
* refactor(ui): migrate shared menus and chat controls
* refactor(ui): migrate page controls to Web Awesome
* refactor(ui): migrate workboard controls to Web Awesome
* fix(ui): harden Web Awesome interactions
* test(ui): type Web Awesome dropdown state
* fix(ui): preserve menu focus and selection
* fix(ui): restore agent picker focus
* fix(ui): track keyboard menu dismissal
* fix(ui): ignore stale session menu closes
* fix(ui): complete Web Awesome keyboard migration
* fix(ui): repair Web Awesome lockfile graph
* docs(ui): keep release note in PR body
* fix(ui): satisfy Web Awesome migration gates
* perf(ui): keep Web Awesome theme in startup stylesheet
* fix(ui): replace reopened Web Awesome session menus
* fix(ui): keep replacement session menus open