diff --git a/packages/mobile/src/components/PermissionSheet.tsx b/packages/mobile/src/components/PermissionSheet.tsx index bb3bc9a..672cbc5 100644 --- a/packages/mobile/src/components/PermissionSheet.tsx +++ b/packages/mobile/src/components/PermissionSheet.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useMemo, useRef } from 'react' import { Animated, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { useKeyboardHeight } from '../hooks/useKeyboardHeight' export interface PermissionRequest { requestId: string @@ -48,7 +47,6 @@ function isDangerous(details: string[]): boolean { export function PermissionSheet({ request, onDecide }: Props) { const { bottom: bottomInset } = useSafeAreaInsets() - const keyboardHeight = useKeyboardHeight() const slideAnim = useRef(new Animated.Value(300)).current const progressAnim = useRef(new Animated.Value(1)).current @@ -73,14 +71,13 @@ export function PermissionSheet({ request, onDecide }: Props) { if (!request) return null const decide = (decision: 'approve' | 'reject' | 'always') => onDecide(request.requestId, decision) - const sheetPaddingBottom = keyboardHeight > 0 ? 24 : 24 + bottomInset + const sheetPaddingBottom = 24 + bottomInset return ( {/* Progress bar */} @@ -142,10 +139,6 @@ export function PermissionSheet({ request, onDecide }: Props) { const styles = StyleSheet.create({ sheet: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, backgroundColor: '#0d1117', borderTopLeftRadius: 16, borderTopRightRadius: 16, diff --git a/packages/mobile/src/screens/TerminalScreen.tsx b/packages/mobile/src/screens/TerminalScreen.tsx index ef470b9..e60a18d 100644 --- a/packages/mobile/src/screens/TerminalScreen.tsx +++ b/packages/mobile/src/screens/TerminalScreen.tsx @@ -180,7 +180,13 @@ export function TerminalScreen() { {/* 承認ボトムシート */} - + + + ) } @@ -217,4 +223,12 @@ const styles = StyleSheet.create({ color: '#d4d4d4', fontSize: 12, }, + permissionOverlay: { + position: 'absolute', + left: 0, + right: 0, + // Keep the permission sheet above the WebView/toolbar stack on mobile. + zIndex: 10, + elevation: 10, + }, }) diff --git a/packages/mobile/src/screens/__tests__/TerminalScreen.test.tsx b/packages/mobile/src/screens/__tests__/TerminalScreen.test.tsx index fe47c6a..75538c7 100644 --- a/packages/mobile/src/screens/__tests__/TerminalScreen.test.tsx +++ b/packages/mobile/src/screens/__tests__/TerminalScreen.test.tsx @@ -3,6 +3,11 @@ import { render, screen, fireEvent, act } from '@testing-library/react-native' import { TerminalScreen } from '../TerminalScreen' import { injectJavaScriptMock } from '../../__mocks__/react-native-webview' import { useLocalSearchParams, mockRouterBack } from '../../__mocks__/expo-router' +import { useKeyboardHeight } from '../../hooks/useKeyboardHeight' + +jest.mock('../../hooks/useKeyboardHeight', () => ({ + useKeyboardHeight: jest.fn(() => 0), +})) describe('TerminalScreen', () => { // WebView から onMessage を発火するヘルパー @@ -16,6 +21,7 @@ describe('TerminalScreen', () => { beforeEach(() => { jest.clearAllMocks() injectJavaScriptMock.mockClear() + ;(useKeyboardHeight as jest.Mock).mockReturnValue(0) ;(useLocalSearchParams as jest.Mock).mockReturnValue({ ip: '100.64.0.1', token: 'test-token', @@ -101,6 +107,29 @@ describe('TerminalScreen', () => { }) describe('PermissionSheet', () => { + it('キーボード表示中でも PermissionSheet が表示される', () => { + ;(useKeyboardHeight as jest.Mock).mockReturnValue(180) + + render() + sendFromWebView({ + type: 'permission_request', + requestId: 'req-keyboard', + toolName: 'Bash', + details: ['echo hello'], + requiresAlways: false, + createdAt: Date.now(), + }) + + const overlay = screen.getByTestId('permission-overlay') + expect(overlay.props.style).toEqual( + expect.arrayContaining([ + expect.objectContaining({ bottom: 180 }), + ]), + ) + expect(screen.getByText('Permission Request')).toBeTruthy() + expect(screen.getByText('Allow')).toBeTruthy() + }) + it('permission_request を受信すると PermissionSheet が表示される', () => { render() sendFromWebView({ @@ -109,6 +138,7 @@ describe('TerminalScreen', () => { toolName: 'Bash', details: ['rm -rf /tmp/test'], requiresAlways: true, + createdAt: Date.now(), }) expect(screen.getByText('Permission Request')).toBeTruthy() @@ -127,6 +157,7 @@ describe('TerminalScreen', () => { toolName: 'Write', details: ['/tmp/file.ts'], requiresAlways: false, + createdAt: Date.now(), }) expect(screen.getByText('Allow')).toBeTruthy() @@ -142,6 +173,7 @@ describe('TerminalScreen', () => { toolName: 'Bash', details: [], requiresAlways: false, + createdAt: Date.now(), }) fireEvent.press(screen.getByText('Allow')) @@ -160,6 +192,7 @@ describe('TerminalScreen', () => { toolName: 'Bash', details: [], requiresAlways: false, + createdAt: Date.now(), }) fireEvent.press(screen.getByText('Deny')) @@ -178,6 +211,7 @@ describe('TerminalScreen', () => { toolName: 'Bash', details: [], requiresAlways: true, + createdAt: Date.now(), }) fireEvent.press(screen.getByText('Always Allow'))