import { useState, useRef, useEffect, useMemo, useCallback, useLayoutEffect, } from "react"; import type { ReactElement, RefObject, ReactNode } from "react"; import { View, Text, Pressable, ScrollView, FlatList, ActivityIndicator, InteractionManager, TextInput, Modal, useWindowDimensions, type LayoutChangeEvent, type ListRenderItem, Platform, NativeSyntheticEvent, TextInputContentSizeChangeEventData, TextInputKeyPressEventData, } from "react-native"; import AsyncStorage from "@react-native-async-storage/async-storage"; 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 { Mic, Check, X, ChevronDown, RefreshCcw } 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 { useDictation } from "@/hooks/use-dictation"; import type { DictationStatus } from "@/hooks/use-dictation"; import { VolumeMeter } from "@/components/volume-meter"; import { AUDIO_DEBUG_ENABLED } from "@/config/audio-debug"; import { AudioDebugNotice, type AudioDebugInfo } from "./audio-debug-notice"; import { DictationStatusNotice, type DictationToastVariant } from "./dictation-status-notice"; import { AGENT_PROVIDER_DEFINITIONS, type AgentProviderDefinition, } from "@server/server/agent/provider-manifest"; import { useDaemonConnections, type ConnectionStatus } from "@/contexts/daemon-connections-context"; import type { AgentProvider, AgentMode, AgentSessionConfig, AgentPersistenceHandle, AgentTimelineItem, AgentModelDefinition, } 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 } from "@/stores/session-store"; export type CreateAgentInitialValues = { workingDir?: string; provider?: AgentProvider; modeId?: string | null; model?: string | null; }; interface AgentFlowModalProps { isVisible: boolean; onClose: () => void; flow: "create" | "import"; initialValues?: CreateAgentInitialValues; serverId?: string | null; onAfterClose?: () => void; } type DictationToastConfig = { variant: DictationToastVariant; title: string; subtitle?: string; meta?: string; actionLabel?: string; onAction?: () => void; onDismiss?: () => 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, audioBlob: Blob, requestId?: string, options?: { mode?: "transcribe_only" | "auto_run" } ) => Promise; agents: Map; providerModels: Map; requestProviderModels: (provider: any, options?: { cwd?: string }) => void; }; const providerDefinitions = AGENT_PROVIDER_DEFINITIONS; const providerDefinitionMap = new Map( providerDefinitions.map((definition) => [definition.id, definition]) ); const fallbackDefinition = providerDefinitions[0]; const DEFAULT_PROVIDER: AgentProvider = fallbackDefinition?.id ?? "claude"; const DEFAULT_MODE_FOR_DEFAULT_PROVIDER = fallbackDefinition?.defaultModeId ?? ""; const BACKDROP_OPACITY = 0.55; const IMPORT_PAGE_SIZE = 20; const PROMPT_MIN_HEIGHT = 64; const PROMPT_MAX_HEIGHT = 200; const FORM_PREFERENCES_STORAGE_KEY = "@paseo:create-agent-preferences"; const IS_WEB = Platform.OS === "web"; const DICTATION_AGENT_ID = "__dictation__"; type WebTextInputKeyPressEvent = NativeSyntheticEvent< TextInputKeyPressEventData & { metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean; } >; type TextAreaHandle = { scrollHeight?: number; style?: { height?: string; minHeight?: string; maxHeight?: string; overflowY?: string; } & Record; }; const isTextAreaLike = (node: unknown): node is TextAreaHandle => { if (!node || typeof node !== "object") { return false; } if (!("scrollHeight" in node) || !("style" in node)) { return false; } const style = (node as { style?: unknown }).style; if (!style || typeof style !== "object") { return false; } return true; }; 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"; 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 shouldHandlePromptDesktopSubmit = IS_WEB; const shouldAutoFocusPrompt = IS_WEB; const isImportFlow = flow === "import"; const isCreateFlow = !isImportFlow; const { recentPaths, 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] = useState(initialServerId); // [INVESTIGATION] Using useSessionStore directly with granular selectors // Get the complete session state for the selected server 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, providerModels: sessionState.providerModels, requestProviderModels: sessionState.methods.requestProviderModels, }; return slice; }, [selectedServerId, sessionState]); // Helper to get session state for background operations // Note: This only returns state, not APIs. For calling methods, need to send WS messages directly. const getSessionState = useCallback((serverId: string) => { return useSessionStore.getState().sessions[serverId] ?? null; }, []); 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: () => {}, 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 providerModels = session?.providerModels; const requestProviderModels = session?.requestProviderModels; 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 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 [workingDir, setWorkingDir] = useState(""); const [initialPrompt, setInitialPrompt] = useState(""); const [promptInputHeight, setPromptInputHeight] = useState(PROMPT_MIN_HEIGHT); const [selectedProvider, setSelectedProvider] = useState(DEFAULT_PROVIDER); const [selectedMode, setSelectedMode] = useState( DEFAULT_MODE_FOR_DEFAULT_PROVIDER ); const [selectedModel, setSelectedModel] = 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 [dictationDebugInfo, setDictationDebugInfo] = useState(null); const [openDropdown, setOpenDropdown] = useState(null); const pendingRequestIdRef = useRef(null); const shouldSyncBaseBranchRef = useRef(true); const [connectionStatus, setConnectionStatus] = useState(() => effectiveWs.getConnectionState ? effectiveWs.getConnectionState() : { isConnected: effectiveWs.isConnected, isConnecting: effectiveWs.isConnecting } ); const [dictationSuccessToastAt, setDictationSuccessToastAt] = useState(null); const promptInputRef = useRef< TextInput | (TextInput & { getNativeRef?: () => unknown }) | null >(null); const focusPromptInputRef = useRef<() => void>(() => {}); const promptInputHeightRef = useRef(PROMPT_MIN_HEIGHT); const promptBaselineHeightRef = useRef(null); const formPreferencesHydratedRef = useRef(false); const userEditedPreferencesRef = useRef({ provider: false, mode: false, model: false, workingDir: false, }); const prevVisibilityRef = useRef(isVisible); const dictationRequestIdRef = useRef(null); const handleDictationTranscript = useCallback( (text: string, _meta: { requestId: string }) => { if (!isCreateFlow || !text) { return; } setInitialPrompt((prev) => { if (!prev) { return text; } const needsSpace = /\s$/.test(prev); return `${prev}${needsSpace ? "" : " "}${text}`; }); focusPromptInputRef.current?.(); }, [isCreateFlow, setInitialPrompt] ); const handleDictationError = useCallback( (dictationError: Error) => { setErrorMessage(dictationError.message); }, [] ); const canStartDictation = useCallback(() => { const allowed = isCreateFlow && !isLoading && isTargetDaemonReady && isWsConnected; console.log("[CreateAgentModal] canStartDictation", { allowed, isCreateFlow, isLoading, isTargetDaemonReady, isWsConnected, }); return allowed; }, [isCreateFlow, isLoading, isTargetDaemonReady, isWsConnected]); const canConfirmDictation = useCallback(() => { const allowed = isTargetDaemonReady && hasSendAgentAudio; console.log("[CreateAgentModal] canConfirmDictation", { allowed, isTargetDaemonReady, hasSendAgentAudio, }); return allowed; }, [hasSendAgentAudio, isTargetDaemonReady]); const { isRecording: isDictating, isProcessing: isDictationProcessing, volume: dictationVolume, pendingRequestId: dictationPendingRequestId, error: dictationError, status: dictationStatus, retryAttempt: dictationRetryAttempt, maxRetryAttempts: dictationMaxRetryAttempts, retryInfo: dictationRetryInfo, failedRecording: dictationFailedRecording, lastOutcome: dictationLastOutcome, startDictation, cancelDictation, confirmDictation, reset: resetDictation, retryFailedDictation, discardFailedDictation, } = useDictation({ agentId: DICTATION_AGENT_ID, sendAgentAudio, ws: effectiveWs, mode: "transcribe_only", onTranscript: handleDictationTranscript, onError: handleDictationError, canStart: canStartDictation, canConfirm: canConfirmDictation, autoStopWhenHidden: { isVisible }, }); const shouldShowAudioDebug = AUDIO_DEBUG_ENABLED; useEffect(() => { dictationRequestIdRef.current = dictationPendingRequestId; }, [dictationPendingRequestId]); useEffect(() => { if (!effectiveWs.subscribeConnectionStatus) { return; } return effectiveWs.subscribeConnectionStatus((status) => { setConnectionStatus(status); }); }, [effectiveWs]); useEffect(() => { if (dictationLastOutcome?.type === "success") { setDictationSuccessToastAt(dictationLastOutcome.timestamp); } }, [dictationLastOutcome]); useEffect(() => { if (dictationSuccessToastAt === null) { return; } const timeout = setTimeout(() => { setDictationSuccessToastAt(null); }, 4000); return () => { clearTimeout(timeout); }; }, [dictationSuccessToastAt]); const dictationSuccessToastVisible = dictationSuccessToastAt !== null; const hasPendingCreateOrResume = pendingRequestIdRef.current !== null; const hasPendingDictation = dictationPendingRequestId !== null; const shouldListenForStatus = isVisible || hasPendingCreateOrResume; const shouldListenForDictation = isCreateFlow && (isVisible || hasPendingDictation); const providerModelRequestTimersRef = useRef< Map> >(new Map()); 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 clearQueuedProviderModelRequest = useCallback((serverId: string | null) => { if (!serverId) { return; } const timer = providerModelRequestTimersRef.current.get(serverId); if (timer) { clearTimeout(timer); providerModelRequestTimersRef.current.delete(serverId); } }, []); const queueProviderModelFetch = useCallback( ( serverId: string | null, options?: { cwd?: string; delayMs?: number } ) => { if (!serverId) { clearQueuedProviderModelRequest(serverId); return; } const sessionState = getSessionState(serverId); if (!sessionState?.ws?.isConnected) { clearQueuedProviderModelRequest(serverId); return; } const currentState = sessionState.providerModels?.get(selectedProvider); if (currentState?.models?.length || currentState?.isLoading) { clearQueuedProviderModelRequest(serverId); return; } const delayMs = options?.delayMs ?? 0; const trigger = () => { providerModelRequestTimersRef.current.delete(serverId); // Send request directly via WebSocket const requestId = generateMessageId(); const message: WSInboundMessage = { type: "session", message: { type: "list_provider_models_request", provider: selectedProvider, ...(options?.cwd ? { cwd: options.cwd } : {}), requestId, }, }; sessionState.ws?.send(message); }; clearQueuedProviderModelRequest(serverId); if (delayMs > 0) { providerModelRequestTimersRef.current.set(serverId, setTimeout(trigger, delayMs)); } else { trigger(); } }, [clearQueuedProviderModelRequest, selectedProvider, getSessionState] ); const setProviderFromUser = useCallback((provider: AgentProvider) => { userEditedPreferencesRef.current.provider = true; setSelectedProvider(provider); userEditedPreferencesRef.current.model = true; setSelectedModel(""); }, []); const setModeFromUser = useCallback((modeId: string) => { userEditedPreferencesRef.current.mode = true; setSelectedMode(modeId); }, []); const setModelFromUser = useCallback((modelId: string) => { userEditedPreferencesRef.current.model = true; setSelectedModel(modelId); }, []); const setWorkingDirFromUser = useCallback((value: string) => { userEditedPreferencesRef.current.workingDir = true; setWorkingDir(value); }, []); const handleUserWorkingDirChange = useCallback( (value: string) => { setWorkingDirFromUser(value); setErrorMessage(""); }, [setWorkingDirFromUser] ); const applyInitialValues = useCallback(() => { if (!isCreateFlow || !initialValues) { return; } if (Object.prototype.hasOwnProperty.call(initialValues, "workingDir")) { const providedWorkingDir = initialValues.workingDir ?? ""; userEditedPreferencesRef.current.workingDir = true; setWorkingDir(providedWorkingDir); } if (initialValues.provider && providerDefinitionMap.has(initialValues.provider)) { userEditedPreferencesRef.current.provider = true; setSelectedProvider(initialValues.provider); } if (typeof initialValues.modeId === "string" && initialValues.modeId.length > 0) { userEditedPreferencesRef.current.mode = true; setSelectedMode(initialValues.modeId); } if (typeof initialValues.model === "string" && initialValues.model.length > 0) { userEditedPreferencesRef.current.model = true; setSelectedModel(initialValues.model); } }, [initialValues, isCreateFlow]); useEffect(() => { const wasVisible = prevVisibilityRef.current; if (isVisible && !wasVisible) { applyInitialValues(); } prevVisibilityRef.current = isVisible; }, [applyInitialValues, isVisible]); const refreshProviderModels = useCallback(() => { if (!requestProviderModels) { return; } const trimmed = workingDir.trim(); requestProviderModels(selectedProvider, { cwd: trimmed.length > 0 ? trimmed : undefined, }); }, [requestProviderModels, selectedProvider, workingDir]); const handleBaseBranchChange = useCallback( (value: string) => { shouldSyncBaseBranchRef.current = false; setBaseBranch(value); setErrorMessage(""); }, [setErrorMessage] ); useEffect(() => { let isActive = true; const hydratePreferences = async () => { try { const stored = await AsyncStorage.getItem(FORM_PREFERENCES_STORAGE_KEY); if (!stored || !isActive) { return; } const parsed = JSON.parse(stored) as { workingDir?: string; provider?: AgentProvider; mode?: string; model?: string; }; if ( parsed.provider && providerDefinitionMap.has(parsed.provider) && !userEditedPreferencesRef.current.provider ) { setSelectedProvider(parsed.provider); } if ( typeof parsed.mode === "string" && !userEditedPreferencesRef.current.mode ) { setSelectedMode(parsed.mode); } if ( typeof parsed.workingDir === "string" && !userEditedPreferencesRef.current.workingDir ) { setWorkingDir(parsed.workingDir); } if ( typeof parsed.model === "string" && !userEditedPreferencesRef.current.model ) { setSelectedModel(parsed.model); } } catch (error) { console.error( "[CreateAgentModal] Failed to hydrate form preferences:", error ); } finally { if (isActive) { formPreferencesHydratedRef.current = true; } } }; void hydratePreferences(); return () => { isActive = false; }; }, []); useEffect(() => { if (!formPreferencesHydratedRef.current) { return; } const persist = async () => { try { await AsyncStorage.setItem( FORM_PREFERENCES_STORAGE_KEY, JSON.stringify({ workingDir, provider: selectedProvider, mode: selectedMode, model: selectedModel, }) ); } catch (error) { console.error( "[CreateAgentModal] Failed to persist form preferences:", error ); } }; void persist(); }, [selectedMode, selectedProvider, workingDir]); 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 agentDefinition = providerDefinitionMap.get(selectedProvider); const modeOptions = agentDefinition?.modes ?? []; const modelState = providerModels?.get(selectedProvider); const availableModels = modelState?.models ?? []; const isModelLoading = modelState?.isLoading ?? false; const modelError = modelState?.error ?? null; useEffect(() => { const targetServerId = selectedServerId ?? session?.serverId ?? null; if (!isVisible || !isTargetDaemonReady || !targetServerId || !session) { clearQueuedProviderModelRequest(targetServerId); return; } const trimmed = workingDir.trim(); queueProviderModelFetch(targetServerId, { cwd: trimmed.length > 0 ? trimmed : undefined, delayMs: 180, }); return () => { clearQueuedProviderModelRequest(targetServerId); }; }, [ clearQueuedProviderModelRequest, isTargetDaemonReady, isVisible, queueProviderModelFetch, selectedServerId, session, workingDir, ]); const setPromptHeight = useCallback((nextHeight: number) => { const bounded = Math.min( PROMPT_MAX_HEIGHT, Math.max(PROMPT_MIN_HEIGHT, nextHeight) ); if (Math.abs(promptInputHeightRef.current - bounded) < 1) { return; } promptInputHeightRef.current = bounded; setPromptInputHeight(bounded); }, []); const applyPromptMeasuredHeight = useCallback( (measuredHeight: number) => { if (promptBaselineHeightRef.current === null) { promptBaselineHeightRef.current = measuredHeight; } const baseline = promptBaselineHeightRef.current ?? measuredHeight; const normalized = measuredHeight - baseline + PROMPT_MIN_HEIGHT; const bounded = Math.min( PROMPT_MAX_HEIGHT, Math.max(PROMPT_MIN_HEIGHT, normalized) ); setPromptHeight(bounded); return bounded; }, [setPromptHeight] ); const getPromptWebTextArea = useCallback((): TextAreaHandle | null => { if (!IS_WEB) { return null; } const node = promptInputRef.current; if (!node) { return null; } if (isTextAreaLike(node)) { return node; } if ( typeof (node as { getNativeRef?: () => unknown }).getNativeRef === "function" ) { const native = ( node as { getNativeRef?: () => unknown } ).getNativeRef?.(); if (isTextAreaLike(native)) { return native; } } return null; }, []); const measurePromptWebInputHeight = useCallback(() => { if (!IS_WEB) { return false; } const element = getPromptWebTextArea(); if (!element?.style || typeof element.scrollHeight !== "number") { return false; } const previousHeight = element.style.height; element.style.height = "auto"; const measuredHeight = element.scrollHeight; element.style.height = previousHeight ?? ""; const bounded = applyPromptMeasuredHeight(measuredHeight); element.style.height = `${bounded}px`; element.style.minHeight = `${PROMPT_MIN_HEIGHT}px`; element.style.maxHeight = `${PROMPT_MAX_HEIGHT}px`; element.style.overflowY = bounded >= PROMPT_MAX_HEIGHT ? "auto" : "hidden"; return true; }, [applyPromptMeasuredHeight, getPromptWebTextArea]); const handlePromptContentSizeChange = useCallback( (event: NativeSyntheticEvent) => { if (IS_WEB && measurePromptWebInputHeight()) { return; } applyPromptMeasuredHeight(event.nativeEvent.contentSize.height); }, [applyPromptMeasuredHeight, measurePromptWebInputHeight] ); const focusPromptInput = useCallback(() => { if (!shouldAutoFocusPrompt) { return; } const node = promptInputRef.current; if (!node) { return; } const target: | (TextInput & { focus?: () => void }) | { focus?: () => void } | null = typeof (node as { focus?: () => void }).focus === "function" ? node : typeof (node as { getNativeRef?: () => unknown }).getNativeRef === "function" ? ((node as { getNativeRef?: () => unknown }).getNativeRef?.() as { focus?: () => void; } | null) : null; if (target && typeof target.focus === "function") { const exec = () => target.focus?.(); if (typeof requestAnimationFrame === "function") { requestAnimationFrame(exec); } else { setTimeout(exec, 0); } } }, [shouldAutoFocusPrompt]); useEffect(() => { focusPromptInputRef.current = focusPromptInput; }, [focusPromptInput]); const activeSessionIds = useMemo(() => { const ids = new Set(); if (!agents) { return ids; } agents.forEach((agent: any) => { if (agent.sessionId) { ids.add(agent.sessionId); } 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, ]); useEffect(() => { if (!agentDefinition) { return; } if (modeOptions.length === 0) { if (selectedMode !== "") { setSelectedMode(""); } return; } const availableModeIds = modeOptions.map((mode) => mode.id); if (!availableModeIds.includes(selectedMode)) { const fallbackModeId = agentDefinition.defaultModeId ?? availableModeIds[0]; setSelectedMode(fallbackModeId); } }, [agentDefinition, selectedMode, modeOptions]); useLayoutEffect(() => { if (!IS_WEB) { return; } measurePromptWebInputHeight(); }, [initialPrompt, measurePromptWebInputHeight]); useEffect(() => { if (!shouldAutoFocusPrompt) { return; } if (!isVisible || !isCreateFlow) { return; } focusPromptInput(); }, [focusPromptInput, isCreateFlow, isVisible, shouldAutoFocusPrompt]); 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(""); promptBaselineHeightRef.current = null; promptInputHeightRef.current = PROMPT_MIN_HEIGHT; setPromptHeight(PROMPT_MIN_HEIGHT); 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; dictationRequestIdRef.current = null; setDictationDebugInfo(null); void cancelDictation(); resetDictation(); cancelRepoInfo(); }, [cancelRepoInfo, cancelDictation, resetDictation, resetRepoInfo, setPromptHeight]); const handleDictationStart = useCallback(async () => { console.log("[CreateAgentModal] handleDictationStart", { isCreateFlow, isLoading, isDictating, isDictationProcessing, isTargetDaemonReady, isWsConnected, }); if (!isCreateFlow || isLoading || isDictating || isDictationProcessing) { return; } if (!isTargetDaemonReady || !isWsConnected) { logOfflineDaemonAction( "dictation", daemonAvailabilityError ?? "WebSocket disconnected" ); return; } try { if (shouldShowAudioDebug) { setDictationDebugInfo(null); } setErrorMessage(""); await startDictation(); console.log("[CreateAgentModal] startDictation invoked"); } catch (error) { const isCancelled = error instanceof Error && error.message.includes("Recording cancelled"); if (!isCancelled) { console.error("[CreateAgentModal] Failed to start dictation:", error); } } }, [ daemonAvailabilityError, isCreateFlow, isDictating, isDictationProcessing, isLoading, isTargetDaemonReady, isWsConnected, logOfflineDaemonAction, shouldShowAudioDebug, startDictation, ]); const handleDictationCancel = useCallback(async () => { console.log("[CreateAgentModal] handleDictationCancel", { isDictating, }); if (dictationStatus === "failed") { discardFailedDictation(); return; } if (!isDictating) { return; } try { await cancelDictation(); } catch (error) { console.error("[CreateAgentModal] Failed to cancel dictation:", error); } }, [cancelDictation, dictationStatus, discardFailedDictation, isDictating]); const handleDictationConfirm = useCallback(async () => { console.log("[CreateAgentModal] handleDictationConfirm", { isDictating, isDictationProcessing, isTargetDaemonReady, hasSendAgentAudio, }); if (dictationStatus === "failed") { void retryFailedDictation(); return; } if (!isDictating || isDictationProcessing) { return; } if (!isTargetDaemonReady || !hasSendAgentAudio) { logOfflineDaemonAction("dictation"); setErrorMessage( daemonAvailabilityError ?? "Dictation is unavailable until the selected host is online. Paseo reconnects automatically—try again once it comes back." ); return; } try { await confirmDictation(); } catch (error) { console.error("[CreateAgentModal] Failed to complete dictation:", error); } }, [ daemonAvailabilityError, confirmDictation, dictationStatus, hasSendAgentAudio, isDictating, isDictationProcessing, isTargetDaemonReady, logOfflineDaemonAction, retryFailedDictation, ]); const handlePromptDictationRetry = useCallback(() => { void retryFailedDictation(); }, [retryFailedDictation]); const handlePromptDictationDiscard = useCallback(() => { discardFailedDictation(); }, [discardFailedDictation]); const promptDictationToast = useMemo(() => { if (!connectionStatus.isConnected) { return { variant: "warning", title: "Offline", subtitle: "Waiting for connection…", }; } if (dictationStatus === "recording") { return { variant: "info", title: "Recording prompt…", subtitle: "Release to insert transcription", }; } if (dictationStatus === "uploading") { const attemptLabel = `Attempt ${Math.max(1, dictationRetryAttempt || 1)}/${dictationMaxRetryAttempts}`; return { variant: "info", title: "Transcribing prompt…", meta: attemptLabel, }; } if (dictationStatus === "retrying") { const attempt = dictationRetryInfo?.attempt ?? Math.max(1, dictationRetryAttempt || 1); const maxAttempts = dictationRetryInfo?.maxAttempts ?? dictationMaxRetryAttempts; const retryMeta = dictationRetryInfo?.nextRetryMs && dictationRetryInfo.nextRetryMs > 0 ? `Attempt ${attempt}/${maxAttempts} · Next in ${Math.ceil(dictationRetryInfo.nextRetryMs / 1000)}s` : `Attempt ${attempt}/${maxAttempts}`; return { variant: "warning", title: "Retrying dictation…", subtitle: dictationRetryInfo?.errorMessage ?? dictationError ?? "Network error", meta: retryMeta, }; } if (dictationStatus === "failed") { return { variant: "error", title: "Dictation failed", subtitle: dictationRetryInfo?.errorMessage ?? dictationError ?? "Unknown error", actionLabel: "Retry", onAction: handlePromptDictationRetry, onDismiss: handlePromptDictationDiscard, }; } if (dictationSuccessToastVisible) { return { variant: "success", title: "Transcribed", subtitle: "Inserted into prompt", }; } return null; }, [ connectionStatus.isConnected, dictationError, dictationMaxRetryAttempts, dictationRetryAttempt, dictationRetryInfo, dictationStatus, dictationSuccessToastVisible, handlePromptDictationDiscard, handlePromptDictationRetry, ]); 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) => { setSelectedServerId(serverId); }, []); 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 () => { providerModelRequestTimersRef.current.forEach((timer) => { clearTimeout(timer); }); providerModelRequestTimersRef.current.clear(); 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(() => { const activeServerIds = new Set(daemonEntries.map(({ daemon }) => daemon.id)); providerModelRequestTimersRef.current.forEach((timer, serverId) => { if (!activeServerIds.has(serverId)) { clearTimeout(timer); providerModelRequestTimersRef.current.delete(serverId); } }); 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 repoInfoStatus: "idle" | "loading" | "ready" | "error" = !shouldInspectRepo ? "idle" : repoAvailabilityError ? "error" : repoRequestStatus === "loading" ? "loading" : repoRequestStatus === "error" ? "error" : repoRequestStatus === "success" ? "ready" : "idle"; const repoInfoError = repoAvailabilityError ?? repoRequestError?.message ?? 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]); 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 ? { ...(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 (!shouldListenForDictation || !ws || !shouldShowAudioDebug) { return; } const unsubscribe = ws.on("transcription_result", (message) => { if (message.type !== "transcription_result") { return; } const pendingId = dictationRequestIdRef.current; if (!pendingId || message.payload.requestId !== pendingId) { return; } dictationRequestIdRef.current = null; setDictationDebugInfo({ requestId: pendingId, transcript: message.payload.text?.trim(), debugRecordingPath: message.payload.debugRecordingPath ?? undefined, format: message.payload.format, byteLength: message.payload.byteLength, duration: message.payload.duration, avgLogprob: message.payload.avgLogprob, isLowConfidence: message.payload.isLowConfidence, }); }); return () => { unsubscribe(); }; }, [shouldListenForDictation, shouldShowAudioDebug, 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 dictationAccessory = isCreateFlow ? ( ) : null; const gitBlockingError = useMemo(() => { 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, repoInfo, validateWorktreeName, worktreeSlug, ]); const workingDirIsEmpty = !workingDir.trim(); const promptIsEmpty = !initialPrompt.trim(); const createDisabled = workingDirIsEmpty || promptIsEmpty || Boolean(gitBlockingError) || isLoading || !isTargetDaemonReady; const handlePromptDesktopSubmitKeyPress = useCallback( (event: WebTextInputKeyPressEvent) => { if (!shouldHandlePromptDesktopSubmit) { return; } if (event.nativeEvent.key !== "Enter") { return; } const { shiftKey, metaKey, ctrlKey } = event.nativeEvent; if (shiftKey || metaKey || ctrlKey) { return; } if (createDisabled) { return; } event.preventDefault(); void handleCreate(); }, [createDisabled, handleCreate, shouldHandlePromptDesktopSubmit] ); 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 ( {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} { console.log('[INVESTIGATION] CreateModal onChange called', { textLength: text.length, }); setInitialPrompt(text); setErrorMessage(""); }} inputRef={promptInputRef} inputHeight={promptInputHeight} onContentSizeChange={handlePromptContentSizeChange} onDesktopSubmit={handlePromptDesktopSubmitKeyPress} autoFocus={shouldAutoFocusPrompt} scrollEnabled={promptInputHeight >= PROMPT_MAX_HEIGHT} accessory={dictationAccessory} /> {shouldShowAudioDebug && dictationDebugInfo ? ( setDictationDebugInfo(null)} title="Prompt Dictation Debug" /> ) : null} 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} recentPaths={recentPaths} 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 )} {promptDictationToast ? ( ) : null} ) : ( {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} /> )} )} ); } 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; } function ModalHeader({ paddingTop, paddingLeft, paddingRight, onClose, title, }: ModalHeaderProps): ReactElement { return ( {title} ); } interface DropdownFieldProps { label: string; value: string; placeholder: string; onPress: () => void; disabled?: boolean; errorMessage?: string | null; warningMessage?: string | null; helperText?: string | null; } function DropdownField({ label, value, placeholder, onPress, disabled, errorMessage, warningMessage, helperText, }: DropdownFieldProps): ReactElement { return ( {label} {value || placeholder} {errorMessage ? ( {errorMessage} ) : null} {warningMessage ? ( {warningMessage} ) : null} {!errorMessage && helperText ? ( {helperText} ) : null} ); } interface DropdownSheetProps { title: string; visible: boolean; onClose: () => void; children: ReactNode; } function DropdownSheet({ title, visible, onClose, children }: DropdownSheetProps): ReactElement { return ( {title} {children} ); } interface AssistantDropdownProps { providerDefinitions: AgentProviderDefinition[]; selectedProvider: AgentProvider; disabled: boolean; isOpen: boolean; onOpen: () => void; onClose: () => void; onSelect: (provider: AgentProvider) => void; } function AssistantDropdown({ providerDefinitions, selectedProvider, disabled, isOpen, onOpen, onClose, onSelect, }: AssistantDropdownProps): ReactElement { const selectedDefinition = providerDefinitions.find( (definition) => definition.id === selectedProvider ); return ( {providerDefinitions.map((definition) => { const isSelected = definition.id === selectedProvider; return ( { onSelect(definition.id); onClose(); }} > {definition.label} {definition.description ? ( {definition.description} ) : null} ); })} ); } interface PermissionsDropdownProps { modeOptions: AgentMode[]; selectedMode: string; disabled: boolean; isOpen: boolean; onOpen: () => void; onClose: () => void; onSelect: (modeId: string) => void; } function PermissionsDropdown({ modeOptions, selectedMode, disabled, isOpen, onOpen, onClose, onSelect, }: PermissionsDropdownProps): ReactElement { const hasOptions = modeOptions.length > 0; const selectedModeLabel = hasOptions ? modeOptions.find((mode) => mode.id === selectedMode)?.label ?? modeOptions[0]?.label ?? "Default" : "Automatic"; return ( {}} disabled={disabled || !hasOptions} helperText={ hasOptions ? undefined : "This assistant does not expose selectable permissions." } /> {hasOptions ? ( {modeOptions.map((mode) => { const isSelected = mode.id === selectedMode; return ( { onSelect(mode.id); onClose(); }} > {mode.label} {mode.description ? ( {mode.description} ) : null} ); })} ) : null} ); } interface ModelDropdownProps { models: AgentModelDefinition[]; selectedModel: string; isLoading: boolean; error: string | null; isOpen: boolean; onOpen: () => void; onClose: () => void; onSelect: (modelId: string) => void; onClear: () => void; onRefresh: () => void; } function ModelDropdown({ models, selectedModel, isLoading, error, isOpen, onOpen, onClose, onSelect, onClear, onRefresh, }: ModelDropdownProps): ReactElement { const selectedLabel = selectedModel ? models.find((model) => model.id === selectedModel)?.label ?? selectedModel : "Automatic"; const placeholder = isLoading && models.length === 0 ? "Loading..." : "Automatic"; const helperText = error ? undefined : isLoading ? "Fetching available models..." : models.length === 0 ? "This assistant did not expose selectable models." : undefined; return ( { onClear(); onClose(); }} > Automatic (provider default) Let the assistant pick the recommended model. {models.map((model) => { const isSelected = model.id === selectedModel; return ( { onSelect(model.id); onClose(); }} > {model.label} {model.description ? ( {model.description} ) : null} ); })} { onRefresh(); }} > Refresh models Request the latest catalog from the provider. {isLoading ? ( ) : null} ); } interface WorkingDirectoryDropdownProps { workingDir: string; errorMessage: string; isOpen: boolean; onOpen: () => void; onClose: () => void; disabled: boolean; recentPaths: string[]; onSelectPath: (value: string) => void; } function WorkingDirectoryDropdown({ workingDir, errorMessage, isOpen, onOpen, onClose, disabled, recentPaths, onSelectPath, }: WorkingDirectoryDropdownProps): ReactElement { const inputRef = useRef(null); const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { if (isOpen) { setSearchQuery(""); inputRef.current?.focus(); } }, [isOpen]); const normalizedSearch = searchQuery.trim().toLowerCase(); const filteredPaths = useMemo(() => { if (!normalizedSearch) { return recentPaths; } return recentPaths.filter((path) => path.toLowerCase().includes(normalizedSearch) ); }, [recentPaths, normalizedSearch]); const hasRecentPaths = recentPaths.length > 0; const hasMatches = filteredPaths.length > 0; const sanitizedSearchValue = searchQuery.trim(); const showCustomOption = sanitizedSearchValue.length > 0; const handleSelect = useCallback( (path: string) => { onSelectPath(path); onClose(); }, [onClose, onSelectPath] ); return ( {!hasRecentPaths && !showCustomOption ? ( We will remember the directories you use most often. ) : null} {showCustomOption ? ( handleSelect(sanitizedSearchValue)} > {`Use "${sanitizedSearchValue}"`} Launch the agent in this directory ) : null} {hasMatches ? ( {filteredPaths.map((path) => { const isActive = path === workingDir; return ( handleSelect(path)} > {path} ); })} ) : hasRecentPaths ? ( No recent paths match your search. ) : null} ); } interface PromptSectionProps { value: string; isLoading: boolean; onChange: (value: string) => void; inputRef: RefObject< TextInput | (TextInput & { getNativeRef?: () => unknown }) | null >; inputHeight: number; onContentSizeChange: ( event: NativeSyntheticEvent ) => void; onDesktopSubmit?: (event: WebTextInputKeyPressEvent) => void; autoFocus?: boolean; scrollEnabled: boolean; accessory?: ReactNode; } function PromptSection({ value, isLoading, onChange, inputRef, inputHeight, onContentSizeChange, onDesktopSubmit, autoFocus, scrollEnabled, accessory, }: PromptSectionProps): ReactElement { return ( Initial Prompt {accessory} ); } interface PromptDictationControlsProps { isRecording: boolean; isProcessing: boolean; disabled: boolean; volume: number; onStart: () => void; onCancel: () => void; onConfirm: () => void; status: DictationStatus; retryAttempt: number; maxRetryAttempts: number; retryCountdownMs?: number | null; errorMessage?: string | null; onRetry?: () => void; onDiscard?: () => void; } function PromptDictationControls({ isRecording, isProcessing, disabled, volume, onStart, onCancel, onConfirm, status, retryAttempt, maxRetryAttempts, retryCountdownMs, errorMessage, onRetry, onDiscard, }: PromptDictationControlsProps): ReactElement { const { theme } = useUnistyles(); const isRetrying = status === "retrying"; const isFailed = status === "failed"; const showActiveState = isRecording || isProcessing || isRetrying || isFailed; const cancelHandler = isFailed ? onDiscard ?? onCancel : onCancel; const confirmHandler = isFailed ? onRetry ?? onConfirm : onConfirm; if (!showActiveState) { return ( ); } return ( {isProcessing || isRetrying ? ( ) : isFailed ? ( ) : ( )} {(isRetrying || isFailed) && ( {isRetrying ? `Retrying ${Math.max(1, retryAttempt)} / ${Math.max(1, maxRetryAttempts)}${ retryCountdownMs && retryCountdownMs > 0 ? ` in ${Math.ceil(retryCountdownMs / 1000)}s` : "" }` : errorMessage ?? "Dictation failed"} )} ); } interface GitOptionsSectionProps { baseBranch: string; onBaseBranchChange: (value: string) => void; branches: Array<{ name: string; isCurrent: boolean }>; status: "idle" | "loading" | "ready" | "error"; repoError: 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; isBaseDropdownOpen: boolean; onToggleBaseDropdown: () => void; onCloseDropdown: () => void; } function GitOptionsSection({ baseBranch, onBaseBranchChange, branches, status, repoError, warning, createNewBranch, onToggleCreateNewBranch, branchName, onBranchNameChange, createWorktree, onToggleCreateWorktree, worktreeSlug, onWorktreeSlugChange, gitValidationError, 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); 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, }, closeButton: { padding: theme.spacing[2], borderRadius: theme.borderRadius.full, alignItems: "center", justifyContent: "center", }, scroll: { flex: 1, }, scrollContent: { paddingTop: theme.spacing[6], paddingBottom: theme.spacing[8], gap: theme.spacing[6], }, dictationToastPortal: { position: "absolute", left: theme.spacing[4], right: theme.spacing[4], bottom: theme.spacing[6], }, dictationNoticeWrapper: { marginTop: theme.spacing[3], }, formSection: { gap: theme.spacing[3], }, labelRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", 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], }, promptInput: { minHeight: theme.spacing[24], textAlignVertical: "top", outlineWidth: 0, outlineColor: "transparent", outlineStyle: "none", }, 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, }, dictationButton: { borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, borderRadius: theme.borderRadius.full, padding: theme.spacing[2], alignItems: "center", justifyContent: "center", }, dictationButtonDisabled: { opacity: theme.opacity[50], }, dictationActiveContainer: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, dictationMeterWrapper: { width: 48, alignItems: "center", justifyContent: "center", }, dictationActionGroup: { flexDirection: "row", gap: theme.spacing[2], }, dictationActionButton: { width: 32, height: 32, borderRadius: theme.borderRadius.full, alignItems: "center", justifyContent: "center", borderWidth: theme.borderWidth[1], }, dictationActionButtonCancel: { borderColor: theme.colors.border, backgroundColor: theme.colors.background, }, dictationActionButtonConfirm: { borderColor: theme.colors.foreground, backgroundColor: theme.colors.foreground, }, dictationActionButtonDisabled: { opacity: theme.opacity[40], }, dictationStatusLabel: { marginTop: theme.spacing[1], fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.medium, }, 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;