Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions packages/mobile/src/components/PermissionSheet.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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 (
<Animated.View
style={[
styles.sheet,
// キーボード表示中は bottom をキーボード高さ分オフセットして隠れを防ぐ
{ bottom: keyboardHeight, paddingBottom: sheetPaddingBottom, transform: [{ translateY: slideAnim }] },
{ paddingBottom: sheetPaddingBottom, transform: [{ translateY: slideAnim }] },
]}
>
{/* Progress bar */}
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 15 additions & 1 deletion packages/mobile/src/screens/TerminalScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,13 @@ export function TerminalScreen() {
<KeyboardToolbar webViewRef={webViewRef} />

{/* 承認ボトムシート */}
<PermissionSheet request={pendingPermission} onDecide={handlePermissionDecide} />
<View
testID="permission-overlay"
pointerEvents="box-none"
style={[styles.permissionOverlay, { bottom: keyboardHeight }]}
>
<PermissionSheet request={pendingPermission} onDecide={handlePermissionDecide} />
</View>
</SafeAreaView>
)
}
Expand Down Expand Up @@ -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,
},
})
34 changes: 34 additions & 0 deletions packages/mobile/src/screens/__tests__/TerminalScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 を発火するヘルパー
Expand All @@ -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',
Expand Down Expand Up @@ -101,6 +107,29 @@ describe('TerminalScreen', () => {
})

describe('PermissionSheet', () => {
it('キーボード表示中でも PermissionSheet が表示される', () => {
;(useKeyboardHeight as jest.Mock).mockReturnValue(180)

render(<TerminalScreen />)
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(<TerminalScreen />)
sendFromWebView({
Expand All @@ -109,6 +138,7 @@ describe('TerminalScreen', () => {
toolName: 'Bash',
details: ['rm -rf /tmp/test'],
requiresAlways: true,
createdAt: Date.now(),
})

expect(screen.getByText('Permission Request')).toBeTruthy()
Expand All @@ -127,6 +157,7 @@ describe('TerminalScreen', () => {
toolName: 'Write',
details: ['/tmp/file.ts'],
requiresAlways: false,
createdAt: Date.now(),
})

expect(screen.getByText('Allow')).toBeTruthy()
Expand All @@ -142,6 +173,7 @@ describe('TerminalScreen', () => {
toolName: 'Bash',
details: [],
requiresAlways: false,
createdAt: Date.now(),
})

fireEvent.press(screen.getByText('Allow'))
Expand All @@ -160,6 +192,7 @@ describe('TerminalScreen', () => {
toolName: 'Bash',
details: [],
requiresAlways: false,
createdAt: Date.now(),
})

fireEvent.press(screen.getByText('Deny'))
Expand All @@ -178,6 +211,7 @@ describe('TerminalScreen', () => {
toolName: 'Bash',
details: [],
requiresAlways: true,
createdAt: Date.now(),
})

fireEvent.press(screen.getByText('Always Allow'))
Expand Down
Loading