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', () => {