From b853bed5b8eaa275ba221c146fd6f5401c9d35fb Mon Sep 17 00:00:00 2001 From: Richer Archambault Date: Tue, 21 Apr 2026 09:01:46 -0400 Subject: [PATCH 1/7] feat(types): Add Composer 2 models and Kimi K2.5; remove Composer 1 Made-with: Cursor --- .../tests/unit/lib/model-resolver.test.ts | 6 +- libs/model-resolver/src/resolver.ts | 8 +-- libs/model-resolver/tests/resolver.test.ts | 19 +++++-- libs/types/src/cursor-models.ts | 56 +++++++++++++++---- libs/types/src/model-migration.ts | 19 ++++++- 5 files changed, 84 insertions(+), 24 deletions(-) diff --git a/apps/server/tests/unit/lib/model-resolver.test.ts b/apps/server/tests/unit/lib/model-resolver.test.ts index 65e3115df..e4d4a2781 100644 --- a/apps/server/tests/unit/lib/model-resolver.test.ts +++ b/apps/server/tests/unit/lib/model-resolver.test.ts @@ -89,14 +89,14 @@ describe('model-resolver.ts', () => { describe('Cursor models', () => { it('should pass through cursor-prefixed models unchanged', () => { - const result = resolveModelString('cursor-composer-1'); - expect(result).toBe('cursor-composer-1'); + const result = resolveModelString('cursor-composer-2'); + expect(result).toBe('cursor-composer-2'); expect(consoleSpy.log).toHaveBeenCalledWith(expect.stringContaining('Using Cursor model')); }); it('should add cursor- prefix to bare Cursor model IDs', () => { const result = resolveModelString('composer-1'); - expect(result).toBe('cursor-composer-1'); + expect(result).toBe('cursor-composer-2'); }); it('should handle cursor-auto model', () => { diff --git a/libs/model-resolver/src/resolver.ts b/libs/model-resolver/src/resolver.ts index 877fcafcf..b4793d941 100644 --- a/libs/model-resolver/src/resolver.ts +++ b/libs/model-resolver/src/resolver.ts @@ -10,7 +10,7 @@ * - Handles multiple model sources with priority * * With canonical model IDs: - * - Cursor: cursor-auto, cursor-composer-1, cursor-gpt-5.2 + * - Cursor: cursor-auto, cursor-composer-2, cursor-gpt-5.2 * - OpenCode: opencode-big-pickle, opencode-kimi-k2.5-free * - Copilot: copilot-gpt-5.1, copilot-claude-sonnet-4.5, copilot-gemini-3-pro-preview * - Gemini: gemini-2.5-flash, gemini-2.5-pro @@ -45,9 +45,9 @@ const OPENAI_O_SERIES_ALLOWED_MODELS = new Set(); * * Handles both canonical prefixed IDs and legacy aliases: * - Canonical: cursor-auto, cursor-gpt-5.2, opencode-big-pickle, claude-sonnet - * - Legacy: auto, composer-1, sonnet, opus + * - Legacy: auto, composer-1 (→ cursor-composer-2), sonnet, opus * - * @param modelKey - Model key (e.g., "claude-opus", "cursor-composer-1", "sonnet") + * @param modelKey - Model key (e.g., "claude-opus", "cursor-composer-2", "sonnet") * @param defaultModel - Fallback model if modelKey is undefined * @returns Full model string */ @@ -71,7 +71,7 @@ export function resolveModelString( console.log(`[ModelResolver] Migrated legacy ID: "${modelKey}" -> "${canonicalKey}"`); } - // Cursor model with explicit prefix (e.g., "cursor-auto", "cursor-composer-1") + // Cursor model with explicit prefix (e.g., "cursor-auto", "cursor-composer-2") // Pass through unchanged - provider will extract bare ID for CLI if (canonicalKey.startsWith(PROVIDER_PREFIXES.cursor)) { console.log(`[ModelResolver] Using Cursor model: ${canonicalKey}`); diff --git a/libs/model-resolver/tests/resolver.test.ts b/libs/model-resolver/tests/resolver.test.ts index 0a3fa0b44..7ebcd97b3 100644 --- a/libs/model-resolver/tests/resolver.test.ts +++ b/libs/model-resolver/tests/resolver.test.ts @@ -114,12 +114,21 @@ describe('model-resolver', () => { describe('with Cursor models', () => { it('should pass through cursor-prefixed model unchanged', () => { - const result = resolveModelString('cursor-composer-1'); + const result = resolveModelString('cursor-composer-2'); - expect(result).toBe('cursor-composer-1'); + expect(result).toBe('cursor-composer-2'); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Using Cursor model')); }); + it('should migrate retired cursor-composer-1 to cursor-composer-2', () => { + const result = resolveModelString('cursor-composer-1'); + + expect(result).toBe('cursor-composer-2'); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining('Migrated legacy ID: "cursor-composer-1" -> "cursor-composer-2"') + ); + }); + it('should handle cursor-auto model', () => { const result = resolveModelString('cursor-auto'); @@ -135,10 +144,10 @@ describe('model-resolver', () => { it('should add cursor- prefix to bare Cursor model IDs', () => { const result = resolveModelString('composer-1'); - expect(result).toBe('cursor-composer-1'); + expect(result).toBe('cursor-composer-2'); // Legacy bare IDs are migrated to canonical prefixed format expect(consoleLogSpy).toHaveBeenCalledWith( - expect.stringContaining('Migrated legacy ID: "composer-1" -> "cursor-composer-1"') + expect.stringContaining('Migrated legacy ID: "composer-1" -> "cursor-composer-2"') ); }); @@ -509,7 +518,7 @@ describe('model-resolver', () => { const entry: PhaseModelEntry = { model: 'composer-1', thinkingLevel: 'high' }; const result = resolvePhaseModel(entry); - expect(result.model).toBe('cursor-composer-1'); + expect(result.model).toBe('cursor-composer-2'); expect(result.thinkingLevel).toBe('high'); }); diff --git a/libs/types/src/cursor-models.ts b/libs/types/src/cursor-models.ts index a48a791f5..f0c0b2a83 100644 --- a/libs/types/src/cursor-models.ts +++ b/libs/types/src/cursor-models.ts @@ -7,7 +7,8 @@ */ export type CursorModelId = | 'cursor-auto' // Auto-select best model - | 'cursor-composer-1' // Cursor Composer agent model + | 'cursor-composer-2' // Cursor Composer 2 agent model + | 'cursor-composer-2-fast' // Cursor Composer 2 fast agent model | 'cursor-sonnet-4.6' // Claude Sonnet 4.6 | 'cursor-sonnet-4.6-thinking' // Claude Sonnet 4.6 with extended thinking | 'cursor-sonnet-4.5' // Claude Sonnet 4.5 @@ -29,13 +30,15 @@ export type CursorModelId = | 'cursor-gpt-5.2-codex-high' // GPT-5.2 Codex High via Cursor | 'cursor-gpt-5.2-codex-max' // GPT-5.2 Codex Max via Cursor | 'cursor-gpt-5.2-codex-max-high' // GPT-5.2 Codex Max High via Cursor - | 'cursor-grok'; // Grok + | 'cursor-grok' // Grok + | 'cursor-kimi-k2.5'; // Kimi K2.5 via Cursor /** * Legacy Cursor model IDs (without prefix) for migration support */ export type LegacyCursorModelId = | 'auto' + /** @deprecated Composer 1 removed; migrates to cursor-composer-2 */ | 'composer-1' | 'sonnet-4.6' | 'sonnet-4.6-thinking' @@ -72,12 +75,20 @@ export const CURSOR_MODEL_MAP: Record = { hasThinking: false, supportsVision: false, // Vision not yet supported by Cursor CLI }, - 'cursor-composer-1': { - id: 'cursor-composer-1', - label: 'Composer 1', - description: 'Cursor Composer agent model optimized for multi-file edits', - hasThinking: false, - supportsVision: false, + 'cursor-composer-2': { + id: 'cursor-composer-2', + label: 'Composer 2', + description: 'Cursor Composer 2 agent model optimized for thinking and writing code', + hasThinking: true, + supportsVision: true, + }, + 'cursor-composer-2-fast': { + id: 'cursor-composer-2-fast', + label: 'Composer 2 Fast', + description: + 'Cursor Composer 2 fast agent model optimized for thinking and writing code, faster', + hasThinking: true, + supportsVision: true, }, 'cursor-sonnet-4.6': { id: 'cursor-sonnet-4.6', @@ -233,6 +244,13 @@ export const CURSOR_MODEL_MAP: Record = { hasThinking: false, supportsVision: false, }, + 'cursor-kimi-k2.5': { + id: 'cursor-kimi-k2.5', + label: 'Kimi K2.5', + description: 'Kimi K2.5 via Cursor', + hasThinking: true, + supportsVision: true, + }, }; /** @@ -240,7 +258,7 @@ export const CURSOR_MODEL_MAP: Record = { */ export const LEGACY_CURSOR_MODEL_MAP: Record = { auto: 'cursor-auto', - 'composer-1': 'cursor-composer-1', + 'composer-1': 'cursor-composer-2', 'sonnet-4.6': 'cursor-sonnet-4.6', 'sonnet-4.6-thinking': 'cursor-sonnet-4.6-thinking', 'sonnet-4.5': 'cursor-sonnet-4.5', @@ -253,6 +271,13 @@ export const LEGACY_CURSOR_MODEL_MAP: Record grok: 'cursor-grok', }; +/** + * Retired Cursor canonical IDs (older releases) → current replacement + */ +export const RETIRED_CURSOR_MODEL_MAP = { + 'cursor-composer-1': 'cursor-composer-2', +} as const satisfies Record; + /** * Helper: Check if model has thinking capability */ @@ -446,6 +471,17 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [ }, ], }, + // Composer 2 group (thinking mode) + { + baseId: 'cursor-composer-2-group', + label: 'Composer 2', + description: 'Cursor Composer 2 agent model optimized for thinking and writing code', + variantType: 'thinking', + variants: [ + { id: 'cursor-composer-2', label: 'Standard', description: 'Standard responses' }, + { id: 'cursor-composer-2-fast', label: 'Fast', description: 'Faster responses' }, + ], + }, ]; /** @@ -454,11 +490,11 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [ */ export const STANDALONE_CURSOR_MODELS: CursorModelId[] = [ 'cursor-auto', - 'cursor-composer-1', 'cursor-opus-4.1', 'cursor-gemini-3-pro', 'cursor-gemini-3-flash', 'cursor-grok', + 'cursor-kimi-k2.5', ]; /** diff --git a/libs/types/src/model-migration.ts b/libs/types/src/model-migration.ts index b42833f77..701c9272a 100644 --- a/libs/types/src/model-migration.ts +++ b/libs/types/src/model-migration.ts @@ -6,7 +6,11 @@ */ import type { CursorModelId, LegacyCursorModelId } from './cursor-models.js'; -import { LEGACY_CURSOR_MODEL_MAP, CURSOR_MODEL_MAP } from './cursor-models.js'; +import { + LEGACY_CURSOR_MODEL_MAP, + CURSOR_MODEL_MAP, + RETIRED_CURSOR_MODEL_MAP, +} from './cursor-models.js'; import type { OpencodeModelId, LegacyOpencodeModelId } from './opencode-models.js'; import { LEGACY_OPENCODE_MODEL_MAP, @@ -55,6 +59,12 @@ export function migrateModelId(legacyId: string | undefined | null): string { return legacyId as string; } + const retiredReplacement = + RETIRED_CURSOR_MODEL_MAP[legacyId as keyof typeof RETIRED_CURSOR_MODEL_MAP]; + if (retiredReplacement) { + return retiredReplacement; + } + // Already has cursor- prefix and is in the map - it's canonical if (legacyId.startsWith('cursor-') && legacyId in CURSOR_MODEL_MAP) { return legacyId; @@ -106,6 +116,11 @@ export function migrateCursorModelIds(ids: string[]): CursorModelId[] { } return ids.map((id) => { + const retired = RETIRED_CURSOR_MODEL_MAP[id as keyof typeof RETIRED_CURSOR_MODEL_MAP]; + if (retired) { + return retired; + } + // Already canonical if (id.startsWith('cursor-') && id in CURSOR_MODEL_MAP) { return id as CursorModelId; @@ -200,7 +215,7 @@ export function migratePhaseModelEntry( * * When calling provider CLIs, we need to strip the provider prefix: * - 'cursor-auto' -> 'auto' (for Cursor CLI) - * - 'cursor-composer-1' -> 'composer-1' (for Cursor CLI) + * - 'cursor-composer-2' -> 'composer-2' (for Cursor CLI) * - 'opencode-big-pickle' -> 'big-pickle' (for OpenCode CLI) * * Note: GPT models via Cursor keep the gpt- part: 'cursor-gpt-5.2' -> 'gpt-5.2' From 534f3def558838e409ee4560e55febccc3eb3da5 Mon Sep 17 00:00:00 2001 From: Richer Archambault Date: Tue, 21 Apr 2026 09:01:46 -0400 Subject: [PATCH 2/7] fix(server): Align cursor-agent CLI with --print and positional prompt Made-with: Cursor --- apps/server/src/providers/cursor-provider.ts | 27 ++++------ .../unit/providers/cursor-provider.test.ts | 51 +++++++++++++++++++ 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/apps/server/src/providers/cursor-provider.ts b/apps/server/src/providers/cursor-provider.ts index 3903ea648..73ca3409f 100644 --- a/apps/server/src/providers/cursor-provider.ts +++ b/apps/server/src/providers/cursor-provider.ts @@ -7,7 +7,10 @@ * - Session ID tracking * - Versions directory detection * - * Spawns the cursor-agent CLI with --output-format stream-json for streaming responses. + * CLI shape differs from OpenAI Codex (`codex exec … --json` + stdin + `-`): + * Cursor Agent requires `--print` for non-interactive use; `--output-format` and + * `--stream-partial-output` only apply with `--print` (see Cursor CLI parameters). + * The user prompt is passed as the final positional argument, not via stdin. */ import { execSync } from 'child_process'; @@ -400,8 +403,7 @@ export class CursorProvider extends CliProvider { } /** - * Extract prompt text from ExecuteOptions - * Used to pass prompt via stdin instead of CLI args to avoid shell escaping issues + * Extract prompt text from ExecuteOptions for the cursor-agent positional prompt argument. */ private extractPromptText(options: ExecuteOptions): string { if (typeof options.prompt === 'string') { @@ -420,9 +422,8 @@ export class CursorProvider extends CliProvider { // Model is already bare (no prefix) - validated by executeQuery const model = options.model || 'auto'; - // Build CLI arguments for cursor-agent - // NOTE: Prompt is NOT included here - it's passed via stdin to avoid - // shell escaping issues when content contains $(), backticks, etc. + // Build CLI arguments for cursor-agent. Prompt is the final positional argument + // (spawn passes argv directly; no shell interpolation on typical native/WSL paths). const cliArgs: string[] = []; // If using Cursor IDE (cliPath is 'cursor' not 'cursor-agent'), add 'agent' subcommand @@ -431,10 +432,10 @@ export class CursorProvider extends CliProvider { } cliArgs.push( - '-p', // Print mode (non-interactive) + '--print', // Required: --output-format / --stream-partial-output only work with --print '--output-format', 'stream-json', - '--stream-partial-output' // Real-time streaming + '--stream-partial-output' ); // In read-only mode, use --mode ask for Q&A style (no tools) @@ -455,8 +456,7 @@ export class CursorProvider extends CliProvider { cliArgs.push('--resume', options.sdkSessionId); } - // Use '-' to indicate reading prompt from stdin - cliArgs.push('-'); + cliArgs.push(this.extractPromptText(options)); return cliArgs; } @@ -870,16 +870,9 @@ export class CursorProvider extends CliProvider { // Embed system prompt into user prompt (Cursor CLI doesn't support separate system messages) const effectiveOptions = this.embedSystemPromptIntoPrompt(options); - // Extract prompt text to pass via stdin (avoids shell escaping issues) - const promptText = this.extractPromptText(effectiveOptions); - const cliArgs = this.buildCliArgs(effectiveOptions); const subprocessOptions = this.buildSubprocessOptions(options, cliArgs); - // Pass prompt via stdin to avoid shell interpretation of special characters - // like $(), backticks, etc. that may appear in file content - subprocessOptions.stdinData = promptText; - let sessionId: string | undefined; // Dedup state for Cursor-specific text block handling diff --git a/apps/server/tests/unit/providers/cursor-provider.test.ts b/apps/server/tests/unit/providers/cursor-provider.test.ts index 846ac69be..2dbd59b35 100644 --- a/apps/server/tests/unit/providers/cursor-provider.test.ts +++ b/apps/server/tests/unit/providers/cursor-provider.test.ts @@ -36,6 +36,57 @@ describe('cursor-provider.ts', () => { expect(args).not.toContain('--resume'); }); + + it('passes the prompt as the final positional argument', () => { + const provider = Object.create(CursorProvider.prototype) as CursorProvider & { + cliPath?: string; + }; + provider.cliPath = '/usr/local/bin/cursor-agent'; + + const prompt = 'Implement the feature'; + const args = provider.buildCliArgs({ + prompt, + model: 'gpt-5', + cwd: '/tmp/project', + }); + + expect(args[args.length - 1]).toBe(prompt); + expect(args).not.toContain('-'); + }); + + it('joins array prompt text blocks with newlines as the final positional', () => { + const provider = Object.create(CursorProvider.prototype) as CursorProvider & { + cliPath?: string; + }; + provider.cliPath = '/usr/local/bin/cursor-agent'; + + const args = provider.buildCliArgs({ + prompt: [ + { type: 'text', text: 'First line' }, + { type: 'text', text: 'Second line' }, + ], + model: 'gpt-5', + cwd: '/tmp/project', + }); + + expect(args[args.length - 1]).toBe('First line\nSecond line'); + }); + + it('preserves shell-like characters in the positional prompt (argv, not shell)', () => { + const provider = Object.create(CursorProvider.prototype) as CursorProvider & { + cliPath?: string; + }; + provider.cliPath = '/usr/local/bin/cursor-agent'; + + const prompt = 'Run `echo $HOME` and $(date)'; + const args = provider.buildCliArgs({ + prompt, + model: 'gpt-5', + cwd: '/tmp/project', + }); + + expect(args[args.length - 1]).toBe(prompt); + }); }); describe('normalizeEvent - result error handling', () => { From 8ffba7fea7890c035c3b54f4f4b4a5a9186f87bc Mon Sep 17 00:00:00 2001 From: Richer Archambault Date: Wed, 22 Apr 2026 07:49:00 -0400 Subject: [PATCH 3/7] fix(types): Align Cursor metadata, speed variant, and bare legacy IDs Made-with: Cursor --- libs/types/src/cursor-models.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/libs/types/src/cursor-models.ts b/libs/types/src/cursor-models.ts index f0c0b2a83..4aa093806 100644 --- a/libs/types/src/cursor-models.ts +++ b/libs/types/src/cursor-models.ts @@ -40,6 +40,9 @@ export type LegacyCursorModelId = | 'auto' /** @deprecated Composer 1 removed; migrates to cursor-composer-2 */ | 'composer-1' + | 'composer-2' + | 'composer-2-fast' + | 'kimi-k2.5' | 'sonnet-4.6' | 'sonnet-4.6-thinking' | 'sonnet-4.5' @@ -80,7 +83,7 @@ export const CURSOR_MODEL_MAP: Record = { label: 'Composer 2', description: 'Cursor Composer 2 agent model optimized for thinking and writing code', hasThinking: true, - supportsVision: true, + supportsVision: false, // Cursor CLI does not pass images; matches other Cursor models }, 'cursor-composer-2-fast': { id: 'cursor-composer-2-fast', @@ -88,7 +91,7 @@ export const CURSOR_MODEL_MAP: Record = { description: 'Cursor Composer 2 fast agent model optimized for thinking and writing code, faster', hasThinking: true, - supportsVision: true, + supportsVision: false, // Cursor CLI does not pass images; matches other Cursor models }, 'cursor-sonnet-4.6': { id: 'cursor-sonnet-4.6', @@ -249,7 +252,7 @@ export const CURSOR_MODEL_MAP: Record = { label: 'Kimi K2.5', description: 'Kimi K2.5 via Cursor', hasThinking: true, - supportsVision: true, + supportsVision: false, // Cursor CLI does not pass images; matches other Cursor models }, }; @@ -259,6 +262,9 @@ export const CURSOR_MODEL_MAP: Record = { export const LEGACY_CURSOR_MODEL_MAP: Record = { auto: 'cursor-auto', 'composer-1': 'cursor-composer-2', + 'composer-2': 'cursor-composer-2', + 'composer-2-fast': 'cursor-composer-2-fast', + 'kimi-k2.5': 'cursor-kimi-k2.5', 'sonnet-4.6': 'cursor-sonnet-4.6', 'sonnet-4.6-thinking': 'cursor-sonnet-4.6-thinking', 'sonnet-4.5': 'cursor-sonnet-4.5', @@ -307,7 +313,7 @@ export function getAllCursorModelIds(): CursorModelId[] { /** * Type of variant options available for grouped models */ -export type VariantType = 'compute' | 'thinking' | 'capacity'; +export type VariantType = 'compute' | 'thinking' | 'capacity' | 'speed'; /** * A single variant option within a grouped model @@ -471,12 +477,12 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [ }, ], }, - // Composer 2 group (thinking mode) + // Composer 2 group (Standard vs faster responses) { baseId: 'cursor-composer-2-group', label: 'Composer 2', description: 'Cursor Composer 2 agent model optimized for thinking and writing code', - variantType: 'thinking', + variantType: 'speed', variants: [ { id: 'cursor-composer-2', label: 'Standard', description: 'Standard responses' }, { id: 'cursor-composer-2-fast', label: 'Fast', description: 'Faster responses' }, From f8e1b87f80fd4d3e6be7232bcd7d29beae989bb7 Mon Sep 17 00:00:00 2001 From: Richer Archambault Date: Wed, 22 Apr 2026 07:49:03 -0400 Subject: [PATCH 4/7] fix(types): Deduplicate migrateCursorModelIds after migration Made-with: Cursor --- libs/types/src/model-migration.ts | 50 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/libs/types/src/model-migration.ts b/libs/types/src/model-migration.ts index 701c9272a..91e70bd39 100644 --- a/libs/types/src/model-migration.ts +++ b/libs/types/src/model-migration.ts @@ -115,30 +115,32 @@ export function migrateCursorModelIds(ids: string[]): CursorModelId[] { return []; } - return ids.map((id) => { - const retired = RETIRED_CURSOR_MODEL_MAP[id as keyof typeof RETIRED_CURSOR_MODEL_MAP]; - if (retired) { - return retired; - } - - // Already canonical - if (id.startsWith('cursor-') && id in CURSOR_MODEL_MAP) { - return id as CursorModelId; - } - - // Legacy ID - if (isLegacyCursorModelId(id)) { - return LEGACY_CURSOR_MODEL_MAP[id]; - } - - // Unknown - assume it might be a valid cursor model with prefix - if (id.startsWith('cursor-')) { - return id as CursorModelId; - } - - // Add prefix if not present - return `cursor-${id}` as CursorModelId; - }); + return ids + .map((id) => { + const retired = RETIRED_CURSOR_MODEL_MAP[id as keyof typeof RETIRED_CURSOR_MODEL_MAP]; + if (retired) { + return retired; + } + + // Already canonical + if (id.startsWith('cursor-') && id in CURSOR_MODEL_MAP) { + return id as CursorModelId; + } + + // Legacy ID + if (isLegacyCursorModelId(id)) { + return LEGACY_CURSOR_MODEL_MAP[id]; + } + + // Unknown - assume it might be a valid cursor model with prefix + if (id.startsWith('cursor-')) { + return id as CursorModelId; + } + + // Add prefix if not present + return `cursor-${id}` as CursorModelId; + }) + .filter((id, index, self) => self.indexOf(id) === index); } /** From 31884dbb3c68361fcd466765fad693a9bb99bc30 Mon Sep 17 00:00:00 2001 From: Richer Archambault Date: Wed, 22 Apr 2026 07:49:04 -0400 Subject: [PATCH 5/7] fix(ui): Label Cursor Composer 2 group as speed variants Made-with: Cursor --- .../settings-view/model-defaults/phase-model-selector.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx index f59f66655..9ddc75e97 100644 --- a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx +++ b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx @@ -2000,7 +2000,9 @@ export function PhaseModelSelector({ ? 'Compute Level' : group.variantType === 'thinking' ? 'Reasoning Mode' - : 'Capacity Options'; + : group.variantType === 'speed' + ? 'Speed' + : 'Capacity Options'; // On mobile, render inline expansion instead of nested popover if (isMobile) { From 0ea10260651aa71d8d285aee612b8796c4765697 Mon Sep 17 00:00:00 2001 From: Richer Archambault Date: Wed, 22 Apr 2026 07:49:04 -0400 Subject: [PATCH 6/7] fix(server): Use stdin for Cursor prompt when Windows spawns with shell Made-with: Cursor --- apps/server/src/providers/cursor-provider.ts | 39 +++++++++++++-- .../unit/providers/cursor-provider.test.ts | 47 +++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/apps/server/src/providers/cursor-provider.ts b/apps/server/src/providers/cursor-provider.ts index 73ca3409f..586f2bd7b 100644 --- a/apps/server/src/providers/cursor-provider.ts +++ b/apps/server/src/providers/cursor-provider.ts @@ -10,7 +10,10 @@ * CLI shape differs from OpenAI Codex (`codex exec … --json` + stdin + `-`): * Cursor Agent requires `--print` for non-interactive use; `--output-format` and * `--stream-partial-output` only apply with `--print` (see Cursor CLI parameters). - * The user prompt is passed as the final positional argument, not via stdin. + * On most platforms the user prompt is the final positional argument. On Windows + * when the subprocess runs with `shell: true` (see platform `spawnJSONLProcess`, + * e.g. `.cmd` shims or `npx`), the prompt is sent via stdin with `-` as the final + * argv element to avoid cmd.exe metacharacter interpretation and command-line length limits. */ import { execSync } from 'child_process'; @@ -45,7 +48,7 @@ import { CURSOR_MODEL_MAP, } from '@automaker/types'; import { createLogger, isAbortError } from '@automaker/utils'; -import { spawnJSONLProcess, execInWsl } from '@automaker/platform'; +import { spawnJSONLProcess, execInWsl, type SubprocessOptions } from '@automaker/platform'; // Create logger for this module const logger = createLogger('CursorProvider'); @@ -402,6 +405,17 @@ export class CursorProvider extends CliProvider { }; } + /** + * True when `spawnJSONLProcess` will use `shell: true` on Windows (see platform + * subprocess: `.cmd`, `npx`, `npm`). In that case the prompt must not be a raw argv tail. + */ + private useStdinForPrompt(): boolean { + if (process.platform !== 'win32') return false; + if (this.detectedStrategy === 'npx') return true; + if (!this.cliPath) return false; + return this.cliPath.toLowerCase().endsWith('.cmd'); + } + /** * Extract prompt text from ExecuteOptions for the cursor-agent positional prompt argument. */ @@ -456,11 +470,30 @@ export class CursorProvider extends CliProvider { cliArgs.push('--resume', options.sdkSessionId); } - cliArgs.push(this.extractPromptText(options)); + if (this.useStdinForPrompt()) { + cliArgs.push('-'); + } else { + cliArgs.push(this.extractPromptText(options)); + } return cliArgs; } + /** + * Pass prompt on stdin when Windows spawns with a shell; otherwise same as base. + */ + protected buildSubprocessOptions(options: ExecuteOptions, cliArgs: string[]): SubprocessOptions { + const subprocessOptions = super.buildSubprocessOptions(options, cliArgs); + if (!this.useStdinForPrompt()) { + return subprocessOptions; + } + const effectiveOptions = this.embedSystemPromptIntoPrompt(options); + return { + ...subprocessOptions, + stdinData: this.extractPromptText(effectiveOptions), + }; + } + /** * Convert Cursor event to AutoMaker ProviderMessage format * Made public as required by CliProvider abstract method diff --git a/apps/server/tests/unit/providers/cursor-provider.test.ts b/apps/server/tests/unit/providers/cursor-provider.test.ts index 2dbd59b35..d0503f90e 100644 --- a/apps/server/tests/unit/providers/cursor-provider.test.ts +++ b/apps/server/tests/unit/providers/cursor-provider.test.ts @@ -87,6 +87,53 @@ describe('cursor-provider.ts', () => { expect(args[args.length - 1]).toBe(prompt); }); + + it('uses stdin placeholder as final arg on Windows when npx strategy', () => { + const origPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'win32' }); + try { + const provider = Object.create(CursorProvider.prototype) as CursorProvider & { + cliPath?: string; + detectedStrategy?: string; + }; + provider.cliPath = 'C:\\npx'; + provider.detectedStrategy = 'npx'; + + const prompt = 'Large or special prompt'; + const args = provider.buildCliArgs({ + prompt, + model: 'gpt-5', + cwd: '/tmp/project', + }); + + expect(args[args.length - 1]).toBe('-'); + } finally { + Object.defineProperty(process, 'platform', { value: origPlatform }); + } + }); + + it('uses stdin placeholder as final arg on Windows when CLI is a .cmd shim', () => { + const origPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'win32' }); + try { + const provider = Object.create(CursorProvider.prototype) as CursorProvider & { + cliPath?: string; + detectedStrategy?: string; + }; + provider.cliPath = 'C:\\Users\\u\\AppData\\Roaming\\npm\\cursor-agent.cmd'; + provider.detectedStrategy = 'native'; + + const args = provider.buildCliArgs({ + prompt: 'x', + model: 'gpt-5', + cwd: '/tmp/project', + }); + + expect(args[args.length - 1]).toBe('-'); + } finally { + Object.defineProperty(process, 'platform', { value: origPlatform }); + } + }); }); describe('normalizeEvent - result error handling', () => { From eb88c6c12271d13d6c75935a6801236b0bbd5e94 Mon Sep 17 00:00:00 2001 From: Richer Archambault Date: Wed, 22 Apr 2026 07:49:05 -0400 Subject: [PATCH 7/7] test(model-resolver): Cover bare Cursor IDs for Composer 2 and Kimi Made-with: Cursor --- libs/model-resolver/tests/resolver.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/libs/model-resolver/tests/resolver.test.ts b/libs/model-resolver/tests/resolver.test.ts index 7ebcd97b3..e86f68469 100644 --- a/libs/model-resolver/tests/resolver.test.ts +++ b/libs/model-resolver/tests/resolver.test.ts @@ -151,6 +151,17 @@ describe('model-resolver', () => { ); }); + it.each([ + ['composer-2', 'cursor-composer-2'], + ['composer-2-fast', 'cursor-composer-2-fast'], + ['kimi-k2.5', 'cursor-kimi-k2.5'], + ] as const)('migrates bare Cursor id %s -> %s', (input, expected) => { + expect(resolveModelString(input)).toBe(expected); + expect(consoleLogSpy).toHaveBeenCalledWith( + expect.stringContaining(`Migrated legacy ID: "${input}" -> "${expected}"`) + ); + }); + it('should add cursor- prefix to auto model', () => { const result = resolveModelString('auto');