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