* feat(ui): add composer microphone picker
* fix(ui): single check indicator in mic picker, matching house radio-menu pattern
* fix(ui): keep System default focusable during mic discovery; update composer contract test for picker caret
* 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.
* feat(webchat): reply-to a message with hydrated reply context
Control UI replies now carry the target transcript id as replyToId on
chat.send. The Gateway resolves the replied-to message from session
history and hydrates the channel-agnostic ReplyToId/ReplyToBody/
ReplyToSender envelope fields, so agents receive reply_to_id,
has_reply_context, and the untrusted reply-target block exactly like
Discord replies (mirrors #90263). Reply targets without a persisted
transcript id keep the inline-quote fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(webchat): keep non-reply chat.send dispatch ordering and satisfy CI gates
* docs(webchat): match reply-context doc to webchat conversation-info policy
* fix(webchat): hydrate reply bodies from display-visible content only
---------
Co-authored-by: openclaw-clawsweeper[bot] <openclaw-clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
Session-row titles run to the sidebar edge instead of reserving the hover
action cluster's width in-flow. The trailing aside overlays the row
(pointer-events off at rest so link clicks pass through) and the link
content fades out beneath the surfaced controls via a mask, which stays
correct over rest/hover/active/selected backgrounds. Rows with a child
toggle keep it rightmost; actions land at the link edge either way. Child
rows keep the in-flow aside since their runtime trail is resting content.
Touch keeps actions visible, so the fade is permanent there.
* 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