import type { IconifyIcon } from "@iconify/react/offline"; import { useEffect, useRef, useState, type ReactNode } from "react"; import Sortable from "sortablejs"; import type { Model, PluginModelDescriptor } from "../../shared/api"; import { Card } from "../../shared/ui/Card"; import { Icon } from "../../shared/ui/Icon"; import { chevronDownIcon, chevronRightIcon, claudeIcon, dragIcon, editIcon, flatColorOrganizationIcon, openAiIcon } from "../../shared/ui/icons"; import { TruncatedButton } from "../../shared/ui/TruncatedButton"; import { CursorModelTestResult, type CursorModelTestState } from "./CursorModelTestResult"; import styles from "./CursorSettings.module.scss"; export type CursorModelGrouping = "flat" | "provider" | "type"; export type CursorModelGroup = { key: string; label: string; icon: IconifyIcon; models: Model[]; }; type CursorModelCardsProps = { models: Model[]; pluginModels: PluginModelDescriptor[]; grouping: CursorModelGrouping; disabled: boolean; testingModelHashes: Set; testResults: Map; onTest: (model: Model) => void; onEdit: (model: Model) => void; onDuplicate: (model: Model) => void; onDelete: (model: Model) => void; onTestPluginModel: (model: PluginModelDescriptor) => void; onPluginSettings: (model: PluginModelDescriptor) => void; onReorder: (modelHashes: string[]) => void; onGroupSettings: (group: CursorModelGroup) => void; }; type ModelGridProps = Omit & { sortable: boolean; }; export function cursorModelGroups(models: Model[], grouping: Exclude): CursorModelGroup[] { const groups = new Map(); for (const model of models) { const descriptor = grouping === "provider" ? providerGroup(model) : typeGroup(model); const group = groups.get(descriptor.key); if (group) { group.models.push(model); } else { groups.set(descriptor.key, { ...descriptor, models: [model] }); } } return [...groups.values()]; } export function CursorModelCards(props: CursorModelCardsProps) { const builtins = props.grouping === "flat" ?
:
{cursorModelGroups(props.models, props.grouping).map((group) => props.onGroupSettings(group) : undefined} > {group.models.map((model) => props.onTest(model)} onEdit={() => props.onEdit(model)} onDuplicate={() => props.onDuplicate(model)} onDelete={() => props.onDelete(model)} />)} )}
; return
{builtins} {pluginGroups(props.pluginModels).map((group) => {group.models.map((model) => props.onTestPluginModel(model)} onSettings={() => props.onPluginSettings(model)} />)} )}
; } function pluginGroups(models: PluginModelDescriptor[]) { const groups: { pluginId: string; pluginName: string; icon: string; models: PluginModelDescriptor[] }[] = []; for (const model of models) { let group = groups.find((candidate) => candidate.pluginId === model.pluginId); if (!group) { group = { pluginId: model.pluginId, pluginName: model.pluginName, icon: model.icon, models: [] }; groups.push(group); } group.models.push(model); } return groups; } function CollapsibleGroup({ label, icon, iconSrc, defaultOpen = true, onSettings, children }: { label: string; icon?: IconifyIcon; iconSrc?: string; defaultOpen?: boolean; onSettings?: () => void; children: ReactNode; }) { const [open, setOpen] = useState(defaultOpen); return
{onSettings && }
{open &&
{children}
}
; } function ModelListRow({ model, disabled, testing, result, onTest, onEdit, onDuplicate, onDelete }: { model: Model; disabled: boolean; testing: boolean; result: CursorModelTestState | undefined; onTest: () => void; onEdit: () => void; onDuplicate: () => void; onDelete: () => void; }) { return
{model.display_name} {model.model_id}
; } function PluginModelRow({ model, disabled, testing, result, onTest, onSettings }: { model: PluginModelDescriptor; disabled: boolean; testing: boolean; result: CursorModelTestState | undefined; onTest: () => void; onSettings: () => void; }) { return
{model.displayName} {model.modelId}
; } function ModelGrid({ models, sortable: sortableEnabled, disabled, testingModelHashes, testResults, onTest, onEdit, onDuplicate, onDelete, onReorder, }: ModelGridProps) { const grid = useRef(null); const sortable = useRef(null); const currentModels = useRef(models); const reorder = useRef(onReorder); currentModels.current = models; reorder.current = onReorder; useEffect(() => { if (!sortableEnabled || !grid.current) return; sortable.current = Sortable.create(grid.current, { animation: 160, dataIdAttr: "data-model-hash", draggable: `.${styles.modelCard}`, handle: `.${styles.sortHandle}`, ghostClass: styles.sortGhost, chosenClass: styles.sortChosen, dragClass: styles.sortDragging, forceFallback: true, fallbackOnBody: true, fallbackTolerance: 3, onEnd: (event) => { const oldIndex = event.oldDraggableIndex ?? event.oldIndex; const newIndex = event.newDraggableIndex ?? event.newIndex; if (typeof oldIndex !== "number" || typeof newIndex !== "number" || oldIndex === newIndex) { sortable.current?.sort(currentModels.current.map((model) => model.model_hash), false); return; } const reordered = currentModels.current.slice(); const [moved] = reordered.splice(oldIndex, 1); if (!moved || newIndex < 0 || newIndex > reordered.length) { sortable.current?.sort(currentModels.current.map((model) => model.model_hash), false); return; } reordered.splice(newIndex, 0, moved); reorder.current(reordered.map((model) => model.model_hash)); }, }); return () => { sortable.current?.destroy(); sortable.current = null; }; }, [sortableEnabled]); useEffect(() => { sortable.current?.option("disabled", disabled); sortable.current?.sort(models.map((model) => model.model_hash), false); }, [disabled, models]); return
{models.map((model) => { const result = testResults.get(model.model_hash); const testing = testingModelHashes.has(model.model_hash); return {sortableEnabled && }
{model.display_name} {model.model_id}
{model.type === "anthropic" ? "Anthropic" : "OpenAI"}
onTest(model)} /> onEdit(model)} /> onDuplicate(model)} /> onDelete(model)} />
; })}
; } function providerGroup(model: Model) { const key = providerDomain(model.base_url); const label = model.group_name?.trim() || key; return { key, label, icon: flatColorOrganizationIcon }; } function providerDomain(baseUrl: string) { const value = baseUrl.trim(); try { return new URL(value).hostname.toLowerCase() || value; } catch { try { return new URL(`https://${value}`).hostname.toLowerCase() || value; } catch { return value; } } } function typeGroup(model: Model) { if (model.type === "anthropic") return { key: "anthropic", label: "Anthropic", icon: claudeIcon }; if (model.openai_endpoint === "/v1/chat/completions") return { key: "openai-chat", label: "OpenAI Chat", icon: openAiIcon }; return { key: "openai-responses", label: "OpenAI Responses", icon: openAiIcon }; }