diff --git a/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx b/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx index 3caa8c6961..432c31f82d 100644 --- a/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx +++ b/apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx @@ -2,9 +2,8 @@ import { useFocusEffect, useRouter } from 'expo-router'; import * as Haptics from 'expo-haptics'; import { Check, Info, Lock, Search, SearchX, Unlock } from '@/components/ui/icons'; import { useCallback, useMemo, useRef, useState } from 'react'; -import { FlatList, Pressable, TextInput, View } from 'react-native'; +import { Pressable, TextInput, View } from 'react-native'; import { useTranslation } from 'react-i18next'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { EmptyState } from '@/components/empty-state'; import { PickerSheet } from '@/components/picker-sheet'; @@ -21,7 +20,6 @@ type PickerListItem = export default function RepoPickerScreen() { const router = useRouter(); const colors = useThemeColors(); - const { bottom } = useSafeAreaInsets(); const { t } = useTranslation(); const [search, setSearch] = useState(''); const [bridge, setBridge] = useState(() => repoPickerSlot.get(UNFENCED_ROUTE_KEY)); @@ -102,7 +100,6 @@ export default function RepoPickerScreen() { @@ -136,17 +133,18 @@ export default function RepoPickerScreen() { } /> ) : ( - item.key} - keyboardShouldPersistTaps="handled" - keyboardDismissMode="on-drag" - contentContainerStyle={{ paddingBottom: bottom }} - renderItem={({ item }) => { + // Mapped rows inside the shell ScrollView instead of a FlatList: the + // FlatList stretches into the space the formSheet offers and its rows + // painted over the pinned search header while scrolling. The shell + // scroll view starts below the header, so a row can never overlap it. + + {listItems.map(item => { if (item.kind === 'header') { return ( - + {t(item.titleKey)} ); @@ -156,6 +154,7 @@ export default function RepoPickerScreen() { const rowLabel = `${platformName} ${repo.fullName}`; return ( { handleSelect(`${repo.platform}:${repo.fullName}`); @@ -182,9 +181,33 @@ export default function RepoPickerScreen() { ) : null} ); - }} - /> + })} + {renderBitbucketNote()} + )} ); + + /** + * Personal Bitbucket never lists repositories (organization-only), so the + * grouped list would end at GitLab with nothing explaining the gap. The + * note renders once, after the provider sections, whenever the picker has + * rows but no Bitbucket section; a connected org's rows suppress it. + */ + function renderBitbucketNote() { + if (search.trim() || !bridge) { + return null; + } + if (bridge.sections.some(section => section.key === 'bitbucket')) { + return null; + } + return ( + + + {t('agentChat.repoPicker.platformBitbucket')} + + {t('agentChat.newSession.bitbucketOrganizationsOnly')} + + ); + } } diff --git a/apps/mobile/src/components/agents/new-session-configure-form.tsx b/apps/mobile/src/components/agents/new-session-configure-form.tsx index 6ebae1b378..33d9762546 100644 --- a/apps/mobile/src/components/agents/new-session-configure-form.tsx +++ b/apps/mobile/src/components/agents/new-session-configure-form.tsx @@ -217,6 +217,7 @@ export function NewSessionConfigureForm({ contentContainerClassName="flex-grow px-4 pb-8 pt-4" keyboardShouldPersistTaps="handled" automaticallyAdjustKeyboardInsets + keyboardDismissMode="on-drag" > >, type: string) { return renderer.root.findAll(node => node.type === type); } +// The model picker hosts its rows in a FlatList and manages its own scrolling +// (PickerSheet scrollable=false); the repository picker renders mapped rows +// inside the shell ScrollView and so always keeps that ScrollView mounted. describe.each([ - { name: 'model', Component: ModelPickerContent }, - { name: 'repository', Component: RepoPickerScreen }, -])('$name picker centering', ({ Component }) => { + { name: 'model', Component: ModelPickerContent, rowHost: 'FlatList', hasShellScrollView: false }, + { + name: 'repository', + Component: RepoPickerScreen, + rowHost: 'Pressable', + hasShellScrollView: true, + }, +])('$name picker centering', ({ Component, rowHost, hasShellScrollView }) => { it('keeps the search input and native header mounted when replacing the list', async () => { const renderer = await mount(Component); const input = hosts(renderer, 'TextInput')[0]; @@ -102,15 +110,17 @@ describe.each([ const group = header.parent; expect(group?.props.collapsable).toBe(false); expect(group?.findAll(node => node === input)).toHaveLength(1); - expect(hosts(renderer, 'FlatList')).toHaveLength(1); + expect(hosts(renderer, rowHost).length).toBeGreaterThan(0); expect(hosts(renderer, 'CenteredState')).toHaveLength(0); const changeSearch = input.props.onChangeText as (text: string) => void; act(() => { changeSearch('no matching choice'); }); - expect(hosts(renderer, 'FlatList')).toHaveLength(0); - expect(hosts(renderer, 'ScrollView')).toHaveLength(0); + expect(hosts(renderer, rowHost)).toHaveLength(0); + if (!hasShellScrollView) { + expect(hosts(renderer, 'ScrollView')).toHaveLength(0); + } expect(hosts(renderer, 'CenteredState')).toHaveLength(1); expect(hosts(renderer, 'TextInput')[0]).toBe(input); expect(hosts(renderer, 'SheetHeader')[0]).toBe(header); @@ -119,7 +129,7 @@ describe.each([ act(() => { changeSearch(''); }); - expect(hosts(renderer, 'FlatList')).toHaveLength(1); + expect(hosts(renderer, rowHost).length).toBeGreaterThan(0); expect(hosts(renderer, 'CenteredState')).toHaveLength(0); expect(hosts(renderer, 'TextInput')[0]).toBe(input); expect(header.parent).toBe(group); @@ -136,7 +146,9 @@ describe.each([ const renderer = await mount(Component); expect(hosts(renderer, 'CenteredState')).toHaveLength(1); expect(hosts(renderer, 'FlatList')).toHaveLength(0); - expect(hosts(renderer, 'ScrollView')).toHaveLength(0); + if (!hasShellScrollView) { + expect(hosts(renderer, 'ScrollView')).toHaveLength(0); + } expect(hosts(renderer, 'TextInput')).toHaveLength(1); }); }); diff --git a/apps/mobile/src/components/agents/repository-branch-selector.tsx b/apps/mobile/src/components/agents/repository-branch-selector.tsx index 6bed318e26..f28cfa5843 100644 --- a/apps/mobile/src/components/agents/repository-branch-selector.tsx +++ b/apps/mobile/src/components/agents/repository-branch-selector.tsx @@ -1,5 +1,5 @@ import { useState, useSyncExternalStore } from 'react'; -import { FlatList, Modal, Pressable, View } from 'react-native'; +import { Modal, Pressable, ScrollView, View } from 'react-native'; import { useTranslation } from 'react-i18next'; import { Check, ChevronDown } from '@/components/ui/icons'; @@ -198,12 +198,13 @@ export function RepositoryBranchSelector({ {t('agentChat.newSession.branchPickerTitle')} - branch} - renderItem={({ item }) => renderBranchRow(item, close)} - showsVerticalScrollIndicator={false} - /> + {/* ScrollView, not FlatList: a FlatList stretches to the space + its container offers, so two branch rows rendered as a mostly + empty sheet. A ScrollView hugs its rows and only scrolls once + the card's max height is reached. */} + + {branches.branches.map(branch => renderBranchRow(branch, close))} +