import type { DaemonClient } from "@server/client/daemon-client"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActivityIndicator, Text, View } from "react-native"; import ReanimatedAnimated from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import invariant from "tiny-invariant"; import { shallow, useShallow } from "zustand/shallow"; import { useStoreWithEqualityFn } from "zustand/traditional"; import { AgentStreamView, type AgentStreamViewHandle } from "@/components/agent-stream-view"; import { ArchivedAgentCallout } from "@/components/archived-agent-callout"; import { Composer } from "@/components/composer"; import { FileDropZone } from "@/components/file-drop-zone"; import type { ImageAttachment } from "@/components/message-input"; import { getProviderIcon } from "@/components/provider-icons"; import { ToastViewport, useToastHost } from "@/components/toast-host"; import type { WorkspaceComposerAttachment } from "@/attachments/types"; import { useWorkspaceAttachments, useWorkspaceAttachmentScopeKey, } from "@/attachments/workspace-attachments-store"; import { useIsCompactFormFactor } from "@/constants/layout"; import { isNative, isWeb } from "@/constants/platform"; import { useAgentAttentionClear } from "@/hooks/use-agent-attention-clear"; import { useAgentInitialization } from "@/hooks/use-agent-initialization"; import { useAgentInputDraft } from "@/hooks/use-agent-input-draft"; import { type AgentScreenAgent, type AgentScreenMissingState, type AgentScreenViewState, useAgentScreenStateMachine, } from "@/hooks/use-agent-screen-state-machine"; import { useArchiveAgent } from "@/hooks/use-archive-agent"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { useStableEvent } from "@/hooks/use-stable-event"; import { usePaneContext, usePaneFocus } from "@/panels/pane-context"; import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry"; import { type HostRuntimeConnectionStatus, useHostRuntimeClient, useHostRuntimeConnectionStatus, useHostRuntimeIsConnected, useHostRuntimeLastError, useHosts, } from "@/runtime/host-runtime"; import { deriveRouteBottomAnchorIntent, deriveRouteBottomAnchorRequest, } from "@/screens/agent/agent-ready-screen-bottom-anchor"; import { useCreateFlowStore } from "@/stores/create-flow-store"; import { buildDraftStoreKey } from "@/stores/draft-keys"; import { usePanelStore } from "@/stores/panel-store"; import { type Agent, useSessionStore } from "@/stores/session-store"; import type { Theme } from "@/styles/theme"; import { SubagentsSection, useArchiveSubagent, useSubagentsForParent } from "@/subagents"; import type { PendingPermission } from "@/types/shared"; import type { StreamItem } from "@/types/stream"; import { getInitDeferred, getInitKey } from "@/utils/agent-initialization"; import { derivePendingPermissionKey, normalizeAgentSnapshot } from "@/utils/agent-snapshots"; import { mergePendingCreateImages } from "@/utils/pending-create-images"; import { navigateToAgent } from "@/utils/navigate-to-agent"; import { deriveSidebarStateBucket } from "@/utils/sidebar-agent-state"; interface ChatAgentStateShape { serverId: string | null; id: string | null; status: Agent["status"] | null; cwd: string | null; lastError?: Agent["lastError"] | null; } interface ChatAgentSelectedState extends ChatAgentStateShape { archivedAt: Date | null; requiresAttention: boolean; attentionReason: Agent["attentionReason"] | null; } function resolveChatAgentFromSession( state: ReturnType, serverId: string, agentId: string | undefined, ): Agent | null { if (!agentId) return null; const session = state.sessions[serverId]; return session?.agents?.get(agentId) ?? session?.agentDetails?.get(agentId) ?? null; } const EMPTY_CHAT_AGENT_STATE: ChatAgentSelectedState = { serverId: null, id: null, status: null, cwd: null, lastError: null, archivedAt: null, requiresAttention: false, attentionReason: null, }; function selectChatAgentState( state: ReturnType, serverId: string, agentId: string | undefined, ): ChatAgentSelectedState { const agent = resolveChatAgentFromSession(state, serverId, agentId); if (!agent) return EMPTY_CHAT_AGENT_STATE; return { serverId: agent.serverId, id: agent.id, status: agent.status, cwd: agent.cwd, lastError: agent.lastError ?? null, archivedAt: agent.archivedAt ?? null, requiresAttention: agent.requiresAttention ?? false, attentionReason: agent.attentionReason ?? null, }; } function buildChatAgentFromState( state: ChatAgentStateShape, projectPlacement: Agent["projectPlacement"] | null, ): AgentScreenAgent | null { if (!state.serverId || !state.id || !state.status || !state.cwd) { return null; } return { serverId: state.serverId, id: state.id, status: state.status, cwd: state.cwd, lastError: state.lastError ?? null, projectPlacement, }; } function renderChatAgentNonReadyView(args: { viewState: AgentScreenViewState; effectiveAgent: AgentScreenAgent | null; }): React.ReactElement | null { const { viewState, effectiveAgent } = args; if (viewState.tag === "not_found") { return ( Agent not found ); } if (viewState.tag === "error") { return ( Failed to load agent {viewState.message} ); } if (viewState.tag === "boot" || !effectiveAgent) { return ( ); } return null; } function formatProviderLabel(provider: Agent["provider"]): string { if (!provider) { return "Agent"; } return provider .split(/[-_\s]+/) .filter((part) => part.length > 0) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function resolveWorkspaceAgentTabLabel(title: string | null | undefined): string | null { if (typeof title !== "string") { return null; } const normalized = title.trim(); if (!normalized) { return null; } if (normalized.toLowerCase() === "new agent") { return null; } return normalized; } function shouldStoreFetchedAgentInActiveDirectory(agent: Agent): boolean { return !agent.archivedAt && Boolean(agent.projectPlacement); } type FetchAgentResult = Awaited>; function storeFetchedAgentDetail(input: { serverId: string; result: NonNullable; }): Agent { const normalized = normalizeAgentSnapshot(input.result.agent, input.serverId); const hydrated: Agent = { ...normalized, projectPlacement: input.result.project, }; const store = useSessionStore.getState(); if (shouldStoreFetchedAgentInActiveDirectory(hydrated)) { store.setAgents(input.serverId, (previous) => { const next = new Map(previous); next.set(hydrated.id, hydrated); return next; }); } else { store.setAgentDetails(input.serverId, (previous) => { const next = new Map(previous); next.set(hydrated.id, hydrated); return next; }); } store.setPendingPermissions(input.serverId, (previous) => { const next = new Map(previous); for (const [key, pending] of next.entries()) { if (pending.agentId === hydrated.id) { next.delete(key); } } for (const request of hydrated.pendingPermissions) { const key = derivePendingPermissionKey(hydrated.id, request); next.set(key, { key, agentId: hydrated.id, request }); } return next; }); return hydrated; } function useAgentPanelDescriptor( target: { kind: "agent"; agentId: string }, context: { serverId: string }, ): PanelDescriptor { const descriptorState = useSessionStore( useShallow((state) => { const session = state.sessions[context.serverId]; const agent = session?.agents?.get(target.agentId) ?? session?.agentDetails?.get(target.agentId) ?? null; return { provider: agent?.provider ?? "codex", title: agent?.title ?? null, status: agent?.status ?? null, pendingPermissionCount: agent?.pendingPermissions.length ?? 0, requiresAttention: agent?.requiresAttention ?? false, attentionReason: agent?.attentionReason ?? null, }; }), ); const provider = descriptorState.provider; const label = resolveWorkspaceAgentTabLabel(descriptorState.title); const icon = getProviderIcon(provider); return { label: label ?? "", subtitle: `${formatProviderLabel(provider)} agent`, titleState: label ? "ready" : "loading", icon, statusBucket: descriptorState.status ? deriveSidebarStateBucket({ status: descriptorState.status, pendingPermissionCount: descriptorState.pendingPermissionCount, requiresAttention: descriptorState.requiresAttention, attentionReason: descriptorState.attentionReason, }) : null, }; } function AgentPanel() { const { serverId, target, openFileInWorkspace } = usePaneContext(); const { isInteractive } = usePaneFocus(); invariant(target.kind === "agent", "AgentPanel requires agent target"); function openWorkspaceFile(input: { filePath: string }) { openFileInWorkspace(input.filePath); } const handleOpenWorkspaceFile = useStableEvent(openWorkspaceFile); return ( ); } export const agentPanelRegistration: PanelRegistration<"agent"> = { kind: "agent", component: AgentPanel, useDescriptor: useAgentPanelDescriptor, }; const EMPTY_STREAM_ITEMS: StreamItem[] = []; const EMPTY_PENDING_PERMISSIONS = new Map(); const EMPTY_PENDING_PERMISSION_LIST: PendingPermission[] = []; type RouteBottomAnchorRequest = ReturnType; type PendingCreateByDraftId = ReturnType["pendingByDraftId"]; type PendingCreateAttempt = PendingCreateByDraftId[string]; function toErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } return String(error); } function isNotFoundErrorMessage(message: string): boolean { return /agent not found|not found/i.test(message); } function findPendingCreateForPanel(input: { pendingByDraftId: PendingCreateByDraftId; serverId: string; agentId?: string; }): PendingCreateAttempt | null { if (!input.agentId) { return null; } const values = Object.values(input.pendingByDraftId); for (const entry of values) { if ( entry.lifecycle === "active" && entry.serverId === input.serverId && entry.agentId === input.agentId ) { return entry; } } return null; } type AgentLookupState = | { tag: "idle" } | { tag: "loading" } | { tag: "not_found"; message: string } | { tag: "error"; message: string }; function AgentPanelContent({ serverId, agentId, isPaneFocused, onOpenWorkspaceFile, }: { serverId: string; agentId: string; isPaneFocused: boolean; onOpenWorkspaceFile?: (input: { filePath: string }) => void; }) { const resolvedAgentId = agentId.trim() || undefined; const resolvedServerId = serverId.trim() || undefined; const daemons = useHosts(); const runtimeServerId = resolvedServerId ?? ""; const runtimeClient = useHostRuntimeClient(runtimeServerId); const runtimeIsConnected = useHostRuntimeIsConnected(runtimeServerId); const runtimeConnectionStatus = useHostRuntimeConnectionStatus(runtimeServerId); const runtimeLastError = useHostRuntimeLastError(runtimeServerId); const connectionServerId = resolvedServerId ?? null; const daemon = connectionServerId ? (daemons.find((entry) => entry.serverId === connectionServerId) ?? null) : null; const serverLabel = daemon?.label ?? connectionServerId ?? "Selected host"; const isUnknownDaemon = Boolean(connectionServerId && !daemon); const connectionStatus: HostRuntimeConnectionStatus = isUnknownDaemon && runtimeConnectionStatus === "connecting" ? "offline" : runtimeConnectionStatus; const lastConnectionError = runtimeLastError; if (!resolvedServerId || !runtimeClient) { return ( ); } return ( ); } function AgentPanelBody({ serverId, agentId, isPaneFocused, client, isConnected, connectionStatus, onOpenWorkspaceFile, }: { serverId: string; agentId?: string; isPaneFocused: boolean; client: NonNullable>; isConnected: boolean; connectionStatus: HostRuntimeConnectionStatus; onOpenWorkspaceFile?: (input: { filePath: string }) => void; }) { const { isArchivingAgent: _isArchivingAgent } = useArchiveAgent(); const hasSession = useSessionStore((state) => Boolean(state.sessions[serverId])); const projectPlacement = useStoreWithEqualityFn( useSessionStore, (state) => { if (!agentId) { return null; } const session = state.sessions[serverId]; return ( session?.agents?.get(agentId)?.projectPlacement ?? session?.agentDetails?.get(agentId)?.projectPlacement ?? null ); }, (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b), ); const agentState = useSessionStore( useShallow((state) => { const session = state.sessions[serverId]; const agent = agentId ? (session?.agents?.get(agentId) ?? session?.agentDetails?.get(agentId) ?? null) : null; return { serverId: agent?.serverId ?? null, id: agent?.id ?? null, status: agent?.status ?? null, cwd: agent?.cwd ?? null, lastError: agent?.lastError ?? null, archivedAt: agent?.archivedAt ?? null, }; }), ); const [lookupState, setLookupState] = useState({ tag: "idle" }); const lookupAttemptTokenRef = useRef(0); useEffect(() => { lookupAttemptTokenRef.current += 1; setLookupState({ tag: "idle" }); }, [agentId, serverId]); useEffect(() => { if (!agentId) { return; } if (agentState.id) { if (lookupState.tag !== "idle") { setLookupState({ tag: "idle" }); } return; } if (!isConnected || !hasSession) { return; } if (lookupState.tag === "loading" || lookupState.tag === "not_found") { return; } setLookupState({ tag: "loading" }); const attemptToken = ++lookupAttemptTokenRef.current; client .fetchAgent(agentId) .then((result) => { if (attemptToken !== lookupAttemptTokenRef.current) { return; } if (!result) { setLookupState({ tag: "not_found", message: `Agent not found: ${agentId}`, }); return; } storeFetchedAgentDetail({ serverId, result }); setLookupState({ tag: "idle" }); return; }) .catch((error) => { if (attemptToken !== lookupAttemptTokenRef.current) { return; } const message = toErrorMessage(error); if (isNotFoundErrorMessage(message)) { setLookupState({ tag: "not_found", message }); return; } setLookupState({ tag: "error", message }); }); }, [agentId, agentState.id, client, hasSession, isConnected, lookupState.tag, serverId]); if (lookupState.tag === "not_found") { return ( Agent not found ); } if (lookupState.tag === "error") { return ( Failed to load agent {lookupState.message} ); } const agent: AgentScreenAgent | null = agentState.serverId && agentState.id && agentState.status && agentState.cwd ? { serverId: agentState.serverId, id: agentState.id, status: agentState.status, cwd: agentState.cwd, lastError: agentState.lastError ?? null, projectPlacement, } : null; if (!agent) { return ( ); } return ( ); } function ChatAgentContent({ serverId, agentId, isPaneFocused, client, isConnected, connectionStatus, onOpenWorkspaceFile, }: { serverId: string; agentId?: string; isPaneFocused: boolean; client: NonNullable>; isConnected: boolean; connectionStatus: HostRuntimeConnectionStatus; onOpenWorkspaceFile?: (input: { filePath: string }) => void; }) { const panelToast = useToastHost(); const { isArchivingAgent } = useArchiveAgent(); const streamViewRef = useRef(null); const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null); const clearOnAgentBlurRef = useRef<() => void>(() => {}); const wasPaneFocusedRef = useRef(isPaneFocused); const reconnectToastArmedRef = useRef(false); const initAttemptTokenRef = useRef(0); const routeBottomAnchorRequestRef = useRef<{ routeKey: string; reason: "initial-entry" | "resume"; } | null>(null); const handleFilesDropped = useCallback((files: ImageAttachment[]) => { addImagesRef.current?.(files); }, []); const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => { addImagesRef.current = addImages; }, []); const agentState = useSessionStore( useShallow((state) => selectChatAgentState(state, serverId, agentId)), ); const projectPlacement = useStoreWithEqualityFn( useSessionStore, (state) => { if (!agentId) { return null; } const session = state.sessions[serverId]; return ( session?.agents?.get(agentId)?.projectPlacement ?? session?.agentDetails?.get(agentId)?.projectPlacement ?? null ); }, (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b), ); const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId); const isInitializingFromMap = useSessionStore((state) => agentId ? (state.sessions[serverId]?.initializingAgents?.get(agentId) ?? false) : false, ); const historySyncGeneration = useSessionStore( (state) => state.sessions[serverId]?.historySyncGeneration ?? 0, ); const hasAppliedAuthoritativeHistory = useSessionStore((state) => agentId ? state.sessions[serverId]?.agentAuthoritativeHistoryApplied?.get(agentId) === true : false, ); const agentHistorySyncGeneration = useSessionStore((state) => agentId ? (state.sessions[serverId]?.agentHistorySyncGeneration?.get(agentId) ?? -1) : -1, ); const hasSession = useSessionStore((state) => Boolean(state.sessions[serverId])); const { ensureAgentIsInitialized } = useAgentInitialization({ serverId, client: hasSession ? client : null, }); const [missingAgentState, setMissingAgentState] = useState({ kind: "idle", }); const pendingCreate = useMemo( () => findPendingCreateForPanel({ pendingByDraftId, serverId, agentId }), [agentId, pendingByDraftId, serverId], ); const isPendingCreateForPanel = Boolean(pendingCreate); const hasHydratedHistoryBefore = hasAppliedAuthoritativeHistory; const attentionController = useAgentAttentionClear({ agentId, client, isConnected, requiresAttention: agentState.requiresAttention, attentionReason: agentState.attentionReason, isScreenFocused: isPaneFocused, }); useEffect(() => { clearOnAgentBlurRef.current = attentionController.clearOnAgentBlur; }, [attentionController.clearOnAgentBlur]); const { style: animatedKeyboardStyle } = useKeyboardShiftStyle({ mode: "translate", }); const handleHistorySyncFailure = useCallback( ({ origin, error }: { origin: "focus" | "entry"; error: unknown }) => { if (agentId) { console.warn("[AgentPanel] history sync failed", { origin, agentId, error, }); } const message = toErrorMessage(error); setMissingAgentState((previous) => { if (previous.kind === "error" && previous.message === message) { return previous; } return { kind: "error", message }; }); }, [agentId], ); const ensureInitializedWithSyncErrorHandling = useCallback( (origin: "focus" | "entry") => { if (!agentId) { return; } ensureAgentIsInitialized(agentId).catch((error) => { handleHistorySyncFailure({ origin, error }); }); }, [agentId, ensureAgentIsInitialized, handleHistorySyncFailure], ); useEffect(() => { if (connectionStatus === "online") { if (reconnectToastArmedRef.current) { reconnectToastArmedRef.current = false; panelToast.dismiss(); } return; } if (connectionStatus === "idle") { return; } if (!reconnectToastArmedRef.current) { reconnectToastArmedRef.current = true; panelToast.api.show("Reconnecting...", { durationMs: null, testID: "agent-reconnecting-toast", }); } }, [connectionStatus, panelToast]); useEffect(() => { if (!isPaneFocused || !agentId || !isConnected || !hasSession) { return; } ensureInitializedWithSyncErrorHandling("focus"); }, [agentId, ensureInitializedWithSyncErrorHandling, hasSession, isConnected, isPaneFocused]); const isArchivingCurrentAgent = Boolean(agentId && isArchivingAgent({ serverId, agentId })); useEffect(() => { if (wasPaneFocusedRef.current && !isPaneFocused) { clearOnAgentBlurRef.current(); } wasPaneFocusedRef.current = isPaneFocused; }, [isPaneFocused]); useEffect(() => { return () => { if (wasPaneFocusedRef.current) { clearOnAgentBlurRef.current(); } }; }, []); const isInitializing = agentId ? isInitializingFromMap : false; const isHistorySyncing = useMemo(() => { if (!agentId || !isInitializing) { return false; } const initKey = getInitKey(serverId, agentId); return Boolean(getInitDeferred(initKey)); }, [agentId, isInitializing, serverId]); const needsAuthoritativeSync = useMemo(() => { if (!agentId) { return false; } return agentHistorySyncGeneration < historySyncGeneration; }, [agentHistorySyncGeneration, agentId, historySyncGeneration]); const shouldUseOptimisticStream = isPendingCreateForPanel; const authoritativeStatus = agentState.status; const isAuthoritativeBootstrapping = authoritativeStatus === "initializing" || authoritativeStatus === "idle"; const showPendingCreateSubmitLoading = isPendingCreateForPanel && (!authoritativeStatus || isAuthoritativeBootstrapping); const canFinalizePendingCreate = Boolean(authoritativeStatus) && !isAuthoritativeBootstrapping; const agent = useMemo( () => buildChatAgentFromState(agentState, projectPlacement), [agentState, projectPlacement], ); const placeholderAgent: AgentScreenAgent | null = useMemo(() => { if (!shouldUseOptimisticStream || !agentId) { return null; } return { serverId, id: agentId, status: "running", cwd: ".", projectPlacement: null, }; }, [agentId, serverId, shouldUseOptimisticStream]); const viewState = useAgentScreenStateMachine({ routeKey: `${serverId}:${agentId ?? ""}`, input: { agent: agent ?? null, placeholderAgent, missingAgentState, isConnected, isArchivingCurrentAgent, isHistorySyncing, needsAuthoritativeSync, shouldUseOptimisticStream, hasHydratedHistoryBefore, }, }); const effectiveAgent = viewState.tag === "ready" ? viewState.agent : null; const routeEntryKey = agentId ? `${serverId}:${agentId}` : null; routeBottomAnchorRequestRef.current = deriveRouteBottomAnchorIntent({ cachedIntent: routeBottomAnchorRequestRef.current, routeKey: routeEntryKey, hasAppliedAuthoritativeHistoryAtEntry: hasAppliedAuthoritativeHistory, }); const routeBottomAnchorRequest = useMemo( () => deriveRouteBottomAnchorRequest({ intent: routeBottomAnchorRequestRef.current, effectiveAgentId: effectiveAgent?.id ?? null, }), [effectiveAgent?.id], ); const handleComposerHeightChange = useCallback( (_height: number) => { if (!agentId) { return; } streamViewRef.current?.prepareForViewportChange(); }, [agentId], ); const handleMessageSent = useCallback(() => { if (!agentId) { return; } streamViewRef.current?.scrollToBottom("message-sent"); }, [agentId]); useEffect(() => { if (!agentId) { return; } if (!isConnected || !hasSession) { return; } const shouldSyncOnEntry = needsAuthoritativeSync || isNative; if (!shouldSyncOnEntry) { return; } ensureInitializedWithSyncErrorHandling("entry"); }, [ agentId, ensureInitializedWithSyncErrorHandling, hasSession, isConnected, needsAuthoritativeSync, ]); useEffect(() => { initAttemptTokenRef.current += 1; setMissingAgentState({ kind: "idle" }); }, [agentId, serverId]); useEffect(() => { if (!agentId) { return; } if (agentState.id || shouldUseOptimisticStream) { if (missingAgentState.kind !== "idle") { setMissingAgentState({ kind: "idle" }); } return; } if (!isConnected || !hasSession) { return; } if (missingAgentState.kind === "resolving" || missingAgentState.kind === "not_found") { return; } setMissingAgentState({ kind: "resolving" }); const attemptToken = ++initAttemptTokenRef.current; ensureAgentIsInitialized(agentId) .then(async () => { if (attemptToken !== initAttemptTokenRef.current) { return; } const currentSession = useSessionStore.getState().sessions[serverId]; const currentAgent = currentSession?.agents.get(agentId) ?? currentSession?.agentDetails.get(agentId); if (!currentAgent) { const result = await client.fetchAgent(agentId); if (attemptToken !== initAttemptTokenRef.current) { return; } if (!result) { setMissingAgentState({ kind: "not_found", message: `Agent not found: ${agentId}`, }); return; } storeFetchedAgentDetail({ serverId, result }); } if (attemptToken !== initAttemptTokenRef.current) { return; } setMissingAgentState({ kind: "idle" }); return; }) .catch((error) => { if (attemptToken !== initAttemptTokenRef.current) { return; } const message = toErrorMessage(error); if (isNotFoundErrorMessage(message)) { setMissingAgentState({ kind: "not_found", message }); return; } setMissingAgentState({ kind: "error", message }); }); }, [ agentState.id, agentId, client, ensureAgentIsInitialized, hasSession, isConnected, missingAgentState.kind, serverId, shouldUseOptimisticStream, ]); const animatedContentStyle = useMemo( () => [styles.content, animatedKeyboardStyle], [animatedKeyboardStyle], ); const nonReadyView = renderChatAgentNonReadyView({ viewState, effectiveAgent, }); if (nonReadyView) return nonReadyView; invariant(effectiveAgent, "effectiveAgent is defined when the non-ready view is absent"); invariant(agentState.cwd, "agent cwd is defined when agent content is ready"); return ( {viewState.tag === "ready" && viewState.sync.status === "catching_up" && viewState.sync.ui === "overlay" ? ( ) : null} {isArchivingCurrentAgent ? ( Archiving agent... Please wait while we archive this agent. ) : null} ); } function AgentStreamSection({ streamViewRef, serverId, agentId, agent, pendingCreate, shouldUseOptimisticStream, canFinalizePendingCreate, routeBottomAnchorRequest, hasAppliedAuthoritativeHistory, toast, onOpenWorkspaceFile, }: { streamViewRef: React.RefObject; serverId: string; agentId?: string; agent: AgentScreenAgent; pendingCreate: PendingCreateAttempt | null; shouldUseOptimisticStream: boolean; canFinalizePendingCreate: boolean; routeBottomAnchorRequest: RouteBottomAnchorRequest; hasAppliedAuthoritativeHistory: boolean; toast: ReturnType["api"]; onOpenWorkspaceFile?: (input: { filePath: string }) => void; }) { const streamItemsRaw = useSessionStore((state) => agentId ? state.sessions[serverId]?.agentStreamTail?.get(agentId) : undefined, ); const streamItems = streamItemsRaw ?? EMPTY_STREAM_ITEMS; const pendingPermissionList = useStoreWithEqualityFn( useSessionStore, (state) => { if (!agentId) { return EMPTY_PENDING_PERMISSION_LIST; } const allPendingPermissions = state.sessions[serverId]?.pendingPermissions; if (!allPendingPermissions) { return EMPTY_PENDING_PERMISSION_LIST; } const filtered: PendingPermission[] = []; for (const permission of allPendingPermissions.values()) { if (permission.agentId === agentId) { filtered.push(permission); } } return filtered.length > 0 ? filtered : EMPTY_PENDING_PERMISSION_LIST; }, shallow, ); const pendingPermissions = useMemo(() => { if (pendingPermissionList.length === 0) { return EMPTY_PENDING_PERMISSIONS; } return new Map(pendingPermissionList.map((permission) => [permission.key, permission])); }, [pendingPermissionList]); const setAgentStreamTail = useSessionStore((state) => state.setAgentStreamTail); const markPendingCreateLifecycle = useCreateFlowStore((state) => state.markLifecycle); const clearPendingCreate = useCreateFlowStore((state) => state.clear); const optimisticStreamItems = useMemo(() => { if (!shouldUseOptimisticStream || !pendingCreate) { return EMPTY_STREAM_ITEMS; } return [ { kind: "user_message", id: pendingCreate.clientMessageId, text: pendingCreate.text, timestamp: new Date(pendingCreate.timestamp), ...(pendingCreate.images && pendingCreate.images.length > 0 ? { images: pendingCreate.images } : {}), ...(pendingCreate.attachments && pendingCreate.attachments.length > 0 ? { attachments: pendingCreate.attachments } : {}), }, ]; }, [pendingCreate, shouldUseOptimisticStream]); const mergedStreamItems = useMemo(() => { if (optimisticStreamItems.length === 0) { return streamItems; } const optimistic = optimisticStreamItems[0]; if (!optimistic) { return streamItems; } const alreadyHasOptimistic = streamItems.some( (item) => item.kind === "user_message" && item.id === optimistic.id, ); return alreadyHasOptimistic ? streamItems : [...optimisticStreamItems, ...streamItems]; }, [optimisticStreamItems, streamItems]); useEffect(() => { if (!shouldUseOptimisticStream || !pendingCreate) { return; } const hasUserMessage = streamItems.some( (item) => item.kind === "user_message" && item.id === pendingCreate.clientMessageId, ); if (!hasUserMessage || !canFinalizePendingCreate) { return; } const pendingImages = pendingCreate.images; const pendingAttachments = pendingCreate.attachments; const hasPendingImages = Boolean(pendingImages && pendingImages.length > 0); const hasPendingAttachments = Boolean(pendingAttachments && pendingAttachments.length > 0); if (agentId && (hasPendingImages || hasPendingAttachments)) { setAgentStreamTail(serverId, (previous) => { const current = previous.get(agentId); if (!current) { return previous; } const merged = mergePendingCreateImages({ streamItems: current, clientMessageId: pendingCreate.clientMessageId, images: pendingImages, attachments: pendingAttachments, }); if (merged === current) { return previous; } const next = new Map(previous); next.set(agentId, merged); return next; }); } markPendingCreateLifecycle({ draftId: pendingCreate.draftId, lifecycle: "sent", }); clearPendingCreate({ draftId: pendingCreate.draftId }); }, [ agentId, canFinalizePendingCreate, clearPendingCreate, markPendingCreateLifecycle, pendingCreate, serverId, setAgentStreamTail, shouldUseOptimisticStream, streamItems, ]); return ( ); } function AgentComposerSection({ agentId, serverId, isPaneFocused, isArchivingCurrentAgent, archivedAt, cwd, isSubmitLoading, onAttentionInputFocus, onAttentionPromptSend, onAddImages, onComposerHeightChange, onMessageSent, }: { agentId?: string; serverId: string; isPaneFocused: boolean; isArchivingCurrentAgent: boolean; archivedAt: Date | null; cwd: string; isSubmitLoading: boolean; onAttentionInputFocus: () => void; onAttentionPromptSend: () => void; onAddImages: (addImages: (images: ImageAttachment[]) => void) => void; onComposerHeightChange: (height: number) => void; onMessageSent: () => void; }) { if (!agentId) { return null; } if (archivedAt) { return ; } if (isArchivingCurrentAgent) { return null; } return ( ); } function ActiveAgentComposer({ agentId, serverId, isPaneFocused, cwd, isSubmitLoading, onAttentionInputFocus, onAttentionPromptSend, onAddImages, onComposerHeightChange, onMessageSent, }: { agentId: string; serverId: string; isPaneFocused: boolean; cwd: string; isSubmitLoading: boolean; onAttentionInputFocus: () => void; onAttentionPromptSend: () => void; onAddImages: (addImages: (images: ImageAttachment[]) => void) => void; onComposerHeightChange: (height: number) => void; onMessageSent: () => void; }) { const insets = useSafeAreaInsets(); const isCompact = useIsCompactFormFactor(); const paneContext = usePaneContext(); const { workspaceId } = paneContext; const subagentRows = useSubagentsForParent({ serverId, parentAgentId: agentId, }); const handleOpenSubagent = useCallback( (subagentId: string) => { navigateToAgent({ serverId, agentId: subagentId }); }, [serverId], ); const handleArchiveSubagent = useArchiveSubagent({ serverId }); const agentInputDraft = useAgentInputDraft({ draftKey: buildDraftStoreKey({ serverId, agentId, }), }); const workspaceAttachmentScopeKey = useWorkspaceAttachmentScopeKey({ serverId, cwd, workspaceId, }); const workspaceAttachments = useWorkspaceAttachments(workspaceAttachmentScopeKey); const openFileExplorerForCheckout = usePanelStore((state) => state.openFileExplorerForCheckout); const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout); const handleOpenWorkspaceAttachment = useCallback( (attachment: WorkspaceComposerAttachment) => { if (attachment.kind !== "review") { return; } const checkout = { serverId, cwd: attachment.attachment.cwd, isGit: true, }; openFileExplorerForCheckout({ checkout, isCompact, }); setExplorerTabForCheckout({ ...checkout, tab: "changes", }); }, [isCompact, openFileExplorerForCheckout, serverId, setExplorerTabForCheckout], ); const inputAreaStyle = useMemo( () => [styles.inputAreaWrapper, { paddingBottom: insets.bottom }], [insets.bottom], ); return ( ); } function AgentSessionUnavailableState({ serverLabel, connectionStatus, lastError, isUnknownDaemon = false, }: { serverLabel: string; connectionStatus: HostRuntimeConnectionStatus; lastError: string | null; isUnknownDaemon?: boolean; }) { if (isUnknownDaemon) { return ( Cannot open this agent because {serverLabel} is not configured on this device. Add the host in Settings or open an agent on a configured server to continue. ); } const isConnecting = connectionStatus === "connecting"; const isPreparingSession = connectionStatus === "online"; return ( {isConnecting || isPreparingSession ? ( <> {isPreparingSession ? `Preparing ${serverLabel} session...` : `Connecting to ${serverLabel}...`} {isPreparingSession ? "We will show this agent in a moment." : "We will show this agent once the host is online."} ) : ( <> Reconnecting to {serverLabel}... We will show this agent again as soon as the host is reachable. {lastError ? {lastError} : null} )} ); } const ThemedActivityIndicator = withUnistyles(ActivityIndicator); const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted, }); const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground, }); const styles = StyleSheet.create((theme) => ({ root: { flex: 1, backgroundColor: theme.colors.surface0, }, container: { flex: 1, backgroundColor: theme.colors.surface0, }, contentContainer: { flex: 1, overflow: "hidden", ...(isWeb ? { userSelect: "none" as const } : {}), }, content: { flex: 1, }, inputAreaWrapper: { width: "100%", backgroundColor: theme.colors.surface0, }, historySyncOverlay: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0, backgroundColor: theme.colors.surface0, alignItems: "center", justifyContent: "center", zIndex: 40, }, archivingOverlay: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0, backgroundColor: "rgba(8, 10, 14, 0.86)", alignItems: "center", justifyContent: "center", paddingHorizontal: theme.spacing[8], gap: theme.spacing[3], zIndex: 50, }, archivingTitle: { fontSize: theme.fontSize.lg, fontWeight: theme.fontWeight.semibold, color: theme.colors.foreground, textAlign: "center", }, archivingSubtitle: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, textAlign: "center", }, loadingText: { fontSize: theme.fontSize.base, color: theme.colors.foregroundMuted, }, centerState: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: theme.spacing[6], gap: theme.spacing[3], }, errorContainer: { flex: 1, alignItems: "center", justifyContent: "center", }, errorText: { fontSize: theme.fontSize.lg, color: theme.colors.foregroundMuted, textAlign: "center", }, statusText: { marginTop: theme.spacing[2], textAlign: "center", fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, }, offlineTitle: { fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.semibold, color: theme.colors.foreground, textAlign: "center", }, offlineDescription: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, textAlign: "center", }, offlineDetails: { fontSize: theme.fontSize.xs, color: theme.colors.foregroundMuted, textAlign: "center", }, }));