| @@ -19,6 +19,7 @@ import * as Layer from "effect/Layer"; · 18 unmodified lines |
| 19 | 19 | import * as Option from "effect/Option"; |
| 20 | 20 | import * as Path from "effect/Path"; |
| 21 | 21 | import * as Result from "effect/Result"; |
| 22 | +import * as Schema from "effect/Schema"; |
| 22 | 23 | import { HttpClient } from "effect/unstable/http"; |
| 23 | 24 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 24 | 25 | import { |
| @@ -42,6 +43,7 @@ import { · 17 unmodified lines |
| 42 | 43 | type ProviderMaintenanceCapabilities, |
| 43 | 44 | } from "../providerMaintenance.ts"; |
| 44 | 45 | import { AcpSessionRuntime } from "../acp/AcpSessionRuntime.ts"; |
| 46 | +import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts"; |
| 45 | 47 | |
| 46 | 48 | const PROVIDER = ProviderDriverKind.make("cursor"); |
| 47 | 49 | const CURSOR_PRESENTATION = { |
| @@ -54,8 +56,6 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ · 6 unmodified lines |
| 54 | 56 | }); |
| 55 | 57 | |
| 56 | 58 | const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000; |
| 57 | | −const CURSOR_ACP_MODEL_CAPABILITY_TIMEOUT = "4 seconds"; |
| 58 | | −const CURSOR_ACP_MODEL_DISCOVERY_CONCURRENCY = 4; |
| 59 | 59 | const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08; |
| 60 | 60 | export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = { |
| 61 | 61 | _meta: { |
| @@ -154,12 +154,6 @@ function normalizeCursorReasoningValue(value: string | null | undefined): string · 92 unmodified lines |
| 154 | 154 | } |
| 155 | 155 | } |
| 156 | 156 | |
| 157 | | −function findCursorModelConfigOption( |
| 158 | | − configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>, |
| 159 | | −): EffectAcpSchema.SessionConfigOption | undefined { |
| 160 | | − return configOptions.find((option) => option.category === "model"); |
| 161 | | −} |
| 162 | | − |
| 163 | 157 | function getCursorConfigOptionCategory(option: EffectAcpSchema.SessionConfigOption): string { |
| 164 | 158 | return option.category?.trim().toLowerCase() ?? ""; |
| 165 | 159 | } |
| @@ -373,36 +367,25 @@ function buildCursorDiscoveredModels( · 207 unmodified lines |
| 373 | 367 | }); |
| 374 | 368 | } |
| 375 | 369 | |
| 376 | | −function hasCursorModelCapabilities(model: Pick<ServerProviderModel, "capabilities">): boolean { |
| 377 | | − return (model.capabilities?.optionDescriptors?.length ?? 0) > 0; |
| 378 | | −} |
| 379 | | − |
| 380 | | −export function buildCursorDiscoveredModelsFromConfigOptions( |
| 381 | | − configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption> | null | undefined, |
| 370 | +function buildCursorDiscoveredModelsFromAvailableModelsResponse( |
| 371 | + response: typeof CursorListAvailableModelsResponse.Type, |
| 382 | 372 | ): ReadonlyArray<ServerProviderModel> { |
| 383 | | − if (!configOptions || configOptions.length === 0) { |
| 384 | | − return []; |
| 385 | | − } |
| 386 | | − |
| 387 | | − const modelOption = findCursorModelConfigOption(configOptions); |
| 388 | | − const modelChoices = flattenSessionConfigSelectOptions(modelOption); |
| 389 | | − if (!modelOption || modelChoices.length === 0) { |
| 390 | | − return []; |
| 391 | | − } |
| 392 | | − |
| 393 | | − const currentModelValue = |
| 394 | | − modelOption.type === "select" ? modelOption.currentValue?.trim() || undefined : undefined; |
| 395 | | − const currentModelCapabilities = buildCursorCapabilitiesFromConfigOptions(configOptions); |
| 396 | | − |
| 397 | 373 | return buildCursorDiscoveredModels( |
| 398 | | − modelChoices.map((modelChoice) => ({ |
| 399 | | − slug: modelChoice.value.trim(), |
| 400 | | − name: modelChoice.name.trim(), |
| 401 | | − capabilities: |
| 402 | | − currentModelValue === modelChoice.value.trim() |
| 403 | | − ? currentModelCapabilities |
| 404 | | − : EMPTY_CAPABILITIES, |
| 405 | | − })), |
| 374 | + response.models.flatMap((model) => { |
| 375 | + const slug = model.value.trim(); |
| 376 | + const name = model.name.trim(); |
| 377 | + if (!slug || !name) { |
| 378 | + return []; |
| 379 | + } |
| 380 | + |
| 381 | + return [ |
| 382 | + { |
| 383 | + slug, |
| 384 | + name, |
| 385 | + capabilities: buildCursorCapabilitiesFromConfigOptions(model.configOptions), |
| 386 | + }, |
| 387 | + ]; |
| 388 | + }), |
| 406 | 389 | ); |
| 407 | 390 | } |
| 408 | 391 | |
| @@ -554,140 +537,28 @@ export function resolveCursorAcpConfigUpdates( · 145 unmodified lines |
| 554 | 537 | return updates; |
| 555 | 538 | } |
| 556 | 539 | |
| 557 | | −export const discoverCursorModelsViaAcp = ( |
| 540 | +const discoverCursorModelsViaListAvailableModels = ( |
| 558 | 541 | cursorSettings: CursorSettings, |
| 559 | 542 | environment: NodeJS.ProcessEnv = process.env, |
| 560 | 543 | ) => |
| 561 | 544 | withCursorAcpProbeRuntime( |
| 562 | 545 | cursorSettings, |
| 563 | 546 | (acp) => |
| 564 | | − Effect.map(acp.start(), (started) => |
| 565 | | − buildCursorDiscoveredModelsFromConfigOptions( |
| 566 | | − started.sessionSetupResult.configOptions ?? [], |
| 567 | | − ), |
| 568 | | − ), |
| 547 | + Effect.gen(function* () { |
| 548 | + yield* acp.start(); |
| 549 | + const response = yield* acp.request("cursor/list_available_models", {}); |
| 550 | + const decoded = yield* Schema.decodeUnknownEffect(CursorListAvailableModelsResponse)( |
| 551 | + response, |
| 552 | + ); |
| 553 | + return buildCursorDiscoveredModelsFromAvailableModelsResponse(decoded); |
| 554 | + }), |
| 569 | 555 | environment, |
| 570 | 556 | ); |
| 571 | 557 | |
| 572 | | −export const discoverCursorModelCapabilitiesViaAcp = ( |
| 558 | +export const discoverCursorModelsViaAcp = ( |
| 573 | 559 | cursorSettings: CursorSettings, |
| 574 | | − existingModels: ReadonlyArray<ServerProviderModel>, |
| 575 | 560 | environment: NodeJS.ProcessEnv = process.env, |
| 576 | | −) => |
| 577 | | − withCursorAcpProbeRuntime( |
| 578 | | − cursorSettings, |
| 579 | | − (acp) => |
| 580 | | − Effect.gen(function* () { |
| 581 | | − const started = yield* acp.start(); |
| 582 | | − const initialConfigOptions = started.sessionSetupResult.configOptions ?? []; |
| 583 | | − const modelOption = findCursorModelConfigOption(initialConfigOptions); |
| 584 | | − const modelChoices = flattenSessionConfigSelectOptions(modelOption); |
| 585 | | − if (!modelOption || modelChoices.length === 0) { |
| 586 | | − return []; |
| 587 | | − } |
| 588 | | − |
| 589 | | − const currentModelValue = |
| 590 | | − modelOption.type === "select" ? modelOption.currentValue?.trim() || undefined : undefined; |
| 591 | | − const capabilitiesBySlug = new Map<string, ModelCapabilities>(); |
| 592 | | − if (currentModelValue) { |
| 593 | | − capabilitiesBySlug.set( |
| 594 | | − currentModelValue, |
| 595 | | − buildCursorCapabilitiesFromConfigOptions(initialConfigOptions), |
| 596 | | − ); |
| 597 | | − } |
| 598 | | − |
| 599 | | − const targetModelSlugs = new Set<string>(); |
| 600 | | − for (const model of existingModels) { |
| 601 | | − if (!model.isCustom && !hasCursorModelCapabilities(model)) { |
| 602 | | − targetModelSlugs.add(model.slug); |
| 603 | | − } |
| 604 | | − } |
| 605 | | − if (targetModelSlugs.size === 0) { |
| 606 | | − return buildCursorDiscoveredModels( |
| 607 | | − modelChoices.map((modelChoice) => ({ |
| 608 | | − slug: modelChoice.value.trim(), |
| 609 | | − name: modelChoice.name.trim(), |
| 610 | | − capabilities: capabilitiesBySlug.get(modelChoice.value.trim()) ?? EMPTY_CAPABILITIES, |
| 611 | | − })), |
| 612 | | − ); |
| 613 | | − } |
| 614 | | − |
| 615 | | − const probedCapabilities = yield* Effect.forEach( |
| 616 | | − modelChoices, |
| 617 | | − (modelChoice) => { |
| 618 | | − const modelSlug = modelChoice.value.trim(); |
| 619 | | − if ( |
| 620 | | − !modelSlug || |
| 621 | | − !targetModelSlugs.has(modelSlug) || |
| 622 | | − capabilitiesBySlug.has(modelSlug) |
| 623 | | − ) { |
| 624 | | − return Effect.void.pipe( |
| 625 | | − Effect.as<readonly [string, ModelCapabilities] | undefined>(undefined), |
| 626 | | − ); |
| 627 | | − } |
| 628 | | − |
| 629 | | − return withCursorAcpProbeRuntime( |
| 630 | | − cursorSettings, |
| 631 | | − (probeAcp) => |
| 632 | | − Effect.gen(function* () { |
| 633 | | − const probeStarted = yield* probeAcp.start(); |
| 634 | | − const probeConfigOptions = probeStarted.sessionSetupResult.configOptions ?? []; |
| 635 | | − const probeModelOption = findCursorModelConfigOption(probeConfigOptions); |
| 636 | | − const probeCurrentModelValue = |
| 637 | | − probeModelOption?.type === "select" |
| 638 | | − ? probeModelOption.currentValue?.trim() || undefined |
| 639 | | − : undefined; |
| 640 | | − yield* Effect.annotateCurrentSpan({ |
| 641 | | − "cursor.acp.model.value": modelSlug, |
| 642 | | − "cursor.acp.model.currentValue": probeCurrentModelValue, |
| 643 | | − "cursor.acp.config_option_id": probeModelOption?.id ?? modelOption.id, |
| 644 | | − }); |
| 645 | | − const nextConfigOptions = |
| 646 | | − probeCurrentModelValue === modelSlug |
| 647 | | − ? probeConfigOptions |
| 648 | | − : yield* probeAcp |
| 649 | | − .setConfigOption(probeModelOption?.id ?? modelOption.id, modelSlug) |
| 650 | | − .pipe( |
| 651 | | − Effect.map((response) => response.configOptions ?? probeConfigOptions), |
| 652 | | − ); |
| 653 | | − return [ |
| 654 | | − modelSlug, |
| 655 | | − buildCursorCapabilitiesFromConfigOptions(nextConfigOptions), |
| 656 | | − ] as const; |
| 657 | | − }), |
| 658 | | − environment, |
| 659 | | − ).pipe( |
| 660 | | − Effect.timeout(CURSOR_ACP_MODEL_CAPABILITY_TIMEOUT), |
| 661 | | − Effect.retry({ times: 3 }), |
| 662 | | − Effect.withSpan("cursor-acp-model-capability-probe"), |
| 663 | | − Effect.catchCause((cause) => |
| 664 | | − Effect.logWarning("Cursor ACP capability probe failed", { |
| 665 | | − modelSlug, |
| 666 | | − cause: Cause.pretty(cause), |
| 667 | | − }), |
| 668 | | − ), |
| 669 | | − ); |
| 670 | | − }, |
| 671 | | − { concurrency: CURSOR_ACP_MODEL_DISCOVERY_CONCURRENCY }, |
| 672 | | − ); |
| 673 | | − |
| 674 | | − for (const entry of probedCapabilities) { |
| 675 | | − if (!entry) { |
| 676 | | − continue; |
| 677 | | − } |
| 678 | | − capabilitiesBySlug.set(entry[0], entry[1]); |
| 679 | | − } |
| 680 | | − |
| 681 | | − return buildCursorDiscoveredModels( |
| 682 | | − modelChoices.map((modelChoice) => ({ |
| 683 | | − slug: modelChoice.value.trim(), |
| 684 | | − name: modelChoice.name.trim(), |
| 685 | | − capabilities: capabilitiesBySlug.get(modelChoice.value.trim()) ?? EMPTY_CAPABILITIES, |
| 686 | | − })), |
| 687 | | − ); |
| 688 | | − }).pipe(Effect.withSpan("cursor-acp-model-capability-discovery", {})), |
| 689 | | − environment, |
| 690 | | − ); |
| 561 | +) => discoverCursorModelsViaListAvailableModels(cursorSettings, environment); |
| 691 | 562 | |
| 692 | 563 | export function getCursorFallbackModels( |
| 693 | 564 | cursorSettings: Pick<CursorSettings, "customModels">, |
| @@ -1219,36 +1090,30 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( · 525 unmodified lines |
| 1219 | 1090 | }); |
| 1220 | 1091 | }); |
| 1221 | 1092 | |
| 1222 | | −export function hasUncapturedCursorModels(snapshot: Pick<ServerProvider, "models">): boolean { |
| 1223 | | − return snapshot.models.some((model) => !model.isCustom && !hasCursorModelCapabilities(model)); |
| 1224 | | −} |
| 1225 | | − |
| 1226 | 1093 | /** |
| 1227 | | − * Background capability enrichment for a Cursor snapshot. |
| 1094 | + * Background maintenance enrichment for a Cursor snapshot. |
| 1228 | 1095 | * |
| 1229 | 1096 | * Used by `CursorDriver` as the `makeManagedServerProvider.enrichSnapshot` |
| 1230 | | − * hook: runs the slow ACP per-model capability probe, and republishes the |
| 1231 | | − * snapshot through `publishSnapshot` when new capabilities arrive. Skips |
| 1232 | | − * the probe when the provider is disabled, unauthenticated, or has no |
| 1233 | | − * uncaptured models. Keeps `EMPTY_CAPABILITIES` and the `PROVIDER` literal |
| 1234 | | − * private to this module. |
| 1097 | + * hook: republishes update/version advisory metadata without performing any |
| 1098 | + * model or capability discovery. Cursor model data comes exclusively from |
| 1099 | + * `cursor/list_available_models` during provider status checks. |
| 1235 | 1100 | */ |
| 1236 | 1101 | export const enrichCursorSnapshot = (input: { |
| 1237 | 1102 | readonly settings: CursorSettings; |
| 1238 | | − readonly environment?: NodeJS.ProcessEnv; |
| 1239 | 1103 | readonly snapshot: ServerProvider; |
| 1240 | 1104 | readonly maintenanceCapabilities: ProviderMaintenanceCapabilities; |
| 1241 | 1105 | readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect<void>; |
| 1242 | 1106 | readonly stampIdentity?: (snapshot: ServerProvider) => ServerProvider; |
| 1243 | 1107 | readonly httpClient: HttpClient.HttpClient; |
| 1244 | | −}): Effect.Effect<void, never, ChildProcessSpawner.ChildProcessSpawner> => { |
| 1108 | +}): Effect.Effect<void> => { |
| 1245 | 1109 | const { settings, snapshot, publishSnapshot } = input; |
| 1246 | 1110 | const stampIdentity = input.stampIdentity ?? ((value) => value); |
| 1247 | 1111 | |
| 1248 | | − const enrichVersionAdvisory = enrichProviderSnapshotWithVersionAdvisory( |
| 1249 | | − snapshot, |
| 1250 | | − input.maintenanceCapabilities, |
| 1251 | | − ).pipe( |
| 1112 | + if (!settings.enabled || snapshot.auth.status === "unauthenticated") { |
| 1113 | + return Effect.void; |
| 1114 | + } |
| 1115 | + |
| 1116 | + return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities).pipe( |
| 1252 | 1117 | Effect.provideService(HttpClient.HttpClient, input.httpClient), |
| 1253 | 1118 | Effect.flatMap((enrichedSnapshot) => |
| 1254 | 1119 | publishSnapshot(stampIdentity(enrichedSnapshot)).pipe(Effect.as(enrichedSnapshot)), |
| @@ -1256,48 +1121,7 @@ export const enrichCursorSnapshot = (input: { · 1 unmodified line |
| 1256 | 1121 | Effect.catchCause((cause) => |
| 1257 | 1122 | Effect.logWarning("Cursor version advisory enrichment failed", { |
| 1258 | 1123 | cause: Cause.pretty(cause), |
| 1259 | | − }).pipe(Effect.as(snapshot)), |
| 1124 | + }).pipe(Effect.asVoid), |
| 1260 | 1125 | ), |
| 1261 | 1126 | ); |
| 1262 | | − |
| 1263 | | − return enrichVersionAdvisory.pipe( |
| 1264 | | − Effect.flatMap((baseSnapshot) => { |
| 1265 | | − if ( |
| 1266 | | − !settings.enabled || |
| 1267 | | − baseSnapshot.auth.status === "unauthenticated" || |
| 1268 | | − !hasUncapturedCursorModels(baseSnapshot) |
| 1269 | | − ) { |
| 1270 | | − return Effect.void; |
| 1271 | | − } |
| 1272 | | − |
| 1273 | | − return discoverCursorModelCapabilitiesViaAcp( |
| 1274 | | − settings, |
| 1275 | | − baseSnapshot.models, |
| 1276 | | − input.environment, |
| 1277 | | − ).pipe( |
| 1278 | | − Effect.flatMap((discoveredModels) => { |
| 1279 | | − if (discoveredModels.length === 0) { |
| 1280 | | − return Effect.void; |
| 1281 | | − } |
| 1282 | | − return publishSnapshot( |
| 1283 | | − stampIdentity({ |
| 1284 | | − ...baseSnapshot, |
| 1285 | | − models: providerModelsFromSettings( |
| 1286 | | − discoveredModels, |
| 1287 | | − PROVIDER, |
| 1288 | | − settings.customModels, |
| 1289 | | − EMPTY_CAPABILITIES, |
| 1290 | | − ), |
| 1291 | | − }), |
| 1292 | | − ); |
| 1293 | | − }), |
| 1294 | | − Effect.catchCause((cause) => |
| 1295 | | − Effect.logWarning("Cursor ACP background capability enrichment failed", { |
| 1296 | | − models: baseSnapshot.models.map((model) => model.slug), |
| 1297 | | − cause: Cause.pretty(cause), |
| 1298 | | − }).pipe(Effect.asVoid), |
| 1299 | | − ), |
| 1300 | | − ); |
| 1301 | | − }), |
| 1302 | | − ); |
| 1303 | 1127 | }; |