* fix(workers): persist placement terminal failures
* fix(workers): refresh placement protocol clients
* refactor(workers): isolate error formatting
* fix: integrate cloud terminal state with current main
* chore(plugin-sdk): refresh API baseline
* refactor(ui): inline one-use cloud terminal-reason banner helper
Keeps the Control UI startup JS bundle inside its 317 KiB gzip budget
(the helper + type-only import tipped it by 16 bytes).
* refactor(ui): trim terminal-reason lookup to type-erased optional access
Recovers the last gzip byte of the Control UI startup budget
(324609 B vs the 324608 B limit).
* feat(control-ui): preview text attachments inline with a download action
Document attachments in chat now render a unified attachment card: text-like
files get a bounded, cached inline preview plus an explicit download button;
other documents get the same card chrome with a download action.
* fix(control-ui): split document preview module and e2e file under lint caps
* fix(control-ui): keep availability helpers module-internal after resolver move
* fix(control-ui): cancel document preview streams at the preview budget
Reads at most the preview budget from the response body and cancels the
reader, so unknown-size or endless text attachments cannot buffer fully
just by rendering; adds a streaming regression test.
* fix(control-ui): slice preview chunks to the byte budget before decoding
A blob or misbehaving source can deliver one giant chunk; cap the bytes
handed to TextDecoder at the remaining preview budget so rendering never
allocates or scans the full payload. Adds a single-oversized-chunk
regression asserting the decoded byte bound.
* test(control-ui): track decoded bytes via a TextDecoder subclass
Replaces the prototype spy that tripped typescript(unbound-method) in
oxlint with a stubbed tracking subclass; unstubGlobals restores it.
* feat(ui): add people session sorting
Offer People sorting in the Control UI session sidebar when the Gateway reports multiple session-sharing identities.
* fix(ui): restore sidebar translation dependency
* fix(ui): preserve session sort ordering contracts
* improve(ui): remove builtin observer dashboard injection
The Control UI fabricated a synthetic Observer tab + read-only builtin
widget on top of the real board snapshot whenever a session produced an
observer digest, which forced hasBoard=true and surfaced the prominent
chat/split/dashboard face switcher for sessions that never had a
dashboard. The dashboard face only duplicated the chat-rail observer HUD.
Delete the injection path, the builtin widget kind and its readOnly
concept, the pane-local ObserverDigestHistory, the observer widget
renderer/styles, and the orphaned chat.observer.* i18n keys. The session
header face switcher now appears only when the board provider has real
content. Chat-rail observer HUD, sidebar narration, config observer
settings, and protocol surfaces are unchanged.
* test(ui): give plugin widget cell chunk waits a real timeout
The four vi.waitFor calls on the lazily imported workboard-card element
used the 1s default, which a cold transform exceeds on loaded machines;
observed flaking 1-3 of 4 tests depending on host contention.
* feat(ui): turn the widget pin action into an icon button
The chat widget card's pin control was a bare text button ("Pin to
dashboard" / "Pinning…" / "Pinned"). It is now a 26px icon button using
the shared Lucide pin icon, styled like the adjacent widget-actions
trigger, with the accent color marking the pinned state. Labels move to
title/aria-label so tooltips and accessible names are unchanged, and the
two pin handlers collapse into one shared helper. The session-dashboard
e2e now records hover/pinned proof screenshots under
.artifacts/control-ui-e2e/workboard-pin when OPENCLAW_UI_E2E_RECORD=1.
* test(ui): assert pin button state via aria-label
The pin control no longer carries text content; the pinned state is
exposed through aria-label/title, which is what the widget-card unit
test now asserts.
* fix(ui): trim pin action label updates
* fix(ui): preserve commentary across steered refreshes
Retain keyed active-run commentary and its original timestamps so Control UI reloads keep progress on the correct side of persisted steering messages.
Closes#121448
* refactor(ui): simplify active-run replay coverage
Reuse the run-bound lookup and remove redundant transcript, hydration, and browser-test setup while preserving the steering refresh regression contract.
The persistent-identity footer for the viewer's own messages is
right-aligned, so the hover-revealed relative timestamp used to push the
always-visible sender name sideways. Flip the visual order with CSS
`order` so the time slots in to the left of the name and the name stays
pinned under the avatar. Peer footers are left-aligned and already
stable; DOM (screen-reader) order is unchanged.
* fix(ui): offer undo and a named recovery path when hiding a sidebar section
Hiding a session catalog section wrote the preference and returned nothing:
the section vanished, no undo was offered, and the only standing recovery
(Settings > Appearance > Sidebar) was never named anywhere near the action.
Announce the outcome at the producer. hideSessionCatalog now names the section
that disappeared, offers Undo, and carries the Settings path in the toast text
for after it times out. The hidden-catalog set gets one owner,
setStoredSessionCatalogHidden, so the sidebar, the undo, and the Settings
toggle all mutate the same authoritative read instead of each rebuilding the
set from its own copy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ui): align the automation-sessions checkmark with its popover siblings
Every selection state in the sidebar sessions filter popover reads on the
trailing edge, but "Show automation sessions" used Web Awesome's
type="checkbox" checkmark, which its template renders before the label. The one
row in the menu that is a checkbox rather than a radio put its check on the
opposite side of the row from every neighbour.
Hide the leading part and render the shared session-menu__check in the details
slot, so the row matches GROUP BY / SORT BY / STATUS while Web Awesome keeps
owning the menuitemcheckbox role and aria-checked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ui): stop toasts from ellipsing away their recovery instruction
app-toast__message was nowrap + ellipsis, so any message longer than one line
lost its tail. That is fine for a bare acknowledgement and wrong for a toast
that tells the operator how to undo or where to re-enable something: the part
that gets cut is exactly the part they need.
Wrap instead, clamped to three lines so a pathological string still cannot grow
the toast without bound.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(ui): make the hidden-catalog writer module-private
With setStoredSessionCatalogHidden owning every hide/show, the raw
storeHiddenSessionCatalogIds writer no longer has a caller outside its own
module and the deadcode:exports gate flagged it. Drop the export rather than
keep a second public way to write the same preference, and move the two tests
that used it onto the owner they are actually meant to exercise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ui): anchor toasts to the bottom-right corner
Toasts were centered over the content column, which on chat routes puts a
passive notification directly on top of the composer. Move the shared
.app-toast container to the trailing bottom corner, the standard desktop
placement for passive notifications.
Anchoring it there alone would trade one overlap for a worse one: at a 20px
inset the toast lands exactly on the composer's mic and send controls. Chat
routes therefore lift it above the composer via --app-toast-inset-bottom on
.shell--chat, which the toast host inherits as a child of .shell. Routes
without a composer keep the tight 20px corner.
Phone widths keep the full-width bottom idiom, and now get it properly: the
centered toast used to collapse to its content width (200px measured at 390px
viewport), so the explicit left/right insets also widen it to 366px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ui): drop the chat-route toast lift
The corner toast no longer clears the composer on chat routes. Overlapping
the composer region is the accepted trade for one placement rule on every
route, so the route-specific 140px lift and its custom property are gone and
every surface uses the same 20px safe-area corner inset. Wrapped toast text
balances its lines now that the message can run to two.
* fix(ui): open the sidebar recovery path from its toast
Naming "Settings > Appearance > Sidebar" in the hide toast still left the
operator to find it. The recovery sentence is now a link built from the
settings-search destination that already describes that block, so the toast
opens the re-enable list instead of describing where it lives. Toast messages
accept a template for that.
* revert: leave the automation-checkmark repair to its own PR
#121429 fixes the same Web Awesome checkmark alignment on its own, with
browser regression coverage this branch does not carry. Two PRs owning one
CSS part is worse than losing the drive-by, so this keeps to the hide and
recovery invariant.
This reverts commit 6b1438a5be4bb1c9dd4c2ce9cd7d0dd54e77cb00.
* fix(ui): let the hide outcome reach a phone operator
On a phone the sidebar only exists inside the navigation drawer, and that
drawer is a modal dialog: a toast raised behind it is both occluded and inert,
so hiding a section there stayed exactly as silent as before this branch.
Promoting the toast into the top layer only fixes the paint half — a modal
dialog inerts everything outside it, so Undo and the recovery link still take
no clicks. So the hide hands the drawer back to the shell first, through a
seam the sidebar can use for any outcome that belongs on the main surface.
It is a no-op wherever the drawer is not open.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(ui): move chat view toggles into a header session menu
Control UI: replace the chat composer eye-icon View dropdown with a Claude Desktop-style three-dot session menu at the far right of the chat pane header. The menu adds Open in for editor/worktree sessions, Rename, the three display toggles under View, Fork, Archive/Restore with undo, and Delete with confirmation.
Reuse the sidebar session-organizer actions through a narrowed SessionActionHost seam, consolidate session permission gating in sessionMenuReasons, and remove the obsolete composer menu state, dismissal handling, tests, and CSS.
* test(ui): update header session menu e2e interactions
Fold the writer-claim fence into the four sync write ops at their
definition sites, collapse the logical-layer fence wrappers to plain
re-exports, delete the session-accessor.sqlite middle barrel (the only
unfenced escape hatch), and retarget its 26 importers to the focused
defining modules. replaceSessionEntrySync intentionally stays unfenced:
its two production callers own their freshness guards and its return
carries no rebound signal. Net -106 production LOC, behavior-neutral.
Part 1 of the session-accessor dual-layer collapse.
* fix(telegram): preserve unsent media while deduplicating streamed replies
## What Problem This Solves
A Telegram final reply could resend an attachment already delivered in a streamed block when the legacy mediaUrl still referenced that sent attachment while another mediaUrls item remained. Conversely, unsent legacy-only attachments must not be discarded.
## Why This Change Was Made
The Telegram media-deduplication owner now clears mediaUrl only when its normalized attachment was actually sent, preserving independently unsent legacy attachments and preventing downstream outbound planning from restoring delivered media.
## User Impact
Telegram replies retain every unsent attachment exactly once and no longer duplicate already streamed images in mixed final-message payloads.
## Context
Seven zero-dependency scenarios passed against the actual media owner and outbound planner, including mixed legacy/vector attachments, whitespace normalization, visible-send handling, and remaining-media preservation. The exact frozen campaign baseline passed 844 tests. Existing oxfmt formatting and staged whitespace checks passed; focused Vitest CI remains pending.
* refactor(telegram): inline legacy media dedup check
* fix(security): unify secret-redaction and SSRF policy ownership
Memory-host errors now redact through the canonical redactor (payment/card/CVV
coverage included) via the existing core facade instead of a stale local
pattern table; ACP error redaction is wired through a single barrel so the
injected canonical redactor no longer depends on module load order, and the
acp-core fallback table shrinks to the minimal standalone set (structured
auth-header patterns proven load-bearing stay). The memory-host SsrFPolicy
structural copy is deleted in favor of the canonical src/infra/net/ssrf.ts
type re-exported through the network facade.
* fix(build): keep memory-core doctor closure execa-free
Import the canonical redactor directly from src/logging/redact.js instead of
the openclaw-runtime-io facade (which reaches execa through the full runtime
graph), and regenerate the plugin SDK API baseline for the intentional
canonical-SsrFPolicy surface change.
* fix(security): merge operator redact patterns with defaults in injected redactors
ClawSweeper found that ACP injected the general redactor, where nonempty logging.redactPatterns replace the built-in provider-token patterns. Use the tool-payload redactor for ACP and memory-host error formatting so operator patterns extend defaults and redaction remains forced for these security boundaries.
* fix(ui): center onboarding dashboard and quiet its chrome
The custodian onboarding route carried the settings takeover class, so the
shell reserved a hidden 288px sidebar column and the whole page rendered
off-center. Onboarding now opts out of the takeover, drops the duplicate
header identity (the thread already introduces the mascot), and option-card
hover no longer lifts or re-tints cards.
* fix(ui): stop next-step cards overlaying the message meta row
The option-card and wizard-step thread attachments pulled themselves up with
a -12px top margin, covering the preceding message's sender/timestamp line.
* test(ui): align nudge e2e with the minimal onboarding header
* fix(ui): soften default dark-mode body text to ~12:1 contrast
Drop --text/--chat-text from #d4d4d8 (12.87:1 on --bg) to #cdcdd1
(12.01:1 on --bg, 11.10:1 on --card) so dark body text sits inside
the 10-12 comfort band instead of above it; brighter body text
halates on dark backgrounds. Light mode, openknot, dash, --text-strong,
and --muted are unchanged.
* fix(ui): dim dark body text a further step to ~11.4:1
#cdcdd1 -> #c8c8cc (11.41:1 on --bg, 10.54:1 on --card), mid-band
instead of top-of-band.
* fix(ui): settle dark body text at ~10:1 on --bg
#c8c8cc -> #bcbcc0 (10.05:1 on --bg, 9.29:1 on --card) per visual
preview pick; low end of the dark comfort band on the page, slightly
under it on cards, still well above AAA.
* docs(ui): sync design-system guides with new dark --text token
ClawSweeper P3: color-tokens.md and accessibility.md still published
#d4d4d8 (~12.9:1); both now name #bcbcc0 (~10.1:1).
The collapsed session rail and the background-tasks toggle both rendered
icons.activity in the chat pane header cluster, so two unrelated actions
were indistinguishable without reading a tooltip. Background tasks keep
the activity metaphor; the rail now uses the companion sparkle, which is
already the product's assist glyph and does not collide with the
split-view panel icons at the end of the same row.
Closes#121418
* refactor(channels): flatten channel turn dispatch naming
* docs(plugin-sdk): narrow inbound reply compat guidance
* docs(channels): point stale references at turn defining modules
* fix(channels): preserve dispatch contracts after flattening
* chore(plugin-sdk): ratchet surface budgets after flattening
* chore(channels): ratchet removed export collisions
* fix(plugin-sdk): restore inbound reply compat exports
Restore eight still-existing legacy callable re-exports from canonical SDK seams and cover the deprecated package subpath with a table-driven compatibility test.
Raise the public export, callable export, and deprecated export budgets by exactly eight; the three maintainer-authorized zero-consumer symbols remain removed.
* test(channels): split channel turn kernel coverage
Replace the oversized kernel test with independently mocked delivery, pipeline, and finalize suites, preserving all 51 tests while removing the max-lines suppression and stale ratchet entry.
* chore(plugin-sdk): refresh inbound reply API hash
* fix(ci): align channel turn review fixes
Restore the test-local DeliveryResult type removed during the split.
Ratchet the public export, callable export, and deprecated export budgets by exactly seven: six channel-inbound plus one channel-outbound legacy re-export.
* fix(plugin-sdk): back every workspace facade export with a real src facade
packages/plugin-sdk declared 59 export subpaths but only 24 had src facade
files; the other 35 defaults pointed at nonexistent ./src/<name>.ts targets
that typecheck (types resolve via boundary dist) but fail runtime resolution.
Create the 35 missing facades (each mirrors the sibling 3-line re-export of
the core src/plugin-sdk entrypoint; all verified live, none retired), and
extend scripts/sync-plugin-sdk-exports.mts to own the facade package too:
exports are regenerated from the src directory, every facade must name a
canonical SDK entrypoint, and --check fails on dangling keys or orphan
facades. The check-changed Plugin SDK surface lane now also triggers on
packages/plugin-sdk/** so the guard actually runs when the map changes.
* fix(plugin-sdk): validate facades before any exports write
ClawSweeper P2: write mode reported a stale facade but still exited 0,
after syncRootPackageExports had already rewritten the root manifest.
Hoist facade validation ahead of all writes and exit nonzero in both
modes; an invalid facade now fails closed with no partial manifest
update (proven: hash-identical manifests plus exit 1 in write mode).