fix(cursor): map base-only CLI sku to fast=false to avoid silent variant no-op (#887)

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 <noreply@hapi.run>
This commit is contained in:
SSU-WEI HUANG
2026-06-18 10:17:34 +08:00
committed by GitHub
co-authored by HAPI
parent 8f3ea10df7
commit d1a686f8d0
2 changed files with 61 additions and 5 deletions
+57 -2
View File
@@ -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', () => {
+4 -3
View File
@@ -91,9 +91,10 @@ function inferSkuParamHints(slug: string): Record<string, string> {
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';