diff --git a/.env.example b/.env.example index 7e1d124c72..09820939ad 100755 --- a/.env.example +++ b/.env.example @@ -28,6 +28,12 @@ HOST=0.0.0.0 # Uncomment the following line if you have a custom claude cli path other than the default "claude" # CLAUDE_CLI_PATH=claude +# MiniMax provider configuration. The default region is global; use "cn" for +# the China endpoint or set an explicit Anthropic-compatible base URL. +# MINIMAX_API_KEY= +# MINIMAX_REGION=global +# MINIMAX_ANTHROPIC_BASE_URL=https://api.minimax.io/anthropic + # ============================================================================= # DATABASE CONFIGURATION # ============================================================================= @@ -42,4 +48,3 @@ HOST=0.0.0.0 VITE_CONTEXT_WINDOW=160000 CONTEXT_WINDOW=160000 - diff --git a/public/api-docs.html b/public/api-docs.html index c2d60173e0..8d3c58a714 100644 --- a/public/api-docs.html +++ b/public/api-docs.html @@ -489,7 +489,7 @@

Agent

http://localhost:3001/api/agent -

Trigger an AI agent (Claude, Cursor, Codex, or OpenCode) to work on a project.

+

Trigger an AI agent, including MiniMax, to work on a project.

Request Body Parameters

@@ -524,7 +524,7 @@

Request Body Parameters

- + @@ -838,6 +838,7 @@

Branch & PR Response

{ id: 'claude', name: 'Anthropic' }, { id: 'codex', name: 'OpenAI' }, { id: 'cursor', name: 'Cursor' }, + { id: 'minimax', name: 'MiniMax' }, { id: 'opencode', name: 'OpenCode' }, ]; diff --git a/server/claude-sdk.js b/server/claude-sdk.js index 6e5bb5cee5..293176a3b4 100644 --- a/server/claude-sdk.js +++ b/server/claude-sdk.js @@ -165,6 +165,10 @@ function mapCliOptionsToSDK(options = {}) { // Forward all host env vars (e.g. ANTHROPIC_BASE_URL) to the subprocess. // Since SDK 0.2.113, options.env replaces process.env instead of overlaying it. sdkOptions.env = { ...process.env }; + for (const environmentName of options.unsetEnvironment || []) { + delete sdkOptions.env[environmentName]; + } + Object.assign(sdkOptions.env, options.environment || {}); // Resolve the executable eagerly on Windows because the SDK uses raw child_process.spawn, // which does not reliably follow npm's shell wrappers like cross-spawn does. @@ -208,7 +212,7 @@ function mapCliOptionsToSDK(options = {}) { sdkOptions.disallowedTools = settings.disallowedTools || []; - sdkOptions.model = options.model || CLAUDE_FALLBACK_MODELS.DEFAULT; + sdkOptions.model = options.model || options.defaultModel || CLAUDE_FALLBACK_MODELS.DEFAULT; const resolvedEffort = resolveClaudeEffort( sdkOptions.model, @@ -301,7 +305,7 @@ function readNumber(value) { * @param {Object} sdkMessage - SDK stream message * @returns {Object|null} Token budget object or null */ -function extractTokenBudget(sdkMessage) { +function extractTokenBudget(sdkMessage, contextWindowOverride) { if (!sdkMessage || typeof sdkMessage !== 'object') { return null; } @@ -315,7 +319,9 @@ function extractTokenBudget(sdkMessage) { const inputTokens = directInputTokens + cacheTokens; const outputTokens = readNumber(messageUsage.output_tokens ?? messageUsage.outputTokens); const totalUsed = inputTokens + outputTokens; - const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000; + const contextWindow = Number.isFinite(contextWindowOverride) + ? contextWindowOverride + : parseInt(process.env.CONTEXT_WINDOW, 10) || 160000; return { used: totalUsed, @@ -455,10 +461,12 @@ async function loadMcpConfig(cwd) { * @param {string} command - User prompt/command * @param {Object} options - Query options * @param {Object} ws - WebSocket connection + * @param {Object} runtimeConfig - Provider identity and compatible runtime overrides * @returns {Promise} */ -async function queryClaudeSDK(command, options = {}, ws) { +async function queryClaudeSDK(command, options = {}, ws, runtimeConfig = {}) { const { sessionId, sessionSummary } = options; + const provider = runtimeConfig.provider || 'claude'; let capturedSessionId = sessionId; let sessionCreatedSent = false; @@ -472,21 +480,24 @@ async function queryClaudeSDK(command, options = {}, ws) { try { const resolvedModel = await providerModelsService.resolveResumeModel( - 'claude', + provider, sessionId, options.model, ); let effortModels = CLAUDE_FALLBACK_MODELS; try { - effortModels = (await providerModelsService.getProviderModels('claude')).models; + effortModels = (await providerModelsService.getProviderModels(provider)).models; } catch (error) { - console.warn('[Claude SDK] Unable to load provider models for effort validation:', error); + console.warn(`[${provider}] Unable to load provider models for effort validation:`, error); } const sdkOptions = mapCliOptionsToSDK({ ...options, model: resolvedModel || options.model, effortModels, + defaultModel: runtimeConfig.defaultModel, + environment: runtimeConfig.environment, + unsetEnvironment: runtimeConfig.unsetEnvironment, }); const mcpServers = await loadMcpConfig(options.cwd); @@ -503,16 +514,16 @@ async function queryClaudeSDK(command, options = {}, ws) { Notification: [{ matcher: '', hooks: [async (input) => { - const message = typeof input?.message === 'string' ? input.message : 'Claude requires your attention.'; + const message = typeof input?.message === 'string' ? input.message : 'The provider requires your attention.'; emitNotification(createNotificationEvent({ - provider: 'claude', + provider, sessionId: capturedSessionId || sessionId || null, kind: 'action_required', code: 'agent.notification', meta: { message, sessionName: sessionSummary }, severity: 'warning', requiresUserAction: true, - dedupeKey: `claude:hook:notification:${capturedSessionId || sessionId || 'none'}:${message}` + dedupeKey: `${provider}:hook:notification:${capturedSessionId || sessionId || 'none'}:${message}` })); return {}; }] @@ -549,16 +560,16 @@ async function queryClaudeSDK(command, options = {}, ws) { } const requestId = createRequestId(); - ws.send(createNormalizedMessage({ kind: 'permission_request', requestId, toolName, input, sessionId: capturedSessionId || sessionId || null, provider: 'claude' })); + ws.send(createNormalizedMessage({ kind: 'permission_request', requestId, toolName, input, sessionId: capturedSessionId || sessionId || null, provider })); emitNotification(createNotificationEvent({ - provider: 'claude', + provider, sessionId: capturedSessionId || sessionId || null, kind: 'action_required', code: 'permission.required', meta: { toolName, sessionName: sessionSummary }, severity: 'warning', requiresUserAction: true, - dedupeKey: `claude:permission:${capturedSessionId || sessionId || 'none'}:${requestId}` + dedupeKey: `${provider}:permission:${capturedSessionId || sessionId || 'none'}:${requestId}` })); const decision = await waitForToolApproval(requestId, { @@ -571,7 +582,7 @@ async function queryClaudeSDK(command, options = {}, ws) { _receivedAt: new Date(), }, onCancel: (reason) => { - ws.send(createNormalizedMessage({ kind: 'permission_cancelled', requestId, reason, sessionId: capturedSessionId || sessionId || null, provider: 'claude' })); + ws.send(createNormalizedMessage({ kind: 'permission_cancelled', requestId, reason, sessionId: capturedSessionId || sessionId || null, provider })); } }); if (!decision) { @@ -647,7 +658,7 @@ async function queryClaudeSDK(command, options = {}, ws) { // Send session-created event only once for new sessions if (!sessionId && !sessionCreatedSent) { sessionCreatedSent = true; - ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'claude' })); + ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider })); } } else { // session_id already captured @@ -658,7 +669,7 @@ async function queryClaudeSDK(command, options = {}, ws) { const sid = capturedSessionId || sessionId || null; // Use adapter to normalize SDK events into NormalizedMessage[] - const normalized = sessionsService.normalizeMessage('claude', transformedMessage, sid); + const normalized = sessionsService.normalizeMessage(provider, transformedMessage, sid); for (const msg of normalized) { // Preserve parentToolUseId from SDK wrapper for subagent tool grouping if (transformedMessage.parentToolUseId && !msg.parentToolUseId) { @@ -668,9 +679,9 @@ async function queryClaudeSDK(command, options = {}, ws) { } // Extract and send token budget updates from assistant/result usage payloads - const tokenBudgetData = extractTokenBudget(message); + const tokenBudgetData = extractTokenBudget(message, runtimeConfig.contextWindow); if (tokenBudgetData) { - ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' })); + ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider })); } } @@ -683,11 +694,11 @@ async function queryClaudeSDK(command, options = {}, ws) { // terminal `complete` (aborted: true) was already sent by abort-session. const wasAborted = capturedSessionId ? abortedSessionIds.delete(capturedSessionId) : false; if (!wasAborted) { - ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 0 })); + ws.send(createCompleteMessage({ provider, sessionId: capturedSessionId || sessionId || null, exitCode: 0 })); } notifyRunStopped({ userId: ws?.userId || null, - provider: 'claude', + provider, sessionId: capturedSessionId || sessionId || null, sessionName: sessionSummary, stopReason: wasAborted ? 'aborted' : 'completed' @@ -709,18 +720,18 @@ async function queryClaudeSDK(command, options = {}, ws) { return; } - // Check if Claude CLI is installed for a clearer error message - const installed = await providerAuthService.isProviderInstalled('claude'); + // Check whether the compatible runtime is installed for a clearer error message. + const installed = await providerAuthService.isProviderInstalled(provider); const errorContent = !installed - ? 'Claude Code is not installed. Please install it first: https://docs.anthropic.com/en/docs/claude-code' + ? 'The compatible coding runtime is not installed.' : error.message; // Send error to WebSocket, then the terminal complete - ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'claude' })); - ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 1 })); + ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider })); + ws.send(createCompleteMessage({ provider, sessionId: capturedSessionId || sessionId || null, exitCode: 1 })); notifyRunFailed({ userId: ws?.userId || null, - provider: 'claude', + provider, sessionId: capturedSessionId || sessionId || null, sessionName: sessionSummary, error diff --git a/server/index.js b/server/index.js index 0832c4905b..55f4cabad5 100755 --- a/server/index.js +++ b/server/index.js @@ -39,6 +39,10 @@ import { spawnOpenCode, abortOpenCodeSession, } from './opencode-cli.js'; +import { + queryMiniMaxSDK, + abortMiniMaxSDKSession, +} from './minimax-sdk.js'; import { stripAnsiSequences, normalizeDetectedUrl, @@ -113,12 +117,14 @@ const wss = createWebSocketServer(server, { claude: queryClaudeSDK, cursor: spawnCursor, codex: queryCodex, + minimax: queryMiniMaxSDK, opencode: spawnOpenCode, }, abortFns: { claude: abortClaudeSDKSession, cursor: abortCursorSession, codex: abortCodexSession, + minimax: abortMiniMaxSDKSession, opencode: abortOpenCodeSession, }, resolveToolApproval, diff --git a/server/minimax-sdk.ts b/server/minimax-sdk.ts new file mode 100644 index 0000000000..b1fb852f3b --- /dev/null +++ b/server/minimax-sdk.ts @@ -0,0 +1,39 @@ +import { + buildMiniMaxRuntimeEnvironment, + readMiniMaxSettingsEnvironment, +} from '@/modules/providers/list/minimax/minimax-config.js'; +import { + MINIMAX_MODELS, + resolveMiniMaxContextWindow, +} from '@/modules/providers/list/minimax/minimax-models.provider.js'; +import type { AnyRecord } from '@/shared/types.js'; + +import { abortClaudeSDKSession, queryClaudeSDK } from './claude-sdk.js'; + +export async function queryMiniMaxSDK( + command: string, + options: AnyRecord = {}, + writer: Parameters[2], +): Promise { + const model = typeof options.model === 'string' && options.model.trim() + ? options.model.trim() + : MINIMAX_MODELS.DEFAULT; + const contextWindow = resolveMiniMaxContextWindow(model); + const settingsEnvironment = await readMiniMaxSettingsEnvironment(); + const environment = buildMiniMaxRuntimeEnvironment( + model, + contextWindow, + process.env, + settingsEnvironment, + ); + + return queryClaudeSDK(command, { ...options, model }, writer, { + provider: 'minimax', + defaultModel: MINIMAX_MODELS.DEFAULT, + contextWindow, + environment, + unsetEnvironment: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'], + }); +} + +export const abortMiniMaxSDKSession = abortClaudeSDKSession; diff --git a/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts b/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts index 9320a2fe02..aa4a8d848b 100644 --- a/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +++ b/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { readFile } from 'node:fs/promises'; import { sessionsDb } from '@/modules/database/index.js'; +import { isMiniMaxModel } from '@/modules/providers/list/minimax/minimax-models.provider.js'; import { buildLookupMap, extractFirstValidJsonlData, @@ -11,10 +12,12 @@ import { readFileTimestamps, } from '@/shared/utils.js'; import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js'; +import type { LLMProvider } from '@/shared/types.js'; type ParsedSession = { sessionId: string; projectPath: string; + provider: LLMProvider; sessionName?: string; }; @@ -22,7 +25,6 @@ type ParsedSession = { * Session indexer for Claude transcript artifacts. */ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer { - private readonly provider = 'claude' as const; private readonly claudeHome = path.join(os.homedir(), '.claude'); /** @@ -65,7 +67,7 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer { const timestamps = await readFileTimestamps(filePath); sessionsDb.createSession( parsed.sessionId, - this.provider, + parsed.provider, parsed.projectPath, parsed.sessionName, timestamps.createdAt, @@ -98,7 +100,7 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer { const timestamps = await readFileTimestamps(filePath); return sessionsDb.createSession( parsed.sessionId, - this.provider, + parsed.provider, parsed.projectPath, parsed.sessionName, timestamps.createdAt, @@ -137,11 +139,18 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer { // ids must be resolved through the provider-id mapping first. const existingSession = sessionsDb.getSessionByProviderSessionId(parsed.sessionId) ?? sessionsDb.getSessionById(parsed.sessionId); + const provider = existingSession?.provider === 'minimax' + ? 'minimax' + : await this.detectCompatibleProvider(filePath); + const defaultSessionName = provider === 'minimax' + ? 'Untitled MiniMax Session' + : 'Untitled Claude Session'; const existingSessionName = existingSession?.custom_name; - if (existingSessionName && existingSessionName !== 'Untitled Claude Session') { + if (existingSessionName && existingSessionName !== defaultSessionName) { return { ...parsed, - sessionName: normalizeSessionName(existingSessionName, 'Untitled Claude Session'), + provider, + sessionName: normalizeSessionName(existingSessionName, defaultSessionName), }; } @@ -152,10 +161,41 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer { return { ...parsed, - sessionName: normalizeSessionName(sessionName, 'Untitled Claude Session'), + provider, + sessionName: normalizeSessionName(sessionName, defaultSessionName), }; } + private async detectCompatibleProvider(filePath: string): Promise { + try { + const content = await readFile(filePath, 'utf8'); + for (const line of content.split(/\r?\n/)) { + if (!line.trim()) { + continue; + } + + let parsed: unknown; + try { + parsed = JSON.parse(line); + } catch { + continue; + } + + const data = parsed as Record; + const message = data.message && typeof data.message === 'object' + ? data.message as Record + : null; + if (isMiniMaxModel(data.model) || isMiniMaxModel(message?.model)) { + return 'minimax'; + } + } + } catch { + // Unreadable transcripts remain attached to the default compatible provider. + } + + return 'claude'; + } + private async extractSessionAiTitleFromEnd( filePath: string, sessionId: string diff --git a/server/modules/providers/list/minimax/minimax-auth.provider.ts b/server/modules/providers/list/minimax/minimax-auth.provider.ts new file mode 100644 index 0000000000..093d44ada6 --- /dev/null +++ b/server/modules/providers/list/minimax/minimax-auth.provider.ts @@ -0,0 +1,48 @@ +import spawn from 'cross-spawn'; + +import { + readMiniMaxSettingsEnvironment, + resolveMiniMaxCredential, +} from '@/modules/providers/list/minimax/minimax-config.js'; +import { resolveClaudeCodeExecutablePath } from '@/shared/claude-cli-path.js'; +import type { IProviderAuth } from '@/shared/interfaces.js'; +import type { ProviderAuthStatus } from '@/shared/types.js'; + +export class MiniMaxProviderAuth implements IProviderAuth { + private checkInstalled(): boolean { + const cliPath = resolveClaudeCodeExecutablePath(process.env.CLAUDE_CLI_PATH); + try { + spawn.sync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 }); + return true; + } catch { + return false; + } + } + + async getStatus(): Promise { + const installed = this.checkInstalled(); + if (!installed) { + return { + installed, + provider: 'minimax', + authenticated: false, + email: null, + method: null, + error: 'The compatible coding CLI is not installed', + }; + } + + const settingsEnvironment = await readMiniMaxSettingsEnvironment(); + const credential = resolveMiniMaxCredential(process.env, settingsEnvironment); + return { + installed, + provider: 'minimax', + authenticated: Boolean(credential), + email: credential ? 'API Key Auth' : null, + method: credential ? 'api_key' : null, + error: credential + ? undefined + : 'Configure MINIMAX_API_KEY or a MiniMax endpoint and token in the coding CLI settings.', + }; + } +} diff --git a/server/modules/providers/list/minimax/minimax-config.ts b/server/modules/providers/list/minimax/minimax-config.ts new file mode 100644 index 0000000000..32fd48e75c --- /dev/null +++ b/server/modules/providers/list/minimax/minimax-config.ts @@ -0,0 +1,112 @@ +import { readFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { readObjectRecord, readOptionalString } from '@/shared/utils.js'; + +export const MINIMAX_ANTHROPIC_ENDPOINTS = { + global: 'https://api.minimax.io/anthropic', + cn: 'https://api.minimaxi.com/anthropic', +} as const; + +type MiniMaxEnvironment = Record; + +const normalizeBaseUrl = (value: string): string => value.trim().replace(/\/+$/, ''); + +export const isMiniMaxBaseUrl = (value: string | undefined): boolean => { + if (!value?.trim()) { + return false; + } + + const normalized = normalizeBaseUrl(value); + return Object.values(MINIMAX_ANTHROPIC_ENDPOINTS).some( + (endpoint) => normalizeBaseUrl(endpoint) === normalized, + ); +}; + +export const resolveMiniMaxBaseUrl = ( + environment: MiniMaxEnvironment = process.env, + settingsEnvironment: MiniMaxEnvironment = {}, +): string => { + const explicitBaseUrl = readOptionalString(environment.MINIMAX_ANTHROPIC_BASE_URL); + if (explicitBaseUrl) { + return normalizeBaseUrl(explicitBaseUrl); + } + + const existingBaseUrl = readOptionalString(environment.ANTHROPIC_BASE_URL); + if (existingBaseUrl && isMiniMaxBaseUrl(existingBaseUrl)) { + return normalizeBaseUrl(existingBaseUrl); + } + + const settingsBaseUrl = readOptionalString(settingsEnvironment.ANTHROPIC_BASE_URL); + if (settingsBaseUrl && isMiniMaxBaseUrl(settingsBaseUrl)) { + return normalizeBaseUrl(settingsBaseUrl); + } + + return environment.MINIMAX_REGION?.trim().toLowerCase() === 'cn' + ? MINIMAX_ANTHROPIC_ENDPOINTS.cn + : MINIMAX_ANTHROPIC_ENDPOINTS.global; +}; + +export const readMiniMaxSettingsEnvironment = async (): Promise => { + try { + const settingsPath = path.join(os.homedir(), '.claude', 'settings.json'); + const settings = readObjectRecord(JSON.parse(await readFile(settingsPath, 'utf8'))); + const environment = readObjectRecord(settings?.env) ?? {}; + return Object.fromEntries( + Object.entries(environment).filter( + (entry): entry is [string, string] => typeof entry[1] === 'string', + ), + ); + } catch { + return {}; + } +}; + +export const resolveMiniMaxCredential = ( + environment: MiniMaxEnvironment, + settingsEnvironment: MiniMaxEnvironment = {}, +): string | null => { + const dedicatedKey = readOptionalString(environment.MINIMAX_API_KEY); + if (dedicatedKey) { + return dedicatedKey; + } + + const environmentBaseUrl = readOptionalString(environment.ANTHROPIC_BASE_URL); + if (isMiniMaxBaseUrl(environmentBaseUrl)) { + return readOptionalString(environment.ANTHROPIC_AUTH_TOKEN) + ?? readOptionalString(environment.ANTHROPIC_API_KEY) + ?? null; + } + + const settingsBaseUrl = readOptionalString(settingsEnvironment.ANTHROPIC_BASE_URL); + if (isMiniMaxBaseUrl(settingsBaseUrl)) { + return readOptionalString(settingsEnvironment.ANTHROPIC_AUTH_TOKEN) + ?? readOptionalString(settingsEnvironment.ANTHROPIC_API_KEY) + ?? null; + } + + return null; +}; + +export const buildMiniMaxRuntimeEnvironment = ( + model: string, + contextWindow: number, + environment: MiniMaxEnvironment, + settingsEnvironment: MiniMaxEnvironment = {}, +): Record => { + const runtimeEnvironment: Record = { + ANTHROPIC_BASE_URL: resolveMiniMaxBaseUrl(environment, settingsEnvironment), + ANTHROPIC_MODEL: model, + ANTHROPIC_DEFAULT_OPUS_MODEL: model, + ANTHROPIC_DEFAULT_SONNET_MODEL: model, + ANTHROPIC_DEFAULT_HAIKU_MODEL: model, + CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(contextWindow), + }; + const credential = resolveMiniMaxCredential(environment, settingsEnvironment); + if (credential) { + runtimeEnvironment.ANTHROPIC_AUTH_TOKEN = credential; + } + + return runtimeEnvironment; +}; diff --git a/server/modules/providers/list/minimax/minimax-models.provider.ts b/server/modules/providers/list/minimax/minimax-models.provider.ts new file mode 100644 index 0000000000..1709c41525 --- /dev/null +++ b/server/modules/providers/list/minimax/minimax-models.provider.ts @@ -0,0 +1,71 @@ +import type { IProviderModels } from '@/shared/interfaces.js'; +import type { + ProviderChangeActiveModelInput, + ProviderCurrentActiveModel, + ProviderModelsDefinition, + ProviderSessionActiveModelChange, +} from '@/shared/types.js'; +import { + buildDefaultProviderCurrentActiveModel, + writeProviderSessionActiveModelChange, +} from '@/shared/utils.js'; + +export const MINIMAX_MODEL_IDS = ['MiniMax-M3', 'MiniMax-M2.7'] as const; + +export const MINIMAX_MODEL_CONTEXT_WINDOWS: Record<(typeof MINIMAX_MODEL_IDS)[number], number> = { + 'MiniMax-M3': 1_000_000, + 'MiniMax-M2.7': 204_800, +}; + +export const MINIMAX_MODELS: ProviderModelsDefinition = { + OPTIONS: [ + { + value: MINIMAX_MODEL_IDS[0], + label: MINIMAX_MODEL_IDS[0], + description: '1,000,000-token context · $0.60 input / $2.40 output per million tokens', + }, + { + value: MINIMAX_MODEL_IDS[1], + label: MINIMAX_MODEL_IDS[1], + description: '204,800-token context · $0.30 input / $1.20 output per million tokens', + }, + ], + DEFAULT: MINIMAX_MODEL_IDS[0], +}; + +export const isMiniMaxModel = (model: unknown): boolean => { + if (typeof model !== 'string') { + return false; + } + + const normalized = model.trim(); + return MINIMAX_MODEL_IDS.some( + (modelId) => normalized === modelId || normalized.startsWith(`${modelId}[`), + ); +}; + +export const resolveMiniMaxContextWindow = (model: string): number => { + const normalized = model.trim(); + const modelId = MINIMAX_MODEL_IDS.find( + (candidate) => normalized === candidate || normalized.startsWith(`${candidate}[`), + ); + return modelId + ? MINIMAX_MODEL_CONTEXT_WINDOWS[modelId] + : MINIMAX_MODEL_CONTEXT_WINDOWS[MINIMAX_MODELS.DEFAULT as keyof typeof MINIMAX_MODEL_CONTEXT_WINDOWS]; +}; + +export class MiniMaxProviderModels implements IProviderModels { + async getSupportedModels(): Promise { + return MINIMAX_MODELS; + } + + async getCurrentActiveModel(): Promise { + return buildDefaultProviderCurrentActiveModel(MINIMAX_MODELS); + } + + async changeActiveModel( + input: ProviderChangeActiveModelInput, + ): Promise { + return writeProviderSessionActiveModelChange('minimax', input); + } +} diff --git a/server/modules/providers/list/minimax/minimax.provider.ts b/server/modules/providers/list/minimax/minimax.provider.ts new file mode 100644 index 0000000000..d7180c9fed --- /dev/null +++ b/server/modules/providers/list/minimax/minimax.provider.ts @@ -0,0 +1,127 @@ +import { ClaudeMcpProvider } from '@/modules/providers/list/claude/claude-mcp.provider.js'; +import { ClaudeSessionsProvider } from '@/modules/providers/list/claude/claude-sessions.provider.js'; +import { ClaudeSkillsProvider } from '@/modules/providers/list/claude/claude-skills.provider.js'; +import { MiniMaxProviderAuth } from '@/modules/providers/list/minimax/minimax-auth.provider.js'; +import { MiniMaxProviderModels } from '@/modules/providers/list/minimax/minimax-models.provider.js'; +import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js'; +import type { + IProviderMcp, + IProviderSessionSynchronizer, + IProviderSessions, + IProviderSkills, +} from '@/shared/interfaces.js'; +import type { + FetchHistoryOptions, + FetchHistoryResult, + McpScope, + NormalizedMessage, + ProviderMcpServer, + ProviderSkill, + ProviderSkillCreateInput, + ProviderSkillListOptions, + ProviderSkillRemoveInput, + UpsertProviderMcpServerInput, +} from '@/shared/types.js'; + +const remapMcpServer = (server: ProviderMcpServer): ProviderMcpServer => ({ + ...server, + provider: 'minimax', +}); + +class MiniMaxMcpProvider implements IProviderMcp { + private readonly delegate = new ClaudeMcpProvider(); + + async listServers(options?: { workspacePath?: string }): Promise> { + const grouped = await this.delegate.listServers(options); + return Object.fromEntries( + Object.entries(grouped).map(([scope, servers]) => [scope, servers.map(remapMcpServer)]), + ) as Record; + } + + async listServersForScope( + scope: McpScope, + options?: { workspacePath?: string }, + ): Promise { + return (await this.delegate.listServersForScope(scope, options)).map(remapMcpServer); + } + + async upsertServer(input: UpsertProviderMcpServerInput): Promise { + return remapMcpServer(await this.delegate.upsertServer(input)); + } + + async removeServer( + input: { name: string; scope?: McpScope; workspacePath?: string }, + ): Promise<{ removed: boolean; provider: 'minimax'; name: string; scope: McpScope }> { + const result = await this.delegate.removeServer(input); + return { ...result, provider: 'minimax' }; + } +} + +const remapSkill = (skill: ProviderSkill): ProviderSkill => ({ + ...skill, + provider: 'minimax', +}); + +class MiniMaxSkillsProvider implements IProviderSkills { + private readonly delegate = new ClaudeSkillsProvider(); + + async listSkills(options?: ProviderSkillListOptions): Promise { + return (await this.delegate.listSkills(options)).map(remapSkill); + } + + async addSkills(input: ProviderSkillCreateInput): Promise { + return (await this.delegate.addSkills(input)).map(remapSkill); + } + + async removeSkill( + input: ProviderSkillRemoveInput, + ): Promise<{ removed: boolean; provider: 'minimax'; directoryName: string }> { + const result = await this.delegate.removeSkill(input); + return { ...result, provider: 'minimax' }; + } +} + +class MiniMaxSessionsProvider implements IProviderSessions { + private readonly delegate = new ClaudeSessionsProvider(); + + normalizeMessage(raw: unknown, sessionId: string | null): NormalizedMessage[] { + return this.delegate.normalizeMessage(raw, sessionId).map((message) => ({ + ...message, + provider: 'minimax', + })); + } + + async fetchHistory( + sessionId: string, + options?: FetchHistoryOptions, + ): Promise { + const result = await this.delegate.fetchHistory(sessionId, options); + return { + ...result, + messages: result.messages.map((message) => ({ ...message, provider: 'minimax' })), + }; + } +} + +class MiniMaxSessionSynchronizer implements IProviderSessionSynchronizer { + async synchronize(): Promise { + return 0; + } + + async synchronizeFile(): Promise { + return null; + } +} + +export class MiniMaxProvider extends AbstractProvider { + readonly models = new MiniMaxProviderModels(); + readonly mcp = new MiniMaxMcpProvider(); + readonly auth = new MiniMaxProviderAuth(); + readonly skills = new MiniMaxSkillsProvider(); + readonly sessions = new MiniMaxSessionsProvider(); + readonly sessionSynchronizer = new MiniMaxSessionSynchronizer(); + + constructor() { + super('minimax'); + } +} diff --git a/server/modules/providers/provider.registry.ts b/server/modules/providers/provider.registry.ts index 8c333fcb72..7fe2f5bacc 100644 --- a/server/modules/providers/provider.registry.ts +++ b/server/modules/providers/provider.registry.ts @@ -1,6 +1,7 @@ import { ClaudeProvider } from '@/modules/providers/list/claude/claude.provider.js'; import { CodexProvider } from '@/modules/providers/list/codex/codex.provider.js'; import { CursorProvider } from '@/modules/providers/list/cursor/cursor.provider.js'; +import { MiniMaxProvider } from '@/modules/providers/list/minimax/minimax.provider.js'; import { OpenCodeProvider } from '@/modules/providers/list/opencode/opencode.provider.js'; import type { IProvider } from '@/shared/interfaces.js'; import type { LLMProvider } from '@/shared/types.js'; @@ -10,6 +11,7 @@ const providers: Record = { claude: new ClaudeProvider(), codex: new CodexProvider(), cursor: new CursorProvider(), + minimax: new MiniMaxProvider(), opencode: new OpenCodeProvider(), }; diff --git a/server/modules/providers/provider.routes.ts b/server/modules/providers/provider.routes.ts index 2352f3a141..98f29294b9 100644 --- a/server/modules/providers/provider.routes.ts +++ b/server/modules/providers/provider.routes.ts @@ -285,6 +285,7 @@ const parseProvider = (value: unknown): LLMProvider => { normalized === 'claude' || normalized === 'codex' || normalized === 'cursor' + || normalized === 'minimax' || normalized === 'opencode' ) { return normalized; diff --git a/server/modules/providers/services/provider-capabilities.service.ts b/server/modules/providers/services/provider-capabilities.service.ts index a1446062f2..aca5f131b6 100644 --- a/server/modules/providers/services/provider-capabilities.service.ts +++ b/server/modules/providers/services/provider-capabilities.service.ts @@ -62,6 +62,16 @@ const PROVIDER_CAPABILITIES: Record = { supportsTokenUsage: true, supportsEffort: true, }, + minimax: { + provider: 'minimax', + permissionModes: ['default', 'auto', 'acceptEdits', 'bypassPermissions', 'plan'], + defaultPermissionMode: 'default', + supportsImages: true, + supportsAbort: true, + supportsPermissionRequests: true, + supportsTokenUsage: true, + supportsEffort: false, + }, opencode: { provider: 'opencode', // Mapped by the runtime onto OpenCode's controls: `--agent plan` (plan), diff --git a/server/modules/providers/services/session-conversations-search.service.ts b/server/modules/providers/services/session-conversations-search.service.ts index 1854eaddc9..f85170e8ca 100644 --- a/server/modules/providers/services/session-conversations-search.service.ts +++ b/server/modules/providers/services/session-conversations-search.service.ts @@ -8,7 +8,7 @@ import { rgPath } from '@vscode/ripgrep'; import { projectsDb, sessionsDb } from '@/modules/database/index.js'; type AnyRecord = Record; -type SearchableProvider = 'claude' | 'codex'; +type SearchableProvider = 'claude' | 'codex' | 'minimax'; type SearchSnippetHighlight = { start: number; @@ -82,7 +82,7 @@ type ProjectBucket = { sessions: SearchableSessionRow[]; }; -const SUPPORTED_PROVIDERS = new Set(['claude', 'codex']); +const SUPPORTED_PROVIDERS = new Set(['claude', 'codex', 'minimax']); const MAX_MATCHES_PER_SESSION = 2; const RIPGREP_FILE_CHUNK_SIZE = 40; const RIPGREP_CHUNK_CONCURRENCY = 6; @@ -789,8 +789,10 @@ async function parseClaudeSessionMatches( const targetSessionIds = new Set(targetSessions.map((candidate) => candidate.session_id)); const customNameBySessionId = new Map(); + const providerBySessionId = new Map(); for (const candidate of targetSessions) { customNameBySessionId.set(candidate.session_id, candidate.custom_name ?? null); + providerBySessionId.set(candidate.session_id, candidate.provider); } type ClaudeSessionSearchState = { @@ -897,7 +899,7 @@ async function parseClaudeSessionMatches( snippet, highlights, timestamp: entry.timestamp ? String(entry.timestamp) : null, - provider: 'claude', + provider: providerBySessionId.get(entrySessionId) ?? session.provider, messageUuid: entry.uuid ? String(entry.uuid) : null, }); } @@ -914,7 +916,7 @@ async function parseClaudeSessionMatches( fileResults.set(sessionId, { sessionId, - provider: 'claude', + provider: providerBySessionId.get(sessionId) ?? session.provider, sessionSummary: toSummaryText( customNameBySessionId.get(sessionId) ?? null, state.resolvedSummary || state.fallbackUserText || state.fallbackAssistantText, @@ -1054,7 +1056,7 @@ async function parseSessionMatches( session: SearchableSessionRow, runtime: SearchRuntime, ): Promise { - if (session.provider === 'claude') { + if (session.provider === 'claude' || session.provider === 'minimax') { return parseClaudeSessionMatches(session, runtime); } if (session.provider === 'codex') { @@ -1146,7 +1148,9 @@ export async function searchConversations( }; for (const [fileKey, sessions] of sessionsByPathKey.entries()) { - const claudeSessions = sessions.filter((session) => session.provider === 'claude'); + const claudeSessions = sessions.filter( + (session) => session.provider === 'claude' || session.provider === 'minimax', + ); if (claudeSessions.length > 0) { runtime.claudeSessionsByFileKey.set(fileKey, claudeSessions); } diff --git a/server/modules/providers/services/session-synchronizer.service.ts b/server/modules/providers/services/session-synchronizer.service.ts index 5bb2b8645f..760975d9c0 100644 --- a/server/modules/providers/services/session-synchronizer.service.ts +++ b/server/modules/providers/services/session-synchronizer.service.ts @@ -21,6 +21,7 @@ export const sessionSynchronizerService = { claude: 0, codex: 0, cursor: 0, + minimax: 0, opencode: 0, }; const failures: string[] = []; diff --git a/server/modules/providers/tests/mcp.test.ts b/server/modules/providers/tests/mcp.test.ts index 439966acd4..66fd4f769b 100644 --- a/server/modules/providers/tests/mcp.test.ts +++ b/server/modules/providers/tests/mcp.test.ts @@ -313,8 +313,9 @@ test('providerMcpService global adder writes to all providers and rejects unsupp workspacePath, }); - assert.equal(globalResult.length, 4); + assert.equal(globalResult.length, 5); assert.ok(globalResult.every((entry) => entry.created === true)); + assert.ok(globalResult.some((entry) => entry.provider === 'minimax')); const claudeProject = await readJson(path.join(workspacePath, '.mcp.json')); assert.ok((claudeProject.mcpServers as Record)['global-http']); @@ -346,4 +347,3 @@ test('providerMcpService global adder writes to all providers and rejects unsupp await fs.rm(tempRoot, { recursive: true, force: true }); } }); - diff --git a/server/modules/providers/tests/minimax-provider.test.ts b/server/modules/providers/tests/minimax-provider.test.ts new file mode 100644 index 0000000000..f08309a2c8 --- /dev/null +++ b/server/modules/providers/tests/minimax-provider.test.ts @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + MINIMAX_ANTHROPIC_ENDPOINTS, + buildMiniMaxRuntimeEnvironment, + resolveMiniMaxBaseUrl, +} from '@/modules/providers/list/minimax/minimax-config.js'; +import { + MINIMAX_MODEL_IDS, + MiniMaxProviderModels, + isMiniMaxModel, + resolveMiniMaxContextWindow, +} from '@/modules/providers/list/minimax/minimax-models.provider.js'; +import { providerRegistry } from '@/modules/providers/provider.registry.js'; + +test('MiniMax provider exposes the configured text models in priority order', async () => { + const models = await new MiniMaxProviderModels().getSupportedModels(); + + assert.deepEqual(models.OPTIONS.map((option) => option.value), [...MINIMAX_MODEL_IDS]); + assert.equal(models.DEFAULT, MINIMAX_MODEL_IDS[0]); + assert.equal(providerRegistry.resolveProvider('minimax').id, 'minimax'); +}); + +test('MiniMax endpoint resolution supports global, China, and explicit endpoints', () => { + assert.equal(resolveMiniMaxBaseUrl({}), MINIMAX_ANTHROPIC_ENDPOINTS.global); + assert.equal(resolveMiniMaxBaseUrl({ MINIMAX_REGION: 'cn' }), MINIMAX_ANTHROPIC_ENDPOINTS.cn); + assert.equal( + resolveMiniMaxBaseUrl({ MINIMAX_ANTHROPIC_BASE_URL: `${MINIMAX_ANTHROPIC_ENDPOINTS.global}/` }), + MINIMAX_ANTHROPIC_ENDPOINTS.global, + ); + assert.equal( + resolveMiniMaxBaseUrl({}, { ANTHROPIC_BASE_URL: `${MINIMAX_ANTHROPIC_ENDPOINTS.cn}/` }), + MINIMAX_ANTHROPIC_ENDPOINTS.cn, + ); +}); + +test('MiniMax runtime environment applies the selected model and context window', () => { + const contextWindow = resolveMiniMaxContextWindow(MINIMAX_MODEL_IDS[1]); + const environment = buildMiniMaxRuntimeEnvironment(MINIMAX_MODEL_IDS[1], contextWindow, {}); + + assert.equal(environment.ANTHROPIC_BASE_URL, MINIMAX_ANTHROPIC_ENDPOINTS.global); + assert.equal(environment.ANTHROPIC_MODEL, MINIMAX_MODEL_IDS[1]); + assert.equal(environment.CLAUDE_CODE_AUTO_COMPACT_WINDOW, '204800'); +}); + +test('MiniMax transcript model detection accepts the configured model variants', () => { + assert.equal(isMiniMaxModel(MINIMAX_MODEL_IDS[0]), true); + assert.equal(isMiniMaxModel(`${MINIMAX_MODEL_IDS[0]}[1m]`), true); + assert.equal(isMiniMaxModel('unrelated-model'), false); +}); diff --git a/server/routes/agent.js b/server/routes/agent.js index 1ae605b69e..dab6ce3f8a 100644 --- a/server/routes/agent.js +++ b/server/routes/agent.js @@ -7,6 +7,7 @@ import { promises as fs } from 'fs'; import crypto from 'crypto'; import { userDb, apiKeysDb, githubTokensDb, projectsDb } from '../modules/database/index.js'; import { queryClaudeSDK } from '../claude-sdk.js'; +import { queryMiniMaxSDK } from '../minimax-sdk.js'; import { spawnCursor } from '../cursor-cli.js'; import { queryCodex } from '../openai-codex.js'; import { spawnOpenCode } from '../opencode-cli.js'; @@ -636,7 +637,7 @@ class ResponseCollector { * - Source for auto-generated branch names (if createBranch=true and no branchName) * - Fallback for PR title if no commits are made * - * @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'opencode' + * @param {string} provider - Optional supported provider identifier. * Default: 'claude' * * @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates. @@ -759,7 +760,7 @@ class ResponseCollector { * Input Validations (400 Bad Request): * - Either githubUrl OR projectPath must be provided (not neither) * - message must be non-empty string - * - provider must be 'claude', 'cursor', 'codex', or 'opencode' + * - provider must be a supported provider identifier * - createBranch/createPR requires githubUrl OR projectPath (not neither) * - branchName must pass Git naming rules (if provided) * @@ -870,8 +871,8 @@ router.post('/', validateExternalApiKey, async (req, res) => { return res.status(400).json({ error: 'message is required' }); } - if (!['claude', 'cursor', 'codex', 'opencode'].includes(provider)) { - return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", or "opencode"' }); + if (!['claude', 'cursor', 'codex', 'minimax', 'opencode'].includes(provider)) { + return res.status(400).json({ error: 'provider is not supported' }); } // Validate GitHub branch/PR creation requirements @@ -950,6 +951,7 @@ router.post('/', validateExternalApiKey, async (req, res) => { } const codexModels = (await providerModelsService.getProviderModels('codex')).models; + const minimaxModels = (await providerModelsService.getProviderModels('minimax')).models; const opencodeModels = (await providerModelsService.getProviderModels('opencode')).models; // Start the appropriate session @@ -986,6 +988,16 @@ router.post('/', validateExternalApiKey, async (req, res) => { effort, permissionMode: 'bypassPermissions' }, writer); + } else if (provider === 'minimax') { + console.log('Starting MiniMax session'); + + await queryMiniMaxSDK(message.trim(), { + projectPath: finalProjectPath, + cwd: finalProjectPath, + sessionId: sessionId || null, + model: model || minimaxModels.DEFAULT, + permissionMode: 'bypassPermissions' + }, writer); } else if (provider === 'opencode') { console.log('Starting OpenCode CLI session'); diff --git a/server/routes/commands.js b/server/routes/commands.js index 722fd545bc..6592181f22 100644 --- a/server/routes/commands.js +++ b/server/routes/commands.js @@ -15,12 +15,13 @@ const APP_ROOT = findAppRoot(__dirname); const router = express.Router(); -const MODEL_PROVIDERS = ["claude", "cursor", "codex", "opencode"]; +const MODEL_PROVIDERS = ["claude", "cursor", "codex", "minimax", "opencode"]; const MODEL_PROVIDER_LABELS = { claude: "Claude", cursor: "Cursor", codex: "Codex", + minimax: "MiniMax", opencode: "OpenCode", }; diff --git a/server/shared/types.ts b/server/shared/types.ts index 45092ecf73..8aef73bb67 100644 --- a/server/shared/types.ts +++ b/server/shared/types.ts @@ -65,7 +65,7 @@ export type AuthenticatedWebSocketRequest = IncomingMessage & { * Use this as the source of truth whenever a function or payload needs to identify * a specific LLM integration. */ -export type LLMProvider = 'claude' | 'codex' | 'cursor' | 'opencode'; +export type LLMProvider = 'claude' | 'codex' | 'cursor' | 'minimax' | 'opencode'; /** * One selectable model row in a provider model catalog. diff --git a/src/components/chat/hooks/useChatComposerState.ts b/src/components/chat/hooks/useChatComposerState.ts index 7543ebae77..3e11dc9855 100644 --- a/src/components/chat/hooks/useChatComposerState.ts +++ b/src/components/chat/hooks/useChatComposerState.ts @@ -41,11 +41,8 @@ interface UseChatComposerStateArgs { permissionMode: PermissionMode | string; cyclePermissionMode: () => void; resolvePermissionModeForProvider: (provider: LLMProvider, requestedMode: PermissionMode | string) => PermissionMode; - cursorModel: string; - claudeModel: string; - codexModel: string; + providerModels: Record; currentProviderEffort: string; - opencodeModel: string; isLoading: boolean; canAbortSession: boolean; tokenBudget: Record | null; @@ -193,11 +190,8 @@ export function useChatComposerState({ permissionMode, cyclePermissionMode, resolvePermissionModeForProvider, - cursorModel, - claudeModel, - codexModel, + providerModels, currentProviderEffort, - opencodeModel, isLoading, canAbortSession, tokenBudget, @@ -369,13 +363,7 @@ export function useChatComposerState({ projectId: selectedProject.projectId, sessionId: currentSessionId, provider, - model: provider === 'cursor' - ? cursorModel - : provider === 'codex' - ? codexModel - : provider === 'opencode' - ? opencodeModel - : claudeModel, + model: providerModels[provider], tokenUsage: tokenBudget, }; @@ -424,15 +412,12 @@ export function useChatComposerState({ } }, [ - claudeModel, - codexModel, currentSessionId, - cursorModel, - opencodeModel, handleBuiltInCommand, handleCustomCommand, input, provider, + providerModels, selectedProject, addMessage, tokenBudget, @@ -595,6 +580,8 @@ export function useChatComposerState({ ? 'cursor-tools-settings' : provider === 'codex' ? 'codex-settings' + : provider === 'minimax' + ? 'claude-settings' : provider === 'opencode' ? 'opencode-settings' : 'claude-settings'; @@ -614,14 +601,7 @@ export function useChatComposerState({ }; const toolsSettings = getToolsSettings(); - const model = - provider === 'cursor' - ? cursorModel - : provider === 'codex' - ? codexModel - : provider === 'opencode' - ? opencodeModel - : claudeModel; + const model = providerModels[provider]; return { model, @@ -632,13 +612,10 @@ export function useChatComposerState({ sessionSummary: getNotificationSessionSummary(selectedSession, currentInput), }; }, [ - claudeModel, - codexModel, currentProviderEffort, - cursorModel, - opencodeModel, permissionMode, provider, + providerModels, resolvePermissionModeForProvider, selectedSession, ]); diff --git a/src/components/chat/hooks/useChatProviderState.ts b/src/components/chat/hooks/useChatProviderState.ts index 1d0aac5af5..e226efa20a 100644 --- a/src/components/chat/hooks/useChatProviderState.ts +++ b/src/components/chat/hooks/useChatProviderState.ts @@ -20,10 +20,11 @@ const FALLBACK_DEFAULT_MODEL: Record = { claude: 'default', cursor: 'gpt-5.3-codex', codex: 'gpt-5.4', + minimax: 'MiniMax-M3', opencode: 'anthropic/claude-sonnet-4-5', }; -const PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'opencode']; +const PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'minimax', 'opencode']; const readStoredProvider = (): LLMProvider => { const storedProvider = localStorage.getItem('selected-provider'); @@ -42,6 +43,7 @@ const FALLBACK_PERMISSION_MODES: Record = { claude: ['default', 'auto', 'acceptEdits', 'bypassPermissions', 'plan'], cursor: ['default', 'acceptEdits', 'bypassPermissions', 'plan'], codex: ['default', 'acceptEdits', 'bypassPermissions'], + minimax: ['default', 'auto', 'acceptEdits', 'bypassPermissions', 'plan'], opencode: ['default', 'acceptEdits', 'bypassPermissions', 'plan'], }; @@ -100,6 +102,9 @@ export function useChatProviderState({ selectedSession, selectedProject: _select const [codexModel, setCodexModel] = useState(() => { return localStorage.getItem('codex-model') || FALLBACK_DEFAULT_MODEL.codex; }); + const [minimaxModel, setMiniMaxModel] = useState(() => { + return localStorage.getItem('minimax-model') || FALLBACK_DEFAULT_MODEL.minimax; + }); const [providerEfforts, setProviderEfforts] = useState>>(() => { return PROVIDERS.reduce>>((acc, targetProvider) => { acc[targetProvider] = localStorage.getItem(`${targetProvider}-effort`) || DEFAULT_EFFORT_VALUE; @@ -151,6 +156,12 @@ export function useChatProviderState({ selectedSession, selectedProject: _select return; } + if (targetProvider === 'minimax') { + setMiniMaxModel(model); + localStorage.setItem('minimax-model', model); + return; + } + setOpenCodeModel(model); localStorage.setItem('opencode-model', model); }, []); @@ -354,8 +365,9 @@ export function useChatProviderState({ selectedSession, selectedProject: _select claude: claudeModel, cursor: cursorModel, codex: codexModel, + minimax: minimaxModel, opencode: opencodeModel, - }), [claudeModel, cursorModel, codexModel, opencodeModel]); + }), [claudeModel, cursorModel, codexModel, minimaxModel, opencodeModel]); useEffect(() => { const claude = providerModelCatalog.claude; @@ -396,6 +408,19 @@ export function useChatProviderState({ selectedSession, selectedProject: _select } }, [providerModelCatalog.codex, codexModel]); + useEffect(() => { + const minimax = providerModelCatalog.minimax; + if (minimax) { + const next = pickStoredOrCurrent('minimax-model', minimaxModel, minimax); + if (next !== minimaxModel) { + setMiniMaxModel(next); + } + if (localStorage.getItem('minimax-model') !== next) { + localStorage.setItem('minimax-model', next); + } + } + }, [providerModelCatalog.minimax, minimaxModel]); + useEffect(() => { const opencode = providerModelCatalog.opencode; if (opencode) { @@ -567,10 +592,14 @@ export function useChatProviderState({ selectedSession, selectedProject: _select setClaudeModel, codexModel, setCodexModel, + minimaxModel, + setMiniMaxModel, currentProviderEffort, currentProviderEffortOptions, opencodeModel, setOpenCodeModel, + providerModels, + setStoredProviderModel, permissionMode, setPermissionMode, pendingPermissionRequests, diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx index 2d7a8dcbb7..33f62c1de2 100644 --- a/src/components/chat/view/ChatInterface.tsx +++ b/src/components/chat/view/ChatInterface.tsx @@ -63,16 +63,10 @@ function ChatInterface({ const { provider, setProvider, - cursorModel, - setCursorModel, - claudeModel, - setClaudeModel, - codexModel, - setCodexModel, + providerModels, + setStoredProviderModel, currentProviderEffort, currentProviderEffortOptions, - opencodeModel, - setOpenCodeModel, permissionMode, pendingPermissionRequests, setPendingPermissionRequests, @@ -198,11 +192,8 @@ function ChatInterface({ provider, permissionMode, cyclePermissionMode, - cursorModel, - claudeModel, - codexModel, + providerModels, currentProviderEffort, - opencodeModel, isLoading: isProcessing, canAbortSession, tokenBudget, @@ -299,6 +290,8 @@ function ChatInterface({ ? t('messageTypes.codex') : provider === 'opencode' ? t('messageTypes.opencode', { defaultValue: 'OpenCode' }) + : provider === 'minimax' + ? t('messageTypes.minimax', { defaultValue: 'MiniMax' }) : t('messageTypes.claude'); return ( @@ -331,14 +324,8 @@ function ChatInterface({ provider={provider} setProvider={(nextProvider) => setProvider(nextProvider as Provider)} textareaRef={textareaRef} - claudeModel={claudeModel} - setClaudeModel={setClaudeModel} - cursorModel={cursorModel} - setCursorModel={setCursorModel} - codexModel={codexModel} - setCodexModel={setCodexModel} - opencodeModel={opencodeModel} - setOpenCodeModel={setOpenCodeModel} + providerModels={providerModels} + setProviderModel={setStoredProviderModel} providerModelCatalog={providerModelCatalog} providerModelsLoading={providerModelsLoading} tasksEnabled={tasksEnabled} @@ -446,6 +433,8 @@ function ChatInterface({ ? t('messageTypes.codex') : provider === 'opencode' ? t('messageTypes.opencode', { defaultValue: 'OpenCode' }) + : provider === 'minimax' + ? t('messageTypes.minimax', { defaultValue: 'MiniMax' }) : t('messageTypes.claude'), })} isTextareaExpanded={isTextareaExpanded} diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx index 23ffaef34e..e0f4dba4fc 100644 --- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx +++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx @@ -32,14 +32,8 @@ interface ChatMessagesPaneProps { provider: LLMProvider; setProvider: (provider: LLMProvider) => void; textareaRef: RefObject; - claudeModel: string; - setClaudeModel: (model: string) => void; - cursorModel: string; - setCursorModel: (model: string) => void; - codexModel: string; - setCodexModel: (model: string) => void; - opencodeModel: string; - setOpenCodeModel: (model: string) => void; + providerModels: Record; + setProviderModel: (provider: LLMProvider, model: string) => void; providerModelCatalog: Partial>; providerModelsLoading: boolean; tasksEnabled: boolean; @@ -80,14 +74,8 @@ function ChatMessagesPane({ provider, setProvider, textareaRef, - claudeModel, - setClaudeModel, - cursorModel, - setCursorModel, - codexModel, - setCodexModel, - opencodeModel, - setOpenCodeModel, + providerModels, + setProviderModel, providerModelCatalog, providerModelsLoading, tasksEnabled, @@ -179,14 +167,8 @@ function ChatMessagesPane({ provider={provider} setProvider={setProvider} textareaRef={textareaRef} - claudeModel={claudeModel} - setClaudeModel={setClaudeModel} - cursorModel={cursorModel} - setCursorModel={setCursorModel} - codexModel={codexModel} - setCodexModel={setCodexModel} - opencodeModel={opencodeModel} - setOpenCodeModel={setOpenCodeModel} + providerModels={providerModels} + setProviderModel={setProviderModel} providerModelCatalog={providerModelCatalog} providerModelsLoading={providerModelsLoading} tasksEnabled={tasksEnabled} diff --git a/src/components/chat/view/subcomponents/CommandResultModal.tsx b/src/components/chat/view/subcomponents/CommandResultModal.tsx index 5096734db6..84b0adbc59 100644 --- a/src/components/chat/view/subcomponents/CommandResultModal.tsx +++ b/src/components/chat/view/subcomponents/CommandResultModal.tsx @@ -61,6 +61,7 @@ const PROVIDER_LABELS: Record = { claude: 'Claude', cursor: 'Cursor', codex: 'Codex', + minimax: 'MiniMax', opencode: 'OpenCode', }; diff --git a/src/components/chat/view/subcomponents/MessageComponent.tsx b/src/components/chat/view/subcomponents/MessageComponent.tsx index 61cf8ee37b..c09332b6f3 100644 --- a/src/components/chat/view/subcomponents/MessageComponent.tsx +++ b/src/components/chat/view/subcomponents/MessageComponent.tsx @@ -154,6 +154,8 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, s ? t('messageTypes.cursor') : provider === 'codex' ? t('messageTypes.codex') + : provider === 'minimax' + ? t('messageTypes.minimax', { defaultValue: 'MiniMax' }) : provider === 'opencode' ? t('messageTypes.opencode', { defaultValue: 'OpenCode' }) : t('messageTypes.claude'))} @@ -401,4 +403,3 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, s }); export default MessageComponent; - diff --git a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx index 6772ae09b9..3b72e91fae 100644 --- a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx +++ b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx @@ -27,6 +27,7 @@ const PROVIDER_META: { id: LLMProvider; name: string }[] = [ { id: "claude", name: "Anthropic" }, { id: "codex", name: "OpenAI" }, { id: "cursor", name: "Cursor" }, + { id: "minimax", name: "MiniMax" }, { id: "opencode", name: "OpenCode" }, ]; @@ -50,14 +51,8 @@ type ProviderSelectionEmptyStateProps = { provider: LLMProvider; setProvider: (next: LLMProvider) => void; textareaRef: React.RefObject; - claudeModel: string; - setClaudeModel: (model: string) => void; - cursorModel: string; - setCursorModel: (model: string) => void; - codexModel: string; - setCodexModel: (model: string) => void; - opencodeModel: string; - setOpenCodeModel: (model: string) => void; + providerModels: Record; + setProviderModel: (provider: LLMProvider, model: string) => void; providerModelCatalog: Partial>; providerModelsLoading: boolean; tasksEnabled: boolean; @@ -80,23 +75,11 @@ function getModelConfig( return entry ?? { OPTIONS: [], DEFAULT: "" }; } -function getCurrentModel( - p: LLMProvider, - c: string, - cu: string, - co: string, - o: string, -) { - if (p === "claude") return c; - if (p === "codex") return co; - if (p === "opencode") return o; - return cu; -} - function getProviderDisplayName(p: LLMProvider) { if (p === "claude") return "Claude"; if (p === "cursor") return "Cursor"; if (p === "codex") return "Codex"; + if (p === "minimax") return "MiniMax"; if (p === "opencode") return "OpenCode"; return "Claude"; } @@ -107,14 +90,8 @@ export default function ProviderSelectionEmptyState({ provider, setProvider, textareaRef, - claudeModel, - setClaudeModel, - cursorModel, - setCursorModel, - codexModel, - setCodexModel, - opencodeModel, - setOpenCodeModel, + providerModels, + setProviderModel, providerModelCatalog, providerModelsLoading, tasksEnabled, @@ -137,13 +114,7 @@ export default function ProviderSelectionEmptyState({ defaultValue: "Start the next task", }); - const currentModel = getCurrentModel( - provider, - claudeModel, - cursorModel, - codexModel, - opencodeModel, - ); + const currentModel = providerModels[provider]; const currentModelLabel = useMemo(() => { const config = getModelConfig(provider, providerModelCatalog); @@ -155,21 +126,9 @@ export default function ProviderSelectionEmptyState({ const setModelForProvider = useCallback( (providerId: LLMProvider, modelValue: string) => { - if (providerId === "claude") { - setClaudeModel(modelValue); - localStorage.setItem("claude-model", modelValue); - } else if (providerId === "codex") { - setCodexModel(modelValue); - localStorage.setItem("codex-model", modelValue); - } else if (providerId === "opencode") { - setOpenCodeModel(modelValue); - localStorage.setItem("opencode-model", modelValue); - } else { - setCursorModel(modelValue); - localStorage.setItem("cursor-model", modelValue); - } + setProviderModel(providerId, modelValue); }, - [setClaudeModel, setCursorModel, setCodexModel, setOpenCodeModel], + [setProviderModel], ); const handleModelSelect = useCallback( @@ -304,16 +263,20 @@ export default function ProviderSelectionEmptyState({ { { claude: t("providerSelection.readyPrompt.claude", { - model: claudeModel, + model: providerModels.claude, }), cursor: t("providerSelection.readyPrompt.cursor", { - model: cursorModel, + model: providerModels.cursor, }), codex: t("providerSelection.readyPrompt.codex", { - model: codexModel, + model: providerModels.codex, + }), + minimax: t("providerSelection.readyPrompt.minimax", { + model: providerModels.minimax, + defaultValue: "Ready with MiniMax {{model}}", }), opencode: t("providerSelection.readyPrompt.opencode", { - model: opencodeModel, + model: providerModels.opencode, defaultValue: "Ready with OpenCode {{model}}", }), }[provider] diff --git a/src/components/llm-logo-provider/MiniMaxLogo.tsx b/src/components/llm-logo-provider/MiniMaxLogo.tsx new file mode 100644 index 0000000000..9c10f63c89 --- /dev/null +++ b/src/components/llm-logo-provider/MiniMaxLogo.tsx @@ -0,0 +1,25 @@ +type MiniMaxLogoProps = { + className?: string; +}; + +const MiniMaxLogo = ({ className = 'w-5 h-5' }: MiniMaxLogoProps) => ( + + + + +); + +export default MiniMaxLogo; diff --git a/src/components/llm-logo-provider/SessionProviderLogo.tsx b/src/components/llm-logo-provider/SessionProviderLogo.tsx index b93d9eb56e..cc916c4501 100644 --- a/src/components/llm-logo-provider/SessionProviderLogo.tsx +++ b/src/components/llm-logo-provider/SessionProviderLogo.tsx @@ -2,6 +2,7 @@ import type { LLMProvider } from '../../types/app'; import ClaudeLogo from './ClaudeLogo'; import CodexLogo from './CodexLogo'; import CursorLogo from './CursorLogo'; +import MiniMaxLogo from './MiniMaxLogo'; import OpenCodeLogo from './OpenCodeLogo'; type SessionProviderLogoProps = { @@ -25,5 +26,9 @@ export default function SessionProviderLogo({ return ; } + if (provider === 'minimax') { + return ; + } + return ; } diff --git a/src/components/mcp/constants.ts b/src/components/mcp/constants.ts index 5a1848d6f2..4a4713e779 100644 --- a/src/components/mcp/constants.ts +++ b/src/components/mcp/constants.ts @@ -4,6 +4,7 @@ export const MCP_PROVIDER_NAMES: Record = { claude: 'Claude', cursor: 'Cursor', codex: 'Codex', + minimax: 'MiniMax', opencode: 'OpenCode', }; @@ -11,6 +12,7 @@ export const MCP_SUPPORTED_SCOPES: Record = { claude: ['user', 'project', 'local'], cursor: ['user', 'project'], codex: ['user', 'project'], + minimax: ['user', 'project', 'local'], opencode: ['user', 'project'], }; @@ -18,6 +20,7 @@ export const MCP_SUPPORTED_TRANSPORTS: Record = { claude: ['stdio', 'http', 'sse'], cursor: ['stdio', 'http'], codex: ['stdio', 'http'], + minimax: ['stdio', 'http', 'sse'], opencode: ['stdio', 'http'], }; @@ -29,6 +32,7 @@ export const MCP_PROVIDER_BUTTON_CLASSES: Record = { claude: 'bg-primary text-primary-foreground hover:bg-primary/90', cursor: 'bg-primary text-primary-foreground hover:bg-primary/90', codex: 'bg-primary text-primary-foreground hover:bg-primary/90', + minimax: 'bg-primary text-primary-foreground hover:bg-primary/90', opencode: 'bg-primary text-primary-foreground hover:bg-primary/90', }; @@ -36,6 +40,7 @@ export const MCP_SUPPORTS_WORKING_DIRECTORY: Record = { claude: false, cursor: false, codex: true, + minimax: false, opencode: false, }; diff --git a/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx index d9148a9559..0a8f771714 100644 --- a/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx +++ b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx @@ -30,6 +30,13 @@ const providerCards = [ iconContainerClassName: 'bg-gray-100 dark:bg-gray-800', loginButtonClassName: 'bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600', }, + { + provider: 'minimax' as const, + title: 'MiniMax', + connectedClassName: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800', + iconContainerClassName: 'bg-red-100 dark:bg-red-900/30', + loginButtonClassName: 'bg-red-600 hover:bg-red-700', + }, { provider: 'opencode' as const, title: 'OpenCode', diff --git a/src/components/provider-auth/types.ts b/src/components/provider-auth/types.ts index 5511c1848f..44e5714109 100644 --- a/src/components/provider-auth/types.ts +++ b/src/components/provider-auth/types.ts @@ -10,12 +10,13 @@ export type ProviderAuthStatus = { export type ProviderAuthStatusMap = Record; -export const CLI_PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'opencode']; +export const CLI_PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'minimax', 'opencode']; export const PROVIDER_AUTH_STATUS_ENDPOINTS: Record = { claude: '/api/providers/claude/auth/status', cursor: '/api/providers/cursor/auth/status', codex: '/api/providers/codex/auth/status', + minimax: '/api/providers/minimax/auth/status', opencode: '/api/providers/opencode/auth/status', }; @@ -23,5 +24,6 @@ export const createInitialProviderAuthStatusMap = (loading = true): ProviderAuth claude: { authenticated: false, email: null, method: null, error: null, loading }, cursor: { authenticated: false, email: null, method: null, error: null, loading }, codex: { authenticated: false, email: null, method: null, error: null, loading }, + minimax: { authenticated: false, email: null, method: null, error: null, loading }, opencode: { authenticated: false, email: null, method: null, error: null, loading }, }); diff --git a/src/components/provider-auth/view/ProviderLoginModal.tsx b/src/components/provider-auth/view/ProviderLoginModal.tsx index 2a0cfc2ae0..386352d14d 100644 --- a/src/components/provider-auth/view/ProviderLoginModal.tsx +++ b/src/components/provider-auth/view/ProviderLoginModal.tsx @@ -41,6 +41,10 @@ const getProviderCommand = ({ return 'opencode auth login'; } + if (provider === 'minimax') { + return 'claude --version'; + } + return 'claude --dangerously-skip-permissions /login'; }; @@ -49,6 +53,7 @@ const getProviderTitle = (provider: LLMProvider) => { if (provider === 'cursor') return 'Cursor CLI Login'; if (provider === 'codex') return 'Codex CLI Login'; if (provider === 'opencode') return 'OpenCode CLI Login'; + if (provider === 'minimax') return 'MiniMax Configuration'; return 'Claude CLI Login'; }; diff --git a/src/components/settings/constants/constants.ts b/src/components/settings/constants/constants.ts index 8c429083df..dc73f0078c 100644 --- a/src/components/settings/constants/constants.ts +++ b/src/components/settings/constants/constants.ts @@ -39,7 +39,7 @@ export const SETTINGS_MAIN_TABS: SettingsMainTabMeta[] = [ { id: 'about', label: 'About', keywords: 'about version info', icon: Info }, ]; -export const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'opencode']; +export const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'minimax', 'opencode']; export const AGENT_CATEGORIES: AgentCategory[] = ['account', 'permissions', 'mcp']; export const DEFAULT_PROJECT_SORT_ORDER: ProjectSortOrder = 'name'; diff --git a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx index 52e87c3272..688ad72812 100644 --- a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx +++ b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx @@ -12,7 +12,7 @@ type AgentListItemProps = { type AgentConfig = { name: string; - color: 'blue' | 'purple' | 'gray' | 'zinc'; + color: 'blue' | 'purple' | 'gray' | 'red' | 'zinc'; }; const agentConfig: Record = { @@ -28,6 +28,10 @@ const agentConfig: Record = { name: 'Codex', color: 'gray', }, + minimax: { + name: 'MiniMax', + color: 'red', + }, opencode: { name: 'OpenCode', color: 'zinc', @@ -44,6 +48,9 @@ const colorClasses = { gray: { dot: 'bg-foreground/60', }, + red: { + dot: 'bg-red-500', + }, zinc: { dot: 'bg-zinc-500', }, diff --git a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx index 31216a103b..33c67e3b6a 100644 --- a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx +++ b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx @@ -27,7 +27,7 @@ export default function AgentsSettingsTab({ ), [selectedAgent]); const visibleAgents = useMemo(() => { - return ['claude', 'cursor', 'codex', 'opencode']; + return ['claude', 'cursor', 'codex', 'minimax', 'opencode']; }, []); const agentContextById = useMemo>(() => ({ @@ -43,6 +43,10 @@ export default function AgentsSettingsTab({ authStatus: providerAuthStatus.codex, onLogin: () => onProviderLogin('codex'), }, + minimax: { + authStatus: providerAuthStatus.minimax, + onLogin: () => onProviderLogin('minimax'), + }, opencode: { authStatus: providerAuthStatus.opencode, onLogin: () => onProviderLogin('opencode'), @@ -52,6 +56,7 @@ export default function AgentsSettingsTab({ providerAuthStatus.claude, providerAuthStatus.codex, providerAuthStatus.cursor, + providerAuthStatus.minimax, providerAuthStatus.opencode, ]); diff --git a/src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx b/src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx index 5bf0de2a0b..7460e88b97 100644 --- a/src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx +++ b/src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx @@ -29,7 +29,7 @@ export default function AgentCategoryContentSection({ /> )} - {selectedCategory === 'permissions' && selectedAgent === 'claude' && ( + {selectedCategory === 'permissions' && (selectedAgent === 'claude' || selectedAgent === 'minimax') && ( = { claude: 'Claude', cursor: 'Cursor', codex: 'Codex', + minimax: 'MiniMax', opencode: 'OpenCode', }; @@ -23,6 +24,7 @@ export default function AgentSelectorSection({ const dotColor = agent === 'claude' ? 'bg-blue-500' : agent === 'cursor' ? 'bg-purple-500' : + agent === 'minimax' ? 'bg-red-500' : agent === 'opencode' ? 'bg-zinc-500' : 'bg-foreground/60'; return ( diff --git a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx index 83fbb24910..af4415598e 100644 --- a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx +++ b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx @@ -45,6 +45,15 @@ const agentConfig: Record = { subtextClass: 'text-gray-700 dark:text-gray-300', buttonClass: 'bg-gray-800 hover:bg-gray-900 active:bg-gray-950 dark:bg-gray-700 dark:hover:bg-gray-600 dark:active:bg-gray-500', }, + minimax: { + name: 'MiniMax', + description: 'MiniMax coding assistant', + bgClass: 'bg-red-50 dark:bg-red-900/20', + borderClass: 'border-red-200 dark:border-red-800', + textClass: 'text-red-900 dark:text-red-100', + subtextClass: 'text-red-700 dark:text-red-300', + buttonClass: 'bg-red-600 hover:bg-red-700 active:bg-red-800', + }, opencode: { name: 'OpenCode', description: 'OpenCode CLI assistant', diff --git a/src/components/skills/view/ProviderSkills.tsx b/src/components/skills/view/ProviderSkills.tsx index 908ed07625..abf89e9bee 100644 --- a/src/components/skills/view/ProviderSkills.tsx +++ b/src/components/skills/view/ProviderSkills.tsx @@ -59,6 +59,7 @@ const PROVIDER_NAMES: Record = { claude: 'Claude', codex: 'Codex', cursor: 'Cursor', + minimax: 'MiniMax', opencode: 'OpenCode', }; @@ -66,6 +67,7 @@ const PROVIDER_SKILL_PATHS: Record, string> claude: '~/.claude/skills//SKILL.md', codex: '~/.agents/skills//SKILL.md', cursor: '~/.cursor/skills//SKILL.md', + minimax: '~/.claude/skills//SKILL.md', }; const SCOPE_LABELS: Record = { diff --git a/src/types/app.ts b/src/types/app.ts index 8fa4a68430..1c0d908585 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -1,4 +1,4 @@ -export type LLMProvider = 'claude' | 'cursor' | 'codex' | 'opencode'; +export type LLMProvider = 'claude' | 'cursor' | 'codex' | 'minimax' | 'opencode'; export type ProviderModelOption = { value: string;
provider string Optionalclaude, cursor, codex, or opencode (default: claude)Supported provider identifier, including minimax (default: claude)
stream