feat(mobile): redesign the app on @expo/ui - #306
Merged
Conversation
Adds @expo/ui and rebuilds the Settings screen as a native SwiftUI Form: grouped sections, a native Toggle, and Link rows that open URLs themselves. The About block moves from the RN MobileHome component into native rows, which leaves MobileHome with no consumers.
Font size becomes a segmented picker and the colour scheme an inline picker, so SwiftUI draws the selection checkmarks. Picker rows accept arbitrary views, which keeps the per-theme swatch — ringed so light themes stay visible against the row.
User turns move from an accent bubble to a neutral one, and tool and reasoning rows drop their leading icons for a plain muted label with the chevron against it. A failed call keeps an icon so the status is not carried by colour alone, but no longer reddens the whole line.
The connected host moves from a pinned bottom bar into a subtitle under the title, groups fold away behind their headers, and the footer pairs a search field with the new-thread action. Settings moves to the header, which leaves the host bar with no callers. Routing and the grouping rules are unchanged.
Rows lose their card and timestamp so the list reads as bare lines like the reference; the agent glyph and status dot stay because this client spans several agents and machines. The new-thread action becomes a near-black pill — its colours come from useThemeColor rather than bg-foreground/text-background utilities, which did not take effect in this uniwind setup.
The screen stops being read-only: a pill composer sends prompts and stops the running turn through useSessionActions, and rides the keyboard on a sticky view rather than resizing the inverted list. One circular action morphs between send and stop, since a turn in flight is the only thing left to do to it.
…E-444) The app pins react to RN's bundled version, so its copy is nested while @testing-library/react stays hoisted — two React instances, and every hook died on a null dispatcher. A mobile project aliases both onto the hoisted pair, which costs nothing because the pin exists for Metro's bundle and vitest never builds it. First tests cover useSessionActions against a real client over a controlled transport, so they assert wire payloads rather than a faked client.
react-native@0.86.0 peers on react ^19.2.3 — a caret range the catalog's 19.2.7 also satisfies — so the exact pin comes from Expo's bundledNativeModules, not from RN. Record the real reasons to hold it, and that keeping the catalog apart is a deliberate trade for cadence rather than an oversight.
Gated to Darwin because it drives the iOS simulator. Nix's wrapper brings its own JRE, so no JDK entry is needed and the JDK-mismatch warnings a Homebrew install produces do not appear. Records the traps: Homebrew's same-named cask is an unrelated app, and analytics upload is on unless disabled.
Covers the first-run gate (sign-in, and the manual route past it) and the settings surface. Both are daemon-free, so they hold while the rest of the UI is reworked. The settings flow doubles as the guard that @expo/ui screens stay readable to XCUITest: SwiftUI exposes its rows natively, so migrated screens need no testID.
Splits flows into e2e/flows, which need nothing but an installed dev build. first-run now deep-links to sign-in and asserts what the connect screen always offers: relying on startup routing and on the empty state made it pass only on a simulator that had never been used. Connect's URL field gains a testID and submits on return, which is worth having on its own.
Also adds the UI flow that proves typing reaches the store, which the RN TextInput could not do — the reason this screen went first.
Device revoke moves onto the row's swipe action, matching saved hosts. The avatar becomes an SF Symbol: @expo/ui's Image cannot load a remote URL.
Pull-to-refresh becomes SwiftUI's own refreshable. Only in-use terminals carry a badge now; an "Available" badge on every other row was noise.
The sheet drops @gorhom's imperative ref for @expo/ui's declarative one. Agent brand marks leave the rows: they are RN SVG components, and the fallback title already names the agent.
Records which surfaces stay React Native and why: each keeps a view that has no SwiftUI counterpart.
Root vitest gains master's setup file inside the default project: projects do not inherit root `test` options, and what it patches is web-only.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49da3e5c46
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The searchEmpty string was added with the restyle and never wired up, so a query matching nothing claimed there were no threads at all.
This was referenced Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Redesigns the Expo app against the ChatGPT/Codex reference and moves every list-and-form screen onto
@expo/ui, i.e. real SwiftUI rather than React Native views.What moved to
@expo/uiSettings · Terminal appearance · Connect · Account · Terminal list · Thread inbox · New-thread sheet · Host connection fallback.
The sheet drops
@gorhom's imperative ref for@expo/ui's declarativeisPresented. Pull-to-refresh and swipe-to-delete are now SwiftUI's own (refreshable,SwipeActions) rather than RN equivalents.What deliberately stays React Native
Each keeps a view with no SwiftUI counterpart, and RN→SwiftUI is the supported direction — going back needs
RNHostView, whose bidirectional nesting is the thing 57.0.5 had to fix:AppleAuthenticationButtonis an RN view and@expo/uihas no Sign in with Applereact-native-keyboard-controllerexpo-libghostty), startup splash (BrandMarkis a bundled RN image), navigation header (react-navigation)Two capability losses are accepted rather than worked around:
Imagetakes SF Symbols, asset-catalog names, and local file URIs but never a remote URL, so the account avatar is an SF Symbol; and the agent brand marks are RN SVG components, so thread rows and the new-thread picker name the agent in text.apps/mobile/AGENTS.mdrecords both, plus the three layout traps found only by driving the simulator (hit areas follow content not rows; aButtonfilling a row swallows swipe drags;Section.isExpandedneedslistStyle('sidebar')).Also here
add-hostdrives typing through to the store, which is what proved the nativeTextFieldfixes the text entry an RNTextInputcould not deliver.Verification
pnpm check:ciandpnpm test(2011 passed) green on the merge result; the three Maestro flows pass on iPhone 17 Pro / iOS 26.5. Every migrated screen was observed rendering on the simulator.Not verified: the connected states of the thread inbox, terminal list, and new-thread sheet — only their loading, empty, and error states. The app cannot reach any daemon in this environment, which is CODE-462: a bug that predates this branch (the same failure was on screen before any code changed here) and is unrelated to the migration. Reviewers with a working host connection should exercise those three.