diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts
index 48c5297..1280f1f 100644
--- a/apps/mobile/app.config.ts
+++ b/apps/mobile/app.config.ts
@@ -41,7 +41,7 @@ if (projectId) {
const config: ExpoConfig = {
name: appName,
slug,
- version: "0.1.3",
+ version: "0.1.4",
newArchEnabled: true,
platforms: ["ios", "android"],
icon: "./assets/icon.png",
@@ -70,7 +70,7 @@ const config: ExpoConfig = {
android: {
package: androidPackage,
...(googleServicesFile ? { googleServicesFile } : {}),
- versionCode: 5,
+ versionCode: 6,
allowBackup: false,
predictiveBackGestureEnabled: false,
softwareKeyboardLayoutMode: "resize",
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 297c40f..a1e7c5c 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode2-mobile/mobile",
- "version": "0.1.3",
+ "version": "0.1.4",
"private": true,
"main": "index.ts",
"scripts": {
@@ -45,6 +45,7 @@
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-gesture-handler": "~2.28.0",
+ "react-native-keyboard-controller": "1.18.5",
"react-native-reanimated": "4.1.7",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.16.0",
diff --git a/apps/mobile/src/app.tsx b/apps/mobile/src/app.tsx
index 39644ff..c76eccf 100644
--- a/apps/mobile/src/app.tsx
+++ b/apps/mobile/src/app.tsx
@@ -2,8 +2,9 @@ import { NavigationContainer, type Theme } from "@react-navigation/native";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { SQLiteProvider } from "expo-sqlite";
import { Component, type ErrorInfo, type ReactNode } from "react";
-import { Pressable, Share, StyleSheet, Text, View } from "react-native";
+import { Platform, Pressable, Share, StyleSheet, Text, View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
+import { KeyboardProvider } from "react-native-keyboard-controller";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { applicationName } from "./application-name";
@@ -48,28 +49,43 @@ const navigationTheme: Theme = {
};
export default function App() {
+ const application = (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {Platform.OS === "android" ? (
+
+ {application}
+
+ ) : (
+ application
+ )}
);
diff --git a/apps/mobile/src/screens/workspace-screen.integration.test.tsx b/apps/mobile/src/screens/workspace-screen.integration.test.tsx
index d9eacd0..0d3c60a 100644
--- a/apps/mobile/src/screens/workspace-screen.integration.test.tsx
+++ b/apps/mobile/src/screens/workspace-screen.integration.test.tsx
@@ -10,7 +10,7 @@ import {
import { type InfiniteData, QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react-native";
import type { ReactNode } from "react";
-import { Dimensions, FlatList, RefreshControl } from "react-native";
+import { Dimensions, FlatList, Platform, RefreshControl } from "react-native";
import { openCodeQueryKeys } from "../state/open-code-query-keys";
import { WorkspaceSelectionProvider } from "../state/workspace-selection-context";
import { SessionScreen, WorkspaceScreen } from "./workspace-screen";
@@ -220,11 +220,56 @@ jest.mock("react-native-gesture-handler/ReanimatedSwipeable", () => ({
__esModule: true,
default: ({ children }: { children: ReactNode }) => children,
}));
+jest.mock("react-native-keyboard-controller", () => {
+ const React = jest.requireActual("react");
+ const { View } = jest.requireActual("react-native");
+ return {
+ KeyboardStickyView: ({ children, ...props }: { children: ReactNode }) =>
+ React.createElement(View, { ...props, testID: "keyboard-sticky-view" }, children),
+ };
+});
const mockGetSession = jest.mocked(getOpenCodeSession);
const mockGetLocation = jest.mocked(getOpenCodeLocation);
const mockListMessages = jest.mocked(listOpenCodeMessages);
+test("moves only the Android composer dock with the keyboard", async () => {
+ const platformOS = Platform.OS;
+ Object.defineProperty(Platform, "OS", { configurable: true, value: "android" });
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ mutations: { networkMode: "always" },
+ queries: { gcTime: Infinity, retry: false },
+ },
+ });
+ const view = render(
+
+
+ ,
+ );
+
+ try {
+ await screen.findByLabelText("Keyboard composer dock");
+ expect(screen.getByTestId("keyboard-sticky-view")).toBeOnTheScreen();
+ expect(screen.getByLabelText("Keyboard-aware session")).toBeOnTheScreen();
+ } finally {
+ view.unmount();
+ queryClient.clear();
+ Object.defineProperty(Platform, "OS", { configurable: true, value: platformOS });
+ }
+});
+
test("shows a permission blocking the open session and can reply", async () => {
mockWorkspacePermissions = [
{
diff --git a/apps/mobile/src/screens/workspace-screen.tsx b/apps/mobile/src/screens/workspace-screen.tsx
index d162755..757f181 100644
--- a/apps/mobile/src/screens/workspace-screen.tsx
+++ b/apps/mobile/src/screens/workspace-screen.tsx
@@ -35,6 +35,7 @@ import {
View,
} from "react-native";
import ReanimatedSwipeable from "react-native-gesture-handler/ReanimatedSwipeable";
+import { KeyboardStickyView } from "react-native-keyboard-controller";
import { useConnections } from "../connections/connections-context";
import type { RootStackParamList } from "../navigation/root-navigation";
import { useConnectionRuntime } from "../state/connection-runtime-context";
@@ -802,6 +803,58 @@ export function SessionScreen({ navigation, route }: SessionProps) {
);
}
+ const composerDockContent = (
+
+ 0 ? (
+
+ ) : undefined
+ }
+ inbox={execution.inbox}
+ onAllowRetry={execution.allowRetry}
+ onCancelInbox={execution.cancelInbox}
+ onCheckAdmission={execution.reconcileAdmission}
+ onInterrupt={execution.interrupt}
+ onQueueInbox={execution.queueInbox}
+ onReplyPermission={workspaceSelection.replyPermission}
+ onSteerInbox={execution.steerInbox}
+ permissionReplyError={workspaceSelection.permissionReplyError}
+ permissions={sessionPermissions}
+ projectedMessageIds={execution.projectedMessageIds}
+ replyingPermissionId={workspaceSelection.replyingPermissionId}
+ />
+ execution.submit(draft.draft)}
+ />
+
+ );
+
return (
) : null}
-
-
- 0 ? (
-
- ) : undefined
- }
- inbox={execution.inbox}
- onAllowRetry={execution.allowRetry}
- onCancelInbox={execution.cancelInbox}
- onCheckAdmission={execution.reconcileAdmission}
- onInterrupt={execution.interrupt}
- onQueueInbox={execution.queueInbox}
- onReplyPermission={workspaceSelection.replyPermission}
- onSteerInbox={execution.steerInbox}
- permissionReplyError={workspaceSelection.permissionReplyError}
- permissions={sessionPermissions}
- projectedMessageIds={execution.projectedMessageIds}
- replyingPermissionId={workspaceSelection.replyingPermissionId}
- />
- execution.submit(draft.draft)}
- />
+ {Platform.OS === "android" ? (
+
+ {composerDockContent}
+
+ ) : (
+
+ {composerDockContent}
-
+ )}
);
diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md
index 1eaef75..fbca940 100644
--- a/docs/COMPATIBILITY.md
+++ b/docs/COMPATIBILITY.md
@@ -580,3 +580,27 @@ The signed iPhone applied the preview update and showed no Live or session-list
flicker during the controlled command checks. The trace and report retained no
command text, address, credential, path, prompt, identifier, event payload, file
content, or server content.
+
+## 2026-08-27: physical Android 17 composer probe
+
+### Stack
+
+- Mobile runtime: signed EAS preview build using Hermes
+- Device: Pixel 8 Pro running Android 17
+- Expo SDK: 54.0.37
+- React Native: 0.81.5
+- Keyboard controller: 1.18.5
+
+### Results
+
+| Probe | Result |
+| --- | --- |
+| Install and launch preview version 0.1.4, build 6 | Pass |
+| Focus the session composer and show the software keyboard | Pass |
+| Keep the composer visible directly above the keyboard | Pass |
+
+The prior build relied on activity resize and hid the composer behind the
+keyboard on this device. The corrected build keeps the transcript container
+fixed and moves only the composer dock from native keyboard inset animation
+frames. The probe recorded no address, credential, identifier, path, prompt, or
+server content.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0ba1111..44e671c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -112,6 +112,9 @@ importers:
react-native-gesture-handler:
specifier: ~2.28.0
version: 2.28.0(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)
+ react-native-keyboard-controller:
+ specifier: 1.18.5
+ version: 1.18.5(react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.7(supports-color@8.1.1))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)
react-native-reanimated:
specifier: 4.1.7
version: 4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.7(supports-color@8.1.1))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)
@@ -4097,6 +4100,13 @@ packages:
react: '*'
react-native: '*'
+ react-native-keyboard-controller@1.18.5:
+ resolution: {integrity: sha512-wbYN6Tcu3G5a05dhRYBgjgd74KqoYWuUmroLpigRg9cXy5uYo7prTMIvMgvLtARQtUF7BOtFggUnzgoBOgk0TQ==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+ react-native-reanimated: '>=3.0.0'
+
react-native-reanimated@4.1.7:
resolution: {integrity: sha512-Q4H6xA3Tn7QL0/E/KjI86I1KK4tcf+ErRE04LH34Etka2oVQhW6oXQ+Q8ZcDCVxiWp5vgbBH6XcH8BOo4w/Rhg==}
peerDependencies:
@@ -9724,6 +9734,13 @@ snapshots:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1)
+ react-native-keyboard-controller@1.18.5(react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.7(supports-color@8.1.1))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ react-native: 0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1)
+ react-native-is-edge-to-edge: 1.3.1(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)
+ react-native-reanimated: 4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.7(supports-color@8.1.1))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)
+
react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.7(supports-color@8.1.1))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-native@0.81.5(@babel/core@7.29.7(supports-color@8.1.1))(@react-native/metro-config@0.86.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0):
dependencies:
react: 19.1.0