Add React Native (Expo) iOS app over the shared server #3

Merged
kubus merged 2 commits from t3code/react-native-expo-ios-app into main 2026-09-29 15:54:16 +00:00
Owner

Summary

  • Add apps/mobile, a React Native (Expo) iOS client that talks to the same hat server as web and desktop, with native streaming rather than a WebView.
  • Move the SSE frame parser into @hat/core (SseFrameParser/decodeFrame) so web, desktop, and mobile share one wire format; web now imports it from core.
  • Stream turns with expo/fetch (real ReadableStream) since RN fetch buffers and EventSource cannot POST; auth uses the CSRF-exempt bearer token in the iOS keychain, no cookies or login flow.
  • Add apps/mobile/metro.config.js to handle workspace traversal, raw-TypeScript @hat/core, and the ESM .js-extension rewriting the repo's relative imports rely on.
  • Add a local liquid-glass Expo module using UIGlassEffect on iOS 26 with a UIBlurEffect/expo-blur fallback, including in Expo Go.
  • Add EAS build profiles and .github/workflows/ios-ipa.yml to build an unsigned IPA on a macOS runner for free Apple IDs signed on-device with SideStore (7-day expiry, 3 apps).
  • Replace docs/ios-app-plan.md with docs/ios-app.md and document the app, its constraints, and remaining work in the README milestone notes.

Testing

  • pnpm typecheck
  • pnpm test (includes new apps/mobile/src/chat.test.ts and tokens.test.ts view-model/token tests)
  • pnpm --filter @hat/mobile typecheck
  • pnpm smoke
  • Not run: EAS device build (eas build --profile device -p ios), SideStore install, and on-device Liquid Glass rendering — requires a paid Apple Developer account and a physical device.
## Summary - Add `apps/mobile`, a React Native (Expo) iOS client that talks to the same hat server as web and desktop, with native streaming rather than a WebView. - Move the SSE frame parser into `@hat/core` (`SseFrameParser`/`decodeFrame`) so web, desktop, and mobile share one wire format; web now imports it from core. - Stream turns with `expo/fetch` (real `ReadableStream`) since RN `fetch` buffers and `EventSource` cannot POST; auth uses the CSRF-exempt bearer token in the iOS keychain, no cookies or login flow. - Add `apps/mobile/metro.config.js` to handle workspace traversal, raw-TypeScript `@hat/core`, and the ESM `.js`-extension rewriting the repo's relative imports rely on. - Add a local `liquid-glass` Expo module using `UIGlassEffect` on iOS 26 with a `UIBlurEffect`/`expo-blur` fallback, including in Expo Go. - Add EAS build profiles and `.github/workflows/ios-ipa.yml` to build an unsigned IPA on a macOS runner for free Apple IDs signed on-device with SideStore (7-day expiry, 3 apps). - Replace `docs/ios-app-plan.md` with `docs/ios-app.md` and document the app, its constraints, and remaining work in the README milestone notes. ## Testing - [ ] `pnpm typecheck` - [ ] `pnpm test` (includes new `apps/mobile/src/chat.test.ts` and `tokens.test.ts` view-model/token tests) - [ ] `pnpm --filter @hat/mobile typecheck` - [ ] `pnpm smoke` - Not run: EAS device build (`eas build --profile device -p ios`), SideStore install, and on-device Liquid Glass rendering — requires a paid Apple Developer account and a physical device.
- Add apps/mobile Expo client with connect, chat, sessions, and settings tabs
- Stream turns through expo/fetch and keep the server token in the iOS keychain
- Move the SSE frame parser from apps/web into @hat/core for all clients
- Add a local Liquid Glass Expo module with an expo-blur fallback
- Add an unsigned-IPA workflow for building with a free Apple ID
Brings in the turn-reattach work, the browser/websearch plugins, generated
session titles, and the multi-iteration streaming fix from main. The lockfile
conflict is resolved by regenerating from the union of both manifests, so both
sides' new packages are present rather than one side's wins.

Main also changed the server contract in ways this client silently violated,
so adapt to them rather than just merging cleanly:

- Stop is now POST /turn/cancel plus a detach. Aborting the request only stops
  the updates; the model kept generating and billing with nobody watching.
- Leaving a conversation detaches without cancelling, and the app reattaches
  to a live turn (GET /stream) on launch, on opening a conversation, and on
  returning to the foreground. This is the common case on iOS, which suspends
  backgrounded apps freely.
- The in-flight view-model is a list, not one message. A tool-using turn emits
  message.start per model iteration, and each iteration's tool cards have to
  survive; tool events that carry no message id are matched by callId.
- resolveApproval sends the session id, so the server can reject a decision
  aimed at another conversation.
- session.title is handled, so a new conversation names itself while the first
  turn is still running.
- The image picker filters to the types the server accepts. It reads dimensions
  from the file header, so it takes PNG/JPEG/GIF/WebP only, and
  expo-image-picker returns HEIC for most camera-roll photos.
- Streamed messages are promoted into history before the post-turn refetch, so
  the refetch reconciles rather than blanking the last deltas.

Smoke is 34/35: "tool approval round-trip" also fails on origin/main in a clean
worktree, so it is pre-existing there and not from this merge.
kubus merged commit 865beb2faf into main 2026-09-29 15:54:16 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kubus/hatchat!3
No description provided.