From 3ba49a337c2b5994672ecfbec1a569603eb6b483 Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Fri, 13 Nov 2026 21:49:26 +0000 Subject: [PATCH] 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. --- src/main/acp/acp-dialects/cursor-dialect.ts | 14 +++++++++---- .../acp/acp-dialects/cursor-model-catalog.ts | 4 +++- .../acp/session/acp-structured-cursor.test.ts | 20 ++++++++++++++++--- src/main/cursor/cursor-acp-launch-spec.ts | 2 +- .../agent-model-catalog-models.ts | 3 +++ .../agent-model-catalog-persistence.ts | 1 + .../structured-agent-session-options-read.ts | 10 ++++++---- .../agent-session/agent-session-wire.ts | 2 ++ .../options/agent-session-model-efforts.ts | 3 +++ ...ructured-agent-session-discovered-model.ts | 6 +++--- ...ructured-agent-session-option-view.test.ts | 8 ++++++-- 11 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/main/acp/acp-dialects/cursor-dialect.ts b/src/main/acp/acp-dialects/cursor-dialect.ts index 211b71b55fd..255a1d44473 100644 --- a/src/main/acp/acp-dialects/cursor-dialect.ts +++ b/src/main/acp/acp-dialects/cursor-dialect.ts @@ -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 } diff --git a/src/main/acp/acp-dialects/cursor-model-catalog.ts b/src/main/acp/acp-dialects/cursor-model-catalog.ts index e76a6b20620..10b3c79e94e 100644 --- a/src/main/acp/acp-dialects/cursor-model-catalog.ts +++ b/src/main/acp/acp-dialects/cursor-model-catalog.ts @@ -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' } : {}) } }) } diff --git a/src/main/acp/session/acp-structured-cursor.test.ts b/src/main/acp/session/acp-structured-cursor.test.ts index fbfcfde2b11..60a16f7a378 100644 --- a/src/main/acp/session/acp-structured-cursor.test.ts +++ b/src/main/acp/session/acp-structured-cursor.test.ts @@ -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 } ]) }) diff --git a/src/main/cursor/cursor-acp-launch-spec.ts b/src/main/cursor/cursor-acp-launch-spec.ts index 23bb82ee350..baa29bc819a 100644 --- a/src/main/cursor/cursor-acp-launch-spec.ts +++ b/src/main/cursor/cursor-acp-launch-spec.ts @@ -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') } diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-models.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-models.ts index 1f8021ffb60..7fb04a46cdb 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-models.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-models.ts @@ -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: diff --git a/src/main/native-chat/agent-model-catalog/agent-model-catalog-persistence.ts b/src/main/native-chat/agent-model-catalog/agent-model-catalog-persistence.ts index 457bda5a97e..924fe9e6451 100644 --- a/src/main/native-chat/agent-model-catalog/agent-model-catalog-persistence.ts +++ b/src/main/native-chat/agent-model-catalog/agent-model-catalog-persistence.ts @@ -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 } : {}), diff --git a/src/main/native-chat/agent-session-wire/options/structured-agent-session-options-read.ts b/src/main/native-chat/agent-session-wire/options/structured-agent-session-options-read.ts index eb233f33c8e..f7a3d28ec54 100644 --- a/src/main/native-chat/agent-session-wire/options/structured-agent-session-options-read.ts +++ b/src/main/native-chat/agent-session-wire/options/structured-agent-session-options-read.ts @@ -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 diff --git a/src/shared/agent-session/agent-session-wire.ts b/src/shared/agent-session/agent-session-wire.ts index b77ff8cd979..5c0035321f1 100644 --- a/src/shared/agent-session/agent-session-wire.ts +++ b/src/shared/agent-session/agent-session-wire.ts @@ -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. */ diff --git a/src/shared/agent-session/options/agent-session-model-efforts.ts b/src/shared/agent-session/options/agent-session-model-efforts.ts index cffdb07180b..9e679adf83d 100644 --- a/src/shared/agent-session/options/agent-session-model-efforts.ts +++ b/src/shared/agent-session/options/agent-session-model-efforts.ts @@ -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 } : {}) } }) diff --git a/src/shared/agent-session/options/structured-agent-session-discovered-model.ts b/src/shared/agent-session/options/structured-agent-session-discovered-model.ts index afb40cdfdfc..86ae58e01ad 100644 --- a/src/shared/agent-session/options/structured-agent-session-discovered-model.ts +++ b/src/shared/agent-session/options/structured-agent-session-discovered-model.ts @@ -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)] : []) ] } diff --git a/src/shared/agent-session/options/structured-agent-session-option-view.test.ts b/src/shared/agent-session/options/structured-agent-session-option-view.test.ts index b4555fdc952..fb1ab4caf7e 100644 --- a/src/shared/agent-session/options/structured-agent-session-option-view.test.ts +++ b/src/shared/agent-session/options/structured-agent-session-option-view.test.ts @@ -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', () => {