import { useState, useRef, useEffect, useMemo, useCallback, } from "react"; import type { ReactElement, ReactNode } from "react"; import { View, Text, Pressable, ScrollView, FlatList, ActivityIndicator, InteractionManager, TextInput, Modal, useWindowDimensions, type LayoutChangeEvent, type ListRenderItem, Platform, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller"; import Animated, { useAnimatedStyle, useSharedValue, withTiming, Easing, runOnJS, } from "react-native-reanimated"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { Monitor, X } from "lucide-react-native"; import { theme as defaultTheme } from "@/styles/theme"; import { useRecentPaths } from "@/hooks/use-recent-paths"; import { useRouter } from "expo-router"; import { generateMessageId } from "@/types/stream"; import { MessageInput } from "./message-input"; import { useDaemonConnections, type ConnectionStatus } from "@/contexts/daemon-connections-context"; import type { AgentProvider, AgentSessionConfig, AgentPersistenceHandle, AgentTimelineItem, } from "@server/server/agent/agent-sdk-types"; import { useDaemonRequest } from "@/hooks/use-daemon-request"; import type { WSInboundMessage, SessionOutboundMessage } from "@server/server/messages"; import { formatConnectionStatus } from "@/utils/daemons"; import { trackAnalyticsEvent } from "@/utils/analytics"; import type { SessionContextValue } from "@/contexts/session-context"; import type { UseWebSocketReturn } from "@/hooks/use-websocket"; import { useSessionStore, type Agent } from "@/stores/session-store"; import { AssistantDropdown, DropdownField, DropdownSheet, ModelDropdown, PermissionsDropdown, WorkingDirectoryDropdown, } from "@/components/agent-form/agent-form-dropdowns"; import { useAgentFormState, type CreateAgentInitialValues, } from "@/hooks/use-agent-form-state"; interface AgentFlowModalProps { isVisible: boolean; onClose: () => void; flow: "create" | "import"; initialValues?: CreateAgentInitialValues; serverId?: string | null; onAfterClose?: () => void; } interface ModalWrapperProps { isVisible: boolean; onClose: () => void; initialValues?: CreateAgentInitialValues; serverId?: string | null; } type CreateAgentSessionSlice = { serverId: string; ws: UseWebSocketReturn | null; createAgent: (options: { config: any; initialPrompt: string; git?: any; worktreeName?: string; requestId?: string; }) => void; resumeAgent: (options: { handle: any; overrides?: any; requestId?: string }) => void; sendAgentAudio: ( agentId: string | undefined, audioBlob: Blob, requestId?: string, options?: { mode?: "transcribe_only" | "auto_run" } ) => Promise; agents: Map; }; const BACKDROP_OPACITY = 0.55; const IMPORT_PAGE_SIZE = 20; const IS_WEB = Platform.OS === "web"; type ImportCandidate = { provider: AgentProvider; sessionId: string; cwd: string; title: string; lastActivityAt: Date; persistence: AgentPersistenceHandle; timeline: AgentTimelineItem[]; }; type ProviderFilter = "all" | AgentProvider; type DropdownKey = | "assistant" | "permissions" | "model" | "workingDir" | "baseBranch" | "host"; type RepoInfoState = { cwd: string; repoRoot: string; branches: Array<{ name: string; isCurrent: boolean }>; currentBranch: string | null; isDirty: boolean; }; type GitRepoInfoResponseMessage = Extract< SessionOutboundMessage, { type: "git_repo_info_response" } >; function formatRelativeTime(date: Date): string { const now = Date.now(); const diffMs = now - date.getTime(); if (!Number.isFinite(diffMs)) { return "unknown"; } const minutes = Math.max(0, Math.floor(diffMs / 60000)); if (minutes < 1) { return "just now"; } if (minutes < 60) { return `${minutes}m ago`; } const hours = Math.floor(minutes / 60); if (hours < 24) { return `${hours}h ago`; } const days = Math.floor(hours / 24); return `${days}d ago`; } function getImportPreview(candidate: ImportCandidate): string { for (const item of candidate.timeline) { if (item.type === "user_message") { const text = item.text.trim(); if (text.length > 0) { return text; } } } return candidate.title || candidate.cwd; } function AgentFlowModal({ isVisible, onClose, flow, initialValues, serverId, onAfterClose, }: AgentFlowModalProps) { const insets = useSafeAreaInsets(); const { height: screenHeight, width: screenWidth } = useWindowDimensions(); const slideOffset = useSharedValue(screenHeight); const backdropOpacity = useSharedValue(0); const { height: keyboardHeight } = useReanimatedKeyboardAnimation(); const isCompactLayout = screenWidth < 720; const shouldAutoFocusPrompt = IS_WEB; const isImportFlow = flow === "import"; const isCreateFlow = !isImportFlow; const { addRecentPath } = useRecentPaths(); const { connectionStates } = useDaemonConnections(); const daemonEntries = useMemo(() => Array.from(connectionStates.values()), [connectionStates]); const initialServerId = useMemo(() => { if (!serverId) { return null; } const exists = daemonEntries.some((entry) => entry.daemon.id === serverId); return exists ? serverId : null; }, [serverId, daemonEntries]); const { selectedServerId, setSelectedServerId, setSelectedServerIdFromUser, selectedProvider, setProviderFromUser, selectedMode, setModeFromUser, selectedModel, setModelFromUser, workingDir, setWorkingDirFromUser, providerDefinitions, providerDefinitionMap, modeOptions, availableModels, isModelLoading, modelError, refreshProviderModels, queueProviderModelFetch, clearQueuedProviderModelRequest, workingDirIsEmpty, } = useAgentFormState({ initialServerId, initialValues, isVisible, isCreateFlow, }); const sessionState = useSessionStore((state) => selectedServerId ? state.sessions[selectedServerId] : undefined ); // Extract only what we need for CreateAgentSessionSlice const session = useMemo(() => { if (!selectedServerId || !sessionState || !sessionState.methods) { return null; } const slice: CreateAgentSessionSlice = { serverId: selectedServerId, ws: sessionState.ws, createAgent: sessionState.methods.createAgent, resumeAgent: sessionState.methods.resumeAgent, sendAgentAudio: sessionState.methods.sendAgentAudio, agents: sessionState.agents, }; return slice; }, [selectedServerId, sessionState]); useEffect(() => { if (selectedServerId) { const exists = daemonEntries.some((entry) => entry.daemon.id === selectedServerId); if (!exists) { setSelectedServerId(initialServerId); } return; } if (initialServerId && selectedServerId !== initialServerId) { setSelectedServerId(initialServerId); } }, [daemonEntries, selectedServerId, initialServerId]); useEffect(() => { if (!isVisible) { return; } if (initialServerId && selectedServerId !== initialServerId) { setSelectedServerId(initialServerId); } }, [isVisible, initialServerId, selectedServerId]); useEffect(() => { if (!isVisible || initialServerId || selectedServerId) { return; } const firstReady = daemonEntries.find( ({ status, sessionReady }) => status === "online" && sessionReady ); if (firstReady) { setSelectedServerId(firstReady.daemon.id); } }, [daemonEntries, initialServerId, isVisible, selectedServerId]); const inertWebSocket = useMemo( () => ({ isConnected: false, isConnecting: false, conversationId: null, lastError: null, send: () => {}, on: () => () => {}, sendPing: () => {}, sendUserMessage: () => {}, clearAgentAttention: () => {}, subscribeConnectionStatus: () => () => {}, getConnectionState: () => ({ isConnected: false, isConnecting: false }), }), [] ); const ws = session?.ws ?? null; const effectiveWs: UseWebSocketReturn = ws ?? inertWebSocket; const createAgent = session?.createAgent; const resumeAgent = session?.resumeAgent; const sessionSendAgentAudio = session?.sendAgentAudio; const noopSendAgentAudio = useCallback(async () => {}, []); const sendAgentAudio = sessionSendAgentAudio ?? noopSendAgentAudio; const hasSendAgentAudio = Boolean(sessionSendAgentAudio); const agents = session?.agents; const agentWorkingDirSuggestions = useMemo(() => { if (!selectedServerId || !agents) { return []; } const uniquePaths = new Set(); agents.forEach((agent) => { if (agent.cwd) { uniquePaths.add(agent.cwd); } }); return Array.from(uniquePaths); }, [agents, selectedServerId]); const gitRepoInfoRequest = useDaemonRequest< { cwd: string }, RepoInfoState, GitRepoInfoResponseMessage >({ ws: effectiveWs, responseType: "git_repo_info_response", buildRequest: ({ params, requestId }) => ({ type: "session", message: { type: "git_repo_info_request", cwd: params?.cwd ?? ".", requestId, }, }), getRequestKey: (params) => params?.cwd ?? "default", selectData: (message) => ({ cwd: message.payload.cwd, repoRoot: message.payload.repoRoot, branches: message.payload.branches ?? [], currentBranch: message.payload.currentBranch ?? null, isDirty: Boolean(message.payload.isDirty), }), extractError: (message) => message.payload.error ? new Error(message.payload.error) : null, keepPreviousData: false, }); const { status: repoRequestStatus, data: repoInfo, error: repoRequestError, execute: inspectRepoInfo, reset: resetRepoInfo, cancel: cancelRepoInfo, } = gitRepoInfoRequest; const isWsConnected = effectiveWs.getConnectionState ? effectiveWs.getConnectionState().isConnected : effectiveWs.isConnected; const router = useRouter(); const sessionServerId = session?.serverId ?? null; const selectedDaemonId = selectedServerId ?? sessionServerId; const selectedDaemonConnection = selectedDaemonId ? connectionStates.get(selectedDaemonId) : null; const selectedDaemonStatus: ConnectionStatus = selectedDaemonConnection?.status ?? (ws?.isConnected ? "online" : ws?.isConnecting ? "connecting" : ws?.lastError ? "error" : "offline"); const selectedDaemonLabel = selectedDaemonConnection?.daemon.label ?? selectedDaemonConnection?.daemon.wsUrl ?? selectedDaemonId ?? "Selected host"; const selectedDaemonStatusLabel = formatConnectionStatus(selectedDaemonStatus); const hasSelectedDaemon = Boolean(selectedServerId); const hostBadgeLabel = hasSelectedDaemon ? selectedDaemonLabel : "Select host"; const selectedDaemonIsOffline = selectedDaemonStatus !== "online"; const selectedDaemonLastError = selectedDaemonConnection?.lastError?.trim(); const daemonAvailabilityError = !hasSelectedDaemon ? "Select a host before creating or importing agents." : selectedDaemonIsOffline ? `${selectedDaemonLabel} is ${selectedDaemonStatusLabel}. We'll reconnect automatically and enable actions once it's online.${ selectedDaemonLastError ? ` ${selectedDaemonLastError}` : "" }` : null; const selectedDaemonSessionReady = selectedDaemonConnection?.status === "online" && selectedDaemonConnection.sessionReady; const isTargetDaemonReady = Boolean( hasSelectedDaemon && selectedDaemonSessionReady && !selectedDaemonIsOffline ); const [isMounted, setIsMounted] = useState(isVisible); const [initialPrompt, setInitialPrompt] = useState(""); const [baseBranch, setBaseBranch] = useState(""); const [createNewBranch, setCreateNewBranch] = useState(false); const [branchName, setBranchName] = useState(""); const [createWorktree, setCreateWorktree] = useState(false); const [worktreeSlug, setWorktreeSlug] = useState(""); const [branchNameEdited, setBranchNameEdited] = useState(false); const [worktreeSlugEdited, setWorktreeSlugEdited] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const [isLoading, setIsLoading] = useState(false); const [importProviderFilter, setImportProviderFilter] = useState("all"); const [importSearchQuery, setImportSearchQuery] = useState(""); const [importCandidates, setImportCandidates] = useState( [] ); const [isImportLoading, setIsImportLoading] = useState(false); const [importError, setImportError] = useState(null); const [openDropdown, setOpenDropdown] = useState(null); const pendingRequestIdRef = useRef(null); const shouldSyncBaseBranchRef = useRef(true); const hasPendingCreateOrResume = pendingRequestIdRef.current !== null; const shouldListenForStatus = isVisible || hasPendingCreateOrResume; const idleProviderPrefetchHandleRef = useRef | null>(null); const pendingNavigationAgentIdRef = useRef(null); const pendingNavigationServerIdRef = useRef(null); const openDropdownSheet = useCallback((key: DropdownKey) => { setOpenDropdown(key); }, []); const closeDropdown = useCallback(() => { setOpenDropdown(null); }, []); const handleUserWorkingDirChange = useCallback( (value: string) => { setWorkingDirFromUser(value); setErrorMessage(""); }, [setWorkingDirFromUser] ); const handleBaseBranchChange = useCallback( (value: string) => { shouldSyncBaseBranchRef.current = false; setBaseBranch(value); setErrorMessage(""); }, [setErrorMessage] ); const providerFilterOptions = useMemo( () => [ { id: "all" as ProviderFilter, label: "All" }, ...providerDefinitions.map((definition) => ({ id: definition.id as ProviderFilter, label: definition.label, })), ], [] ); const getProviderLabel = useCallback( (provider: AgentProvider) => providerDefinitionMap.get(provider)?.label ?? provider, [] ); const activeSessionIds = useMemo(() => { const ids = new Set(); if (!agents) { return ids; } // Use persistence.sessionId for filtering - this is the canonical reference // to the provider's session file (Claude resume token, Codex thread ID, etc.) agents.forEach((agent) => { const persistedSessionId = agent.persistence?.sessionId; if (persistedSessionId) { ids.add(persistedSessionId); } }); return ids; }, [agents]); const filteredImportCandidates = useMemo(() => { const providerFilter = importProviderFilter; const query = importSearchQuery.trim().toLowerCase(); return importCandidates .filter((candidate) => !activeSessionIds.has(candidate.sessionId)) .filter( (candidate) => providerFilter === "all" || candidate.provider === providerFilter ) .filter((candidate) => { if (query.length === 0) { return true; } const titleText = candidate.title.toLowerCase(); const cwdText = candidate.cwd.toLowerCase(); return titleText.includes(query) || cwdText.includes(query); }) .sort((a, b) => b.lastActivityAt.getTime() - a.lastActivityAt.getTime()); }, [ activeSessionIds, importCandidates, importProviderFilter, importSearchQuery, ]); const logOfflineDaemonAction = useCallback( (action: "create" | "resume" | "dictation" | "import_list", reason?: string | null) => { trackAnalyticsEvent({ type: "offline_daemon_action_attempt", action, daemonId: selectedDaemonId, status: selectedDaemonStatus ?? null, reason: reason ?? daemonAvailabilityError, }); }, [ daemonAvailabilityError, selectedDaemonId, selectedDaemonStatus, ] ); const resetFormState = useCallback(() => { setInitialPrompt(""); setBaseBranch(""); setCreateNewBranch(false); setBranchName(""); setCreateWorktree(false); setWorktreeSlug(""); setBranchNameEdited(false); setWorktreeSlugEdited(false); setErrorMessage(""); setIsLoading(false); resetRepoInfo(); setOpenDropdown(null); pendingRequestIdRef.current = null; pendingNavigationServerIdRef.current = null; shouldSyncBaseBranchRef.current = true; cancelRepoInfo(); }, [cancelRepoInfo, resetRepoInfo]); const navigateToAgentIfNeeded = useCallback(() => { const agentId = pendingNavigationAgentIdRef.current; const targetServerId = pendingNavigationServerIdRef.current ?? selectedDaemonId; if (!agentId || !targetServerId) { return; } pendingNavigationAgentIdRef.current = null; pendingNavigationServerIdRef.current = null; InteractionManager.runAfterInteractions(() => { router.push({ pathname: "/agent/[serverId]/[agentId]", params: { serverId: targetServerId, agentId, }, }); }); }, [router, selectedDaemonId]); const handleSelectServer = useCallback( (serverId: string) => { setSelectedServerIdFromUser(serverId); }, [setSelectedServerIdFromUser] ); const handleCloseAnimationComplete = useCallback(() => { console.log("[CreateAgentModal] close animation complete – resetting form"); resetFormState(); setIsMounted(false); navigateToAgentIfNeeded(); onAfterClose?.(); }, [navigateToAgentIfNeeded, onAfterClose, resetFormState]); const requestImportCandidates = useCallback( (provider?: AgentProvider) => { if (!isTargetDaemonReady || !ws || !ws.isConnected) { setIsImportLoading(false); logOfflineDaemonAction("import_list"); setImportError( daemonAvailabilityError ?? "Import candidates load automatically once the selected host is back online." ); return; } setIsImportLoading(true); setImportError(null); const msg: WSInboundMessage = { type: "session", message: { type: "list_persisted_agents_request", ...(provider ? { provider } : {}), limit: IMPORT_PAGE_SIZE, }, }; try { ws.send(msg); } catch (error) { console.error( "[CreateAgentModal] Failed to request persisted agents:", error ); setIsImportLoading(false); setImportError("Unable to load agents to import. Please try again."); } }, [daemonAvailabilityError, isTargetDaemonReady, logOfflineDaemonAction, ws] ); useEffect(() => { if (!isVisible) { console.log( "[CreateAgentModal] visibility effect skipped (isVisible is false)", { isMounted, } ); return; } console.log("[CreateAgentModal] visibility effect triggered", { wasMounted: isMounted, screenHeight, }); setIsMounted(true); slideOffset.value = screenHeight; backdropOpacity.value = 0; backdropOpacity.value = withTiming(BACKDROP_OPACITY, { duration: 200, easing: Easing.out(Easing.cubic), }); slideOffset.value = withTiming(0, { duration: 240, easing: Easing.out(Easing.cubic), }); }, [isVisible, slideOffset, backdropOpacity, screenHeight]); useEffect(() => { if (!isMounted || isVisible) { console.log("[CreateAgentModal] close animation skipped", { isMounted, isVisible, }); return; } console.log("[CreateAgentModal] close animation starting", { screenHeight, }); backdropOpacity.value = withTiming(0, { duration: 160, easing: Easing.out(Easing.cubic), }); slideOffset.value = withTiming( screenHeight, { duration: 220, easing: Easing.in(Easing.cubic), }, (finished) => { if (finished) { console.log("[CreateAgentModal] slide animation finished"); runOnJS(handleCloseAnimationComplete)(); } } ); }, [ isMounted, isVisible, slideOffset, backdropOpacity, screenHeight, handleCloseAnimationComplete, ]); useEffect(() => { return () => { idleProviderPrefetchHandleRef.current?.cancel?.(); }; }, []); const footerAnimatedStyle = useAnimatedStyle(() => { "worklet"; const absoluteHeight = Math.abs(keyboardHeight.value); const shift = Math.max(0, absoluteHeight - insets.bottom); return { transform: [{ translateY: -shift }], }; }, [insets.bottom, keyboardHeight]); const containerAnimatedStyle = useAnimatedStyle(() => { "worklet"; return { transform: [{ translateY: slideOffset.value }], }; }, [slideOffset]); const backdropAnimatedStyle = useAnimatedStyle(() => { "worklet"; return { opacity: backdropOpacity.value, }; }, [backdropOpacity]); const slugifyWorktreeName = useCallback((input: string): string => { return input .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); }, []); const validateWorktreeName = useCallback( (name: string): { valid: boolean; error?: string } => { if (!name) { return { valid: true }; } if (name.length > 100) { return { valid: false, error: "Worktree name too long (max 100 characters)", }; } if (!/^[a-z0-9-/]+$/.test(name)) { return { valid: false, error: "Must contain only lowercase letters, numbers, hyphens, and forward slashes", }; } if (name.startsWith("-") || name.endsWith("-")) { return { valid: false, error: "Cannot start or end with a hyphen" }; } if (name.includes("--")) { return { valid: false, error: "Cannot have consecutive hyphens" }; } return { valid: true }; }, [] ); const handleClose = useCallback(() => { onClose(); }, [onClose]); useEffect(() => { const slug = slugifyWorktreeName(initialPrompt); if (!branchNameEdited) { setBranchName(slug); } if (!worktreeSlugEdited) { setWorktreeSlug(slug); } }, [ initialPrompt, branchNameEdited, worktreeSlugEdited, slugifyWorktreeName, ]); useEffect(() => { if (!isCreateFlow || !isVisible) { return; } shouldSyncBaseBranchRef.current = true; }, [isCreateFlow, isVisible, workingDir]); useEffect(() => { idleProviderPrefetchHandleRef.current?.cancel?.(); idleProviderPrefetchHandleRef.current = InteractionManager.runAfterInteractions(() => { daemonEntries.forEach(({ daemon, status }) => { const serverId = daemon.id; const isSelected = serverId === selectedServerId; const isReady = status === "online" || status === "idle"; if (isSelected || !isReady) { clearQueuedProviderModelRequest(serverId); return; } queueProviderModelFetch(serverId, { delayMs: 320 }); }); }); return () => { idleProviderPrefetchHandleRef.current?.cancel?.(); }; }, [ clearQueuedProviderModelRequest, daemonEntries, queueProviderModelFetch, selectedProvider, selectedServerId, ]); const trimmedWorkingDir = workingDir.trim(); const shouldInspectRepo = isCreateFlow && isVisible && trimmedWorkingDir.length > 0; const repoAvailabilityError = shouldInspectRepo && (!isTargetDaemonReady || !isWsConnected) ? daemonAvailabilityError ?? "Repository details will load automatically once the selected host is back online." : null; const isNonGitDirectory = repoRequestStatus === "error" && /not in a git repository/i.test(repoRequestError?.message ?? ""); const repoInfoStatus: "idle" | "loading" | "ready" | "error" = !shouldInspectRepo ? "idle" : repoAvailabilityError ? "error" : repoRequestStatus === "loading" ? "loading" : repoRequestStatus === "error" ? isNonGitDirectory ? "idle" : "error" : repoRequestStatus === "success" ? "ready" : "idle"; const repoInfoError = repoAvailabilityError ?? (isNonGitDirectory ? null : repoRequestError?.message ?? null); const gitHelperText = isNonGitDirectory ? "No git repository detected. Git options are disabled for this directory." : null; useEffect(() => { if (!shouldInspectRepo) { cancelRepoInfo(); resetRepoInfo(); return; } if (repoAvailabilityError) { cancelRepoInfo(); return; } inspectRepoInfo({ cwd: trimmedWorkingDir }).catch(() => {}); return () => { cancelRepoInfo(); }; }, [ cancelRepoInfo, inspectRepoInfo, repoAvailabilityError, resetRepoInfo, shouldInspectRepo, trimmedWorkingDir, ]); useEffect(() => { if (!repoInfo) { return; } setBaseBranch((prev) => { if (shouldSyncBaseBranchRef.current || prev.trim().length === 0) { shouldSyncBaseBranchRef.current = false; return repoInfo.currentBranch ?? ""; } return prev; }); }, [repoInfo]); useEffect(() => { if (!isNonGitDirectory) { return; } if ( createNewBranch || createWorktree || baseBranch.trim().length > 0 || branchName.trim().length > 0 || worktreeSlug.trim().length > 0 ) { setCreateNewBranch(false); setCreateWorktree(false); setBaseBranch(""); setBranchName(""); setWorktreeSlug(""); setBranchNameEdited(false); setWorktreeSlugEdited(false); shouldSyncBaseBranchRef.current = true; } }, [ baseBranch, branchName, createNewBranch, createWorktree, isNonGitDirectory, worktreeSlug, ]); const handleCreate = useCallback(async () => { const trimmedPath = workingDir.trim(); if (!trimmedPath) { setErrorMessage("Working directory is required"); return; } const trimmedPrompt = initialPrompt.trim(); if (!trimmedPrompt) { setErrorMessage("Initial prompt is required"); return; } if (isLoading) { return; } if (gitBlockingError) { setErrorMessage(gitBlockingError); return; } if (!createAgent || !isTargetDaemonReady) { logOfflineDaemonAction("create"); setErrorMessage( daemonAvailabilityError ?? "Creating agents is temporarily unavailable while the selected host is offline. Paseo reconnects automatically—try again once it's online." ); return; } const trimmedBaseBranch = baseBranch.trim(); try { await addRecentPath(trimmedPath); } catch (error) { console.error("[CreateAgentModal] Failed to save recent path:", error); } const requestId = generateMessageId(); pendingRequestIdRef.current = requestId; pendingNavigationServerIdRef.current = selectedDaemonId ?? null; setIsLoading(true); setErrorMessage(""); const modeId = modeOptions.length > 0 && selectedMode !== "" ? selectedMode : undefined; const trimmedModel = selectedModel.trim(); const config: AgentSessionConfig = { provider: selectedProvider, cwd: trimmedPath, ...(modeId ? { modeId } : {}), ...(trimmedModel ? { model: trimmedModel } : {}), }; const currentBranch = repoInfo?.currentBranch ?? ""; const shouldIncludeBase = createNewBranch || createWorktree || (trimmedBaseBranch.length > 0 && trimmedBaseBranch !== currentBranch); const gitOptions = shouldIncludeBase && !isNonGitDirectory ? { ...(trimmedBaseBranch ? { baseBranch: trimmedBaseBranch } : {}), ...(createNewBranch ? { createNewBranch: true, newBranchName: branchName.trim() } : {}), ...(createWorktree ? { createWorktree: true, worktreeSlug: (worktreeSlug || branchName).trim(), } : {}), } : undefined; try { createAgent({ config, initialPrompt: trimmedPrompt, git: gitOptions, requestId, }); } catch (error) { console.error("[CreateAgentModal] Failed to create agent:", error); setErrorMessage("Failed to create agent. Please try again."); setIsLoading(false); pendingRequestIdRef.current = null; pendingNavigationServerIdRef.current = null; } }, [ workingDir, initialPrompt, baseBranch, createNewBranch, branchName, createWorktree, worktreeSlug, repoInfo, selectedMode, modeOptions, logOfflineDaemonAction, selectedProvider, isLoading, validateWorktreeName, addRecentPath, createAgent, daemonAvailabilityError, isTargetDaemonReady, selectedDaemonId, ]); const handleImportCandidatePress = useCallback( (candidate: ImportCandidate) => { if (isLoading) { return; } if (!resumeAgent || !isTargetDaemonReady) { logOfflineDaemonAction("resume"); setImportError( daemonAvailabilityError ?? "Importing agents will resume automatically once the selected host is online." ); return; } setErrorMessage(""); const requestId = generateMessageId(); pendingRequestIdRef.current = requestId; pendingNavigationServerIdRef.current = selectedDaemonId ?? null; setIsLoading(true); resumeAgent({ handle: candidate.persistence, requestId, }); }, [ daemonAvailabilityError, isLoading, logOfflineDaemonAction, isTargetDaemonReady, resumeAgent, selectedDaemonId, setImportError, ] ); const renderImportItem = useCallback>( ({ item }) => ( handleImportCandidatePress(item)} disabled={isLoading || !isTargetDaemonReady} style={styles.resumeItem} > {getImportPreview(item)} {formatRelativeTime(item.lastActivityAt)} {item.cwd} {getProviderLabel(item.provider)} Tap to import ), [getProviderLabel, handleImportCandidatePress, isLoading, isTargetDaemonReady] ); useEffect(() => { if (!isImportFlow || !isVisible || !ws) { return; } const unsubscribe = ws.on("list_persisted_agents_response", (message) => { if (message.type !== "list_persisted_agents_response") { return; } const mapped = message.payload.items.map((item) => ({ provider: item.provider, sessionId: item.sessionId, cwd: item.cwd, title: item.title ?? `Session ${item.sessionId.slice(0, 8)}`, lastActivityAt: new Date(item.lastActivityAt), persistence: item.persistence, timeline: item.timeline ?? [], })) as ImportCandidate[]; setImportCandidates(mapped); setIsImportLoading(false); }); return () => { unsubscribe(); }; }, [isImportFlow, isVisible, ws]); useEffect(() => { if (!shouldListenForStatus || !ws) { return; } const unsubscribe = ws.on("status", (message) => { if (message.type !== "status") { return; } const payload = message.payload as { status: string; agentId?: string; requestId?: string; error?: string; }; if (payload.status === "agent_create_failed") { const expectedRequestId = pendingRequestIdRef.current; if (!expectedRequestId || payload.requestId !== expectedRequestId) { return; } pendingRequestIdRef.current = null; pendingNavigationServerIdRef.current = null; setIsLoading(false); setErrorMessage( payload.error ?? "Failed to create agent. Resolve git issues or try again." ); return; } if ( (payload.status !== "agent_created" && payload.status !== "agent_resumed") || !payload.agentId ) { return; } const expectedRequestId = pendingRequestIdRef.current; if (!expectedRequestId || payload.requestId !== expectedRequestId) { return; } console.log("[CreateAgentModal] Agent created:", payload.agentId); pendingRequestIdRef.current = null; setIsLoading(false); pendingNavigationAgentIdRef.current = payload.agentId; handleClose(); }); return () => { unsubscribe(); }; }, [handleClose, shouldListenForStatus, ws]); useEffect(() => { if (!isVisible || !isImportFlow) { return; } const provider = importProviderFilter === "all" ? undefined : importProviderFilter; requestImportCandidates(provider); }, [importProviderFilter, isImportFlow, isVisible, requestImportCandidates]); const refreshImportList = useCallback(() => { const provider = importProviderFilter === "all" ? undefined : importProviderFilter; requestImportCandidates(provider); }, [requestImportCandidates, importProviderFilter]); const shouldRender = isVisible || isMounted; const modalTitle = isImportFlow ? "Import Agent" : "Create New Agent"; const gitBlockingError = useMemo(() => { if (isNonGitDirectory) { return null; } const trimmedBase = baseBranch.trim(); const currentBranch = repoInfo?.currentBranch ?? ""; const isCustomBase = trimmedBase.length > 0 && (currentBranch.length === 0 || trimmedBase !== currentBranch); const requiresBase = createNewBranch || createWorktree || isCustomBase; if (requiresBase && !trimmedBase) { return "Select a base branch before launching the agent"; } if (createNewBranch) { const slug = branchName.trim(); const validation = validateWorktreeName(slug); if (!slug || !validation.valid) { return `Invalid branch name: ${ validation.error ?? "Must use lowercase letters, numbers, hyphens, or forward slashes" }`; } } if (createWorktree) { const slug = (worktreeSlug || branchName).trim(); const validation = validateWorktreeName(slug); if (!slug || !validation.valid) { return `Invalid worktree name: ${ validation.error ?? "Must use lowercase letters, numbers, or hyphens" }`; } } if (!createWorktree && repoInfo?.isDirty) { const intendsCheckout = createNewBranch || (trimmedBase.length > 0 && trimmedBase !== repoInfo.currentBranch); if (intendsCheckout) { return "Working directory has uncommitted changes. Clean up or create a worktree first."; } } return null; }, [ baseBranch, branchName, createNewBranch, createWorktree, isNonGitDirectory, repoInfo, validateWorktreeName, worktreeSlug, ]); const promptIsEmpty = !initialPrompt.trim(); const createDisabled = workingDirIsEmpty || promptIsEmpty || Boolean(gitBlockingError) || isLoading || !isTargetDaemonReady; const headerPaddingTop = useMemo( () => insets.top + defaultTheme.spacing[4], [insets.top] ); const horizontalPaddingLeft = useMemo( () => defaultTheme.spacing[6] + insets.left, [insets.left] ); const horizontalPaddingRight = useMemo( () => defaultTheme.spacing[6] + insets.right, [insets.right] ); const handleSheetLayout = useCallback((event: LayoutChangeEvent) => { const { height, y } = event.nativeEvent.layout; console.log("[CreateAgentModal] sheet layout", { height, y }); }, []); if (!shouldRender) { // console.log("[CreateAgentModal] render skipped", { // isVisible, // isMounted, // }); return null; } // console.log("[CreateAgentModal] rendering modal", { // isVisible, // isMounted, // }); return ( openDropdownSheet("host")} > {hostBadgeLabel} ) : undefined } /> {isCreateFlow ? ( <> Target Host {daemonEntries.length === 0 ? ( No hosts available ) : ( {daemonEntries.map(({ daemon, status }) => { const isSelected = daemon.id === selectedServerId; const label = daemon.label || daemon.wsUrl; return ( handleSelectServer(daemon.id)} style={[styles.daemonChip, isSelected && styles.daemonChipSelected]} > {label} {formatConnectionStatus(status)} ); })} )} {daemonAvailabilityError ? ( {daemonAvailabilityError} ) : null} Initial Prompt { setInitialPrompt(text); setErrorMessage(""); }} onSubmit={() => { void handleCreate(); }} ws={effectiveWs} sendAgentAudio={sendAgentAudio} placeholder="Describe what you want the agent to do" autoFocus={shouldAutoFocusPrompt} disabled={isLoading || !isTargetDaemonReady} isSubmitDisabled={createDisabled} /> openDropdownSheet("assistant")} onClose={closeDropdown} onSelect={setProviderFromUser} /> openDropdownSheet("permissions")} onClose={closeDropdown} onSelect={setModeFromUser} /> { refreshProviderModels(); openDropdownSheet("model"); }} onClose={closeDropdown} onSelect={(modelId) => { setModelFromUser(modelId); setErrorMessage(""); }} onClear={() => { setModelFromUser(""); setErrorMessage(""); }} onRefresh={refreshProviderModels} /> openDropdownSheet("workingDir")} onClose={closeDropdown} disabled={isLoading} suggestedPaths={agentWorkingDirSuggestions} onSelectPath={handleUserWorkingDirChange} /> { setCreateNewBranch(next); if (next) { if (!branchNameEdited) { const slug = slugifyWorktreeName( initialPrompt || baseBranch || "" ); setBranchName(slug); } } else { setBranchName(""); setBranchNameEdited(false); } }} branchName={branchName} onBranchNameChange={(value) => { setBranchName(slugifyWorktreeName(value)); setBranchNameEdited(true); }} createWorktree={createWorktree} onToggleCreateWorktree={(next) => { setCreateWorktree(next); if (next) { if (!worktreeSlugEdited) { const slug = slugifyWorktreeName( initialPrompt || branchName || baseBranch || "" ); setWorktreeSlug(slug); } } else { setWorktreeSlug(""); setWorktreeSlugEdited(false); } }} worktreeSlug={worktreeSlug} onWorktreeSlugChange={(value) => { setWorktreeSlug(slugifyWorktreeName(value)); setWorktreeSlugEdited(true); }} gitValidationError={gitBlockingError} isBaseDropdownOpen={openDropdown === "baseBranch"} onToggleBaseDropdown={() => openDropdownSheet("baseBranch")} onCloseDropdown={closeDropdown} /> {isLoading ? ( Creating... ) : ( Create Agent )} ) : ( {daemonAvailabilityError ? ( {daemonAvailabilityError} ) : null} {providerFilterOptions.map((option) => { const isActive = importProviderFilter === option.id; return ( setImportProviderFilter(option.id)} style={[ styles.providerFilterButton, isActive && styles.providerFilterButtonActive, ]} > {option.label} ); })} Refresh {importError ? ( {importError} ) : null} {isImportLoading ? ( Loading agents to import... ) : filteredImportCandidates.length === 0 ? ( No agents to import We will load the latest Claude and Codex sessions from your local history so you can import them. Try Again ) : ( `${item.provider}:${item.sessionId}` } ItemSeparatorComponent={() => ( )} showsVerticalScrollIndicator={false} contentContainerStyle={styles.resumeListContent} /> )} )} {daemonEntries.length === 0 ? ( No hosts available yet. ) : ( {daemonEntries.map(({ daemon, status }) => { const isSelected = daemon.id === selectedServerId; const label = daemon.label ?? daemon.wsUrl ?? daemon.id; return ( { setSelectedServerIdFromUser(daemon.id); closeDropdown(); }} > {label} {formatConnectionStatus(status)} ); })} )} ); } function LazyAgentFlowModal(props: Omit) { const { isVisible } = props; const [shouldRender, setShouldRender] = useState(isVisible); useEffect(() => { if (isVisible) { setShouldRender(true); } }, [isVisible]); const handleAfterClose = useCallback(() => { setShouldRender(false); }, []); if (!shouldRender) { return null; } return ; } export function CreateAgentModal(props: ModalWrapperProps) { return ; } export function ImportAgentModal(props: ModalWrapperProps) { return ; } interface ModalHeaderProps { paddingTop: number; paddingLeft: number; paddingRight: number; onClose: () => void; title: string; rightContent?: ReactNode; } function ModalHeader({ paddingTop, paddingLeft, paddingRight, onClose, title, rightContent, }: ModalHeaderProps): ReactElement { return ( {title} {rightContent} ); } interface GitOptionsSectionProps { baseBranch: string; onBaseBranchChange: (value: string) => void; branches: Array<{ name: string; isCurrent: boolean }>; status: "idle" | "loading" | "ready" | "error"; repoError: string | null; helperText?: string | null; warning: string | null; createNewBranch: boolean; onToggleCreateNewBranch: (value: boolean) => void; branchName: string; onBranchNameChange: (value: string) => void; createWorktree: boolean; onToggleCreateWorktree: (value: boolean) => void; worktreeSlug: string; onWorktreeSlugChange: (value: string) => void; gitValidationError: string | null; isGitDisabled?: boolean; isBaseDropdownOpen: boolean; onToggleBaseDropdown: () => void; onCloseDropdown: () => void; } function GitOptionsSection({ baseBranch, onBaseBranchChange, branches, status, repoError, helperText, warning, createNewBranch, onToggleCreateNewBranch, branchName, onBranchNameChange, createWorktree, onToggleCreateWorktree, worktreeSlug, onWorktreeSlugChange, gitValidationError, isGitDisabled, isBaseDropdownOpen, onToggleBaseDropdown, onCloseDropdown, }: GitOptionsSectionProps): ReactElement { const [branchSearch, setBranchSearch] = useState(""); const branchFilter = branchSearch.trim().toLowerCase(); const filteredBranches = branchFilter.length === 0 ? branches : branches.filter((branch) => branch.name.toLowerCase().includes(branchFilter) ); const maxVisible = 30; const currentBranchLabel = branches.find((branch) => branch.isCurrent)?.name ?? ""; const baseInputRef = useRef(null); const gitInputsDisabled = Boolean(isGitDisabled) || status === "loading"; useEffect(() => { if (isBaseDropdownOpen) { setBranchSearch(""); baseInputRef.current?.focus(); } }, [isBaseDropdownOpen]); return ( Git Setup Choose a base branch, then optionally create a feature branch or isolated worktree. {status === "loading" ? ( Inspecting repository… ) : filteredBranches.length === 0 ? ( {branchFilter.length === 0 ? "No branches detected yet." : "No branches match your search."} ) : ( {filteredBranches.slice(0, maxVisible).map((branch) => { const isActive = branch.name === baseBranch; return ( { onBaseBranchChange(branch.name); onCloseDropdown(); }} > {branch.name} {branch.isCurrent ? " (current)" : ""} ); })} )} {filteredBranches.length > maxVisible ? ( Showing first {maxVisible} matches. Keep typing to narrow it down. ) : null} {createNewBranch ? ( ) : null} {createWorktree ? ( ) : null} {gitValidationError ? ( {gitValidationError} ) : null} ); } interface ToggleRowProps { label: string; description?: string; value: boolean; onToggle: (value: boolean) => void; disabled?: boolean; } function ToggleRow({ label, description, value, onToggle, disabled, }: ToggleRowProps): ReactElement { return ( { if (!disabled) { onToggle(!value); } }} style={[styles.toggleRow, disabled && styles.toggleRowDisabled]} > {value ? : null} {label} {description ? ( {description} ) : null} ); } const styles = StyleSheet.create(((theme: any) => ({ overlay: { flex: 1, justifyContent: "flex-end", }, backdrop: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0, backgroundColor: theme.colors.palette.gray[900], zIndex: 1, }, backdropPressable: { flex: 1, }, sheet: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0, zIndex: 2, width: "100%", backgroundColor: theme.colors.card, overflow: "hidden", }, content: { flex: 1, position: "relative", }, header: { paddingBottom: theme.spacing[4], borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.border, flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, headerTitle: { color: theme.colors.foreground, fontSize: theme.fontSize["2xl"], fontWeight: theme.fontWeight.semibold, }, headerActions: { flexDirection: "row", alignItems: "center", gap: theme.spacing[3], }, closeButton: { padding: theme.spacing[2], borderRadius: theme.borderRadius.full, alignItems: "center", justifyContent: "center", }, hostBadge: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], backgroundColor: theme.colors.muted, borderRadius: theme.borderRadius.full, }, hostBadgeLabel: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.semibold, }, hostStatusDot: { width: 6, height: 6, borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.mutedForeground, }, hostStatusDotOnline: { backgroundColor: theme.colors.palette.green[500], }, scroll: { flex: 1, }, scrollContent: { paddingTop: theme.spacing[6], paddingBottom: theme.spacing[8], gap: theme.spacing[6], }, formSection: { gap: theme.spacing[3], }, label: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, }, input: { backgroundColor: theme.colors.background, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, borderRadius: theme.borderRadius.lg, paddingVertical: theme.spacing[3], paddingHorizontal: theme.spacing[4], color: theme.colors.foreground, fontSize: theme.fontSize.base, }, dropdownControl: { flexDirection: "row", alignItems: "center", gap: theme.spacing[3], backgroundColor: theme.colors.background, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, borderRadius: theme.borderRadius.lg, paddingVertical: theme.spacing[3], paddingHorizontal: theme.spacing[4], }, dropdownControlDisabled: { opacity: theme.opacity[50], }, dropdownValue: { flex: 1, color: theme.colors.foreground, fontSize: theme.fontSize.base, }, dropdownPlaceholder: { flex: 1, color: theme.colors.mutedForeground, fontSize: theme.fontSize.base, }, dropdownSearchInput: { borderRadius: theme.borderRadius.md, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, backgroundColor: theme.colors.background, paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], color: theme.colors.foreground, }, dropdownLoading: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, dropdownSheetOverlay: { flex: 1, justifyContent: "flex-end", }, dropdownSheetBackdrop: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0, backgroundColor: theme.colors.palette.gray[900], opacity: 0.45, }, dropdownSheetContainer: { backgroundColor: theme.colors.card, borderTopLeftRadius: theme.borderRadius["2xl"], borderTopRightRadius: theme.borderRadius["2xl"], paddingTop: theme.spacing[4], paddingHorizontal: theme.spacing[5], paddingBottom: theme.spacing[6] + theme.spacing[2], maxHeight: 560, width: "100%", }, dropdownSheetHandle: { width: 56, height: 4, borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.border, alignSelf: "center", marginBottom: theme.spacing[3], }, dropdownSheetTitle: { fontSize: theme.fontSize.lg, fontWeight: theme.fontWeight.semibold, color: theme.colors.foreground, textAlign: "center", marginBottom: theme.spacing[4], }, dropdownSheetScrollContent: { paddingBottom: theme.spacing[8], paddingHorizontal: theme.spacing[1], }, dropdownSheetList: { marginTop: theme.spacing[3], }, dropdownSheetOption: { paddingVertical: theme.spacing[3], paddingHorizontal: theme.spacing[4], borderRadius: theme.borderRadius.lg, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, backgroundColor: theme.colors.background, marginBottom: theme.spacing[2], }, dropdownSheetOptionSelected: { borderColor: theme.colors.palette.blue[400], backgroundColor: "rgba(59, 130, 246, 0.18)", }, dropdownSheetOptionLabel: { color: theme.colors.foreground, fontWeight: theme.fontWeight.semibold, }, dropdownSheetOptionDescription: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.sm, marginTop: theme.spacing[1], }, dropdownSheetLoading: { alignItems: "center", paddingVertical: theme.spacing[4], }, inputDisabled: { opacity: theme.opacity[50], }, errorText: { color: theme.colors.palette.red[500], fontSize: theme.fontSize.sm, }, warningText: { color: theme.colors.palette.orange[400], fontSize: theme.fontSize.sm, }, helperText: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.sm, }, selectorRow: { flexDirection: "row", gap: theme.spacing[4], }, selectorRowStacked: { flexDirection: "column", }, selectorColumn: { flex: 1, gap: theme.spacing[3], }, selectorColumnFull: { width: "100%", }, toggleRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[3], paddingVertical: theme.spacing[2], }, toggleRowDisabled: { opacity: theme.opacity[50], }, checkbox: { width: 22, height: 22, borderRadius: theme.borderRadius.sm, borderWidth: theme.borderWidth[2], borderColor: theme.colors.border, alignItems: "center", justifyContent: "center", }, checkboxChecked: { borderColor: theme.colors.palette.blue[500], backgroundColor: theme.colors.palette.blue[500], }, checkboxDisabled: { borderColor: theme.colors.border, }, checkboxDot: { width: 10, height: 10, borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.palette.white, }, toggleTextContainer: { flex: 1, gap: theme.spacing[1], }, toggleLabel: { color: theme.colors.foreground, fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.semibold, }, daemonSelectorSection: { marginBottom: theme.spacing[6], }, daemonSelectorLabel: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.xs, textTransform: "uppercase", letterSpacing: 0.5, marginBottom: theme.spacing[2], }, daemonSelectorChips: { flexDirection: "row", gap: theme.spacing[2], }, daemonChip: { borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, borderRadius: theme.borderRadius.lg, paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], marginRight: theme.spacing[2], }, daemonChipSelected: { backgroundColor: theme.colors.palette.blue[900], borderColor: theme.colors.palette.blue[500], }, daemonChipText: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.semibold, }, daemonChipTextSelected: { color: theme.colors.palette.white, }, daemonChipStatus: { fontSize: theme.fontSize.xs, color: theme.colors.mutedForeground, }, daemonAvailabilityText: { marginTop: theme.spacing[2], marginBottom: theme.spacing[2], color: theme.colors.destructive, fontSize: theme.fontSize.sm, }, footer: { borderTopWidth: theme.borderWidth[1], borderTopColor: theme.colors.border, paddingTop: theme.spacing[4], backgroundColor: theme.colors.card, }, createButton: { backgroundColor: theme.colors.palette.blue[500], paddingVertical: theme.spacing[4], borderRadius: theme.borderRadius.lg, alignItems: "center", }, createButtonDisabled: { backgroundColor: theme.colors.palette.blue[900], opacity: theme.opacity[50], }, createButtonText: { color: theme.colors.palette.white, fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.semibold, }, loadingContainer: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, resumeContainer: { flex: 1, gap: theme.spacing[4], }, resumeFilters: { gap: theme.spacing[3], }, providerFilterRow: { flexDirection: "row", flexWrap: "wrap", gap: theme.spacing[2], }, providerFilterButton: { paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[2], borderRadius: theme.borderRadius.full, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, backgroundColor: theme.colors.background, }, providerFilterButtonActive: { borderColor: theme.colors.palette.blue[500], backgroundColor: theme.colors.muted, }, providerFilterText: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, }, providerFilterTextActive: { color: theme.colors.foreground, fontWeight: theme.fontWeight.semibold, }, resumeSearchRow: { flexDirection: "row", gap: theme.spacing[3], alignItems: "center", }, resumeSearchInput: { flex: 1, borderRadius: theme.borderRadius.lg, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, backgroundColor: theme.colors.background, paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[3], color: theme.colors.foreground, }, refreshButton: { borderRadius: theme.borderRadius.lg, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[3], }, refreshButtonText: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, }, importErrorText: { color: theme.colors.palette.red[500], fontSize: theme.fontSize.sm, }, resumeLoading: { flex: 1, alignItems: "center", justifyContent: "center", gap: theme.spacing[2], }, resumeLoadingText: { color: theme.colors.mutedForeground, }, resumeEmptyState: { flex: 1, alignItems: "center", justifyContent: "center", gap: theme.spacing[2], paddingHorizontal: theme.spacing[6], textAlign: "center", }, resumeEmptyTitle: { color: theme.colors.foreground, fontSize: theme.fontSize.lg, fontWeight: theme.fontWeight.semibold, textAlign: "center", }, resumeEmptySubtitle: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.sm, textAlign: "center", }, refreshButtonAlt: { marginTop: theme.spacing[2], borderRadius: theme.borderRadius.lg, backgroundColor: theme.colors.palette.blue[500], paddingHorizontal: theme.spacing[6], paddingVertical: theme.spacing[3], }, refreshButtonAltText: { color: theme.colors.palette.white, fontWeight: theme.fontWeight.semibold, }, resumeListContent: { paddingBottom: theme.spacing[8], gap: theme.spacing[2], }, resumeItem: { borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, borderRadius: theme.borderRadius.lg, padding: theme.spacing[4], backgroundColor: theme.colors.background, gap: theme.spacing[2], }, resumeItemHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", gap: theme.spacing[2], }, resumeItemTitle: { color: theme.colors.foreground, fontWeight: theme.fontWeight.semibold, flex: 1, }, resumeItemTimestamp: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.sm, }, resumeItemPath: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.sm, }, resumeItemMetaRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, resumeProviderBadge: { paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[1], borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.muted, }, resumeProviderBadgeText: { color: theme.colors.foreground, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.semibold, }, resumeItemHint: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.xs, }, resumeItemSeparator: { height: theme.spacing[2], }, })) as any) as Record;