The site now lives at orca.dev; the old onorca.dev and www.onorca.dev hosts
redirect there. Linking to orca.dev directly skips that redirect.
Only page links change. Service hosts, sign-in and relay endpoints stay as they are.
* build(mobile): upgrade to Expo SDK 58 and React Native 0.88
Expo 58 drops Zod from @expo/cli, which clears the last Zod 3 advisory
(GHSA-mrhx-c7xc-m3w5) in mobile along with its local patch and override.
- Expo 55 -> 58, React Native 0.83 -> 0.88, React 19.3, gesture-handler 3,
webview 14, reanimated 4.7, worklets 0.13 (SDK 58 bundled versions,
pinned below today's batch to respect minimumReleaseAge).
- Port the react-native and react-native-webview patches unchanged; drop
expo-constants, expo-notifications, @expo/cli, expo-router, zod and
query-string patches plus the zod/query-string/xcode>uuid overrides,
which upstream fixed or no longer need.
- iOS minimum rises to 16.4 (Expo 58 / RN 0.88 requirement).
- Migrate to RN 0.88 strict types: instance ref types, NamedStyles,
AppState.currentState, list header/footer nulls.
- Replace removed runtime APIs: InteractionManager -> idle callback with a
macrotask fallback; StyleSheet.absoluteFillObject -> absoluteFill;
FileHandle.readBytes -> readBytesSync (it became async).
- Resolve react-native/asset-registry for shared assets in Metro and alias
the old registry path for react-native-svg in the desktop mobile-web bundle.
- Reduce the iOS scene plugin to the reconnect override; Expo 58's template
now ships the scene delegate itself.
* fix(mobile): bridge react-native-svg web imports and libsodium runtime for RN 0.88
- Share react-native-svg web shims between the mobile-web build and its render
checks: map the old asset-registry path to react-native-web's registry and stub
the Android-only touch-sound TurboModule.
- Patch react-native-libsodium 1.7.0 to read the JSI runtime from the bridge proxy;
RN 0.88 removed RCTCxxBridge.runtime.
* fix(mobile): use Expo 58's legacy promise resolver for audio permissions
* fix(mobile): skip null handles in the RPC recording timer shims for RN 0.88 types
* style(mobile): format MobilePRSidebar after rebase
* fix(mobile): name RN 0.88 text input refs for declarations; allow queued back in stack render check
- Type text input refs with an exported interface so declaration emit can name hook results
(RN 0.88 aliases TextInputInstance to an unexported class).
- Expo Router 58 drains back() after a commit; check the interrupted push's exit from the
frame it starts, bounded to three frames of lag.
* chore(mobile): justify the text input handle interface for React Doctor
* chore(mobile): use a nameable intersection alias for text input refs
* fix(mobile): use sync expo-file-system writes, copies and moves on Expo 58
Expo 58 made File.write/copy/move and FileHandle.writeBytes return promises.
Our callers never awaited them, so failures escaped their try/catch and the
store's tmp-then-move and PDF writes could race their close or rename.
* build(mobile): raise the page bundle budget to 10 MiB for Expo 58
Expo 58 adds about 310 KB to the page: expo-router (+258 KB, now carrying
React Navigation, net about +110 KB) and Reanimated 4.7 (+152 KB). Main had
85 KB of headroom left. 10 MiB still stays within the 10 MiB per-asset contract.
* fix(mobile): drop the window-metrics seed RN 0.88 removed from Android
RN 0.88 no longer holds window metrics: DeviceInfoModule reads the activity's
WindowMetrics on demand and setWindowDisplayMetrics is gone, so MainActivity
no longer compiled. Layout scale still comes from the screen metrics we reseed.
---------
Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
* fix(mobile): tell Android users which WebView to update when it is too old
The page shell needs androidx's DOCUMENT_START_SCRIPT:1, which stable
WebView only reports from Chromium 101; the comments claimed 83/88.
Name the required WebView and the one installed, and how to switch it.
* fix(mobile): read the WebView provider through the shell module index
Tests stub the module index; a direct file import loaded expo-modules-core unstubbed.
iOS emptied Library/Caches under storage pressure, and Android's cacheDir is
cleared by Clear cache, cleaner apps and low storage. Each purge cost a full
foreground download of the page build on the next open. Builds now live in
Application Support (excluded from backup) on iOS and filesDir on Android
(kept off device transfers), and the old cache folder moves over once.
* perf(mobile): back off spare page refills after an unclaimed drop or memory warning
* fix(mobile): skip a spare whose store read outlived an unclaimed drop
* fix(mobile): arm the spare refill timer on whole milliseconds
* perf(mobile): answer a page's ready that arrived before its bridge host
A page's ready can reach the shell before the screen has built its bridge
host (the host waits on the client and the host profile read). It used to
be dropped, so init waited for the page's next retry, up to 2 s later.
Keep the latest ready per session and answer it once the host exists.
* perf(mobile): keep one prewarmed page document while Home is shown
While native Home is on screen, boot one page document for the most
recently connected host with a cached build, parked invisibly in the
window. Opening a page from that build adopts it instead of building a
WebView, which removes the WebView start and script parse from the tap.
The spare is dropped when Home is left without a claim (2 s grace), on
background, memory pressure, its own failure and before the cache deletes
its files. It is never kept beside a shown page, and only phones with
more than 3 GiB of RAM (4 GB phones) get one. A parked page is told to
stop asking for init; the adopting view answers the ready it sent.
Builds on Jinwoo's spare pool and replay work in #32203 and #32204.
Co-authored-by: Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com>
* fix(mobile): unregister the shell's memory callbacks from the context that registered them
---------
Co-authored-by: Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com>
The native shell view now hosts a MobileWebShellDocument that owns the
WebView, its scheme handler or request intercept, the network block, the
bridge and the navigation policy. The view keeps layout, the back-edge
swipe and its React events. Nothing changes for a page; this is the seam
a prewarmed spare page needs.
Based on Jinwoo's page split in #32202.
Co-authored-by: Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com>
* perf(mobile): warm WebKit's processes on iOS before the first page opens
Builds a blank, non-persistent WKWebView a second after Home has a paired
host, so the first page open starts against WebContent, GPU and Network
processes already running. A release before the delayed start now makes
the start a no-op.
Based on #32163 by Jinwoo-H.
Co-authored-by: Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com>
* fix(mobile): guard warmUp on older binaries and release the warm view on page failure
Co-authored-by: Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com>
---------
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com>
* fix(mobile): remount silently after background WebView losses
* fix(mobile): hold a reclaimed remount until the app is in front
* fix(mobile): give a reclaimed renderer one free remount per painted document
* fix(mobile): report a dead Android renderer again with its original loss facts
* refactor(mobile): build the page update-failure record in one place
* fix(mobile): declare desktop-bound native verbs in the verb table and let the device store skip the verdict hold
* refactor(mobile): name the update-failure record builder after the effect it returns
* test(mobile): check the device store read before the verdict actually succeeds
* test(mobile): leave the store-verb hold test to its own PR
* fix(mobile): a failed page remounts instead of re-downloading; deadlines count only foreground time
Page deadlines now pause in the background and restart on return, so React Native firing overdue timers on resume no longer reads as a page failure. A failed document remounts the same files once, then shows the failure screen; it never deletes and re-downloads on its own. Try again after a page failure or an unreadable generation deletes the host cache first, so it downloads fresh. Replaces #34550 without its file-size check.
* fix(mobile): drop reports a remounted page's retired view already queued
A failed page remounts at once, minting the replacement's session before
React commits it. A fault or ready the old view had already queued then
landed on the replacement: a second fault showed the failure screen
before the new page loaded, and a late ready marked it ready and
cancelled its load deadline. Each report now carries the session of the
view that rendered it and is dropped once that view is retired.
* Protect Android insertion and selection magnifier readback
* ci: refresh Android checks against current main
* ci: refresh Android checks after main repairs
---------
Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
* feat(mobile): copy, save and share an image from the chat image viewer
* refactor(mobile): pass image viewer actions in so diagrams skip them
* fix(mobile): name the image verb native.image.act and serialize image actions
* fix(mobile): queue image actions per page and skip SVG links
* refactor(mobile): share the cache-folder helper and table the viewer's image actions
* fix(mobile): keep the toast's live region mounted so screen readers hear it
* test(mobile): re-record agent history goldens for the always-mounted toast region
* test(mobile): import the declared routes in the moved routes test
* fix(mobile): keep iPad page headers clear of the window controls
The reserved-regions probe reported the window's plain safe area, which on
iPadOS 26+ windowed mode does not cover the window controls. On iOS 27.1+
pages take their top inset from that value, so the host header sat under the
controls while native screens, which get the corner-adapted inset, cleared
them.
* fix(mobile): only raise the page's top inset for window controls
* fix(mobile): back from a session lands on its host's session list
A session opened from Home's "Jump back in" or a notification replaced the
host list it had just mounted, so Back went to Home. It is now pushed over
that list.
A session opened from the host list lives inside the list's own page, and
the stack holding it has one screen, so the iOS edge swipe belonged to the
parent screen and popped the whole host to Home. While the page claims Back
the parent's swipe is turned off too, and a left-edge pan on the shell view
sends Back to the page.
* fix(mobile): keep the host swipe off until its last page claim ends
Count claims per parent screen instead of guessing whether a shell is its
stack's root, and measure the back edge from the shell's own left edge so
a landscape safe area does not hide it.
* refactor(mobile): share the session route name and write the host swipe once per claim run
* test(mobile): give the catch-all screen mock a parent navigator
The iOS web shell cancelled every subframe navigation, including the sealed about:srcdoc frames that HTML previews and chat visual cards render in, so they stayed blank. Allow exactly about:srcdoc subframes, let only a main-frame load spend the shell-load allowance, and never fail the shell document over a subframe response. Chromium never asks the Android shell about about: subframes, so Android already renders them.
UIKit's split-keyboard update crashes breaking our constraints on
keyboardLayoutGuide, so the probe reads keyboardWillChangeFrame instead.
The hinge interaction goes too: UIKit already relays out a view that
read reserved regions in layoutSubviews when they change.
Fixes#36742
Carries over #33221 and #33210. Replaces our #34484 Duo support with the community design, on top of main's rebuilt terminal key bar (#36166).
Fixes#33200
Co-authored-by: Takeshi Matsumoto <zytakeshi520@gmail.com>
Co-authored-by: Lars Keggenhoff <lars@kghf.io>
Catch-up pages called the native ledger once per dismissal, each call
re-reading, encoding and writing the full 24h UserDefaults ledger. A new
rememberAll bridge merges a whole page in one locked read/encode/write with
identical semantics (max seq per identity, refreshed 24h expiry).
* 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
WebKit reports no source frame for an app-initiated load() before iOS 18.5 (WebKit c28b242), so the iOS shell read its own document load as subframe-initiated and cancelled it, leaving the phone on "Opening workspace" forever. The navigation policy no longer takes the initiating frame as an input: the shell's own document loads when its one-shot load flag is up, the URL is the exact document URL, the target is the main frame, and it is not a download, matching the Android copy.
Verified on an iOS 18.1 simulator (hang before, workspace opens after) and iOS 26.5 (unchanged).
Refs #28626, STA-10545
Load Jira attachment images separately from issue Markdown so descriptions and comments stay small while screenshots load lazily and open in the existing lightbox. Downloads begin when an image mounts, capture its owning host, require negotiated image support, and use bounded authenticated byte streams and caches. Older clients retain their inline-image handlers.
Keep legacy and cancellable downloads in separate in-flight domains so cancelling a new image reader cannot abort a legacy reader; completed bytes share the bounded cache. Preserve the landed assignee, board, transition/comment queue and request lifetimes. Move unchanged project metadata IPC handlers into their domain module and tighten two inherited focus-check ceilings without changing behavior.
Validation:410 tests/54 suites; all changed-source quality stages, lint, format, routing and contract checks; exact final node/web types; independent source review;48/48 fresh hidden Electron checks with226 stable source hashes and nine inspected screenshots. Synthetic transports exercise production clients and UI. Current CI static checks, mobile bundle, wire compatibility, Linux/Windows packaging and Windows serve pass; five test shards were pending at review.
Extends the existing attachment work credited to Frank (@lifefloating) and Brennan Benson in PR #8938; existing contribution credits and media remain in this PR.
* fix(mobile): keep chat drafts and the sidebar reachable on iPad
Chat composer drafts now live in a module-level store keyed by host,
workspace and tab, so leaving the session screen no longer drops them.
On iPadOS 26 windowed mode the window buttons covered the top-left
header controls; a native launch hook adds their height to the root
safe area. A hidden sidebar now shows a Show sidebar button in the
session header.
* fix(mobile): preserve draft edits across chat remounts
* test(mobile): type sidebar header fixture as a partial controller
* Let in-page links switch sections inside a chat visual
* Make in-page links work in chat visuals on mobile and in WebKit
The first fix (an about:srcdoc base in the visual) only reached desktop
Chromium. On mobile, the page hosting the visual carried a policy with
base-uri 'none', and a srcdoc visual inherits its host's policy before
its own, so the base was refused and an in-page link still took the
visual off its page and showed it as failed. The mobile web shell sends
the same rule as a header. Separately, WebKit (iOS, and the web client
in Safari) fires the frame's load event for an in-page link inside a
sandboxed frame, and both hosts treated any second load as the visual
navigating away and retired it.
- The mobile host page and the mobile web shell header now allow only an
about: base, so the shell's own base applies; the visual's own policy
still refuses any base it declares.
- Instead of retiring a visual on its second load, the host now asks the
visual's shell after every load. A later load must be answered by the
same run of the shell within the plugin panel watchdog's 5 s deadline;
no answer, or an answer from a new run (a reload), still retires it.
- Tests: unit tests for the policy, the answer reader and the frame's
keep/retire rules, and a Chromium + WebKit test of both mobile surfaces
that runs in the mobile web app CI job.
* Keep any answered later load, and say plainly what the check is for
The previous commit tagged each run of a visual's wrapper with a random
id so a reload would still retire it. The visual shares a window with
its wrapper, so it could read or set that id itself, and a reload only
reruns the same Orca-written document, which is not an escape. Drop it:
after any load past the first, the host asks once, and any answer on
this visual's channel within the wait keeps it; silence retires it.
- Comments now say the check is a tripwire for a frame gone blank or to
another page; the frame guard (desktop), the host page's policy plus
the app's load filter (mobile) and the sandbox are the boundaries.
- Visuals get their own 5 s wait instead of borrowing the plugin panel
watchdog's, because a 1 s wait retired a busy visual in WebKit.
- Tests: a reload is now kept; going blank, or dropping the base and
following a link, still retires. The CI routing for the shared wrapper
is now pinned in the path-classification test.
* Load the visual links check's mobile modules only where they can build
The unit shards run every config/scripts test with only the root
dependencies installed. This check imported a mobile/ module at load,
and transforming it reads mobile/tsconfig.json, which extends a package
those shards do not have. The transform error could not be sent back to
the test worker under Bun, so the whole shard stopped with
"DataCloneError: The object can not be cloned".
Import the visual shell and the mobile host page inside the dependency
gate, as the session-dictation check already does, so the shards skip
the file without loading them. The mobile web app job, which installs
mobile dependencies, still runs it in full.
With isTextInteractionEnabled = false (#21589) WebKit delivered keydown to a focused page field but never inserted text. The page's user-select rule (#24277) now keeps WebKit's selection off long presses. Needs an iOS shell release.
The shell no longer shortens the WebView for the keyboard; it publishes the keyboard height like the safe-area insets, so native's keyboard lift, refit hold and dismiss key run on the page unchanged. Keyboard and inset arithmetic read the shell's OS through a host-os seam. One page-version floor (manifest pageVersion, shell floor 1) replaces per-feature accept negotiation; a page below the floor gets the existing update wall, a desktop with no bundle keeps native screens. iOS shell drops the form accessory bar and its own keyboard observers. Native session screens untouched.
* fix(mobile-web): stop the page declaring viewport-fit=cover
The shell already pads the WebView out of the status and navigation bars. With
viewport-fit=cover, Android's edge-to-edge WebView still reports the window's
bar insets through env(safe-area-inset-*), which react-native-safe-area-context
on web reads, so every page-side SafeAreaView padded a full bar a second time.
Without it env() reads 0 and the shell's pad is the only one.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): keep the shell's window insets out of the page WebView
WebView M144+ forwards the window's systemBars and displayCutout insets to
CSS env(safe-area-inset-*) for every WebView, and Chromium applies them
regardless of viewport-fit. The shell already pads the WebView out of both
bars, so every page-side SafeAreaView (expo-router's DefaultNavigator and the
session header) padded a bar a second time. M139+ likewise resizes the visual
viewport for ime(), which the shell has already done by shortening the WebView.
The WebView now sees those three types zeroed, per Android's "zeroing" approach
(not CONSUMED, so later changes still reach it). A listener replaces the
WebView's own onApplyWindowInsets, so the zeroed set is passed back into it.
iOS needs nothing: the WKWebView uses contentInsetAdjustmentBehavior = .never
inside the padded shell and reports zero insets.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile-web): say why the page declares no viewport-fit
The earlier comment claimed dropping viewport-fit=cover makes env() read 0 on
Android; Chromium's WebView applies the safe area regardless of viewport-fit.
The page simply never asks to extend under the bars, and the shell owns the
safe area.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* refactor(mobile): keep the page inset zeroing private to the shell view
The transformation has no honest JVM test (the builder runs as SDK 0 there
and drops every inset type), so it moves into MobileWebShellView.kt as
private members instead of standing alone. The listener comment now covers
both the P-R listener and the S+ onApplyWindowInsets path it replaces, and
the page document's comment says the env() zeroing is Android's; on iOS the
padded WKWebView reports none.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb