mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
* feat(web): in-place cursor variant drill-down (closes #48) Rebased onto upstream/main: iOS-style nested picker keeps overlay open on multi-variant base pick, applies default variant immediately, dismisses on variant selection; preserves upstream Pi model panels and Codex Fast mode. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web+cli): accept bare Cursor ACP model ids in picker catalog Current Cursor ACP returns bare bases (composer-2.5, …) with empty cliModelSkus. The bracket-only wire gate emptied the catalog so the picker showed only Default. Treat bare non-default ACP ids as catalog rows, keep CLI effort/speed SKUs as variants, and widen SKU enrichment the same way. Closes #1129. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): ignore stale selectedModelVariant during Cursor base drill-down Only highlight a session variant when it is still among the visible rows, so a multi-variant base switch uses the new default until parent state catches up (Codex Minor on #947). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli+shared): do not attach CLI variant SKUs to bare ACP catalogs Bare ACP bases cannot express effort/speed (apply is model+fast on parameterized wires). Drop suffixed SKUs unless a base has bracket wires, and refuse matchCliSkuToAcpWireId collapse onto bare-only rows. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): serialize Cursor model applies across base/variant picks Drill-down default apply and a quick variant click could race setModel RPCs; last-finisher wins. Queue Cursor applies in SessionChat so the explicit variant cannot be overwritten by a late default. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): align cursor picker auto-row label with upstream Auto Rebase onto main picked up Default→Auto rename; keep #1129 coverage. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
201 lines
9.0 KiB
TypeScript
201 lines
9.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
buildCursorEffortPickerOptions,
|
|
buildCursorModelCatalog,
|
|
buildFlatCursorModelPickerOptions,
|
|
CURSOR_AUTO_MODEL_LABEL,
|
|
cursorCatalogHasMultiVariantBases,
|
|
shouldUseCursorDualPickers,
|
|
cursorEffortPickerLabel,
|
|
cursorModelBaseId,
|
|
cursorModelDedupeKey,
|
|
cursorModelVariantId,
|
|
cursorVariantDisambiguationSuffix,
|
|
cursorVariantLabel,
|
|
cursorVaryingWireParamKeys,
|
|
filterCursorModelOptionsForCompactView,
|
|
formatCursorModelPickerLabel,
|
|
parseCursorWireParams,
|
|
resolveCursorBaseKey,
|
|
resolveCursorVariantOptions
|
|
} from './cursorModelOptions'
|
|
|
|
const acpModels = [
|
|
{ modelId: 'default[]', name: 'Auto' },
|
|
{ modelId: 'composer-2.5[fast=true]', name: 'Composer 2.5 Fast' },
|
|
{ modelId: 'composer-2.5[fast=false]', name: 'Composer 2.5' },
|
|
{ modelId: 'claude-opus-4-8[thinking=true,context=300k,effort=high,fast=false]', name: 'Claude Opus 4.8' },
|
|
{ modelId: 'claude-opus-4-8[thinking=true,context=300k,effort=low,fast=false]', name: 'Claude Opus 4.8' },
|
|
] as const
|
|
|
|
describe('raw Cursor ACP model splitting', () => {
|
|
it('splits wire ids into raw base and variant parts', () => {
|
|
expect(cursorModelBaseId('composer-2.5[fast=true]')).toBe('composer-2.5')
|
|
expect(cursorModelDedupeKey('composer-2.5-fast')).toBe('composer-2.5-fast')
|
|
expect(cursorModelVariantId('composer-2.5[fast=true]')).toBe('fast=true')
|
|
expect(cursorVariantLabel('claude-opus-4-8[thinking=true,context=300k,effort=high,fast=false]')).toBe(
|
|
'thinking=true,context=300k,effort=high,fast=false'
|
|
)
|
|
})
|
|
|
|
it('parses raw comma-separated wire parameters without changing labels', () => {
|
|
expect(
|
|
parseCursorWireParams('claude-opus-4-8[thinking=true,context=300k,effort=high,fast=false]')
|
|
).toEqual({
|
|
thinking: 'true',
|
|
context: '300k',
|
|
effort: 'high',
|
|
fast: 'false',
|
|
})
|
|
expect(cursorVaryingWireParamKeys([
|
|
'composer-2.5[fast=true]',
|
|
'composer-2.5[fast=false]',
|
|
])).toEqual(['fast'])
|
|
})
|
|
})
|
|
|
|
describe('buildCursorModelCatalog', () => {
|
|
it('groups exact ACP wire variants by raw base id and sorts model bases', () => {
|
|
const catalog = buildCursorModelCatalog([...acpModels])
|
|
expect(catalog.baseOptions.map((o) => o.label)).toEqual([
|
|
CURSOR_AUTO_MODEL_LABEL,
|
|
'claude-opus-4-8',
|
|
'composer-2.5',
|
|
])
|
|
expect(resolveCursorVariantOptions('composer-2.5', catalog).map((v) => v.label)).toEqual([
|
|
'fast=true',
|
|
'fast=false',
|
|
])
|
|
})
|
|
|
|
it('does not merge legacy sku aliases into ACP bases', () => {
|
|
const catalog = buildCursorModelCatalog([
|
|
{ modelId: 'composer-2.5-fast', name: 'Composer 2.5 Fast' },
|
|
{ modelId: 'composer-2.5[fast=true]', name: 'composer-2.5' },
|
|
])
|
|
expect(catalog.baseOptions.map((o) => o.value)).toEqual([
|
|
null,
|
|
'composer-2.5',
|
|
'composer-2.5-fast',
|
|
])
|
|
})
|
|
|
|
it('resolves current wire to base and variant list', () => {
|
|
const catalog = buildCursorModelCatalog([...acpModels], {
|
|
currentModel: 'composer-2.5[fast=true]',
|
|
})
|
|
expect(resolveCursorBaseKey('composer-2.5[fast=true]', catalog)).toBe('composer-2.5')
|
|
expect(resolveCursorVariantOptions('composer-2.5', catalog)).toHaveLength(2)
|
|
})
|
|
|
|
it('ignores Cursor-provided display names for picker labels', () => {
|
|
const catalog = buildCursorModelCatalog([
|
|
{ modelId: 'gpt-5.3-codex[reasoning=medium,fast=false]', name: 'Codex 5.3' },
|
|
], { defaultValue: 'auto' })
|
|
expect(catalog.baseOptions.find((entry) => entry.value === 'gpt-5.3-codex')?.label).toBe('gpt-5.3-codex')
|
|
})
|
|
})
|
|
|
|
describe('picker labels and modes', () => {
|
|
it('formats flat labels as raw base when a base has one variant', () => {
|
|
const catalog = buildCursorModelCatalog([
|
|
{ modelId: 'claude-opus-4-7[thinking=true,context=300k,effort=xhigh,fast=false]', name: 'Claude Opus 4.7' },
|
|
{ modelId: 'composer-2.5[fast=true]', name: 'Composer 2.5' },
|
|
], { defaultValue: 'auto' })
|
|
expect(cursorCatalogHasMultiVariantBases(catalog)).toBe(false)
|
|
expect(buildFlatCursorModelPickerOptions(catalog, { defaultValue: 'auto' })).toEqual([
|
|
{ value: 'auto', label: CURSOR_AUTO_MODEL_LABEL },
|
|
{
|
|
value: 'claude-opus-4-7[thinking=true,context=300k,effort=xhigh,fast=false]',
|
|
label: 'claude-opus-4-7 · thinking=true,context=300k,effort=xhigh,fast=false'
|
|
},
|
|
{ value: 'composer-2.5[fast=true]', label: 'composer-2.5 · fast=true' }
|
|
])
|
|
})
|
|
|
|
it('lists a single variant row when a base has one wire', () => {
|
|
const catalog = buildCursorModelCatalog([
|
|
{ modelId: 'gpt-5.5[context=272k,reasoning=medium,fast=false]', name: 'gpt-5.5' },
|
|
], { defaultValue: 'auto' })
|
|
expect(buildCursorEffortPickerOptions(resolveCursorVariantOptions('gpt-5.5', catalog))).toEqual([
|
|
{
|
|
value: 'gpt-5.5[context=272k,reasoning=medium,fast=false]',
|
|
label: 'context=272k,reasoning=medium,fast=false'
|
|
}
|
|
])
|
|
})
|
|
|
|
it('shows raw variant labels when a base has multiple variants', () => {
|
|
const catalog = buildCursorModelCatalog([
|
|
{ modelId: 'composer-2.5[fast=true]', name: 'Composer 2.5' },
|
|
{ modelId: 'composer-2.5[fast=false]', name: 'Composer 2.5' },
|
|
], { defaultValue: 'auto' })
|
|
expect(buildFlatCursorModelPickerOptions(catalog, { defaultValue: 'auto' })).toEqual([
|
|
{ value: 'auto', label: CURSOR_AUTO_MODEL_LABEL },
|
|
{ value: 'composer-2.5[fast=true]', label: 'fast=true' },
|
|
{ value: 'composer-2.5[fast=false]', label: 'fast=false' },
|
|
])
|
|
expect(buildCursorEffortPickerOptions(resolveCursorVariantOptions('composer-2.5', catalog))).toEqual([
|
|
{ value: 'composer-2.5[fast=true]', label: 'fast=true' },
|
|
{ value: 'composer-2.5[fast=false]', label: 'fast=false' },
|
|
])
|
|
})
|
|
|
|
it('enables dual pickers when a base has multiple exact wire ids', () => {
|
|
const catalog = buildCursorModelCatalog([
|
|
{ modelId: 'claude-opus-4-7[thinking=true,context=300k,effort=xhigh,fast=false]', name: 'Claude Opus 4.7' },
|
|
{ modelId: 'composer-2.5[fast=true]', name: 'Composer 2.5' },
|
|
{ modelId: 'composer-2.5[fast=false]', name: 'Composer 2.5' },
|
|
], { defaultValue: 'auto' })
|
|
expect(cursorCatalogHasMultiVariantBases(catalog)).toBe(true)
|
|
expect(shouldUseCursorDualPickers(catalog, 'composer-2.5[fast=false]')).toBe(true)
|
|
})
|
|
|
|
it('uses raw variant suffixes for compatibility helpers', () => {
|
|
expect(cursorEffortPickerLabel('claude-opus-4-8[effort=high,fast=false]', [])).toBe('effort=high,fast=false')
|
|
expect(cursorVariantDisambiguationSuffix('claude-opus-4-8[effort=high,fast=false]')).toBe('effort=high,fast=false')
|
|
expect(formatCursorModelPickerLabel('composer-2.5[fast=true]', 'ignored')).toBe('composer-2.5 · fast=true')
|
|
})
|
|
})
|
|
|
|
describe('filterCursorModelOptionsForCompactView (iOS-style nested picker)', () => {
|
|
const options: { value: string | null; label: string }[] = [
|
|
{ value: 'auto', label: 'Default' },
|
|
{ value: 'claude-fable-5', label: 'claude-fable-5' },
|
|
{ value: 'claude-opus-4-7', label: 'claude-opus-4-7' },
|
|
{ value: 'composer-2.5', label: 'composer-2.5' },
|
|
{ value: 'gpt-5.5', label: 'gpt-5.5' }
|
|
]
|
|
|
|
it('passes the full list through when nothing or Default is selected', () => {
|
|
expect(filterCursorModelOptionsForCompactView(options, undefined)).toEqual(options)
|
|
expect(filterCursorModelOptionsForCompactView(options, null)).toEqual(options)
|
|
expect(filterCursorModelOptionsForCompactView(options, 'auto')).toEqual(options)
|
|
})
|
|
|
|
it('collapses to Default + the selected base when a non-Default base is picked', () => {
|
|
expect(filterCursorModelOptionsForCompactView(options, 'claude-fable-5')).toEqual([
|
|
{ value: 'auto', label: 'Default' },
|
|
{ value: 'claude-fable-5', label: 'claude-fable-5' }
|
|
])
|
|
})
|
|
|
|
it('treats `null`-valued Default rows as the Default passthrough', () => {
|
|
const withNullDefault: { value: string | null; label: string }[] = [
|
|
{ value: null, label: 'Default' },
|
|
{ value: 'composer-2.5', label: 'composer-2.5' },
|
|
{ value: 'gpt-5.5', label: 'gpt-5.5' }
|
|
]
|
|
expect(filterCursorModelOptionsForCompactView(withNullDefault, 'gpt-5.5')).toEqual([
|
|
{ value: null, label: 'Default' },
|
|
{ value: 'gpt-5.5', label: 'gpt-5.5' }
|
|
])
|
|
})
|
|
|
|
it('returns just Default when the selected base is not in the option set (catalog drift)', () => {
|
|
expect(filterCursorModelOptionsForCompactView(options, 'phantom-model-9')).toEqual([
|
|
{ value: 'auto', label: 'Default' }
|
|
])
|
|
})
|
|
})
|