diff --git a/packages/app/src/app/agent/[serverId]/[agentId].tsx b/packages/app/src/app/agent/[serverId]/[agentId].tsx index 68b8027c6..99c984a77 100644 --- a/packages/app/src/app/agent/[serverId]/[agentId].tsx +++ b/packages/app/src/app/agent/[serverId]/[agentId].tsx @@ -29,7 +29,6 @@ import { GitBranch, Folder, RotateCcw, - Download, Users, ChevronRight, PlusIcon, @@ -39,7 +38,6 @@ import { MenuHeader } from "@/components/headers/menu-header"; import { BackHeader } from "@/components/headers/back-header"; import { AgentStreamView } from "@/components/agent-stream-view"; import { AgentInputArea } from "@/components/agent-input-area"; -import { ImportAgentModal } from "@/components/create-agent-modal"; import { ExplorerSidebar } from "@/components/explorer-sidebar"; import { FileDropZone } from "@/components/file-drop-zone"; import type { ImageAttachment } from "@/components/message-input"; @@ -184,7 +182,6 @@ function AgentScreenContent({ const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 }); const [menuContentHeight, setMenuContentHeight] = useState(0); const menuButtonRef = useRef(null); - const [showImportAgentModal, setShowImportAgentModal] = useState(false); const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null); const handleFilesDropped = useCallback((files: ImageAttachment[]) => { @@ -636,15 +633,6 @@ function AgentScreenContent({ router.push({ pathname: "/", params }); }, [agent, agentModel, handleCloseMenu, router, serverId]); - const handleImportAgent = useCallback(() => { - handleCloseMenu(); - setShowImportAgentModal(true); - }, [handleCloseMenu]); - - const handleCloseImportAgentModal = useCallback(() => { - setShowImportAgentModal(false); - }, []); - const handleNavigateToChildAgent = useCallback( (childAgentId: string) => { handleCloseMenu(); @@ -659,25 +647,14 @@ function AgentScreenContent({ [handleCloseMenu, router, serverId] ); - const importAgentModal = ( - - ); - if (!agent) { return ( - <> - - - - Agent not found - + + + + Agent not found - {importAgentModal} - + ); } @@ -879,10 +856,6 @@ function AgentScreenContent({ Browse Files - - - Import Agent - New Agent @@ -934,8 +907,6 @@ function AgentScreenContent({ {isMobile && resolvedAgentId && ( )} - - {importAgentModal} ); } diff --git a/packages/app/src/components/create-agent-modal.tsx b/packages/app/src/components/create-agent-modal.tsx index f54e31992..3cc2022a0 100644 --- a/packages/app/src/components/create-agent-modal.tsx +++ b/packages/app/src/components/create-agent-modal.tsx @@ -11,14 +11,11 @@ import { 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"; @@ -31,7 +28,7 @@ import Animated, { runOnJS, } from "react-native-reanimated"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; -import { Monitor, X } from "lucide-react-native"; +import { X } from "lucide-react-native"; import { theme as defaultTheme } from "@/styles/theme"; import { useRecentPaths } from "@/hooks/use-recent-paths"; import { useRouter } from "expo-router"; @@ -41,8 +38,6 @@ import { useDaemonConnections, type ConnectionStatus } from "@/contexts/daemon-c 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"; @@ -53,8 +48,8 @@ import type { UseWebSocketReturn } from "@/hooks/use-websocket"; import { useSessionStore, type Agent } from "@/stores/session-store"; import { AssistantDropdown, - DropdownField, DropdownSheet, + GitOptionsSection, ModelDropdown, PermissionsDropdown, WorkingDirectoryDropdown, @@ -67,7 +62,6 @@ import { interface AgentFlowModalProps { isVisible: boolean; onClose: () => void; - flow: "create" | "import"; initialValues?: CreateAgentInitialValues; serverId?: string | null; onAfterClose?: () => void; @@ -90,7 +84,6 @@ type CreateAgentSessionSlice = { worktreeName?: string; requestId?: string; }) => void; - resumeAgent: (options: { handle: any; overrides?: any; requestId?: string }) => void; sendAgentAudio: ( agentId: string | undefined, audioBlob: Blob, @@ -101,20 +94,8 @@ type CreateAgentSessionSlice = { }; 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" @@ -136,43 +117,9 @@ type GitRepoInfoResponseMessage = Extract< { 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, @@ -182,10 +129,7 @@ function AgentFlowModal({ 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(); @@ -223,7 +167,7 @@ function AgentFlowModal({ initialServerId, initialValues, isVisible, - isCreateFlow, + isCreateFlow: true, }); const sessionState = useSessionStore((state) => @@ -239,7 +183,6 @@ function AgentFlowModal({ serverId: selectedServerId, ws: sessionState.ws, createAgent: sessionState.methods.createAgent, - resumeAgent: sessionState.methods.resumeAgent, sendAgentAudio: sessionState.methods.sendAgentAudio, agents: sessionState.agents, }; @@ -299,7 +242,6 @@ function AgentFlowModal({ 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; @@ -377,11 +319,10 @@ function AgentFlowModal({ "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." + ? "Select a host before creating agents." : selectedDaemonIsOffline ? `${selectedDaemonLabel} is ${selectedDaemonStatusLabel}. We'll reconnect automatically and enable actions once it's online.${ selectedDaemonLastError ? ` ${selectedDaemonLastError}` : "" @@ -396,29 +337,14 @@ function AgentFlowModal({ 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 [useWorktree, setUseWorktree] = 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 hasPendingCreate = pendingRequestIdRef.current !== null; + const shouldListenForStatus = isVisible || hasPendingCreate; const idleProviderPrefetchHandleRef = useRef { setWorkingDirFromUser(value); setErrorMessage(""); - shouldSyncBaseBranchRef.current = true; }, [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) => { + (action: "create" | "dictation", reason?: string | null) => { trackAnalyticsEvent({ type: "offline_daemon_action_attempt", action, @@ -526,20 +385,13 @@ function AgentFlowModal({ const resetFormState = useCallback(() => { setInitialPrompt(""); - setBaseBranch(""); - setCreateNewBranch(false); - setBranchName(""); - setCreateWorktree(false); - setWorktreeSlug(""); - setBranchNameEdited(false); - setWorktreeSlugEdited(false); + setUseWorktree(false); setErrorMessage(""); setIsLoading(false); resetRepoInfo(); setOpenDropdown(null); pendingRequestIdRef.current = null; pendingNavigationServerIdRef.current = null; - shouldSyncBaseBranchRef.current = true; cancelRepoInfo(); }, [cancelRepoInfo, resetRepoInfo]); @@ -578,41 +430,6 @@ function AgentFlowModal({ 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( @@ -754,27 +571,6 @@ function AgentFlowModal({ 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?.(); @@ -802,7 +598,7 @@ function AgentFlowModal({ ]); const trimmedWorkingDir = workingDir.trim(); - const shouldInspectRepo = isCreateFlow && isVisible && trimmedWorkingDir.length > 0; + const shouldInspectRepo = isVisible && trimmedWorkingDir.length > 0; const repoAvailabilityError = shouldInspectRepo && (!isTargetDaemonReady || !isWsConnected) ? daemonAvailabilityError ?? "Repository details will load automatically once the selected host is back online." @@ -854,45 +650,32 @@ function AgentFlowModal({ ]); useEffect(() => { - if (!repoInfo) { - return; + if (isNonGitDirectory && useWorktree) { + setUseWorktree(false); } - setBaseBranch((prev) => { - if (shouldSyncBaseBranchRef.current || prev.trim().length === 0) { - shouldSyncBaseBranchRef.current = false; - return repoInfo.currentBranch ?? ""; - } - return prev; - }); - }, [repoInfo]); + }, [isNonGitDirectory, useWorktree]); - useEffect(() => { - if (!isNonGitDirectory) { - return; + const gitBlockingError = useMemo(() => { + if (!useWorktree || isNonGitDirectory) { + return null; } - 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; + const slug = slugifyWorktreeName(initialPrompt); + if (!slug) { + return null; } + const validation = validateWorktreeName(slug); + if (!validation.valid) { + return `Invalid worktree name: ${ + validation.error ?? "Must use lowercase letters, numbers, or hyphens" + }`; + } + return null; }, [ - baseBranch, - branchName, - createNewBranch, - createWorktree, + useWorktree, isNonGitDirectory, - worktreeSlug, + initialPrompt, + slugifyWorktreeName, + validateWorktreeName, ]); const handleCreate = useCallback(async () => { @@ -926,8 +709,6 @@ function AgentFlowModal({ return; } - const trimmedBaseBranch = baseBranch.trim(); - try { await addRecentPath(trimmedPath); } catch (error) { @@ -952,24 +733,11 @@ function AgentFlowModal({ ...(trimmedModel ? { model: trimmedModel } : {}), }; - const currentBranch = repoInfo?.currentBranch ?? ""; - const shouldIncludeBase = - createNewBranch || - createWorktree || - (trimmedBaseBranch.length > 0 && trimmedBaseBranch !== currentBranch); - - const gitOptions = shouldIncludeBase && !isNonGitDirectory + const worktreeSlug = slugifyWorktreeName(trimmedPrompt); + const gitOptions = useWorktree && !isNonGitDirectory && worktreeSlug ? { - ...(trimmedBaseBranch ? { baseBranch: trimmedBaseBranch } : {}), - ...(createNewBranch - ? { createNewBranch: true, newBranchName: branchName.trim() } - : {}), - ...(createWorktree - ? { - createWorktree: true, - worktreeSlug: (worktreeSlug || branchName).trim(), - } - : {}), + createWorktree: true, + worktreeSlug, } : undefined; @@ -990,117 +758,23 @@ function AgentFlowModal({ }, [ workingDir, initialPrompt, - baseBranch, - createNewBranch, - branchName, - createWorktree, - worktreeSlug, - repoInfo, + useWorktree, + slugifyWorktreeName, selectedMode, modeOptions, logOfflineDaemonAction, selectedProvider, isLoading, - validateWorktreeName, addRecentPath, createAgent, daemonAvailabilityError, isTargetDaemonReady, selectedDaemonId, + isNonGitDirectory, + gitBlockingError, + selectedModel, ]); - 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; @@ -1132,11 +806,7 @@ function AgentFlowModal({ return; } - if ( - (payload.status !== "agent_created" && - payload.status !== "agent_resumed") || - !payload.agentId - ) { + if (payload.status !== "agent_created" || !payload.agentId) { return; } @@ -1157,79 +827,7 @@ function AgentFlowModal({ }; }, [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 = @@ -1291,27 +889,9 @@ function AgentFlowModal({ paddingLeft={horizontalPaddingLeft} paddingRight={horizontalPaddingRight} onClose={handleClose} - title={modalTitle} - rightContent={ - isImportFlow ? ( - openDropdownSheet("host")} - > - - {hostBadgeLabel} - - - ) : undefined - } + title="Create New Agent" /> - {isCreateFlow ? ( - <> + <> - - 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} + + + {!isNonGitDirectory ? ( + + ) : 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} - /> - )} - - )} - - {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)} - - - ); - })} - - )} - @@ -1685,7 +1040,7 @@ function AgentFlowModal({ ); } -function LazyAgentFlowModal(props: Omit) { +function LazyCreateAgentModal(props: Omit) { const { isVisible } = props; const [shouldRender, setShouldRender] = useState(isVisible); @@ -1706,8 +1061,8 @@ function LazyAgentFlowModal(props: Omit) { return ; } -export function ImportAgentModal(props: ModalWrapperProps) { - return ; +export function CreateAgentModal(props: ModalWrapperProps) { + return ; } interface ModalHeaderProps { @@ -1740,241 +1095,6 @@ function ModalHeader({ ); } -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, @@ -2031,29 +1151,6 @@ const styles = StyleSheet.create(((theme: any) => ({ 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, }, @@ -2328,161 +1425,4 @@ const styles = StyleSheet.create(((theme: any) => ({ 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; diff --git a/packages/app/src/components/home-footer.tsx b/packages/app/src/components/home-footer.tsx index bddc6b5a7..a32a680b3 100644 --- a/packages/app/src/components/home-footer.tsx +++ b/packages/app/src/components/home-footer.tsx @@ -3,12 +3,11 @@ import { View, Pressable, Text, Platform, Modal, Alert } from "react-native"; import { useRouter } from "expo-router"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; -import { AudioLines, Users, Plus, Download } from "lucide-react-native"; +import { AudioLines, Users, Plus } from "lucide-react-native"; import { useRealtime } from "@/contexts/realtime-context"; import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { FOOTER_HEIGHT } from "@/constants/layout"; import { RealtimeControls } from "./realtime-controls"; -import { ImportAgentModal } from "./create-agent-modal"; import Animated, { FadeIn, FadeOut, @@ -22,7 +21,6 @@ export function HomeFooter() { const router = useRouter(); const { isRealtimeMode, startRealtime } = useRealtime(); const { connectionStates } = useDaemonConnections(); - const [showImportModal, setShowImportModal] = useState(false); const [showRealtimeHostPicker, setShowRealtimeHostPicker] = useState(false); // Guard Reanimated entry/exit transitions on Android to avoid ViewGroup.dispatchDraw crashes // tracked in react-native-reanimated#8422. @@ -141,25 +139,6 @@ export function HomeFooter() { Agents - { - setShowImportModal(true); - }} - style={({ pressed }) => [ - styles.footerButton, - pressed && styles.buttonPressed, - ]} - > - - - - Import - - { console.log("[HomeFooter] New Agent button pressed"); @@ -202,10 +181,6 @@ export function HomeFooter() { - setShowImportModal(false)} - /> void; - resumeAgent: (options: { handle: any; overrides?: any; requestId?: string }) => void; setAgentMode: (agentId: string, modeId: string) => void; respondToPermission: (agentId: string, requestId: string, response: any) => void; } @@ -1573,19 +1572,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid ws.send(msg); }, [encodeImages, ws]); - const resumeAgent = useCallback(({ handle, overrides, requestId }: { handle: any; overrides?: any; requestId?: string }) => { - const msg: WSInboundMessage = { - type: "session", - message: { - type: "resume_agent_request", - handle, - ...(overrides ? { overrides } : {}), - ...(requestId ? { requestId } : {}), - }, - }; - ws.send(msg); - }, [ws]); - const setAgentMode = useCallback((agentId: string, modeId: string) => { const msg: WSInboundMessage = { type: "session", @@ -1816,7 +1802,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid sendAgentMessage, sendAgentAudio, createAgent, - resumeAgent, setAgentMode, respondToPermission, }), @@ -1840,7 +1825,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid sendAgentMessage, sendAgentAudio, createAgent, - resumeAgent, setAgentMode, respondToPermission, ] @@ -1866,7 +1850,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid sendAgentAudio, deleteAgent, createAgent, - resumeAgent, setAgentMode, respondToPermission, }), [ @@ -1886,7 +1869,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid sendAgentAudio, deleteAgent, createAgent, - resumeAgent, setAgentMode, respondToPermission, ]); diff --git a/packages/app/src/lib/send-rpc-request.ts b/packages/app/src/lib/send-rpc-request.ts index 8deeb5a49..a4d6d471f 100644 --- a/packages/app/src/lib/send-rpc-request.ts +++ b/packages/app/src/lib/send-rpc-request.ts @@ -105,7 +105,6 @@ const RESPONSE_TYPE_MAP: Record = { git_repo_info_request: "git_repo_info_response", list_provider_models_request: "list_provider_models_response", list_conversations_request: "list_conversations_response", - list_persisted_agents_request: "list_persisted_agents_response", create_agent_request: "agent_state", refresh_agent_request: "agent_state", initialize_agent_request: "initialize_agent_request", diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 2fb55ee41..93fb08822 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -204,7 +204,6 @@ export interface SessionState { worktreeName?: string; requestId?: string; }) => Promise; - resumeAgent: (options: { handle: any; overrides?: any; requestId?: string }) => void; setAgentMode: (agentId: string, modeId: string) => void; respondToPermission: (agentId: string, requestId: string, response: any) => void; } | null; diff --git a/packages/server/src/server/messages.ts b/packages/server/src/server/messages.ts index 0ef52f699..4d66780a2 100644 --- a/packages/server/src/server/messages.ts +++ b/packages/server/src/server/messages.ts @@ -407,12 +407,6 @@ export const RestartServerRequestMessageSchema = z.object({ reason: z.string().optional(), }); -export const ListPersistedAgentsRequestMessageSchema = z.object({ - type: z.literal("list_persisted_agents_request"), - provider: AgentProviderSchema.optional(), - limit: z.number().int().positive().optional(), -}); - export const InitializeAgentRequestMessageSchema = z.object({ type: z.literal("initialize_agent_request"), agentId: z.string(), @@ -557,7 +551,6 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ HighlightedDiffRequestSchema, FileExplorerRequestSchema, FileDownloadTokenRequestSchema, - ListPersistedAgentsRequestMessageSchema, GitRepoInfoRequestMessageSchema, ClearAgentAttentionMessageSchema, ]); @@ -750,25 +743,6 @@ export const AgentDeletedMessageSchema = z.object({ }), }); -const PersistedAgentDescriptorPayloadSchema = z.object({ - provider: AgentProviderSchema, - sessionId: z.string(), - cwd: z.string(), - title: z.string(), - lastActivityAt: z.string(), - persistence: AgentPersistenceHandleSchema, - timeline: z.array(AgentTimelineItemPayloadSchema), -}); - -export type PersistedAgentDescriptorPayload = z.infer; - -export const ListPersistedAgentsResponseSchema = z.object({ - type: z.literal("list_persisted_agents_response"), - payload: z.object({ - items: z.array(PersistedAgentDescriptorPayloadSchema), - }), -}); - export const GitDiffResponseSchema = z.object({ type: z.literal("git_diff_response"), payload: z.object({ @@ -863,7 +837,6 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ AgentPermissionRequestMessageSchema, AgentPermissionResolvedMessageSchema, AgentDeletedMessageSchema, - ListPersistedAgentsResponseSchema, GitDiffResponseSchema, HighlightedDiffResponseSchema, FileExplorerResponseSchema, @@ -896,7 +869,6 @@ export type DeleteConversationResponseMessage = z.infer; export type AgentPermissionResolvedMessage = z.infer; export type AgentDeletedMessage = z.infer; -export type ListPersistedAgentsResponseMessage = z.infer; export type ListProviderModelsResponseMessage = z.infer< typeof ListProviderModelsResponseMessageSchema >; @@ -916,7 +888,6 @@ export type ListProviderModelsRequestMessage = z.infer< >; export type ResumeAgentRequestMessage = z.infer; export type DeleteAgentRequestMessage = z.infer; -export type ListPersistedAgentsRequestMessage = z.infer; export type InitializeAgentRequestMessage = z.infer; export type SetAgentModeMessage = z.infer; export type AgentPermissionResponseMessage = z.infer; diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index c456d51b2..5fcccf571 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -856,10 +856,6 @@ export class Session { await this.handleFileDownloadTokenRequest(msg); break; - case "list_persisted_agents_request": - await this.handleListPersistedAgentsRequest(msg); - break; - case "git_repo_info_request": await this.handleGitRepoInfoRequest(msg); break; @@ -1550,9 +1546,18 @@ export class Session { } if (normalized.createWorktree) { - const targetBranch = normalized.createNewBranch - ? normalized.newBranchName - : normalized.baseBranch; + let targetBranch: string; + + if (normalized.createNewBranch) { + targetBranch = normalized.newBranchName!; + } else { + // Resolve current branch name from HEAD + const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { + cwd, + env: READ_ONLY_GIT_ENV, + }); + targetBranch = stdout.trim(); + } if (!targetBranch) { throw new Error( @@ -1730,12 +1735,6 @@ export class Session { } } - if (createWorktree && !createNewBranch && !baseBranch) { - throw new Error( - "Base branch is required when creating a worktree without a new branch" - ); - } - return { baseBranch, createNewBranch, @@ -1836,48 +1835,6 @@ export class Session { } } - private async handleListPersistedAgentsRequest( - msg: Extract - ): Promise { - const { provider, limit } = msg; - try { - const entries = await this.agentManager.listPersistedAgents({ - provider, - limit, - }); - this.emit({ - type: "list_persisted_agents_response", - payload: { - items: entries.map((entry) => ({ - provider: entry.provider, - sessionId: entry.sessionId, - cwd: entry.cwd, - title: entry.title ?? `Session ${entry.sessionId.slice(0, 8)}`, - lastActivityAt: entry.lastActivityAt.toISOString(), - persistence: entry.persistence, - timeline: entry.timeline ?? [], - })), - }, - }); - } catch (error) { - console.error( - `[Session ${this.clientId}] Failed to list persisted agents:`, - error - ); - this.emit({ - type: "activity_log", - payload: { - id: uuidv4(), - timestamp: new Date(), - type: "error", - content: `Failed to list saved agents: ${ - (error as Error)?.message ?? error - }`, - }, - }); - } - } - /** * Handle set agent mode request */ diff --git a/packages/server/src/server/test-utils/daemon-client.ts b/packages/server/src/server/test-utils/daemon-client.ts index c706d3183..6cc20a095 100644 --- a/packages/server/src/server/test-utils/daemon-client.ts +++ b/packages/server/src/server/test-utils/daemon-client.ts @@ -5,7 +5,6 @@ import type { SessionOutboundMessage, AgentSnapshotPayload, AgentStreamEventPayload, - PersistedAgentDescriptorPayload, } from "../messages.js"; import type { AgentModelDefinition, @@ -296,16 +295,6 @@ export class DaemonClient { ); } - async listPersistedAgents(): Promise { - this.send({ type: "list_persisted_agents_request" }); - return this.waitFor((msg) => { - if (msg.type === "list_persisted_agents_response") { - return msg.payload.items; - } - return null; - }); - } - async resumeAgent( handle: AgentPersistenceHandle, overrides?: Partial