import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { UserComposerAttachment } from "@/attachments/types"; import type { TextReplacement } from "@/composer/types"; import type { DraftAgentControlsProps } from "@/composer/agent-controls"; import type { DraftCommandTarget } from "@/hooks/use-agent-commands-query"; import { useAgentFormState, type CreateAgentInitialValues, type UseAgentFormStateResult, } from "@/hooks/use-agent-form-state"; import { useDraftAgentFeatures } from "@/hooks/use-draft-agent-features"; import { buildDraftAgentControls, hasDraftContent, resolveDraftKey, type DraftKeyInput, } from "@/composer/draft/input-draft-core"; import { buildDraftCommandTarget, resolveEffectiveComposerModeId, resolveEffectiveComposerModelId, resolveEffectiveComposerThinkingOptionId, type ProviderSelectionState, } from "@/provider-selection/provider-selection"; import { useDraftStore } 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"; type AttachmentUpdater = | UserComposerAttachment[] | ((prev: UserComposerAttachment[]) => UserComposerAttachment[]); interface AgentInputDraftComposerOptions { initialServerId: string | null; initialValues?: CreateAgentInitialValues; initialFeatureValues?: Record; isVisible?: boolean; lockedWorkingDir?: string; } interface UseAgentInputDraftInput { draftKey: DraftKeyInput; composer?: AgentInputDraftComposerOptions; } type DraftComposerState = UseAgentFormStateResult & { workingDir: string; effectiveModeId: string; effectiveModelId: string; effectiveThinkingOptionId: string; featureValues: Record | undefined; 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: { text: string; attachments: UserComposerAttachment[] }) => { text: string; attachments: UserComposerAttachment[]; }, ) => { const store = useDraftStore.getState(); const current = store.getDraftInput(draftKey) ?? { text: "", attachments: [] }; const next = update(current); if (!hasDraftContent(next)) { store.clearDraftInput({ draftKey, lifecycle: "abandoned" }); return; } store.saveDraftInput({ draftKey, draft: next }); }, [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 clear = useCallback( (lifecycle: "sent" | "abandoned") => { textPublication.cancel(); useDraftStore.getState().clearDraftInput({ draftKey, lifecycle }); }, [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 { features: draftFeatures, featureValues: draftFeatureValues, setFeatureValue: setDraftFeatureValue, applyProfileFeatureValues, } = useDraftAgentFeatures({ serverId: formState.selectedServerId, provider: formState.selectedProvider, cwd: workingDir, modeId: formState.selectedMode, modelId: effectiveModelId, thinkingOptionId: effectiveThinkingOptionId, initialFeatureValues: composerOptions?.initialFeatureValues, }); const applyDraftAgentProfile = useCallback( (profile: Parameters[0]) => { formState.applyProfileFromUser(profile); applyProfileFeatureValues(profile.featureValues); }, [applyProfileFeatureValues, formState], ); const commandDraft = useMemo( () => buildDraftCommandTarget({ selection: providerSelection, cwd: workingDir, effectiveModelId, effectiveThinkingOptionId, featureValues: draftFeatureValues, }), [ effectiveModelId, effectiveThinkingOptionId, draftFeatureValues, providerSelection, workingDir, ], ); const composerState = useMemo(() => { if (!composerOptions) { return null; } return { ...formState, workingDir, effectiveModeId, effectiveModelId, effectiveThinkingOptionId, featureValues: draftFeatureValues, agentControls: buildDraftAgentControls({ formState, features: draftFeatures, onSetFeature: setDraftFeatureValue, onApplyAgentProfile: applyDraftAgentProfile, }), commandDraft, }; }, [ commandDraft, composerOptions, effectiveModeId, effectiveModelId, effectiveThinkingOptionId, draftFeatures, draftFeatureValues, applyDraftAgentProfile, formState, setDraftFeatureValue, workingDir, ]); return { textSource, editText, replaceText, textReplacement, attachments, setAttachments, clear, isHydrated, attachmentFocusRequestId, composerState, }; } export const __private__ = { resolveDraftKey, resolveEffectiveComposerModeId, resolveEffectiveComposerModelId, resolveEffectiveComposerThinkingOptionId, buildDraftCommandTarget, buildDraftAgentControls, };