feat: Address review comments, add stage/unstage functionality, conflict resolution improvements, support for Sonnet 4.6

This commit is contained in:
gsxdsm
2026-02-18 18:58:33 -08:00
parent df9a6314da
commit 983eb21faa
66 changed files with 2317 additions and 823 deletions

View File

@@ -30,6 +30,13 @@ export interface CopilotModelConfig {
*/
export const COPILOT_MODEL_MAP = {
// Claude models (Anthropic via GitHub Copilot)
'copilot-claude-sonnet-4.6': {
label: 'Claude Sonnet 4.6',
description: 'Anthropic Claude Sonnet 4.6 via GitHub Copilot.',
supportsVision: true,
supportsTools: true,
contextWindow: 200000,
},
'copilot-claude-sonnet-4.5': {
label: 'Claude Sonnet 4.5',
description: 'Anthropic Claude Sonnet 4.5 via GitHub Copilot.',
@@ -147,7 +154,7 @@ export function getAllCopilotModelIds(): CopilotModelId[] {
/**
* Default Copilot model
*/
export const DEFAULT_COPILOT_MODEL: CopilotModelId = 'copilot-claude-sonnet-4.5';
export const DEFAULT_COPILOT_MODEL: CopilotModelId = 'copilot-claude-sonnet-4.6';
/**
* GitHub Copilot authentication status

View File

@@ -8,6 +8,8 @@
export type CursorModelId =
| 'cursor-auto' // Auto-select best model
| 'cursor-composer-1' // Cursor Composer 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
| 'cursor-sonnet-4.5-thinking' // Claude Sonnet 4.5 with extended thinking
| 'cursor-opus-4.5' // Claude Opus 4.5
@@ -35,6 +37,8 @@ export type CursorModelId =
export type LegacyCursorModelId =
| 'auto'
| 'composer-1'
| 'sonnet-4.6'
| 'sonnet-4.6-thinking'
| 'sonnet-4.5'
| 'sonnet-4.5-thinking'
| 'opus-4.5'
@@ -75,6 +79,20 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
hasThinking: false,
supportsVision: false,
},
'cursor-sonnet-4.6': {
id: 'cursor-sonnet-4.6',
label: 'Claude Sonnet 4.6',
description: 'Anthropic Claude Sonnet 4.6 via Cursor',
hasThinking: false,
supportsVision: false, // Model supports vision but Cursor CLI doesn't pass images
},
'cursor-sonnet-4.6-thinking': {
id: 'cursor-sonnet-4.6-thinking',
label: 'Claude Sonnet 4.6 (Thinking)',
description: 'Claude Sonnet 4.6 with extended thinking enabled',
hasThinking: true,
supportsVision: false,
},
'cursor-sonnet-4.5': {
id: 'cursor-sonnet-4.5',
label: 'Claude Sonnet 4.5',
@@ -223,6 +241,8 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
export const LEGACY_CURSOR_MODEL_MAP: Record<LegacyCursorModelId, CursorModelId> = {
auto: 'cursor-auto',
'composer-1': 'cursor-composer-1',
'sonnet-4.6': 'cursor-sonnet-4.6',
'sonnet-4.6-thinking': 'cursor-sonnet-4.6-thinking',
'sonnet-4.5': 'cursor-sonnet-4.5',
'sonnet-4.5-thinking': 'cursor-sonnet-4.5-thinking',
'opus-4.5': 'cursor-opus-4.5',
@@ -378,6 +398,22 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [
},
],
},
// Sonnet 4.6 group (thinking mode)
{
baseId: 'cursor-sonnet-4.6-group',
label: 'Claude Sonnet 4.6',
description: 'Anthropic Claude Sonnet 4.6 via Cursor',
variantType: 'thinking',
variants: [
{ id: 'cursor-sonnet-4.6', label: 'Standard', description: 'Fast responses' },
{
id: 'cursor-sonnet-4.6-thinking',
label: 'Thinking',
description: 'Extended reasoning',
badge: 'Reasoning',
},
],
},
// Sonnet 4.5 group (thinking mode)
{
baseId: 'cursor-sonnet-4.5-group',

View File

@@ -253,7 +253,7 @@ export const REASONING_EFFORT_LABELS: Record<ReasoningEffort, string> = {
* ```typescript
* getModelDisplayName("haiku"); // "Claude Haiku"
* getModelDisplayName("sonnet"); // "Claude Sonnet"
* getModelDisplayName("claude-opus-4-20250514"); // "claude-opus-4-20250514"
* getModelDisplayName("claude-sonnet-4-6"); // "Claude Sonnet 4.6"
* ```
*/
export function getModelDisplayName(model: ModelAlias | string): string {
@@ -261,6 +261,11 @@ export function getModelDisplayName(model: ModelAlias | string): string {
haiku: 'Claude Haiku',
sonnet: 'Claude Sonnet',
opus: 'Claude Opus',
'claude-haiku': 'Claude Haiku',
'claude-sonnet': 'Claude Sonnet',
'claude-opus': 'Claude Opus',
'claude-sonnet-4-6': 'Claude Sonnet 4.6',
'claude-opus-4-6': 'Claude Opus 4.6',
[CODEX_MODEL_MAP.gpt53Codex]: 'GPT-5.3-Codex',
[CODEX_MODEL_MAP.gpt53CodexSpark]: 'GPT-5.3-Codex-Spark',
[CODEX_MODEL_MAP.gpt52Codex]: 'GPT-5.2-Codex',

View File

@@ -8,7 +8,11 @@
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 {
LEGACY_OPENCODE_MODEL_MAP,
OPENCODE_MODEL_CONFIG_MAP,
RETIRED_OPENCODE_MODEL_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';
@@ -61,11 +65,16 @@ export function migrateModelId(legacyId: string | undefined | null): string {
return LEGACY_CURSOR_MODEL_MAP[legacyId];
}
// Already has opencode- prefix - it's canonical
// Already has opencode- prefix - check if it's a current canonical ID
if (legacyId.startsWith('opencode-') && legacyId in OPENCODE_MODEL_CONFIG_MAP) {
return legacyId;
}
// Retired opencode- canonical IDs (e.g., 'opencode-grok-code' → 'opencode-big-pickle')
if (legacyId.startsWith('opencode-') && legacyId in RETIRED_OPENCODE_MODEL_MAP) {
return RETIRED_OPENCODE_MODEL_MAP[legacyId];
}
// Legacy OpenCode model ID (with slash format)
if (isLegacyOpencodeModelId(legacyId)) {
return LEGACY_OPENCODE_MODEL_MAP[legacyId];
@@ -128,29 +137,36 @@ export function migrateOpencodeModelIds(ids: string[]): OpencodeModelId[] {
return [];
}
return ids.map((id) => {
// Already canonical (dash format)
if (id.startsWith('opencode-') && id in OPENCODE_MODEL_CONFIG_MAP) {
return ids
.map((id) => {
// Already canonical (dash format) and current
if (id.startsWith('opencode-') && id in OPENCODE_MODEL_CONFIG_MAP) {
return id as OpencodeModelId;
}
// Retired canonical IDs (e.g., 'opencode-grok-code') → replacement
if (id.startsWith('opencode-') && id in RETIRED_OPENCODE_MODEL_MAP) {
return RETIRED_OPENCODE_MODEL_MAP[id];
}
// 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;
}
// 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;
});
})
.filter((id, index, self) => self.indexOf(id) === index); // Deduplicate after migration
}
/**

View File

@@ -17,7 +17,7 @@ export type ClaudeCanonicalId = 'claude-haiku' | 'claude-sonnet' | 'claude-opus'
*/
export const CLAUDE_CANONICAL_MAP: Record<ClaudeCanonicalId, string> = {
'claude-haiku': 'claude-haiku-4-5-20251001',
'claude-sonnet': 'claude-sonnet-4-5-20250929',
'claude-sonnet': 'claude-sonnet-4-6',
'claude-opus': 'claude-opus-4-6',
} as const;
@@ -28,7 +28,7 @@ export const CLAUDE_CANONICAL_MAP: Record<ClaudeCanonicalId, string> = {
*/
export const CLAUDE_MODEL_MAP: Record<string, string> = {
haiku: 'claude-haiku-4-5-20251001',
sonnet: 'claude-sonnet-4-5-20250929',
sonnet: 'claude-sonnet-4-6',
opus: 'claude-opus-4-6',
} as const;

View File

@@ -8,18 +8,23 @@
export type OpencodeModelId =
// OpenCode Free Tier Models
| 'opencode-big-pickle'
| 'opencode-glm-4.7-free'
| 'opencode-glm-5-free'
| 'opencode-gpt-5-nano'
| 'opencode-grok-code'
| 'opencode-minimax-m2.1-free';
| 'opencode-kimi-k2.5-free'
| 'opencode-minimax-m2.5-free';
/**
* Legacy OpenCode model IDs (with slash format) for migration support
* Includes both current and previously-available models for backward compatibility.
*/
export type LegacyOpencodeModelId =
| 'opencode/big-pickle'
| 'opencode/glm-4.7-free'
| 'opencode/glm-5-free'
| 'opencode/gpt-5-nano'
| 'opencode/kimi-k2.5-free'
| 'opencode/minimax-m2.5-free'
// Retired models (kept for migration from older settings)
| 'opencode/glm-4.7-free'
| 'opencode/grok-code'
| 'opencode/minimax-m2.1-free';
@@ -35,23 +40,40 @@ 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',
'glm-free': 'opencode-glm-5-free',
'glm-5': 'opencode-glm-5-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',
'kimi-free': 'opencode-kimi-k2.5-free',
kimi: 'opencode-kimi-k2.5-free',
minimax: 'opencode-minimax-m2.5-free',
} as const;
/**
* Map from legacy slash-format model IDs to canonical prefixed IDs
* Map from legacy slash-format model IDs to canonical prefixed IDs.
* Retired models are mapped to their closest replacement.
*/
export const LEGACY_OPENCODE_MODEL_MAP: Record<LegacyOpencodeModelId, OpencodeModelId> = {
// Current models
'opencode/big-pickle': 'opencode-big-pickle',
'opencode/glm-4.7-free': 'opencode-glm-4.7-free',
'opencode/glm-5-free': 'opencode-glm-5-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/kimi-k2.5-free': 'opencode-kimi-k2.5-free',
'opencode/minimax-m2.5-free': 'opencode-minimax-m2.5-free',
// Retired models → mapped to replacements
'opencode/glm-4.7-free': 'opencode-glm-5-free',
'opencode/grok-code': 'opencode-big-pickle', // grok-code retired, fallback to default
'opencode/minimax-m2.1-free': 'opencode-minimax-m2.5-free',
};
/**
* Map from retired canonical (dash-format) model IDs to their replacements.
* Used to migrate settings that reference models no longer available.
*/
export const RETIRED_OPENCODE_MODEL_MAP: Record<string, OpencodeModelId> = {
'opencode-glm-4.7-free': 'opencode-glm-5-free',
'opencode-grok-code': 'opencode-big-pickle',
'opencode-minimax-m2.1-free': 'opencode-minimax-m2.5-free',
};
/**
@@ -81,8 +103,8 @@ export const OPENCODE_MODELS: OpencodeModelConfig[] = [
tier: 'free',
},
{
id: 'opencode-glm-4.7-free',
label: 'GLM 4.7 Free',
id: 'opencode-glm-5-free',
label: 'GLM 5 Free',
description: 'OpenCode free tier GLM model',
supportsVision: false,
provider: 'opencode',
@@ -97,16 +119,16 @@ export const OPENCODE_MODELS: OpencodeModelConfig[] = [
tier: 'free',
},
{
id: 'opencode-grok-code',
label: 'Grok Code',
description: 'OpenCode free tier Grok model for coding',
id: 'opencode-kimi-k2.5-free',
label: 'Kimi K2.5 Free',
description: 'OpenCode free tier Kimi model for coding',
supportsVision: false,
provider: 'opencode',
tier: 'free',
},
{
id: 'opencode-minimax-m2.1-free',
label: 'MiniMax M2.1 Free',
id: 'opencode-minimax-m2.5-free',
label: 'MiniMax M2.5 Free',
description: 'OpenCode free tier MiniMax model',
supportsVision: false,
provider: 'opencode',
@@ -160,7 +182,8 @@ export function getOpencodeModelProvider(modelId: OpencodeModelId): OpencodeProv
}
/**
* Helper: Resolve an alias or partial model ID to a full model ID
* Helper: Resolve an alias or partial model ID to a full model ID.
* Also handles retired model IDs by mapping them to their replacements.
*/
export function resolveOpencodeModelId(input: string): OpencodeModelId | undefined {
// Check if it's already a valid model ID
@@ -168,6 +191,11 @@ export function resolveOpencodeModelId(input: string): OpencodeModelId | undefin
return input as OpencodeModelId;
}
// Check retired model map (handles old canonical IDs like 'opencode-grok-code')
if (input in RETIRED_OPENCODE_MODEL_MAP) {
return RETIRED_OPENCODE_MODEL_MAP[input];
}
// Check alias map
const normalized = input.toLowerCase();
return OPENCODE_MODEL_MAP[normalized];

View File

@@ -9,7 +9,11 @@
import type { ModelProvider } from './settings.js';
import { LEGACY_CURSOR_MODEL_MAP } from './cursor-models.js';
import { CLAUDE_MODEL_MAP, CODEX_MODEL_MAP } from './model.js';
import { OPENCODE_MODEL_CONFIG_MAP, LEGACY_OPENCODE_MODEL_MAP } from './opencode-models.js';
import {
OPENCODE_MODEL_CONFIG_MAP,
LEGACY_OPENCODE_MODEL_MAP,
RETIRED_OPENCODE_MODEL_MAP,
} from './opencode-models.js';
import { GEMINI_MODEL_MAP } from './gemini-models.js';
import { COPILOT_MODEL_MAP } from './copilot-models.js';
@@ -51,7 +55,7 @@ export function isCursorModel(model: string | undefined | null): boolean {
/**
* Check if a model string represents a Claude model
*
* @param model - Model string to check (e.g., "sonnet", "opus", "claude-sonnet-4-20250514")
* @param model - Model string to check (e.g., "sonnet", "opus", "claude-sonnet-4-6")
* @returns true if the model is a Claude model
*/
export function isClaudeModel(model: string | undefined | null): boolean {
@@ -310,7 +314,10 @@ export function getBareModelId(model: string): string {
export function normalizeModelString(model: string | undefined | null): string {
if (!model || typeof model !== 'string') return 'claude-sonnet'; // Default to canonical
// Already has a canonical prefix - return as-is
// Already has a canonical prefix - return as-is (but check for retired opencode models first)
if (model.startsWith(PROVIDER_PREFIXES.opencode) && model in RETIRED_OPENCODE_MODEL_MAP) {
return RETIRED_OPENCODE_MODEL_MAP[model];
}
if (
model.startsWith(PROVIDER_PREFIXES.cursor) ||
model.startsWith(PROVIDER_PREFIXES.codex) ||
@@ -364,7 +371,7 @@ export function normalizeModelString(model: string | undefined | null): string {
*
* @example
* supportsStructuredOutput('sonnet') // true (Claude)
* supportsStructuredOutput('claude-sonnet-4-20250514') // true (Claude)
* supportsStructuredOutput('claude-sonnet-4-6') // true (Claude)
* supportsStructuredOutput('codex-gpt-5.2') // true (Codex/OpenAI)
* supportsStructuredOutput('cursor-auto') // false
* supportsStructuredOutput('gemini-2.5-pro') // false