Generated: 2026-01-08 | Files: 209 | Role: Expo/React Native iOS app
Native iOS app built with Expo 54 and React Native 0.81. Connects to OpenChamber web server via HTTP. Separate UI implementation from web/desktop (does not use @openchamber/ui).
app/ # Expo Router (file-based routing)
├── _layout.tsx # Root: GestureHandler, SafeArea, Theme
├── index.tsx # Entry/splash
├── (tabs)/ # Tab navigation
│ ├── _layout.tsx # Tab container
│ ├── chat.tsx # Main chat screen
│ ├── git.tsx # Git operations
│ └── terminal.tsx # Terminal view
├── onboarding/ # Server pairing flow
│ ├── scan.tsx # QR code scanner
│ └── manual.tsx # Manual URL entry
└── settings/ # Settings screens
├── index.tsx
└── agents/[name].tsx # Dynamic routes
src/
├── components/ # React Native components
│ ├── chat/ # Chat UI
│ ├── settings/ # Settings UI
│ │ └── primitives/ # Reusable settings components
│ └── ui/ # Base primitives
├── hooks/ # Custom hooks
├── stores/ # Zustand stores
├── api/ # HTTP API adapters
├── lib/ # Utilities
│ ├── httpClient.ts # XMLHttpRequest wrapper
│ ├── offlineCache.ts # LRU message cache
│ └── storage.ts # AsyncStorage + SecureStore
└── theme/ # Theme system
| Aspect | Mobile | Web/Desktop |
|---|---|---|
| UI components | Own implementation | @openchamber/ui |
| Styling | Uniwind (Tailwind for RN) | Tailwind CSS v4 |
| Navigation | Expo Router | React Router |
| State | Own Zustand stores | Shared stores |
| Terminal | HTTP streaming | node-pty/portable-pty |
| Auth | SecureStore + biometric | Session cookies |
<View className="flex-row items-center gap-2 px-3 py-2 rounded-xl" />// component.styles.ts
import { tv } from 'tailwind-variants';
const container = tv({
base: "relative",
variants: { disabled: { true: "opacity-50" } }
});const styles = StyleSheet.create({ container: { flex: 1 } });// app/(tabs)/newscreen.tsx
export default function NewScreen() {
return <View className="flex-1">...</View>;
}Add tab in app/(tabs)/_layout.tsx.
// app/settings/newsetting.tsx
export default function NewSetting() { ... }Add navigation in app/settings/index.tsx.
// app/settings/items/[id].tsx
export default function ItemDetail() {
const { id } = useLocalSearchParams();
}| Module | Purpose |
|---|---|
expo-camera |
QR code scanning |
expo-secure-store |
Encrypted token storage |
expo-haptics |
Tactile feedback |
expo-local-authentication |
Face ID / Touch ID |
expo-file-system |
Offline cache |
expo-clipboard |
Copy operations |
import * as Haptics from 'expo-haptics';
// Button press
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
// Selection change
Haptics.selectionAsync();
// Success/Error
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);Used in 30+ files - add to all interactive elements.
LRU cache in lib/offlineCache.ts:
- Max 50 sessions, 10 with full messages
- 500 messages per session
- 100MB total, 7-day TTL
import { offlineCache } from '@/lib/offlineCache';
const cached = await offlineCache.getMessages(sessionId);Use components from components/settings/primitives/:
import { SettingsScreen, SettingsSection, SettingsRow } from '@/components/settings/primitives';
<SettingsScreen title="My Settings">
<SettingsSection title="General">
<SettingsRow title="Option" onPress={...} />
</SettingsSection>
</SettingsScreen>All in src/api/:
import { sessionsApi, gitApi, terminalApi } from '@/api';
// HTTP calls to web server
const sessions = await sessionsApi.list();Auth token injected automatically via httpClient.ts.
| Store | Purpose |
|---|---|
useConnectionStore |
Server URL, auth token |
useTerminalStore |
Terminal sessions |
Persistence via SecureStore for sensitive data.
bun run start # Expo dev server
bun run ios:simulator # iOS Simulator
bun run ios # Physical device
bun run build:ios # EAS cloud build| Profile | Bundle ID | Purpose |
|---|---|---|
| development | ceo.nerd.opencode.dev | Dev builds |
| preview | ceo.nerd.opencode.preview | TestFlight |
| production | ceo.nerd.opencode | App Store |
| Pattern | Fix |
|---|---|
| Inline styles for static | Use className (Uniwind) |
| Missing haptics | Add to all interactions |
| Ignoring safe areas | Use useSafeAreaInsets() |
| Blocking renders | Use @shopify/flash-list |
| Storing secrets in AsyncStorage | Use SecureStore |
- Minimum iOS 14.0
- iPad support with adaptive layouts
- New Architecture enabled (
newArchEnabled: true) - Deep linking:
opencode://scheme