feat(ui): add favorite models to selector
This commit is contained in:
@@ -3,6 +3,6 @@
|
|||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/plugin": "1.1.30"
|
"@opencode-ai/plugin": "1.1.36"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const PreferencesSchema = z.object({
|
|||||||
locale: z.string().optional(),
|
locale: z.string().optional(),
|
||||||
environmentVariables: z.record(z.string()).default({}),
|
environmentVariables: z.record(z.string()).default({}),
|
||||||
modelRecents: z.array(ModelPreferenceSchema).default([]),
|
modelRecents: z.array(ModelPreferenceSchema).default([]),
|
||||||
|
modelFavorites: z.array(ModelPreferenceSchema).default([]),
|
||||||
modelThinkingSelections: z.record(z.string(), z.string()).default({}),
|
modelThinkingSelections: z.record(z.string(), z.string()).default({}),
|
||||||
diffViewMode: z.enum(["split", "unified"]).default("split"),
|
diffViewMode: z.enum(["split", "unified"]).default("split"),
|
||||||
toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Combobox } from "@kobalte/core/combobox"
|
import { Combobox } from "@kobalte/core/combobox"
|
||||||
import { createEffect, createMemo, createSignal } from "solid-js"
|
import { createEffect, createMemo, createSignal } from "solid-js"
|
||||||
import { providers, fetchProviders } from "../stores/sessions"
|
import { providers, fetchProviders } from "../stores/sessions"
|
||||||
import { ChevronDown } from "lucide-solid"
|
import { ChevronDown, Star } from "lucide-solid"
|
||||||
import type { Model } from "../types/session"
|
import type { Model } from "../types/session"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
|
import { preferences, toggleFavoriteModelPreference } from "../stores/preferences"
|
||||||
import Kbd from "./kbd"
|
import Kbd from "./kbd"
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
@@ -26,8 +27,19 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const instanceProviders = () => providers().get(props.instanceId) || []
|
const instanceProviders = () => providers().get(props.instanceId) || []
|
||||||
const [isOpen, setIsOpen] = createSignal(false)
|
const [isOpen, setIsOpen] = createSignal(false)
|
||||||
|
const [manualAll, setManualAll] = createSignal(false)
|
||||||
|
const [explicitFavorites, setExplicitFavorites] = createSignal(false)
|
||||||
|
const [autoFavoritesEligibleAtOpen, setAutoFavoritesEligibleAtOpen] = createSignal(false)
|
||||||
|
const [searchDirty, setSearchDirty] = createSignal(false)
|
||||||
|
const [initialQuery, setInitialQuery] = createSignal("")
|
||||||
|
const [initialQueryReady, setInitialQueryReady] = createSignal(false)
|
||||||
|
const [inputValue, setInputValue] = createSignal("")
|
||||||
let triggerRef!: HTMLButtonElement
|
let triggerRef!: HTMLButtonElement
|
||||||
let searchInputRef!: HTMLInputElement
|
let searchInputRef!: HTMLInputElement
|
||||||
|
let listboxRef!: HTMLUListElement
|
||||||
|
let suppressNextClose = false
|
||||||
|
let wasFavoritesOnlyEnabled = false
|
||||||
|
let wasCurrentModelFavorite = false
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (instanceProviders().length === 0) {
|
if (instanceProviders().length === 0) {
|
||||||
@@ -46,61 +58,232 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const favoriteKeySet = createMemo(() => {
|
||||||
|
const result = new Set<string>()
|
||||||
|
for (const item of preferences().modelFavorites ?? []) {
|
||||||
|
if (item.providerId && item.modelId) {
|
||||||
|
result.add(`${item.providerId}/${item.modelId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
const favoriteModels = createMemo<FlatModel[]>(() => {
|
||||||
|
const keys = favoriteKeySet()
|
||||||
|
if (keys.size === 0) return []
|
||||||
|
return allModels().filter((m) => keys.has(m.key))
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasFavorites = createMemo(() => favoriteModels().length > 0)
|
||||||
|
|
||||||
const currentModelValue = createMemo(() =>
|
const currentModelValue = createMemo(() =>
|
||||||
allModels().find((m) => m.providerId === props.currentModel.providerId && m.id === props.currentModel.modelId),
|
allModels().find((m) => m.providerId === props.currentModel.providerId && m.id === props.currentModel.modelId),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const currentModelIsFavorite = createMemo(() => {
|
||||||
|
const current = props.currentModel
|
||||||
|
return favoriteKeySet().has(`${current.providerId}/${current.modelId}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentModelKey = createMemo(() => {
|
||||||
|
const current = props.currentModel
|
||||||
|
return `${current.providerId}/${current.modelId}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const searchActive = createMemo(() => {
|
||||||
|
if (!searchDirty()) return false
|
||||||
|
const next = inputValue().trim()
|
||||||
|
return next.length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const favoritesOnlyEnabled = createMemo(() => {
|
||||||
|
if (searchActive()) return false
|
||||||
|
if (manualAll()) return false
|
||||||
|
if (!hasFavorites()) return false
|
||||||
|
return explicitFavorites() || autoFavoritesEligibleAtOpen()
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleOptions = createMemo<FlatModel[]>(() => {
|
||||||
|
if (!favoritesOnlyEnabled()) {
|
||||||
|
return allModels()
|
||||||
|
}
|
||||||
|
return favoriteModels()
|
||||||
|
})
|
||||||
|
|
||||||
const handleChange = async (value: FlatModel | null) => {
|
const handleChange = async (value: FlatModel | null) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
await props.onModelChange({ providerId: value.providerId, modelId: value.id })
|
await props.onModelChange({ providerId: value.providerId, modelId: value.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
const customFilter = (option: FlatModel, inputValue: string) => {
|
const customFilter = (option: FlatModel, rawInput: string) => {
|
||||||
return option.searchText.toLowerCase().includes(inputValue.toLowerCase())
|
if (!searchDirty()) return true
|
||||||
|
return option.searchText.toLowerCase().includes(rawInput.toLowerCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (isOpen()) {
|
if (isOpen()) {
|
||||||
|
setManualAll(false)
|
||||||
|
setExplicitFavorites(false)
|
||||||
|
setAutoFavoritesEligibleAtOpen(hasFavorites() && currentModelIsFavorite())
|
||||||
|
setSearchDirty(false)
|
||||||
|
setInitialQuery("")
|
||||||
|
setInputValue("")
|
||||||
|
setInitialQueryReady(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
const seeded = searchInputRef?.value ?? ""
|
||||||
|
setInitialQuery(seeded)
|
||||||
|
setInputValue(seeded)
|
||||||
|
setInitialQueryReady(true)
|
||||||
searchInputRef?.focus()
|
searchInputRef?.focus()
|
||||||
|
searchInputRef?.select()
|
||||||
}, 100)
|
}, 100)
|
||||||
|
} else {
|
||||||
|
setInitialQueryReady(false)
|
||||||
|
setSearchDirty(false)
|
||||||
|
setAutoFavoritesEligibleAtOpen(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!isOpen()) {
|
||||||
|
wasFavoritesOnlyEnabled = favoritesOnlyEnabled()
|
||||||
|
wasCurrentModelFavorite = currentModelIsFavorite()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nowFavoritesOnlyEnabled = favoritesOnlyEnabled()
|
||||||
|
const nowCurrentModelFavorite = currentModelIsFavorite()
|
||||||
|
|
||||||
|
if (wasFavoritesOnlyEnabled && !nowFavoritesOnlyEnabled && wasCurrentModelFavorite && !nowCurrentModelFavorite) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const key = currentModelKey()
|
||||||
|
const target = listboxRef?.querySelector(`[data-key="${key}"]`) as HTMLElement | null
|
||||||
|
target?.scrollIntoView({ block: "nearest" })
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
wasFavoritesOnlyEnabled = nowFavoritesOnlyEnabled
|
||||||
|
wasCurrentModelFavorite = nowCurrentModelFavorite
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSearchInput = (event: InputEvent & { currentTarget: HTMLInputElement }) => {
|
||||||
|
const next = event.currentTarget.value
|
||||||
|
setInputValue(next)
|
||||||
|
if (!initialQueryReady()) return
|
||||||
|
if (searchDirty()) return
|
||||||
|
if (next !== initialQuery()) {
|
||||||
|
setSearchDirty(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preventListboxPress = (event: PointerEvent | MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopImmediatePropagation?.()
|
||||||
|
event.stopPropagation()
|
||||||
|
suppressNextClose = true
|
||||||
|
setTimeout(() => {
|
||||||
|
suppressNextClose = false
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFavoritesOnly = () => {
|
||||||
|
if (!hasFavorites()) return
|
||||||
|
if (searchActive()) return
|
||||||
|
|
||||||
|
if (favoritesOnlyEnabled()) {
|
||||||
|
setManualAll(true)
|
||||||
|
setExplicitFavorites(false)
|
||||||
|
setAutoFavoritesEligibleAtOpen(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setExplicitFavorites(true)
|
||||||
|
setManualAll(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showAllModels = () => {
|
||||||
|
setManualAll(true)
|
||||||
|
setExplicitFavorites(false)
|
||||||
|
setAutoFavoritesEligibleAtOpen(false)
|
||||||
|
setTimeout(() => searchInputRef?.focus(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="sidebar-selector">
|
<div class="sidebar-selector">
|
||||||
<Combobox<FlatModel>
|
<Combobox<FlatModel>
|
||||||
|
open={isOpen()}
|
||||||
value={currentModelValue()}
|
value={currentModelValue()}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onOpenChange={setIsOpen}
|
onOpenChange={(next) => {
|
||||||
options={allModels()}
|
if (!next && suppressNextClose) return
|
||||||
|
setIsOpen(next)
|
||||||
|
}}
|
||||||
|
options={visibleOptions()}
|
||||||
optionValue="key"
|
optionValue="key"
|
||||||
optionTextValue="searchText"
|
optionTextValue="searchText"
|
||||||
optionLabel="name"
|
optionLabel="name"
|
||||||
placeholder={t("modelSelector.placeholder.search")}
|
placeholder={t("modelSelector.placeholder.search")}
|
||||||
defaultFilter={customFilter}
|
defaultFilter={customFilter}
|
||||||
allowsEmptyCollection
|
allowsEmptyCollection
|
||||||
itemComponent={(itemProps) => (
|
itemComponent={(itemProps) => {
|
||||||
<Combobox.Item
|
const isFavorite = () => favoriteKeySet().has(itemProps.item.rawValue.key)
|
||||||
item={itemProps.item}
|
return (
|
||||||
class="selector-option"
|
<Combobox.Item
|
||||||
>
|
item={itemProps.item}
|
||||||
<div class="selector-option-content">
|
class="selector-option"
|
||||||
<Combobox.ItemLabel class="selector-option-label">
|
>
|
||||||
{itemProps.item.rawValue.name}
|
<>
|
||||||
</Combobox.ItemLabel>
|
<div class="selector-option-content">
|
||||||
<Combobox.ItemDescription class="selector-option-description">
|
<Combobox.ItemLabel class="selector-option-label">{itemProps.item.rawValue.name}</Combobox.ItemLabel>
|
||||||
{itemProps.item.rawValue.providerName} • {itemProps.item.rawValue.providerId}/
|
<Combobox.ItemDescription class="selector-option-description">
|
||||||
{itemProps.item.rawValue.id}
|
{itemProps.item.rawValue.providerName} • {itemProps.item.rawValue.providerId}/{itemProps.item.rawValue.id}
|
||||||
</Combobox.ItemDescription>
|
</Combobox.ItemDescription>
|
||||||
</div>
|
</div>
|
||||||
<Combobox.ItemIndicator class="selector-option-indicator">
|
<Combobox.ItemIndicator class="selector-option-indicator">
|
||||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</Combobox.ItemIndicator>
|
</Combobox.ItemIndicator>
|
||||||
</Combobox.Item>
|
<button
|
||||||
)}
|
type="button"
|
||||||
|
class="selector-option-star"
|
||||||
|
data-active={isFavorite()}
|
||||||
|
aria-label={
|
||||||
|
isFavorite()
|
||||||
|
? t("modelSelector.favorite.remove")
|
||||||
|
: t("modelSelector.favorite.add")
|
||||||
|
}
|
||||||
|
onPointerDown={preventListboxPress}
|
||||||
|
onPointerUp={preventListboxPress}
|
||||||
|
onMouseDown={preventListboxPress}
|
||||||
|
onMouseUp={preventListboxPress}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
suppressNextClose = true
|
||||||
|
setTimeout(() => {
|
||||||
|
suppressNextClose = false
|
||||||
|
}, 0)
|
||||||
|
}}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
toggleFavoriteModelPreference({
|
||||||
|
providerId: itemProps.item.rawValue.providerId,
|
||||||
|
modelId: itemProps.item.rawValue.id,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Star
|
||||||
|
class="w-4 h-4"
|
||||||
|
fill={isFavorite() ? "currentColor" : "none"}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
</Combobox.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Combobox.Control class="relative w-full" data-model-selector-control>
|
<Combobox.Control class="relative w-full" data-model-selector-control>
|
||||||
<Combobox.Input class="sr-only" data-model-selector />
|
<Combobox.Input class="sr-only" data-model-selector />
|
||||||
@@ -130,13 +313,53 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
|||||||
<Combobox.Portal>
|
<Combobox.Portal>
|
||||||
<Combobox.Content class="selector-popover">
|
<Combobox.Content class="selector-popover">
|
||||||
<div class="selector-search-container">
|
<div class="selector-search-container">
|
||||||
<Combobox.Input
|
<div class="selector-input-group">
|
||||||
ref={searchInputRef}
|
<Combobox.Input
|
||||||
class="selector-search-input"
|
ref={searchInputRef}
|
||||||
placeholder={t("modelSelector.placeholder.search")}
|
class="selector-search-input flex-1 min-w-0"
|
||||||
/>
|
placeholder={t("modelSelector.placeholder.search")}
|
||||||
|
onInput={handleSearchInput}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="selector-favorites-toggle"
|
||||||
|
aria-label={t("modelSelector.favoritesOnly.toggle.ariaLabel")}
|
||||||
|
aria-pressed={favoritesOnlyEnabled()}
|
||||||
|
disabled={!hasFavorites() || searchActive()}
|
||||||
|
data-active={favoritesOnlyEnabled()}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
toggleFavoritesOnly()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Star class="w-4 h-4" fill={favoritesOnlyEnabled() ? "currentColor" : "none"} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Combobox.Listbox ref={listboxRef} class="selector-listbox" />
|
||||||
|
<div class="selector-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="selector-option selector-option-action w-full"
|
||||||
|
style={{ display: favoritesOnlyEnabled() && !searchActive() ? "flex" : "none" }}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
showAllModels()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="selector-option-label">{t("modelSelector.favoritesOnly.showAll")}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Combobox.Listbox class="selector-listbox" />
|
|
||||||
</Combobox.Content>
|
</Combobox.Content>
|
||||||
</Combobox.Portal>
|
</Combobox.Portal>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export const settingsMessages = {
|
|||||||
"modelSelector.placeholder.search": "Search models...",
|
"modelSelector.placeholder.search": "Search models...",
|
||||||
"modelSelector.none": "None",
|
"modelSelector.none": "None",
|
||||||
"modelSelector.trigger.primary": "Model: {model}",
|
"modelSelector.trigger.primary": "Model: {model}",
|
||||||
|
"modelSelector.favoritesOnly.toggle.ariaLabel": "Toggle favorites only",
|
||||||
|
"modelSelector.favoritesOnly.showAll": "Show all models",
|
||||||
|
"modelSelector.favorite.add": "Add to favorites",
|
||||||
|
"modelSelector.favorite.remove": "Remove from favorites",
|
||||||
|
|
||||||
"thinkingSelector.variant.default": "Default",
|
"thinkingSelector.variant.default": "Default",
|
||||||
"thinkingSelector.label": "Thinking: {variant}",
|
"thinkingSelector.label": "Thinking: {variant}",
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export const settingsMessages = {
|
|||||||
"modelSelector.placeholder.search": "Buscar modelos...",
|
"modelSelector.placeholder.search": "Buscar modelos...",
|
||||||
"modelSelector.none": "Ninguno",
|
"modelSelector.none": "Ninguno",
|
||||||
"modelSelector.trigger.primary": "Modelo: {model}",
|
"modelSelector.trigger.primary": "Modelo: {model}",
|
||||||
|
"modelSelector.favoritesOnly.toggle.ariaLabel": "Alternar solo favoritos",
|
||||||
|
"modelSelector.favoritesOnly.showAll": "Mostrar todos los modelos",
|
||||||
|
"modelSelector.favorite.add": "Agregar a favoritos",
|
||||||
|
"modelSelector.favorite.remove": "Quitar de favoritos",
|
||||||
|
|
||||||
"thinkingSelector.variant.default": "Por defecto",
|
"thinkingSelector.variant.default": "Por defecto",
|
||||||
"thinkingSelector.label": "Pensamiento: {variant}",
|
"thinkingSelector.label": "Pensamiento: {variant}",
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export const settingsMessages = {
|
|||||||
"modelSelector.placeholder.search": "Rechercher des modèles...",
|
"modelSelector.placeholder.search": "Rechercher des modèles...",
|
||||||
"modelSelector.none": "Aucun",
|
"modelSelector.none": "Aucun",
|
||||||
"modelSelector.trigger.primary": "Modèle : {model}",
|
"modelSelector.trigger.primary": "Modèle : {model}",
|
||||||
|
"modelSelector.favoritesOnly.toggle.ariaLabel": "Basculer en favoris uniquement",
|
||||||
|
"modelSelector.favoritesOnly.showAll": "Afficher tous les modèles",
|
||||||
|
"modelSelector.favorite.add": "Ajouter aux favoris",
|
||||||
|
"modelSelector.favorite.remove": "Retirer des favoris",
|
||||||
|
|
||||||
"thinkingSelector.variant.default": "Par défaut",
|
"thinkingSelector.variant.default": "Par défaut",
|
||||||
"thinkingSelector.label": "Réflexion : {variant}",
|
"thinkingSelector.label": "Réflexion : {variant}",
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export const settingsMessages = {
|
|||||||
"modelSelector.placeholder.search": "モデルを検索...",
|
"modelSelector.placeholder.search": "モデルを検索...",
|
||||||
"modelSelector.none": "なし",
|
"modelSelector.none": "なし",
|
||||||
"modelSelector.trigger.primary": "モデル: {model}",
|
"modelSelector.trigger.primary": "モデル: {model}",
|
||||||
|
"modelSelector.favoritesOnly.toggle.ariaLabel": "お気に入りのみ",
|
||||||
|
"modelSelector.favoritesOnly.showAll": "すべてのモデルを表示",
|
||||||
|
"modelSelector.favorite.add": "お気に入りに追加",
|
||||||
|
"modelSelector.favorite.remove": "お気に入りから削除",
|
||||||
|
|
||||||
"thinkingSelector.variant.default": "デフォルト",
|
"thinkingSelector.variant.default": "デフォルト",
|
||||||
"thinkingSelector.label": "思考: {variant}",
|
"thinkingSelector.label": "思考: {variant}",
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export const settingsMessages = {
|
|||||||
"modelSelector.placeholder.search": "Поиск моделей…",
|
"modelSelector.placeholder.search": "Поиск моделей…",
|
||||||
"modelSelector.none": "Нет",
|
"modelSelector.none": "Нет",
|
||||||
"modelSelector.trigger.primary": "Модель: {model}",
|
"modelSelector.trigger.primary": "Модель: {model}",
|
||||||
|
"modelSelector.favoritesOnly.toggle.ariaLabel": "Только избранное",
|
||||||
|
"modelSelector.favoritesOnly.showAll": "Показать все модели",
|
||||||
|
"modelSelector.favorite.add": "Добавить в избранное",
|
||||||
|
"modelSelector.favorite.remove": "Удалить из избранного",
|
||||||
|
|
||||||
"thinkingSelector.variant.default": "По умолчанию",
|
"thinkingSelector.variant.default": "По умолчанию",
|
||||||
"thinkingSelector.label": "Размышления: {variant}",
|
"thinkingSelector.label": "Размышления: {variant}",
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export const settingsMessages = {
|
|||||||
"modelSelector.placeholder.search": "搜索模型...",
|
"modelSelector.placeholder.search": "搜索模型...",
|
||||||
"modelSelector.none": "无",
|
"modelSelector.none": "无",
|
||||||
"modelSelector.trigger.primary": "模型:{model}",
|
"modelSelector.trigger.primary": "模型:{model}",
|
||||||
|
"modelSelector.favoritesOnly.toggle.ariaLabel": "仅显示收藏",
|
||||||
|
"modelSelector.favoritesOnly.showAll": "显示所有模型",
|
||||||
|
"modelSelector.favorite.add": "添加到收藏",
|
||||||
|
"modelSelector.favorite.remove": "从收藏移除",
|
||||||
|
|
||||||
"thinkingSelector.variant.default": "默认",
|
"thinkingSelector.variant.default": "默认",
|
||||||
"thinkingSelector.label": "思考:{variant}",
|
"thinkingSelector.label": "思考:{variant}",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export interface Preferences {
|
|||||||
locale?: string
|
locale?: string
|
||||||
environmentVariables: Record<string, string>
|
environmentVariables: Record<string, string>
|
||||||
modelRecents: ModelPreference[]
|
modelRecents: ModelPreference[]
|
||||||
|
modelFavorites: ModelPreference[]
|
||||||
modelThinkingSelections: Record<string, string>
|
modelThinkingSelections: Record<string, string>
|
||||||
diffViewMode: DiffViewMode
|
diffViewMode: DiffViewMode
|
||||||
toolOutputExpansion: ExpansionPreference
|
toolOutputExpansion: ExpansionPreference
|
||||||
@@ -66,6 +67,7 @@ export type ThemePreference = NonNullable<ConfigData["theme"]>
|
|||||||
|
|
||||||
const MAX_RECENT_FOLDERS = 20
|
const MAX_RECENT_FOLDERS = 20
|
||||||
const MAX_RECENT_MODELS = 5
|
const MAX_RECENT_MODELS = 5
|
||||||
|
const MAX_FAVORITE_MODELS = 50
|
||||||
|
|
||||||
const defaultPreferences: Preferences = {
|
const defaultPreferences: Preferences = {
|
||||||
showThinkingBlocks: false,
|
showThinkingBlocks: false,
|
||||||
@@ -73,6 +75,7 @@ const defaultPreferences: Preferences = {
|
|||||||
showTimelineTools: true,
|
showTimelineTools: true,
|
||||||
environmentVariables: {},
|
environmentVariables: {},
|
||||||
modelRecents: [],
|
modelRecents: [],
|
||||||
|
modelFavorites: [],
|
||||||
modelThinkingSelections: {},
|
modelThinkingSelections: {},
|
||||||
diffViewMode: "split",
|
diffViewMode: "split",
|
||||||
toolOutputExpansion: "expanded",
|
toolOutputExpansion: "expanded",
|
||||||
@@ -105,6 +108,9 @@ function normalizePreferences(pref?: Partial<Preferences> & { agentModelSelectio
|
|||||||
const sourceModelRecents = sanitized.modelRecents ?? defaultPreferences.modelRecents
|
const sourceModelRecents = sanitized.modelRecents ?? defaultPreferences.modelRecents
|
||||||
const modelRecents = sourceModelRecents.map((item) => ({ ...item }))
|
const modelRecents = sourceModelRecents.map((item) => ({ ...item }))
|
||||||
|
|
||||||
|
const sourceModelFavorites = sanitized.modelFavorites ?? defaultPreferences.modelFavorites
|
||||||
|
const modelFavorites = sourceModelFavorites.map((item) => ({ ...item }))
|
||||||
|
|
||||||
const modelThinkingSelections = {
|
const modelThinkingSelections = {
|
||||||
...defaultPreferences.modelThinkingSelections,
|
...defaultPreferences.modelThinkingSelections,
|
||||||
...(sanitized.modelThinkingSelections ?? {}),
|
...(sanitized.modelThinkingSelections ?? {}),
|
||||||
@@ -118,6 +124,7 @@ function normalizePreferences(pref?: Partial<Preferences> & { agentModelSelectio
|
|||||||
locale: sanitized.locale ?? defaultPreferences.locale,
|
locale: sanitized.locale ?? defaultPreferences.locale,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
modelRecents,
|
modelRecents,
|
||||||
|
modelFavorites,
|
||||||
modelThinkingSelections,
|
modelThinkingSelections,
|
||||||
diffViewMode: sanitized.diffViewMode ?? defaultPreferences.diffViewMode,
|
diffViewMode: sanitized.diffViewMode ?? defaultPreferences.diffViewMode,
|
||||||
toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultPreferences.toolOutputExpansion,
|
toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultPreferences.toolOutputExpansion,
|
||||||
@@ -132,6 +139,29 @@ function getModelKey(model: { providerId: string; modelId: string }): string {
|
|||||||
return `${model.providerId}/${model.modelId}`
|
return `${model.providerId}/${model.modelId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFavoriteModelPreference(model: ModelPreference): boolean {
|
||||||
|
if (!model.providerId || !model.modelId) return false
|
||||||
|
return (preferences().modelFavorites ?? []).some(
|
||||||
|
(item) => item.providerId === model.providerId && item.modelId === model.modelId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFavoriteModelPreference(model: ModelPreference): void {
|
||||||
|
if (!model.providerId || !model.modelId) return
|
||||||
|
const favorites = preferences().modelFavorites ?? []
|
||||||
|
const exists = favorites.some((item) => item.providerId === model.providerId && item.modelId === model.modelId)
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
const updated = favorites.filter((item) => item.providerId !== model.providerId || item.modelId !== model.modelId)
|
||||||
|
updatePreferences({ modelFavorites: updated })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = favorites.filter((item) => item.providerId !== model.providerId || item.modelId !== model.modelId)
|
||||||
|
const updated = [model, ...filtered].slice(0, MAX_FAVORITE_MODELS)
|
||||||
|
updatePreferences({ modelFavorites: updated })
|
||||||
|
}
|
||||||
|
|
||||||
function getModelThinkingSelection(model: { providerId: string; modelId: string }): string | undefined {
|
function getModelThinkingSelection(model: { providerId: string; modelId: string }): string | undefined {
|
||||||
if (!model.providerId || !model.modelId) return undefined
|
if (!model.providerId || !model.modelId) return undefined
|
||||||
return preferences().modelThinkingSelections?.[getModelKey(model)]
|
return preferences().modelThinkingSelections?.[getModelKey(model)]
|
||||||
@@ -566,6 +596,8 @@ export {
|
|||||||
addEnvironmentVariable,
|
addEnvironmentVariable,
|
||||||
removeEnvironmentVariable,
|
removeEnvironmentVariable,
|
||||||
addRecentModelPreference,
|
addRecentModelPreference,
|
||||||
|
isFavoriteModelPreference,
|
||||||
|
toggleFavoriteModelPreference,
|
||||||
getModelThinkingSelection,
|
getModelThinkingSelection,
|
||||||
setModelThinkingSelection,
|
setModelThinkingSelection,
|
||||||
setAgentModelPreference,
|
setAgentModelPreference,
|
||||||
|
|||||||
@@ -148,6 +148,61 @@
|
|||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selector-option-action {
|
||||||
|
@apply flex items-center justify-center py-1;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-option-star {
|
||||||
|
@apply p-1 rounded transition-colors flex-shrink-0 mt-0.5;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-option-star[data-active="true"] {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-option-star:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-option-star:focus-visible {
|
||||||
|
@apply ring-2;
|
||||||
|
ring-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-favorites-toggle {
|
||||||
|
@apply p-2 rounded border transition-colors flex items-center justify-center;
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
border-color: var(--border-base);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-favorites-toggle[data-active="true"] {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-favorites-toggle:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-favorites-toggle:focus-visible {
|
||||||
|
@apply ring-2;
|
||||||
|
ring-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-favorites-toggle:disabled {
|
||||||
|
@apply opacity-50 cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-footer {
|
||||||
|
@apply border-t;
|
||||||
|
border-color: var(--border-base);
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.selector-section {
|
.selector-section {
|
||||||
@apply px-3 py-2 border-b;
|
@apply px-3 py-2 border-b;
|
||||||
border-color: var(--border-base);
|
border-color: var(--border-base);
|
||||||
|
|||||||
Reference in New Issue
Block a user