Skip to content

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

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

Kitenite wants to merge 111 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.
  • 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.
  • 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, and a bottom-sheet attachments picker.
  • 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, and top-aligned multiline fields.
  • Social sign-in now surfaces its error; host-service requests get a 60 s deadline so Android cannot hang; Android keeps its query cache in a file. 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 open in an iframe; attachments use the browser file chooser; the terminal runs in an iframe; 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.
  • 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 7e22da6. 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 30, 2026 15:05
…r a reload on web

The server message is written for the App Store. iOS keeps it and the App
Store button; Android and web show their own translated text.
expo-file-system's File.preview shows QuickLook on iOS and starts an
ACTION_VIEW intent through its FileProvider content URI on Android. Web opens
the blob URL in a new tab, or downloads it when the browser cannot show it.
Viewers opened through the content URI titled the file with its attachment id.
# Conflicts:
#	apps/mobile/screens/(authenticated)/workspace/[id]/WorkspaceScreen/WorkspaceScreen.tsx
#	apps/mobile/screens/(authenticated)/workspace/[id]/sessions/SessionsSheet.tsx
The Android update screen imported ANDROID_APP, which only exists once the
release config commit lands, so the branch did not build without it.
MOBILE_WEB_BASE_URL sets expo-router's baseUrl only for the web export, so
native builds and the iOS/Android fingerprints do not change. scripts/build-web.ts
packs the export as a Vercel prebuilt output at /agents with an SPA fallback, and
browser sign-in returns to the base path.
…ics through /ingest

Under a base path the web app is served by apps/web, so PostHog goes through the
dashboard's same-origin /ingest proxy and the /agents CSP needs no PostHog host.
scripts/serve-web.ts serves the Vercel output with the same routes on AGENTS_PORT.
apps/web rewrites /agents to AGENTS_URL, so the agents app shares the dashboard
origin and its session cookie. /agents gets its own CSP (API, relay, realtime,
uploads, Sentry, page frames, media) and allows the microphone for voice; the
dashboard's CSP is unchanged. Setup allocates AGENTS_PORT (base+19) and
AGENTS_URL for each workspace.
…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.
…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