* feat(gateway): add remote-exec cloud placements
* feat(codex): run cloud turns through remote exec
* fix(sandbox): quote ssh_config path directives containing whitespace
Crabbox lease keys default to ~/Library/Application Support/... on macOS;
unquoted IdentityFile/UserKnownHostsFile/CertificateFile arguments tokenize
on the space and fail as 'extra arguments'. Found via live remote-exec
cloud-worker proof.
* test: consolidate gateway maintenance schedule coverage
* fix(ci): invalidate plugin sdk declarations on state changes
* fix(ui): add persistent lobster dismissal
* fix(ui): keep the lobster dismiss menu from scrolling its own items
The dismiss menu anchors a synthetic trigger at the raw pointer position,
and the pet always sits on the sidebar footer ledge at the bottom of the
viewport. Web Awesome caps its popup to `--auto-size-available-height`,
and its `size` middleware runs after `flip`, so the menu was shrunk in
place to 60px against 64px of content instead of moving up. The two
dismissal items then scrolled inside a 6px overflow, which shows as a
scrollbar for anyone running the system setting that always renders them.
Clamp the anchor to the viewport the way every other pointer-anchored
menu already does (session-menu.ts:264, catalog-session-menu.ts:55,
native-link-menu.ts, sidebar-menus-controller.ts:270), so the popup keeps
the room it needs and renders both items in full.
* docs(web): describe the lobster dismiss menu options
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: vyctorbrzezowski <krzyszchweski@gmail.com>
* fix(ui): fork active sessions from stable history
* fix(protocol): align active fork types
* test(ui): match main-session fork routing
* fix(sessions): unify stable fork admission
* fix(native): fork active sessions from stable history
* fix(android): fork active sessions from stable history
* style(android): simplify active fork result handling
* fix(native): preserve legacy fork transport API
* test(native): complete legacy fork transport stub
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
chat links used --accent while user bubbles are filled from --accent-subtle (and peer bubbles from a per-sender hue), so links sat on their own hue at 3.95:1 worst case. New --link/--link-hover tokens derive from each palette's audited --accent-hover and clear WCAG AA on every bubble surface and sender hue; the hover opacity fade, which lowered contrast, is replaced by a color shift.
The header-to-transcript blend spanned the full pane width, so it painted over the strip where the transcript scrollbar lives and the thumb vanished under the fade while scrolling. Stop the blend one transcript gutter short of both pane edges and make that gutter a token shared with the transcript column, which keeps every pixel of content faded while the scrollbar stays visible (symmetric so RTL keeps the same clearance).
An auto-disabled job (10 consecutive run failures / 3 schedule errors)
rendered as plain 'Paused' and simultaneously vanished from the failed
chip, the failing count, and the error dot — the escalation removed
visibility exactly when the problem became permanent. The recorded fact
(state.autoDisabled, already public and used by CLI/doctor) now renders as
a distinct danger note with the reason and count, and auto-disabled jobs
stay in the active-failure predicate.
* fix(ui): report connection loss instead of silently dropping session actions
patchSession returned 'stale' with no message when the gateway dropped
before a queued dialog submit (rename and similar) — the dialog closed and
the edit vanished with no visible outcome. loadCheckpoint had the sibling
gap: expanding a row while disconnected left the drawer claiming 'No
checkpoints' beside a nonzero checkpoint badge. Both now surface the
existing actionRequiresConnection message at their owning error surface.
* test: widen patchSession test-support patch type for label patches
The session row's terminal stamp (status/endedAt/runtimeMs) and its usage
persist (outputTokens) are separate gateway writes. resolveTurnRecap settled
on the first fresh terminal row and froze whatever outputTokens it carried —
frequently the PREVIOUS turn's count. Treat an unchanged-from-baseline value
as that lag and fall back to the watched run's live usage-stream counter
(captured while watching, since the usage map entry dies at lifecycle end).
* fix(ui): complete pairing setup lifecycle
Redeemed and expired mobile setup codes stayed on screen as usable QR
codes, so a successful pairing had no visible outcome and expired bearer
material still looked live.
The Gateway now mints an opaque, non-authorizing setupId beside each
setup credential, returns its authoritative expiresAtMs, records the
terminal outcome of the exact redemption, and broadcasts
device.pair.setup.completed. Because that broadcast is dropped for
buffered operator sockets, the completion is persisted first and can be
reconciled through the new device.pair.setupStatus method: the Control UI
asks for the recorded outcome before it may present a credential as
expired, so a pairing that succeeds is never shown as a failure.
The Control UI models one closed lifecycle (selection, loading, waiting,
success, expired, error) correlated only by the active setupId, and
Pairing help now carries the external-link affordance.
* fix(ui): preserve unknown pairing outcomes
* test(ui): target pairing terminal headings
* test(ui): align pairing completion fixture
* fix(gateway-protocol): decode setup-code results from older gateways
Older protocol-v4 gateways omit the new setupId and expiresAtMs
lifecycle fields, so requiring them in the generated native model broke
decoding an existing device.pair.setupCode response. Keep both optional
at the wire boundary, require lifecycle metadata before the Control UI
enters its waiting state so a missing outcome stays visible, and cover
the legacy payload with Swift and schema regressions.
* fix(ui): surface rejected pairing dialog loads
The lazy pairing chunk could reject while its overlay was already open,
leaving the shell rendering nothing at all. Record the rejection on the
shell and render a recoverable modal with a reason and a retry so the
open action always ends in a visible outcome.
* fix(state): preserve pairing setup schema compatibility
* test(gateway): cover pairing setup release train
* fix(gateway): commit pairing setup completion atomically
* refactor(state): distinguish setup transaction helpers
* refactor(state): remove obsolete bootstrap restore path
* fix(gateway): preserve setup handoff type safety
* fix(gateway): keep pairing completion terminal after consume
* fix(gateway): validate Watch binding during setup commit
* fix(gateway): revalidate setup credential expiry at commit
* chore: refresh Plugin SDK API contracts after rebase
* fix(pairing): prune expired setup completions
* chore: retrigger CI
* fix(protocol): deduplicate setup expiry field
* fix(protocol): refresh pairing setup clients
* fix(gateway): make pairing setup completion durable
* fix(ui): retire expired pairing credentials immediately
* fix(ui): keep pairing dialog visible while loading
* fix(macos): align setup result initializer order
* fix(gateway): restore generic bootstrap retries
* chore(ui): record pairing startup budget
* chore(ui): refresh pairing startup budget
* style(gateway): format maintenance imports
* test(gateway): cover session-sharing mock
* fix(state): defer setup correlation schema
---------
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
The archive toast outlives the Sessions page (toast host is app-shell
level), but its Undo action was gated on the page's request scope, which
dies the moment the operator navigates away — clicking Undo within the 6s
window then silently did nothing. Run the un-archive through the shared
sessions mutations store directly (it already fails closed on connection
replacement and publishes errors), mirroring the always-mounted sidebar
undo sibling.
* fix(ui): stop flagging running automations as overdue and show their state
The gateway intentionally leaves nextRunAtMs past-due while a run executes
(it advances only on the outcome), and agentTurn runs may take up to an
hour — 12x the sidebar's 5-minute overdue grace. The overdue chip and the
cron table therefore reported healthy in-flight jobs as 'overdue' / 'next
run: X minutes ago'. runningAtMs is the recorded fact for an in-flight run
(already public and used by the CLI's status), but no UI surface read it.
Add the shared isCronJobRunning helper, exclude running jobs from the
overdue filter, and render 'Running' in the next-run cell.
* test(cron): move running-state row test beside the capped view suite
When a gateway connection is replaced while a config form draft is
dirty, autosave intentionally latches off (autoSaveRequiresExplicitSubmit)
until an explicit Save — but the latch was invisible: the indicator
rendered nothing on idle, form mode has no Save button, and every
subsequent edit (plus the dispose-time teardown flush) silently never
saved. A recorded fact existed in the coordinator with no read where
the operator needed it.
The latch now projects a visible "paused" autosave status owned by the
same arm/clear transitions (conflict outranks it; local edits cannot
clear it), and the save indicator renders "Autosave paused after
reconnect" with a Save action reusing the existing retry plumbing.
* fix(ui): block renaming config map keys that still hold redacted secrets
Renaming a map entry whose value is the server redaction sentinel moves
the sentinel to a new key. The gateway's exact-key restore then fails
closed forever (dead-end draft with a Retry that can never succeed), and
when the rename is folded with a delete of the target key inside one
autosave window, the restore silently binds the deleted entry's old
credential under the new name and drops the intended one — a green
Saved with the wrong secret.
The rename handler is the only owner that knows a sentinel was moved
rather than typed, so it now rejects the rename like the existing
duplicate-key guard and explains the retype path via input validity.
Plain values rename exactly as before.
* chore(ui): record startup JS growth for the redacted-rename guard string
The renameRedactedBlocked validity message in the startup en locale adds
65 gzip bytes over the recorded baseline; record the CI-measured size
per the established baseline pattern.
t() interpolation used || so a provided empty-string param fell through to
the visible {placeholder} fallback meant for missing params — e.g. the
devices page rendered 'Bound to {node}' whenever an agent binding was the
empty string. ?? keeps the missing-param debugging aid and renders provided
empties as empty.
* fix(ui): validate attachment size against hello policy before encoding
The gateway advertises decoded-size attachment ceilings in hello-ok
policy.attachments precisely so clients can validate before sending,
but the Control UI never read them. A file over ~18.7MB decoded
expands past the gateway's 25MiB WS frame cap: ws hard-terminates the
connection (1009) for every pane and session in the tab, the message
parks as waiting-reconnect with wrong copy, and manual retry kills the
connection again.
appendAttachmentFiles — the single funnel for file input, paste, drop,
and plus-menu — now rejects oversized files up front with a named
toast, mirroring the terminal-upload sibling guard. Limits thread from
hello policy at both composer assembly sites (chat pane, new-session);
absent policy means no client-side cap, matching older gateways.
* refactor: keep chat-pane-render under the line cap
The attachmentLimits prop tipped chat-pane-render.ts to 701 counted
lines (cap 700, main sits exactly at cap). Compact the checkpoint
deep-link callback and read limits from the pane's own hello snapshot.
* fix(ui): hiding a catalog no longer makes its adopted sessions vanish everywhere
The adopted-key exclusion set was computed from the unfiltered catalog list
while the renderer shows only visible catalogs (hidden ids filtered; none
under the Archived filter). Hiding a catalog therefore removed its adopted
sessions — possibly running, unread, or selected — from the entire sidebar
with no row anywhere. One visibleSessionCatalogs() projection now feeds both
the renderer and the exclusion; the subclass's duplicate filter is deleted.
* refactor: fold sidebar catalog projection into its owner module
The visibleSessionCatalogs projection and adopted-PR-row merge pushed
app-sidebar-session-navigation.ts over the 700-line lint cap. Move the
projection predicate to app-sidebar-session-catalogs.ts (the catalog
helper owner) and the PR-row merge to the navigation-logic module,
inlining the single-caller passthrough into the indicators controller.
* fix(ui): gateway client rotation silently discarded plain staged attachments
replacePaneStagedAttachmentGatewayOwner discarded every staged
attachment when the gateway client object rotated — which happens on
event-gap recovery (laptop sleep/wake reconnect) and after plugin
install/enable, not just on real credential changes. Draft text
survived; attachments vanished with no error, so a user who staged
screenshots before sleeping delivered a text-only message.
Only browser-annotation attachments actually depend on the old client
(their Undo context dies with it). Plain file/image payloads are
client-local data URLs; rotation now drops annotation-backed
attachments and keeps the rest, in both live state and memory
fallbacks.
* test: align annotation-lifecycle rotation tests with selective discard
Three tests pinned the old wholesale-discard contract; rotation now
drops only annotation-backed attachments while plain payloads survive
(live state, fallbacks, and post-undo-toast state).
ui-e2e intermittently failed with `page.waitForFunction: Timeout 10000ms
exceeded` on loaded CI runners (model-agent-scoping, terminal-embedded),
both green on rerun. The 10s deadlines were repo-imposed at the mock
Gateway helper and readiness helper, plus twelve per-test
setDefaultTimeout sprinkles.
Give the harness one owner for the wait budget: 30s under CI, 10s
locally, mirroring the ui-e2e vitest config's existing expect.poll
reasoning. Suite-created contexts inherit it, so two per-test overrides
are deleted outright.
The UI carried three token formatters with proven divergence: 15,600
rendered as 16k on the Sessions page (formatTokens) but 15.6k in the chat
composer and /usage (formatCompactTokenCount); 999,600 as 1.0M vs 999.6k.
The message-meta cost line also bypassed formatCost with a hardcoded
toFixed(4), showing $1.2346 next to a popover showing $1.23.
formatCompactTokenCount is now the one owner: it absorbs formatTokens'
nullish fallback and model-providers' local billion-suffix wrapper (both
deleted), and the message cost line goes through formatCost. Production
net −19.