The image-actions feature landed with an untyped fetch spy and an unknown
URL argument; newer vitest types surface both. Capture anchor downloads via
a typed mock implementation and coerce the probed block URL. Also add the
toolCallId/itemId fields to the Discord harness's stale local copies of the
onToolStart/onCommandOutput payload types (canonical type already has them).
Give Control UI managed images bounded previews and shared full-image Open, Download, and Copy actions. Keep artifact access transcript-bound; the existing ticket is intentionally attachment-scoped to the lower-fidelity thumbnail.
Co-authored-by: Ittiz <github@daein.org>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(codex): report harness context window as session contextTokens
Codex app-server reports model_context_window per turn. Carry it through the projector into the run result meta so session rows show the real window instead of the catalog's standard-tier input cap (272k vs 1M for gpt-5.6 models).
* improve(ui): compact chat context popover
Inline stat rows replace boxed tiles; zero-value cost rows and the whole cost section when empty are omitted; provider/model provenance lines are removed because the footer already shows the model; and the popover is narrowed to 300px.
* refactor(codex): split attempt-result assembly out of event projector
* fix(codex): seed attempt context window from startup binding
App-server v2 turn/started omits the core model_context_window, so thread/tokenUsage/updated is the only live carrier. Seed usage-less attempts from the retained startup binding rollout/session window so session metadata cannot regress to the catalog fallback.
* fix(codex): prefer native startup context window
Persisted session contextTokens has no source provenance and may contain the catalog fallback. Keep the minimum window for the conservative rotation fuse, but seed the projector from the native rollout when it is available.
* chore(plugin-sdk): regenerate api baseline (new format)
* revert(gateway): "prevent restart replay after final delivery" (broke 5 CI jobs)
* feat(ui): show unsent-draft pencil on sidebar session rows
Typed-but-unsent composer text now surfaces as a pencil badge on the
owning session's sidebar row (and Home row) once you switch away.
Draft persistence now notifies stored-outbox subscribers so the
indicator appears and clears live. The active session suppresses the
badge since its composer is already visible.
* chore: refresh merge ref for CI against current main
* chore: refresh merge ref against healed main
* fix(ui): notify draft indicator only on presence transitions
Unconditional notify on every draft persist let outbox-projection
subscribers re-persist a stale pane over a newer draft (chat-state
route-fallback invariant). The sidebar pencil only consumes presence,
so notify on empty/non-empty transitions only.
* fix(gateway): unify media privacy in chat history
Centralize image, audio, video, and persisted media-fact privacy at the shared Gateway history projection. Remove duplicate sessions_history redaction, validate managed media claims canonically, and keep safe media-only user turns renderable.
* test(gateway): type history RPC integration
* test(agents): align history fixture with gateway projection
Poll both fixed-width selection rails together and compare their trailing edges so the browser assertion measures settled popover geometry instead of sampling different points in the open animation.
Keep Web Awesome checkbox semantics while rendering the selection state in the sidebar filter shared trailing rail. Add mocked-browser coverage for the hidden native mark and one-pixel alignment invariant.
Co-authored-by: Vyctor H. Brzezowski <krzyszchweski@gmail.com>
* fix(ui): unify initial prompt handoff projection
Route create-time prompts through the canonical session projection so live persistence and delayed history adopt one stable bubble. Preserve inline attachment content across reconnects and authoritative identity handoff while removing duplicate matching and post-reducer mutation paths.
* fix(ui): preserve initial prompt projection contracts
* fix(ci): restore exact-head validation
* fix(ui): keep typing indicator in the composer footer
Keep typing activity in the fixed-height Control UI composer footer so collaborators no longer shift the textarea.
Render up to three author avatars before the localized typing label.
* test(ui): target the typing label in E2E
* fix(ui): hide typing avatars from the status announcement
Status text already names typers; role=img avatars made screen readers announce names twice. Addresses ClawSweeper P2.
* 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