import { useCallback, useEffect, useMemo, useRef } from "react"; import { Keyboard, ScrollView, Text, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import invariant from "tiny-invariant"; import { Composer } from "@/composer"; import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control"; import { ComposerImportPill } from "@/composer/draft/import-pill"; import { FileDropZone } from "@/components/file-drop-zone"; import { AgentStreamView } from "@/agent-stream/view"; import { composerWorkspaceAttachment } from "@/composer/attachments/workspace"; import type { ImageAttachment } from "@/composer/types"; import { useAgentInputDraft } from "@/composer/draft/input-draft"; import type { CreateAgentInitialValues } from "@/hooks/use-agent-form-state"; import { useDraftAgentCreateFlow, type DraftCreateAttempt } from "@/composer/draft/create-flow"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { buildWorkspaceDraftAgentConfig } from "@/screens/workspace/workspace-draft-agent-config"; import { buildDraftStoreKey } from "@/stores/draft-keys"; import { usePanelStore } from "@/stores/panel-store"; import { useCreateFlowStore } from "@/stores/create-flow-store"; import type { Agent } from "@/stores/session-store"; import { useWorkspaceExecutionAuthority } from "@/stores/session-store-hooks"; import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store"; import { encodeImages } from "@/utils/encode-images"; import type { WorkspaceFileOpenRequest } from "@/workspace/file-open"; import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/workspace-draft-pane-focus"; import { validateDraftSubmission } from "@/composer/draft/workspace-tab-core"; import type { AgentCapabilityFlags } from "@getpaseo/protocol/agent-types"; import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import type { WorkspaceComposerAttachment } from "@/attachments/types"; import { useWorkspaceAttachments, useWorkspaceAttachmentScopeKey, } from "@/attachments/workspace-attachments-store"; import type { UserMessageImageAttachment } from "@/types/stream"; import { MAX_CONTENT_WIDTH, useIsCompactFormFactor } from "@/constants/layout"; import { isWeb } from "@/constants/platform"; import type { WorkspaceDraftTabSetup } from "@/stores/workspace-tabs-store"; const EMPTY_PENDING_PERMISSIONS = new Map(); const EMPTY_ONLINE_SERVER_IDS: string[] = []; const DRAFT_CAPABILITIES: AgentCapabilityFlags = { supportsStreaming: true, supportsSessionPersistence: false, supportsDynamicModes: false, supportsMcpServers: false, supportsReasoningStream: false, supportsToolInvocations: false, }; interface AutoSubmitConfig { provider: string; modeId: string | null; model: string | null; thinkingOptionId: string | null; featureValues: Record; } function resolveAutoSubmitConfig( pending: { provider: string; modeId?: string | null; model?: string | null; thinkingOptionId?: string | null; featureValues?: Record; } | null, ): AutoSubmitConfig | null { if (!pending) return null; return { provider: pending.provider, modeId: pending.modeId ?? null, model: pending.model ?? null, thinkingOptionId: pending.thinkingOptionId ?? null, featureValues: pending.featureValues ?? {}, }; } function resolveDraftModeIdOverride(input: { autoSubmitConfig: AutoSubmitConfig | null; modeOptionsCount: number; selectedMode: string; }): { modeId: string } | Record { const { autoSubmitConfig, modeOptionsCount, selectedMode } = input; if (autoSubmitConfig?.modeId) { return { modeId: autoSubmitConfig.modeId }; } if (modeOptionsCount > 0 && selectedMode !== "") { return { modeId: selectedMode }; } return {}; } function resolveDraftModeId(input: { autoSubmitConfig: AutoSubmitConfig | null; modeOptionsCount: number; selectedMode: string; }): string | null { const { autoSubmitConfig, modeOptionsCount, selectedMode } = input; if (autoSubmitConfig?.modeId !== undefined) { return autoSubmitConfig.modeId; } if (modeOptionsCount > 0 && selectedMode !== "") { return selectedMode; } return null; } async function submitDraftCreateRequest(input: { attempt: { clientMessageId: string }; text: string; images?: UserMessageImageAttachment[]; attachments?: unknown; client: DaemonClient | null; workspaceDirectory: string | null; workspaceExecutionAuthority: { workspaceId: string } | null; autoSubmitConfig: AutoSubmitConfig | null; composerState: { selectedProvider: string | null; selectedMode: string; modeOptions: unknown[]; effectiveModelId: string | null; effectiveThinkingOptionId: string | null; featureValues: Record | undefined; }; }): Promise<{ agentId: string | null; result: AgentSnapshotPayload }> { const { attempt, text, images, attachments, client, workspaceDirectory, workspaceExecutionAuthority, autoSubmitConfig, composerState, } = input; invariant(workspaceDirectory, "Workspace directory is required"); invariant(workspaceExecutionAuthority, "Workspace authority is required"); if (!client) { throw new Error("Host is not connected"); } const provider = autoSubmitConfig?.provider ?? composerState.selectedProvider; if (!provider) { throw new Error("Select a model"); } const modeIdOverride = resolveDraftModeIdOverride({ autoSubmitConfig, modeOptionsCount: composerState.modeOptions.length, selectedMode: composerState.selectedMode, }); const config = buildWorkspaceDraftAgentConfig({ provider, cwd: workspaceDirectory, ...modeIdOverride, model: autoSubmitConfig?.model ?? (composerState.effectiveModelId || undefined), thinkingOptionId: autoSubmitConfig?.thinkingOptionId ?? (composerState.effectiveThinkingOptionId || undefined), featureValues: autoSubmitConfig?.featureValues ?? composerState.featureValues, }); const imagesData = await encodeImages(images); const attachmentsArray = Array.isArray(attachments) ? attachments : undefined; const result = await client.createAgent({ config, workspaceId: workspaceExecutionAuthority.workspaceId, ...(text ? { initialPrompt: text } : {}), clientMessageId: attempt.clientMessageId, ...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}), ...(attachmentsArray && attachmentsArray.length > 0 ? { attachments: attachmentsArray } : {}), }); return { agentId: result.id, result, }; } function buildDraftAgentSnapshot(input: { attempt: { timestamp: Date }; serverId: string; tabId: string; workspaceDirectory: string | null; autoSubmitConfig: AutoSubmitConfig | null; composerState: { effectiveModelId: string | null; effectiveThinkingOptionId: string | null; modeOptions: unknown[]; selectedMode: string; selectedProvider: string | null; agentControls: { features?: Agent["features"] }; }; }): Agent { const { attempt, serverId, tabId, workspaceDirectory, autoSubmitConfig, composerState } = input; invariant(workspaceDirectory, "Workspace directory is required"); const now = attempt.timestamp; const model = autoSubmitConfig?.model ?? (composerState.effectiveModelId || null); const thinkingOptionId = autoSubmitConfig?.thinkingOptionId ?? (composerState.effectiveThinkingOptionId || null); const modeId = resolveDraftModeId({ autoSubmitConfig, modeOptionsCount: composerState.modeOptions.length, selectedMode: composerState.selectedMode, }); const provider = autoSubmitConfig?.provider ?? composerState.selectedProvider; if (!provider) { throw new Error("Select a model"); } return { serverId, id: tabId, provider, status: "running", createdAt: now, updatedAt: now, lastUserMessageAt: now, lastActivityAt: now, capabilities: DRAFT_CAPABILITIES, currentModeId: modeId, availableModes: [], pendingPermissions: [], persistence: null, runtimeInfo: { provider, sessionId: null, model, modeId }, title: "Agent", cwd: workspaceDirectory, model, features: composerState.agentControls.features, thinkingOptionId, parentAgentId: null, labels: {}, }; } function buildDraftInitialValues(input: { workingDir: string | null; initialSetup: WorkspaceDraftTabSetup | null; }): CreateAgentInitialValues | undefined { if (!input.workingDir) { return undefined; } if (!input.initialSetup) { return { workingDir: input.workingDir }; } return { workingDir: input.workingDir, provider: input.initialSetup.provider, modeId: input.initialSetup.modeId, model: input.initialSetup.model, thinkingOptionId: input.initialSetup.thinkingOptionId, }; } function resolveDraftWorkingDirectory(input: { workspaceDirectory: string | null; initialSetup: WorkspaceDraftTabSetup | null; }): string | null { if (input.initialSetup) { return input.initialSetup.cwd; } return input.workspaceDirectory; } function resolveOnlineServerIds(input: { isConnected: boolean; serverId: string }): string[] { if (!input.isConnected) { return EMPTY_ONLINE_SERVER_IDS; } return [input.serverId]; } interface WorkspaceDraftAgentTabProps { serverId: string; workspaceId: string; tabId: string; draftId: string; initialSetup?: WorkspaceDraftTabSetup; isPaneFocused: boolean; onCreated: (snapshot: AgentSnapshotPayload) => void; onOpenWorkspaceFile: (request: WorkspaceFileOpenRequest) => void; onOpenImportSheet?: () => void; } function resolveImportPillPress( onOpenImportSheet: (() => void) | undefined, isSubmitting: boolean, ): (() => void) | null { if (isSubmitting) { return null; } return onOpenImportSheet ?? null; } export function WorkspaceDraftAgentTab({ serverId, workspaceId, tabId, draftId, initialSetup = undefined, isPaneFocused, onCreated, onOpenWorkspaceFile, onOpenImportSheet, }: WorkspaceDraftAgentTabProps) { const insets = useSafeAreaInsets(); const client = useHostRuntimeClient(serverId); const isConnected = useHostRuntimeIsConnected(serverId); const workspaceAuthority = useWorkspaceExecutionAuthority(serverId, workspaceId); const workspaceExecutionAuthority = workspaceAuthority?.ok ? workspaceAuthority.authority : null; const workspaceDirectory = workspaceExecutionAuthority?.workspaceDirectory ?? null; const draftSetup = initialSetup ?? null; const draftWorkingDirectory = resolveDraftWorkingDirectory({ workspaceDirectory, initialSetup: draftSetup, }); const draftInitialValues = buildDraftInitialValues({ workingDir: draftWorkingDirectory, initialSetup: draftSetup, }); const onlineServerIds = resolveOnlineServerIds({ isConnected, serverId }); const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null); const draftStoreKey = useMemo( () => buildDraftStoreKey({ serverId, agentId: tabId, draftId, }), [draftId, serverId, tabId], ); const draftInput = useAgentInputDraft({ draftKey: draftStoreKey, composer: { initialServerId: serverId, initialValues: draftInitialValues, initialFeatureValues: draftSetup?.featureValues, isVisible: true, onlineServerIds, lockedWorkingDir: draftWorkingDirectory ?? undefined, }, }); const composerState = draftInput.composerState; if (!composerState) { throw new Error("Workspace draft composer state is required"); } const clearDraftInput = draftInput.clear; const setDraftText = draftInput.setText; const setDraftAttachments = draftInput.setAttachments; const pendingAutoSubmit = useWorkspaceDraftSubmissionStore((state) => { const pending = state.pendingByDraftId[draftId] ?? null; return pending?.serverId === serverId && pending.workspaceId === workspaceId ? pending : null; }); const pendingCreateAttempt = useCreateFlowStore((state) => { const pending = state.pendingByDraftId[draftId] ?? null; return pending?.serverId === serverId && pending.lifecycle === "active" ? pending : null; }); const consumePendingAutoSubmit = useWorkspaceDraftSubmissionStore( (state) => state.consumePending, ); const autoSubmitConfig = resolveAutoSubmitConfig(pendingAutoSubmit); const initialCreateAttempt = useMemo(() => { if (!pendingAutoSubmit || !pendingCreateAttempt) { return null; } if (pendingAutoSubmit.clientMessageId !== pendingCreateAttempt.clientMessageId) { return null; } return { clientMessageId: pendingCreateAttempt.clientMessageId, text: pendingCreateAttempt.text, timestamp: new Date(pendingCreateAttempt.timestamp), ...(pendingCreateAttempt.images && pendingCreateAttempt.images.length > 0 ? { images: pendingCreateAttempt.images } : {}), ...(pendingCreateAttempt.attachments && pendingCreateAttempt.attachments.length > 0 ? { attachments: pendingCreateAttempt.attachments } : {}), }; }, [pendingAutoSubmit, pendingCreateAttempt]); const allowsEmptyAutoSubmit = pendingAutoSubmit?.allowEmptyText === true; const isCompact = useIsCompactFormFactor(); const workspaceAttachmentScopeKey = useWorkspaceAttachmentScopeKey({ serverId, cwd: composerState.workingDir, 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 { formErrorMessage, isSubmitting, optimisticStreamItems, draftAgent, handleCreateFromInput, continueCreateFromAttempt, } = useDraftAgentCreateFlow({ draftId, getPendingServerId: () => serverId, initialAttempt: initialCreateAttempt, allowEmptyText: allowsEmptyAutoSubmit, validateBeforeSubmit: ({ text }) => validateDraftSubmission({ text, allowsEmptyAutoSubmit, composerState, autoSubmitConfig, workspaceDirectory: draftWorkingDirectory, hasClient: Boolean(client), }), onBeforeSubmit: () => { void composerState.persistFormPreferences(); if (isWeb) { (document.activeElement as HTMLElement | null)?.blur?.(); } Keyboard.dismiss(); }, buildDraftAgent: (attempt) => buildDraftAgentSnapshot({ attempt, serverId, tabId, workspaceDirectory: draftWorkingDirectory, autoSubmitConfig, composerState, }), createRequest: async ({ attempt, text, images, attachments }) => submitDraftCreateRequest({ attempt, text, images, attachments, client, workspaceDirectory: draftWorkingDirectory, workspaceExecutionAuthority, autoSubmitConfig, composerState, }), onCreateSuccess: ({ result }) => { clearDraftInput("sent"); onCreated(result); }, }); const isReadyForPendingAutoSubmit = Boolean( pendingAutoSubmit && draftInput.isHydrated && draftWorkingDirectory && client && !composerState.isModelLoading, ); const autoSubmitKeyRef = useRef(null); useEffect(() => { if (!isReadyForPendingAutoSubmit) { return; } const submitKey = `${serverId}:${workspaceId}:${draftId}`; if (autoSubmitKeyRef.current === submitKey) { return; } const submission = consumePendingAutoSubmit({ serverId, workspaceId, draftId }); if (!submission) { return; } autoSubmitKeyRef.current = submitKey; setDraftText(""); setDraftAttachments([]); const preparedAttempt = initialCreateAttempt?.clientMessageId === submission.clientMessageId ? initialCreateAttempt : null; const createPromise = preparedAttempt ? continueCreateFromAttempt({ attempt: preparedAttempt, cwd: submission.cwd, }) : handleCreateFromInput({ text: submission.text, attachments: submission.attachments, cwd: submission.cwd, }); void createPromise.catch(() => { setDraftText(submission.text); setDraftAttachments(composerWorkspaceAttachment.userAttachmentsOnly(submission.attachments)); autoSubmitKeyRef.current = null; }); }, [ continueCreateFromAttempt, consumePendingAutoSubmit, draftId, handleCreateFromInput, initialCreateAttempt, isReadyForPendingAutoSubmit, serverId, setDraftAttachments, setDraftText, workspaceId, ]); const handleFilesDropped = useCallback((files: ImageAttachment[]) => { addImagesRef.current?.(files); }, []); const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => { addImagesRef.current = addImages; }, []); const focusInputRef = useRef<(() => void) | null>(null); const handleFocusInputCallback = useCallback((focus: () => void) => { focusInputRef.current = focus; }, []); const handleProviderSelectWithFocus = useCallback( (provider: Parameters[0]) => { composerState.setProviderFromUser(provider); focusInputRef.current?.(); }, [composerState], ); const handleModeSelectWithFocus = useCallback( (modeId: string) => { composerState.setModeFromUser(modeId); focusInputRef.current?.(); }, [composerState], ); const handleModelSelectWithFocus = useCallback( (modelId: string) => { composerState.setModelFromUser(modelId); focusInputRef.current?.(); }, [composerState], ); const handleProviderAndModelSelectWithFocus = useCallback( ( provider: Parameters[0], modelId: string, ) => { composerState.setProviderAndModelFromUser(provider, modelId); focusInputRef.current?.(); }, [composerState], ); const handleThinkingOptionSelectWithFocus = useCallback( (optionId: string) => { composerState.setThinkingOptionFromUser(optionId); focusInputRef.current?.(); }, [composerState], ); const handleSetFeatureWithFocus = useCallback( (featureId: string, value: unknown) => { composerState.agentControls.onSetFeature?.(featureId, value); focusInputRef.current?.(); }, [composerState], ); const inputAreaWrapperStyle = useMemo( () => [styles.inputAreaWrapper, { paddingBottom: insets.bottom }], [insets.bottom], ); const handleDropdownCloseFocus = useCallback(() => { focusInputRef.current?.(); }, []); const importPillPress = resolveImportPillPress(onOpenImportSheet, isSubmitting); const composerAgentControls = useMemo( () => ({ ...composerState.agentControls, onSelectProvider: handleProviderSelectWithFocus, onSelectMode: handleModeSelectWithFocus, onSelectModel: handleModelSelectWithFocus, onSelectProviderAndModel: handleProviderAndModelSelectWithFocus, onSelectThinkingOption: handleThinkingOptionSelectWithFocus, onSetFeature: handleSetFeatureWithFocus, onDropdownClose: handleDropdownCloseFocus, disabled: isSubmitting, }), [ composerState.agentControls, handleProviderSelectWithFocus, handleModeSelectWithFocus, handleModelSelectWithFocus, handleProviderAndModelSelectWithFocus, handleThinkingOptionSelectWithFocus, handleSetFeatureWithFocus, handleDropdownCloseFocus, isSubmitting, ], ); const composerFooter = useMemo( () => isCompact ? ( ) : undefined, [isCompact, composerAgentControls], ); return ( {isSubmitting && draftAgent ? ( ) : ( {formErrorMessage ? ( {formErrorMessage} ) : null} )} {importPillPress ? ( ) : null} ); } const styles = StyleSheet.create((theme) => ({ container: { flex: 1, width: "100%", backgroundColor: theme.colors.surface0, }, contentContainer: { flex: 1, }, streamContainer: { flex: 1, }, scrollView: { flex: 1, }, configScrollContent: { paddingHorizontal: theme.spacing[4], paddingTop: theme.spacing[4], paddingBottom: theme.spacing[6], }, configSection: { gap: theme.spacing[3], }, inputAreaWrapper: { width: "100%", backgroundColor: theme.colors.surface0, }, importPillRow: { width: "100%", paddingHorizontal: theme.spacing[4], paddingTop: theme.spacing[3], paddingBottom: theme.spacing[3], alignItems: "center", }, importPillContent: { width: "100%", maxWidth: MAX_CONTENT_WIDTH, flexDirection: "row", }, errorContainer: { marginTop: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], borderRadius: theme.borderRadius.md, backgroundColor: theme.colors.surface2, borderWidth: 1, borderColor: theme.colors.destructive, }, errorText: { color: theme.colors.destructive, }, }));