Skip to content

feat(mobile): redesign the app on @expo/ui - #306

Merged
AprilNEA merged 28 commits into
masterfrom
xuan/mobile-chatgpt-redesign
Jul 29, 2026
Merged

feat(mobile): redesign the app on @expo/ui#306
AprilNEA merged 28 commits into
masterfrom
xuan/mobile-chatgpt-redesign

Conversation

@AprilNEA

Copy link
Copy Markdown
Member

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/ui

Settings · 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 declarative isPresented. 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:

  • Sign-inAppleAuthenticationButton is an RN view and @expo/ui has no Sign in with Apple
  • Conversation surface — timeline, composer, and the screen holding them; the composer also rides react-native-keyboard-controller
  • Terminal canvas (expo-libghostty), startup splash (BrandMark is a bundled RN image), navigation header (react-navigation)

Two capability losses are accepted rather than worked around: Image takes 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.md records both, plus the three layout traps found only by driving the simulator (hit areas follow content not rows; a Button filling a row swallows swipe drags; Section.isExpanded needs listStyle('sidebar')).

Also here

  • Maestro UI e2e, pinned in devenv and Darwin-gated. Three daemon-free flows; add-host drives typing through to the store, which is what proved the native TextField fixes the text entry an RN TextInput could not deliver.
  • A vitest project for mobile (CODE-444) so its hooks are unit-testable despite the app pinning react to RN's bundled renderer.
  • Host-connection recovery (CODE-193) and the runtime seams (CODE-443), which had not landed separately.

Verification

pnpm check:ci and pnpm 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.

AprilNEA added 27 commits July 25, 2026 13:17
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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread apps/mobile/src/app/host/[hostId]/index.tsx Outdated
The searchEmpty string was added with the restyle and never wired up, so
a query matching nothing claimed there were no threads at all.
@AprilNEA
AprilNEA merged commit 04d3207 into master Jul 29, 2026
8 of 10 checks passed
@AprilNEA
AprilNEA deleted the xuan/mobile-chatgpt-redesign branch July 29, 2026 09:17
@linear-code

linear-code Bot commented Jul 29, 2026

Copy link
Copy Markdown

CODE-465

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