* fix: support Back within channel setup
* docs: note channel setup Back navigation
* fix: keep navigation outcome type private
* style: format navigation outcome type
* chore: leave changelog to release prep
* refactor(ui): extract the steered-message lifecycle into a typed state machine
The steer flow was an implicit state machine spread across five chat modules,
with chip states encoded in loosely coupled optional fields and duplicated
guard predicates. This extracts it behind structural types:
- steered-chip.ts: leaf chip model — SteeredChip union (in-flight requires
sendState "steering"; acknowledged requires pendingRunId and forbids
sendState), constructors, and narrowing guards; impossible states are
unrepresentable and every call site uses the guards
- steer-lifecycle.ts: owns the steer send flow, single terminal retirement
pipeline (remember -> materialize acked chips -> clear), and history-proven
retirement; absorbs queued-user-turn.ts
- terminal handling in chat-gateway/chat-state and the stored-outbox history
verification in chat-send consolidate onto the shared helpers; chat-send.ts
shrinks by ~300 lines; net prod LOC is flat
- behavior fixes riding the pipeline: acknowledged chips retire once
authoritative history contains their user turn (no more brief chip/history
double display) and lingering chips keyed to a filtered run id now retire on
the next history load
- isTerminalFailureChatSendAck moves to chat-send-contract.ts so ack
narrowing stays type-safe at every formatter call site
* fix(ui): satisfy type, lint, and dead-export gates for the steer-lifecycle split
- restore the snapshotChatAttachments helper (oxc no-map-spread fires on the
inlined form)
- select the stored-outbox drain head with an explicit loop; the find-predicate
form made TS narrow away the failed state and flagged the guard (TS2367)
- drop the unused chip re-exports from steer-lifecycle and the consumer-less
isSteeredChip guard; chip types stay module-local, guard consumers import
the steered-chip leaf directly
* style(ui): oxfmt line joins in chat-send and chat-state
* feat(ui): accept drag-and-drop attachments in new-session composer
The new-thread composer only accepted attachments via paste and the +
menu; dragging a file onto it was silently ignored. Wire the shared
chat attachment drop handling onto the composer shell with the same
balanced drag affordance as the chat pane, and gate both composers'
drop/dragover cancellation on file drags so text/URL drops keep the
textarea's native behavior.
* fix(ui): cancel non-file drops outside editable composer targets
A URL dropped on the transcript, header, or composer chrome would hit
the browser default and navigate the app away, discarding drafts. Keep
native text/URL drops only when the drop target is an editable control;
cancel them everywhere else in both composers.
* fix(ui): treat disabled and readonly inputs as non-editable drop targets
A URL dropped precisely on a disabled composer textarea would still hit
the browser default and could navigate away. Check actual editability
(disabled/readOnly/isContentEditable) instead of selector shape.
* fix(ui): limit native drops to text-entry inputs
Enabled non-text inputs (checkbox, range) counted as editable drop
targets, so a URL dropped on one skipped cancellation and could
navigate the browser away. Restrict the native-drop exception to
text-capable input types, textareas, and contenteditable content.
* 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
The local exec runtime sanitized each stdout/stderr chunk with the
stateless `sanitizeBinaryOutput`, so an escape sequence straddling a read
boundary was escaped into visible text instead of being consumed. The
remote bash path already uses a per-stream parser via
`createStreamingBinaryOutputSanitizer` (#103706); this applies the same
treatment to the local path, with separate parsers for stdout and stderr
so neither stream can consume the other's pending sequence.
* 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).