* fix(apple): gate gateway RPC polling on the hello method catalog
Released 2026.7.x gateways authorize before method dispatch and reject
unknown methods with INVALID_REQUEST / "missing scope: operator.admin",
which never names the method, so the shared Swift kit's error-text
matchers could not detect an unsupported gateway: queued sends wedged in
a delay-free sessions.branches.list reconcile loop, question.list burned
its retry budget per health event, and progressCard.get fired a rejected
fetch per event.
Port the Android fix pattern (#126540): generalize the progressCard-only
transport seam into tri-state gatewayAdvertisesMethod(_:), make hello
catalog parsing distinguish absent (nil) from empty, route branch
listing through a catalog-checking dispatch point with a typed
BranchListingUnadvertisedError, tighten error-text matching to the
modern "unknown method:" shape, and skip question.list/progressCard.get
when unadvertised. Regression tests encode the exact 2026.7.1-2 wire
shape and fail pre-fix (A/B verified).
* fix(apple): keep explicit unsupported branch-listing replies releasing sends
ClawSweeper P1: the tightened matcher dropped the shipped acceptance of
explicit unsupported/unimplemented GatewayResponseError replies that
name sessions.branches.list, which would wedge queued sends on a
pre-catalog gateway emitting that shape. Collapse the matcher onto the
bridged localizedDescription (errorDescription always prefixes the
method name), preserving both legacy qualifier shapes while still
rejecting bare missing-scope denials and the old false-positive-prone
INVALID_REQUEST arm; add the releasing-send regression test.
* fix(apps): render legacy plan events when the Gateway lacks the progress-card store
progressCard.get ships in no released Gateway tag, so iOS/macOS on any current
release silently showed no status card after the durable-card migration. Restore
legacy stream:"plan" rendering, synthesized into the unified progress card and
gated on a tri-state capability probe so a dual-emitting Gateway never fights the
durable store. Mirrors the Android fix in #125444.
* fix(apps): invalidate progress-card capability on gateway route replacement
A replacement route may be a different Gateway, so a cached known-absent store
must not authorize the legacy plan fallback against a new Gateway that dual-emits
both sources. Clear and re-probe on .routeChanged.
* feat(apps): migrate iOS/macOS plan surface to the durable progress card
Replace the legacy stream:"plan" agent-event pipeline (runId-scoped state,
run-gated pill) with the sessionKey-scoped progress-card store: the shared
chat surface now renders progressCard.get snapshots, refetches on
progressCard.changed pokes with revision dedupe, clears on null-revision
pokes, and persists the card after the run completes. The card renders
markdown through the shared markdown view plus typed steps. Legacy Apple-side
plan handling (agent-event case, run-snapshot plan reconciliation,
OpenClawChatPlanStep parsing) is deleted; gateway emission stays for Android.
Removes the ios progressCard.changed coverage allowlist entry so the check
enforces the handler.
* chore(i18n): refresh native inventory for the progress-card rename
* fix(apps): keep the last progress card when a refresh fails
A transient progressCard.get failure no longer clears an already-rendered
durable card; only a successful null fetch or a null-revision poke clears it.
* 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>
* fix(apps): open session desktop on its machine
* fix(ui): scope the desktop session lookup to the key's own agent
`sessions.list` has no exact-key filter, so the viewer resolves a `session=`
parameter by searching for the key and matching it exactly in the response. A
key that prefixes longer ones — `agent:main:main` alongside user-named sessions
that start the same way — could push the exact row outside a five-row page and
silently fall back to the picker.
Session keys encode their agent and the list API accepts `agentId`, so scope the
search to that agent and widen the page.
* refactor(ui): stop parking a session key in the desktop environment id
Document-mode inventory failures stashed the requested session key in
`environmentId` purely so the Retry button's non-null guard would pass, even
though document-mode retry refreshes the inventory and never reads the value.
A session key only names a machine once the inventory loads, so it now stays
out of `environmentId`, and the retry branch that ignores it runs before the
guard. Adds E2E coverage for recovering a session-preselected desktop.
* fix(ui): resolve the desktop session with an exact-key lookup
The session-preselect path searched `sessions.list` and scoped the search to
the key's own agent, but a bounded search cannot rule a key out: 25 newer
same-agent sessions sharing the requested key's prefix would push the exact row
off the page, and the viewer would report the source as unavailable for a
session that exists. `sessions.describe` is the exact-key operation and already
projects placement, so the panel calls it directly and the app-root resolver
plumbing goes away with it.
* refactor(ui): move the desktop document keyboard bridge into its own controller
`desktop-panel.ts` crossed the 700-line cap. The mobile keyboard bridge — the
padded sentinel, the value diffing that turns composed input into backspaces
and text, and the field focus/reset helpers — is a self-contained concern, so
it moves into a `DesktopMobileKeyboard` controller alongside the existing
fullscreen controller instead of taking a `max-lines` suppression.
* test(ios): drain text fields instead of assuming a delete burst lands
`testReleaseChatScreenshot` typed a 5-character probe, sent 5 deletes in one
`typeText`, then asserted the field was empty. CI dropped one synthetic
keystroke under simulator load and the assertion failed with a leftover "f".
XCUITest makes no lossless-burst guarantee, so clearing now re-sends against
whatever the field actually still holds, bounded. The two gateway-setup fields
that overtyped through the same burst use the helper for the same reason.
* fix(ui): keep the chat placement owner out of the startup chunk
`desktop-document-mode.ts` is imported by bootstrap, so importing the chat
placement owner from it pulled the chat page's dependency tree into the startup
bundle and pushed startup JS past its gzip budget (331075 B against a 330507 B
allowance). The route module now only parses the URL; resolving a session to its
machine moves next to the lazily loaded desktop panel, which is the only caller.
Startup JS is back to 329710 B.
* feat(ui): add mobile desktop document mode
Add a shell-free mobile desktop route that reuses the dock panel controller and lazy noVNC client, with source preselection, touch controls, keyboard input, and retryable inventory failures.
* feat(ios): add desktop viewer entry points
* feat(android): add desktop viewer
* fix(android): keep System Back inside the desktop viewer
The per-session viewer replaces SessionDashboardScreen in place instead of
pushing a shell tab, so System Back fell through to the shell-level handler
and popped the whole Dashboard tab, stranding the operator on Chat. Claim
Back while the viewer is showing.
Also carry over TerminalSettingsScreen's imePadding: the viewer's own touch
toolbar hosts the keyboard affordance, so the soft keyboard would cover it.
Proof (emulator, Medium_Phone_API_36.0, stub control UI on 18789):
pre-fix Back from the viewer lands on Chat; post-fix it returns to Dashboard.
No Robolectric regression test — no existing screen test constructs
MainViewModel, and BackHandler under Robolectric would need new scaffolding
for weaker evidence than the live repro.
* test(ui): stop the pairing views leaking dialogs into the shared document
`ui/vitest.config.ts` runs the unit project with `isolate: false`, so test files
share one jsdom document inside a worker. `view.pairing.test.ts` appends a
container to `document.body` for every case and never tears down, unlike its
sibling `channels-page.test.ts`, so whichever suite the worker scheduled next
inherited a mounted pairing dialog.
That surfaced on this PR's first CI run as ten failures in the untouched
`input-dialog.test.ts`, which found "Approve DM access" where it expected
"Rename session". A rerun went green, so the ordering is scheduler-dependent
rather than deterministic; this removes the contamination source rather than
leaving the next suite to lose the race.
Not a proven fix for that specific run — the leak reproduces only under CI's
file scheduling, and the full suite passes locally either way — but the missing
teardown is a real violation of the shared-environment contract.
* test(ui): stop the background-tasks rail asserting on a ticking clock
The rail e2e captured the main transcript's text before opening a task detail
and required it to be byte-identical afterwards. A running task renders a live
elapsed label, so the assertion failed whenever a second ticked over between
the two reads — twice while landing this PR, both times "12s" against "13s"
with no other difference.
Normalize elapsed labels on both sides instead of weakening the assertion. The
invariant it protects, that opening a detail leaves the main transcript alone,
still holds: a real content change is still caught, and only complete duration
tokens collapse, so diffstat figures like +14/-3 and phrases like "5 messages"
are untouched.
* fix(thinking): fall back instead of erroring on unsupported adaptive level (#109351)
The adaptive level is the auto sentinel (normalizeThinkLevel maps auto ->
adaptive) meaning 'pick an appropriate level', not 'force this exact level'.
Some clients (e.g. the Apple Watch client) send it as a transport default
while the iOS app sends none, so hard-erroring on an unsupported adaptive
override rejected otherwise-valid turns from those clients. Now the auto
sentinel always falls back to a supported level; explicit concrete levels
(e.g. an explicit /think xhigh) still hard-error as before.
Closes#109351
* docs(proof): add real-behavior-proof screenshot + run log for #111301
Captured run of the real get-reply-run.media-only test (101 passed)
showing the adaptive-thinking fallback fix verified.
Co-Authored-By: yuvrajlaptop2008-byte <284412773+yuvrajlaptop2008-byte@users.noreply.github.com>
* fix(ios): inherit Watch chat thinking defaults
Co-authored-by: yuvraj thakur <284412773+yuvrajlaptop2008-byte@users.noreply.github.com>
* chore(ios): sync native i18n inventory
---------
Co-authored-by: yuvrajlaptop2008-byte <284412773+yuvrajlaptop2008-byte@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Branch menu (list/switch) for macOS+iOS shared chat via sessions.branches.*;
rewind/fork/switch gated on run activity and pending outbox work through a
durable session-mutation lease; outbox rows carry branch-epoch ownership with
local-only flush checks, atomic confirm/park transitions, attempt-versioned
delivery callbacks, fresh retry identity for possibly-accepted rows, and
epoch-guarded branch evidence; 21-locale native translations for new strings.
* feat(mobile): session dashboard screens on iOS and Android via authenticated Control UI webview
* fix(android): keep configured Control UI base path in session dashboard URL
* docs(android): note system-trust boundary of the shared Control UI webview
* fix(android): origin-only document-start rule for Control UI auth script
* chore(i18n): refresh native inventory on rebased head
* fix(ios): swiftlint closure form in session dashboard toolbar
* fix(i18n): tolerate workflow-owned pending native rows in PR alignment checks
* fix(android): KTX toUri per lint and refresh native inventory
* fix(android): ktlint import order incl. main-inherited fleet test, refresh inventory
* feat(clients): adopt model controls, session management, and keyboard history recall on iOS
* chore(i18n): translate iOS adoption strings and realign locale artifacts
* refactor(clients): drop test-only group response inits flagged by the shared dead-code intersect
* chore: retrigger ci
* chore(i18n): resync inventory after voice-unification main rebase
* chore(i18n): resync inventory after main rebase
* chore(i18n): resync inventory after main rebase
* chore(i18n): realign locale artifacts after main rebase
* fix(clients): preserve native chat API and locale copy
* fix(ios): retain public session DTO initializers
* refactor(clients): delete test-only session DTO inits flagged dead instead of suppressing
* fix(apple): prevent stale model thinking state
* test(apple): complete attachment transport fixture
* fix(apple): scope model patch ordering by route identity
* chore(apple): sync native CI metadata
The shared chat session sheet (iOS chat + macOS webchat) gains server-backed
search (sessions.list search param, 250ms debounce, cancellation-safe local
fallback when offline), an Active/Archived scope, swipe/context actions for
pin/rename/archive with optimistic updates and rollback, pin indicators, and
restore-on-open for archived rows. Archiving the open session switches back
to the main session so the composer never points at a send-rejecting session.
OpenClawChatTransport consolidates the two list requirements into canonical
listSessions(limit:search:archived:) with forwarding sugars (unreleased
internal API; all in-repo conformers updated: iOS gateway transport, demo and
fixture transports, previews, macOS webchat, test fakes). macOS webchat also
implements patchSession, giving the sheet's controls a live transport there.
OpenClawChatSessionEntry becomes var-based and gains pinnedAt/archivedAt;
the two full-init rebuild blocks in ChatViewModel collapse to copy-mutation
(the pattern that silently dropped newly added fields), preserving main's
model-identity/thinking-metadata semantics. A shared session list organizer
mirrors gateway ordering (pinnedAt desc, updatedAt desc, key) for cached and
offline lists, and pinned sessions survive the 24h recency cutoff in the
session picker.
Refs #100712
Gateway (additive, no protocol version bump): SessionEntry gains
lastReadAt/markedUnreadAt/lastActivityAt; session rows expose a derived
unread flag (explicit mark, or last read before latest activity; never-read
sessions stay read so upgrades do not light up). lastActivityAt is stamped
in the canonical post-run store update - user, channel, and cron runs count
as activity; heartbeat, internal-event, and preserved-state runs do not.
sessions.patch gains unread; sessions.create gains fork (transcript fork
from parentSessionKey under the parent lifecycle lock, refusing active,
concurrently-changed, and oversized parents, cross-agent aware).
Web sidebar: Pinned/custom-group/Ungrouped sections, unread dots, kebab and
right-click context menu (pin, mark unread/read, rename, fork, move to group,
archive, delete guarded for agent main sessions and active runs), mark-read
on view with loop-safe re-acknowledgement and failure retry; sessions page
gets unread + fork actions and shared custom-group helpers.
iOS Command Center: grouped sections, unread/pin indicators, Show Archived
gated on per-entry state, full context menu with rename/new-group alerts and
delete confirmation, current-session preview guarantee, read-episode
re-acknowledgement; new patch/delete/fork transport calls; Swift protocol
models regenerated.
Android SessionsScreen: grouped headers, unread/pin indicators, Archived
filter gated on per-entry state, long-press menu with the full control set,
agent-scoped forks, explicit label/category clears from session events,
main-session fallback when archiving/deleting the open chat, read-episode
re-acknowledgement with failure retry.
Closes#100739
Adds an additive worktree flag to sessions.create so any new chat can run in an
isolated managed worktree of the agent's git workspace, with the branch checked
out and .worktreeinclude provisioning applied. The session's spawnedCwd points at
the matching subdirectory inside the worktree so chat runs, CLI, and the file
browser execute there. agents.list gains workspaceGit (workspace or an ancestor
is a git checkout) to gate the affordance; web sidebar, iOS, and Android expose a
New-Chat-in-worktree action. Uses the method's operator.write scope, but the
.openclaw/worktree-setup.sh step runs only for operator.admin callers since it
executes repo code. Deleting the session, or leaving via a plain New Chat, clears
the cwd and lossless-removes the worktree; idle GC treats recent session activity
as worktree activity so an active session's checkout is never swept.
Live-verified end-to-end on a real gateway; follow-up to #100535 (issue #100534).
* fix(ios): restore in-flight chat runs from gateway history
Restore active Apple chat ownership across reconnect, foreground, and sequence-gap recovery using the existing chat.history snapshot. Preserve agent/session scoping and Gateway user-turn identity across Codex and Copilot mirrors, including current offline-cache integration.
* fix(ios): restore in-flight chat runs from gateway history
* feat: add session thread management
Squash of codex/thread-management (025aefc3ad1) onto origin/main:
pin/archive/rename sessions via sessions.patch, archived-aware
sessions.list, lifecycle fencing, read-only archived chat, SDK +
Swift protocol support, Control UI session management.
* refactor(ui): minimal session rows with hover-revealed management
Chat picker and sidebar recents share session-row primitives: single-line
rows, relative timestamps, rename/archive/pin revealed on hover or focus,
accent pin badge for pinned rows, and an active-run spinner in the trail
slot. Sidebar floats pinned sessions above recency via the shared
comparator and gains archive/pin actions through the unified sessions-view
patch fallback. Archive eligibility is one shared policy
(canArchiveSessionRow); the sidebar/picker active-run tooltip now uses the
real sessionsView.activeRun locale key.
* fix: align session admission with mailbox-era main
Integration fixes after rebasing onto current main: sessions_list mailbox
test expectations learn the archived/pinned row fields and archived:false
list param; gateway agent admission treats a session as deleted only when
both the requested and canonical alias sets miss it (legacy bare-main
stores and exec-approval followups read under different spellings); cron
persist tests keep a consistent store across claim-guarded persist calls;
the ACP abort hook test asserts abort propagation instead of signal
identity; drop dead lifecycle writes flagged by no-useless-assignment and
fix the promise-executor return in the codex compact test.
* fix(qa): align UI e2e and shard fixtures with redesigned session rows
Sidebar session rows are wrapper divs with an inner link now: update the
navigation browser tests and chat-flow Playwright selectors. Seed a real
per-test session store for the auto-fallback admission guard instead of
depending on leftover host files at /tmp/sessions.json. Teach the
test-projects routing fixture about the suites that newly import the
shared temp-dir helper. Document the Codex thread-format contract for
archivedAt/pinnedAt (flag derived from server-stamped timestamp, epoch ms
here vs Codex epoch seconds) at the type and in the session docs.
* test: route auto-fallback suite through temp-dir helper plans
The auto-fallback suite now imports the shared temp-dir helper for its
seeded session store, so the top-level helper routing fixture must list
it in the auto-reply plan.