From 57952a8a004e03525d6f8fe379672153c053c38b Mon Sep 17 00:00:00 2001 From: Shun Okada Date: Fri, 3 Apr 2026 12:09:56 +0900 Subject: [PATCH 1/3] fix(mobile): stabilize PermissionSheet visibility --- .../mobile/src/components/PermissionSheet.tsx | 12 +++------- .../mobile/src/screens/TerminalScreen.tsx | 15 +++++++++++- .../screens/__tests__/TerminalScreen.test.tsx | 23 +++++++++++++++++++ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/packages/mobile/src/components/PermissionSheet.tsx b/packages/mobile/src/components/PermissionSheet.tsx index bb3bc9a..19cd289 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, @@ -158,6 +151,7 @@ const styles = StyleSheet.create({ shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.4, shadowRadius: 8, + zIndex: 10, }, progressBar: { height: 3, diff --git a/packages/mobile/src/screens/TerminalScreen.tsx b/packages/mobile/src/screens/TerminalScreen.tsx index ef470b9..514d433 100644 --- a/packages/mobile/src/screens/TerminalScreen.tsx +++ b/packages/mobile/src/screens/TerminalScreen.tsx @@ -180,7 +180,12 @@ export function TerminalScreen() { {/* 承認ボトムシート */} - + + + ) } @@ -217,4 +222,12 @@ const styles = StyleSheet.create({ color: '#d4d4d4', fontSize: 12, }, + permissionOverlay: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + 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..b2e5b64 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,23 @@ 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(), + }) + + expect(screen.getByText('Permission Request')).toBeTruthy() + expect(screen.getByText('Allow')).toBeTruthy() + }) + it('permission_request を受信すると PermissionSheet が表示される', () => { render() sendFromWebView({ From e1c743861ef8906ff2e3275602128f90170af6ce Mon Sep 17 00:00:00 2001 From: Shun Okada Date: Fri, 3 Apr 2026 14:07:14 +0900 Subject: [PATCH 2/3] test(mobile): tighten PermissionSheet overlay assertions --- packages/mobile/src/components/PermissionSheet.tsx | 1 - packages/mobile/src/screens/TerminalScreen.tsx | 1 + .../src/screens/__tests__/TerminalScreen.test.tsx | 11 +++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/mobile/src/components/PermissionSheet.tsx b/packages/mobile/src/components/PermissionSheet.tsx index 19cd289..672cbc5 100644 --- a/packages/mobile/src/components/PermissionSheet.tsx +++ b/packages/mobile/src/components/PermissionSheet.tsx @@ -151,7 +151,6 @@ const styles = StyleSheet.create({ shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.4, shadowRadius: 8, - zIndex: 10, }, progressBar: { height: 3, diff --git a/packages/mobile/src/screens/TerminalScreen.tsx b/packages/mobile/src/screens/TerminalScreen.tsx index 514d433..1834d6f 100644 --- a/packages/mobile/src/screens/TerminalScreen.tsx +++ b/packages/mobile/src/screens/TerminalScreen.tsx @@ -181,6 +181,7 @@ export function TerminalScreen() { {/* 承認ボトムシート */} diff --git a/packages/mobile/src/screens/__tests__/TerminalScreen.test.tsx b/packages/mobile/src/screens/__tests__/TerminalScreen.test.tsx index b2e5b64..75538c7 100644 --- a/packages/mobile/src/screens/__tests__/TerminalScreen.test.tsx +++ b/packages/mobile/src/screens/__tests__/TerminalScreen.test.tsx @@ -120,6 +120,12 @@ describe('TerminalScreen', () => { 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() }) @@ -132,6 +138,7 @@ describe('TerminalScreen', () => { toolName: 'Bash', details: ['rm -rf /tmp/test'], requiresAlways: true, + createdAt: Date.now(), }) expect(screen.getByText('Permission Request')).toBeTruthy() @@ -150,6 +157,7 @@ describe('TerminalScreen', () => { toolName: 'Write', details: ['/tmp/file.ts'], requiresAlways: false, + createdAt: Date.now(), }) expect(screen.getByText('Allow')).toBeTruthy() @@ -165,6 +173,7 @@ describe('TerminalScreen', () => { toolName: 'Bash', details: [], requiresAlways: false, + createdAt: Date.now(), }) fireEvent.press(screen.getByText('Allow')) @@ -183,6 +192,7 @@ describe('TerminalScreen', () => { toolName: 'Bash', details: [], requiresAlways: false, + createdAt: Date.now(), }) fireEvent.press(screen.getByText('Deny')) @@ -201,6 +211,7 @@ describe('TerminalScreen', () => { toolName: 'Bash', details: [], requiresAlways: true, + createdAt: Date.now(), }) fireEvent.press(screen.getByText('Always Allow')) From f587bd7056f0e86a521ca11c9350456d8dcdb489 Mon Sep 17 00:00:00 2001 From: Shun Okada Date: Fri, 3 Apr 2026 14:12:06 +0900 Subject: [PATCH 3/3] style(mobile): clarify permission overlay stacking --- packages/mobile/src/screens/TerminalScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mobile/src/screens/TerminalScreen.tsx b/packages/mobile/src/screens/TerminalScreen.tsx index 1834d6f..e60a18d 100644 --- a/packages/mobile/src/screens/TerminalScreen.tsx +++ b/packages/mobile/src/screens/TerminalScreen.tsx @@ -227,7 +227,7 @@ const styles = StyleSheet.create({ position: 'absolute', left: 0, right: 0, - bottom: 0, + // Keep the permission sheet above the WebView/toolbar stack on mobile. zIndex: 10, elevation: 10, },