22// System-WebView browser + AI chat + agents + settings. This is the companion
33// app, NOT the Ungoogled Chromium engine — see docs/mobile-architecture.md.
44import { StatusBar } from 'expo-status-bar' ;
5- import { useState } from 'react' ;
6- import { SafeAreaView , StyleSheet , Text , TouchableOpacity , View } from 'react-native' ;
5+ import { useState , type ReactNode } from 'react' ;
6+ import { Keyboard , StyleSheet , Text , TouchableOpacity , View } from 'react-native' ;
7+ import { SafeAreaProvider , useSafeAreaInsets } from 'react-native-safe-area-context' ;
78import { BrowserScreen } from './src/screens/BrowserScreen' ;
89import { ChatScreen } from './src/screens/ChatScreen' ;
910import { AgentsScreen } from './src/screens/AgentsScreen' ;
@@ -19,26 +20,75 @@ const TABS: { key: TabKey; label: string; icon: string }[] = [
1920 { key : 'settings' , label : 'Settings' , icon : '⚙️' } ,
2021] ;
2122
23+ // Inactive scenes are parked offscreen inside an overflow-hidden host instead
24+ // of being unmounted (which destroys WebView history and chat state) or given
25+ // `display: 'none'` (which Android can treat as a native detach). Same
26+ // technique as react-navigation's ResourceSavingView.
27+ const DETACHED_TOP = 100000 ;
28+
29+ function TabScene ( { active, children } : { active : boolean ; children : ReactNode } ) {
30+ return (
31+ < View
32+ style = { [ styles . scene , { zIndex : active ? 0 : - 1 } ] }
33+ collapsable = { false }
34+ pointerEvents = { active ? 'auto' : 'none' }
35+ importantForAccessibility = { active ? 'auto' : 'no-hide-descendants' }
36+ accessibilityElementsHidden = { ! active }
37+ >
38+ < View style = { [ styles . sceneInner , ! active && styles . sceneDetached ] } collapsable = { false } >
39+ { children }
40+ </ View >
41+ </ View >
42+ ) ;
43+ }
44+
2245export default function App ( ) {
46+ return (
47+ < SafeAreaProvider >
48+ < AppShell />
49+ </ SafeAreaProvider >
50+ ) ;
51+ }
52+
53+ function AppShell ( ) {
2354 const [ tab , setTab ] = useState < TabKey > ( 'browser' ) ;
55+ // Android 15/16 enforce edge-to-edge: the app draws under the system bars,
56+ // so the toolbar and tab bar must pad themselves out of the way explicitly.
57+ const insets = useSafeAreaInsets ( ) ;
2458
2559 return (
26- < SafeAreaView style = { styles . root } >
60+ < View
61+ style = { [
62+ styles . root ,
63+ { paddingTop : insets . top , paddingLeft : insets . left , paddingRight : insets . right } ,
64+ ] }
65+ >
2766 < StatusBar style = "light" />
2867 < View style = { styles . screen } >
29- { tab === 'browser' && < BrowserScreen /> }
30- { tab === 'chat' && < ChatScreen /> }
31- { tab === 'agents' && < AgentsScreen /> }
32- { tab === 'settings' && < SettingsScreen /> }
68+ < TabScene active = { tab === 'browser' } >
69+ < BrowserScreen isActive = { tab === 'browser' } />
70+ </ TabScene >
71+ < TabScene active = { tab === 'chat' } >
72+ < ChatScreen />
73+ </ TabScene >
74+ < TabScene active = { tab === 'agents' } >
75+ < AgentsScreen />
76+ </ TabScene >
77+ < TabScene active = { tab === 'settings' } >
78+ < SettingsScreen />
79+ </ TabScene >
3380 </ View >
34- < View style = { styles . tabBar } >
81+ < View style = { [ styles . tabBar , { paddingBottom : 6 + insets . bottom } ] } >
3582 { TABS . map ( ( t ) => {
3683 const active = t . key === tab ;
3784 return (
3885 < TouchableOpacity
3986 key = { t . key }
4087 style = { styles . tab }
41- onPress = { ( ) => setTab ( t . key ) }
88+ onPress = { ( ) => {
89+ if ( t . key !== tab ) Keyboard . dismiss ( ) ;
90+ setTab ( t . key ) ;
91+ } }
4292 accessibilityRole = "tab"
4393 accessibilityState = { { selected : active } }
4494 >
@@ -48,19 +98,21 @@ export default function App() {
4898 ) ;
4999 } ) }
50100 </ View >
51- </ SafeAreaView >
101+ </ View >
52102 ) ;
53103}
54104
55105const styles = StyleSheet . create ( {
56106 root : { flex : 1 , backgroundColor : theme . bg } ,
57107 screen : { flex : 1 } ,
108+ scene : { position : 'absolute' , top : 0 , left : 0 , right : 0 , bottom : 0 , overflow : 'hidden' } ,
109+ sceneInner : { flex : 1 } ,
110+ sceneDetached : { top : DETACHED_TOP } ,
58111 tabBar : {
59112 flexDirection : 'row' ,
60113 backgroundColor : theme . surface ,
61114 borderTopWidth : StyleSheet . hairlineWidth ,
62115 borderTopColor : theme . border ,
63- paddingBottom : 6 ,
64116 } ,
65117 tab : { flex : 1 , alignItems : 'center' , paddingVertical : 8 , gap : 2 } ,
66118 tabIcon : { fontSize : 18 } ,
0 commit comments