mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
fix(cursor): support ACP parameterized model picker (#969)
* test: reproduce issue #968 * fix: support Cursor parameterized model picker (closes #968)
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const transportState = vi.hoisted(() => ({
|
||||
calls: [] as Array<{ method: string; params?: unknown }>
|
||||
}));
|
||||
|
||||
vi.mock('./AcpStdioTransport', () => ({
|
||||
AcpStdioTransport: class {
|
||||
constructor(_options: unknown) {}
|
||||
onNotification = vi.fn();
|
||||
onStderrError = vi.fn();
|
||||
registerRequestHandler = vi.fn();
|
||||
sendRequest = vi.fn(async (method: string, params?: unknown) => {
|
||||
transportState.calls.push({ method, params });
|
||||
if (method === 'initialize') {
|
||||
return { protocolVersion: 1, authMethods: [] };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
close = vi.fn(async () => {});
|
||||
}
|
||||
}));
|
||||
|
||||
import { AcpSdkBackend } from './AcpSdkBackend';
|
||||
|
||||
describe('AcpSdkBackend.initialize', () => {
|
||||
it('advertises Cursor-compatible parameterized model picker support', async () => {
|
||||
const backend = new AcpSdkBackend({ command: 'agent', args: ['acp'] });
|
||||
|
||||
await backend.initialize();
|
||||
|
||||
expect(transportState.calls).toContainEqual({
|
||||
method: 'initialize',
|
||||
params: expect.objectContaining({
|
||||
clientCapabilities: expect.objectContaining({
|
||||
_meta: expect.objectContaining({
|
||||
parameterizedModelPicker: true
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -131,7 +131,13 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: {
|
||||
fs: { readTextFile: false, writeTextFile: false },
|
||||
terminal: false
|
||||
terminal: false,
|
||||
_meta: {
|
||||
// Cursor ACP exposes Composer's non-fast/fast choice as separate
|
||||
// `model` + `fast` config options only when the client advertises
|
||||
// this capability. Agents that do not know this metadata ignore it.
|
||||
parameterizedModelPicker: true
|
||||
}
|
||||
},
|
||||
clientInfo: {
|
||||
name: 'hapi',
|
||||
@@ -839,7 +845,7 @@ export class AcpSdkBackend implements AgentBackend {
|
||||
|
||||
for (const entry of response.configOptions) {
|
||||
if (!isObject(entry)) continue;
|
||||
if (asString(entry.category) !== 'model') continue;
|
||||
if (asString(entry.category) !== 'model' && asString(entry.id) !== 'model') continue;
|
||||
return {
|
||||
currentValue: asString(entry.currentValue),
|
||||
options: Array.isArray(entry.options) ? entry.options : []
|
||||
|
||||
@@ -47,4 +47,50 @@ describe('buildCursorModelsSnapshotFromAcp', () => {
|
||||
|
||||
expect(snapshot?.availableModels).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('synthesizes Composer fast variants from parameterized model + fast config options', () => {
|
||||
const backend = {
|
||||
getSessionModelsMetadata: () => ({
|
||||
availableModels: [{ modelId: 'composer-2.5', name: 'Composer 2.5' }],
|
||||
currentModelId: 'composer-2.5'
|
||||
}),
|
||||
getConfigOptionByCategory: (_sessionId: string, category: string) => {
|
||||
if (category === 'model') {
|
||||
return {
|
||||
id: 'model',
|
||||
currentValue: 'composer-2.5',
|
||||
options: [{ value: 'composer-2.5', name: 'Composer 2.5' }]
|
||||
};
|
||||
}
|
||||
if (category === 'fast') {
|
||||
return {
|
||||
id: 'fast',
|
||||
currentValue: 'false',
|
||||
options: [{ value: 'false', name: 'Off' }, { value: 'true', name: 'Fast' }]
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
getSessionConfigOptions: () => [
|
||||
{
|
||||
id: 'model',
|
||||
currentValue: 'composer-2.5',
|
||||
options: [{ value: 'composer-2.5', name: 'Composer 2.5' }]
|
||||
},
|
||||
{
|
||||
id: 'fast',
|
||||
currentValue: 'false',
|
||||
options: [{ value: 'false', name: 'Off' }, { value: 'true', name: 'Fast' }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const snapshot = buildCursorModelsSnapshotFromAcp(backend, 's1');
|
||||
|
||||
expect(snapshot?.availableModels.map((entry) => entry.modelId).sort()).toEqual([
|
||||
'composer-2.5[fast=false]',
|
||||
'composer-2.5[fast=true]'
|
||||
]);
|
||||
expect(snapshot?.currentModelId).toBe('composer-2.5[fast=false]');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,18 @@ export type CursorModelsSnapshot = {
|
||||
currentModelId: string | null;
|
||||
};
|
||||
|
||||
type CursorAcpModelSnapshotBackend = Pick<AcpSdkBackend, 'getSessionModelsMetadata' | 'getConfigOptionByCategory'>
|
||||
& Partial<Pick<AcpSdkBackend, 'getSessionConfigOptions'>>;
|
||||
|
||||
function findConfigOption(
|
||||
backend: CursorAcpModelSnapshotBackend,
|
||||
sessionId: string,
|
||||
key: string
|
||||
) {
|
||||
return backend.getConfigOptionByCategory?.(sessionId, key)
|
||||
?? backend.getSessionConfigOptions?.(sessionId)?.find((option) => option.id === key || option.category === key);
|
||||
}
|
||||
|
||||
function mergeModelEntries(
|
||||
target: Map<string, CursorModelSummary>,
|
||||
entries: Iterable<{ modelId: string; name?: string | null }>
|
||||
@@ -31,19 +43,43 @@ function mergeModelEntries(
|
||||
* `availableModels` alone is often one variant per base family.
|
||||
*/
|
||||
export function buildCursorModelsSnapshotFromAcp(
|
||||
backend: Pick<AcpSdkBackend, 'getSessionModelsMetadata' | 'getConfigOptionByCategory'>,
|
||||
backend: CursorAcpModelSnapshotBackend,
|
||||
sessionId: string
|
||||
): CursorModelsSnapshot | null {
|
||||
const metadata = backend.getSessionModelsMetadata(sessionId);
|
||||
const modelOption = backend.getConfigOptionByCategory?.(sessionId, 'model');
|
||||
const modelOption = findConfigOption(backend, sessionId, 'model');
|
||||
const fastOption = findConfigOption(backend, sessionId, 'fast');
|
||||
|
||||
if (!metadata && !modelOption) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const merged = new Map<string, CursorModelSummary>();
|
||||
const parameterizedFastModels: CursorModelSummary[] = [];
|
||||
|
||||
if (modelOption?.options?.length) {
|
||||
if (modelOption?.options?.length && fastOption?.options?.length) {
|
||||
const fastValues = fastOption.options
|
||||
.map((option) => option.value.trim())
|
||||
.filter((value) => value === 'false' || value === 'true');
|
||||
if (fastValues.length > 0) {
|
||||
for (const option of modelOption.options) {
|
||||
const modelId = option.value.trim();
|
||||
if (!modelId || modelId.includes('[')) {
|
||||
continue;
|
||||
}
|
||||
for (const fast of fastValues) {
|
||||
parameterizedFastModels.push({
|
||||
modelId: `${modelId}[fast=${fast}]`,
|
||||
name: option.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parameterizedFastModels.length > 0) {
|
||||
mergeModelEntries(merged, parameterizedFastModels);
|
||||
} else if (modelOption?.options?.length) {
|
||||
mergeModelEntries(merged, modelOption.options.map((option) => ({
|
||||
modelId: option.value,
|
||||
name: option.name
|
||||
@@ -51,16 +87,23 @@ export function buildCursorModelsSnapshotFromAcp(
|
||||
}
|
||||
|
||||
if (metadata?.availableModels?.length) {
|
||||
mergeModelEntries(merged, metadata.availableModels);
|
||||
mergeModelEntries(
|
||||
merged,
|
||||
parameterizedFastModels.length > 0
|
||||
? metadata.availableModels.filter((entry) => entry.modelId.includes('['))
|
||||
: metadata.availableModels
|
||||
);
|
||||
}
|
||||
|
||||
if (merged.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentModelId = metadata?.currentModelId
|
||||
?? modelOption?.currentValue
|
||||
?? null;
|
||||
const currentModelId = parameterizedFastModels.length > 0 && modelOption?.currentValue && fastOption?.currentValue
|
||||
? `${modelOption.currentValue}[fast=${fastOption.currentValue}]`
|
||||
: metadata?.currentModelId
|
||||
?? modelOption?.currentValue
|
||||
?? null;
|
||||
|
||||
return {
|
||||
availableModels: [...merged.values()],
|
||||
|
||||
@@ -194,6 +194,109 @@ describe('applyCursorAcpModel', () => {
|
||||
expect(setConfigOption).toHaveBeenCalledWith('s1', 'model-opt', 'composer-2.5[fast=false]');
|
||||
});
|
||||
|
||||
it('applies parameterized Cursor Composer fast=false for base CLI sku requests', async () => {
|
||||
const setConfigOption = vi.fn(async () => {});
|
||||
const backend = mockModelBackend({
|
||||
setConfigOption,
|
||||
getSessionModelsMetadata: vi.fn(() => ({
|
||||
availableModels: [{ modelId: 'composer-2.5', name: 'Composer 2.5' }],
|
||||
currentModelId: 'composer-2.5'
|
||||
})),
|
||||
getConfigOptionByCategory: vi.fn((_sessionId: string, category: string) => {
|
||||
if (category === 'model') {
|
||||
return {
|
||||
id: 'model',
|
||||
category: 'model',
|
||||
currentValue: 'composer-2.5',
|
||||
options: [{ value: 'composer-2.5', name: 'Composer 2.5' }]
|
||||
};
|
||||
}
|
||||
if (category === 'fast') {
|
||||
return {
|
||||
id: 'fast',
|
||||
category: 'fast',
|
||||
currentValue: 'true',
|
||||
options: [{ value: 'false', name: 'Off' }, { value: 'true', name: 'Fast' }]
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
});
|
||||
|
||||
await expect(applyCursorAcpModel(backend, 's1', 'composer-2.5')).resolves.toEqual({
|
||||
applied: true,
|
||||
resolvedWireId: 'composer-2.5[fast=false]',
|
||||
requestedWireId: 'composer-2.5'
|
||||
});
|
||||
expect(setConfigOption).toHaveBeenNthCalledWith(1, 's1', 'model', 'composer-2.5');
|
||||
expect(setConfigOption).toHaveBeenNthCalledWith(2, 's1', 'fast', 'false');
|
||||
expect(backend.pinSessionModelWireId).toHaveBeenCalledWith('s1', 'composer-2.5[fast=false]');
|
||||
});
|
||||
|
||||
it('applies parameterized Cursor Composer fast=true for -fast CLI sku requests', async () => {
|
||||
const setConfigOption = vi.fn(async () => {});
|
||||
const backend = mockModelBackend({
|
||||
setConfigOption,
|
||||
getSessionModelsMetadata: vi.fn(() => ({
|
||||
availableModels: [{ modelId: 'composer-2.5', name: 'Composer 2.5' }],
|
||||
currentModelId: 'composer-2.5'
|
||||
})),
|
||||
getConfigOptionByCategory: vi.fn((_sessionId: string, category: string) => {
|
||||
if (category === 'model') {
|
||||
return { id: 'model', category: 'model', options: [{ value: 'composer-2.5' }] };
|
||||
}
|
||||
if (category === 'fast') {
|
||||
return { id: 'fast', category: 'fast', options: [{ value: 'false' }, { value: 'true' }] };
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
});
|
||||
|
||||
await expect(applyCursorAcpModel(backend, 's1', 'composer-2.5-fast')).resolves.toMatchObject({
|
||||
applied: true,
|
||||
resolvedWireId: 'composer-2.5[fast=true]',
|
||||
requestedWireId: 'composer-2.5-fast'
|
||||
});
|
||||
expect(setConfigOption).toHaveBeenNthCalledWith(1, 's1', 'model', 'composer-2.5');
|
||||
expect(setConfigOption).toHaveBeenNthCalledWith(2, 's1', 'fast', 'true');
|
||||
});
|
||||
|
||||
it('does not fall back to base-only model apply when parameterized fast update fails', async () => {
|
||||
const setConfigOption = vi.fn()
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockRejectedValueOnce(new Error('fast update failed'));
|
||||
const backend = mockModelBackend({
|
||||
setConfigOption,
|
||||
getSessionModelsMetadata: vi.fn(() => ({
|
||||
availableModels: [{ modelId: 'composer-2.5', name: 'Composer 2.5' }],
|
||||
currentModelId: 'composer-2.5'
|
||||
})),
|
||||
getConfigOptionByCategory: vi.fn((_sessionId: string, category: string) => {
|
||||
if (category === 'model') {
|
||||
return {
|
||||
id: 'model',
|
||||
category: 'model',
|
||||
options: [{ value: 'composer-2.5', name: 'Composer 2.5' }]
|
||||
};
|
||||
}
|
||||
if (category === 'fast') {
|
||||
return {
|
||||
id: 'fast',
|
||||
category: 'fast',
|
||||
options: [{ value: 'false', name: 'Off' }, { value: 'true', name: 'Fast' }]
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
});
|
||||
|
||||
await expect(applyCursorAcpModel(backend, 's1', 'composer-2.5')).resolves.toEqual({
|
||||
applied: false
|
||||
});
|
||||
expect(setConfigOption).toHaveBeenCalledTimes(2);
|
||||
expect(backend.pinSessionModelWireId).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('retries set_config_option once before failing apply', async () => {
|
||||
const setConfigOption = vi.fn()
|
||||
.mockRejectedValueOnce(new Error('transient'))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CursorPermissionMode } from '@hapi/protocol/types';
|
||||
import { matchCliSkuToAcpWireId } from '@hapi/protocol';
|
||||
import { cursorCliSkuBaseId, cursorModelBaseId, matchCliSkuToAcpWireId } from '@hapi/protocol';
|
||||
import type { AcpSdkBackend } from '@/agent/backends/acp';
|
||||
import { logger } from '@/ui/logger';
|
||||
|
||||
@@ -71,6 +71,9 @@ export type ApplyCursorAcpModelResult = {
|
||||
requestedWireId?: string;
|
||||
};
|
||||
|
||||
type ConfigOption = NonNullable<ReturnType<AcpSdkBackend['getConfigOptionByCategory']>>;
|
||||
type ParameterizedCursorModelResult = ApplyCursorAcpModelResult | 'unsupported' | 'failed';
|
||||
|
||||
/** Wire id stored on session + keepalive (preserve explicit variant picks). */
|
||||
export function wireIdForCursorSessionState(requested: string, resolved: string): string {
|
||||
const trimmed = requested.trim();
|
||||
@@ -101,6 +104,61 @@ export function resolveCursorAcpWireId(
|
||||
return matchCliSkuToAcpWireId(trimmed, available);
|
||||
}
|
||||
|
||||
function findConfigOption(backend: AcpSdkBackend, sessionId: string, key: string): ConfigOption | undefined {
|
||||
return backend.getConfigOptionByCategory?.(sessionId, key)
|
||||
?? backend.getSessionConfigOptions?.(sessionId)?.find((option) => option.id === key || option.category === key);
|
||||
}
|
||||
|
||||
function optionHasValue(option: ConfigOption | undefined, value: string): boolean {
|
||||
return Boolean(option?.options?.some((entry) => entry.value === value));
|
||||
}
|
||||
|
||||
function fastHintForCursorSkuOrWire(modelId: string): 'false' | 'true' {
|
||||
const lower = modelId.trim().toLowerCase();
|
||||
const fastMatch = lower.match(/[\[,](?:\s*)fast=(true|false)(?:\s*)[\],]/)
|
||||
?? lower.match(/\[\s*fast=(true|false)\s*\]/);
|
||||
if (fastMatch?.[1] === 'true' || fastMatch?.[1] === 'false') {
|
||||
return fastMatch[1];
|
||||
}
|
||||
return lower.includes('-fast') ? 'true' : 'false';
|
||||
}
|
||||
|
||||
function cursorRequestBaseId(modelId: string): string {
|
||||
return modelId.includes('[')
|
||||
? cursorModelBaseId(modelId)
|
||||
: cursorCliSkuBaseId(modelId);
|
||||
}
|
||||
|
||||
async function applyParameterizedCursorModel(
|
||||
backend: AcpSdkBackend,
|
||||
sessionId: string,
|
||||
requested: string
|
||||
): Promise<ParameterizedCursorModelResult> {
|
||||
const modelOption = findConfigOption(backend, sessionId, 'model');
|
||||
const fastOption = findConfigOption(backend, sessionId, 'fast');
|
||||
if (!modelOption || !fastOption || !backend.setConfigOption) {
|
||||
return 'unsupported';
|
||||
}
|
||||
|
||||
const baseModel = cursorRequestBaseId(requested);
|
||||
const fast = fastHintForCursorSkuOrWire(requested);
|
||||
if (!optionHasValue(modelOption, baseModel) || !optionHasValue(fastOption, fast)) {
|
||||
return 'unsupported';
|
||||
}
|
||||
|
||||
try {
|
||||
await backend.setConfigOption(sessionId, modelOption.id, baseModel);
|
||||
await backend.setConfigOption(sessionId, fastOption.id, fast);
|
||||
} catch (error) {
|
||||
logger.debug('[cursor-acp] parameterized model config failed', error);
|
||||
return 'failed';
|
||||
}
|
||||
|
||||
const resolved = `${baseModel}[fast=${fast}]`;
|
||||
backend.pinSessionModelWireId(sessionId, resolved);
|
||||
return { applied: true, resolvedWireId: resolved, requestedWireId: requested };
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a model from the live ACP configOptions list (Zed-style).
|
||||
* Only wire ids present in `availableModels` are accepted.
|
||||
@@ -118,6 +176,15 @@ export async function applyCursorAcpModel(
|
||||
const metadata = backend.getSessionModelsMetadata(sessionId);
|
||||
const available = metadata?.availableModels ?? [];
|
||||
const modelOption = backend.getConfigOptionByCategory?.(sessionId, 'model');
|
||||
|
||||
const parameterized = await applyParameterizedCursorModel(backend, sessionId, trimmed);
|
||||
if (parameterized === 'failed') {
|
||||
return { applied: false };
|
||||
}
|
||||
if (parameterized !== 'unsupported') {
|
||||
return parameterized;
|
||||
}
|
||||
|
||||
const optionWireIds = modelOption?.options?.map((option) => ({ modelId: option.value })) ?? [];
|
||||
const catalog = [...available, ...optionWireIds];
|
||||
const resolved = resolveCursorAcpWireId(trimmed, catalog);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ModelEffortSettingsSection } from './HappyComposer';
|
||||
|
||||
vi.mock('@/lib/use-translation', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key === 'misc.variant' ? 'Variant' : key
|
||||
})
|
||||
}));
|
||||
|
||||
describe('ModelEffortSettingsSection', () => {
|
||||
it('renders Cursor variant choices and marks the selected variant', () => {
|
||||
render(
|
||||
<ModelEffortSettingsSection
|
||||
agentFlavor="cursor"
|
||||
options={[
|
||||
{ value: 'composer-2.5', label: 'Composer 2.5' },
|
||||
{ value: 'composer-2.5-fast', label: 'Composer 2.5 Fast' }
|
||||
]}
|
||||
selectedValue="composer-2.5"
|
||||
controlsDisabled={false}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Variant')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /^Composer 2.5$/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Composer 2.5 Fast/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /^Composer 2.5$/ }).innerHTML).toContain('bg-[var(--app-link)]');
|
||||
});
|
||||
});
|
||||
@@ -81,6 +81,57 @@ export type ComposerSendError = {
|
||||
|
||||
const defaultSuggestionHandler = async (): Promise<Suggestion[]> => []
|
||||
|
||||
export function ModelEffortSettingsSection(props: {
|
||||
agentFlavor?: string | null
|
||||
options: Array<{ value: string; label: string }>
|
||||
selectedValue: string | null | undefined
|
||||
controlsDisabled: boolean
|
||||
onChange: (value: string) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { agentFlavor, options, selectedValue, controlsDisabled, onChange } = props
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="px-3 pb-1 text-xs font-semibold text-[var(--app-hint)]">
|
||||
{agentFlavor === 'cursor' ? t('misc.variant') : t('misc.effort')}
|
||||
</div>
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedValue === option.value
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
disabled={controlsDisabled}
|
||||
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
controlsDisabled
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: 'cursor-pointer hover:bg-[var(--app-secondary-bg)]'
|
||||
}`}
|
||||
onClick={() => onChange(option.value)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
className={`flex h-4 w-4 items-center justify-center rounded-full border-2 ${
|
||||
isSelected
|
||||
? 'border-[var(--app-link)]'
|
||||
: 'border-[var(--app-hint)]'
|
||||
}`}
|
||||
>
|
||||
{isSelected && (
|
||||
<div className="h-2 w-2 rounded-full bg-[var(--app-link)]" />
|
||||
)}
|
||||
</div>
|
||||
<span className={isSelected ? 'text-[var(--app-link)]' : ''}>
|
||||
{option.label}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function HappyComposer(props: {
|
||||
sessionId?: string
|
||||
disabled?: boolean
|
||||
@@ -984,6 +1035,24 @@ export function HappyComposer(props: {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showModelSettings && showModelEffortSettings ? (
|
||||
<div className="mx-3 h-px bg-[var(--app-divider)]" />
|
||||
) : null}
|
||||
|
||||
{showModelEffortSettings ? (
|
||||
<ModelEffortSettingsSection
|
||||
agentFlavor={agentFlavor}
|
||||
options={modelEffortOptions!}
|
||||
selectedValue={selectedModelVariant ?? model}
|
||||
controlsDisabled={controlsDisabled}
|
||||
onChange={handleModelEffortChange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{(showModelSettings || showModelEffortSettings) && showModelReasoningEffortSettings ? (
|
||||
<div className="mx-3 h-px bg-[var(--app-divider)]" />
|
||||
) : null}
|
||||
|
||||
{(showModelSettings || showModelEffortSettings || showModelReasoningEffortSettings) && showEffortSettings ? (
|
||||
<div className="mx-3 h-px bg-[var(--app-divider)]" />
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user