* feat(ui): author avatar chips on user messages with gravatar and initials fallback
Renders a small author avatar on user-authored chat messages, including
queued/pending sends, so multi-user gateways show at a glance who wrote
which prompt. Resolution: profile avatar URL (when available) -> Gravatar
by SHA-256 email hash (d=404) -> deterministic initials with stable color.
Assistant/tool output stays unmarked, and messages without sender identity
render unchanged, keeping single-user gateways visually identical.
* fix(ui): clear avatar fallback state on image load; document gravatar tradeoff
* fix(ui): gate email-hash avatars behind a gateway proxy base URL
Without a same-origin proxy base the email tier is disabled outright, so
rendering a transcript can never disclose hashed sender emails or viewer
IPs to a third-party avatar host. The gateway-side proxy route (follow-up)
supplies the base and becomes the only avatar fetch path.
* fix(ui): restrict profile avatar URLs to same-origin relative paths
* fix(ui): validate profile avatar URLs with the URL parser, not string prefixes
Backslashes normalize to slashes and tabs/newlines are stripped by browser
URL parsing, so startsWith checks can be bypassed into cross-origin fetches.
Resolving against a probe origin and requiring the origin to survive is
parser-equivalent and closes the class.
* 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
Live-append voice transcripts into the agent session and persist a per-agent SQLite call record across relay and client transcript paths.
Add run-scoped spoken confirmation for high-impact actions, mutation digests, bootstrap-context injection, talk.client.transcript and talk.client.close protocol methods, and Control UI adoption. This adds zero new configuration.
Co-authored-by: Clifton King <clifton@users.noreply.github.com>
The copy-as-markdown and open-in-canvas actions under chat messages carried
the global .btn chrome; the light-theme .btn background override outranks the
chat footer's ghost-button reset, so both buttons rendered as large boxed
squares (44px touch targets made them worse on narrow viewports) while the
hide button next to them stayed a small ghost icon. Render them bare in chat
so the footer ghost rules own their look; the boxed variant remains for the
file preview and connect-command surfaces.
* 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