feat(desktop): open read-only TurkServis combo details in browser pane
This commit is contained in:
1 parent
ff2eb47302
commit
3332458708
16 files changed
+110
-4
No files matched your search
@@ -12,6 +12,7 @@ import {
|
||||
setModelVisibilityOpen,
|
||||
setVisibleModels
|
||||
} from '@/store/model-visibility'
|
||||
import { openPreview } from '@/store/preview'
|
||||
import type { LocalRuntimeJob } from '@/types/hermes'
|
||||
|
||||
import { ModelCatalogMenu, type ModelMenuController } from './model-catalog-menu'
|
||||
@@ -24,6 +25,7 @@ beforeAll(() => {
|
||||
})
|
||||
|
||||
const getGlobalModelOptions = vi.fn()
|
||||
vi.mock('@/store/preview', () => ({ openPreview: vi.fn() }))
|
||||
|
||||
vi.mock('@/hermes', () => ({
|
||||
getGlobalModelOptions: (...args: unknown[]) => getGlobalModelOptions(...args),
|
||||
@@ -88,6 +90,22 @@ function renderMenu() {
|
||||
// the kanban board would end up disagreeing about what "my models" means —
|
||||
// which is exactly the drift extracting this component was meant to prevent.
|
||||
describe('the catalog owns model curation', () => {
|
||||
it('opens the chosen TurkServis model details without selecting that model', async () => {
|
||||
getGlobalModelOptions.mockResolvedValue({ providers: [{
|
||||
models: ['Outer'], name: 'TurkServis', slug: 'turkservis', api_url: 'https://ai.turkservis.online/v1'
|
||||
}] })
|
||||
const select = renderMenu()
|
||||
const trigger = await screen.findByRole('menuitem', { name: /Outer/ })
|
||||
|
||||
fireEvent.keyDown(trigger, { key: 'ArrowRight' })
|
||||
fireEvent.click(await screen.findByRole('menuitem', { name: /Model \/ combo (details|ayrıntıları)/ }))
|
||||
|
||||
expect(openPreview).toHaveBeenCalledWith(expect.objectContaining({
|
||||
kind: 'url', url: 'https://turkservis.online/router?model=Outer'
|
||||
}))
|
||||
expect(select).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('honours the stored Edit Models shortlist', async () => {
|
||||
setVisibleModels(new Set([modelVisibilityKey('google', 'gemini-2.5-flash')]))
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { modelOptionsQueryKey, requestModelOptions } from '@/lib/model-options'
|
||||
import { displayModelName, modelDisplayParts } from '@/lib/model-status-label'
|
||||
import { DEFAULT_REASONING_EFFORT, reasoningEffortLabel } from '@/lib/reasoning-effort'
|
||||
import { normalize } from '@/lib/text'
|
||||
import { turkservisComboDetailsUrl } from '@/lib/turkservis-combo-details'
|
||||
import { useStoreSelector } from '@/lib/use-session-slice'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $localModelsEnabled } from '@/store/local-models-flag'
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
modelVisibilityKey,
|
||||
setModelVisibilityOpen
|
||||
} from '@/store/model-visibility'
|
||||
import { openPreview } from '@/store/preview'
|
||||
import { $collapsedProviders, toggleCollapsedProvider } from '@/store/provider-collapse'
|
||||
import { $defaultReasoningEffort } from '@/store/session'
|
||||
import type { LocalModelLoadProgress, ModelOptionProvider, ModelOptionsResponse } from '@/types/hermes'
|
||||
@@ -588,6 +590,12 @@ export function ModelCatalogMenu({
|
||||
fastControl={fastControl}
|
||||
isActive={isCurrent}
|
||||
model={family.id}
|
||||
onOpenDetails={turkservisComboDetailsUrl(group.provider.api_url, family.id) ? () => {
|
||||
const url = turkservisComboDetailsUrl(group.provider.api_url, family.id)!
|
||||
|
||||
closeMenu()
|
||||
openPreview({ kind: 'url', label: t.shell.modelOptions.comboDetails, source: url, url })
|
||||
} : undefined}
|
||||
onSelectModel={nextModel => controller.select(nextModel, group.provider.slug)}
|
||||
onSetOptions={patch =>
|
||||
controller.setOptions(patch, {
|
||||
|
||||
@@ -29,6 +29,7 @@ function renderSubmenu(opts: {
|
||||
fastControl: FastControl
|
||||
isActive?: boolean
|
||||
onSelectModel?: (model: string) => void
|
||||
onOpenDetails?: () => void
|
||||
onSetOptions: (patch: { effort?: string; fast?: boolean }) => void
|
||||
reasoning: boolean
|
||||
}) {
|
||||
@@ -43,6 +44,7 @@ function renderSubmenu(opts: {
|
||||
fastControl={opts.fastControl}
|
||||
isActive={opts.isActive ?? true}
|
||||
model="m1"
|
||||
onOpenDetails={opts.onOpenDetails}
|
||||
onSelectModel={opts.onSelectModel ?? vi.fn()}
|
||||
onSetOptions={opts.onSetOptions}
|
||||
provider="p1"
|
||||
@@ -60,6 +62,19 @@ function renderSubmenu(opts: {
|
||||
// ever writes directly again, picking an effort for a kanban card would reach
|
||||
// over and change the user's live chat.
|
||||
describe('ModelEditSubmenu reports edits without performing them', () => {
|
||||
it('opens read-only details without selecting a model or changing its options', () => {
|
||||
const onOpenDetails = vi.fn()
|
||||
const onSelectModel = vi.fn()
|
||||
const onSetOptions = vi.fn()
|
||||
|
||||
renderSubmenu({ fastControl: { kind: 'none' }, reasoning: false, onOpenDetails, onSelectModel, onSetOptions })
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /Model \/ combo (details|ayrıntıları)/ }))
|
||||
|
||||
expect(onOpenDetails).toHaveBeenCalledOnce()
|
||||
expect(onSelectModel).not.toHaveBeenCalled()
|
||||
expect(onSetOptions).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('param fast: reports the toggle', () => {
|
||||
const onSetOptions = vi.fn()
|
||||
renderSubmenu({ fastControl: { kind: 'param', on: true }, onSetOptions, reasoning: false })
|
||||
|
||||
@@ -75,6 +75,8 @@ interface ModelEditSubmenuProps {
|
||||
isActive: boolean
|
||||
/** This row's model id. */
|
||||
model: string
|
||||
/** Read-only details; the owner handles opening without changing selection. */
|
||||
onOpenDetails?: () => void
|
||||
/** Switch to a specific model id (used to swap base ⇄ -fast variant). */
|
||||
onSelectModel: (model: string) => Promise<boolean | void> | void
|
||||
/** Report an option change. This submenu is PURE: it never writes to a
|
||||
@@ -107,6 +109,7 @@ function ModelEditSubmenuBody({
|
||||
effort,
|
||||
fastControl,
|
||||
isActive,
|
||||
onOpenDetails,
|
||||
onSelectModel,
|
||||
onSetOptions,
|
||||
reasoning
|
||||
@@ -140,11 +143,19 @@ function ModelEditSubmenuBody({
|
||||
const hasFast = fastControl.kind !== 'none'
|
||||
const fastOn = fastControl.kind === 'none' ? false : fastControl.on
|
||||
|
||||
return !hasFast && !reasoning ? (
|
||||
return !hasFast && !reasoning && !onOpenDetails ? (
|
||||
<div className="px-2.5 py-3 text-xs text-(--ui-text-tertiary)">{copy.noOptions}</div>
|
||||
) : (
|
||||
<>
|
||||
<DropdownMenuLabel className={dropdownMenuSectionLabel}>{copy.options}</DropdownMenuLabel>
|
||||
{onOpenDetails ? (
|
||||
<>
|
||||
<DropdownMenuItem className={dropdownMenuRow} onSelect={onOpenDetails}>
|
||||
{copy.comboDetails}
|
||||
</DropdownMenuItem>
|
||||
{hasFast || reasoning ? <DropdownMenuSeparator className="mx-0" /> : null}
|
||||
</>
|
||||
) : null}
|
||||
{hasFast || reasoning ? <DropdownMenuLabel className={dropdownMenuSectionLabel}>{copy.options}</DropdownMenuLabel> : null}
|
||||
{showThinkingToggle ? (
|
||||
<DropdownMenuItem className={dropdownMenuRow} onSelect={event => event.preventDefault()}>
|
||||
{copy.thinking}
|
||||
|
||||
Reference in new issue
Block a user