* feat(ui): show waiting approval run status
* fix(ui): hydrate waiting approval status
* fix(ui): correlate approval status by run
* fix(ui): harden approval status reconciliation
* fix(ui): keep steered composer messages visible until the transcript owns them
Typing while the agent was busy showed a brief steering state and then the
message vanished until the next full history reload. The steer path removed
the queued row at ack time and only restored the steered chip when the tab
had adopted the active run id, which never happens for runs started by
automations, other clients, or tabs opened mid-run.
- steer sends reuse the durable row's sendRunId as the wire idempotencyKey so
delivered-turn and history-proof reconciliation can correlate steered rows
- the steered chip is restored after every accepted ack, keyed to the active
run when the tab still tracks it, else to the steer's own gateway lifecycle
- terminal events materialize acknowledged steered chips into the transcript
before clearing them, and stale history reloads keep idempotency-marked
local turns until authoritative history catches up
- in-flight (unacknowledged) steers are never materialized, so a rejected
send cannot leave a phantom user turn behind
Fix authored with Codex; reviewed and hardened via structured review.
* fix(ui): use bracket access for __openclaw test marker (no-underscore-dangle)
* fix(ui): resolve steered attachment payloads through the store before materializing
Queue rows carry attachment metadata only; composer upload bytes live in the
payload store. Resolving through getChatAttachmentDataUrl keeps attachment-only
steers visible when their chip retires (Codex connector review finding).
* fix(ui): only user-role entries satisfy queued-turn presence checks
An assistant entry can carry the same run's idempotency key; matching it let
the chip re-add and terminal materialization skip the user's copy
(ClawSweeper rank-up move).
* fix(ui): show attributed sender identity properly in chat
Three defects around multi-user sender attribution:
- Sender labels rendered as 'name (<profile uuid>)': resolveSenderLabel
appended the id part even when it is an opaque UUID. Suppress UUID
suffixes server-side and strip already-baked ones in the UI normalizer
so historical transcripts read clean too.
- Attributed messages showed initials instead of the author's avatar:
chat sender metadata carries only the profile id, so resolveAvatar now
derives the canonical /api/users/<id>/avatar route (upload -> gateway
Gravatar proxy -> initials) from UUID-shaped sender ids.
- Author identity was hover-only: identity-resolving gateways keep the
always-visible avatar gutter even in 1:1-shaped sessions, and the
gutter renders the message author's avatar (with identity-colored
initials fallback) instead of the local viewer's.
* fix(ui): preserve legacy author identity and 404-safe sender avatars
Review findings: stripping the baked UUID suffix discarded the only
author key on legacy rows (the gutter then showed the local viewer's
face under another person's label) — the UUID now survives as
structured non-display sender identity. And the derived avatar route
can 404 (no upload, no Gravatar), so the gutter image swaps to
identity-colored initials on error instead of a broken image.
* fix(ui): attribute bare-UUID legacy sender labels to their profile
A nameless legacy sender labels as the bare profile UUID; recognize that
whole-label shape as the sender id (keeping it as last-resort display)
so the row resolves the author's avatar instead of misattributing to the
local viewer.
* feat(ui): who's-online roster menu on the footer facepile
Clicking the sidebar footer facepile now opens a scrollable roster of
everyone online — avatar, name, and email subtitle per person, with your
own entry pinned first and marked (you). Uses the stock menu-surface +
wa-dropdown idiom; long rosters scroll inside the popup via the dropdown's
menu part. Session-row facepiles stay non-interactive so row clicks keep
navigating.
* fix(ui): roster menu interaction hardening from review
- close explicitly on wa-select (preventDefault also cancels the
dropdown's own select-and-hide)
- clear the open state when a presence update unmounts the footer
facepile, so the menu cannot remount stale when presence returns
- restore focus to the visible facepile button on keyboard dismissal
(the dropdown's own trigger is a hidden throwaway anchor)
* fix(ui): return focus to the facepile trigger on roster row selection
The About page commit row now embeds the commit's committer timestamp
(commitAt, advisory like branch/dirty) and renders it as an always-relative
age with the exact localized date-time on hover. The copy button becomes a
compact borderless icon revealed on hover/focus only where hover exists;
touch keeps it visible.
normalizeMessage / isToolResultMessage / isStandaloneToolMessageForDisplay
cast their unknown input to a record and then read `typeof m.role`. That
still evaluates `m.role`, so an undefined entry (e.g. groupMessages calling
normalizeMessage(item.message) with an absent message on a pending or
malformed transcript row) threw "Cannot read properties of undefined
(reading 'role')" inside the gateway event handler — a caught but recurring
error storm on the live team instance. Coerce non-object input to {} at the
source so every downstream `typeof m.<field>` check works and role degrades
to "unknown". The existing safeNormalizeMessage try/catch wrapper existed
precisely for this hazard; the guard now lives in normalizeMessage itself.
* feat: add session discussion panel seam
* fix: keep discussion iframe cookie-capable and changelog release-owned
* test: cover cookie-capable discussion iframe sandbox
* fix: stretch discussion panel host so the embed fills the rail
* fix: keep provider failures retryable and probe discussion availability before showing the action
* fix: block same-origin discussion embeds, show action on catalog sessions, close stale panel on reconnect
* fix: scope discussion probes and panel callbacks to the issuing connection
* fix: dedupe in-flight discussion probes per session
* fix: retry superseded discussion probes and key-scope panel results
* fix: regenerate Swift protocol models and extend advertised-method expectations
* style: format chat-pane-header
* fix: regenerate Kotlin protocol models and date discussion methods in the 2026.7 train
* chore: restore release-owned changelog to main state
* chore: keep changelog untouched relative to merge-base
* feat: avatar upload UI, self-service profiles, server-side avatar proxy
* refactor(ui): make resolveAvatar synchronous so avatar updates render immediately
resolveAvatar no longer does async work (gravatar moved server-side), so
the until()-deferred render left the identity chip img stale after
updateSelfUser. Rendering synchronously reuses the img element and
reflects avatarUrl changes on the next render.
* fix(gateway): bound avatar Gravatar lookup to one timeout budget
Linked-email Gravatar lookups ran sequentially, each with its own 5s
timeout, so an upstream outage stalled the held connection for
5s x linked-email-count. Resolve them concurrently and pick the first
hit in email order, keeping primary-email precedence while capping the
request to a single timeout budget.
* fix(gateway): cap per-profile Gravatar fan-out to bound sockets and memory
A profile with many linked emails would fan out one concurrent fetch per
email with no bound, each able to buffer up to MAX_GRAVATAR_BYTES before
the cache budget applies. Cap the lookups to the first 8 primary-ordered
emails so a single avatar request holds a bounded number of sockets and
transient bytes.
* test: reconcile avatar tests with server-side gravatar model after merge
main landed a client-side Gravatar path (browser computes the gravatar.com
URL); this branch resolves avatars through the same-origin gateway route,
which is the only approach that works under the Control UI CSP (img-src
'self'). Update the identity-section, app-sidebar footer chip, and
profile-page e2e expectations to assert the canonical /api/users/<id>/avatar
route (gateway serves the Gravatar fallback behind it) instead of a
CSP-blocked direct gravatar.com image.
* test(gateway): fix fetchImpl mock param type for check-test-types
The Gravatar mock typed its param as (url: string), narrower than the
fetchImpl signature (URL | RequestInfo); function-param contravariance
made it unassignable under tsgo:test. Widen to the fetch input type and
extract the URL via a small helper that avoids no-base-to-string on a
Request.
* fix(gateway): short-circuit Gravatar lookups and version presence avatar URL
Two autoreview findings on the avatar path:
- Privacy: the concurrent Promise.all fan-out queried every linked email's
hash against Gravatar even when the primary already had one, exposing
secondary work/personal addresses. Resolve sequentially with short-circuit
so a later hash is disclosed only after the earlier email is a definite
miss, under one shared request deadline that still bounds total latency.
- Staleness: presence published an unversioned /api/users/<id>/avatar, so a
reconnecting viewer's <img> kept the stale cached image after an upload.
Carry the profile revision as ?v=<updatedAt> so a changed avatar refetches.
* feat(agents): authoritative write diffs end to end
The write tool now reports what actually happened: no-op (changed:false),
created files (created:true with a bounded numbered diff), and overwrites
(created:false with a real old-to-new diff when the old content is
readable text within byte, line, and edit-distance budgets — unknown
removals are never fabricated). The gateway history projection forwards
the changed/created flags, the web and iOS write rows prefer the
authoritative diff and suppress the +N -0 stat when creation is not
proven, and the iOS transcript cache persists one bounded apply_patch
envelope so patch diffs survive cold opens.
* chore(i18n): sync native inventory
* 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.