From d1a686f8d004a8ba51ba3fb4ae0ec52d1eff8c1a Mon Sep 17 00:00:00 2001 From: SSU-WEI HUANG Date: Thu, 18 Jun 2026 10:17:34 +0800 Subject: [PATCH] fix(cursor): map base-only CLI sku to fast=false to avoid silent variant no-op (#887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without an explicit -fast suffix, inferSkuParamHints returned no fast hint, so matchCliSkuToAcpWireId tied between fast=true and fast=false wires and kept the first one. For composer-2.5 that meant the picker's "non-fast" sku silently resolved to composer-2.5[fast=true] — the same wire the fast sku resolves to — producing the "selected but no response" symptom in #883. Treat absence of -fast as fast=false so base-only skus pick the slow variant and round-trip back to the matching radio. Fixes #883 via [HAPI](https://hapi.run) Co-authored-by: HAPI --- shared/src/cursorCliSku.test.ts | 59 +++++++++++++++++++++++++++++++-- shared/src/cursorCliSku.ts | 7 ++-- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/shared/src/cursorCliSku.test.ts b/shared/src/cursorCliSku.test.ts index 5f5cb7ca..270de320 100644 --- a/shared/src/cursorCliSku.test.ts +++ b/shared/src/cursorCliSku.test.ts @@ -34,8 +34,14 @@ describe('matchCliSkuToAcpWireId', () => { expect(matchCliSkuToAcpWireId('gpt-5.5-medium', available)).toBe('gpt-5.5[context=272k,reasoning=medium,fast=false]'); }); - it('picks the best wire when multiple ACP variants exist', () => { - expect(matchCliSkuToAcpWireId('composer-2.5', available)).toBe('composer-2.5[fast=true]'); + it('maps base-only SKU to fast=false when fast variants exist (cursor CLI convention)', () => { + expect(matchCliSkuToAcpWireId('composer-2.5', available)).toBe('composer-2.5[fast=false]'); + }); + + it('still maps base-only SKU when only one variant exists', () => { + expect(matchCliSkuToAcpWireId('composer-2.5', [{ modelId: 'composer-2.5[fast=true]' }])).toBe( + 'composer-2.5[fast=true]' + ); }); }); @@ -49,6 +55,55 @@ describe('findBestCliSkuForAcpWire', () => { ]); expect(best).toBe('gpt-5.5-medium'); }); + + it('prefers base-only sku for fast=false wire over -fast sku', () => { + const wire = 'composer-2.5[fast=false]'; + const best = findBestCliSkuForAcpWire(wire, ['composer-2.5', 'composer-2.5-fast']); + expect(best).toBe('composer-2.5'); + }); + + it('prefers -fast sku for fast=true wire over base-only sku', () => { + const wire = 'composer-2.5[fast=true]'; + const best = findBestCliSkuForAcpWire(wire, ['composer-2.5', 'composer-2.5-fast']); + expect(best).toBe('composer-2.5-fast'); + }); +}); + +describe('round-trip (regression for #883: "selected but no response")', () => { + const acpWires = [ + { modelId: 'composer-2.5[fast=true]' }, + { modelId: 'composer-2.5[fast=false]' } + ]; + const pickerSkus = ['composer-2.5', 'composer-2.5-fast']; + + function simulateRoundTrip(clickedSku: string): { sessionModel: string; radioOn: string | null } { + // CLI side: applyCursorAcpModel → resolveCursorAcpWireId → matchCliSkuToAcpWireId + const sessionModel = matchCliSkuToAcpWireId(clickedSku, acpWires); + if (!sessionModel) { + throw new Error('CLI rejected sku'); + } + // Web side after refetch: cursorVariantSelectValue uses findBestCliSkuForAcpWire + const radioOn = findBestCliSkuForAcpWire(sessionModel, pickerSkus); + return { sessionModel, radioOn }; + } + + it('clicking composer-2.5 (slow) lands on the slow radio, not the fast one', () => { + const result = simulateRoundTrip('composer-2.5'); + expect(result.sessionModel).toBe('composer-2.5[fast=false]'); + expect(result.radioOn).toBe('composer-2.5'); + }); + + it('clicking composer-2.5-fast lands on the fast radio', () => { + const result = simulateRoundTrip('composer-2.5-fast'); + expect(result.sessionModel).toBe('composer-2.5[fast=true]'); + expect(result.radioOn).toBe('composer-2.5-fast'); + }); + + it('clicking each picker option lands on a distinct session model (no collapse)', () => { + const slow = simulateRoundTrip('composer-2.5').sessionModel; + const fast = simulateRoundTrip('composer-2.5-fast').sessionModel; + expect(slow).not.toBe(fast); + }); }); describe('isCursorAcpWireModelId', () => { diff --git a/shared/src/cursorCliSku.ts b/shared/src/cursorCliSku.ts index f72d3eda..9d61f5c2 100644 --- a/shared/src/cursorCliSku.ts +++ b/shared/src/cursorCliSku.ts @@ -91,9 +91,10 @@ function inferSkuParamHints(slug: string): Record { hints.reasoning = 'none'; } - if (lower.endsWith('-fast') || lower.includes('-fast')) { - hints.fast = 'true'; - } + // Cursor CLI convention: `-fast` suffix means fast=true; absence means fast=false. + // Without an explicit hint, base-only SKUs (e.g. `composer-2.5`) would tie-break to the + // first wire and silently coerce to the fast variant. + hints.fast = lower.includes('-fast') ? 'true' : 'false'; if (lower.includes('thinking')) { hints.thinking = 'true';