Merge remote-tracking branch 'origin/v0.13.0rc' into feature/claude-code-max-glm-api-keys

This commit is contained in:
Stefan de Vogelaere
2026-01-19 14:42:15 +01:00
200 changed files with 6671 additions and 1311 deletions

View File

@@ -2,18 +2,19 @@
* Cursor CLI Model IDs
* Reference: https://cursor.com/docs
*
* IMPORTANT: GPT models use 'cursor-' prefix to distinguish from Codex CLI models
* All Cursor model IDs use 'cursor-' prefix for consistent provider routing.
* This prevents naming collisions (e.g., cursor-gpt-5.2-codex vs codex-gpt-5.2-codex).
*/
export type CursorModelId =
| 'auto' // Auto-select best model
| 'composer-1' // Cursor Composer agent model
| 'sonnet-4.5' // Claude Sonnet 4.5
| 'sonnet-4.5-thinking' // Claude Sonnet 4.5 with extended thinking
| 'opus-4.5' // Claude Opus 4.5
| 'opus-4.5-thinking' // Claude Opus 4.5 with extended thinking
| 'opus-4.1' // Claude Opus 4.1
| 'gemini-3-pro' // Gemini 3 Pro
| 'gemini-3-flash' // Gemini 3 Flash
| 'cursor-auto' // Auto-select best model
| 'cursor-composer-1' // Cursor Composer agent model
| 'cursor-sonnet-4.5' // Claude Sonnet 4.5
| 'cursor-sonnet-4.5-thinking' // Claude Sonnet 4.5 with extended thinking
| 'cursor-opus-4.5' // Claude Opus 4.5
| 'cursor-opus-4.5-thinking' // Claude Opus 4.5 with extended thinking
| 'cursor-opus-4.1' // Claude Opus 4.1
| 'cursor-gemini-3-pro' // Gemini 3 Pro
| 'cursor-gemini-3-flash' // Gemini 3 Flash
| 'cursor-gpt-5.2' // GPT-5.2 via Cursor
| 'cursor-gpt-5.1' // GPT-5.1 via Cursor
| 'cursor-gpt-5.2-high' // GPT-5.2 High via Cursor
@@ -26,7 +27,22 @@ 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
| 'grok'; // Grok
| 'cursor-grok'; // Grok
/**
* Legacy Cursor model IDs (without prefix) for migration support
*/
export type LegacyCursorModelId =
| 'auto'
| 'composer-1'
| 'sonnet-4.5'
| 'sonnet-4.5-thinking'
| 'opus-4.5'
| 'opus-4.5-thinking'
| 'opus-4.1'
| 'gemini-3-pro'
| 'gemini-3-flash'
| 'grok';
/**
* Cursor model metadata
@@ -42,66 +58,67 @@ export interface CursorModelConfig {
/**
* Complete model map for Cursor CLI
* All keys use 'cursor-' prefix for consistent provider routing.
*/
export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
auto: {
id: 'auto',
'cursor-auto': {
id: 'cursor-auto',
label: 'Auto (Recommended)',
description: 'Automatically selects the best model for each task',
hasThinking: false,
supportsVision: false, // Vision not yet supported by Cursor CLI
},
'composer-1': {
id: 'composer-1',
'cursor-composer-1': {
id: 'cursor-composer-1',
label: 'Composer 1',
description: 'Cursor Composer agent model optimized for multi-file edits',
hasThinking: false,
supportsVision: false,
},
'sonnet-4.5': {
id: 'sonnet-4.5',
'cursor-sonnet-4.5': {
id: 'cursor-sonnet-4.5',
label: 'Claude Sonnet 4.5',
description: 'Anthropic Claude Sonnet 4.5 via Cursor',
hasThinking: false,
supportsVision: false, // Model supports vision but Cursor CLI doesn't pass images
},
'sonnet-4.5-thinking': {
id: 'sonnet-4.5-thinking',
'cursor-sonnet-4.5-thinking': {
id: 'cursor-sonnet-4.5-thinking',
label: 'Claude Sonnet 4.5 (Thinking)',
description: 'Claude Sonnet 4.5 with extended thinking enabled',
hasThinking: true,
supportsVision: false,
},
'opus-4.5': {
id: 'opus-4.5',
'cursor-opus-4.5': {
id: 'cursor-opus-4.5',
label: 'Claude Opus 4.5',
description: 'Anthropic Claude Opus 4.5 via Cursor',
hasThinking: false,
supportsVision: false,
},
'opus-4.5-thinking': {
id: 'opus-4.5-thinking',
'cursor-opus-4.5-thinking': {
id: 'cursor-opus-4.5-thinking',
label: 'Claude Opus 4.5 (Thinking)',
description: 'Claude Opus 4.5 with extended thinking enabled',
hasThinking: true,
supportsVision: false,
},
'opus-4.1': {
id: 'opus-4.1',
'cursor-opus-4.1': {
id: 'cursor-opus-4.1',
label: 'Claude Opus 4.1',
description: 'Anthropic Claude Opus 4.1 via Cursor',
hasThinking: false,
supportsVision: false,
},
'gemini-3-pro': {
id: 'gemini-3-pro',
'cursor-gemini-3-pro': {
id: 'cursor-gemini-3-pro',
label: 'Gemini 3 Pro',
description: 'Google Gemini 3 Pro via Cursor',
hasThinking: false,
supportsVision: false,
},
'gemini-3-flash': {
id: 'gemini-3-flash',
'cursor-gemini-3-flash': {
id: 'cursor-gemini-3-flash',
label: 'Gemini 3 Flash',
description: 'Google Gemini 3 Flash (faster)',
hasThinking: false,
@@ -191,8 +208,8 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
hasThinking: false,
supportsVision: false,
},
grok: {
id: 'grok',
'cursor-grok': {
id: 'cursor-grok',
label: 'Grok',
description: 'xAI Grok via Cursor',
hasThinking: false,
@@ -200,6 +217,22 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
},
};
/**
* Map from legacy model IDs to canonical prefixed IDs
*/
export const LEGACY_CURSOR_MODEL_MAP: Record<LegacyCursorModelId, CursorModelId> = {
auto: 'cursor-auto',
'composer-1': 'cursor-composer-1',
'sonnet-4.5': 'cursor-sonnet-4.5',
'sonnet-4.5-thinking': 'cursor-sonnet-4.5-thinking',
'opus-4.5': 'cursor-opus-4.5',
'opus-4.5-thinking': 'cursor-opus-4.5-thinking',
'opus-4.1': 'cursor-opus-4.1',
'gemini-3-pro': 'cursor-gemini-3-pro',
'gemini-3-flash': 'cursor-gemini-3-flash',
grok: 'cursor-grok',
};
/**
* Helper: Check if model has thinking capability
*/
@@ -254,6 +287,7 @@ export interface GroupedModel {
/**
* Configuration for grouping Cursor models with variants
* All variant IDs use 'cursor-' prefix for consistent provider routing.
*/
export const CURSOR_MODEL_GROUPS: GroupedModel[] = [
// GPT-5.2 group (compute levels)
@@ -346,14 +380,14 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [
},
// Sonnet 4.5 group (thinking mode)
{
baseId: 'sonnet-4.5-group',
baseId: 'cursor-sonnet-4.5-group',
label: 'Claude Sonnet 4.5',
description: 'Anthropic Claude Sonnet 4.5 via Cursor',
variantType: 'thinking',
variants: [
{ id: 'sonnet-4.5', label: 'Standard', description: 'Fast responses' },
{ id: 'cursor-sonnet-4.5', label: 'Standard', description: 'Fast responses' },
{
id: 'sonnet-4.5-thinking',
id: 'cursor-sonnet-4.5-thinking',
label: 'Thinking',
description: 'Extended reasoning',
badge: 'Reasoning',
@@ -362,14 +396,14 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [
},
// Opus 4.5 group (thinking mode)
{
baseId: 'opus-4.5-group',
baseId: 'cursor-opus-4.5-group',
label: 'Claude Opus 4.5',
description: 'Anthropic Claude Opus 4.5 via Cursor',
variantType: 'thinking',
variants: [
{ id: 'opus-4.5', label: 'Standard', description: 'Fast responses' },
{ id: 'cursor-opus-4.5', label: 'Standard', description: 'Fast responses' },
{
id: 'opus-4.5-thinking',
id: 'cursor-opus-4.5-thinking',
label: 'Thinking',
description: 'Extended reasoning',
badge: 'Reasoning',
@@ -380,14 +414,15 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [
/**
* Cursor models that are not part of any group (standalone)
* All IDs use 'cursor-' prefix for consistent provider routing.
*/
export const STANDALONE_CURSOR_MODELS: CursorModelId[] = [
'auto',
'composer-1',
'opus-4.1',
'gemini-3-pro',
'gemini-3-flash',
'grok',
'cursor-auto',
'cursor-composer-1',
'cursor-opus-4.1',
'cursor-gemini-3-pro',
'cursor-gemini-3-flash',
'cursor-grok',
];
/**

View File

@@ -77,12 +77,15 @@ export type { ImageData, ImageContentBlock } from './image.js';
// Model types and constants
export {
CLAUDE_MODEL_MAP,
CLAUDE_CANONICAL_MAP,
LEGACY_CLAUDE_ALIAS_MAP,
CODEX_MODEL_MAP,
CODEX_MODEL_IDS,
REASONING_CAPABLE_MODELS,
supportsReasoningEffort,
getAllCodexModelIds,
DEFAULT_MODELS,
type ClaudeCanonicalId,
type ModelAlias,
type CodexModelId,
type AgentModel,
@@ -242,6 +245,18 @@ export {
validateBareModelId,
} from './provider-utils.js';
// Model migration utilities
export {
isLegacyCursorModelId,
isLegacyOpencodeModelId,
isLegacyClaudeAlias,
migrateModelId,
migrateCursorModelIds,
migrateOpencodeModelIds,
migratePhaseModelEntry,
getBareModelIdForCli,
} from './model-migration.js';
// Pipeline types
export type {
PipelineStep,
@@ -297,3 +312,10 @@ export type {
EventReplayHookResult,
} from './event-history.js';
export { EVENT_HISTORY_VERSION, DEFAULT_EVENT_HISTORY_INDEX } from './event-history.js';
// Worktree and PR types
export type { PRState, WorktreePRInfo } from './worktree.js';
export { PR_STATES, validatePRState } from './worktree.js';
// Terminal types
export type { TerminalInfo } from './terminal.js';

View File

@@ -0,0 +1,218 @@
/**
* Model ID Migration Utilities
*
* Provides functions to migrate legacy model IDs to the canonical prefixed format.
* This ensures backward compatibility when loading settings from older versions.
*/
import type { CursorModelId, LegacyCursorModelId } from './cursor-models.js';
import { LEGACY_CURSOR_MODEL_MAP, CURSOR_MODEL_MAP } from './cursor-models.js';
import type { OpencodeModelId, LegacyOpencodeModelId } from './opencode-models.js';
import { LEGACY_OPENCODE_MODEL_MAP, OPENCODE_MODEL_CONFIG_MAP } from './opencode-models.js';
import type { ClaudeCanonicalId } from './model.js';
import { LEGACY_CLAUDE_ALIAS_MAP, CLAUDE_CANONICAL_MAP, CLAUDE_MODEL_MAP } from './model.js';
import type { PhaseModelEntry } from './settings.js';
/**
* Check if a string is a legacy Cursor model ID (without prefix)
*/
export function isLegacyCursorModelId(id: string): id is LegacyCursorModelId {
return id in LEGACY_CURSOR_MODEL_MAP;
}
/**
* Check if a string is a legacy OpenCode model ID (with slash format)
*/
export function isLegacyOpencodeModelId(id: string): id is LegacyOpencodeModelId {
return id in LEGACY_OPENCODE_MODEL_MAP;
}
/**
* Check if a string is a legacy Claude alias (short name without prefix)
*/
export function isLegacyClaudeAlias(id: string): boolean {
return id in LEGACY_CLAUDE_ALIAS_MAP;
}
/**
* Migrate a single model ID to canonical format
*
* Handles:
* - Legacy Cursor IDs (e.g., 'auto' -> 'cursor-auto')
* - Legacy OpenCode IDs (e.g., 'opencode/big-pickle' -> 'opencode-big-pickle')
* - Legacy Claude aliases (e.g., 'sonnet' -> 'claude-sonnet')
* - Already-canonical IDs are passed through unchanged
*
* @param legacyId - The model ID to migrate
* @returns The canonical model ID
*/
export function migrateModelId(legacyId: string | undefined | null): string {
if (!legacyId) {
return legacyId as string;
}
// Already has cursor- prefix and is in the map - it's canonical
if (legacyId.startsWith('cursor-') && legacyId in CURSOR_MODEL_MAP) {
return legacyId;
}
// Legacy Cursor model ID (without prefix)
if (isLegacyCursorModelId(legacyId)) {
return LEGACY_CURSOR_MODEL_MAP[legacyId];
}
// Already has opencode- prefix - it's canonical
if (legacyId.startsWith('opencode-') && legacyId in OPENCODE_MODEL_CONFIG_MAP) {
return legacyId;
}
// Legacy OpenCode model ID (with slash format)
if (isLegacyOpencodeModelId(legacyId)) {
return LEGACY_OPENCODE_MODEL_MAP[legacyId];
}
// Already has claude- prefix and is in canonical map
if (legacyId.startsWith('claude-') && legacyId in CLAUDE_CANONICAL_MAP) {
return legacyId;
}
// Legacy Claude alias (short name)
if (isLegacyClaudeAlias(legacyId)) {
return LEGACY_CLAUDE_ALIAS_MAP[legacyId];
}
// Unknown or already canonical - pass through
return legacyId;
}
/**
* Migrate an array of Cursor model IDs to canonical format
*
* @param ids - Array of legacy or canonical Cursor model IDs
* @returns Array of canonical Cursor model IDs
*/
export function migrateCursorModelIds(ids: string[]): CursorModelId[] {
if (!ids || !Array.isArray(ids)) {
return [];
}
return ids.map((id) => {
// 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;
});
}
/**
* Migrate an array of OpenCode model IDs to canonical format
*
* @param ids - Array of legacy or canonical OpenCode model IDs
* @returns Array of canonical OpenCode model IDs
*/
export function migrateOpencodeModelIds(ids: string[]): OpencodeModelId[] {
if (!ids || !Array.isArray(ids)) {
return [];
}
return ids.map((id) => {
// Already canonical (dash format)
if (id.startsWith('opencode-') && id in OPENCODE_MODEL_CONFIG_MAP) {
return id as OpencodeModelId;
}
// Legacy ID (slash format)
if (isLegacyOpencodeModelId(id)) {
return LEGACY_OPENCODE_MODEL_MAP[id];
}
// Convert slash to dash format for unknown models
if (id.startsWith('opencode/')) {
return id.replace('opencode/', 'opencode-') as OpencodeModelId;
}
// Add prefix if not present
if (!id.startsWith('opencode-')) {
return `opencode-${id}` as OpencodeModelId;
}
return id as OpencodeModelId;
});
}
/**
* Migrate a PhaseModelEntry to use canonical model IDs
*
* @param entry - The phase model entry to migrate
* @returns Migrated phase model entry with canonical model ID
*/
export function migratePhaseModelEntry(
entry: PhaseModelEntry | string | undefined | null
): PhaseModelEntry {
// Handle null/undefined
if (!entry) {
return { model: 'claude-sonnet' }; // Default
}
// Handle legacy string format
if (typeof entry === 'string') {
return { model: migrateModelId(entry) };
}
// Handle PhaseModelEntry object
return {
...entry,
model: migrateModelId(entry.model),
};
}
/**
* Get the bare model ID for CLI calls (strip provider prefix)
*
* 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)
* - 'opencode-big-pickle' -> 'big-pickle' (for OpenCode CLI)
*
* Note: GPT models via Cursor keep the gpt- part: 'cursor-gpt-5.2' -> 'gpt-5.2'
*
* @param modelId - The canonical model ID with provider prefix
* @returns The bare model ID for CLI usage
*/
export function getBareModelIdForCli(modelId: string): string {
if (!modelId) return modelId;
// Cursor models
if (modelId.startsWith('cursor-')) {
const bareId = modelId.slice(7); // Remove 'cursor-'
// For GPT models, keep the gpt- prefix since that's what the CLI expects
// e.g., 'cursor-gpt-5.2' -> 'gpt-5.2'
return bareId;
}
// OpenCode models - strip prefix
if (modelId.startsWith('opencode-')) {
return modelId.slice(9); // Remove 'opencode-'
}
// Codex models - strip prefix
if (modelId.startsWith('codex-')) {
return modelId.slice(6); // Remove 'codex-'
}
// Claude and other models - pass through
return modelId;
}

View File

@@ -4,12 +4,42 @@
import type { CursorModelId } from './cursor-models.js';
import type { OpencodeModelId } from './opencode-models.js';
/**
* Canonical Claude model IDs with provider prefix
* Used for internal storage and consistent provider routing.
*/
export type ClaudeCanonicalId = 'claude-haiku' | 'claude-sonnet' | 'claude-opus';
/**
* Canonical Claude model map - maps prefixed IDs to full model strings
* Use these IDs for internal storage and routing.
*/
export const CLAUDE_CANONICAL_MAP: Record<ClaudeCanonicalId, string> = {
'claude-haiku': 'claude-haiku-4-5-20251001',
'claude-sonnet': 'claude-sonnet-4-5-20250929',
'claude-opus': 'claude-opus-4-5-20251101',
} as const;
/**
* Legacy Claude model aliases (short names) for backward compatibility
* These map to the same full model strings as the canonical map.
* @deprecated Use CLAUDE_CANONICAL_MAP for new code
*/
export const CLAUDE_MODEL_MAP: Record<string, string> = {
haiku: 'claude-haiku-4-5-20251001',
sonnet: 'claude-sonnet-4-5-20250929',
opus: 'claude-opus-4-5-20251101',
} as const;
/**
* Map from legacy aliases to canonical IDs
*/
export const LEGACY_CLAUDE_ALIAS_MAP: Record<string, ClaudeCanonicalId> = {
haiku: 'claude-haiku',
sonnet: 'claude-sonnet',
opus: 'claude-opus',
} as const;
/**
* Codex/OpenAI model identifiers
* Based on OpenAI Codex CLI official models
@@ -62,10 +92,11 @@ export function getAllCodexModelIds(): CodexModelId[] {
/**
* Default models per provider
* Uses canonical prefixed IDs for consistent routing.
*/
export const DEFAULT_MODELS = {
claude: 'claude-opus-4-5-20251101',
cursor: 'auto', // Cursor's recommended default
cursor: 'cursor-auto', // Cursor's recommended default (with prefix)
codex: CODEX_MODEL_MAP.gpt52Codex, // GPT-5.2-Codex is the most advanced agentic coding model
} as const;

View File

@@ -1,9 +1,22 @@
/**
* OpenCode Model IDs
* Models available via OpenCode CLI (opencode models command)
*
* All OpenCode model IDs use 'opencode-' prefix for consistent provider routing.
* This prevents naming collisions and ensures clear provider attribution.
*/
export type OpencodeModelId =
// OpenCode Free Tier Models
| 'opencode-big-pickle'
| 'opencode-glm-4.7-free'
| 'opencode-gpt-5-nano'
| 'opencode-grok-code'
| 'opencode-minimax-m2.1-free';
/**
* Legacy OpenCode model IDs (with slash format) for migration support
*/
export type LegacyOpencodeModelId =
| 'opencode/big-pickle'
| 'opencode/glm-4.7-free'
| 'opencode/gpt-5-nano'
@@ -20,16 +33,27 @@ export type OpencodeProvider = 'opencode';
*/
export const OPENCODE_MODEL_MAP: Record<string, OpencodeModelId> = {
// OpenCode free tier aliases
'big-pickle': 'opencode/big-pickle',
pickle: 'opencode/big-pickle',
'glm-free': 'opencode/glm-4.7-free',
'gpt-nano': 'opencode/gpt-5-nano',
nano: 'opencode/gpt-5-nano',
'grok-code': 'opencode/grok-code',
grok: 'opencode/grok-code',
minimax: 'opencode/minimax-m2.1-free',
'big-pickle': 'opencode-big-pickle',
pickle: 'opencode-big-pickle',
'glm-free': 'opencode-glm-4.7-free',
'gpt-nano': 'opencode-gpt-5-nano',
nano: 'opencode-gpt-5-nano',
'grok-code': 'opencode-grok-code',
grok: 'opencode-grok-code',
minimax: 'opencode-minimax-m2.1-free',
} as const;
/**
* Map from legacy slash-format model IDs to canonical prefixed IDs
*/
export const LEGACY_OPENCODE_MODEL_MAP: Record<LegacyOpencodeModelId, OpencodeModelId> = {
'opencode/big-pickle': 'opencode-big-pickle',
'opencode/glm-4.7-free': 'opencode-glm-4.7-free',
'opencode/gpt-5-nano': 'opencode-gpt-5-nano',
'opencode/grok-code': 'opencode-grok-code',
'opencode/minimax-m2.1-free': 'opencode-minimax-m2.1-free',
};
/**
* OpenCode model metadata
*/
@@ -44,11 +68,12 @@ export interface OpencodeModelConfig {
/**
* Complete list of OpenCode model configurations
* All IDs use 'opencode-' prefix for consistent provider routing.
*/
export const OPENCODE_MODELS: OpencodeModelConfig[] = [
// OpenCode Free Tier Models
{
id: 'opencode/big-pickle',
id: 'opencode-big-pickle',
label: 'Big Pickle',
description: 'OpenCode free tier model - great for general coding',
supportsVision: false,
@@ -56,7 +81,7 @@ export const OPENCODE_MODELS: OpencodeModelConfig[] = [
tier: 'free',
},
{
id: 'opencode/glm-4.7-free',
id: 'opencode-glm-4.7-free',
label: 'GLM 4.7 Free',
description: 'OpenCode free tier GLM model',
supportsVision: false,
@@ -64,7 +89,7 @@ export const OPENCODE_MODELS: OpencodeModelConfig[] = [
tier: 'free',
},
{
id: 'opencode/gpt-5-nano',
id: 'opencode-gpt-5-nano',
label: 'GPT-5 Nano',
description: 'OpenCode free tier nano model - fast and lightweight',
supportsVision: false,
@@ -72,7 +97,7 @@ export const OPENCODE_MODELS: OpencodeModelConfig[] = [
tier: 'free',
},
{
id: 'opencode/grok-code',
id: 'opencode-grok-code',
label: 'Grok Code',
description: 'OpenCode free tier Grok model for coding',
supportsVision: false,
@@ -80,7 +105,7 @@ export const OPENCODE_MODELS: OpencodeModelConfig[] = [
tier: 'free',
},
{
id: 'opencode/minimax-m2.1-free',
id: 'opencode-minimax-m2.1-free',
label: 'MiniMax M2.1 Free',
description: 'OpenCode free tier MiniMax model',
supportsVision: false,
@@ -104,7 +129,7 @@ export const OPENCODE_MODEL_CONFIG_MAP: Record<OpencodeModelId, OpencodeModelCon
/**
* Default OpenCode model - OpenCode free tier
*/
export const DEFAULT_OPENCODE_MODEL: OpencodeModelId = 'opencode/big-pickle';
export const DEFAULT_OPENCODE_MODEL: OpencodeModelId = 'opencode-big-pickle';
/**
* Helper: Get display name for model

View File

@@ -7,9 +7,9 @@
*/
import type { ModelProvider } from './settings.js';
import { CURSOR_MODEL_MAP } from './cursor-models.js';
import { CURSOR_MODEL_MAP, LEGACY_CURSOR_MODEL_MAP } from './cursor-models.js';
import { CLAUDE_MODEL_MAP, CODEX_MODEL_MAP } from './model.js';
import { OPENCODE_MODEL_CONFIG_MAP } from './opencode-models.js';
import { OPENCODE_MODEL_CONFIG_MAP, LEGACY_OPENCODE_MODEL_MAP } from './opencode-models.js';
/** Provider prefix constants */
export const PROVIDER_PREFIXES = {
@@ -21,20 +21,23 @@ export const PROVIDER_PREFIXES = {
/**
* Check if a model string represents a Cursor model
*
* @param model - Model string to check (e.g., "cursor-composer-1" or "composer-1")
* @returns true if the model is a Cursor model (excluding Codex-specific models)
* With canonical model IDs, Cursor models always have 'cursor-' prefix.
* Legacy IDs without prefix are handled by migration utilities.
*
* @param model - Model string to check (e.g., "cursor-auto", "cursor-composer-1")
* @returns true if the model is a Cursor model
*/
export function isCursorModel(model: string | undefined | null): boolean {
if (!model || typeof model !== 'string') return false;
// Check for explicit cursor- prefix
// Canonical format: all Cursor models have cursor- prefix
if (model.startsWith(PROVIDER_PREFIXES.cursor)) {
return true;
}
// Check if it's a bare Cursor model ID (excluding Codex-specific models)
// Codex-specific models should always route to Codex provider, not Cursor
if (model in CURSOR_MODEL_MAP) {
// Legacy support: check if it's a known legacy bare ID
// This handles transition period before migration
if (model in LEGACY_CURSOR_MODEL_MAP) {
return true;
}
@@ -90,12 +93,14 @@ export function isCodexModel(model: string | undefined | null): boolean {
/**
* Check if a model string represents an OpenCode model
*
* With canonical model IDs, static OpenCode models use 'opencode-' prefix.
* Dynamic models from OpenCode CLI still use provider/model format.
*
* OpenCode models can be identified by:
* - Explicit 'opencode-' prefix (for routing in Automaker)
* - 'opencode/' prefix (OpenCode free tier models)
* - 'opencode-' prefix (canonical format for static models)
* - 'opencode/' prefix (legacy format, will be migrated)
* - 'amazon-bedrock/' prefix (AWS Bedrock models via OpenCode)
* - Full model ID from OPENCODE_MODEL_CONFIG_MAP
* - Dynamic models from OpenCode CLI with provider/model format (e.g., "github-copilot/gpt-4o", "google/gemini-2.5-pro")
* - Dynamic models with provider/model format (e.g., "github-copilot/gpt-4o")
*
* @param model - Model string to check
* @returns true if the model is an OpenCode model
@@ -103,19 +108,18 @@ export function isCodexModel(model: string | undefined | null): boolean {
export function isOpencodeModel(model: string | undefined | null): boolean {
if (!model || typeof model !== 'string') return false;
// Check for explicit opencode- prefix (Automaker routing prefix)
// Canonical format: opencode- prefix for static models
if (model.startsWith(PROVIDER_PREFIXES.opencode)) {
return true;
}
// Check if it's a known OpenCode model ID
// Check if it's a known OpenCode model ID (handles both formats during transition)
if (model in OPENCODE_MODEL_CONFIG_MAP) {
return true;
}
// Check for OpenCode native model prefixes
// - opencode/ = OpenCode free tier models
// - amazon-bedrock/ = AWS Bedrock models
// Legacy format: opencode/ prefix (will be migrated to opencode-)
// Also supports amazon-bedrock/ for AWS Bedrock models
if (model.startsWith('opencode/') || model.startsWith('amazon-bedrock/')) {
return true;
}
@@ -228,32 +232,47 @@ export function getBareModelId(model: string): string {
/**
* Normalize a model string to its canonical form
* - For Cursor: adds cursor- prefix if missing
* - For Codex: can add codex- prefix (but bare gpt-* is also valid)
* - For Claude: returns as-is
*
* With the new canonical format:
* - Cursor models: always have cursor- prefix
* - OpenCode models: always have opencode- prefix (static) or provider/model format (dynamic)
* - Claude models: can use legacy aliases or claude- prefix
* - Codex models: always have codex- prefix
*
* @param model - Model string to normalize
* @returns Normalized model string
*/
export function normalizeModelString(model: string | undefined | null): string {
if (!model || typeof model !== 'string') return 'sonnet'; // Default
if (!model || typeof model !== 'string') return 'claude-sonnet'; // Default to canonical
// If it's a Cursor model without prefix, add the prefix
if (model in CURSOR_MODEL_MAP && !model.startsWith(PROVIDER_PREFIXES.cursor)) {
return `${PROVIDER_PREFIXES.cursor}${model}`;
// Already has a canonical prefix - return as-is
if (
model.startsWith(PROVIDER_PREFIXES.cursor) ||
model.startsWith(PROVIDER_PREFIXES.codex) ||
model.startsWith(PROVIDER_PREFIXES.opencode) ||
model.startsWith('claude-')
) {
return model;
}
// For Codex, bare gpt-* and o-series models are valid canonical forms
// Check if it's in the CODEX_MODEL_MAP
if (model in CODEX_MODEL_MAP) {
// If it already starts with gpt- or o, it's canonical
if (model.startsWith('gpt-') || /^o\d/.test(model)) {
return model;
}
// Otherwise, it might need a prefix (though this is unlikely)
if (!model.startsWith(PROVIDER_PREFIXES.codex)) {
return `${PROVIDER_PREFIXES.codex}${model}`;
}
// Check if it's a legacy Cursor model ID
if (model in LEGACY_CURSOR_MODEL_MAP) {
return LEGACY_CURSOR_MODEL_MAP[model as keyof typeof LEGACY_CURSOR_MODEL_MAP];
}
// Check if it's a legacy OpenCode model ID
if (model in LEGACY_OPENCODE_MODEL_MAP) {
return LEGACY_OPENCODE_MODEL_MAP[model as keyof typeof LEGACY_OPENCODE_MODEL_MAP];
}
// Legacy Claude aliases
if (model in CLAUDE_MODEL_MAP) {
return `claude-${model}`;
}
// For Codex, bare gpt-* and o-series models need codex- prefix
if (model.startsWith('gpt-') || /^o\d/.test(model)) {
return `${PROVIDER_PREFIXES.codex}${model}`;
}
return model;

View File

@@ -541,6 +541,10 @@ export interface GlobalSettings {
/** Terminal font family (undefined = use default Menlo/Monaco) */
terminalFontFamily?: string;
// Terminal Configuration
/** How to open terminals from "Open in Terminal" worktree action */
openTerminalMode?: 'newTab' | 'split';
// UI State Preferences
/** Whether sidebar is currently open */
sidebarOpen: boolean;
@@ -669,6 +673,10 @@ export interface GlobalSettings {
/** Default editor command for "Open In" action (null = auto-detect: Cursor > VS Code > first available) */
defaultEditorCommand: string | null;
// Terminal Configuration
/** Default external terminal ID for "Open In Terminal" action (null = integrated terminal) */
defaultTerminalId: string | null;
// Prompt Customization
/** Custom prompts for Auto Mode, Agent Runner, Backlog Planning, and Enhancements */
promptCustomization?: PromptCustomization;
@@ -859,34 +867,42 @@ export interface ProjectSettings {
* Value: agent configuration
*/
customSubagents?: Record<string, import('./provider.js').AgentDefinition>;
// Auto Mode Configuration (per-project)
/** Whether auto mode is enabled for this project (backend-controlled loop) */
automodeEnabled?: boolean;
/** Maximum concurrent agents for this project (overrides global maxConcurrency) */
maxConcurrentAgents?: number;
}
/**
* Default values and constants
*/
/** Default phase model configuration - sensible defaults for each task type */
/** Default phase model configuration - sensible defaults for each task type
* Uses canonical prefixed model IDs for consistent routing.
*/
export const DEFAULT_PHASE_MODELS: PhaseModelConfig = {
// Quick tasks - use fast models for speed and cost
enhancementModel: { model: 'sonnet' },
fileDescriptionModel: { model: 'haiku' },
imageDescriptionModel: { model: 'haiku' },
enhancementModel: { model: 'claude-sonnet' },
fileDescriptionModel: { model: 'claude-haiku' },
imageDescriptionModel: { model: 'claude-haiku' },
// Validation - use smart models for accuracy
validationModel: { model: 'sonnet' },
validationModel: { model: 'claude-sonnet' },
// Generation - use powerful models for quality
specGenerationModel: { model: 'opus' },
featureGenerationModel: { model: 'sonnet' },
backlogPlanningModel: { model: 'sonnet' },
projectAnalysisModel: { model: 'sonnet' },
suggestionsModel: { model: 'sonnet' },
specGenerationModel: { model: 'claude-opus' },
featureGenerationModel: { model: 'claude-sonnet' },
backlogPlanningModel: { model: 'claude-sonnet' },
projectAnalysisModel: { model: 'claude-sonnet' },
suggestionsModel: { model: 'claude-sonnet' },
// Memory - use fast model for learning extraction (cost-effective)
memoryExtractionModel: { model: 'haiku' },
memoryExtractionModel: { model: 'claude-haiku' },
// Commit messages - use fast model for speed
commitMessageModel: { model: 'haiku' },
commitMessageModel: { model: 'claude-haiku' },
};
/** Current version of the global settings schema */
@@ -936,18 +952,18 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
useWorktrees: true,
defaultPlanningMode: 'skip',
defaultRequirePlanApproval: false,
defaultFeatureModel: { model: 'opus' },
defaultFeatureModel: { model: 'claude-opus' }, // Use canonical ID
muteDoneSound: false,
serverLogLevel: 'info',
enableRequestLogging: true,
enableAiCommitMessages: true,
phaseModels: DEFAULT_PHASE_MODELS,
enhancementModel: 'sonnet',
validationModel: 'opus',
enabledCursorModels: getAllCursorModelIds(),
cursorDefaultModel: 'auto',
enabledOpencodeModels: getAllOpencodeModelIds(),
opencodeDefaultModel: DEFAULT_OPENCODE_MODEL,
enhancementModel: 'sonnet', // Legacy alias still supported
validationModel: 'opus', // Legacy alias still supported
enabledCursorModels: getAllCursorModelIds(), // Returns prefixed IDs
cursorDefaultModel: 'cursor-auto', // Use canonical prefixed ID
enabledOpencodeModels: getAllOpencodeModelIds(), // Returns prefixed IDs
opencodeDefaultModel: DEFAULT_OPENCODE_MODEL, // Already prefixed
enabledDynamicModelIds: [],
disabledProviders: [],
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS,
@@ -971,6 +987,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
codexThreadId: undefined,
mcpServers: [],
defaultEditorCommand: null,
defaultTerminalId: null,
enableSkills: true,
skillsSources: ['user', 'project'],
enableSubagents: true,

View File

@@ -0,0 +1,15 @@
/**
* Terminal types for the "Open In Terminal" functionality
*/
/**
* Information about an available external terminal
*/
export interface TerminalInfo {
/** Unique identifier for the terminal (e.g., 'iterm2', 'warp') */
id: string;
/** Display name of the terminal (e.g., "iTerm2", "Warp") */
name: string;
/** CLI command or open command to launch the terminal */
command: string;
}

View File

@@ -0,0 +1,32 @@
/**
* Worktree and PR-related types
* Shared across server and UI components
*/
/** GitHub PR states as returned by the GitHub API (uppercase) */
export type PRState = 'OPEN' | 'MERGED' | 'CLOSED';
/** Valid PR states for validation */
export const PR_STATES: readonly PRState[] = ['OPEN', 'MERGED', 'CLOSED'] as const;
/**
* Validates a PR state value from external APIs (e.g., GitHub CLI).
* Returns the validated state if it matches a known PRState, otherwise returns 'OPEN' as default.
* This is safer than type assertions as it handles unexpected values from external APIs.
*
* @param state - The state string to validate (can be any string)
* @returns A valid PRState value
*/
export function validatePRState(state: string | undefined | null): PRState {
return PR_STATES.find((s) => s === state) ?? 'OPEN';
}
/** PR information stored in worktree metadata */
export interface WorktreePRInfo {
number: number;
url: string;
title: string;
/** PR state: OPEN, MERGED, or CLOSED */
state: PRState;
createdAt: string;
}