import { useEffect, useMemo, useRef, useState } from 'react' import { ActivityIndicator, FlatList, Pressable, StyleSheet, Text, TextInput, View } from 'react-native' import type { RpcClient } from '../transport/rpc-client' import type { SmartWorkspaceSourceRow as SourceRow } from '../../../src/shared/new-workspace/smart-workspace-source-results' import { MR_STATE_FILTER_OPTIONS, resolveAvailableSmartModes, resolveDefaultSmartMode, SMART_MODE_OPTIONS, type SmartModeAvailabilityInput, type SmartModeOption } from '../tasks/mobile-smart-source-modes' import type { MrStateFilter, SmartNameMode } from '../tasks/mobile-composer-source-types' import { lookupGitHubItemByOwnerRepo, type PasteRepoCandidate } from '../tasks/smart-source-paste-intent' import { useSmartWorkspaceSource } from '../tasks/use-smart-workspace-source' import type { MobileComposerSource } from '../tasks/use-mobile-composer-source' import { colors, radii, spacing, typography } from '../theme/mobile-theme' import { BottomDrawer, BOTTOM_DRAWER_HIDE_DURATION_MS } from './BottomDrawer' import { SmartSourceModeIcon } from './SmartSourceModeIcon' import { SmartWorkspaceSourceRow } from './SmartWorkspaceSourceRow' type Props = { visible: boolean client: RpcClient | null composer: MobileComposerSource availability: SmartModeAvailabilityInput repoId: string | null repos: readonly PasteRepoCandidate[] linearWorkspaceId?: string | null sshReady: boolean onRepoChange: (repoId: string) => void onClose: () => void } export function SmartWorkspaceSourceDrawer({ visible, client, composer, availability, repoId, repos, linearWorkspaceId, sshReady, onRepoChange, onClose }: Props) { const availableModes = useMemo(() => resolveAvailableSmartModes(availability), [availability]) const [mode, setMode] = useState(() => resolveDefaultSmartMode(availability)) const [mrStateFilter, setMrStateFilter] = useState('opened') // Why: read latest availability inside the open effect without making it a // reactive dep (the object is recreated each render), so re-seeding happens // only on open, not on every availability recompute. const availabilityRef = useRef(availability) availabilityRef.current = availability // Reset to the default mode each time the drawer opens. useEffect(() => { if (visible) { setMode(resolveDefaultSmartMode(availabilityRef.current)) } }, [visible]) // Snap the chosen mode back into the available set if availability changes. const effectiveMode = availableModes.includes(mode) ? mode : (availableModes[0] ?? 'text') // Linear searches without a repo; every other provider/branch search needs a // connected repo-backed target. const searchEnabled = visible && (effectiveMode === 'linear' || sshReady) const { rows, loading, error, needsGitHubRemote, emptyHint, crossRepoPrompt, dismissCrossRepoPrompt } = useSmartWorkspaceSource({ client, enabled: searchEnabled, mode: effectiveMode, query: composer.name, repoId, githubAvailable: availability.githubAvailable, gitlabAvailable: availability.gitlabAvailable, linearAvailable: availability.linearAvailable, mrStateFilter, linearWorkspaceId, repos }) function closeSoon(): void { setTimeout(onClose, BOTTOM_DRAWER_HIDE_DURATION_MS) } function handleSelectRow(row: SourceRow): void { switch (row.kind) { case 'use-name': composer.setName(row.name) break case 'create-branch': composer.handleSmartCreateBranch(row.name) break case 'github': composer.handleSmartGitHubItemSelect(row.item) break case 'gitlab': composer.handleSmartGitLabItemSelect(row.item) break case 'branch': composer.handleSmartBranchSelect(row.refName, row.localBranchName) break case 'linear': composer.handleSmartLinearIssueSelect(row.issue) break } onClose() } async function handleAcceptCrossRepo(): Promise { if (!client || !crossRepoPrompt) { return } const { link, matchingRepo } = crossRepoPrompt try { const item = await lookupGitHubItemByOwnerRepo( client, matchingRepo.id, link.slug, link.number, link.type ) if (item) { onRepoChange(matchingRepo.id) composer.handleSmartGitHubItemSelect(item) onClose() } } catch { dismissCrossRepoPrompt() } } const showEmpty = !loading && !error && !needsGitHubRemote && effectiveMode !== 'text' && rows.length === 0 return ( Name or 'Create From' Done {SMART_MODE_OPTIONS.filter((option: SmartModeOption) => availableModes.includes(option.id) ).map((option) => { const selected = option.id === effectiveMode const tint = selected ? colors.textPrimary : colors.textSecondary return ( setMode(option.id)} > {option.label} ) })} {effectiveMode === 'gitlab' ? ( {MR_STATE_FILTER_OPTIONS.map((option) => { const selected = option.id === mrStateFilter return ( setMrStateFilter(option.id)} > {option.label} ) })} ) : null} {crossRepoPrompt ? ( This item lives in {crossRepoPrompt.link.slug.owner}/{crossRepoPrompt.link.slug.repo}. Cancel void handleAcceptCrossRepo()}> Switch to {crossRepoPrompt.matchingRepo.displayName} ) : null} {!sshReady && effectiveMode !== 'text' && effectiveMode !== 'linear' ? ( Connect the repository to search sources. ) : needsGitHubRemote ? ( This SSH repo needs a GitHub remote to list issues and PRs. ) : error ? ( {error} ) : null} row.value} style={styles.list} keyboardShouldPersistTaps="handled" nestedScrollEnabled ListFooterComponent={ loading ? ( ) : showEmpty ? ( {emptyHint || 'No results found.'} ) : null } renderItem={({ item }) => ( handleSelectRow(item)} /> )} /> ) } const styles = StyleSheet.create({ header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.xs, paddingBottom: spacing.sm }, title: { fontSize: 15, fontWeight: '600', color: colors.textPrimary }, done: { fontSize: typography.bodySize, fontWeight: '600', color: colors.accentBlue }, search: { backgroundColor: colors.bgRaised, color: colors.textPrimary, borderRadius: radii.input, paddingHorizontal: spacing.md, paddingVertical: spacing.sm, fontSize: typography.bodySize, borderWidth: 1, borderColor: colors.borderSubtle, marginBottom: spacing.sm }, tabRow: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.xs, marginBottom: spacing.sm }, tab: { flexDirection: 'row', alignItems: 'center', gap: spacing.xs, paddingHorizontal: spacing.sm + 2, paddingVertical: spacing.xs + 2, borderRadius: radii.button, borderWidth: 1, borderColor: colors.borderSubtle }, tabSelected: { backgroundColor: colors.bgPanel, borderColor: colors.textSecondary }, tabText: { fontSize: 13, color: colors.textSecondary }, tabTextSelected: { color: colors.textPrimary, fontWeight: '600' }, chipRow: { flexDirection: 'row', gap: spacing.xs, marginBottom: spacing.sm }, chip: { paddingHorizontal: spacing.md, paddingVertical: spacing.xs, borderRadius: radii.button, borderWidth: 1, borderColor: colors.borderSubtle }, chipSelected: { backgroundColor: colors.bgPanel, borderColor: colors.textSecondary }, chipText: { fontSize: 12, color: colors.textSecondary }, chipTextSelected: { color: colors.textPrimary, fontWeight: '600' }, crossRepo: { backgroundColor: colors.bgRaised, borderRadius: radii.input, borderWidth: 1, borderColor: colors.borderSubtle, padding: spacing.md, marginBottom: spacing.sm, gap: spacing.sm }, crossRepoText: { fontSize: 13, color: colors.textSecondary }, crossRepoActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: spacing.sm }, crossRepoDismiss: { paddingHorizontal: spacing.md, paddingVertical: spacing.xs + 2, borderRadius: radii.button, borderWidth: 1, borderColor: colors.borderSubtle }, crossRepoDismissText: { fontSize: 13, color: colors.textSecondary }, crossRepoSwitch: { paddingHorizontal: spacing.md, paddingVertical: spacing.xs + 2, borderRadius: radii.button, backgroundColor: colors.bgPanel, borderWidth: 1, borderColor: colors.textSecondary }, crossRepoSwitchText: { fontSize: 13, fontWeight: '600', color: colors.textPrimary }, notice: { fontSize: 12, color: colors.textMuted, paddingHorizontal: spacing.xs, paddingBottom: spacing.sm }, errorNotice: { fontSize: 12, color: colors.statusRed, paddingHorizontal: spacing.xs, paddingBottom: spacing.sm }, list: { backgroundColor: colors.bgPanel, borderRadius: radii.card, overflow: 'hidden', maxHeight: 420, flexGrow: 0 }, loading: { paddingVertical: spacing.lg, alignItems: 'center' }, empty: { paddingVertical: spacing.lg, textAlign: 'center', color: colors.textMuted, fontSize: 13 } })