import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import type { TFunction } from "i18next"; import { SquarePen } from "lucide-react-native"; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; 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 "@/agent-stream/view"; import { ArchivedAgentCallout } from "@/components/archived-agent-callout"; import { Composer } from "@/composer"; import { AgentModeControl } from "@/composer/agent-controls/mode-control"; import { FileDropZone } from "@/components/file-drop-zone"; import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore"; import type { ImageAttachment } from "@/composer/types"; 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 { COMPACT_FORM_FACTOR_WIDTH, 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, type AgentInputDraft } from "@/composer/draft/input-draft"; import { type AgentScreenAgent, type AgentScreenContinuity, 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 { useContainerWidthBelow } from "@/hooks/use-container-width"; import { usePaneContext, usePaneFocus } from "@/panels/pane-context"; import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry"; import { RenderProfile } from "@/utils/render-profiler"; import { buildDraftPanelDescriptor } from "@/panels/draft-panel-descriptor"; import { type HostRuntimeConnectionStatus, useHostRuntimeClient, useHostRuntimeConnectionStatus, useHostRuntimeIsConnected, useHostRuntimeLastError, useHosts, } from "@/runtime/host-runtime"; import { deriveRouteBottomAnchorIntent, deriveRouteBottomAnchorRequest, } from "@/screens/agent/agent-ready-screen-bottom-anchor"; import { WorkspaceDraftAgentTab } from "@/composer/draft/workspace-tab"; import { useCreateFlowStore } from "@/stores/create-flow-store"; import { buildDraftStoreKey, generateDraftId } from "@/stores/draft-keys"; import { usePanelStore } from "@/stores/panel-store"; import { type Agent, useSessionStore } from "@/stores/session-store"; import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store"; import { buildWorkspaceTabPersistenceKey } from "@/stores/workspace-tabs-store"; import type { Theme } from "@/styles/theme"; import { useArchiveSubagent, useSubagentsForParent } from "@/subagents"; import { SubagentsTrack } from "@/subagents/track"; 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 type { WorkspaceFileOpenRequest } from "@/workspace/file-open"; import { navigateToAgent } from "@/utils/navigate-to-agent"; import { deriveSidebarStateBucket } from "@/utils/sidebar-agent-state"; import { buildDraftAgentSetup, type ClientSlashCommand } from "@/client-slash-commands"; interface ChatAgentStateShape { serverId: string | null; id: string | null; status: Agent["status"] | null; cwd: string | null; capabilities?: Agent["capabilities"]; 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, capabilities: agent.capabilities, 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, capabilities: state.capabilities, lastError: state.lastError ?? null, projectPlacement, }; } function renderChatAgentNonReadyView(args: { viewState: AgentScreenViewState; effectiveAgent: AgentScreenAgent | null; t: TFunction; }): React.ReactElement | null { const { viewState, effectiveAgent, t } = args; if (viewState.tag === "not_found") { return ( {t("agentPanel.states.notFound")} ); } if (viewState.tag === "error") { return ( {t("agentPanel.states.failedToLoad")} {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"); return ( ); } function DraftPanel() { const { serverId, workspaceId, tabId, target, openFileInWorkspace, openImportSheet, retargetCurrentTab, } = usePaneContext(); const { isInteractive } = usePaneFocus(); invariant(target.kind === "draft", "DraftPanel requires draft target"); const handleCreated = useCallback( (agentSnapshot: Parameters[0]) => { const normalized = normalizeAgentSnapshot(agentSnapshot, serverId); useSessionStore.getState().setAgents(serverId, (prev) => { const next = new Map(prev); next.set(agentSnapshot.id, normalized); return next; }); retargetCurrentTab({ kind: "agent", agentId: agentSnapshot.id }); }, [retargetCurrentTab, serverId], ); return ( ); } export function AgentConversationPanel() { const { target } = usePaneContext(); if (target.kind === "draft") { return ; } if (target.kind === "agent") { return ; } invariant(false, "AgentConversationPanel requires an agent or draft target"); } export const agentPanelRegistration: PanelRegistration<"agent"> = { kind: "agent", component: AgentConversationPanel, useDescriptor: useAgentPanelDescriptor, }; export function useDraftPanelDescriptor( target: { kind: "draft"; draftId: string }, context: { serverId: string }, ) { const createDescriptorState = useCreateFlowStore( useShallow((state) => { const pending = state.pendingByDraftId[target.draftId]; if (pending?.serverId !== context.serverId || pending.lifecycle !== "active") { return { isCreating: false, pendingPrompt: null, }; } return { isCreating: true, pendingPrompt: pending.text, }; }), ); return buildDraftPanelDescriptor({ ...createDescriptorState, icon: SquarePen, }); } const EMPTY_STREAM_ITEMS: StreamItem[] = []; const EMPTY_PENDING_PERMISSIONS = new Map(); const EMPTY_PENDING_PERMISSION_LIST: PendingPermission[] = []; type RouteBottomAnchorRequest = ReturnType; function findActiveCreateHandoff(input: { pendingByDraftId: ReturnType["pendingByDraftId"]; serverId: string; agentId?: string; }): boolean { if (!input.agentId) { return false; } return Object.values(input.pendingByDraftId).some( (pending) => pending.lifecycle === "sent" && pending.serverId === input.serverId && pending.agentId === input.agentId, ); } 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); } 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?: (request: WorkspaceFileOpenRequest) => void; }) { const { t } = useTranslation(); 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 ?? t("agentPanel.unavailable.selectedHost"); 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?: (request: WorkspaceFileOpenRequest) => void; }) { const { t } = useTranslation(); 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 ( {t("agentPanel.states.notFound")} ); } if (lookupState.tag === "error") { return ( {t("agentPanel.states.failedToLoad")} {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?: (request: WorkspaceFileOpenRequest) => void; }) { const { t } = useTranslation(); 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 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 hasActiveCreateHandoff = useCreateFlowStore((state) => findActiveCreateHandoff({ pendingByDraftId: state.pendingByDraftId, serverId, agentId }), ); const hasSession = useSessionStore((state) => Boolean(state.sessions[serverId])); const { ensureAgentIsInitialized } = useAgentInitialization({ serverId, client: hasSession ? client : null, }); const [missingAgentState, setMissingAgentState] = useState({ kind: "idle", }); 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(t("agentPanel.states.reconnecting"), { durationMs: null, testID: "agent-reconnecting-toast", }); } }, [connectionStatus, panelToast, t]); 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 agent = useMemo( () => buildChatAgentFromState(agentState, projectPlacement), [agentState, projectPlacement], ); const continuity = useMemo(() => { if (!hasActiveCreateHandoff || !agentId) { return { kind: "none" }; } return { kind: "optimistic-create", agent: { serverId, id: agentId, status: "running", cwd: agent?.cwd ?? ".", projectPlacement: agent?.projectPlacement ?? null, }, }; }, [agent, agentId, hasActiveCreateHandoff, serverId]); const viewState = useAgentScreenStateMachine({ routeKey: `${serverId}:${agentId ?? ""}`, input: { agent: agent ?? null, missingAgentState, isConnected, isArchivingCurrentAgent, isHistorySyncing, needsAuthoritativeSync, continuity, 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) { 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, ]); const animatedContentStyle = useMemo( () => [styles.content, animatedKeyboardStyle], [animatedKeyboardStyle], ); const nonReadyView = renderChatAgentNonReadyView({ viewState, effectiveAgent, t, }); if (nonReadyView) return nonReadyView; invariant(agentId, "agent id is defined when agent content is ready"); invariant(effectiveAgent, "effectiveAgent is defined when the non-ready view is absent"); const agentCwd = agentState.cwd; invariant(agentCwd, "agent cwd is defined when agent content is ready"); const showHistorySyncOverlay = viewState.tag === "ready" && viewState.sync.status === "catching_up" && viewState.sync.ui === "overlay"; return ( ); } function ChatAgentReadyContent({ serverId, agentId, isPaneFocused, isArchivingCurrentAgent, agentState, effectiveAgent, routeBottomAnchorRequest, hasAppliedAuthoritativeHistory, panelToast, streamViewRef, animatedContentStyle, handleFilesDropped, handleAddImagesCallback, handleComposerHeightChange, handleMessageSent, showHistorySyncOverlay, cwd, attentionController, onOpenWorkspaceFile, }: { serverId: string; agentId: string; isPaneFocused: boolean; isArchivingCurrentAgent: boolean; agentState: ChatAgentSelectedState; effectiveAgent: AgentScreenAgent; routeBottomAnchorRequest: RouteBottomAnchorRequest; hasAppliedAuthoritativeHistory: boolean; panelToast: ReturnType; streamViewRef: React.RefObject; animatedContentStyle: object[]; handleFilesDropped: (files: ImageAttachment[]) => void; handleAddImagesCallback: (addImages: (images: ImageAttachment[]) => void) => void; handleComposerHeightChange: (height: number) => void; handleMessageSent: () => void; showHistorySyncOverlay: boolean; cwd: string; attentionController: ReturnType; onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => void; }) { const { t } = useTranslation(); const agentInputDraft = useAgentInputDraft({ draftKey: buildDraftStoreKey({ serverId, agentId, }), }); const streamSection = ( ); const composerSection = ( ); const streamContent = ( {streamSection} ); const contentContainer = {streamContent}; return ( {contentContainer} {composerSection} {showHistorySyncOverlay ? ( ) : null} {isArchivingCurrentAgent ? ( {t("agentPanel.states.archivingTitle")} {t("agentPanel.states.archivingSubtitle")} ) : null} ); } const AgentStreamSection = memo(function AgentStreamSection({ streamViewRef, serverId, agentId, agent, routeBottomAnchorRequest, hasAppliedAuthoritativeHistory, toast, onOpenWorkspaceFile, }: { streamViewRef: React.RefObject; serverId: string; agentId?: string; agent: AgentScreenAgent; routeBottomAnchorRequest: RouteBottomAnchorRequest; hasAppliedAuthoritativeHistory: boolean; toast: ReturnType["api"]; onOpenWorkspaceFile?: (request: WorkspaceFileOpenRequest) => 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]); return ( ); }); function AgentComposerSection({ agentId, serverId, isPaneFocused, isArchivingCurrentAgent, archivedAt, cwd, isSubmitLoading, agentInputDraft, onAttentionInputFocus, onAttentionPromptSend, onAddImages, onComposerHeightChange, onMessageSent, }: { agentId?: string; serverId: string; isPaneFocused: boolean; isArchivingCurrentAgent: boolean; archivedAt: Date | null; cwd: string; isSubmitLoading: boolean; agentInputDraft: AgentInputDraft; 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, agentInputDraft, onAttentionInputFocus, onAttentionPromptSend, onAddImages, onComposerHeightChange, onMessageSent, }: { agentId: string; serverId: string; isPaneFocused: boolean; cwd: string; isSubmitLoading: boolean; agentInputDraft: AgentInputDraft; onAttentionInputFocus: () => void; onAttentionPromptSend: () => void; onAddImages: (addImages: (images: ImageAttachment[]) => void) => void; onComposerHeightChange: (height: number) => void; onMessageSent: () => void; }) { const insets = useSafeAreaInsets(); const isCompactFormFactor = useIsCompactFormFactor(); const { onLayout: onInputAreaLayout, isBelow: isCompactComposerLayout } = useContainerWidthBelow( COMPACT_FORM_FACTOR_WIDTH, { initialIsBelow: isCompactFormFactor }, ); const paneContext = usePaneContext(); const { workspaceId, tabId, retargetCurrentTab } = paneContext; const { archiveAgent } = useArchiveAgent(); const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab); const hideWorkspaceAgent = useWorkspaceLayoutStore((state) => state.hideAgent); const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent); const subagentRows = useSubagentsForParent({ serverId, parentAgentId: agentId, }); const handleOpenSubagent = useCallback( (subagentId: string) => { navigateToAgent({ serverId, agentId: subagentId }); }, [serverId], ); const handleArchiveSubagent = useArchiveSubagent({ serverId }); 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: isCompactFormFactor, }); setExplorerTabForCheckout({ ...checkout, tab: "changes", }); }, [isCompactFormFactor, openFileExplorerForCheckout, serverId, setExplorerTabForCheckout], ); const handleClientSlashCommand = useCallback( async (command: ClientSlashCommand) => { const agent = resolveChatAgentFromSession(useSessionStore.getState(), serverId, agentId); if (!agent) { throw new Error("Agent not found"); } const workspaceKey = buildWorkspaceTabPersistenceKey({ serverId, workspaceId }); if (workspaceKey) { unpinWorkspaceAgent(workspaceKey, agentId); hideWorkspaceAgent(workspaceKey, agentId); } if (command.kind === "replace-agent-with-draft") { retargetCurrentTab({ kind: "draft", draftId: generateDraftId(), setup: buildDraftAgentSetup(agent), }); } else if (workspaceKey) { closeWorkspaceTab(workspaceKey, tabId); } await archiveAgent({ serverId, agentId }); }, [ agentId, archiveAgent, closeWorkspaceTab, hideWorkspaceAgent, retargetCurrentTab, serverId, tabId, unpinWorkspaceAgent, workspaceId, ], ); const { style: composerKeyboardStyle } = useKeyboardShiftStyle({ mode: "translate", }); const inputAreaStyle = useMemo( () => [styles.inputAreaWrapper, { paddingBottom: insets.bottom }, composerKeyboardStyle], [insets.bottom, composerKeyboardStyle], ); const composerFooter = useMemo( () => isCompactComposerLayout ? ( ) : undefined, [isCompactComposerLayout, serverId, agentId], ); return ( ); } function AgentSessionUnavailableState({ serverLabel, connectionStatus, lastError, isUnknownDaemon = false, t, }: { serverLabel: string; connectionStatus: HostRuntimeConnectionStatus; lastError: string | null; isUnknownDaemon?: boolean; t: TFunction; }) { if (isUnknownDaemon) { return ( {t("agentPanel.unavailable.unknownHost", { serverLabel })} {t("agentPanel.unavailable.addHost")} ); } const isConnecting = connectionStatus === "connecting"; const isPreparingSession = connectionStatus === "online"; return ( {isConnecting || isPreparingSession ? ( <> {isPreparingSession ? t("agentPanel.unavailable.preparingSession", { serverLabel }) : t("agentPanel.unavailable.connecting", { serverLabel })} {isPreparingSession ? t("agentPanel.unavailable.showSoon") : t("agentPanel.unavailable.showWhenOnline")} ) : ( <> {t("agentPanel.unavailable.reconnectingTo", { serverLabel })} {t("agentPanel.unavailable.showAgainWhenReachable")} {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", }, }));