import { router } from "expo-router"; import type { WorkspaceProjectDescriptorPayload } from "@getpaseo/protocol/messages"; import { ArrowLeft, Folder, FolderOpen, FolderPlus, Github, HardDrive, Plus, Search, Server, } from "lucide-react-native"; import { createElement, useCallback, useEffect, useMemo, useRef, useState, type ComponentType, } from "react"; import { Modal, Pressable, ScrollView, Text, TextInput, View, type PressableStateCallbackType, } from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { applyAvailableAddProjectHosts, backAddProjectPage, chooseAddProjectHost, currentAddProjectPage, moveAddProjectSelection, openAddProjectFlow, openDirectorySearchPage, openGithubLocationPage, openGithubSearchPage, openNewDirectoryNamePage, openNewDirectoryParentPage, setAddProjectActiveIndex, setAddProjectPageInput, setNewDirectoryName, updateCurrentAddProjectPage, type AddProjectFlowState, type AddProjectHost, type AddProjectPage, type GithubRepositoryChoice, } from "@/add-project-flow/model"; import { buildAddProjectMethods, addProjectMethodEmptyText, buildCloneLocationOptions, buildManualGithubRepositoryChoices, buildSuggestedParentDirectories, filterAddProjectHosts, joinDirectoryPath, pathBaseName, type AddProjectMethodId, } from "@/add-project-flow/options"; import { buildProjectPickerOptions, type ProjectPickerOption, } from "@/components/project-picker-options"; import { Shortcut } from "@/components/ui/shortcut"; import { useKeyboardShortcutsAvailable } from "@/keyboard/availability"; import { getIsElectronRuntime } from "@/constants/layout"; import { isNative, isWeb } from "@/constants/platform"; import { pickDirectory } from "@/desktop/pick-directory"; import { useFetchQuery } from "@/data/query"; import { getOpenProjectFailureReason, registerProjectDescriptor } from "@/hooks/open-project"; import { useIsLocalDaemon, useLocalDaemonServerId } from "@/hooks/use-is-local-daemon"; import { useCloneGithubProject, useOpenProject } from "@/hooks/use-open-project"; import { OverlayLayerProvider, useGlobalWebOverlayLayer, useWebOverlayRegistration, } from "@/lib/overlay-root"; import { useHosts, useHostRuntimeClient, useHostRuntimeConnectionStatuses, } from "@/runtime/host-runtime"; import { useHostFeatureMap } from "@/runtime/host-features"; import { useSessionStore } from "@/stores/session-store"; import { useRecommendedProjectPaths } from "@/stores/session-store-hooks"; import type { AddProjectFlowRequest } from "@/stores/add-project-flow-store"; import type { Theme } from "@/styles/theme"; import { shortenPath } from "@/utils/shorten-path"; import { buildNewWorkspaceRoute, buildSettingsAddHostRoute } from "@/utils/host-routes"; interface AddProjectFlowProps { request: AddProjectFlowRequest; onClose: () => void; } interface FlowRowOption { id: string; title: string; subtitle: string | null; icon: ComponentType<{ size?: number; color?: string }>; disabled?: boolean; testID: string; select: () => void; } type GithubLocationPage = Extract; interface FlowIconProps { icon: ComponentType<{ size?: number; color?: string }>; size?: number; color?: string; } function FlowIcon({ icon: Icon, size, color }: FlowIconProps) { return ; } const MutedFlowIcon = withUnistyles(FlowIcon, (theme) => ({ color: theme.colors.foregroundMuted, })); const ThemedArrowLeft = withUnistyles(ArrowLeft); const ThemedTextInput = withUnistyles(TextInput, (theme) => ({ placeholderTextColor: theme.colors.foregroundMuted, })); const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); const lastCloneParentByHost = new Map(); const EMPTY_PATHS: string[] = []; const NAVIGATION_HINT_KEYS = ["Up", "Down"]; const SELECT_HINT_KEYS = ["Enter"]; const ESCAPE_HINT_KEYS = ["Esc"]; function FlowBackButton({ onPress }: { onPress: () => void }) { return ( {({ hovered, pressed }) => ( )} ); } function methodIcon(method: AddProjectMethodId): FlowRowOption["icon"] { if (method === "github") return Github; if (method === "browse") return FolderOpen; if (method === "new-directory") return FolderPlus; return Search; } function directoryOptionSubtitle(option: ProjectPickerOption, shortPath: string): string | null { if (option.kind === "path") return "Open this path"; if (shortPath === option.path) return null; return option.path; } function progressText(page: AddProjectPage): string { if (page.kind === "github-location") return "Cloning project..."; if (page.kind === "new-directory-name") return "Creating directory..."; return "Adding project..."; } function emptyText(page: AddProjectPage, host: AddProjectHost | null): string { if (page.kind === "host") return "No connected hosts"; if (page.kind === "github-search") return "Enter a GitHub URL or owner/repo"; if (page.kind === "method") return addProjectMethodEmptyText(host); return "No matching options"; } interface QueryErrorInput { searchesDirectories: boolean; directoryFailed: boolean; githubFailed: boolean; githubAvailable: boolean | null; githubError: string | null; } function queryErrorText(input: QueryErrorInput): string | null { if (input.searchesDirectories && input.directoryFailed) return "Unable to search directories"; if (input.githubFailed) return "Unable to search GitHub repositories"; if (input.githubError) return input.githubError; if (input.githubAvailable === false) return input.githubError ?? "GitHub search is unavailable"; return null; } function pageHostId(page: AddProjectPage): string | null { return page.kind === "host" ? null : page.hostId; } function pageTitle(page: AddProjectPage): string { switch (page.kind) { case "host": return "Choose host"; case "method": return "Add project"; case "directory-search": return "Search for directory"; case "github-search": return "Clone from GitHub"; case "github-location": return "Choose destination"; case "new-directory-parent": return "Choose parent directory"; case "new-directory-name": return "Name directory"; } } type AddProjectInputPage = Exclude; function pagePlaceholder(page: AddProjectInputPage): string { switch (page.kind) { case "host": return "Search hosts..."; case "directory-search": return "Search directories or enter a path..."; case "github-search": return "Search or enter a GitHub repository..."; case "github-location": case "new-directory-parent": return "Search parent directories or enter a path..."; case "new-directory-name": return "Directory name"; } } function pageInput(page: AddProjectInputPage): string { return page.kind === "new-directory-name" ? page.name : page.query; } function pathTestId(path: string): string { return `add-project-flow-path-${encodeURIComponent(path)}`; } function FlowRow({ option, active }: { option: FlowRowOption; active: boolean }) { const accessibilityState = useMemo( () => ({ disabled: option.disabled === true, selected: active }), [active, option.disabled], ); const rowStyle = useCallback( ({ hovered = false, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.row, (active || hovered || pressed) && styles.rowActive, option.disabled && styles.disabled, ], [active, option.disabled], ); return ( {option.title} {option.subtitle ? ( {option.subtitle} ) : null} ); } function FlowHint({ keys, action }: { keys: string[]; action: string }) { const shortcutsAvailable = useKeyboardShortcutsAvailable(); if (!shortcutsAvailable) return null; return ( {action} ); } function setPageStatus( state: AddProjectFlowState, kind: AddProjectPage["kind"], input: { isSubmitting?: boolean; error?: string | null }, ): AddProjectFlowState { return updateCurrentAddProjectPage(state, (page) => page.kind === kind ? { ...page, ...input } : page, ); } // The product flow is intentionally one cohesive page-stack state machine. // eslint-disable-next-line complexity export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) { const hosts = useHosts(); const hostIds = useMemo(() => hosts.map((host) => host.serverId), [hosts]); const connectionStatuses = useHostRuntimeConnectionStatuses(hostIds); const projectAddByHost = useHostFeatureMap(hostIds, "projectAdd"); // COMPAT(stableProjectIdentity): added in v0.1.109, remove gate after 2027-01-15. const stableProjectIdentityByHost = useHostFeatureMap(hostIds, "stableProjectIdentity"); // COMPAT(projectGithubClone): added in v0.1.108, remove gate after 2027-01-15. const githubCloneByHost = useHostFeatureMap(hostIds, "projectGithubClone"); // COMPAT(workspaceGithubRepositorySearch): added in v0.1.108, remove gate after 2027-01-15. const githubSearchByHost = useHostFeatureMap(hostIds, "workspaceGithubRepositorySearch"); // COMPAT(projectCreateDirectory): added in v0.1.108, remove gate after 2027-01-15. const createDirectoryByHost = useHostFeatureMap(hostIds, "projectCreateDirectory"); const localServerId = useLocalDaemonServerId(); const availableHosts = useMemo( () => hosts.flatMap((host) => { if (connectionStatuses.get(host.serverId) !== "online") return []; const canAddProject = projectAddByHost.get(host.serverId) === true && stableProjectIdentityByHost.get(host.serverId) === true; return [ { serverId: host.serverId, label: host.label, canAddProject, canBrowse: canAddProject && getIsElectronRuntime() && localServerId === host.serverId, canCloneGithubRepositories: githubCloneByHost.get(host.serverId) === true, canSearchGithubRepositories: githubSearchByHost.get(host.serverId) === true, canCreateDirectory: createDirectoryByHost.get(host.serverId) === true, }, ]; }), [ connectionStatuses, createDirectoryByHost, githubCloneByHost, githubSearchByHost, hosts, localServerId, projectAddByHost, stableProjectIdentityByHost, ], ); const [state, setState] = useState(() => openAddProjectFlow({ hosts: availableHosts, ...(request.preferredHostId ? { preferredHostId: request.preferredHostId } : {}), }), ); const page = currentAddProjectPage(state); const hostId = pageHostId(page); const host = hostId ? state.hosts.find((candidate) => candidate.serverId === hostId) : null; const client = useHostRuntimeClient(hostId ?? ""); const isLocalDaemon = useIsLocalDaemon(hostId ?? ""); const recommendedPaths = useRecommendedProjectPaths(hostId); const openProject = useOpenProject(hostId); const cloneGithubProject = useCloneGithubProject(hostId); const upsertProject = useSessionStore((store) => store.upsertProject); const setHasHydratedWorkspaces = useSessionStore((store) => store.setHasHydratedWorkspaces); const inputRef = useRef(null); const submissionInFlightRef = useRef(false); const browseInFlightRef = useRef(false); const query = page.kind === "new-directory-name" || page.kind === "method" ? "" : page.query; const [debouncedQuery, setDebouncedQuery] = useState(query); useEffect(() => { setState((current) => applyAvailableAddProjectHosts(current, availableHosts, request.preferredHostId), ); }, [availableHosts, request.preferredHostId]); useEffect(() => { const timer = setTimeout(() => setDebouncedQuery(query), 250); return () => clearTimeout(timer); }, [query]); useEffect(() => { const timer = setTimeout(() => inputRef.current?.focus(), 0); return () => clearTimeout(timer); }, [page.kind]); const searchesDirectories = page.kind === "directory-search" || page.kind === "github-location" || page.kind === "new-directory-parent"; const directoryQuery = useFetchQuery({ queryKey: ["add-project-flow-directories", hostId, debouncedQuery], queryFn: async () => { if (!client) return { query: debouncedQuery, paths: [] as string[] }; const payload = await client.getDirectorySuggestions({ query: debouncedQuery, includeDirectories: true, includeFiles: false, limit: 30, }); return { query: debouncedQuery, paths: payload.entries?.flatMap((entry) => (entry.kind === "directory" ? [entry.path] : [])) ?? [], }; }, enabled: Boolean(client && searchesDirectories), dataShape: "value", retry: false, staleTimeMs: 15_000, }); const githubQuery = useFetchQuery({ queryKey: ["add-project-flow-github", hostId, debouncedQuery], queryFn: async () => { if (!client) throw new Error("Host is unavailable"); const payload = await client.searchGithubRepositories({ query: debouncedQuery, limit: 30 }); return { query: debouncedQuery, payload }; }, enabled: Boolean(client && page.kind === "github-search" && host?.canSearchGithubRepositories), dataShape: "value", retry: false, staleTimeMs: 15_000, }); const handleBack = useCallback(() => { setState((current) => { const previous = backAddProjectPage(current); if (previous) return previous; onClose(); return current; }); }, [onClose]); const openNewWorkspaceForProject = useCallback( (serverId: string, project: WorkspaceProjectDescriptorPayload) => { onClose(); router.push( buildNewWorkspaceRoute({ serverId, projectId: project.projectId, sourceDirectory: project.projectRootPath, displayName: project.projectDisplayName, }), ); }, [onClose], ); const openAddedProject = useCallback( async (path: string, sourceKind: "directory-search" | "method") => { if (!hostId || submissionInFlightRef.current) return; submissionInFlightRef.current = true; setState((current) => setPageStatus(current, sourceKind, { isSubmitting: true, error: null }), ); try { const result = await openProject(path); if (result.ok) { openNewWorkspaceForProject(hostId, result.project); return; } const reason = getOpenProjectFailureReason(result); const message = reason === "directory_not_found" ? "Directory not found" : "Unable to add project"; setState((current) => setPageStatus(current, sourceKind, { isSubmitting: false, error: message }), ); } catch { setState((current) => setPageStatus(current, sourceKind, { isSubmitting: false, error: "Unable to add project", }), ); } finally { submissionInFlightRef.current = false; } }, [hostId, openNewWorkspaceForProject, openProject], ); const browse = useCallback(async () => { if (!hostId || !isLocalDaemon || browseInFlightRef.current) return; browseInFlightRef.current = true; try { const path = await pickDirectory(); if (path) await openAddedProject(path, "method"); } catch { setState((current) => setPageStatus(current, "method", { error: "Unable to browse for a directory" }), ); } finally { browseInFlightRef.current = false; } }, [hostId, isLocalDaemon, openAddedProject]); const selectMethod = useCallback( (method: AddProjectMethodId) => { if (!hostId) return; if (method === "directory-search") { setState((current) => openDirectorySearchPage(current, hostId)); } else if (method === "browse") { void browse(); } else if (method === "github") { setState((current) => openGithubSearchPage(current, hostId)); } else { setState((current) => openNewDirectoryParentPage(current, hostId)); } }, [browse, hostId], ); const directoryPaths = useMemo( () => (directoryQuery.data?.query === query ? directoryQuery.data.paths : EMPTY_PATHS), [directoryQuery.data, query], ); const pathOptions = useMemo( () => buildProjectPickerOptions({ recommendedPaths, serverPaths: directoryPaths, query, }), [directoryPaths, query, recommendedPaths], ); const cloneRepository = useCallback( async (locationPage: GithubLocationPage, parentPath: string) => { if (submissionInFlightRef.current) return; submissionInFlightRef.current = true; setState((current) => setPageStatus(current, "github-location", { isSubmitting: true, error: null }), ); try { const result = await cloneGithubProject( locationPage.repository.cloneUrl, parentPath, locationPage.repository.cloneProtocol, ); if (result.ok) { lastCloneParentByHost.set(locationPage.hostId, parentPath); openNewWorkspaceForProject(locationPage.hostId, result.project); return; } setState((current) => setPageStatus(current, "github-location", { isSubmitting: false, error: result.error ?? "Unable to clone repository", }), ); } catch (error) { setState((current) => setPageStatus(current, "github-location", { isSubmitting: false, error: error instanceof Error ? error.message : "Unable to clone repository", }), ); } finally { submissionInFlightRef.current = false; } }, [cloneGithubProject, openNewWorkspaceForProject], ); const rows = useMemo(() => { if (page.kind === "host") { const choices = filterAddProjectHosts(state.hosts, page.query).map( (choice) => ({ id: choice.serverId, title: choice.label, subtitle: choice.serverId, icon: Server, testID: `add-project-flow-host-${choice.serverId}`, select: () => setState((current) => chooseAddProjectHost(current, choice.serverId)), }), ); if (state.hosts.length === 0) { choices.push({ id: "add-host", title: "Add host", subtitle: "No connected hosts", icon: Plus, testID: "add-project-flow-add-host", select: () => { onClose(); router.push(buildSettingsAddHostRoute(Date.now())); }, }); } return choices; } if (page.kind === "method") { if (!host) return []; return buildAddProjectMethods(host).map((method) => ({ id: method.id, title: method.label, subtitle: method.description, icon: methodIcon(method.id), disabled: method.disabled, testID: `add-project-flow-method-${method.id}`, select: () => selectMethod(method.id), })); } if (page.kind === "directory-search") { return pathOptions.map((option) => { const shortPath = shortenPath(option.path); return { id: option.path, title: shortPath, subtitle: directoryOptionSubtitle(option, shortPath), icon: Folder, testID: pathTestId(option.path), select: () => void openAddedProject(option.path, "directory-search"), }; }); } if (page.kind === "github-search") { const search = githubQuery.data?.query === page.query ? githubQuery.data.payload : null; const repositories = search?.repositories ?? []; const normalizedQuery = page.query.trim().toLowerCase(); const hasExactSearchResult = repositories.some( (repository) => repository.nameWithOwner.toLowerCase() === normalizedQuery || repository.cloneUrl.toLowerCase() === normalizedQuery, ); const manualRepositories = hasExactSearchResult ? [] : buildManualGithubRepositoryChoices(page.query); const repositoryChoices: GithubRepositoryChoice[] = [...manualRepositories, ...repositories]; return repositoryChoices.map((repository) => ({ id: repository.id, title: repository.cloneProtocol ? `${repository.nameWithOwner} via ${repository.cloneProtocol.toUpperCase()}` : repository.nameWithOwner, subtitle: repository.description, icon: Github, testID: `add-project-flow-repository-${repository.id}`, select: () => setState((current) => openGithubLocationPage(current, page.hostId, repository)), })); } if (page.kind === "github-location") { const repositoryName = pathBaseName(page.repository.nameWithOwner); const lastParent = lastCloneParentByHost.get(page.hostId); const parents = buildSuggestedParentDirectories(recommendedPaths); const orderedParents = lastParent ? [lastParent, ...parents.filter((parent) => parent !== lastParent)] : parents; const filteredParents = buildProjectPickerOptions({ recommendedPaths: orderedParents, serverPaths: directoryPaths, query: page.query, }).map((option) => option.path); return buildCloneLocationOptions({ parents: filteredParents, repositoryName, existingPaths: [...recommendedPaths, ...directoryPaths], }).map((option) => ({ id: option.id, title: shortenPath(option.displayPath), subtitle: option.secondaryText, icon: HardDrive, disabled: option.disabled, testID: pathTestId(option.displayPath), select: () => void cloneRepository(page, option.path), })); } if (page.kind === "new-directory-parent") { return pathOptions.map((option) => ({ id: option.path, title: shortenPath(option.path), subtitle: option.kind === "path" ? "Use this parent" : option.path, icon: Folder, testID: pathTestId(option.path), select: () => setState((current) => openNewDirectoryNamePage(current, page.hostId, option.path)), })); } return []; }, [ cloneRepository, directoryPaths, githubQuery.data, host, onClose, openAddedProject, page, pathOptions, recommendedPaths, selectMethod, state.hosts, ]); const activeIndex = rows.length === 0 ? 0 : Math.min(page.activeIndex, rows.length - 1); const createDirectory = useCallback(async () => { if (page.kind !== "new-directory-name" || !client) return; const name = page.name.trim(); if (!name || name === "." || name === ".." || /[\\/]/.test(name)) { setState((current) => setPageStatus(current, "new-directory-name", { error: "Enter a directory name" }), ); return; } if (submissionInFlightRef.current) return; submissionInFlightRef.current = true; setState((current) => setPageStatus(current, "new-directory-name", { isSubmitting: true, error: null }), ); try { const payload = await client.createProjectDirectory({ parentPath: page.parentPath, name, }); if (payload.error || !payload.project) { setState((current) => setPageStatus(current, "new-directory-name", { isSubmitting: false, error: payload.error ?? "Unable to create directory", }), ); return; } registerProjectDescriptor({ serverId: page.hostId, project: payload.project, upsertProject, setHasHydratedWorkspaces, }); openNewWorkspaceForProject(page.hostId, payload.project); } catch { setState((current) => setPageStatus(current, "new-directory-name", { isSubmitting: false, error: "Unable to create directory", }), ); } finally { submissionInFlightRef.current = false; } }, [client, openNewWorkspaceForProject, page, setHasHydratedWorkspaces, upsertProject]); const submitActive = useCallback(() => { if (page.kind === "new-directory-name") { void createDirectory(); return; } const option = rows[activeIndex]; if (option && !option.disabled) option.select(); }, [activeIndex, createDirectory, page.kind, rows]); const handleKey = useCallback( (key: string): boolean => { if (key === "Escape") { handleBack(); return true; } if (key === "Enter") { submitActive(); return true; } if (key !== "ArrowDown" && key !== "ArrowUp") return false; const next = moveAddProjectSelection( activeIndex, rows.map((row) => row.disabled !== true), key === "ArrowDown" ? "next" : "previous", ); setState((current) => setAddProjectActiveIndex(current, next)); return true; }, [activeIndex, handleBack, rows, submitActive], ); const modalLayer = useGlobalWebOverlayLayer("modal", isWeb); const handleWebOverlayKeyDown = useCallback( (event: KeyboardEvent) => { if (!handleKey(event.key)) return false; event.preventDefault(); return true; }, [handleKey], ); const setWebOverlayScope = useWebOverlayRegistration({ active: isWeb, layer: modalLayer, onKeyDown: handleWebOverlayKeyDown, }); const handleNativeKeyPress = useCallback( ({ nativeEvent: { key } }: { nativeEvent: { key: string } }) => { if (key === "ArrowDown" || key === "ArrowUp" || key === "Escape") { handleKey(key); } }, [handleKey], ); const handleInputChange = useCallback((value: string) => { setState((current) => currentAddProjectPage(current).kind === "new-directory-name" ? setNewDirectoryName(current, value) : setAddProjectPageInput(current, value), ); }, []); const isSubmitting = "isSubmitting" in page && page.isSubmitting; const currentGithubSearch = page.kind === "github-search" && githubQuery.data?.query === page.query ? githubQuery.data.payload : null; const loading = (searchesDirectories && (query !== debouncedQuery || directoryQuery.isFetching)) || (page.kind === "github-search" && host?.canSearchGithubRepositories === true && (query !== debouncedQuery || githubQuery.isFetching)); const queryError = queryErrorText({ searchesDirectories, directoryFailed: directoryQuery.isError, githubFailed: page.kind === "github-search" && githubQuery.isError, githubAvailable: currentGithubSearch?.available ?? null, githubError: currentGithubSearch?.error ?? null, }); const preview = page.kind === "new-directory-name" && page.name.trim() ? joinDirectoryPath(page.parentPath, page.name.trim()) : null; const modal = ( {state.pages.length > 1 ? : null} {pageTitle(page)} {host ? ( {host.label} ) : null} {page.kind === "method" && isNative ? ( // Native hardware-keyboard events need a focused responder even without a visible field. ) : null} {page.kind !== "method" ? ( ) : null} {preview ? ( {shortenPath(preview)} ) : null} {isSubmitting ? ( {progressText(page)} ) : null} {!isSubmitting && page.error ? ( {page.error} ) : null} {!isSubmitting && queryError ? ( {queryError} ) : null} {!isSubmitting && loading ? ( Loading... ) : null} {!isSubmitting && (!loading || page.kind === "github-search") && (!queryError || page.kind === "github-search") ? rows.map((option, index) => ( )) : null} {!isSubmitting && !loading && !queryError && rows.length === 0 && page.kind !== "new-directory-name" ? ( {emptyText(page, host ?? null)} ) : null} 1 ? "Back" : "Close"} /> ); return createElement(OverlayLayerProvider, { layer: isWeb ? modalLayer : 0 }, modal); } const styles = StyleSheet.create((theme) => ({ overlay: { flex: 1, justifyContent: "flex-start", alignItems: "center", paddingTop: theme.spacing[12], }, backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0, 0, 0, 0.5)", }, panel: { width: 640, maxWidth: "92%", maxHeight: "80%", borderWidth: 1, borderColor: theme.colors.border, borderRadius: theme.borderRadius.lg, backgroundColor: theme.colors.surface0, overflow: "hidden", ...theme.shadow.lg, }, header: { flexShrink: 0, paddingHorizontal: theme.spacing[4], paddingTop: theme.spacing[2], paddingBottom: theme.spacing[3], borderBottomWidth: 1, borderBottomColor: theme.colors.border, gap: theme.spacing[2], }, titleRow: { minHeight: theme.iconSize.lg, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, backButton: { width: 18, height: theme.iconSize.lg, alignItems: "center", justifyContent: "center", flexShrink: 0, }, titleGroup: { flex: 1, minWidth: 0, flexDirection: "row", alignItems: "baseline", gap: theme.spacing[2], }, title: { minWidth: 0, flexShrink: 1, color: theme.colors.foreground, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, }, hostContext: { minWidth: 0, flexShrink: 1, color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, }, input: { color: theme.colors.foreground, fontSize: theme.fontSize.base, paddingVertical: theme.spacing[1], outlineStyle: "none", } as object, keyboardCapture: { position: "absolute", width: 1, height: 1, opacity: 0, }, results: { flexGrow: 0, flexShrink: 1, minHeight: 0 }, resultsContent: { paddingVertical: theme.spacing[2] }, row: { flexDirection: "row", alignItems: "center", gap: theme.spacing[3], paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[2], }, rowActive: { backgroundColor: theme.colors.surface1 }, disabled: { opacity: theme.opacity[50] }, iconSlot: { width: 18, alignItems: "center" }, rowText: { flex: 1, minWidth: 0 }, rowTitle: { color: theme.colors.foreground, fontSize: theme.fontSize.sm }, rowSubtitle: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, marginTop: 2 }, preview: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[2], }, stateText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.base, paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[4], }, errorText: { color: theme.colors.destructive, fontSize: theme.fontSize.xs, paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[3], }, footer: { flexShrink: 0, flexDirection: "row", gap: theme.spacing[4], alignItems: "center", flexWrap: "wrap", paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[2], borderTopWidth: 1, borderTopColor: theme.colors.border, }, footerHint: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1.5], }, footerKeyText: { color: theme.colors.foreground, fontSize: theme.fontSize.xs, }, footerAction: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, }, }));