* test: remove redundant cases and repair behavioral assertions
* docs: reconcile audit records with current main
* fix(deps): apply security updates from #32101
Apply the six-file DOMPurify 3.4.17 and Next.js 16.4.1 patch from PR #32101 to resolve the dependency security gate for this test audit. Original patch by Brennan Benson.
* test: satisfy changed-line quality gates
Add required braces and replace filter indexing with find. Rerun both CI quality gates and the 27 affected relay tests; refresh audit hashes and record the corrections.
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(mobile): keep a terminal tab's chat identity while its handle is unchanged
A status frame that omits agentType or providerSession flipped chat back to the
terminal for tabs with no launchAgent (#31977), and left a launched tab with no
session to read (#32134). Mobile now remembers the last resolved agent and
session per tab and handle, so a missing field is treated as missing evidence.
* fix(mobile): keep remembered chat read-only and drop it on relaunch
A chat resolved only from this phone's memory may sit on a bare shell, so its
writes stay locked until live status confirms the agent. A live agent frame
without a session no longer inherits the previous conversation, and the
latest remembered agent outranks an older launch hint.
* fix(mobile): gate native-chat image sends on a confirmed live agent
An image send from a chat only this phone remembers could paste into a bare
shell; it now shares the confirmed-agent gate with text sends.
* fix(mobile): type remembered chat identity structurally
Importing MobileSessionTab into the eligibility module pulled React Native
globals into the e2e program through nested-agent-idle-session-owner.spec.ts,
retyping setTimeout as number there.
* fix(mobile): the shell returns to a screen already on its stack
The page's own "back to hosts" chevron calls dismissTo('/') inside the page, which the page's
router hands to the shell as one navigate message. The shell pushed every such href, so leaving
a host from its page left [Home, Host, Home] with the old host page still mounted (WebView and
live subscriptions), and every round trip stacked another one. System Back then walked back
through stale host pages.
The shell now reads the navigation container's state first: an href that is already a screen
below this one (same path and query) is popped back to, and an href that is this screen is
dropped. Anything else is pushed as before. The pop names its navigator and source route
explicitly, because a screen's dispatch otherwise stamps its own key as the source and a parent
stack that cannot find it drops the pop.
The wide host-route dismissTo stays for the hrefs this rule does not cover: a host route whose
query differs from the one below (the missing-worktree bounce), and a cold deep link with no host
route below to return to. Overlay replace is unchanged, and so is the bridge message.
* fix(mobile): a param cleared to empty and a fragment still match a screen on the stack
The session screen clears paneKey with setParams({ paneKey: '' }), which leaves ?paneKey= on the
stack entry, so a later navigate to the plain session href found no match and pushed a duplicate.
Empty query values are now dropped when canonicalising, and the fragment is stripped since
expo-router keeps it as a param no href spells the same way.
* feat(mobile): one download bar whose fill follows progress on the native driver
The page download's replies keep the JS thread busy, so React committed
nothing until the end and both bars sat at zero, then jumped to full.
The session now publishes each progress step to a small feed as it is
dispatched, and one shared bar eases its fill to each value with
Animated on the native driver (scaleX). The caption stays React-drawn.
* fix(mobile): the download bar always moves on the native driver
With reduced motion, the fill jumped through a JS-side setValue on a value
that never became native, so on Fabric it only moved with React commits,
which the download holds off. An unread preference also counted as
reduced, so a freshly mounted bar took that path. Every step is now a
native-driven timing, of zero length when motion is reduced, and the
preference is read through a ref.
* fix(mobile): the download bar starts at most one ease per 50 ms
The feed fires per asset, often well under a millisecond apart. Starting a
new 150 ms timing on each one replaced every ease before it drew a frame,
so on iOS the fill stayed near empty until the last update.
Now a callback inside the 50 ms window only records the latest fraction;
the next callback past the window, or a trailing timer at its end, eases
to it over 100 ms. Full lands at once. The effect cleanup clears the timer.
* feat(mobile): download only the page assets the cached generation lacks
Before paging the bundle, the phone matches the new manifest against the
host's active generation by content (sha256 and length). Matches are read
from disk and hashed again while the changed assets come over the wire;
one whose bytes no longer hash to the manifest is downloaded instead.
Progress counts only the bytes that cross the wire.
* test(mobile): put the older-build manifest note back on its describe
* perf(mobile): copy unchanged page assets natively instead of reading and hashing them
The reuse pass read every unchanged asset into JS and hashed it again,
which kept the JS thread busy for most of an update. The cached
generation was hash-verified when it was written, is never written
again, and is what the page already opens as is, so matching assets are
now copied natively into the staging tree and checked by size. A copy
that fails or comes up short is downloaded instead; stage refuses a
reused copy that is no longer whole. Path rules move to
generation-asset-path.ts and the copying to generation-reuse.ts.
* fix(mobile): copy reused page assets at stage, on the store's queue
Copies made when the manifest was read sat in tmp for the whole download,
where a second shell mount's sweep, or a second download of the same
build, could take them away and fail the stage. The fetch now only checks
that each reusable file is still whole (a native stat) and leaves it to
the stage, which copies it from the generation active at that moment and
checks its size. Copy destinations must not exist, as on both platforms.
* feat(mobile): decode page-bundle download windows natively
Each 384 KiB window of the page-bundle download cost about 200-300 ms of
Hermes JS time (tweetnacl secretbox open, fflate gunzip, a base64 re-encode
check and the TextDecoder polyfill), which kept the JS thread busy for the
whole download.
The phone now opens v2 frames with react-native-libsodium (secretbox open and
strict base64) and decodes ranges with a local Expo module,
orca-bundle-window-codec (bounded gunzip via system zlib on iOS and
java.util.zip.Inflater on Android, plus strict UTF-8). The shared v2 session
takes these as optional primitives; the desktop keeps tweetnacl.
If any native piece is missing or fails a known-answer probe, the phone keeps
today's JS path, with the base64 re-encode replaced by an O(1) canonical check.
* refactor(mobile): name the native decode for every inbound frame, not just bundle windows
The native secretbox, base64 and UTF-8 primitives decode every inbound v2
frame on the phone, so the files and the Expo module are renamed for what
they cover (paired-e2ee-native-*, orca-gzip-utf8-codec). A throw from the
native UTF-8 decode now falls back to TextDecoder, since the frame is
already open and its counter advanced.
* test(mobile): pin the TextDecoder fallback when native UTF-8 throws
* fix(mobile): hand the native decode to the transport from the app entry
The transport modules are reached by shared, desktop and e2e code. Importing
the native bindings there pulled expo-modules-core into the e2e typecheck,
whose global types make NODE_ENV required and broke ~20 e2e files. The app
entry now provides the bindings; without them the transport runs the JS path.
* fix(mobile): a native-codec lookup before the app provides bindings does not pin the JS path
* fix(native-chat): attach a long paste as a text file, and say a message is too large before it is sent
A paste of 32 KB or more (or one that would push the message past the 256 KB
send limit) into a structured chat is saved as a file the agent reads, through
the same pending-chip and @path route a dropped file takes: Orca's paste folder
here, or the chat's attachment store on its paired server. Claude gets the paste
folder as an allowed directory. A message still over the limit is handed back
before it is sent, on desktop and the phone, with words naming the limit instead
of the generic 'not sent, send it again'.
* test: Claude launch parity snapshot grants the paste folder
* fix(native-chat): a long paste replaces the selection at once and settles in place
A long paste now puts an inline Pasted text pill where it went, replacing any
selected text immediately, and the saved file (or inline text, or the original
selection on failure) replaces that same pill when the save finishes, in its own
chat's draft. Nothing reads the caret later, so order and position hold. The
paste never joins the image row, so image numbers are unchanged. The size check
measures the real message body with the selection replaced and images included.
A server that keeps no files takes the paste inline when the message still fits.
A pending paste a restart lost shows Paste not saved, and Send says so.
* fix(native-chat): type the pasted-text parser and the composer's missing draft scope
* fix(native-chat): admit paste references before goals and commands
* fix(native-chat): resolve staged paste paths without editing the draft
* fix(native-chat): consolidate paste imports for code quality checks
* fix(native-chat): retain checked string types for saved paste paths
* fix(native-chat): retain completed paste facts through history and delimit paths
* fix(native-chat): keep structured question rendering within file limit
* fix(native-chat): pasted or inserted text that looks like a paste token stays literal
* fix(web): keep the browser's UI bridge under the file length limit
* style: format the files #29982 merged unformatted
* fix(native-chat): retain oversized drafts before send admission
* fix(native-chat): keep the composer handle's merge resolution to main's destructure
Preserve deliberate terminal taps after scrolling and keep them ordered with Live input. Retain fresh keyless edits, cap coalesced sends at the existing UTF-8 request limit, and settle cancelled flush barriers without waiting for retired acknowledgements.
Adapted from @innocarpe's community PR #30514, with additional ordering, lifecycle and bounded-send regressions.
Fixes#30510
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
* fix(mobile): retire dismissed branch diff requests
A pending branch diff reopens a dismissed drawer and blocks replacement previews. Retire requests on dismissal and host/workspace changes, and keep spinner ownership with the current request.
* fix(mobile): retain branch preview through suspended transitions
* test(mobile): retain confirmed branch previews across connection cutover
* fix(mobile): retain quick command editor ownership during saves
A deferred Save discards newer edits or closes a replacement editor, including after host/repository changes. Bind each save to its editor and scope; preserve newer text and the persisted command id.
* fix(mobile): preserve quick command saves through suspended renders
* test(mobile): preserve quick-command edits during connection migration
Reconnect refreshes only the root and retains deleted nested entries; interrupted children can remain loading. Invalidate child reads and refresh expanded folders. Bound deep indentation to keep filenames reachable.
Fresh repository objects reset explicit setup choices, while host changes can retain stale scripts and trust. Use logical repository and runtime ownership.
Delayed hydration and out-of-order storage writes can reverse a browser destination selection. Order durable writes and preserve the confirmed value when saving fails.
Classify substantially delayed connect/handshake deadlines as interrupted
instead of blaming endpoint reachability, retaining the existing close/retry
path and ordinary timeout errors. Limit sleep-clock compensation to observed
background segments and let fresh failures replace delayed evidence.
Remove the stale host-version fallback and discard captured versions when
report hydration crosses an observed reconnect. Independently reviewed;
128 focused tests, adjacent transport contracts and changed quality pass.
Adapted from community PR #24425.
Fixes#18829.
Co-authored-by: Wooseong Kim <innocarpe@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Capture Android terminal keyboard controls in the OTA live input before native web input edits or moves focus. Preserve composition handling, send ordering, and stale-target protection.
Fixes#25496
Co-authored-by: Sangrak Choi <kars@kargn.as>
Co-authored-by: zenghaolun02 <zenghaolun02@baidu.com>
Switching desktops retains another host history and can scan its filesystem path on the new host. Reset content by host while preserving same-host reconnect identity.
Concurrent close replies filter captured terminal arrays and resurrect an already closed terminal. Reconcile the current collection synchronously and respect newer selection and metadata.
* fix(mobile): own commit generation by workspace and admit once
AI commit generation crosses workspace transitions and admits duplicate calls before a render. Retire the active run on scope changes and use synchronous admission.
* fix(mobile): retire commit generations only after scope commits
Pass the selected repository to New Workspace agent detection through the host-authoritative runtime resolver, including WSL paths and Windows project preferences. Preserve old-client request shape, SSH routing, and strict ownership/refusal behavior.
This merge credits @welldoingk’s original WSL agent-detection investigation and community PR #20101, adapted to the current host-resolved contract.
Co-authored-by: welldoingk <thkwon96@gmail.com>
Fixes#20060
* fix(mobile): show commit comparison failures with retry
Comparison refusals, transport errors and malformed payloads appear as No file changes. Decode accepted data, preserve loaded files on failed refresh, and offer Retry.
* test(mobile): record commit comparison errors and retry affordance
* refactor(browser): simplify address bar callbacks within lint limit
* style(browser): keep client-hosted pane within line limit
* fix(i18n): retire obsolete Portuguese default zoom entries
* fix(mobile): invalidate chat file search on authority migration
Modern search caches and late replies survive authority migration on the same client object. Own cache, capability decision and inventory by logical generation.
* test(mobile): record retired file-search requests after reset
* refactor(browser): simplify address bar callbacks within lint limit
* style(browser): keep client-hosted pane within line limit
* fix(i18n): retire obsolete Portuguese default zoom entries
* fix(mobile): the home screen's New Workspace opens the create sheet again
The native host-list route dropped the home screen's action=newWorktree when it built
the page's route, so the page opened as a plain list. The route now forwards it, and
the page names it back once applied so a reload does not reopen the sheet.
Reaches phones only with the next native build.
* test(mobile): drop the New Workspace tests that only re-check types and a null guard
* fix(mobile): Home's New Workspace opens the create sheet over Home
Replaces the action=newWorktree route hand-off. The quick action picks a
host (as Tasks does), then mounts the host's create sheet on Home; a
create opens the new workspace's session marked as just created.
The sheet controller becomes a plain visible/onClose component, and the
host screen's route-action state, the action route param and its builder
are deleted.
* fix(mobile): Home's create sheet seeds only proven worktree rows
Home's cold-start seed is a pathless resume row that the name suggester
cannot read, so the sheet now seeds from proven rows and is keyed by host.
The host screen gets one setter for the sheet's visibility and its ref.
* test(mobile): name the Home new-workspace test after the screen it renders
* proto(mobile): Home's New Workspace opens a native drawer hosting the page's create form
Home pushes /new-workspace/<host>, a react-native-screens formSheet over Home whose content is
MobileWebShellScreen for the page route /h/<host>/new-workspace. The page renders the shared
Create Workspace form with its drawers inline, so the form and its pickers fill the native
drawer. The shell's loading, download and failure states render inside the drawer. Prototype only.
* proto(mobile): the New Workspace drawer is the host screen's own sheet over a transparent modal
The native route is a transparentModal hosting the shell over Home; the page route renders the
host screen's NewWorktreeModal on a transparent page, so the drawer is the same component. While
the page loads, the shell shows a placeholder of that sheet (scrim, grabber, spinner or download
bar), and the page's sheet opens in place without its slide. A host that cannot serve the page
(old desktop, blocked, failed) opens the host list instead of a wall in a drawer.
* feat(mobile): Home's New Workspace opens the page's create drawer over a dimmed Home
A transparent native overlay route hosts the host picker and then the page's
/h/[hostId]/new-workspace route, which renders the same NewWorktreeModal as the
host screen's +. The native scrim matches the drawer backdrop and the page's
first drawer opens already dimmed, so Home stays dimmed throughout. Old desktops
without the route fall back to the host list.
* fix(mobile): one loading layer for Home's New Workspace overlay
The overlay route owns a single tappable loading layer (spinner, or the
download on a small card) that stays mounted from the pick until the page
paints, so the fade and spinner no longer restart per shell state. The shell
reports its load and shows the host list for anything it cannot open; the
picker hands the pick over at once and keeps sliding away. The page's first
drawer takes the dimmed backdrop once, and a gate's overlay pending view no
longer stacks a second scrim over a mounted stack. Native /h/[hostId]/new-workspace
redirects to the overlay. Drops the unused session-route params, the
clipboard grant, and pass-through tests.
* feat(mobile): New Workspace loads in a short drawer over an undimmed Home
While the page loads, the overlay shows one short native drawer in the shared
drawer chrome with a spinner or the download, and nothing dims Home: a tap
above it, a drag down or Back cancels. The page's real drawer then slides up
with its own backdrop exactly as on the host screen's +. Drops the native
scrim, the loading card and the already-dimmed backdrop handoff; the overlay's
WebView stays invisible until the page paints, since a fresh one draws opaque.
* feat(mobile): the New Workspace loading drawer dims Home like any drawer
The short loading drawer opens with the shared drawer backdrop and show
duration, and a tap on that backdrop, a drag down or Back cancels. It stays
in the screen's own window (a native Modal over the loading WebView stalls
the page) and goes in the frame the page paints: the shell reports its load
from a layout effect so the WebView shows and the drawer leaves together.
* fix(mobile): the page's first paint waits for a drawer opening with it
A drawer opening as its page screen mounts holds that screen's first-paint
report until its open animation ends, through a hold on the existing report
rather than a new bridge message. The New Workspace overlay's loading drawer
now stays until the create drawer is up, so a cold open no longer shows Home
undimmed between the two.
* refactor(mobile): the New Workspace overlay owns its exits; the loading drawer fades out
The shell no longer imports the feature: an overlay is a shell with
onOverlayLoad, which reports `unavailable` for what it cannot show, and the
overlay route opens the host list for every exit. HostProtocolGate takes one
transparentWhilePending flag. The loading drawer's drag handler follows a
stable cancel, and on paint the drawer fades out eased in, as the shell's page
cover does, because the WebView takes a frame or two to show the page after it
is revealed; removing it outright left Home undimmed for those frames.
* Revert "fix(mobile): the page's first paint waits for a drawer opening with it"
This reverts commit be0913b9d5.
* fix(mobile): Home's host picker leaves when there is no desktop to pick
An unreadable host catalog, or offered desktops that are all gone, left the
picker invisible inside the transparent overlay, holding a screen over Home
with nothing to dismiss it. The catalog read now resolves to no hosts on
failure, and an empty pick list goes back to Home.
* fix(mobile): Home's create drawer names SSH hosts as the host screen does
The page drawer opened from Home passed no host labels, so a project on an SSH
host showed its run target by raw host id. Reading the SSH target summaries
and display overrides moves into one function that both the host screen's
metadata fetch and the page drawer call.