"use client"; import { useEffect } from "react"; import type { AgentProfileOption } from "@/lib/state/slices"; import { getLocalStorage } from "@/lib/local-storage"; import { STORAGE_KEYS } from "@/lib/settings/constants"; import { createDebugLogger } from "@/lib/debug/log"; import type { DialogFormState, StoreSelections } from "@/components/task-create-dialog-types"; /** * Agent-profile autopick: the logic that decides which profile to pre-select * when the task-create dialog opens (or when its inputs change). Lives in its * own file so `task-create-dialog-effects.ts` stays under the 600-line lint * cap. Tests in `task-create-dialog-effects.test.ts` exercise both effects * through the same import surface re-exported from there. */ const autopickDebug = createDebugLogger("executor-compat:autopick"); const workflowAutopickDebug = createDebugLogger("executor-compat:workflow-autopick"); /** * Pure decision function for the agent-profile autopick. Extracted so the * effect can stay below the 100-line lint cap once the autopick trace logs * are inlined, and so the same decision can be tested without rendering. * * Order: lastId (localStorage) → defId (workspace default) → first * compatible. Every candidate is filtered against `compatibleAgentProfiles` * so a previously-used profile that's not wired for the chosen executor * isn't restored, then immediately fails the executor-compat gate. */ export type AutopickDecision = | { kind: "skip"; reason: string } | { kind: "defer"; reason: string } | { kind: "pick"; source: "lastId" | "defId" | "first"; id: string }; export function decideAgentProfileAutopick(input: { open: boolean; agentProfileId: string; workflowAgentProfileId: string; workflowHasAgent: boolean; agentProfiles: AgentProfileOption[]; compatibleAgentProfiles: AgentProfileOption[]; authLoaded: boolean; executorProfileId: string; hasExecutors: boolean; defaultAgentProfileId: string | null; }): AutopickDecision { if (!input.open) return { kind: "skip", reason: "closed" }; if (input.agentProfileId) return { kind: "skip", reason: "already-set" }; if (input.workflowAgentProfileId) return { kind: "skip", reason: "workflow-locked" }; if (input.workflowHasAgent) return { kind: "skip", reason: "workflow-has-agent" }; if (input.agentProfiles.length === 0) return { kind: "skip", reason: "no-profiles" }; if (!input.authLoaded) return { kind: "defer", reason: "auth-not-loaded" }; // Defer until the executor profile is selected too - useExecutorProfileCompat // short-circuits to the unfiltered list when `selectedExecutorProfile` is null, // so without this gate we'd happily restore an incompatible lastId during the // single render where `authLoaded` is already true but the executor // auto-select hasn't queued through yet. Only deferrable if there ARE // executors to pick from - otherwise the executor effect will never fire and // we'd defer forever. if (!input.executorProfileId && input.hasExecutors) { return { kind: "defer", reason: "executor-not-selected" }; } if (input.compatibleAgentProfiles.length === 0) return { kind: "skip", reason: "no-compatible" }; const lastId = getLocalStorage(STORAGE_KEYS.LAST_AGENT_PROFILE_ID, null); if (lastId && input.compatibleAgentProfiles.some((p) => p.id === lastId)) { return { kind: "pick", source: "lastId", id: lastId }; } const defId = input.defaultAgentProfileId; if (defId && input.compatibleAgentProfiles.some((p) => p.id === defId)) { return { kind: "pick", source: "defId", id: defId }; } return { kind: "pick", source: "first", id: input.compatibleAgentProfiles[0].id }; } export function useWorkflowAgentProfileEffect( fs: DialogFormState, workflows: Array<{ id: string; agent_profile_id?: string }>, agentProfiles: AgentProfileOption[], compatibleAgentProfiles: AgentProfileOption[], ) { const { selectedWorkflowId, setAgentProfileId, setWorkflowAgentProfileId } = fs; useEffect(() => { if (!selectedWorkflowId) { setWorkflowAgentProfileId(""); workflowAutopickDebug("no-workflow", { cleared: "workflowAgentProfileId" }); return; } const workflow = workflows.find((w) => w.id === selectedWorkflowId); if (workflow?.agent_profile_id) { // Always lock the selector when the workflow specifies an agent profile. // This prevents the race condition where agentProfiles hasn't loaded yet. setWorkflowAgentProfileId(workflow.agent_profile_id); // Only set the agentProfileId once the profile is confirmed available. const profileExists = agentProfiles.some((p) => p.id === workflow.agent_profile_id); if (profileExists) { setAgentProfileId(workflow.agent_profile_id); workflowAutopickDebug("locked", { workflow: selectedWorkflowId, set_to: workflow.agent_profile_id, }); } else { workflowAutopickDebug("locked-missing", { workflow: selectedWorkflowId, missing: workflow.agent_profile_id, }); } } else { setWorkflowAgentProfileId(""); // Restore the user's last-used agent profile when unlocking. Filter // against `compatibleAgentProfiles` (not the full `agentProfiles` list) // so an executor-incompatible id from a previous session - including // stale UUIDs from a wiped DB - is dropped rather than restored. // useDefaultSelectionsEffect would otherwise see agentProfileId become // truthy, early-exit on "already-set", and leave the dialog stuck on // "No compatible agent profiles". const lastId = getLocalStorage(STORAGE_KEYS.LAST_AGENT_PROFILE_ID, null); const isValidLastId = Boolean(lastId && compatibleAgentProfiles.some((p) => p.id === lastId)); const finalId = isValidLastId && lastId ? lastId : ""; setAgentProfileId(finalId); workflowAutopickDebug("workflow-no-override", { last_id: lastId ?? "-", valid: isValidLastId, set_to: finalId || "-empty-", }); } }, [ selectedWorkflowId, workflows, agentProfiles, compatibleAgentProfiles, setAgentProfileId, setWorkflowAgentProfileId, ]); } export function useAgentProfileAutopickEffect( fs: DialogFormState, open: boolean, sel: StoreSelections, workflows: Array<{ id: string; agent_profile_id?: string }>, ) { const { agentProfiles, compatibleAgentProfiles, authLoaded, executors, workspaceDefaults } = sel; const { agentProfileId, workflowAgentProfileId, selectedWorkflowId, executorProfileId, setAgentProfileId, } = fs; useEffect(() => { // Check synchronously whether the selected workflow has an agent override. // This avoids a race condition where workflowAgentProfileId state hasn't // been committed yet by the workflow effect running in the same cycle. const workflowHasAgent = selectedWorkflowId ? workflows.some((w) => w.id === selectedWorkflowId && w.agent_profile_id) : false; const decision = decideAgentProfileAutopick({ open, agentProfileId, workflowAgentProfileId, workflowHasAgent, agentProfiles, compatibleAgentProfiles, authLoaded, executorProfileId, hasExecutors: executors.length > 0, defaultAgentProfileId: workspaceDefaults?.default_agent_profile_id ?? null, }); autopickDebug(decision.kind, { reason: decision.kind === "pick" ? decision.source : decision.reason, pick: decision.kind === "pick" ? decision.id : "-", current: agentProfileId || "-", workflow_id: selectedWorkflowId ?? "-", executor_profile_id: executorProfileId || "-", agent_count: agentProfiles.length, compat_count: compatibleAgentProfiles.length, auth_loaded: authLoaded, }); if (decision.kind === "pick") { const id = decision.id; void Promise.resolve().then(() => setAgentProfileId(id)); } }, [ open, agentProfileId, workflowAgentProfileId, selectedWorkflowId, workflows, agentProfiles, compatibleAgentProfiles, authLoaded, executorProfileId, executors, workspaceDefaults, setAgentProfileId, ]); }