Commit Graph

5450 Commits

Author SHA1 Message Date
Peter Steinberger 7173aeb663 fix: surface hidden-pane steer failures and demote per-turn gateway log noise (#124560)
* fix(ui): surface hidden-pane steer terminal failures globally

Three terminal branches in steer-lifecycle.ts (transport null result,
failed queue-row restore, failed queue-row removal) still gated their
error on itemStillVisible, so a steer that failed after the operator
navigated away parked the error on the queue row with no visible
outcome — the exact invariant #124473 introduced
surfaceChatDeliveryFailure() to protect.

Route all three through the canonical helper and delete the divergent
visibility-only branches. Regression test fails pre-fix
(stash-verified): steer transport failure with the pane hidden now
surfaces the session-named global toast.

* fix(logging): demote per-turn gateway log noise to debug

Live campaign evidence showed three lines dominating operator logs at
info level with no per-turn diagnostic value:

- 'tool policy removed N tool(s)': the policy pipeline runs on every
  turn, so this repeated 42x in one session. Demote to debug and delete
  the now-dead toolPolicyAuditLogLevel/auditLogLevel plumbing that only
  existed to lower diagnostic probes to the level that is now the
  default (net -13 production LOC).
- 'codex app-server one-shot cleanup checked shared client retirement':
  routine per-attempt teardown detail; demote to debug.
- 'codex trajectory capture requires the SQLite host recorder': static
  config condition warned per attempt; warn once per process.

Skipped: the [model-fetch] info carve-out in model-transport-debug.ts is
a named contract (docs/logging.md, #89648) — always-info by design.

* fix(codex): drop test-only trajectory warn-once reset export

Knip's production unused-export gate rejects
resetCodexTrajectoryRecorderWarningForTest — it was a test-only seam in
production code. Reset the process-wide warn-once flag via
vi.resetModules() + fresh dynamic import in the test instead.

* test(cron): wait for backoff re-arm instead of fixed sleep

The 0ms retry timer arms only after async watcher-state persistence, so
'await delay(5)' races it on loaded CI workers (flaked on
checks-node-compact-large-2: spawn called 1 time, expected 2). Replace
both fixed-sleep re-arm waits with vi.waitFor on the spawn count. The
remaining delay(5) guards a negative no-further-spawn assertion after
cancel, where a bounded sleep is the correct shape.

* fix(codex): scope trajectory recorder warn dedupe to session

ClawSweeper P2: the host recorder factory returns null for per-session
target-mapping conflicts, not only static config, so a process-wide
warn-once flag silenced a later distinct session's recorder loss. Warn
once per session (bounded set, cleared past 64 entries) so retries stay
quiet but each newly affected session records its loss. Regression
covers a later distinct session still warning.
2026-08-16 06:23:44 -07:00
Peter Steinberger 509f947a20 fix(ui): scope Labs mutations to connection (#124578) 2026-08-16 06:03:14 -07:00
Peter Steinberger e3e45ed462 fix(ui): preserve task recovery across refresh (#124371) 2026-08-16 05:39:57 -07:00
Peter Steinberger 32ddbb3be2 fix(ui): chat send failures were invisible when the session pane was hidden (#124473)
* fix(ui): always surface terminal chat send failures

Terminal chat send failures were recorded only on the queue item when the
owning pane was not visible (reconnect, alias drift, split-pane routing),
so the operator saw nothing at all — an action ending in silence.

Root cause: every error surface in the send/drain path was gated on
visibleSessionMatches(...), and the FIFO outbox drain treated every
chat.history reconcile rejection as a silent retryable "blocked", so a
non-retryable rejection (e.g. auth loss) wedged the head — and everything
behind it — permanently with no visible outcome.

Fix, at the owner:
- New surfaceChatDeliveryFailure() in steer-lifecycle.ts (the shared
  error-text owner): visible pane keeps the inline chat error; otherwise
  the failure routes through the existing global toast host, naming the
  session. All terminal failure sites in chat-send-delivery,
  chat-outbox-drain, chat-send-queue-state, and steer-lifecycle now use it.
- reconcileStoredChatOutboxHead: a non-retryable GatewayRequestError on the
  head now terminally fails a never-attempted head (unblocking the lane)
  or parks an attempted head as unconfirmed — both with a visible outcome —
  instead of blocking the lane forever.
- Composer disabled-reason: the reason now also renders while a draft hides
  the placeholder, and the cloud-startup-pending gate gets a reason instead
  of a silently disabled composer.

Regression tests: hidden-pane terminal failure surfaces via toast; wedged
head fails visibly and the lane drains the next message; attempted head
parks unconfirmed; disabled reason visible with draft text present.

* fix(ci): raise startup JS baseline for global failure surfacing and pin workspace-sync test clock

The chat send path now imports session-display naming for the global
failure toast, adding ~1 KiB gzip to startup JS (335452 B on CI's Linux
builder, still well under the 358400 B committed cap).

workspace-sync "never commands" asserted the exact dispatch timeoutMs
(777) but the impl derives it from a Date.now() deadline, so any elapsed
ms between admission and dispatch failed the exact-equality assertion on
a loaded runner. Pin the clock like the sibling timeout tests do.

* fix(ui): surface route-switched command failures and agent-scope global toast naming

ClawSweeper review findings on #124473:

- A queued local command failing after the operator navigated away hit
  failCommand(error) with expose=false; the dispatcher's stale-scope
  guard had already withheld the inline error, so a successful state
  write recorded the failure invisibly — the silent class this PR
  removes. Expose it globally when the scope is stale and the owning
  pane is hidden; a stale scope with the pane still visible keeps the
  failed queue chip (the new connection owns the inline surface).
- Global session rows are agent-scoped behind one shared "global" key,
  so the toast row lookup could borrow another agent's label. Match the
  row's agentId to the failed outbox's agent for global keys.

Both regression tests fail pre-fix (stash-verified).
2026-08-16 01:38:06 -07:00
Peter Steinberger fa57fe5ae5 fix(ci): target sidebar reconnect roster response (#124430)
* test(ui): target selected reconnect roster request

* test(ui): decouple reconnect request scheduling

* test(ui): keep reconnect proof outcome-focused
2026-08-15 23:15:17 -07:00
Peter Steinberger 35011e49db fix(ui): complete stale header outcome ownership (#124442)
* fix(ui): retire stale header outcomes

* test(ui): prove current header outcomes stay visible
2026-08-15 22:56:53 -07:00
Peter Steinberger 1384e277af refactor(types): enforce chained-assertion guard in AI and UI (#124361)
* refactor(types): drain chained-assertion ledger — ai and ui

* refactor(ui): extract chat state contract

* fix(ai): preserve strict Claude request cleanup

* test(ui): synchronize terminal timeout clock

* fix(ui): validate observer digest before run mutation
2026-08-15 22:25:42 -07:00
Peter Steinberger 203aafc415 feat(lint): SAFETY-comment assertion ratchet + repo deslop skill (#124359)
* feat(lint): add SAFETY-comment assertion ratchet

* feat(skills): add repo deslop skill

* fix(lint): refresh initial assertion baseline

* chore(lint): align assertion baseline with main

* chore(lint): sync baseline with current assertion ledger

* test(ui): stabilize Control UI E2E waits

* chore(lint): sync narrowed assertion ledger

* chore(lint): sync fully drained assertion ledger

* fix(lint): close assertion ratchet bypasses
2026-08-15 22:03:48 -07:00
Peter Steinberger 0e65272e21 fix(ui): reject stale session delete confirmations (#124097)
* fix(ui): bind session deletes to listed identity

* fix(gateway): authorize guarded session deletes

* test(ui): expect guarded batch delete identity
2026-08-15 21:43:10 -07:00
Vyctor H. Brzezowski f61ec66249 Preserve ClawHub external source identity and expose only supported actions (#124250)
* fix(skills): keep ClawHub search results on the source the operator picked

ClawHub search returns each result's origin under `install.reference`, but the
response model expected a flat `installRef`. That field is never present, so
every row fell through to a synthesized `@owner/slug` reference. External
skills.sh results were rewritten onto a ClawHub-native identity, dropping both
the commit-pinned source and the "not scanned by ClawHub" trust record.

Map the search wire shape explicitly and make the search contract
action-specific: `installRef` always names the result's own source, `detailRef`
appears only while ClawHub can serve a detail card for that identity, and
`trustState` travels with unscanned sources. Clients render install directly
when detail is absent instead of offering a review the Gateway must refuse.

Covers the Control UI, macOS, iOS Settings, iOS AgentPro, and Android, which
previously routed every row through review and could not install an external
skill at all.

* fix(skills): make install-only sources explicit and keep legacy review intact

Address review findings on the search identity contract:

- Replace the detail-reference capability with an explicit `installOnly` flag.
  A Gateway released before this field omits it, and reading omission as
  install-only made ordinary registry results skip the reviewed-version flow on
  every client. Absence now means the existing review-then-install path.
- Parse closed source variants in the producer. A row whose source is unknown,
  whose external reference is missing, or whose registry publisher is absent is
  dropped instead of falling through to `@owner/slug`, which was the original
  source swap in a different disguise.
- Carry the exact install reference alongside the canonical slug. The Gateway
  already records `requestedReference`; the clients dropped it and matched
  installs by slug, so a completed external install read back as unknown.
- Gate the direct-install action on admin rights. The row previously stayed
  enabled for read-only operators and reached a guard that silently returned.
- Route the unscanned-source warning through the native and Control UI string
  catalogs instead of a hardcoded literal.

* chore(i18n): leave generated native locale artifacts to the refresh workflow

Preflight isolates generated locale output from source changes: only the native
sources and apps/.i18n/native-source.json belong in a feature commit.

* fix(skills): satisfy Android ktlint wrapping and Swift test link construction

Extract the ClawHub result action guard into a named value so the multiline
condition follows ktlint wrapping, and pass the new requestedReference field in
the OpenClawKit installed-link fixtures.

* fix(skills): preserve external install identity across clients

* test(skills): add exact refs to recommendation fixtures

---------

Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
2026-08-15 21:38:46 -07:00
Peter Steinberger a846407edf fix(ui): refresh config before saving after reconnect (#124360)
* fix(ui): refresh config after reconnect

* test(ui): cover prefs reconnect config refresh

* fix(ui): gate reconnect schema refresh
2026-08-15 21:17:16 -07:00
Peter Steinberger 9a3f3b7bf1 fix(ui): release losing fallback payloads on composer adoption (#124391)
Global-scope composer fallback adoption coalesces sibling candidates
(bare-global, default-main, qualified-main) into one winner and deletes
the losers from chatComposerFallbackByScope — but never released their
attachment payload-store entries, unlike every clearChatComposerMemory-
Fallback caller. The dropped data URLs and object URLs leaked for the
pane's lifetime.

Release dropped candidates' payloads at the adoption site, retaining ids
still referenced by the live composer or any surviving fallback (the
same retention rule the pane-handoff owner uses).
2026-08-15 20:54:45 -07:00
Peter Steinberger 148229b6dd fix(ui): keep Logs tails bound to their source file (#124369)
* fix(ui): reset logs when source changes

Reload the active tail without a cursor when logs.tail switches files so the Control UI never mixes sources or skips a new file prefix.

* fix(ui): reset log source after reconnect

* test(ui): harden logs lifecycle proof

* test(ui): route Logs lifecycle through real Gateway lane
2026-08-15 20:42:58 -07:00
Vyctor H. Brzezowski f38a2f51b3 fix(ui): keep chat file links whole when a line wraps (#123310)
* fix(ui): keep chat file links whole when a line wraps

A file reference in chat prose could end a line on its file-type glyph and strand its label on the next line, splitting one reference into two fragments. The chip is now an atomic inline, so it moves to the next line whole, and it is capped at the column width so an unshortened label wraps inside the chip instead of overflowing the bubble.

Closes #123309

* fix(ui): keep the file-link glyph attached to its label on wrap

The prior fix (display: inline-block + max-width: 100% on the anchor)
stopped the whole chip from splitting across the parent's lines, but
left the glyph itself splittable from its label: the ::before glyph
was display: inline-block, an atomic inline with its own contingent
line-break opportunity, so when a long/unbreakable label couldn't fit
next to it, the UA pushed the whole label onto the next line instead
of letting overflow-wrap continue the run — stranding the glyph alone
above it. Switching the glyph to display: inline (sized via
padding-inline-start + an explicit mask size instead of width/height +
contain) removes that break opportunity; the glyph and the label's
first character now share one unbreakable run, while the rest of a
long label still wraps normally via the inherited
overflow-wrap: anywhere.

The existing wrap test couldn't have caught this: a.markdown-file-link
is inline-block, so its own getClientRects() always reports exactly
one rect regardless of how the content wraps inside it. Added a
table-driven probe (short basename, an unbroken long basename with no
natural break points, and a path with a :line suffix) that compares
the chip's own top against its first character's top via a Range,
which does see the internal split. It fails on the pre-fix CSS and
passes after.

* fix(ui): let the wrap probe infer its page.evaluate types

A single explicit type argument on page.evaluate binds Arg to void, so the
sweep's case ids arrived untyped and the shard failed to compile. Inference
reads both the argument and the result from the call.

Also condenses the layout-invariant comments on the file-link rules and the
probe to three lines each.

* test(ui): sweep the glyph probe over enormous and non-ASCII labels

A 240-character basename with no separator and a CJK label reached through a
host-local href both wrap several times inside the chip, so the first cut is
where a glyph would strand. Both fail on the pre-fix rule.

* fix(ui): give the file-link chip its own wrap policy

The atomic chip capped its box with max-width but relied on .chat-text for
overflow-wrap: anywhere. .sidebar-markdown declares no wrapping policy, so in
the Chat Detail Panel an unbreakable label painted past the capped box and the
panel's hidden horizontal overflow clipped it.

Declare the wrap policy on the shared file-link owner and sweep both renderers
in the browser probe: the separation cases now assert the painted label never
runs past the column's content edge.
2026-08-16 00:37:47 -03:00
Peter Steinberger 6279636985 perf(test): remove duplicate UI place cases (#124370)
* perf(test): remove duplicate UI place cases

* test(ui): fix latent e2e timing assumptions
2026-08-15 20:21:22 -07:00
Patrick Erichsen a73249c7cd fix(ui): clarify active model context in picker (#124207)
* fix(ui): show active model context budget

* fix(ui): simplify compact model label

* fix(ui): preserve provider when matching context budget

* fix(ui): hide stale context during model switches

---------

Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
2026-08-15 19:57:27 -07:00
Peter Steinberger e68f3d8ad1 fix(ui): surface rejected terminal actions (#124046)
Mark stale terminal tabs exited when the Gateway rejects input or resize, so operators see an actionable outcome instead of silently losing keystrokes.
2026-08-15 19:53:47 -07:00
Peter Steinberger b170c2c021 feat(ui): grid picker for session icons (#124335)
* feat(ui): add session icon grid picker

* fix(ui): use radio semantics for icon grid

* fix(ui): use pressed-state action-grid semantics for the icon picker

ClawSweeper: menuitemradio/radio semantics promise arrow-key selection, but arrows here only move focus (selection persists server-side, so select-on-focus would patch per keypress). Plain buttons with aria-pressed match the click/Enter-only contract.
2026-08-15 19:44:52 -07:00
Patrick Erichsen 1b954672b1 feat(ui): move incognito to new-session rail (#124352) 2026-08-15 19:44:08 -07:00
Vyctor H. Brzezowski aed4510bd0 improve(ui): unify chat side rails in a tabbed panel (#123874)
* Control UI: unify chat rails in a tabbed panel

* fix(ui): compact chat side panel navigation

* Polish rail separators around the active tab

* Soften rail tab separators

* Inset the rail tab strip from the resize handle

* Remove the rail tab strip bottom rule

* Add compact close controls to rail tabs

* Redistribute rail tabs after closing a surface

* Restore bottom docking from the rail terminal

* Fade clipped rail tab labels at the edge

* Add drag reordering to rail tabs

* Polish terminal tab chrome

* Polish rail tab close controls

* Regularize rail tab separators

* Space rail tabs evenly

* Add feedback to inactive rail tabs

* Smooth rail tab selection

* Hide unavailable rail menu items

* Preserve browser multi-tab entry

* Support bottom-docked rail tabs

* Polish rail tab sizing

* Refine side panel actions

* Neutralize rail empty-state icons

* Separate dark rail surfaces

* Fade only clipped rail tab labels

* Compact rail header controls

* Refine the surface picker

* Raise and tighten the surface empty state

* Keep Browser and Tasks in the chat topbar

* Unify surface list cards

* Keep embedded rails inside their dock

* Reveal rail tab close actions on demand

* Show full rail labels only when clipped

* Let rail tabs use available space

* Align chat topbar surface controls

* Unify rail surface empty states

* Center rail tab glyphs

* Fade clipped rail labels smoothly

* Keep rail tab labels visible

* Tighten rail tab separator and label fade

* Keep the new-tab control beside the rail tabs

* Keep rail tab separators from reflowing on activation

* Anchor the new-tab control outside the rail tab scroll

* Scroll rail tabs at the floor instead of clipping them

* Tighten spacing between rail tabs

* Report an unusable terminal open response as a readable failure

* Keep the terminal session menu inside the panel

* Share one dock destination cluster across rails, terminal and browser

* Align the terminal mini rail with its management icons

* Tidy panel pickers, browser chrome and task detail retry

* Type the sidebar callbacks object so tsgo:ui passes

* Space out the panel picker rows

* Measure tab strip edges by rect and cancel superseded installs

* Validate every terminal session field and release unusable sessions

* Fold the terminal rail stretch rules into one

* test(ui): match rail tab checks to the tabbed panel header

Anchoring the new-tab control outside the scroll area wrapped the strip in
.side-panel__header-tabs, and every query written as a direct-child chain
started matching nothing. The assertions did not fail loudly: they compared
an empty label list, so three side-panel journeys and the background-tasks
rail went red on a selector detail rather than on behaviour.

Give the label query one named owner scoped to the panel's own header, so
Terminal and Browser strips in the panel body stay excluded while header
layout can keep moving. Freeing that width also means two short tabs now
fit where they used to be squeezed, so the closing step asserts the honest
invariant - a strip that fits again releases the fade - while the overflow
and tooltip regime stays proven earlier in the same journey.

Settle the shared side-panel helper on whichever surface renders: an empty
panel offers its type list and a populated one offers the header menu, and
probing before either existed left it waiting for a control that never
comes. Also drop a shadowed seed binding and an unused locator that oxlint
flagged in the same file.

* fix(ui): adapt session discussion slot to the unified panel layout

* test(ui): capture the bottom dock evidence with a resting divider

* fix(ui): drop the dead previousDock toggle path

* test(ui): align sibling suites with the tabbed panel decisions

* fix(ui): keep narrow-pane hides above the icon-button styling cascade

* fix(ui): create tooltip descriptions via ownerDocument

* fix(lint): merge duplicate terminal header styles and drop dead branches

* refactor(ui): extract embedded panel templates from the pane render

* refactor(ui): delete rail-era dead exports

* test(ui): repair type drift after the rebase

* fix(ui): keep the workspace toggle module-local and settle test caps

* fix(ui): give side-panel tabs their runtime and their own header actions

The tabbed side panel dropped two things the old rails owned. The discussion
tab lost its only runtime registration, so `openclaw-session-discussion` was
never defined and the tab rendered as a permanently blank box; it now loads
through the per-slot lazy runtime map like the other panel types.

Panels also have no header of their own anymore, so any action on the active
panel's content had to reach the shared header. The header now takes panel
contributed actions instead of a discussion-only open-url special case, which
brings back the side-chat "Clear thread" overflow menu — its gateway reset had
no reachable entry point at all in the embedded rail.

* test(ui): anchor the sidebar scroll proof to the panel host

The bounding contract moved from `.sidebar-column__panel` to
`.side-panel__panel`; mounting the detail panel under the removed class let it
grow instead of scroll, so the test failed for its harness rather than for the
behavior it guards.

* refactor(ui): move side-panel wiring out of the chat render monolith

`chat-pane-render.ts` crossed its line budget. The region callbacks are layout
policy, so they resolve in the layout module with the pane injecting only what
it owns, and the two panel-action producers collapse into the single header
contribution the region consumes.

* fix(ui): keep one terminal intent queue per document

A session route mounts the side-panel terminal beside the shell instance kept
for the bottom dock. Each owned a private action array over one sessionStorage
key, so their whole-array writes erased each other's intents and a freshly
mounted panel drained through a reconnect fence it never saw.

The queue, its persisted record, and the fence now belong to the document;
panels bind as executors while connected. The chat pane records a toggle intent
where it observes it, instead of parking the raw event in memory and handing it
to a panel that may not be mounted yet — a service-worker reload in that window
dropped the request, thread id and all, with nothing explaining why. That makes
the deferred-restore handshake dead weight, so it goes.

* fix(ui): let the panel that starts a terminal intent finish it

Opening a catalog terminal from a chat route mounts the side-panel terminal
right after the request, which swapped the queue's executor mid-flight and
stranded the in-flight action; the panel's own mount restore then opened a
plain session instead. The executor that began an action stays responsible for
it as long as it can still run.

* test(ui): cover closed legacy side docks

* refactor(ui): split side panel controllers

* fix(ui): stabilize embedded panel handoffs

* style(ui): format browser panel refresh

* test(ui): settle catalog prepend paint incrementally

* docs(ui): describe the unified chat side panel

* test(ui): settle catalog clock transitions

---------

Co-authored-by: Jason (Json) <263060202+fuller-stack-dev@users.noreply.github.com>
2026-08-15 20:40:00 -06:00
Peter Steinberger e2e070c927 fix(ui): keep session context menus on sessions (#124344) 2026-08-15 19:24:59 -07:00
Jason (Json) 4490eaac9f feat(ui): open links in Control UI browser (#123912)
* feat(ui): open links in Control UI browser

* fix(ui): centralize Control UI link routing

* fix(ui): carry narrowed native link poster
2026-08-15 20:05:02 -06:00
Peter Steinberger 1fda883530 fix(ui): recover authorized deep links after Control UI updates (#124328)
* fix(ui): refresh stale dashboard documents

* test(ui): count recovery document requests
2026-08-15 18:32:59 -07:00
Peter Steinberger f870d93e2a fix(ui): no-op the second reconcile of the same sessions.changed event (#124326)
Every sessions.changed is reconciled twice — once by the capability's own
subscribeEvents handler and once by the chat page's reconcileSessionEvent —
and reconcileSessionHistory always built a fresh sessions array, so the
second, content-identical pass defeated every downstream
result === state.result publish gate and re-rendered the sidebar per event.

The merge now returns the original result identity when the merged row is
content-equal to the existing row (shallow compare with structural fallback;
rows are wire scalars/plain objects). The isOlderSessionSnapshot strict-<
guard could not catch this: the duplicate pass carries the same updatedAt.

Production +31 (the equality gate); regression test proves second-pass
identity and fails pre-fix. 9082 UI tests green.
2026-08-15 18:11:53 -07:00
Peter Steinberger 60babddd62 fix(ui): stop config snapshot refreshes from eating focused in-flight edits
Main run 31914404948 (checks-ui-e2e 6/12) timed out waiting for
config.set: the test filled the Advanced settings Endpoint field ~300ms
after navigation and the captured diagnostics show the value reverted to
the loaded snapshot with no model commit. syncScalarInputIdentity resets
element.value whenever the snapshot identity changes, which also wipes a
focused field's DOM text the model has not committed yet (mid-keystroke
or mid-automation fill) when a boot-time config.get response lands.

Focused inputs with a live edit now keep their value through identity
refreshes (revalidate only); blurred fields keep the authoritative-reset
contract, pinned by the existing repeated-rows test. Regression test is
mutation-proven (fails 'local-api' != 'form-api' without the guard). The
e2e test now asserts the filled value stuck before awaiting config.set,
so a future regression fails loudly instead of a silent 30s timeout.
2026-08-15 17:54:18 -07:00
Peter Steinberger 78502eda6d feat(workers): run device sessions from Gateway bundles (#124037)
* feat(workers): run device sessions from Gateway bundles

Install the current Gateway bundle before a device environment becomes ready, verify it at attach and tunnel boundaries, launch only from the immutable namespaced bundle directory, and retire stale environments for idempotent reprovisioning. Remove the local execution mode and preserve the node-local build claim only as temporary inventory metadata for the final projection/cleanup slice.

* docs(runners): record Gateway bundle cutover

* test(ci): repair runner validation fixtures

# Conflicts:
#	src/scripts/test-projects.test.ts

* fix(workers): surface outdated node recovery

Keep legacy runner inventory diagnostic-only while exposing the update-and-reconnect action through node, environment, provider, placement, and Control UI surfaces.

* fix(workers): reject legacy inventory with recovery

* fix(workers): bundle worker deploy closure

* test(workers): close bundle cutover gates

* fix(workers): compose browser runtime at build

* fix(workers): satisfy bundle cutover gates

* fix(workers): route temp runtime through infra

* docs(workers): align bundle host guidance

* fix(ui): fence outdated session destinations
2026-08-15 17:46:44 -07:00
Vyctor H. Brzezowski 38e0ae6ec8 fix(ui): GitHub link hovercard closes before the pointer can reach it (#124274)
* fix(ui): keep the GitHub hovercard open while the pointer reaches it

Hover intent now spans the link and the portaled card: leaving one for the other keeps the card open through a bounded traversal grace, and only leaving both dismisses it. The card also receives pointer events again, so its text can be selected. Escape, click, focus, viewport, and route-removal dismissals are unchanged, and the close timer is cleared on every close and disconnect.

* fix(ui): make the GitHub hovercard title a navigable link

The preview card's title now renders as an <a> reusing the exact
activation href, opening in a new tab like other external Control UI
links (buildExternalLinkRel/EXTERNAL_LINK_TARGET). Because the card is
portaled to document.body outside the provider's delegated focus
listeners, focus landing on the title link needed its own tracking,
mirroring the existing pointerenter/pointerleave pattern; that focus
hold releases once the pointer leaves the card after a pointer-initiated
open, so clicking the title link can never leave a hover-opened card
stuck open with the mouse gone.

* test(ui): prove hovercard pointer traversal and title-link click in real Chromium

Extends the existing github-link-hovercard e2e test with two cases: one
drives real page.mouse.move coordinates from the source link, across the
gap, onto the portaled card, and confirms it stays open and only closes
once the pointer leaves both surfaces; the other clicks the card's title
link and confirms the resulting popup navigates to the item URL.

* fix(ui): make the GitHub hovercard an accessible interactive popover

The card declared role="tooltip" while owning a focusable title link. ARIA
tooltips are descriptive text: their content is flattened and unreachable, so
the link existed for the pointer only. Drop the tooltip contract instead of the
link, and give the card the popover semantics its content already needs.

The card is now a non-modal role="dialog" named by every render state, and the
trigger carries aria-haspopup/aria-expanded/aria-controls in place of the old
aria-describedby juggling. Because the card is portaled to document.body it has
no tab-sequence neighbour, so Tab from the trigger forwards into it, Tab moves
between its links natively, and Tab past either edge or Escape returns focus to
the trigger with the card closed and no reopen.

The card's other references are links too: the repo reference and the title open
the item, the author opens their profile, and a pull request's diff-size chip
deep-links to the files-changed view. The title keeps the card's only underline;
the rest stay quiet until hovered or focused. Pointer behavior is unchanged -
leaving both the link and the card still dismisses it after the traversal grace
period, with no click required.
2026-08-15 21:05:25 -03:00
Sebastien Tardif bf4ec4b58a fix(ui): disambiguate multi-account session labels (#124228)
* fix(ui): disambiguate multi-account session labels

Parse the optional account segment on per-account-channel-peer DM and
group keys so Control UI no longer falls through to the raw routing
key. Append a non-default account as " · accountId" on label,
displayName, and fallback so two Alice rows from different Telegram
accounts stay distinguishable.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* fix(ui): take the session account from the gateway row

The display resolver was reading the account out of the session key and
inferring it from label text. That missed the shipped `dm` spelling of
direct keys, invented an account-qualified group shape no key builder
emits, and treated any name ending in "(<account>)" as already
disambiguated.

The Gateway already projects the canonical `accountId` onto every session
row from the delivery route, so display consumes that and parses the key
only for panes rendered before their row arrives. Key parsing now covers
both `direct` and the pre-#11881 `dm` spelling, group keys go back to
their canonical account-less shape, and the account suffix is applied at
a single point instead of five.

`resolveChannelSessionInfo` classifies `dm` keys as channel sessions and
no longer reads a peer kind sitting in the channel slot as a channel.

* fix(ui): stop inferring session identity from rendered text

Three related text-inference defects in the same display neighborhood:

The account suffix was skipped when the resolved name already ended in
" · <account>". That reads rendered output as if it were a fact, and it
cannot tell an account apart from a user label that happens to look like
one. The suffix is now appended from the recorded account alone.

The string it was defending against came from the sidebar rename dialog,
which pre-filled with the resolved display name. Submitting persisted a
derived string as a real user label, which then outranked every later
derivation. Sidebar rows now carry the stored label alongside the
resolved one, and rename edits that, matching the Sessions page.

Channel classification accepted an account segment before any peer kind,
so `agent:<x>:<channel>:<account>:group:<id>` was filed under a channel
even though accounts qualify direct chats only and the canonical route
parser rejects the shape. The grammar now allows an account only before
`direct`/`dm`.

Resolver tests now prove the row projection is authoritative rather than
duplicating an account the key already carries: one case where only the
projection supplies it, and one where the two disagree.

* ci: retrigger after check-lint shard cancel

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* test(ui): prove rename opens on the stored label

The existing sidebar rename coverage uses a row that already has a stored
label, so it passes whether rename pre-fills the stored label or the
resolved display name. It could not catch the round-trip this branch
repaired.

Adds a browser case on a displayName-only row with a projected account:
the sidebar shows "Alice · cards" and the rename field opens blank. On
the parent commit the field opens holding "Alice · cards", which is the
string that used to become a real user label on submit.

* fix(ui): do not double-append a stored account suffix

A persisted label that already ends in " · accountId" must stay as-is
so "Alice · cards" does not become "Alice · cards · cards".

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>

* docs(ui): name the producer the account suffix check defends

The comment above withAccountDisambiguator claimed the rendered name is
never inspected, which the suffix check contradicts. State the real
contract instead: the account itself still comes only from the recorded
value, and the endsWith check is idempotence against the chat pane's
inline rename, which seeds its input with the rendered title and
persists a partial edit verbatim.

---------

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
Co-authored-by: vyctorbrzezowski <krzyszchweski@gmail.com>
2026-08-15 20:37:51 -03:00
Vyctor H. Brzezowski 5014839937 fix(ui): abort stale destructive session confirmations on reconnect (#124254)
* fix(ui): abort stale destructive session confirmations on reconnect

SessionDataController retires the session-mutation scope on reconnect,
capability replacement, and host disconnect, but left any open
destructive confirmation dialog in place. If the operator confirmed
after reconnect the action silently no-opped, and the dialog's global
lock stayed held until manually dismissed.

Give SessionDataController an AbortController owned by the mutation
epoch, abort it whenever the epoch advances, and expose the signal on
SidebarSessionMutationScope. Wire it into every scope-bound confirm
dialog: batch delete, single delete, cloud-worker stop, preserved-
worktree removal, and session-group delete. confirm-dialog.ts already
tears down its modal and releases the lock on an aborted signal, so a
stale dialog now dismisses itself instead of surviving the reconnect.

Fixes #124227

* fix(ui): abort chat-header confirms too and surface a retry notice

The sidebar epoch-abort wiring left the chat-header delete/fork/archive
path uncovered: it builds its own SidebarSessionMutationScope from
connectionGeneration but never handed it a signal, so a header confirm
left open across a reconnect kept the shared confirmation lock forever.
Give ChatPaneBase its own AbortController, retire it everywhere
connectionGeneration advances (reconnect and pane teardown), and wire
the signal into the header's captured scope, matching
SessionDataController's controller for the sidebar.

Separately, resolving an aborted confirm to `false` looked identical to
an ordinary cancel, so the operator's lost intent vanished silently.
Reorder every destructive confirm site to check scope staleness ahead
of the `confirmed` guard and show the existing "Gateway connection
replaced ... Try again." toast on abort, matching the pattern
deleteSessionGroup already used for the same race. This also fixes a
regression the PR had introduced in a pre-existing main test
(app-sidebar.test.ts's "keeps a reconnected group delete retryable
after its confirm opened"), which is what failed
checks-node-compact-large-22 and checks-ui on the previous head.
2026-08-15 20:18:48 -03:00
Vyctor H. Brzezowski 94552862ad fix(ui): persist the sidebar session sort selection (#124277)
The sessions sidebar held its sort mode in component state only, so a
reload silently reverted the user's Created/Last updated/People choice
back to Created while every sibling sidebar preference persisted.

Store the mode under openclaw:sidebar:sessions:sort-mode alongside the
existing sidebar preference keys and read it back at init. Resolution of
a gateway-denied People sort now lives in one helper shared by the render
path and the setter, keeping the element net-neutral in lines.
2026-08-15 19:57:42 -03:00
Vyctor H. Brzezowski 1db8fe4d4f fix(ui): reject blank required strings in Control UI update readers (#124264)
* fix(ui): reject blank required strings in Control UI update readers

The Control UI update readers accepted `""` for fields the canonical
UpdateAvailableSchema/UpdateScheduleStateSchema declare as NonEmptyString,
so a malformed Gateway payload rendered an update banner with blank
version text.

Root cause is the reader treating "is a string" as the contract for
fields whose canonical contract is "is a non-empty string". Fixed at that
boundary with the protocol's own dependency-free `isNonEmptyProtocolString`
primitive: required NonEmptyString fields now reject the payload when
blank, and optional ones are dropped rather than discarding the whole
payload.

Deliberately does NOT re-derive the canonical schema in the browser:

- `additionalProperties: false` is a producer-side contract the Gateway
  enforces on its own outbound results (src/gateway/server-methods/update.ts).
  Rejecting unknown keys client-side would turn every additive protocol
  field into a blank overlay, since a service-worker-cached document keeps
  an older bundle across a Gateway upgrade.
- Canonical `maxLength` counts grapheme clusters (typebox 1.3.6
  guard/string.mjs), so the previous `subject.length <= 120` copy dropped
  valid emoji subjects the schema accepts. The bound is producer-side and
  is gone rather than approximated.
- typebox itself stays out of this module: it is in the Control UI startup
  graph, which has a hard gzip budget (scripts/check-control-ui-performance.mts).

Drift is now pinned in tests instead of duplicated in production: the
boundary suite runs fixtures through both the reader and Value.Check over
the canonical schemas, asserting that anything the schema accepts still
reaches the overlay.

Production LOC: net -7 against main.

* fix(ui): restore the five-entry cap on tolerant commit filtering

readUpdateAvailableValue's per-entry filter/map dropped the existing
five-commit bound while switching from all-or-nothing rejection to
tolerant filtering. The canonical schema caps commits at 5
(packages/gateway-protocol/src/schema/config.ts), and the Updates
page renders every entry this reader returns, so the cap is the
protocol's own render-side contract, not extra strictness. Slice to
MAX_COMMITS after filtering and cover an eight-entry valid payload.
2026-08-15 19:47:38 -03:00
Vyctor H. Brzezowski 5374c110fb fix(ui): make file-link label disambiguation near-linear (#124258)
* fix(ui): make file-link label disambiguation near-linear

shortestFileLabels rescanned every path list at each suffix depth to
find the shortest unambiguous label, which is quadratic in path
count. A Markdown message with thousands of distinct file references
could stall the chat renderer. Replace the repeated scan with one
reversed-segment suffix trie built per render, so each path's label
depth is found with one child lookup per depth instead of a
comparison against every other path. Output is unchanged.

Fixes #124230

* test(ui): assert file-label lookup cost stays linear

Add a deterministic performance-contract regression for
shortestFileLabels: count Map#get calls via vi.spyOn while the trie
runs at two path-count scales and assert the growth stays linear
(generous band) rather than the pre-fix O(n^2) full-list rescan's
quadratic signature. No wall-clock timing, so it can't flake under CI
load; verified locally that it fails deterministically against the
pre-fix implementation and passes clean against the fix.
2026-08-15 19:41:24 -03:00
Vyctor H. Brzezowski 8a3fff21a8 fix(ui): stop inferring session sharing identity kind from ID strings (#124257)
* fix(ui): drive session sharing icon from identity type

Remove the ID-string channel heuristic in the chat sharing menu.
Sharing identities are typed human/agent/system by the Gateway
protocol; there is no channel identity kind, so a human profile
whose ID happened to contain "channel:" was misrendered with the
channel glyph instead of the owner chip. Presentation now branches
on identity.type only.

* test(ui): describe the sharing fixture ID as opaque, not canonical

ClawSweeper flagged the comment on the channel-shaped human ID case:
it implied channel-shaped IDs are canonical for human profiles, but
durable profile IDs are generated UUIDs. The real supported path is
an opaque human identity ID (e.g. an inbound SenderId) that can
happen to contain "channel:" as a substring.
2026-08-15 19:34:15 -03:00
Vyctor H. Brzezowski 80ee4b97e5 fix(ui): use native placement for the lobster dismiss menu (#124261)
* fix(ui): use native placement for the lobster dismiss menu

The dismiss menu pre-clamped the pointer anchor against a hard-coded
264x80 guess before opening a bottom-start dropdown. Web Awesome's
dropdown enables `flip` without ever setting `flip-fallback-placements`,
so flip can never move the popup off its preferred placement; against
a bottom-start anchor pinned to the footer's bottom edge, that left
`auto-size` as the only thing that could react, shrinking the menu and
scrolling its two items whenever content grew past the guessed height.

Since the pet always sits on the footer ledge, anchor the popup with
`top-start` instead, the placement that already has room, and pass the
raw pointer position straight through. `shift` and `auto-size`, already
enabled on every wa-dropdown, then size the popup from its real content
instead of a guess that breaks under longer labels or larger type scale.

* fix(ui): correct the type-scale test and floating-ui rationale

The "enlarged type scale" e2e coverage overrode --wa-font-size-smaller/
--wa-line-height-condensed, but the dismiss menu's items render through
the app's own .session-menu__item class (layout.css), which sets an
explicit font-size from --control-ui-text-scale and wins over
wa-dropdown-item's shadow styles - the override was a no-op, which is
why the added test failed in CI. Switch it to set --control-ui-text-scale
the way bootstrap.ts does at runtime, and assert the label's computed
font-size and its own unclamped height both grow versus a baseline (the
row's 28px min-height floor otherwise hides a no-op).

Also correct the PR's floating-ui rationale: @floating-ui/core's flip
middleware does not treat an empty flip-fallback-placements as "no
fallback" - "" is JS-falsy, so it computes a real candidate list via
getExpandedPlacements. The actual reason a cramped placement can still
get shrunk instead of relocated is that wa-dropdown's auto-size (size)
middleware runs after flip and clamps whatever placement flip lands on.

* fix(ui): condense lobster dismiss-menu placement comment

ClawSweeper P3: the 12-line version-pinned comment explaining top-start
placement was the sole reason production stayed net +2. Trim it to the
repo's inline-comment doctrine (1-3 lines, why/contract/bad-outcome, no
version-pinned essay).
2026-08-15 19:28:39 -03:00
Vyctor H. Brzezowski 195913ad98 fix(ui): commit queued-message reorders as one durable batch write (#124266)
* fix(ui): commit queued-message reorders as one durable batch write

The reorder loop persisted each changed row with a separate
sessionStorage read-modify-write. A failure partway through left the
durable queue in a mixed permutation that matched neither the old nor
the new order.

Add a batch compare-and-set in composer persistence that validates
every affected row against one fresh read, then commits the whole
permutation with a single write and read-back verification. The
existing single-row update now delegates to this batch primitive.
Callers still applying an in-memory-only move (a row not yet admitted
to storage) keep using the direct projection path, since that never
touches sessionStorage.

Fixes #124233

* test(ui): cover two-row stale-CAS rejection and browser reload proof for queue reorder

Adds a regression that exercises the batch compare-and-set primitive
directly with a two-row permutation where one row went stale from a
concurrent writer between read and commit, proving the whole batch
rejects instead of committing a mixed reorder. Also adds a Control UI
E2E scenario (mocked Gateway, real Chromium) that reorders an offline
queue, injects a sessionStorage failure mid-batch, reloads the page,
and confirms the durable and rendered order both stay the untouched
original — never a mix of the attempted and prior permutations.
2026-08-15 19:03:06 -03:00
Vyctor H. Brzezowski 4be9091a3e refactor(ui): unify composer menu active-option scrolling (#124256)
* refactor(ui): unify composer menu active-option scrolling

The slash-command and skill-mention composer menus each kept their own
byte-identical algorithm for scrolling the keyboard-active option into
view inside their ".slash-menu__scroll" viewport. Move that algorithm
into the shared chat-composer-dom module as
scrollActiveMenuOptionIntoView(activeId), and have each menu owner keep
resolving its own active-option id while delegating the scroll math to
the shared helper.

Fixes #124238

* refactor(ui): inline forwarding scroll wrappers in composer menus

scrollActiveSkillMenuOptionIntoView and scrollActiveSlashMenuOptionIntoView
only forwarded their state/paneId args to getActive*MenuOptionId and the
shared scrollActiveMenuOptionIntoView helper. Delete both wrappers and have
the chat-composer.ts keydown call sites resolve the active option id and
call the shared geometry helper directly, since both id resolvers were
already exported and consumed elsewhere.
2026-08-15 18:57:40 -03:00
Vyctor H. Brzezowski abe9800777 test(ui): assert positive New Session draft behavior (#124253)
* test(ui): assert positive New Session draft behavior

Replace the New Session draft E2E's only assertion, a zero-count check
against a CSS selector that appears nowhere in production code, with
positive coverage: the draft text stays on the composer, the route and
sidebar stay put, no sessions.create request fires before submit, and
submitting creates exactly one session with canonical navigation.

Fixes #124237

* test(ui): settle before asserting exactly-once sessions.create

Reorders the exactly-once sessions.create assertion to run after the
canonical navigation-settle poll (the same signal the neighboring
submission test waits on), instead of right after waitForRequest
resolves. The old order could pass prematurely if a late duplicate
create landed after the count check but before navigation completed.
2026-08-15 18:51:34 -03:00
Vyctor H. Brzezowski ac6940641c fix(ui): keep GitHub deep-link labels destination-specific (#124252)
Gate compact owner/repo#N label rewriting on exact issue/pull root
URLs, so links with a trailing path segment, query, or fragment keep
their full generated label instead of being collapsed to a generic
item reference. Item identity parsing used by the hovercard preview
and navigation is unchanged.
2026-08-15 18:47:02 -03:00
Vyctor H. Brzezowski f4910cbb38 fix(ui): standardize scrollbar styling across the Control UI (#124285)
* fix(ui): standardize scrollbar styling across the Control UI

Give the Control UI one canonical scrollbar definition (a 12px hit
target with a thin, muted-at-rest, 6px painted thumb, derived from
--muted so every theme family and both modes follow with no override)
and delete the sixteen scattered per-surface overrides that used to
make every scroll container look different.

Shadow-DOM parity ships in the same change: a grouped ::part() rule
covers Web Awesome menus/listboxes, and a shared Lit fragment covers
the terminal/browser/desktop panels' own shadow roots, since neither
::-webkit-scrollbar* rules nor scrollbar-width cross a shadow
boundary on their own.

Fixes #124272

* fix(ui): reach every Web Awesome scroll part through generic selectors

The canonical scrollbar group enumerated six menu hosts plus the settings
select, so every unlisted Web Awesome surface kept the raw platform
scrollbar: a Chromium probe against the previous stylesheet reports
width:auto and a fully transparent thumb for wa-dropdown::part(menu),
wa-select::part(listbox), and wa-popover::part(body).

Style the three generic part families instead -- the same host group the
shared corner treatment already uses at base.css:435 -- which covers every
dropdown, listbox, and popover in the UI and removes 23 production lines.

The new-session composer's ten-line cap test asserted thinness as a
<=6px hit target, which was the old shrink-the-target recipe the canonical
profile replaces. It now asserts the real contract: a 12px hit target
painting a 6px thumb through the transparent border. The chat-page e2e
gains a genericity guard that walks every wa-dropdown/wa-select/wa-popover
on the page and requires each scroll part to report the canonical width,
so an unlisted component cannot silently regress.
2026-08-15 18:41:08 -03:00
Peter Steinberger 50df307fab test(ui): consolidate dynamic route startup coverage (#124287) 2026-08-15 14:29:16 -07:00
Vyctor H. Brzezowski 73c45824e1 refactor(ui): remove unreachable assistant message expanded flag (#124263)
Loaded full assistant messages always render in full, so the loaded
state's expanded boolean and its toggle branch could never be reached
by the automatic loader or the exhausted-retry action. Drop the dead
field and branch, and shrink the browser E2E fixture from an
8,000-plus character architectural essay describing the removed field
to a compact deterministic Markdown sample that still exceeds the
transport preview limit.

Fixes #124235
2026-08-15 18:15:40 -03:00
Vyctor H. Brzezowski 292506566b improve(control-ui): continuous corner curvature with a radius scale (#124129)
* improve(control-ui): continuous corner curvature with a radius scale

Route every step of the Control UI radius scale through one knob and, on
engines that can draw a superellipse, widen it by 1.25 while switching the
app's surface families to continuous corner curvature. A corner that keeps
curving into the edge reads as a shorter arc than a circular corner of the
same radius, so the scale gives that visual weight back.

The whole refinement sits behind @supports (corner-shape: superellipse(1.5)),
so engines without it keep today's radii and today's arcs unchanged. The shape
is opt-in per surface: a superellipse flattens the ends of anything fully
rounded, and pills, circles, avatars and status dots are a third of this app's
corners.

Menu item radius is now derived from the menu panel radius minus its padding,
which is the optical relation its comment already documented and which now
holds at any corner scale.

* test(control-ui): assert chat radii against the live corner scale

* fix(ui): scope the corner-curvature radius scale to opted-in surfaces

The @supports block scaled --openclaw-corner-radius-scale at :root, so
every --radius-* consumer inherited the 1.25 multiplier — including
surfaces that never opted into corner-shape (e.g. .run-inspector__panel,
which stayed `round` but drew an inflated 12.5px radius) and the radius
tokens mcp-app-theme.ts exports to embedded MCP apps.

The canonical --radius-sm/md/lg/xl/--radius tokens at :root are back to
their pre-refinement literal values. The scaled radii now live only on
the selector list that also receives corner-shape, redeclaring the
tokens locally so the widened radius stays contained to opted-in
surfaces and their descendants. Menu panels needed their derived
--menu-radius/--menu-item-radius redeclared too (a custom property's
nested var() resolves relative to where it's declared, not where it's
read, so redeclaring only --radius-md wouldn't reach them), and
wa-dropdown/wa-popover/wa-select scale on the host rather than the
::part() panel since slotted menu items inherit from the light-DOM host,
not from the shadow part they render inside.

Adds regression coverage for the excluded consumer and the exported MCP
theme tokens to corner-shape.browser.test.ts.

* fix(ui): stop redeclared corner-curvature radius tokens from leaking to nested descendants

Round 1 fixed the :root-level leak by redeclaring --radius-* as an
inheritable custom property on each opted-in container instead. That
still leaks: any non-opted descendant nested inside an opted surface
inherits the container's redeclared token too. Proof: .settings-segmented
nested inside .settings-group rendered 12.5px round instead of its
canonical 10px, and .agent-chat__search-bar's own --radius-lg-derived
corners doubled up incorrectly depending on ancestor nesting.

Every opted selector now sets border-radius as a direct property computed
from the scale, not an inheritable token — border-radius does not
inherit, so nested non-opted descendants keep reading the untouched
canonical --radius-* from :root. Each selector is specificity-doubled
(`.card.card`) rather than !important, so it reliably outranks its own
bare-class declaration in a later-loaded file without silently clobbering
a genuinely more specific existing override elsewhere in the app (e.g.
`.chat:has(> .agent-chat__search-bar)`, `.chat-task-detail__transcript >
.chat-thread`) — both surfaced as regressions during this fix and are
preserved by design now. The one exception is the generic
wa-dropdown/wa-popover/wa-select ::part() panel selector, which by design
must outrank dozens of concrete per-feature panel classes app-wide; a bare
type selector can never out-specificity a class selector, so that rule
keeps !important.

--menu-item-radius at :root also serializes back to the byte-identical
`var(--radius-sm)` from base f21d34d71d in the unscaled fallback path;
the derived panel-minus-padding formula now lives only in the scaled
wa-dropdown/wa-popover/wa-select host redeclaration where it's needed.

Adds a nested-inheritance regression case to
corner-shape.browser.test.ts (.settings-segmented inside .settings-group)
and a .settings-group entry to prove the container itself still scales.

* fix(ui): pair scale and corner-shape on every corner-curvature selector

ClawSweeper caught two more instances of scale and shape traveling apart:

1. .option-card__choice wasn't in the corner-curvature list at all, despite
   being a real nested member of the .option-card family (a button inside
   .option-card__choices inside .option-card, drawn with the same 14px
   --radius-lg step as .card/.settings-group). It kept its canonical 14px
   round corner even under the scale.
2. .slash-menu-item / .chat-reply-context-menu button / .chat-selection-popup
   button picked up the scaled radius from round 2 but never got
   corner-shape, so their corners grew without curving into a squircle —
   scale without shape, the same incoherence class as (1) in reverse.

Decision on (2): items get corner-shape added (not scale removed). The
panel they sit in is already squircle; an item whose radius grows but stays
circular reads as an unrelated circle poking out of the panel's superellipse,
not a coherent nested row.

A final invariant pass over the full @supports block (every selector that
sets a live border-radius must also set corner-shape, and vice versa) found
one more forgotten member beyond the two reported: .agent-chat__search-bar
scaled its own (bottom-only) radius but never carried corner-shape, same gap
as the menu items. Fixed alongside the reported two. Every remaining
selector in the block already pairs the two; the wa-dropdown/wa-popover/
wa-select host rule is the one correct exception, since it only carries
custom properties for descendants and draws no border of its own.

Test coverage now exercises each surface's real nested DOM instead of a
synthetic flat selector: option-card.ts's actual button-inside-choices-
inside-card structure, chat-composer-slash-menu.ts's item three levels
below the panel, and chat-thread-interactions.ts's plain <button> item
matched via descendant tag selector. Added a corner field to CornerCase so
the harness can probe the correct physical corner (search-bar's bottom-left
is its only non-zero corner) instead of universally reading top-left. All
four new/changed cases are proven to fail on pre-fix base.css with the
exact "radius scaled, shape stuck at round" (or, for option-card__choice,
"not opted in at all") signature before the corresponding fix.
2026-08-15 14:48:56 -03:00
Peter Steinberger f4a047b87c test: trim residual subset assertions (#124203) 2026-08-15 09:11:36 -07:00
Peter Steinberger aad64693ba test: remove redundant subset assertions (#124198) 2026-08-15 08:57:45 -07:00
WhatsSkiLL 632581477f fix(session-catalog): preserve explicit agent ownership across UI and CLI (#123899)
* fix(session-catalog): preserve explicit agent ownership across UI and CLI

* fix(session-catalog): repair owner routing regressions

* Fix-session-catalog-CI-checks

* test(ui): split chat catalog pane coverage

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>

* refactor(session-catalog): bind Codex homes per request

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>

* refactor(session-catalog): centralize owner resolution

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>

* fix(codex): resolve session fork against the adopted home

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>

* refactor(ui): extract chat session discussion

* test(sessions): stabilize archive worker responsiveness check

---------

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-15 04:47:28 -07:00
Peter Steinberger f916e690c0 test: remove UI and native test residue (#124152) 2026-08-15 04:08:22 -07:00
Peter Steinberger 939aa7dddc fix(ui): keep queued message removals exact (#124150) 2026-08-15 03:46:47 -07:00
Peter Steinberger 717b2d7474 fix(ui): prevent oversized request frames from disconnecting (#124139)
* fix(ui): reject oversized Gateway request frames

* fix(ui): size requests without params exactly

* test(ui): align mock attachment limits with Gateway
2026-08-15 03:10:19 -07:00
Peter Steinberger 5b96cbc52d fix(ui): preserve agent file lifecycle outcomes (#124140) 2026-08-15 02:57:37 -07:00
Peter Steinberger 23dfbee041 fix(ui): scope global outbox run checks by agent (#124121) 2026-08-15 02:18:09 -07:00