sessions.patch resolves null without sending a request when the
connection scope cannot be captured, but the read-marker guard only
unlatched on promise rejection — so the once-per-episode latch stayed
set with no request in flight, and the unread badge stayed lit on the
open session until navigating away and back. Treat a null resolution
like a failure.
* fix(ui): automations model picker shows raw "common.default" key instead of Default
t("common.default") never existed in the catalog, so the picker's
inherit option rendered the literal key string in every locale
(introduced in #122964 when cron's free-text model input became the
shared model picker). Use the existing quickSettings.model.default key.
Same-sweep i18n call-site drift fixes:
- sessions view Reasoning override now routes through
buildSessionLevelOptions like its Fast/Verbose siblings (adds the
missing sessionsView.stream key; deletes the unlabeled duplicate
withCurrentOption helper)
- activity entry summary collapses the placeholder-only
activity.entrySummary pass-through (two silently dropped params) to
the direct label call; orphan key removed
- group-delete confirm drops its dead {group} param (string has no
placeholder since the reword)
* test(ui): compact multi-line expects in cron view test to stay under max-lines
The file sits at the 1000 counted-line cap on main; extracting named
intermediates keeps the new inherit-option regression net-neutral.
* fix(ui): typing a decimal point into numeric config fields no longer deletes the stored value
For <input type=number>, partial numeric text ("0.", "-", "1e")
reports value === "" with validity.badInput. The input handlers
treated that as an intentional clear: they committed undefined, the
800ms autosave persisted the removal, and the identity resync wiped the
user's in-progress text from the DOM. Typing 0.5 into sampleRate stored
5.
Resolve numeric input state (badInput/empty/value) once in a shared
helper used by both renderTextInput's number branch and
renderNumberInput, absorbing their duplicated parse/commit/revalidate
logic. badInput now surfaces the invalid-number validity message
without committing; genuine clears keep removing optional overrides.
* test(ui): use plain validity objects instead of spreading ValidityState
Spreading a class instance drops its prototype (no-misused-spread); prod
only reads validity.badInput, so a plain object is the honest stub.
* feat(secrets): add authenticated egress substitution proxy
* feat(secrets): bind egress substitution to hosts
* ci(codeql): classify egress proxy bypass tunnel in network boundary query
* refactor(proxy-capture): use the canonical IP parser instead of node:net
* fix(secrets): compare proxy tokens with a process-keyed MAC
The chat message meta popover read message.cost, a field no producer ever
writes — AssistantMessage nests cost under usage.cost — so the $ line was
permanently dead. Read usage.cost with the bare-field fallback removed to a
single expression.
The usage page empty state keyed off !data.totals, but the gateway always
returns a totals object (all-zero when idle), so the "no usage data yet"
card never rendered; it also ignored data.error, so a failed load could show
the empty card under the error callout. Key it off actual content and never
render it in the error state.
Adopted catalog keys are excluded from thread roots because the Coding
catalog section renders them live, but the selected-session fallback
re-insertion had no adopted guard — its lineageRoot sibling does. Select
an adopted Codex/Claude session and it appeared both as a top-level
thread and inside the catalog group, both aria-current. Add the same
guard to the fallback filter.
* fix(ui): report the outcome of a cross-device token rotation
Rotating another device's token ended with nothing on screen. The Gateway
returns the replacement only to a device rotating its own token
(shouldReturnRotatedDeviceToken; docs/cli/devices.md), so every rotation of
another device answered without a token and the Control UI, which only rendered
when one came back, showed no dialog, no message, and no error while the target
device was invalidated and disconnected.
device.token.rotate now records how it delivered the replacement in a
tokenDelivery discriminator ("in-band" | "withheld-cross-device"), additive and
optional so gateways that predate it still decode. The Devices page reports the
recorded fact rather than inferring one from a missing token: the show-once
reveal for an in-band rotation, and an outcome dialog for a withheld one that
states the token is not shown here, that the device was disconnected, that it
picks up the replacement on its next connect with a Gateway token or password,
and that a device which can only sign in with its device token has to be paired
again.
The reveal component takes an optional secret: with one, dismissal stays refused
because the value is unrecoverable; without one there is nothing to lose, so
Escape and backdrop close it like any dialog.
* fix(ui): lead the rotation outcome with what happened, not why
The withheld-rotation dialog opened on a negation, stacked three clauses of
protocol reasoning, and left "do I have to do anything?" to the last sentence.
It now reads outcome, then next step, then the one exception, with the security
rationale demoted to a muted trailing line: the device is named, told to be
signed out, and the common case says nothing else is needed. The rotate callback
carries the row's resolved label so the dialog names the same device the operator
clicked instead of rederiving the precedence.
The reveal component takes paragraphs plus an optional muted note, so an outcome
and its exception stay separate instead of collapsing into one block.
* fix(ui): give the rotation outcome a visual hierarchy
Four near-equal paragraphs in a plain box read as one undifferentiated block:
the reassurance, the conditional branch, and the footnote all carried the same
weight, and the calm text was rendered in muted grey because it reused
.exec-approval-sub.
The dialog now has zones. A success mark in the shared icon-tile geometry sits
beside the title, the two reassurance sentences carry full --text weight, the
one conditional branch becomes a house .callout.info so it cannot be skimmed
past, and the security rationale is a 12px muted footnote. Spacing follows the
card's 4px rhythm.
The acknowledge button follows the confirm-dialog convention: the accent button
is the action that commits something, so a show-once reveal keeps it and a
report of work already done uses the neutral button.
* fix(ui): collapse the rotation outcome to a single reassurance
The body zigzagged: it announced a new token, then raised a disconnect, then
spent the next line walking that alarm back. The middle beat existed only to be
contradicted, so it is gone -- the disconnect is transient and self-healing, and
naming it bought the reader nothing.
The title now carries the announcement and the device name, the body is the one
reassurance, and the callout drops its "this device" echo. The neutral dismiss
button gets --border-strong: .btn's resting border is within ~4/255 of --card in
dark, so it read as unpainted on this surface. The callout wraps balanced.
Both dialogs now pass a single message, so the paragraph-array support added for
the previous structure is removed rather than left speculative.
* fix(ui): key the rotation callout to an observable symptom
"If it can only sign in with its device token" asked the operator to know which
credential the device holds, which is internal state they cannot see. The
condition is now the symptom that state produces: the device does not come back
on its own. Same behavior, same technical truth, decidable from the Devices page.
* fix(ui): reject contradictory device-token rotation results
The Devices page inferred the outcome from an unchecked RPC payload, so an
explicit `tokenDelivery: "in-band"` with no token, a withheld result that
carried one, or a delivery mode this client predates all rendered as a
successful withheld rotation - telling the operator the device re-credentials
itself while their previous token was already invalid.
rotateDeviceToken now parses the response into the closed outcome: the two
current pairs, the two legacy omission states from gateways released before
tokenDelivery, and every other explicit pair through the existing error path,
which surfaces as the page's danger callout carrying the recovery step.
* chore(plugin-sdk): refresh the API contract for the rotation result schema
Registering DeviceTokenRotateResultSchema in PluginLifecycleProtocolSchemas is
what makes the generators emit the Swift model, and that registry sits in the
type closure of 18 plugin-sdk exports, so their closureHash entries move. This
is generator output rather than a hand edit: the export set and every declaration
are unchanged, only the hashes.
* fix(ui): reject malformed device-token rotation envelopes
The parser trusted the envelope around the fields it read. A null, scalar, or
empty payload carries neither tokenDelivery nor token, so it matched the legacy
omission state and produced the reassuring completion dialog after the previous
credential had already been invalidated. A blank token did the same, though the
result schema bounds token to a non-empty string.
Only DeviceTokenRotateResultSchema's shapes are accepted now: the payload must be
a record that identifies the grant it rotated - every Gateway answering this
method returns deviceId and role, before and after tokenDelivery existed - and
token must be either absent or a non-empty string. This has to happen here
because the browser Gateway client resolves frame.payload directly, so the
registered result schema never runs on the client.
The mid-flight-reconnect reveal test asserted on a two-field stub no Gateway
sends; it now uses the real response shape.
* fix(ui): reject rotation results that do not answer the request
The rotation parser accepted any envelope naming some device and role. It
never checked scopes or rotatedAtMs, both required by
DeviceTokenRotateResultSchema, and never compared the returned grant with the
one that was requested -- so an incomplete or unrelated reply opened a success
dialog claiming a credential the operator may still hold had been replaced.
Require the full result shape and bind it to the requested grant, comparing on
the same trim normalization the device-auth store applies. The page fixture
now echoes the requested grant instead of hardcoding device-1, which is what
let a cross-device test accept a success dialog for the wrong device.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(ui): complete the rotation fixture the epoch test resolves
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(ui): update token rotation E2E envelope
* fix(protocol): correlate rotated token delivery
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
Browsers can drop compositionend (element detach or blur mid-IME).
composerComposing persists across renders in the pane state map, so a
stuck flag killed Enter-send, input history keys, and command menus
until the Send button — which force-resets the flag — was clicked.
handleBlur was a half-repair of this invariant: it cleared the
composingDraft shadow state but not the flag. Clear both.
The expanded session rail section was aria-live="polite" and contains a
1Hz elapsed-run clock, so screen readers announced the time every second
for the whole run — the exact anti-pattern exec-approval-card documents.
Messages were also double-announced via the nested thread live region.
Drop the section-level live region; the thread keeps its own.
Also give the initials-avatar divs role="img" so their aria-label
(the sender name) is honored by AT instead of reading raw initials.
Search matched the full update.* schema, but the Updates page renders
only curated channel/auto rows and the Advanced page excludes the scoped
update section — so a hit on update.checkOnStart landed on a page where
the field is editable nowhere but the Raw editor. Generalize the
memory-only visibleMemorySchema special case into a per-route
visible-keys map so search promises exactly what each curated
destination can edit.
routeIdFromPath matched static route paths case-sensitively while
uirouter's pathKey lowercases them, so a /Usage deep link failed the
startApplicationRouter pre-gate and was rewritten to /chat even though
the router itself would have matched it. Lowercase the candidate path in
the pre-gate so both matchers agree.
The Control UI put the hello device token first in its ordered HTTP
credential candidates, but the plugin/catalog/workspace icon byte routes
only accept the gateway shared secret. Every icon fetch on every page
load therefore 401'd once before retrying with the working token — and
each of those 401s is recorded by the gateway as a shared-secret
brute-force failure, escalating loopback penalty delays (250ms-5s
serialized before the response) and risking the 5-minute remote IP
lockout, where the icon loader treats the resulting 429 as terminal.
Order shared secrets first; pairing-only browsers still reach the device
token as the final candidate. Also switch the profile page hero avatar
from a single-candidate fetch (device token only, permanently 401 with
no fallback) to the same ordered candidates helper.
The phone-width ellipsis fix (#123085) promoted a chat-composer-scoped
138px minimum onto the shared select-picker inline style, which beats
every consumer stylesheet. Containers that legitimately shrink below
138px overflow instead: the cron 'Every N <unit>' and stagger-window
unit pickers spill out of their minmax(0,1fr) grid cells at 320px, and
workboard's min-width:0 became dead. Cap the floor at the host width
with min(138px,100%); the readable-label floor is preserved everywhere
the container is wide enough (the mobile e2e asserts it at 364px). Also
fix the stale '8ch' comment from the pre-138px iteration.
Blanking the 'To' field in the Automations delivery editor omitted the
key from the cron.update patch. The Gateway merges delivery patches
by key presence, so the stored destination survived and every future
run kept announcing to the old chat with no error. Send an explicit
null clear on update, exactly like the accountId field one line above
(same bug class fixed there in #105762). Webhook mode is unaffected:
its blank-URL validation rejects the form before submit.
* fix(ui): retry failed full-assistant-message loads instead of pinning the truncated preview
Since ce466fccb0 removed the manual Show-more toggle, a failed
loadFullAssistantMessage left {status:error} that no renderer reads and the
render-time kick-off never retried (it only fired when no expansion state
existed). One transient RPC failure pinned the '...(truncated)...' preview
for the rest of the session with no visible outcome. Retry on later render
passes while attempts remain, bounded by revision so a dead loader cannot
hot-loop.
* fix(ui): render a retry affordance once automatic full-message loads exhaust
Addresses ClawSweeper P2 on the bounded-retry fix: after
FULL_MESSAGE_RETRY_REVISION_LIMIT the truncated preview previously stayed
silent. The exhausted error state now renders an inline error line with a
Retry button that re-enters the canonical loader via the same toggle path;
the retry bound is a named shared constant.
Since retained panes (#121625) every pane — hidden or presented — keeps
its sessions subscription alive, and applySessionsState marked the
pane's selected session read unconditionally. New activity in a
recently-viewed session was cleared by its hidden pane instantly: the
sidebar unread dot never stuck, agentStatus attention was wiped by the
read patch, and "Mark as unread" self-reverted. Gate the read-marking
on this.presented, matching applyActiveSessionBindings.
* fix(ui): keep composer pickers visible near viewport edges
Model and effort pickers now share viewport-aware, preferred-above placement through the anchored overlay. This replaces the fixed CSS positioning policy so either picker can flip and shift within the viewport.
* test(ui): move picker regression to model flow suite
Keep the viewport-placement regression in the existing model and reasoning E2E owner so the composer redesign suite remains below the max-lines gate without losing coverage.
* test(ui): wait for adaptive pickers before interaction
Wait for the Web Awesome popup content to become visible before measuring menus or sending keyboard input. This aligns existing E2E interactions with the asynchronous details toggle lifecycle.
* fix(ui): do not call a portal unreachable when policy refused the probe
The Portals page probes a portal with fetch before mounting its preview,
but a Content Security Policy refusal fails the same way a dead port
does, so a reachable portal was reported as unreachable. That fires
whenever the request Host differs from the hostname the browser used —
the Control UI reached through a portal, or a proxy that rewrites Host.
Frames obey frame-src, not connect-src, so a refused probe says nothing
about the portal: report it as blocked and mount the preview anyway.
Also stop an unrelated chat geometry assertion from flaking the merge
gate: boundingBox returns subpixel floats (16.000001907348633 on CI),
so compare with tolerance instead of exact equality.
Found by running a dev gateway on a cloud box and reaching its Control
UI through a portal; the browser console showed the connect-src refusal
for a portal that curl could reach.
* test(auth): isolate OAuth callback unit coverage
* test(ui): assert attachment failures through toast host
Clicking the sidebar item for the page you are already on pushed an
identical history entry every time, so browser Back appeared dead until
every duplicate was popped. navigateAndWait now downgrades push to
replace when the computed target equals the current history location,
reusing the existing sameRouteLocation helper; context.replace collapses
into the same navigateWithMode owner instead of a parallel copy.
* fix(ui): preserve selected automation owner
Keep the concrete selected agent on model lookup and new automation creation while the Automations page is filtered to All agents, avoiding AgentSelectionRequiredError in multi-agent setups.
* fix(ci): remove duplicate Codex test shard assignment
Keep run-attempt-state.test.ts in the light Codex app-server shard only so the full-suite inventory remains one-to-one.
Restyle the effort popover slider from a 3px line to a full-height capsule rail with a pill thumb.
Retune the fill and dots for the tall track.
Verify the result in both dark and light themes.
On narrow cards the nowrap bundled-app badge left the title as the only
shrinkable item in the title row, so "Apple Watch" / "Wear OS" broke
across two lines. Let the row wrap: the badge drops to its own line
(left-aligned under the title) while wide cards keep it inline right.
Move llama.cpp chat and local embeddings onto a verified externally managed llama-server runtime. Remove the in-process native runtime, forked embedding workers, and node-llama-cpp dependency while preserving guided setup, local GGUF models, tool-capable agent runs, diagnostics, and operator docs.
jsonSummaryLabel returned hardcoded English ('Object (7 keys)', 'Array (2
items)', 'JSON'), the chat-json-badge span carried a raw 'JSON' literal, and
the tool sender label was a raw 'Tool' string — all rendered on localized
chat surfaces while sibling strings go through t(). #115730's localization
sweep missed these. Raw-copy baseline shrinks by the two badge entries.
* fix(ui): report attachment read failures instead of dropping files silently
Unreadable drops and pastes (folder drops, permission-denied files) resolved
null in readAttachmentFile and were filtered out with no visible outcome —
the attachment simply never appeared. Name the skipped files in a toast while
still attaching the successful siblings; aborted batches stay silent.
* test: assert the read-failure toast message is a plain string (oxlint no-base-to-string)
Await intercepted banner-module routes and isolate optional-module failure from stale-chunk reloads. Settle and atomically sample responsive geometry. Complete probe handshakes before teardown, and assert portal server ownership instead of reusable ephemeral ports.
* test(ui): stabilize responsive layout geometry
* test(ui): fix lint errors in layout-settle helper
Rename the evaluate-scope selector binding and stop returning the rAF id
from the Promise executor (no-shadow, no-promise-executor-return).
The light terminal theme reused the dark-background ANSI palette (1.4-2.6:1
contrast on #f7f8fa; brightWhite was 1.06), and the intro banner hardcoded
256-color indices 223/216 that bypass the client theme entirely.
Light mode now gets its own darkened ANSI-16 palette (>=4.5:1, bright
variants darker for emphasis, brightWhite maps to strongest ink), and the
banner emits themable ANSI-16 yellow/bright-red so each mode owns its
rendering. Dark mode is visually unchanged.
* fix(control-ui): recover auth-cold model pickers
Keep configured auth-cold models visible but disabled in chat and new-session, block submission, and link Model Setup. Fixes#123080.
* fix(control-ui): preserve rebased model picker behavior
* fix(ui): split composer model picker under the lint size cap
Repairs main's red check-lint caused by the #123090 restore.
* fix(ui): align auth-cold picker CI contracts