import { useState, useEffect, useCallback, useMemo } from "react"; import { fetchModels, updateTask } from "../api"; import type { ModelInfo } from "../api"; import type { Task, TaskDetail } from "@kb/core"; import type { ToastType } from "../hooks/useToast"; import { filterModels } from "../utils/modelFilter"; interface ModelSelectorTabProps { task: Task | TaskDetail; addToast: (message: string, type?: ToastType) => void; } export function ModelSelectorTab({ task, addToast }: ModelSelectorTabProps) { const [availableModels, setAvailableModels] = useState([]); const [modelsLoading, setModelsLoading] = useState(false); const [modelsError, setModelsError] = useState(null); // Filter states for executor and validator const [executorFilter, setExecutorFilter] = useState(""); const [validatorFilter, setValidatorFilter] = useState(""); // Local state for selections (not saved until user clicks Save) const [executorProvider, setExecutorProvider] = useState(task.modelProvider); const [executorModelId, setExecutorModelId] = useState(task.modelId); const [validatorProvider, setValidatorProvider] = useState(task.validatorModelProvider); const [validatorModelId, setValidatorModelId] = useState(task.validatorModelId); const [isSaving, setIsSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); // 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); }); }, []); // Track if selections differ from task's saved values useEffect(() => { const executorChanged = executorProvider !== task.modelProvider || executorModelId !== task.modelId; const validatorChanged = validatorProvider !== task.validatorModelProvider || validatorModelId !== task.validatorModelId; setHasChanges(executorChanged || validatorChanged); }, [executorProvider, executorModelId, validatorProvider, validatorModelId, task]); // Filtered models for executor and validator const filteredExecutorModels = useMemo(() => filterModels(availableModels, executorFilter), [availableModels, executorFilter]); const filteredValidatorModels = useMemo(() => filterModels(availableModels, validatorFilter), [availableModels, validatorFilter]); // Group filtered models by provider const executorModelsByProvider = useMemo(() => { return filteredExecutorModels.reduce>((acc, m) => { (acc[m.provider] ??= []).push(m); return acc; }, {}); }, [filteredExecutorModels]); const validatorModelsByProvider = useMemo(() => { return filteredValidatorModels.reduce>((acc, m) => { (acc[m.provider] ??= []).push(m); return acc; }, {}); }, [filteredValidatorModels]); // Build select values (provider/id combination or empty for default) const executorValue = executorProvider && executorModelId ? `${executorProvider}/${executorModelId}` : ""; const validatorValue = validatorProvider && validatorModelId ? `${validatorProvider}/${validatorModelId}` : ""; const handleExecutorChange = useCallback((value: string) => { if (!value) { setExecutorProvider(undefined); setExecutorModelId(undefined); } else { const slashIdx = value.indexOf("/"); setExecutorProvider(value.slice(0, slashIdx)); setExecutorModelId(value.slice(slashIdx + 1)); } }, []); const handleValidatorChange = useCallback((value: string) => { if (!value) { setValidatorProvider(undefined); setValidatorModelId(undefined); } else { const slashIdx = value.indexOf("/"); setValidatorProvider(value.slice(0, slashIdx)); setValidatorModelId(value.slice(slashIdx + 1)); } }, []); const handleSave = useCallback(async () => { setIsSaving(true); try { await updateTask(task.id, { modelProvider: executorProvider, modelId: executorModelId, validatorModelProvider: validatorProvider, validatorModelId: validatorModelId, }); addToast("Model settings saved", "success"); setHasChanges(false); } catch (err: any) { addToast(err.message || "Failed to save model settings", "error"); } finally { setIsSaving(false); } }, [task.id, executorProvider, executorModelId, validatorProvider, validatorModelId, addToast]); const handleReset = useCallback(() => { setExecutorProvider(task.modelProvider); setExecutorModelId(task.modelId); setValidatorProvider(task.validatorModelProvider); setValidatorModelId(task.validatorModelId); }, [task]); // Check if using defaults (both provider and modelId are undefined) const executorUsingDefault = !task.modelProvider && !task.modelId; const validatorUsingDefault = !task.validatorModelProvider && !task.validatorModelId; 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.
) : ( <> {/* Executor Model Selector */}
{executorUsingDefault ? ( Using default ) : ( {task.modelProvider}/{task.modelId} )}
{/* Filter input for executor */}
setExecutorFilter(e.target.value)} disabled={isSaving} /> {executorFilter && ( )} {filteredExecutorModels.length} model{filteredExecutorModels.length !== 1 ? "s" : ""}
{filteredExecutorModels.length === 0 && executorFilter && (
No models match '{executorFilter}'
)} The AI model used to implement this task.
{/* Validator Model Selector */}
{validatorUsingDefault ? ( Using default ) : ( {task.validatorModelProvider}/{task.validatorModelId} )}
{/* Filter input for validator */}
setValidatorFilter(e.target.value)} disabled={isSaving} /> {validatorFilter && ( )} {filteredValidatorModels.length} model{filteredValidatorModels.length !== 1 ? "s" : ""}
{filteredValidatorModels.length === 0 && validatorFilter && (
No models match '{validatorFilter}'
)} The AI model used to review code and plans for this task.
{/* Action buttons */}
{!hasChanges && (
{executorUsingDefault && validatorUsingDefault ? "Using global default models." : "Model settings are up to date."}
)} )}
); }