Skip to content

feat(mobile): Android and web support (staging) - #9126

Draft
Kitenite wants to merge 121 commits into
mainfrom
android/staging
Draft

Kitenite wants to merge 121 commits into
mainfrom
android/staging

Conversation

@Kitenite

@Kitenite Kitenite commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

How this branch works

android/staging collects the work as cherry-picked commits from the android workspaces (all landed). The last three commits are the release tail: chore(mobile): Android release config and two fingerprint: commits (Android label + R8 rule, Expo web dependencies). Supersedes #9118.

What

apps/mobile runs on iOS, Android and web from one codebase. Each platform gets its most native UI; plain React Native is the fallback. iOS behaviour is unchanged.

  • Rule (now in apps/mobile/AGENTS.md): use an @expo/ui universal component or community drop-in first (SwiftUI on iOS, Jetpack Compose on Android, DOM on web); otherwise X.ios.tsx / X.android.tsx / X.web.tsx behind an X.types.ts contract; X.tsx is a React Native fallback that must work.
  • Android native: Google sign-in via Credential Manager, agent card as an ongoing notification (Kotlin), Material menus (MenuView) for share access, file actions, composer mode and symbol buttons, Compose AlertDialog for the rename prompt, Material bottom sheet for attachments, header actions through a toolbarIcon seam.
  • Web: Expo web target (react-native-web). Header buttons and menus through a HeaderToolbar seam (36 screens), chat with token streaming and bottom pinning, diffs, Pages in an iframe, voice orb fallback, browser session cookie, terminal in an iframe, browser file chooser, browser WebRTC, backdrop-filter glass, in-app alert dialog, keyboard focus rings, reload-to-update gate. API accepts origins in MOBILE_WEB_ORIGINS (unset = none).
  • Web at /agents: the web build is served at app.superset.sh/agents, same-origin with the dashboard (shared sign-in cookie, no extra CORS). apps/web rewrites /agents/* in its proxy so the route keeps its own CSP; the sidebar and Home link to it, and /workspaces/[id] offers "Continue in browser". Off until set up: needs a superset-agents Vercel project, VERCEL_AGENTS_PROJECT_ID and AGENTS_URL (production) secrets — steps in docs/agents-web.md. Without them the links stay hidden and deploy-agents skips.
  • Web on a computer: tab titles per screen; Escape closes sheets; sheets open as a dialog (desktop) or bottom sheet (narrow) over the screen they came from; Enter sends and Shift+Enter breaks the line; click-to-type terminal; search focuses on open and Return opens the top hit; trackpad scrolling in diffs; hover states; rows are real links (⌘-click / middle-click / copy link); workspace, chat and composer keep to a centred 720px column on wide screens; back after a deep link goes to the parent screen (all platforms).
  • Android: choices (Branch, review comment Edit/Delete) open in a Compose bottom sheet; search sheet has a close button; empty Home no longer scrolls.
  • Android composer: dictation (SpeechRecognizer; Web Speech on web), paste of images and files, PDF thumbnails, swipe-down; voice mode owns the Android audio route; update gate points to Google Play.
  • All platforms: sent file attachments open in the platform viewer (QuickLook on iOS, ACTION_VIEW on Android, new tab on web).
  • Shared fallback: the React Native composer is Composer.tsx (Android, web, tests); iOS keeps the SwiftUI composer.
  • Dev setup and store: scripts/android-dev.sh, .agents/skills/mobile-android-verification, play-store.config.js, scripts/play-listing.ts, plans/20261029-android-launch-checklist.md, manual deploy-android.yml workflow.

⚠️ The last three commits start an iOS release

They change the iOS runtime fingerprint. Merging them makes deploy.yml build and submit a new iOS binary, and installed iOS builds stop taking OTA updates until they update. Drop all three to merge the Android and web code dark (web then needs its dependency commit back before it builds); every commit before them leaves the fingerprint unchanged.

Verified

  • Tests: mobile 277, api 240, shared 1504, auth 88, all pass. Biome clean. tsc: mobile 188 (pre-existing), api/shared 0. check:i18n: 0 missing in 16 locales. sherif clean.
  • expo export for iOS, Android and web all succeed. iOS fingerprint unchanged by everything after the release tail.
  • iOS simulator, merged staging (review account, production): Home, workspace, native composer, attachments sheet, workspace "…" menu identical.
  • Android emulator, merged staging: composer "+" Material sheet, system photo picker; signed-in header pass on the local stack (Home, workspace menu, files changed, jump to file, sessions, PR overflow and link, pages, version gate) all render and work.
  • Web: production export boots; per-workspace verification on the local stack covers headers, chat streaming and scroll, Pages, attachments, uploads, alerts, focus rings, version gate, CORS with MOBILE_WEB_ORIGINS.

Not verified / not done

  • Push notifications (needs a Firebase project and an APNs key). A real Google/GitHub login round trip on Android (needs a Play-signed install); App Links wait for the Play signing key.
  • Recognised speech on the Android emulator (the recogniser path ran, the emulator mic heard nothing); the voice sheet's mute toolbar (no live voice session).
  • Found, pre-existing on every platform, left for separate PRs against main: native chat shows a streaming reply only when the turn ends (LegendList memo); the checks sheet's fix button was never wired; PR sheets drop explicit owner/repo; the voice failure alert shows raw OpenAI JSON.

and light.

Not verified / not done

  • A real GitHub or Google login round trip on Android (native Google needs a Play-signed install). App Links wait for the Play signing key.
  • Web: Stack.Toolbar header controls do not render on web (~40 screens); a hosted origin needs adding to the API CORS list and better-auth trusted origins; chat sessions, the voice orb (Rive) and the Pages webview are unverified on web.
  • Dictation and voice on Android, push notifications on both platforms, PR checks sheets on Android (no entry point yet).

Summary by cubic

Adds Android and web support to apps/mobile; iOS behavior is unchanged. Every iOS-only API now sits behind a typed platform contract picked by Metro, so Android and web resolve to React Native fallbacks instead of @expo/ui, native Expo modules, and Sign in with Apple.

Android

  • React Native composer (shared fallback for web and tests) with mic dictation, audio-route ownership, clipboard paste, PDF thumbnails, and swipe-down; plus session tabs, quick keys, slash suggestions, a sheet header, glass surface, alert-prompt, and forward-swipe implementations.
  • Native Google sign-in via Credential Manager; no Apple button on Android; social sign-in failures surface their error.
  • Agent card as an ongoing notification with a stale "Not updating" form and per-row session navigation, plus a one-time notification permission request and a Chinese Intl fix for Hermes.
  • Native Material surfaces: MenuView menus, a Compose AlertDialog rename prompt, a bottom-sheet attachments picker, and a Compose host theme.
  • android-dev.sh dev setup (one emulator per worktree, no stale Metro port reuse), a verification skill, Play listing config, and App Links intent filters.
  • Screen fixes: toolbar icons as vector drawables, a JS search field, sheet headers that never show route names, aligned page comment pins, Send-to-chat on the diff screen, top-aligned multiline fields, and an empty Home that no longer scrolls.
  • Host-service requests get a 60 s deadline so Android cannot hang; Android keeps its query cache in a file; the update gate names Google Play. The home-screen label comes from the brand name and is stripped from the runtime fingerprint; an R8 keep rule preserves WorkManager's merger classes.

Web

  • Header buttons and menus render through a HeaderToolbar seam; chat markdown renders and code highlights via Shiki's JS engine; the chat list pins to the newest item only while the reader is at the bottom, streaming token by token.
  • The voice orb falls back to an animated pulse; pages and the terminal open in iframes; attachments use the browser file chooser; voice uses the browser's WebRTC; Alert.alert draws in a modal with all its buttons; the browser's session cookie carries auth; glass uses backdrop-filter. Sent attachments open in the platform viewer — QuickLook, an Android intent, or a new tab.
  • Tabs take the screen's title and Escape closes a sheet; the composer sends on Enter, breaks the line on Shift+Enter, and closes on Escape. Upgrade and manage-plan buttons name the web host they open.
  • The app exports under /agents, which apps/web serves on the dashboard origin, sharing its session cookie and its own CSP; CI deploys the export to its own Vercel project. /agents stays dark until AGENTS_URL and that project exist.

Before merging

  • The last three commits (Android release config, home-screen-label/fingerprint, Expo web dependencies) change the iOS runtime fingerprint: merging them builds and submits a new iOS binary, and installed iOS builds stop taking OTA updates until they update. Drop them to merge the Android work dark.

Written for commit d55d882. Summary will update on new commits.

View guided diff Turn on auto-fix

Kitenite and others added 18 commits October 29, 2026 14:13
Each local Expo module and SwiftUI call site now has a typed contract
(X.types.ts), an iOS binding (X.ios.tsx) and a fallback (X.tsx) picked
by Metro platform extensions. Android bundles resolve to the fallbacks
and contain no @expo/ui/swift-ui or expo-apple-authentication code.
Native fingerprint is unchanged (only modules/*/ios is hashed).

Co-Authored-By: Claude <noreply@anthropic.com>
Composer.android.tsx implements the composer contract in React Native:
draft with initialDraft/onDraftChange, submit, isSending, canStop/onStop,
attachments tray, header chips, agent and launch-option pickers, the
permission-mode menu with SF Symbol names mapped to lucide icons,
onHeightChange, onExpandedChange and the ComposerHandle methods, plus
presentImage (a viewer hosted by the mounted composer) and fileThumbnail
(images only). iOS keeps its native binding unchanged.

Plan and the RN-vs-Kotlin decision: apps/mobile/plans/20261029-android-composer.md
- Serve /.well-known/assetlinks.json from apps/web beside the
  apple-app-site-association route, public in the proxy. The cert
  fingerprint list (ANDROID_SIGNING_CERT_SHA256) is empty until the
  upload and Play App Signing keys exist; it lives in constants.ts, not
  app-identity.ts, because app-identity.ts is hashed into the mobile
  runtime fingerprint.
- Add autoVerify https intent filters for /page/ and /workspaces/ on the
  same host as the iOS associated domain.
- eas.json: apk for development, development-simulator (inherited),
  preview and e2e-test; app-bundle for production; submit.production
  .android to the internal track as a draft, using the service account
  key stored on EAS.

EAS Metadata (store.config.js) supports only the App Store, so the Play
listing stays manual. eas-dev.sh stays iOS-only: `eas simulator --egress
local` supports only iOS. eas-runtime-version.test.ts and
fingerprint.config.js make no iOS-only assumption.

The iOS runtime fingerprint changes with this commit (eas.json, the
android block of the Expo config and app-identity.ts are all hashed for
every platform), so the next deploy builds and submits a new iOS binary.
… header seams

- alert-prompt: RN Modal + TextInput, drawn by AlertPromptHost at the root
- symbol-button: Pressable + lucide icon map, dropdown menu for items
- title-press: pressableTitle() gives a tappable headerTitle where the
  native bar cannot report taps; iOS returns undefined (native title)
- forward-swipe: right-edge pan strip (gesture-handler) that pushes
- GlassSurface and glassHeaderOptions seams: expo-glass-effect is now
  imported only from .ios files; Android gets an opaque header and a
  flat tonal surface
- WorkspaceScreen rename uses the alert-prompt seam, not Alert.prompt
- lingui scans apps/mobile/modules

Co-Authored-By: Claude <noreply@anthropic.com>
Ports the Live Activity contract to Android: one ongoing BigText
notification that asks to be promoted to a Live Update on Android 16+.
Same contract and caller; iOS unchanged (binding renamed .native.ts).
Plan covers the gaps and push token registration for both platforms.

Co-Authored-By: Claude <noreply@anthropic.com>
…droid app config

Android emulator boot spike (step 1 of Android support).

- intl-polyfills: Hermes on Android lists zh-Hans-CN/zh-Hant-TW but not
  zh-CN/zh-TW, so NumberFormat and DateTimeFormat formatted Chinese as
  en-US. scriptSubtagFallback.android.ts wraps both constructors and passes
  the maximized tag when the native one does not support the tag as
  written. iOS and bun resolve to a no-op. Measured on API 33 for all 17
  supported locales; nothing else throws or falls back.
- signInWithApple fallback (Android): surface the API error instead of
  failing silently. Apple web OAuth still fails server-side: the API has no
  Apple Services ID or client secret (CLIENT_ID_AND_SECRET_REQUIRED).
- app.config android: adaptive icon background #151110 to match the icon
  ground, and expo-localization supportedLocales for Android (generates
  locales_config.xml for per-app language).

Note: any app.config.ts edit changes the iOS runtime-version fingerprint.

Co-Authored-By: Claude <noreply@anthropic.com>
Android has no Sign in with Apple, so the provider list is per platform
(socialProviders.ios.ts keeps Apple; the .ts fallback drops it) and the
Apple web-OAuth fallback is gone. iOS renders and calls exactly what it
did: native Apple, browser GitHub and Google.

Google on Android goes through Credential Manager (the account bottom
sheet) via modules/google-credential, a small Android-only Kotlin Expo
module on androidx.credentials + googleid. It asks for an ID token for
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID (the API's GOOGLE_CLIENT_ID) with a
nonce and signs in with signIn.social({ provider: "google", idToken }).
better-auth already verifies that token against GOOGLE_CLIENT_ID, so the
server is unchanged. With no Google account on the device, or no client
ID configured, it falls back to the browser flow; a canceled sheet does
nothing.

Chosen over @react-native-google-signin/google-signin: its free edition
uses the deprecated legacy Google Sign-In SDK on Android (Credential
Manager is in the paid edition), and its config plugin and GoogleSignIn
pod would touch the iOS build. The local module has no config plugin,
so the iOS runtime fingerprint is unchanged (333eefdb...).

Co-Authored-By: Claude <noreply@anthropic.com>
…back fixes from device

- sheetHeaderOptions seam for formSheet routes: iOS is glassHeaderOptions
  unchanged; Android draws a JS drag handle + title (+ header items) and
  rounds the sheet top, since Android form sheets have no native bar
- contentScrollsUnderHeader seam: Home and the chat workspace keep a
  transparent header on iOS only; on Android the first rows sat behind
  the bar and the host/sort chips took no taps
- alert-prompt: KeyboardAvoidingView so the dialog stays above the IME
- Input: see-through selection colour on Android (white hid the text)
- symbol-button: style on the menu root, so the trigger fills its slot
- forward-swipe: 1px edge view with 40dp hit slop; taps under it reach
  the content and the swipe starts inside the gesture-nav Back zone

Verified on the Pixel 2 API 33 emulator.

Co-Authored-By: Claude <noreply@anthropic.com>
…poser

- collapsable={false} on the field's wrapper: flattening it in one state and
  not the other re-parented the field, and Android dropped focus.
- Lift by keyboard + bottom inset: RN reports the keyboard without the
  navigation bar.
- Blur on keyboardDidHide, so Back collapses the composer as on iOS.

Verified on the Pixel_2_API_33 emulator; findings in the plan.
Holds back the Android release config; see the final commit.
Holds back the Android release config; see the final commit.
Holds back the Android release config; see the final commit.
Holds back the Android release config; see the final commit.
Holds back the Android release config; see the final commit.
- scripts/android-dev.sh boots the emulator, builds the dev client only
  when the installed APK is not this worktree's (owner + fingerprint on
  the device), starts Metro with --clear, reverses the service ports and
  opens the app. --doctor, --reload, --user, --shot, --stop.
- .agents/skills/mobile-android-verification: Android mechanics and the
  traps found while porting (JDK 17, shared emulator, stale Metro cache,
  dev auto sign-in, iOS fingerprint files).
- plans/20261029-android-launch-checklist.md: account, signing, Google
  sign-in, App Links and Play steps a person must do, and known gaps.
- play-store.config.js + scripts/play-listing.ts: the Play listing
  (text reused from the App Store listing, icon, feature graphic, form
  answers) checked against Play's limits and pushed through the Play
  Developer API with --push.

Co-Authored-By: Claude <noreply@anthropic.com>
…uth clients

Co-Authored-By: Claude <noreply@anthropic.com>
…note

Co-Authored-By: Claude <noreply@anthropic.com>
Parallel worktrees can each boot and drive their own AVD on a fixed port.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@expo

expo Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

No installed development build can load this pull request as an update: its native fingerprint is 96ab39e9ca58 and no development build has it. Add the mobile-build label to build one.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) ✅ View Branch
Vercel API (Vercel) ✅ Open Preview
Vercel Web (Vercel) ✅ Open Preview
Vercel Marketing (Vercel) ✅ Open Preview
Vercel Docs (Vercel) ✅ Open Preview

Preview updates automatically with new commits

…rage row

A persisted cache over ~2 MB could not be read back from Android's SQLite
("Row too big to fit into CursorWindow"), and AsyncStorage stops at 6 MB.
Android now writes it to a file in the cache directory and removes the old
row unread. iOS keeps AsyncStorage.
signIn.social reports a failure in res.error and does not throw, so GitHub,
Google (browser) and Apple failed silently. A dismissed browser still
resolves without an error.
Kitenite and others added 30 commits October 31, 2026 14:22
…e link does not open the desktop app

The sidebar gets an Agents item and Home an "Open in browser" button, both as
full-document links because /agents is another deployment behind a rewrite.
/workspaces/[id] shows "Continue in browser" (/agents/workspace/[id]) once the
desktop deep link has had time to open.
deploy-agents builds apps/mobile's web export in Actions and uploads it to its
own Vercel project with --prebuilt. Previews are aliased to
agents-pr-<n>-superset.vercel.app and the preview web deploy rewrites /agents
there; production web reads AGENTS_URL. docs/agents-web.md lists the Vercel
project and secrets to create.
A next.config rewrite to another origin drops the headers() entries, so the
/agents document had no CSP or security headers at all. Rewriting pages from
src/proxy.ts, after the sign-in check, keeps them; assets still use the
next.config rewrite.
…tays at its baseline

The mobile tsconfig has no Bun types, so import.meta.dir and Bun.serve added six
errors.
…xist

AGENTS_URL is optional; without it the dashboard hides the Agents links and
the workspace browser fallback, and deploy-agents skips when
VERCEL_AGENTS_PROJECT_ID is not set, so merging before the one-time setup
does not break the web deploy.
The list's minHeight assumed a transparent 44pt header the content scrolls
under, which only iOS has. Under an opaque header the list already starts
below it, so the content came out about 20pt taller than the list and an
empty Home scrolled.
The labels read COMPANY.DOMAIN while the buttons open billingSettingsUrl on
EXPO_PUBLIC_WEB_URL, so a build pointed at another web origin (dev, preview)
named the wrong place. webHost() reads the same variable.
…t a hook

It calls no React hooks, so the use- name broke biome's useHookAtTopLevel
rule in its test.
On the web a formSheet route replaced the page. sheetScreenOptions maps
it to a transparent modal there, so the stack keeps the screen below
visible, and WebScreen draws it as a centred dialog on a wide window or
a bottom sheet on a narrow one: dimmed backdrop that closes on click,
role=dialog with aria-modal, focus trapped inside and returned to the
opener on close, Escape as before.

Stack.Toolbar turns the stack header back on, so the sheet's JS header
(SheetHeader, the search field) renders through SheetHeaderSlot, which
portals it into the dialog on the web and renders in place elsewhere.
iOS and Android keep presentation formSheet.
Opening /workspace/<id>/files-changed cold (a reload on the web, a deep
link on a phone) gave the stack the anchor and the screen, so back went
Home. ParentScreens, the authenticated stack's layout, puts the missing
parent screens below the focused one once, from the state the stack
opens with: files changed and pull request go back to their workspace,
a page to Pages, a settings page to Settings. Navigation after that,
and every stack that already has history, is unchanged.
showActionSheet fell back to Alert.alert on Android, which keeps an
empty title band above the buttons. Android now draws the choices in a
Compose ModalBottomSheet (ActionSheetHost, mounted at the root like the
attachments sheet): one row per choice, the destructive one in red, no
Cancel row; dismissing reports the cancel index. iOS keeps
ActionSheetIOS and the web keeps the in-app dialog; their host renders
nothing.
The JS search header replaces the sheet's header items there, so the
toolbar ✕ never showed and only Back or Escape closed it. SearchBar
takes onClose and draws a ✕ beside the field. iOS keeps its native
search bar and toolbar close.
…reens

The SwiftUI composer caps its tabs, quick keys and card at 720 pt on iPad.
The React Native composer, which web and Android use, stretched them across
the window: 1440 px of quick keys on a desktop browser. A clusterColumn seam
gives the web the same 720 px centred column; Android keeps full width.
The READABLE_WIDTH test now pins the RN constant too.
LegendList's web rows are absolutely positioned at left: 0 and right: 0, so
the content container's side padding never reached them. At 1440 px the
card grid computed a 180 px inset and drew from the window edge anyway,
while the header kept its gutter. listSideInset puts the inset on the
content natively and on each row and the scope bar on web.
A chat at 1440 px ran its replies across the whole window. useColumnInset
is the readable inset (useReadableInset) on web and the base padding
elsewhere, so the transcript and its dock chips sit in the same 720 px
column as the composer. Native keeps 16 px, as before.
… while it has the keyboard

On a computer the terminal takes typing directly (#5 in report 17), so the
touch quick-key strip only took room while you typed there. The web shim
reports when xterm's textarea holds focus; the workspace hides the keys
until focus moves back to the composer or elsewhere. Wider than the readable
column, the terminal iframe gets 12 px of padding instead of sitting flush
against the window edge. The native WebView never reports focus, and the
generated terminal page is unchanged.
Rows were divs, so ⌘-click, middle-click and Copy Link Address did
nothing. RowLink wraps a row in an <a href> (display: contents) with the
path under the export's base path. A plain click cancels the anchor and
lets the row's own press handler navigate in the app; a modified click
skips the handler and the browser opens a tab. The row's right-click menu
gains Open in new tab and Copy link, since it replaces the browser's menu.
Natively RowLink renders the row unchanged.
…print)

app.config.ts (android block: adaptive icon, locales, App Links intent
filters), eas.json (Android build and submit profiles), the mobile
.gitignore and app-identity.ts all feed the iOS fingerprint. Merging
this commit to main makes deploy.yml build and submit a new iOS binary,
and installed iOS builds stop taking OTA updates until they update.
The assetlinks.json route and its constants come along because App
Links need the intent filters. Drop this commit to merge the Android
work without an iOS release.

Co-Authored-By: Claude <noreply@anthropic.com>
…nager mergers under R8

- withAndroidLabel sets app_name to BRAND.NAME, the Android counterpart of
  CFBundleDisplayName. `name` stays "Superset" because iOS derives its product
  and scheme names from it.
- fingerprint.config.js strips the label from the hashed config, as it does
  for the iOS display name, so a rename does not need a new binary.
- R8 full mode keeps WorkManager's InputMerger classes without their
  constructors (work-runtime 2.9.1 consumer rule has no members), so every
  job failed with NoSuchMethodException, among them expo-observe's
  ObservabilityBackgroundWorker. Added a keep rule with <init>() through
  expo-build-properties.

Changes the iOS runtime fingerprint (new plugin entry); no iOS behaviour change.
react-native-web, react-dom and @expo/metro-runtime, at the versions
bunx expo install picks for SDK 58. Expo adds "web" to the config
platforms and bun re-hashes peer sets, so the iOS runtime fingerprint
changes: 5115ae66c305 -> e42314744de3 (android bea2605d7cfe -> d26c50902269).

This branch was successfully deployed

1 active (outdated) deployment
preview — 8499f3bd Deployed Oct 31, 2026 by Kitenite via Deploy Web #13404
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant