diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx
index 10721cb..ee68a4b 100644
--- a/apps/mobile/App.tsx
+++ b/apps/mobile/App.tsx
@@ -3,7 +3,15 @@
// app, NOT the Ungoogled Chromium engine — see docs/mobile-architecture.md.
import { StatusBar } from 'expo-status-bar';
import { useState, type ReactNode } from 'react';
-import { Keyboard, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+import {
+ Keyboard,
+ KeyboardAvoidingView,
+ Platform,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import { BrowserScreen } from './src/screens/BrowserScreen';
import { ChatScreen } from './src/screens/ChatScreen';
@@ -57,7 +65,12 @@ function AppShell() {
const insets = useSafeAreaInsets();
return (
-
-
+
);
}
diff --git a/apps/mobile/test/keyboard.test.tsx b/apps/mobile/test/keyboard.test.tsx
new file mode 100644
index 0000000..1707bd2
--- /dev/null
+++ b/apps/mobile/test/keyboard.test.tsx
@@ -0,0 +1,42 @@
+import { describe, expect, it } from 'vitest';
+import App from '../App';
+import { flat, hosts, renderScreen, scenes, tabButton } from './harness';
+import { Platform } from './mocks/react-native';
+import { setMockSafeAreaInsets } from './mocks/react-native-safe-area-context';
+
+// Native geometry is exercised separately with the APK. These tests pin the
+// platform wiring so mocked native components cannot claim to prove IME layout.
+describe('shell keyboard avoidance', () => {
+ it('resizes the Android shell containing the scenes and tabs', async () => {
+ const renderer = await renderScreen();
+ const shell = hosts(renderer.root, 'KeyboardAvoidingView').find(
+ (node) => node.props.enabled === true,
+ );
+ expect(shell).toBeDefined();
+ expect(shell!.props.behavior).toBe('height');
+ expect(shell!.props.keyboardVerticalOffset).toBe(0);
+ expect(scenes(shell!)).toHaveLength(4);
+ expect(tabButton(shell!, 'Chat')).toBeDefined();
+ });
+
+ it('keeps system insets inside the keyboard-aware root without a second offset', async () => {
+ setMockSafeAreaInsets({ top: 37, right: 12, bottom: 48, left: 12 });
+ const renderer = await renderScreen();
+ const shell = hosts(renderer.root, 'KeyboardAvoidingView').find(
+ (node) => node.props.enabled === true,
+ );
+ expect(shell).toBeDefined();
+ expect(flat(shell!)).toMatchObject({ paddingTop: 37, paddingLeft: 12, paddingRight: 12 });
+ expect(shell!.props.keyboardVerticalOffset).toBe(0);
+ });
+
+ it('does not add a second keyboard adjustment to the existing iOS chat', async () => {
+ Platform.OS = 'ios';
+ const renderer = await renderScreen();
+ const views = hosts(renderer.root, 'KeyboardAvoidingView');
+ const shell = views.find((node) => node.props.enabled === false);
+ expect(shell).toBeDefined();
+ expect(shell!.props.behavior).toBeUndefined();
+ expect(views.some((node) => node.props.behavior === 'padding')).toBe(true);
+ });
+});
diff --git a/apps/mobile/test/safe-area.test.tsx b/apps/mobile/test/safe-area.test.tsx
index 8b8821a..2ddd139 100644
--- a/apps/mobile/test/safe-area.test.tsx
+++ b/apps/mobile/test/safe-area.test.tsx
@@ -11,7 +11,7 @@ import { setMockSafeAreaInsets } from './mocks/react-native-safe-area-context';
import type { ReactTestInstance } from 'react-test-renderer';
-const shellRoot = (root: ReactTestInstance) => hosts(root, 'View')[0];
+const shellRoot = (root: ReactTestInstance) => hosts(root, 'KeyboardAvoidingView')[0];
const tabBar = (root: ReactTestInstance) =>
hostWhere(