import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { fetchModels, updateTask } from "../api"; import type { ModelInfo } from "../api"; import type { Task, TaskDetail } from "@fusion/core"; import type { ToastType } from "../hooks/useToast"; import { CustomModelDropdown } from "./CustomModelDropdown"; import { ProviderIcon } from "./ProviderIcon"; interface ModelSelectorTabProps { task: Task | TaskDetail; addToast: (message: string, type?: ToastType) => void; } interface ModelSelection { provider?: string; modelId?: string; } function normalizeModelField(value: string | null | undefined): string | undefined { return value ?? undefined; } function getExecutorSelection(task: Task | TaskDetail): ModelSelection { return { provider: normalizeModelField(task.modelProvider), modelId: normalizeModelField(task.modelId), }; } function getValidatorSelection(task: Task | TaskDetail): ModelSelection { return { provider: normalizeModelField(task.validatorModelProvider), modelId: normalizeModelField(task.validatorModelId), }; } function parseModelValue(value: string): ModelSelection { if (!value) { return { provider: undefined, modelId: undefined }; } const slashIdx = value.indexOf("/"); return { provider: value.slice(0, slashIdx), modelId: value.slice(slashIdx + 1), }; } function getDropdownValue(selection: ModelSelection): string { return selection.provider && selection.modelId ? `${selection.provider}/${selection.modelId}` : ""; } function selectionsEqual(a: ModelSelection, b: ModelSelection): boolean { return a.provider === b.provider && a.modelId === b.modelId; } function getSuccessToastMessage(target: "executor" | "validator", selection: ModelSelection): string { const label = target === "executor" ? "Executor" : "Validator"; if (!selection.provider || !selection.modelId) { return `${label} model set to default`; } return `${label} model set to ${selection.provider}/${selection.modelId}`; } export function ModelSelectorTab({ task, addToast }: ModelSelectorTabProps) { const [availableModels, setAvailableModels] = useState([]); const [modelsLoading, setModelsLoading] = useState(false); const [modelsError, setModelsError] = useState(null); const [selectedExecutor, setSelectedExecutor] = useState(() => getExecutorSelection(task)); const [savedExecutor, setSavedExecutor] = useState(() => getExecutorSelection(task)); const [selectedValidator, setSelectedValidator] = useState(() => getValidatorSelection(task)); const [savedValidator, setSavedValidator] = useState(() => getValidatorSelection(task)); const [savingTarget, setSavingTarget] = useState<"executor" | "validator" | null>(null); const activeTaskIdRef = useRef(task.id); // Load available models on mount useEffect(() => { setModelsLoading(true); setModelsError(null); fetchModels() .then((models) => { setAvailableModels(models); }) .catch((err) => { setModelsError(err.message || "Failed to load models"); }) .finally(() => { setModelsLoading(false); }); }, []); useEffect(() => { activeTaskIdRef.current = task.id; const nextExecutor = getExecutorSelection(task); const nextValidator = getValidatorSelection(task); setSelectedExecutor(nextExecutor); setSavedExecutor(nextExecutor); setSelectedValidator(nextValidator); setSavedValidator(nextValidator); setSavingTarget(null); }, [task.id, task.modelProvider, task.modelId, task.validatorModelProvider, task.validatorModelId]); const executorValue = useMemo(() => getDropdownValue(selectedExecutor), [selectedExecutor]); const validatorValue = useMemo(() => getDropdownValue(selectedValidator), [selectedValidator]); const isSaving = savingTarget !== null; const saveSelection = useCallback( async (target: "executor" | "validator", nextSelection: ModelSelection) => { const requestTaskId = task.id; const previousSavedExecutor = savedExecutor; const previousSavedValidator = savedValidator; setSavingTarget(target); try { const updatedTask = await updateTask(requestTaskId, { modelProvider: target === "executor" ? nextSelection.provider ?? null : previousSavedExecutor.provider ?? null, modelId: target === "executor" ? nextSelection.modelId ?? null : previousSavedExecutor.modelId ?? null, validatorModelProvider: target === "validator" ? nextSelection.provider ?? null : previousSavedValidator.provider ?? null, validatorModelId: target === "validator" ? nextSelection.modelId ?? null : previousSavedValidator.modelId ?? null, }); if (activeTaskIdRef.current !== requestTaskId) { return; } const nextSavedExecutor = getExecutorSelection(updatedTask); const nextSavedValidator = getValidatorSelection(updatedTask); setSavedExecutor(nextSavedExecutor); setSelectedExecutor(nextSavedExecutor); setSavedValidator(nextSavedValidator); setSelectedValidator(nextSavedValidator); addToast( getSuccessToastMessage( target, target === "executor" ? nextSavedExecutor : nextSavedValidator, ), "success", ); } catch (err: any) { if (activeTaskIdRef.current !== requestTaskId) { return; } if (target === "executor") { setSelectedExecutor(previousSavedExecutor); } else { setSelectedValidator(previousSavedValidator); } addToast(err.message || "Failed to save model settings", "error"); } finally { if (activeTaskIdRef.current === requestTaskId) { setSavingTarget(null); } } }, [task.id, savedExecutor, savedValidator, addToast], ); const handleExecutorChange = useCallback( (value: string) => { const nextSelection = parseModelValue(value); setSelectedExecutor(nextSelection); if (selectionsEqual(nextSelection, savedExecutor)) { return; } void saveSelection("executor", nextSelection); }, [savedExecutor, saveSelection], ); const handleValidatorChange = useCallback( (value: string) => { const nextSelection = parseModelValue(value); setSelectedValidator(nextSelection); if (selectionsEqual(nextSelection, savedValidator)) { return; } void saveSelection("validator", nextSelection); }, [savedValidator, saveSelection], ); const executorUsingDefault = !savedExecutor.provider && !savedExecutor.modelId; const validatorUsingDefault = !savedValidator.provider && !savedValidator.modelId; return (

Model Configuration

Override the AI models used for this task. When not specified, global default settings are used.

{modelsLoading ? (
Loading available models…
) : modelsError ? (
Error loading models: {modelsError}
) : availableModels.length === 0 ? (
No models available. Configure authentication in Settings to enable model selection.
) : ( <>
{executorUsingDefault ? ( Using default ) : ( {savedExecutor.provider && } {savedExecutor.provider}/{savedExecutor.modelId} )}
The AI model used to implement this task.
{validatorUsingDefault ? ( Using default ) : ( {savedValidator.provider && } {savedValidator.provider}/{savedValidator.modelId} )}
The AI model used to review code and plans for this task.
{executorUsingDefault && validatorUsingDefault ? "Using global default models." : "Model settings are up to date."}
)}
); }