* fix(models): stop models.list from silently collapsing to the harness default
Explicit models.list refresh reads now run to completion instead of racing
the 750ms browse deadline, and implicit reads that do hit the deadline fall
back to the published prepared catalog instead of returning an empty
snapshot that the Codex harness augmentation refilled with only the default
model. The Control UI model picker now revalidates silently in the
background: a warm catalog stays rendered and interactive, and the
'Refreshing models…' state is deleted end to end (chat + New Session).
* test(ui): prove the model picker stays interactive during background refresh
* test(ui): give the shared chat-pane test host a model catalog
The background-revalidation change reads chatModelCatalog.length in
refreshPageChat; createTestChatPane omitted the field, breaking the
chat-pane lifecycle suites on CI.
* improve(control-ui): align composer invocations with current main
* fix(ui): close composer invocation regressions
* test(ui): follow grouped slash hint rule
* fix(ui): preserve reasoning reset cleanup after rebase
* fix(ui): keep checkbox capability rows switch-free
The unified capability toggle row embedded a wa-switch even in checkbox
mode, reintroducing a hidden-but-real switch control inside the
new-session composer (breaking the rail-owns-switches contract asserted
by new-session-page.places e2e) and an aria-hidden focusable control.
Checkbox rows rely on the dropdown item's own checked state.
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Collapsed, the composer progress card's hover highlight painted only the
<summary> band, leaving slivers of the resting background in the underlap
strip and the arcs beside the composer's rounded corners. Move the
collapsed hover fill to the card container (clipped to the full rounded
shape, extending under the composer) and keep the summary-scoped fill for
the expanded state.
Prevent delayed rewind, branch-switch, and fork completions from overwriting replacement chat state after reconnects while preserving canonical history reconciliation.
Refs #128617.
Reviewed-by: @shakkernerd
Co-authored-by: Shakker <165377636+shakkernerd@users.noreply.github.com>
Consecutive runs whose entire visible outcome is tool activity (heartbeat
wakes, cron ticks) now collapse into a single expandable activity row
instead of stacking identical rows down the transcript. Runs with a
visible reply keep per-run activity separation. Group summaries reuse the
standalone row's tool display label, so the rollup reads
"Used Heartbeat Respond ×N". Splits tool call/result pairing out of
chat-thread-grouping.ts into chat-tool-activity-coalesce.ts (max-lines).
* fix(ui): own chat history issuance with a closed state machine
loadChatHistory silently returned undefined when the pane was not yet
connected, so any caller racing the connection lifecycle left the pane
in skeletons forever: no request, no error, no recorded fact. Failures
that did happen fed a dismiss-only alert with no recovery (#128655).
Record issuance as one ChatHistoryLoadState union
(idle | pending-connection | in-flight | committed | failed) at the
owner, absorbing the old inFlightHistory nullable. Disconnected requests
record pending-connection and re-arm exactly once at the pane context's
connected transition; failures render a retryable transcript error and
retryable ones auto-resume on reconnect; invalidated in-flight loads
transfer intent back to pending instead of vanishing.
Closes#128655. Supersedes #128691.
* fix(ui): scope disconnect loading normalization to the transition
Repeated identical disconnected snapshots must stay no-ops for pane
ownership (chat-pane-lifecycle contract); normalize chatLoading only on
the connected->disconnected edge.
* fix(ui): keep failed history refreshes actionable over cached transcripts
A refresh failure with cached messages rendered nothing: the failure
panel only replaced an empty thread, so stale content sat with no error
or Retry. Render the same failure surface as a compact non-displacing
banner above the transcript when content exists, sharing one builder
with the empty-state panel. Adds the cached-refresh browser regression
with screenshot proof.
* refactor(ui): name the roster load decision blocks and dedupe test harnesses
Extract preserveCurrentSessionRow and retainSessionPaginationWindow from
load()'s 140-line body so its spine reads as normalize -> loading publish ->
request+owner merge -> provisional return -> decide -> publish; collapse the
hasForegroundListOptions/hasSeededListOptions boolean pair into one closed
listOptionsSource mode. Behavior-neutral.
The event-refresh and owner-first suites hand-rolled copies of
sessionsResult/createHarness/sessionChangedEvent; session-capability
test-support already owned the canonical versions, so it gains selfUser
support plus createSessionCapabilityHarness and the copies are deleted.
* test(ui): move the attention-panel surface test to its component suite
The app-sidebar harness mocks sidebar-attention.ts away (health RPCs must
stay out of client call-order assertions), so the transient-menus case
asserting the real attention panel renders was import-order dependent:
deterministic-fail in local single-file runs on clean main, pass in CI's
worker mix. The invariant (panel opens inside openclaw-menu-surface) now
lives in sidebar-attention.test.ts against the real component, and the
harness mock documents where attention rendering coverage belongs.
* test(ui): give the moved attention-panel test its agent selection
sidebar-attention reads context.agentSelection.state during refresh
ownership; CI's striped worker mix exercised that path and crashed the
render, leaving the trigger unrendered.
* improve(control-ui): align composer stack with current main
* refactor(ui): isolate new-session permission state
* fix(ui): close composer CI regressions
* test(ui): align composer presentation contracts
* test(ui): fix composer browser contracts
* test(ui): stabilize composer browser ownership
* test(ui): target presented mobile composer
* test(ui): select accessible mobile composer
* test(ui): open mobile rail session directly
* fix(ui): keep mobile composer content laid out
* fix(ui): address composer review findings
* fix(ui): close composer interaction gaps
* fix(ui): bound swarm task popover
* fix(ui): restore progress-card docking beside the wide composer
The multiline restructure dropped the .session-progress-card--dock
placement shipped in #129141 and left the completed marker inside the
collapsed-only summary span, so it vanished when the disclosure opened.
Restore the dock positioning block and hoist the marker into the
always-visible summary grid.
* fix(ui): bound swarm task popover by its composer anchor
The popover capped its height against the raw viewport, so on short
mobile viewports its top rendered above the screen. Register the swarm
group with the existing composer popover anchor observer and consume the
measured --chat-composer-popover-max-height, keeping the viewport calc
only as fallback. The new actual-tree mobile regression failed with
-71px top clipping before the fix.
* fix(ui): restore Draft capability controls and split the new-session composer
Restores the Draft option and selected-state indicators the capability
menu lost in the restructure, and moves the capability/visibility
controls into composer-capability-controls.ts so composer.ts drops back
under the 700-line lint gate (677 lines) without a suppression.
* test(ui): prove permission-mode final effect before session creation
Adds mocked-gateway proof that an admin's Full selection reaches
sessions.create as permissionMode:"full" exactly once, and that a
write-scoped operator's retained Full selection is rejected before
creation with a visible status notice and zero create requests. Also
aligns the mobile footer reading-order assertion with the shipped
attachments-draft-model order.
* test(ui): register swarm popover layout test as node-driven
chat-swarm-progress.browser.test.ts drives its own Playwright chromium
from node, but d3396c4d3b added it without registering it in
nodeDrivenBrowserLayoutTests, so the browser-mode chromium project tried
to fetch it into the page and failed on the node-only playwright import.
This was the deterministic checks-ui failure on both prior heads.
* test(ci): register codex attempt-startup-retry in its support lane
#129505 added extensions/codex/src/app-server/attempt-startup-retry.test.ts
without lane ownership, so the full-suite coverage guard
(test/vitest-projects-config.test.ts) fails on every full run. Register it
beside attempt-startup.test.ts in the attempt-support lane.
* test(ui): expect last-activity suffix in composer summary label
Merge weave: #129520 appends the accessible last-activity to the summary
aria-label; align the restructured composer card's assertion with it.
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(ui): stop warm sidebar refreshes from blanking foreign-owned rows
The owner-first roster plan (#128767) published its provisional owner-only
window as a full membership replacement on every primary refresh, so on
multi-owner gateways every other user's sessions blinked out of the sidebar
until the shared merge landed - up to once a second under event load. The
provisional phase now publishes only when no roster is on screen (cold start
keeps its fast first paint), and the shared phase merges the owner window
from the initial load's returned rows instead of reading published state;
the mergeExisting option is gone. A warm refresh whose shared phase fails
now keeps the previous roster instead of collapsing to owner-only rows.
The thread and child session lists also rendered rows positionally with
map()+keyed(), so any reorder tore down and rebuilt every shifted row's DOM
(spinners restarting, avatars remounting). Both now use repeat() with key
identity, matching the catalog renderer.
Regression coverage: owner-first-roster.test.ts pins the publish-sequence
invariant and shared-failure retention (both fail pre-fix); the new browser
test proves row DOM identity survives a reorder (fails on map()). The
owner-first tests moved out of index.event-refresh.test.ts, which hit the
max-lines limit.
* test(ui): prove foreign-owned rows survive a warm owner-first refresh
Browser-level regression for the warm-refresh half of this fix: holds the
shared phase deferred after a sessions.changed event and asserts the
foreign-owned row never leaves the DOM. Fails on pre-fix code (the row
count drops to zero the moment the provisional owner window publishes).
* test(anthropic): pin the local retired-profile id to the plugin-sdk constant
The provider-policy artifact keeps this id as a local literal so it never
imports the provider-auth barrel (#129052 regressed dist-less CI checkouts
into 120s jiti compiles of ~2.2k modules); this parity test stops the two
constants from drifting apart.
* fix(control-ui): render the chat Markdown preview in its own text direction
The chat transcript already picks a direction per message
(`chat-message-markdown.ts` sets `dir` from `detectTextDirection`), but the
Markdown preview in the chat detail panel never did. A Hebrew or Arabic
document opened there rendered left-to-right: bullets and quote bars on the
wrong side, table columns in the wrong order, and sentence-final punctuation
detached from its sentence.
Detect the direction from the document source and set `dir` on the reader, then
let the shared `.sidebar-markdown` rules mirror with it by using logical
properties instead of left/right ones. Code blocks get `unicode-bidi: plaintext`
so each hard-wrapped line keeps its own direction — that also covers the raw-text
view, which renders the whole document as one fenced block. Block art carries no
strong characters, so it stays left-to-right as before.
Under LTR the logical properties resolve to exactly the previous physical ones,
so nothing changes for existing documents.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(control-ui): make the task-list checkbox gap direction-aware
The sidebar sheet moved its indents, quote bar and cell alignment to logical
properties, but `.task-list-item-checkbox` kept `margin-right`. In an RTL
document the label sits to the left of the box, so the physical margin pushed
the gap to the outer edge of the row instead of between the box and its label
— the one remaining place in this sheet where direction still leaked through.
`margin-inline-end` is what the message renderer already uses for the same
control (`chat/text.css` positions it with `inset-inline-start`), so this also
lines the two sheets up.
The RTL and LTR browser tests each grow a task item and assert the computed
margin lands on the inline-end side, mirroring the existing quote-bar and
list-indent assertions.
Formatting proof (node_modules absent in this checkout, so the pre-commit
oxfmt hook could not run): oxfmt 0.60.0 --check and oxlint 1.78.0 both pass
on the changed test file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(control-ui): assert the checkbox gap by side, not against zero
The assertions added in the previous commit demanded the opposite margin be
0, and `checks-ui` failed on exactly that: 3px in RTL, 4px in LTR. Those are
the UA sheet's own `margin: 3px 3px 3px 4px` on an `input[type=checkbox]`.
The rule overrides only the inline-end side, so the opposite side keeps the
default and never reaches zero.
Assert instead that the sheet's 0.4em resolved onto the expected physical
side — margin-left under RTL, margin-right under LTR — which is the mapping
the fix is actually about and owes nothing to UA defaults.
Verified against a real Chromium before pushing: with `margin-inline-end`
both directions land 5.33px (0.4em of the 13.333px UA control font) on the
inline-end side; with the pre-fix `margin-right` the RTL assertion fails and
the LTR one passes, which is the regression shape this test exists to catch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ui(chat): scope the code-block bidi rule to the markdown reader
`unicode-bidi: plaintext` sat in the shared `.sidebar-markdown pre` rule, which
also matches the skills, session-progress-card and agent status-file previews.
Those surfaces never opt into a document direction, so this branch was changing
their code-block rendering while declaring them unchanged.
Move the declaration to `.sidebar-markdown-reader pre` — the chat preview is the
only consumer that sets `dir`. The shared layout properties stay shared, and the
existing direction test already queried the reader-scoped selector, so it is
unaffected. Adds a regression test pinning that a bare `.sidebar-markdown`
surface keeps its previous code-block rendering.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: yehezkel-assistant[bot] <3902212+yehezkel-assistant[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The suggestion tray and the docked progress card both floated over the
conversation's top-right area as independent absolutely-positioned
elements. The tray is 450px at z-index 24 and the card sat at z-index 3,
so whenever a suggestion arrived while a plan was live the tray covered
the card outright: session state disappearing with no trace, which is
the failure mode this repo weighs heaviest.
Give the gutter a single owner. .chat-gutter-stack is one absolutely
positioned column at top-right; the tray and the card become its
children and cannot overlap by construction. The tray keeps its exact
geometry (same inset, same width, position: relative only so its stacked
-card shadow still anchors to itself); the card right-aligns beneath it.
The card is now top-anchored rather than pinned to the composer's bottom
edge, so appended steps and a longer note extend downward instead of
shifting the rows already being read, and the empty gutter falls below
the card instead of above it. Its width becomes a fixed token rather
than gutter-derived arithmetic: the pane already gates this placement on
a measured gutter wide enough to hold it, so the clearance is guaranteed
and a stable card beats one that grows with the window.
Covered by a new placement case asserting the card sits fully below the
tray when both render.