import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { UserComposerAttachment } from "@/attachments/types"; import type { DraftAgentStatusBarProps } from "@/components/agent-status-bar"; import type { DraftCommandConfig } 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 { areAttachmentsEqual, buildDraftComposerCommandConfig, buildDraftStatusControls, hasDraftContent, resolveDraftKey, resolveEffectiveComposerModelId, resolveEffectiveComposerThinkingOptionId, type DraftKeyInput, } from "@/hooks/use-agent-input-draft-core"; import { useDraftStore } from "@/stores/draft-store"; type AttachmentUpdater = | UserComposerAttachment[] | ((prev: UserComposerAttachment[]) => UserComposerAttachment[]); interface AgentInputDraftComposerOptions { initialServerId: string | null; initialValues?: CreateAgentInitialValues; isVisible?: boolean; onlineServerIds?: string[]; lockedWorkingDir?: string; } interface UseAgentInputDraftInput { draftKey: DraftKeyInput; initialCwd?: string; composer?: AgentInputDraftComposerOptions; } type DraftComposerState = UseAgentFormStateResult & { workingDir: string; effectiveModelId: string; effectiveThinkingOptionId: string; featureValues: Record | undefined; statusControls: DraftAgentStatusBarProps; commandDraftConfig: DraftCommandConfig | undefined; }; interface AgentInputDraft { text: string; setText: (text: string) => void; attachments: UserComposerAttachment[]; setAttachments: (updater: AttachmentUpdater) => void; cwd: string; setCwd: (cwd: string) => void; clear: (lifecycle: "sent" | "abandoned") => void; isHydrated: boolean; composerState: DraftComposerState | null; } export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDraft { const composerOptions = input.composer ?? null; const formState = useAgentFormState({ initialServerId: composerOptions?.initialServerId ?? null, initialValues: composerOptions?.initialValues, isVisible: composerOptions?.isVisible ?? false, isCreateFlow: true, onlineServerIds: composerOptions?.onlineServerIds ?? [], }); const draftKey = useMemo( () => resolveDraftKey({ draftKey: input.draftKey, selectedServerId: formState.selectedServerId, }), [formState.selectedServerId, input.draftKey], ); const [text, setText] = useState(""); const [attachments, setAttachmentsState] = useState([]); const [cwd, setCwd] = useState(input.initialCwd ?? ""); const [isHydrated, setIsHydrated] = useState(false); const draftGenerationRef = useRef(0); const hydratedGenerationRef = useRef(0); const setAttachments = useCallback((updater: AttachmentUpdater) => { setAttachmentsState((previousAttachments) => { if (typeof updater === "function") { return updater(previousAttachments); } return updater; }); }, []); const clear = useCallback( (lifecycle: "sent" | "abandoned") => { const store = useDraftStore.getState(); store.clearDraftInput({ draftKey, lifecycle }); const generation = store.beginDraftGeneration(draftKey); draftGenerationRef.current = generation; hydratedGenerationRef.current = generation; setText(""); setAttachmentsState([]); setCwd(""); setIsHydrated(true); }, [draftKey], ); useEffect(() => { const store = useDraftStore.getState(); const generation = store.beginDraftGeneration(draftKey); draftGenerationRef.current = generation; hydratedGenerationRef.current = 0; setText(""); setAttachmentsState([]); setCwd(input.initialCwd ?? ""); setIsHydrated(false); let cancelled = false; void (async () => { const draft = await store.hydrateDraftInput({ draftKey, initialCwd: input.initialCwd, }); if (cancelled) { return; } if (!useDraftStore.getState().isDraftGenerationCurrent({ draftKey, generation })) { return; } if (draft) { setText(draft.text); setAttachmentsState(draft.attachments); setCwd(draft.cwd); } hydratedGenerationRef.current = generation; setIsHydrated(true); })(); return () => { cancelled = true; }; }, [draftKey, input.initialCwd]); useEffect(() => { const currentGeneration = draftGenerationRef.current; if (currentGeneration <= 0) { return; } const store = useDraftStore.getState(); const isCurrentGeneration = store.isDraftGenerationCurrent({ draftKey, generation: currentGeneration, }); if (!isCurrentGeneration) { return; } if (hydratedGenerationRef.current !== currentGeneration) { return; } const existing = store.getDraftInput(draftKey); const isSameDraft = existing !== undefined && existing.text === text && existing.cwd === cwd && areAttachmentsEqual({ left: existing.attachments, right: attachments, }); if (isSameDraft) { return; } if (!hasDraftContent({ text, attachments, cwd })) { if (existing) { store.clearDraftInput({ draftKey, lifecycle: "abandoned" }); } return; } store.saveDraftInput({ draftKey, draft: { text, attachments, cwd, }, }); }, [attachments, cwd, draftKey, text]); const lockedWorkingDir = composerOptions?.lockedWorkingDir?.trim() ?? ""; useEffect(() => { if (!composerOptions || !lockedWorkingDir) { return; } if (formState.workingDir.trim() === lockedWorkingDir) { return; } formState.setWorkingDir(lockedWorkingDir); }, [composerOptions, formState, lockedWorkingDir]); const effectiveModelId = useMemo( () => resolveEffectiveComposerModelId({ selectedModel: formState.selectedModel, availableModels: formState.availableModels, }), [formState.availableModels, formState.selectedModel], ); const effectiveThinkingOptionId = useMemo( () => resolveEffectiveComposerThinkingOptionId({ selectedThinkingOptionId: formState.selectedThinkingOptionId, availableModels: formState.availableModels, effectiveModelId, }), [effectiveModelId, formState.availableModels, formState.selectedThinkingOptionId], ); const workingDir = lockedWorkingDir || formState.workingDir; const { features: draftFeatures, featureValues: draftFeatureValues, setFeatureValue: setDraftFeatureValue, } = useDraftAgentFeatures({ serverId: formState.selectedServerId, provider: formState.selectedProvider, cwd: workingDir, modeId: formState.selectedMode, modelId: effectiveModelId, thinkingOptionId: effectiveThinkingOptionId, }); const commandDraftConfig = useMemo( () => composerOptions ? buildDraftComposerCommandConfig({ provider: formState.selectedProvider, cwd: workingDir, modeOptions: formState.modeOptions, selectedMode: formState.selectedMode, effectiveModelId, effectiveThinkingOptionId, featureValues: draftFeatureValues, }) : undefined, [ composerOptions, effectiveModelId, effectiveThinkingOptionId, draftFeatureValues, workingDir, formState.modeOptions, formState.selectedMode, formState.selectedProvider, ], ); const composerState = useMemo(() => { if (!composerOptions) { return null; } return { ...formState, workingDir, effectiveModelId, effectiveThinkingOptionId, featureValues: draftFeatureValues, statusControls: buildDraftStatusControls({ formState, features: draftFeatures, onSetFeature: setDraftFeatureValue, }), commandDraftConfig, }; }, [ commandDraftConfig, composerOptions, effectiveModelId, effectiveThinkingOptionId, draftFeatures, draftFeatureValues, formState, setDraftFeatureValue, workingDir, ]); return { text, setText, attachments, setAttachments, cwd, setCwd, clear, isHydrated, composerState, }; } export const __private__ = { resolveDraftKey, resolveEffectiveComposerModelId, resolveEffectiveComposerThinkingOptionId, buildDraftComposerCommandConfig, buildDraftStatusControls, };