Files
hapi/web/src/lib/sessionChatCursorModel.ts
T
ad038bbf2e fix(cursor): merge SKU catalog under ACP lock and refcount agent guard (#835)
Fixes incomplete cliModelSkus while agent acp holds the CLI lock (#831) and
replace single-pid ACP lock with cross-process refcount (#832). Web picker
merges machine/session catalogs and waits for SKU readiness before showing
variant labels.

Fixes #831
Fixes #832

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:30:43 +08:00

215 lines
6.6 KiB
TypeScript

import { findBestCliSkuForAcpWire, matchCliSkuToAcpWireId } from '@hapi/protocol'
import type { CursorModelCatalog } from '@/lib/cursorModelOptions'
import type { CursorModelSummary } from '@/types/api'
import {
buildCursorCatalogFromSources,
buildCursorPickerState,
resolveCursorBaseFromWire,
resolveWireIdForBaseChange,
type CursorPickerState
} from '@/lib/cursorPickerState'
export function isCursorEffortWireInCatalog(
wireId: string,
catalog: CursorModelCatalog
): boolean {
return catalog.wireToBase.has(wireId)
}
export function resolveSessionCursorBaseSelectValue(
picker: CursorPickerState,
cursorSelectedBase: string
): string {
if (picker.mode !== 'dual') {
return picker.wireId ?? 'auto'
}
if (cursorSelectedBase !== 'auto') {
return cursorSelectedBase
}
// Catalog already reflects session.model; local base state may still be initial 'auto'.
if (picker.baseKey && picker.baseKey !== 'auto') {
return picker.baseKey
}
return 'auto'
}
export function resolveSessionCursorModelChange(args: {
picker: CursorPickerState
sessionModel: string | null | undefined
cursorSelectedBase: string
kind: 'base' | 'effort' | 'flat'
value: string | null
}): { ok: true; wireId: string | null; nextSelectedBase: string; shouldApply: boolean } | { ok: false; reason: string } {
const { picker, sessionModel, cursorSelectedBase, kind, value } = args
if (kind === 'base') {
if (!value || value === 'auto') {
return { ok: true, wireId: null, nextSelectedBase: 'auto', shouldApply: true }
}
if (value.includes('[')) {
const base = resolveCursorBaseFromWire(value, picker.catalog)
return { ok: true, wireId: value, nextSelectedBase: base, shouldApply: true }
}
const wireId = resolveWireIdForBaseChange(value, picker.catalog, sessionModel)
return {
ok: true,
wireId: wireId === 'auto' ? null : wireId,
nextSelectedBase: value,
shouldApply: wireId !== null
}
}
if (kind === 'effort') {
if (!value || value === 'auto') {
return { ok: true, wireId: null, nextSelectedBase: cursorSelectedBase, shouldApply: true }
}
if (!isCursorEffortWireInCatalog(value, picker.catalog)) {
return { ok: false, reason: 'effort wire id not in catalog' }
}
const base = resolveCursorBaseFromWire(value, picker.catalog)
return { ok: true, wireId: value, nextSelectedBase: base, shouldApply: true }
}
// flat picker: value is wire id or auto
if (!value || value === 'auto') {
return { ok: true, wireId: null, nextSelectedBase: 'auto', shouldApply: true }
}
if (!picker.catalog.wireToBase.has(value)) {
return { ok: false, reason: 'model wire id not in catalog' }
}
return {
ok: true,
wireId: value,
nextSelectedBase: resolveCursorBaseFromWire(value, picker.catalog),
shouldApply: true
}
}
export function resolveSessionCursorVariantSelectValue(
sessionModel: string | null | undefined,
effortOptions: readonly { value: string }[]
): string | null {
if (!sessionModel || effortOptions.length === 0) {
return null
}
const trimmed = sessionModel.trim()
const exact = effortOptions.find((option) => option.value === trimmed)
if (exact) {
return exact.value
}
if (trimmed.includes('[')) {
const bestSku = findBestCliSkuForAcpWire(
trimmed,
effortOptions.map((option) => option.value)
);
if (bestSku) {
return bestSku;
}
}
return null
}
export function isSessionCursorCatalogLoading(args: {
sessionLoading: boolean
machineLoading: boolean
hasMachineId: boolean
sessionError: string | null
machineError: string | null
}): boolean {
if (args.sessionLoading) {
return true
}
if (args.hasMachineId && args.machineLoading && !args.machineError) {
return true
}
return false
}
export function isSessionCursorCatalogAwaitingSkus(args: {
sessionLoading: boolean
machineLoading: boolean
sessionError: string | null
machineError: string | null
mergedSkus: readonly CursorModelSummary[]
picker: CursorPickerState | null
}): boolean {
if (args.sessionLoading || args.machineLoading) {
return false
}
if (args.sessionError || args.machineError) {
return false
}
if (!args.picker || args.picker.mode !== 'dual') {
return false
}
if (args.mergedSkus.length > 0) {
return false
}
return args.picker.showEffortPicker
}
export const SESSION_CURSOR_CATALOG_SKU_TIMEOUT_MS = 15_000
export function isSessionCursorCatalogPending(args: {
sessionLoading: boolean
machineLoading: boolean
hasMachineId: boolean
sessionError: string | null
machineError: string | null
mergedSkus: readonly CursorModelSummary[]
picker: CursorPickerState | null
}): boolean {
return isSessionCursorCatalogLoading(args)
|| isSessionCursorCatalogAwaitingSkus(args)
}
export function isSessionCursorCatalogPendingWithTimeout(args: {
sessionLoading: boolean
machineLoading: boolean
hasMachineId: boolean
sessionError: string | null
machineError: string | null
mergedSkus: readonly CursorModelSummary[]
picker: CursorPickerState | null
awaitingStartedAtMs: number | null
nowMs?: number
timeoutMs?: number
}): boolean {
if (!isSessionCursorCatalogPending(args)) {
return false
}
if (!isSessionCursorCatalogAwaitingSkus(args)) {
return true
}
if (args.awaitingStartedAtMs === null) {
return true
}
const elapsed = (args.nowMs ?? Date.now()) - args.awaitingStartedAtMs
return elapsed < (args.timeoutMs ?? SESSION_CURSOR_CATALOG_SKU_TIMEOUT_MS)
}
export function buildSessionCursorPickerState(args: {
sessionModels: readonly CursorModelSummary[]
machineModels: readonly CursorModelSummary[]
cliModelSkus?: readonly CursorModelSummary[]
sessionModel: string | null | undefined
sessionCurrentModelId: string | null
}): CursorPickerState {
const catalog = buildCursorCatalogFromSources({
sessionModels: args.sessionModels,
machineModels: args.machineModels,
cliModelSkus: args.cliModelSkus,
currentWireId: args.sessionCurrentModelId ?? args.sessionModel,
sessionModelFromHub: args.sessionModel,
defaultValue: null
})
return buildCursorPickerState({
catalog,
currentWireId: args.sessionModel ?? args.sessionCurrentModelId,
defaultValue: null
})
}