fix(chat): show each Cursor model's own Fast default before a chat runs

Composer and Grok run Fast until the person picks, so the toggle reads the
model's default from Cursor's listing instead of showing Off. Live chats keep
the listing's Fast support while their current model has none, question
groups too big for one card are skipped with a reason so Cursor asks again in
smaller groups, and the config pin follows an inherited XDG_CONFIG_HOME.
This commit is contained in:
Kelvin Amoaba
2026-11-13 22:56:36 +00:00
parent eafddefbb1
commit 3ba49a337c
11 changed files with 55 additions and 18 deletions
+10 -4
View File
@@ -41,10 +41,6 @@ function questionRequest(params: unknown): AcpRequestPresentation {
if (!parsed.success) {
throw new AcpRpcError(-32602, 'Invalid agent question request')
}
// A card keeps only so many questions; refused, Cursor asks each one on its own.
if (parsed.data.questions.length > MAX_JOURNAL_GROUPED_PROMPT_QUESTIONS) {
throw new AcpRpcError(-32602, 'Too many questions for one card')
}
const questions = parsed.data.questions.map((question) => ({
id: question.id,
question: question.prompt,
@@ -115,10 +111,20 @@ function fallbackQuestionRequest(params: unknown): AcpRequestPresentation | unde
}
}
const TOO_MANY_QUESTIONS = `Orca shows at most ${MAX_JOURNAL_GROUPED_PROMPT_QUESTIONS} questions at once; ask again with at most ${MAX_JOURNAL_GROUPED_PROMPT_QUESTIONS}.`
function settleRequest(method: string, params: unknown): AcpRequestSettlement | undefined {
if (REPORTS.includes(method)) {
return { reply: {} }
}
// A card keeps only so many questions; any error would make Cursor drop multi-select ones.
if (
method === ASK_QUESTION &&
(questionRequestSchema.safeParse(params).data?.questions.length ?? 0) >
MAX_JOURNAL_GROUPED_PROMPT_QUESTIONS
) {
return { reply: { outcome: { outcome: 'skipped', reason: TOO_MANY_QUESTIONS } } }
}
if (method !== CREATE_PLAN) {
return undefined
}
@@ -30,6 +30,7 @@ export async function readCursorModelCatalog(connection: {
(raw) => SessionConfigOptionSchema.safeParse(raw).data ?? []
)
const effort = acpEffortOption(options)
const fast = options.find((option) => option.id === 'fast')
return {
id: model.value,
label: model.name,
@@ -39,7 +40,8 @@ export async function readCursorModelCatalog(connection: {
? acpSelectChoices(effort).map((choice) => ({ value: choice.value, label: choice.name }))
: [],
...(effort ? { defaultEffort: effort.currentValue } : {}),
supportsFastMode: options.some((option) => option.id === 'fast')
supportsFastMode: fast !== undefined,
...(fast?.type === 'select' ? { defaultFastMode: fast.currentValue === 'true' } : {})
}
})
}
@@ -243,7 +243,7 @@ it("writes the person's Fast choice again after a model switch resets it", async
).toEqual(['model=grok-4.7', 'fast=false', 'model=composer-2.5', 'fast=false'])
})
it("lists each model's own effort levels and Fast support before a chat exists", async () => {
it("lists each model's own effort levels and Fast default before a chat exists", async () => {
const models = await readCursorModelCatalog({
requestSessionFreeExtension: async (method) => {
expect(method).toBe('cursor/list_available_models')
@@ -255,7 +255,12 @@ it("lists each model's own effort levels and Fast support before a chat exists",
name: 'Claude Opus 4.8',
configOptions: OPUS_OPTIONS.slice(1)
},
{ value: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', configOptions: [] }
{ value: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', configOptions: [] },
{
value: 'composer-2.5',
name: 'Composer 2.5',
configOptions: [{ ...OPUS_OPTIONS[3]!, currentValue: 'true' }]
}
]
}
}
@@ -272,7 +277,8 @@ it("lists each model's own effort levels and Fast support before a chat exists",
{ value: 'max', label: 'Max' }
],
defaultEffort: 'high',
supportsFastMode: true
supportsFastMode: true,
defaultFastMode: false
},
{
id: 'gemini-3.1-pro',
@@ -280,6 +286,14 @@ it("lists each model's own effort levels and Fast support before a chat exists",
isDefault: false,
efforts: [],
supportsFastMode: false
},
{
id: 'composer-2.5',
label: 'Composer 2.5',
isDefault: false,
efforts: [],
supportsFastMode: true,
defaultFastMode: true
}
])
})
+1 -1
View File
@@ -15,7 +15,7 @@ const CURSOR_SESSION_HISTORY: StructuredAgentSessionHistory = {
// The config folder holds Cursor's allowlist and sessions; its login lives elsewhere per platform.
function cursorConfigDirectory(homePath: string, env: NodeJS.ProcessEnv): string {
const xdg = env.XDG_CONFIG_HOME?.trim()
const xdg = (env.XDG_CONFIG_HOME ?? process.env.XDG_CONFIG_HOME)?.trim()
return xdg ? join(xdg, 'cursor') : join(homePath, '.cursor')
}
@@ -69,6 +69,9 @@ export function mergedAgentModelCatalogModels(
...(model.supportsFastMode === undefined && olderModel?.supportsFastMode !== undefined
? { supportsFastMode: olderModel.supportsFastMode }
: {}),
...(model.defaultFastMode === undefined && olderModel?.defaultFastMode !== undefined
? { defaultFastMode: olderModel.defaultFastMode }
: {}),
...(observation !== undefined
? {
declaredDefaultEffort:
@@ -81,6 +81,7 @@ function parseModel(value: unknown): AgentSessionModelOption | null {
...(typeof row.supportsFastMode === 'boolean'
? { supportsFastMode: row.supportsFastMode }
: {}),
...(typeof row.defaultFastMode === 'boolean' ? { defaultFastMode: row.defaultFastMode } : {}),
...(typeof row.supportsAutoMode === 'boolean'
? { supportsAutoMode: row.supportsAutoMode }
: {}),
@@ -134,12 +134,14 @@ export async function readStructuredAgentSessionOptions(
const restingOptions = await atRest()
const recovery = await readStructuredSessionRecovery(session, context, sessionId)
if (live && fillsLiveModelsFromCatalog(agent)) {
const listed = restingOptions.inventory.origin === 'unknown' ? null : restingOptions.inventory
options = {
...options,
models: fillUnknownModelEfforts(
options.models,
restingOptions.inventory.origin === 'unknown' ? [] : restingOptions.inventory.models
)
models: fillUnknownModelEfforts(options.models, listed?.models ?? []),
// A current model without Fast must not hide it for the listed models that have it.
...(!options.fastModeSupport && listed?.fastModeSupport
? { fastModeSupport: listed.fastModeSupport }
: {})
}
}
const resting = live ? restingOptions : null
@@ -377,6 +377,8 @@ export type AgentSessionModelOption = {
chatEfforts?: AgentSessionOptionChoice[]
/** Provider catalog fact. Absent means the host could not determine support. */
supportsFastMode?: boolean
/** Whether this model runs Fast until the person picks; absent is off. */
defaultFastMode?: boolean
/** Whether the model runs Approve for me; absent is unknown, which neither offers nor launches it. */
supportsAutoMode?: boolean
/** Provider tier IDs beyond standard; capable clients replace Fast with one speed choice including default. Absence means unknown. */
@@ -32,6 +32,9 @@ export function fillUnknownModelEfforts(
: {}),
...(rest.supportsFastMode === undefined && discovered.supportsFastMode !== undefined
? { supportsFastMode: discovered.supportsFastMode }
: {}),
...(rest.defaultFastMode === undefined && discovered.defaultFastMode !== undefined
? { defaultFastMode: discovered.defaultFastMode }
: {})
}
})
@@ -32,12 +32,12 @@ function effortOption(
}
}
function fastModeOption(): CatalogOption {
function fastModeOption(defaultValue: boolean): CatalogOption {
return {
id: 'fastMode',
label: 'Fast mode',
category: 'mode',
kind: { type: 'boolean', defaultValue: false },
kind: { type: 'boolean', defaultValue },
apply: {}
}
}
@@ -89,7 +89,7 @@ export function discoveredModel(
...(serviceTier
? [serviceTier]
: sessionSupportsFastMode && model.supportsFastMode === true
? [fastModeOption()]
? [fastModeOption(model.defaultFastMode === true)]
: [])
]
}
@@ -189,14 +189,15 @@ describe('a Claude chat on the model id an older host lists by alias', () => {
})
describe('what a send carries', () => {
it("leaves a Cursor model's own Fast default alone until the person picks Fast", () => {
it("shows and keeps a Cursor model's own Fast default until the person picks Fast", () => {
const seed = structuredAgentSessionSeedCatalog('cursor')
const composer = {
id: 'composer-2.5',
label: 'Composer 2.5',
isDefault: true,
efforts: [],
supportsFastMode: true
supportsFastMode: true,
defaultFastMode: true
}
const listed = applyStructuredAgentSessionModelCatalog(
createStructuredAgentSessionOptionState('cursor', seed),
@@ -206,6 +207,9 @@ describe('what a send carries', () => {
)
const picked = commitStructuredAgentSessionOption(listed, 'model', 'composer-2.5')
expect(structuredAgentSessionMessageSelection(picked)).toEqual({ model: 'composer-2.5' })
expect(
structuredAgentSessionOptionSnapshot(picked).find((option) => option.id === 'fastMode')?.kind
).toMatchObject({ type: 'boolean', currentValue: true })
})
it('keeps a provisional Codex effort out of capture across a reply and catalog refresh', () => {