import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { JsonValue } from "@getpaseo/protocol/agent-types"; import type { UserComposerAttachment } from "@/attachments/types"; import type { TextReplacement } from "@/composer/types"; import type { DraftAgentControlsProps } from "@/composer/agent-controls"; import type { DraftCommandTarget } from "@/agent-commands"; import { useAgentFormState, type CreateAgentInitialValues, type UseAgentFormStateResult, } from "@/hooks/use-agent-form-state"; import { useDraftProviderOptions } from "@/hooks/use-draft-provider-options"; import { buildDraftAgentControls, resolveDraftKey, type DraftKeyInput, } from "@/composer/draft/input-draft-core"; import { buildDraftCommandTarget, resolveEffectiveComposerModeId, resolveEffectiveComposerModelId, resolveEffectiveComposerThinkingOptionId, type ProviderSelectionState, } from "@/provider-selection/provider-selection"; import { useDraftStore, type DraftInput } from "@/stores/draft-store"; import { AfterPaintPublication } from "@/composer/after-paint-publication"; import { useShallow } from "zustand/shallow"; import type { ComposerTextSource } from "@/composer/text-source"; import { isWeb } from "@/constants/platform"; const NO_METADATA: Record = {}; type AttachmentUpdater = | UserComposerAttachment[] | ((prev: UserComposerAttachment[]) => UserComposerAttachment[]); interface AgentInputDraftComposerOptions { initialServerId: string | null; initialValues?: CreateAgentInitialValues; initialProviderOptions?: Record; /** Which composer a plugin's `useComposer()` reports. */ context?: "draft" | "new-workspace"; isVisible?: boolean; lockedWorkingDir?: string; } interface UseAgentInputDraftInput { draftKey: DraftKeyInput; composer?: AgentInputDraftComposerOptions; } type DraftComposerState = UseAgentFormStateResult & { workingDir: string; effectiveModeId: string; effectiveModelId: string; effectiveThinkingOptionId: string; providerOptions: Record | undefined; /** What plugins set on the agent this draft creates. */ metadata: Record; agentControls: DraftAgentControlsProps; commandDraft: DraftCommandTarget; }; export interface AgentInputDraft { textSource: ComposerTextSource; editText: (text: string) => void; replaceText: (text: string) => void; textReplacement: TextReplacement; attachments: UserComposerAttachment[]; setAttachments: (updater: AttachmentUpdater) => void; clear: (lifecycle: "sent" | "abandoned") => void; isHydrated: boolean; attachmentFocusRequestId: number; composerState: DraftComposerState | null; } export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDraft { const composerOptions = input.composer ?? null; const workingDir = composerOptions?.lockedWorkingDir?.trim() || ""; const formState = useAgentFormState({ workingDir, serverId: composerOptions?.initialServerId ?? null, initialValues: composerOptions?.initialValues, isVisible: composerOptions?.isVisible ?? false, isCreateFlow: true, }); const draftKey = useMemo( () => resolveDraftKey({ draftKey: input.draftKey, selectedServerId: formState.selectedServerId, }), [formState.selectedServerId, input.draftKey], ); const attachments = useDraftStore( useShallow((state) => state.drafts[draftKey]?.lifecycle === "active" ? (state.drafts[draftKey].input.attachments ?? []) : [], ), ); const textSource = useMemo( () => ({ getSnapshot: () => { const record = useDraftStore.getState().drafts[draftKey]; return record?.lifecycle === "active" ? record.input.text : ""; }, subscribe: (listener) => useDraftStore.subscribe((state, previous) => { if ( state.drafts[draftKey]?.input.text !== previous.drafts[draftKey]?.input.text || state.drafts[draftKey]?.lifecycle !== previous.drafts[draftKey]?.lifecycle ) listener(); }), }), [draftKey], ); const attachmentFocusRequestId = useDraftStore( (state) => state.attachmentFocusRequestByDraftKey[draftKey] ?? 0, ); const [hydratedDraftKey, setHydratedDraftKey] = useState(null); const isHydrated = hydratedDraftKey === draftKey; const textReplacementRevisionRef = useRef(0); const [textReplacement, setTextReplacement] = useState(() => ({ kind: "initial", key: `${draftKey}:0`, text: textSource.getSnapshot(), })); const publishTextReplacement = useCallback( (kind: "hydrated" | "replace", nextText: string) => { textReplacementRevisionRef.current += 1; setTextReplacement({ kind, key: `${draftKey}:${textReplacementRevisionRef.current}`, text: nextText, }); }, [draftKey], ); const saveDraft = useCallback( (update: (draft: DraftInput) => DraftInput) => useDraftStore.getState().updateDraftInput({ draftKey, update }), [draftKey], ); const textPublication = useMemo( () => new AfterPaintPublication((nextText) => { useDraftStore.getState().editDraftText({ draftKey, text: nextText }); }), [draftKey], ); const editText = useCallback( (nextText: string) => { if (isWeb) { textPublication.stage(nextText); } else { useDraftStore.getState().editDraftText({ draftKey, text: nextText }); } }, [draftKey, textPublication], ); const replaceText = useCallback( (nextText: string) => { textPublication.cancel(); useDraftStore.getState().editDraftText({ draftKey, text: nextText }); publishTextReplacement("replace", nextText); }, [draftKey, publishTextReplacement, textPublication], ); const setAttachments = useCallback( (updater: AttachmentUpdater) => { saveDraft((current) => ({ ...current, attachments: typeof updater === "function" ? updater(current.attachments) : updater, })); }, [saveDraft], ); const metadata = useDraftStore((state) => state.metadataByDraftKey[draftKey] ?? NO_METADATA); const setMetadata = useCallback( (patch: Record) => useDraftStore.getState().patchDraftMetadata({ draftKey, patch }), [draftKey], ); const clear = useCallback( (lifecycle: "sent" | "abandoned") => { textPublication.cancel(); useDraftStore.getState().clearDraftInput({ draftKey, lifecycle }); if (lifecycle === "sent") useDraftStore.getState().clearDraftMetadata({ draftKey }); }, [draftKey, textPublication], ); useEffect(() => { const flushWhenHidden = () => { if (document.visibilityState === "hidden") textPublication.flush(); }; const flush = () => textPublication.flush(); const canListenForPageHide = isWeb && typeof window !== "undefined" && typeof window.addEventListener === "function"; if (isWeb && typeof document !== "undefined") { document.addEventListener("visibilitychange", flushWhenHidden); } if (canListenForPageHide) { window.addEventListener("pagehide", flush); } return () => { if (isWeb && typeof document !== "undefined") { document.removeEventListener("visibilitychange", flushWhenHidden); } if (canListenForPageHide) { window.removeEventListener("pagehide", flush); } textPublication.flush(); }; }, [textPublication]); useEffect(() => { let cancelled = false; void (async () => { await useDraftStore.getState().hydrateDraftInput({ draftKey }); if (!cancelled) { const hydratedText = useDraftStore.getState().getDraftInput(draftKey)?.text ?? ""; publishTextReplacement("hydrated", hydratedText); setHydratedDraftKey(draftKey); } })(); return () => { cancelled = true; }; }, [draftKey, publishTextReplacement]); const providerSelection = useMemo( () => ({ provider: formState.selectedProvider, modelId: formState.selectedModel, modeId: formState.selectedMode, thinkingOptionId: formState.selectedThinkingOptionId, availableModels: formState.availableModels, modeOptions: formState.modeOptions, }), [ formState.availableModels, formState.modeOptions, formState.selectedMode, formState.selectedModel, formState.selectedProvider, formState.selectedThinkingOptionId, ], ); const effectiveModeId = useMemo( () => resolveEffectiveComposerModeId(providerSelection, formState.isModelLoading), [formState.isModelLoading, providerSelection], ); const effectiveModelId = useMemo( () => resolveEffectiveComposerModelId(providerSelection), [providerSelection], ); const effectiveThinkingOptionId = useMemo( () => resolveEffectiveComposerThinkingOptionId(providerSelection, effectiveModelId), [effectiveModelId, providerSelection], ); const { optionControls: draftOptionControls, options: draftOptions, shownOptions: draftShownOptions, setOption: setDraftOption, applyProfile: applyProfileOptions, } = useDraftProviderOptions({ serverId: formState.selectedServerId, provider: formState.selectedProvider, getProvider: formState.getSelectedProvider, cwd: workingDir, modeId: formState.selectedMode, modelId: effectiveModelId, thinkingOptionId: effectiveThinkingOptionId, initialOptions: composerOptions?.initialProviderOptions, }); const applyDraftAgentProfile = useCallback( (profile: Parameters[0]) => { formState.applyProfileFromUser(profile); applyProfileOptions({ provider: profile.provider, options: profile.options }); }, [applyProfileOptions, formState], ); const commandDraft = useMemo( () => buildDraftCommandTarget({ selection: providerSelection, cwd: workingDir, effectiveModelId, effectiveThinkingOptionId, providerOptions: draftOptions, }), [effectiveModelId, effectiveThinkingOptionId, draftOptions, providerSelection, workingDir], ); const composerState = useMemo(() => { if (!composerOptions) { return null; } return { ...formState, workingDir, effectiveModeId, effectiveModelId, effectiveThinkingOptionId, providerOptions: draftOptions, metadata, agentControls: buildDraftAgentControls({ formState, optionControls: draftOptionControls, options: draftShownOptions, onSetOption: setDraftOption, onApplyAgentProfile: applyDraftAgentProfile, context: composerOptions.context ?? "draft", metadata, onSetMetadata: setMetadata, }), commandDraft, }; }, [ commandDraft, composerOptions, effectiveModeId, effectiveModelId, effectiveThinkingOptionId, draftOptionControls, draftOptions, draftShownOptions, setDraftOption, applyDraftAgentProfile, formState, metadata, setMetadata, workingDir, ]); return { textSource, editText, replaceText, textReplacement, attachments, setAttachments, clear, isHydrated, attachmentFocusRequestId, composerState, }; } export const __private__ = { resolveDraftKey, resolveEffectiveComposerModeId, resolveEffectiveComposerModelId, resolveEffectiveComposerThinkingOptionId, buildDraftCommandTarget, buildDraftAgentControls, };