From 29a41e50857654bd695be4be71d9ada3e38ab80a Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Tue, 9 Dec 2025 09:25:59 +0800 Subject: [PATCH 1/2] feat: Add support for creating new model configs in ModelConfigDialog - Add 'Add New Config' option in ModelSelector for UI, IDEA, and VSCode versions - Support new config mode when currentConfigName is null - Add i18n strings for 'addNewConfig' and 'add' in English and Chinese - Create ModelConfigDialog component for VSCode webview - Update IdeaAgentViewModel to support new config mode - Add onAddNewConfig callback throughout the component hierarchy This allows users to create new model configurations instead of only being able to edit existing ones. --- .../devins/idea/editor/IdeaModelSelector.kt | 28 +- .../devins/idea/editor/SwingBottomToolbar.kt | 15 ++ .../devins/idea/toolwindow/IdeaAgentApp.kt | 15 +- .../idea/toolwindow/IdeaAgentViewModel.kt | 18 ++ .../idea/toolwindow/IdeaDevInInputArea.kt | 11 + .../ui/i18n/AutoDevStrings_en.properties | 2 + .../ui/i18n/AutoDevStrings_zh.properties | 2 + .../devins/ui/compose/editor/ModelSelector.kt | 30 ++- .../kotlin/cc/unitmesh/devins/ui/i18n/I18n.kt | 2 + mpp-vscode/webview/src/App.tsx | 35 ++- .../webview/src/components/ChatInput.tsx | 3 + .../src/components/ModelConfigDialog.css | 167 ++++++++++++ .../src/components/ModelConfigDialog.tsx | 240 ++++++++++++++++++ .../webview/src/components/ModelSelector.tsx | 22 +- 14 files changed, 578 insertions(+), 12 deletions(-) create mode 100644 mpp-vscode/webview/src/components/ModelConfigDialog.css create mode 100644 mpp-vscode/webview/src/components/ModelConfigDialog.tsx diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelSelector.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelSelector.kt index 1c54a7d10f..05b41cd2df 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelSelector.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelSelector.kt @@ -35,6 +35,7 @@ fun IdeaModelSelector( currentConfigName: String?, onConfigSelect: (NamedModelConfig) -> Unit, onConfigureClick: () -> Unit, + onAddNewConfig: () -> Unit, modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } @@ -131,7 +132,32 @@ fun IdeaModelSelector( separator() } - // Configure button + // Add New Config button + selectableItem( + selected = false, + onClick = { + onAddNewConfig() + expanded = false + } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = IdeaComposeIcons.Add, + contentDescription = null, + tint = JewelTheme.globalColors.text.normal, + modifier = Modifier.size(16.dp) + ) + Text( + text = "Add New Config", + style = JewelTheme.defaultTextStyle.copy(fontSize = 13.sp) + ) + } + } + + // Configure button (edit current config) selectableItem( selected = false, onClick = { diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/SwingBottomToolbar.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/SwingBottomToolbar.kt index caa55a89a9..887263c60d 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/SwingBottomToolbar.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/SwingBottomToolbar.kt @@ -32,6 +32,7 @@ class SwingBottomToolbar( private var availableConfigs: List = emptyList() private var onConfigSelect: (NamedModelConfig) -> Unit = {} private var onConfigureClick: () -> Unit = {} + private var onAddNewConfig: () -> Unit = {} private var isProcessing = false private var isEnhancing = false @@ -52,6 +53,16 @@ class SwingBottomToolbar( } add(modelComboBox) + // Add New Config button + val addConfigButton = JButton(AllIcons.General.Add).apply { + toolTipText = "Add New Config" + preferredSize = Dimension(28, 28) + isBorderPainted = false + isContentAreaFilled = false + addActionListener { onAddNewConfig() } + } + add(addConfigButton) + tokenLabel.foreground = JBUI.CurrentTheme.Label.disabledForeground() add(tokenLabel) } @@ -138,5 +149,9 @@ class SwingBottomToolbar( fun setOnConfigureClick(callback: () -> Unit) { onConfigureClick = callback } + + fun setOnAddNewConfig(callback: () -> Unit) { + onAddNewConfig = callback + } } diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt index d38a6f408f..b50c9407b1 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt @@ -65,6 +65,7 @@ fun IdeaAgentApp( var isExecuting by remember { mutableStateOf(false) } var currentPlan by remember { mutableStateOf(null) } var showConfigDialog by remember { mutableStateOf(false) } + var isNewConfig by remember { mutableStateOf(false) } var mcpPreloadingMessage by remember { mutableStateOf("") } var configWrapper by remember { mutableStateOf(null) } var currentModelConfig by remember { mutableStateOf(null) } @@ -89,6 +90,9 @@ fun IdeaAgentApp( IdeaLaunchedEffect(viewModel, project = project) { viewModel.showConfigDialog.collect { showConfigDialog = it } } + IdeaLaunchedEffect(viewModel, project = project) { + viewModel.isNewConfig.collect { isNewConfig = it } + } IdeaLaunchedEffect(viewModel, project = project) { viewModel.mcpPreloadingMessage.collect { mcpPreloadingMessage = it } } @@ -217,7 +221,8 @@ fun IdeaAgentApp( viewModel.setActiveConfig(config.name) }, currentPlan = currentPlan, - onConfigureClick = { viewModel.setShowConfigDialog(true) } + onConfigureClick = { viewModel.showEditConfigDialog() }, + onAddNewConfig = { viewModel.showAddNewConfigDialog() } ) } ) @@ -266,7 +271,8 @@ fun IdeaAgentApp( onConfigSelect = { config -> viewModel.setActiveConfig(config.name) }, - onConfigureClick = { viewModel.setShowConfigDialog(true) } + onConfigureClick = { viewModel.showEditConfigDialog() }, + onAddNewConfig = { viewModel.showAddNewConfigDialog() } ) } ) @@ -304,12 +310,13 @@ fun IdeaAgentApp( // DialogWrapper must be created on EDT, so we use invokeLater DisposableEffect(showConfigDialog) { if (showConfigDialog) { - val dialogConfig = currentModelConfig ?: ModelConfig() + val dialogConfig = if (isNewConfig) ModelConfig() else (currentModelConfig ?: ModelConfig()) + val dialogConfigName = if (isNewConfig) null else currentConfigName com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater { IdeaModelConfigDialogWrapper.show( project = project, currentConfig = dialogConfig, - currentConfigName = currentConfigName, + currentConfigName = dialogConfigName, onSave = { configName, newModelConfig -> // If creating a new config (not editing current), ensure unique name val existingNames = availableConfigs.map { it.name } diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt index c4635a92c3..222bc1e5ff 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt @@ -81,6 +81,8 @@ class IdeaAgentViewModel( // Show config dialog private val _showConfigDialog = MutableStateFlow(false) val showConfigDialog: StateFlow = _showConfigDialog.asStateFlow() + private val _isNewConfig = MutableStateFlow(false) + val isNewConfig: StateFlow = _isNewConfig.asStateFlow() // Current execution job (for cancellation) private var currentJob: Job? = null @@ -587,6 +589,22 @@ class IdeaAgentViewModel( _showConfigDialog.value = show } + /** + * Show config dialog for adding a new config. + */ + fun showAddNewConfigDialog() { + _isNewConfig.value = true + _showConfigDialog.value = true + } + + /** + * Show config dialog for editing current config. + */ + fun showEditConfigDialog() { + _isNewConfig.value = false + _showConfigDialog.value = true + } + /** * Set the active configuration by name */ diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaDevInInputArea.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaDevInInputArea.kt index 1f8482969d..1b76238025 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaDevInInputArea.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaDevInInputArea.kt @@ -80,6 +80,7 @@ fun IdeaDevInInputArea( currentConfigName: String? = null, onConfigSelect: (NamedModelConfig) -> Unit = {}, onConfigureClick: () -> Unit = {}, + onAddNewConfig: () -> Unit = {}, currentPlan: AgentPlan? = null ) { val scope = rememberIdeaCoroutineScope(project) @@ -116,6 +117,11 @@ fun IdeaDevInInputArea( onDispose { } } + DisposableEffect(onAddNewConfig) { + swingInputArea?.setOnAddNewConfig(onAddNewConfig) + onDispose { } + } + DisposableEffect(currentPlan) { swingInputArea?.setCurrentPlan(currentPlan) onDispose { } @@ -140,6 +146,7 @@ fun IdeaDevInInputArea( it.setCurrentConfigName(currentConfigName) it.setOnConfigSelect(onConfigSelect) it.setOnConfigureClick(onConfigureClick) + it.setOnAddNewConfig(onAddNewConfig) it.setCurrentPlan(currentPlan) } }, @@ -327,6 +334,10 @@ class SwingDevInInputArea( bottomToolbar.setOnConfigureClick(callback) } + fun setOnAddNewConfig(callback: () -> Unit) { + bottomToolbar.setOnAddNewConfig(callback) + } + fun setCurrentPlan(plan: AgentPlan?) { currentPlan = plan // TODO: Add plan summary bar support diff --git a/mpp-ui/src/commonMain/i18n/cc/unitmesh/devins/ui/i18n/AutoDevStrings_en.properties b/mpp-ui/src/commonMain/i18n/cc/unitmesh/devins/ui/i18n/AutoDevStrings_en.properties index f77bf0ce99..495d7044c0 100644 --- a/mpp-ui/src/commonMain/i18n/cc/unitmesh/devins/ui/i18n/AutoDevStrings_en.properties +++ b/mpp-ui/src/commonMain/i18n/cc/unitmesh/devins/ui/i18n/AutoDevStrings_en.properties @@ -7,6 +7,7 @@ common.back=Back common.confirm=Confirm common.configure=Configure common.loading=Loading +common.add=Add # Chat UI chat.title=AutoDev @@ -25,6 +26,7 @@ modelConfig.temperature=Temperature modelConfig.maxTokens=Max Tokens modelConfig.advancedParameters=Advanced Parameters modelConfig.configureModel=Configure Model +modelConfig.addNewConfig=Add New Config modelConfig.noSavedConfigs=No saved configurations modelConfig.enterModel=Enter or select model name modelConfig.modelHint=Select from list or type custom model name diff --git a/mpp-ui/src/commonMain/i18n/cc/unitmesh/devins/ui/i18n/AutoDevStrings_zh.properties b/mpp-ui/src/commonMain/i18n/cc/unitmesh/devins/ui/i18n/AutoDevStrings_zh.properties index e7bf457831..6a7c0eb3cb 100644 --- a/mpp-ui/src/commonMain/i18n/cc/unitmesh/devins/ui/i18n/AutoDevStrings_zh.properties +++ b/mpp-ui/src/commonMain/i18n/cc/unitmesh/devins/ui/i18n/AutoDevStrings_zh.properties @@ -7,6 +7,7 @@ common.back=返回 common.confirm=确认 common.configure=配置 common.loading=加载中 +common.add=添加 # Chat UI chat.title=AutoDev @@ -25,6 +26,7 @@ modelConfig.temperature=温度 modelConfig.maxTokens=最大令牌数 modelConfig.advancedParameters=高级参数 modelConfig.configureModel=配置模型... +modelConfig.addNewConfig=新增配置 modelConfig.noSavedConfigs=没有保存的配置 modelConfig.enterModel=输入或选择模型名称 modelConfig.modelHint=从列表中选择或输入自定义模型名称 diff --git a/mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/ModelSelector.kt b/mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/ModelSelector.kt index 1cd7571616..be6de05eb6 100644 --- a/mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/ModelSelector.kt +++ b/mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/ModelSelector.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.launch fun ModelSelector(onConfigChange: (ModelConfig) -> Unit = {}) { var expanded by remember { mutableStateOf(false) } var showConfigDialog by remember { mutableStateOf(false) } + var isNewConfig by remember { mutableStateOf(false) } var availableConfigs by remember { mutableStateOf>(emptyList()) } var currentConfigName by remember { mutableStateOf(null) } @@ -135,10 +136,28 @@ fun ModelSelector(onConfigChange: (ModelConfig) -> Unit = {}) { HorizontalDivider() } - // Configure button + // Add New Config button + DropdownMenuItem( + text = { Text(Strings.addNewConfig) }, + onClick = { + isNewConfig = true + showConfigDialog = true + expanded = false + }, + leadingIcon = { + Icon( + imageVector = AutoDevComposeIcons.Add, + contentDescription = Strings.add, + modifier = Modifier.size(18.dp) + ) + } + ) + + // Configure button (edit current config) DropdownMenuItem( text = { Text(Strings.configureModel) }, onClick = { + isNewConfig = false showConfigDialog = true expanded = false }, @@ -154,9 +173,12 @@ fun ModelSelector(onConfigChange: (ModelConfig) -> Unit = {}) { if (showConfigDialog) { ModelConfigDialog( - currentConfig = currentConfig?.toModelConfig() ?: ModelConfig(), - currentConfigName = currentConfigName, - onDismiss = { showConfigDialog = false }, + currentConfig = if (isNewConfig) ModelConfig() else (currentConfig?.toModelConfig() ?: ModelConfig()), + currentConfigName = if (isNewConfig) null else currentConfigName, + onDismiss = { + showConfigDialog = false + isNewConfig = false + }, onSave = { configName, newModelConfig -> scope.launch { try { diff --git a/mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/i18n/I18n.kt b/mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/i18n/I18n.kt index cd3415c975..ab7caa49c1 100644 --- a/mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/i18n/I18n.kt +++ b/mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/i18n/I18n.kt @@ -66,6 +66,7 @@ object Strings { val confirm: String get() = AutoDevStrings.common_confirm.toString() val configure: String get() = AutoDevStrings.common_configure.toString() val loading: String get() = AutoDevStrings.common_loading.toString() + val add: String get() = AutoDevStrings.common_add.toString() // Chat UI val chatTitle: String get() = AutoDevStrings.chat_title.toString() @@ -85,6 +86,7 @@ object Strings { val maxTokens: String get() = AutoDevStrings.modelConfig_maxTokens.toString() val advancedParameters: String get() = AutoDevStrings.modelConfig_advancedParameters.toString() val configureModel: String get() = AutoDevStrings.modelConfig_configureModel.toString() + val addNewConfig: String get() = AutoDevStrings.modelConfig_addNewConfig.toString() val noSavedConfigs: String get() = AutoDevStrings.modelConfig_noSavedConfigs.toString() val enterModel: String get() = AutoDevStrings.modelConfig_enterModel.toString() val enterApiKey: String get() = AutoDevStrings.modelConfig_enterApiKey.toString() diff --git a/mpp-vscode/webview/src/App.tsx b/mpp-vscode/webview/src/App.tsx index ad10404a5b..63fc4b5f21 100644 --- a/mpp-vscode/webview/src/App.tsx +++ b/mpp-vscode/webview/src/App.tsx @@ -9,6 +9,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Timeline } from './components/Timeline'; import { ChatInput } from './components/ChatInput'; import { ModelConfig } from './components/ModelSelector'; +import { ModelConfigDialog } from './components/ModelConfigDialog'; import { SelectedFile } from './components/FileChip'; import { CompletionItem } from './components/CompletionPopup'; import { PlanData } from './components/plan'; @@ -74,6 +75,11 @@ const App: React.FC = () => { // Omnibar state const [showOmnibar, setShowOmnibar] = useState(false); + // Model config dialog state + const [showModelConfigDialog, setShowModelConfigDialog] = useState(false); + const [modelConfigDialogConfig, setModelConfigDialogConfig] = useState(null); + const [isNewConfig, setIsNewConfig] = useState(false); + const { postMessage, onMessage, isVSCode } = useVSCode(); // Handle messages from extension @@ -327,8 +333,22 @@ const App: React.FC = () => { // Handle open config const handleOpenConfig = useCallback(() => { - postMessage({ type: 'openConfig' }); - }, [postMessage]); + const currentConfig = configState.availableConfigs.find(c => c.name === configState.currentConfigName); + if (currentConfig) { + setModelConfigDialogConfig(currentConfig); + setIsNewConfig(false); + setShowModelConfigDialog(true); + } else { + postMessage({ type: 'openConfig' }); + } + }, [postMessage, configState]); + + // Handle add new config + const handleAddNewConfig = useCallback(() => { + setModelConfigDialogConfig(null); + setIsNewConfig(true); + setShowModelConfigDialog(true); + }, []); // Handle stop execution const handleStop = useCallback(() => { @@ -470,6 +490,7 @@ const App: React.FC = () => { onStop={handleStop} onConfigSelect={handleConfigSelect} onConfigureClick={handleOpenConfig} + onAddNewConfig={handleAddNewConfig} onMcpConfigClick={handleMcpConfigClick} onPromptOptimize={handlePromptOptimize} onGetCompletions={handleGetCompletions} @@ -484,6 +505,16 @@ const App: React.FC = () => { totalTokens={totalTokens} currentPlan={currentPlan} /> + + {/* Model Config Dialog */} + {showModelConfigDialog && ( + setShowModelConfigDialog(false)} + currentConfig={modelConfigDialogConfig || undefined} + isNewConfig={isNewConfig} + /> + )} ); }; diff --git a/mpp-vscode/webview/src/components/ChatInput.tsx b/mpp-vscode/webview/src/components/ChatInput.tsx index 4932df666c..26ee083224 100644 --- a/mpp-vscode/webview/src/components/ChatInput.tsx +++ b/mpp-vscode/webview/src/components/ChatInput.tsx @@ -13,6 +13,7 @@ interface ChatInputProps { onStop?: () => void; onConfigSelect?: (config: ModelConfig) => void; onConfigureClick?: () => void; + onAddNewConfig?: () => void; onMcpConfigClick?: () => void; onPromptOptimize?: (prompt: string) => Promise; onGetCompletions?: (text: string, cursorPosition: number) => void; @@ -34,6 +35,7 @@ export const ChatInput: React.FC = ({ onStop, onConfigSelect, onConfigureClick, + onAddNewConfig, onMcpConfigClick, onPromptOptimize, onGetCompletions, @@ -225,6 +227,7 @@ export const ChatInput: React.FC = ({ currentConfigName={currentConfigName} onConfigSelect={onConfigSelect || (() => {})} onConfigureClick={onConfigureClick || (() => {})} + onAddNewConfig={onAddNewConfig} /> {/* Token usage indicator */} {totalTokens != null && totalTokens > 0 && ( diff --git a/mpp-vscode/webview/src/components/ModelConfigDialog.css b/mpp-vscode/webview/src/components/ModelConfigDialog.css new file mode 100644 index 0000000000..826a3b1b5e --- /dev/null +++ b/mpp-vscode/webview/src/components/ModelConfigDialog.css @@ -0,0 +1,167 @@ +.model-config-dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.model-config-dialog { + background: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + width: 90%; + max-width: 600px; + max-height: 90vh; + display: flex; + flex-direction: column; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.model-config-dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid var(--vscode-panel-border); +} + +.model-config-dialog-header h2 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: var(--vscode-foreground); +} + +.close-button { + background: none; + border: none; + color: var(--vscode-foreground); + font-size: 24px; + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; +} + +.close-button:hover { + background: var(--vscode-list-hoverBackground); +} + +.model-config-dialog-content { + padding: 20px; + overflow-y: auto; + flex: 1; +} + +.form-field { + margin-bottom: 16px; +} + +.form-field label { + display: block; + margin-bottom: 6px; + font-size: 13px; + font-weight: 500; + color: var(--vscode-foreground); +} + +.form-field input, +.form-field select { + width: 100%; + padding: 8px 12px; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border); + border-radius: 2px; + color: var(--vscode-input-foreground); + font-size: 13px; + box-sizing: border-box; +} + +.form-field input:focus, +.form-field select:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.input-with-icon { + position: relative; + display: flex; + align-items: center; +} + +.input-with-icon input { + padding-right: 40px; +} + +.toggle-visibility { + position: absolute; + right: 8px; + background: none; + border: none; + color: var(--vscode-foreground); + cursor: pointer; + padding: 4px; + font-size: 16px; +} + +.toggle-advanced { + background: none; + border: none; + color: var(--vscode-foreground); + cursor: pointer; + padding: 8px 0; + font-size: 13px; + text-align: left; + width: 100%; +} + +.toggle-advanced:hover { + color: var(--vscode-textLink-foreground); +} + +.model-config-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 16px 20px; + border-top: 1px solid var(--vscode-panel-border); +} + +.cancel-button, +.save-button { + padding: 8px 16px; + border: none; + border-radius: 2px; + font-size: 13px; + cursor: pointer; + font-weight: 500; +} + +.cancel-button { + background: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); +} + +.cancel-button:hover { + background: var(--vscode-button-secondaryHoverBackground); +} + +.save-button { + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +.save-button:hover { + background: var(--vscode-button-hoverBackground); +} + diff --git a/mpp-vscode/webview/src/components/ModelConfigDialog.tsx b/mpp-vscode/webview/src/components/ModelConfigDialog.tsx new file mode 100644 index 0000000000..35f550a0b3 --- /dev/null +++ b/mpp-vscode/webview/src/components/ModelConfigDialog.tsx @@ -0,0 +1,240 @@ +/** + * ModelConfigDialog Component + * + * Dialog for configuring LLM models in VSCode webview. + * Communicates with extension via postMessage. + */ + +import React, { useState, useEffect } from 'react'; +import { useVSCode } from '../hooks/useVSCode'; +import './ModelConfigDialog.css'; + +interface ModelConfigDialogProps { + isOpen: boolean; + onClose: () => void; + currentConfig?: { + name?: string; + provider?: string; + model?: string; + apiKey?: string; + baseUrl?: string; + temperature?: number; + maxTokens?: number; + } | null; + isNewConfig?: boolean; +} + +const PROVIDERS = [ + { value: 'openai', label: 'OpenAI' }, + { value: 'anthropic', label: 'Anthropic' }, + { value: 'google', label: 'Google' }, + { value: 'deepseek', label: 'DeepSeek' }, + { value: 'ollama', label: 'Ollama' }, + { value: 'openrouter', label: 'OpenRouter' }, + { value: 'glm', label: 'GLM' }, + { value: 'qwen', label: 'Qwen' }, + { value: 'kimi', label: 'Kimi' }, + { value: 'custom-openai-base', label: 'Custom OpenAI-compatible' }, +]; + +export const ModelConfigDialog: React.FC = ({ + isOpen, + onClose, + currentConfig, + isNewConfig = false +}) => { + const [configName, setConfigName] = useState(currentConfig?.name || ''); + const [provider, setProvider] = useState(currentConfig?.provider || 'openai'); + const [model, setModel] = useState(currentConfig?.model || ''); + const [apiKey, setApiKey] = useState(currentConfig?.apiKey || ''); + const [baseUrl, setBaseUrl] = useState(currentConfig?.baseUrl || ''); + const [temperature, setTemperature] = useState(currentConfig?.temperature?.toString() || '0.7'); + const [maxTokens, setMaxTokens] = useState(currentConfig?.maxTokens?.toString() || '8192'); + const [showApiKey, setShowApiKey] = useState(false); + const [showAdvanced, setShowAdvanced] = useState(false); + const { postMessage } = useVSCode(); + + useEffect(() => { + if (isOpen && currentConfig) { + setConfigName(currentConfig.name || ''); + setProvider(currentConfig.provider || 'openai'); + setModel(currentConfig.model || ''); + setApiKey(currentConfig.apiKey || ''); + setBaseUrl(currentConfig.baseUrl || ''); + setTemperature(currentConfig.temperature?.toString() || '0.7'); + setMaxTokens(currentConfig.maxTokens?.toString() || '8192'); + } else if (isOpen && isNewConfig) { + // Reset to defaults for new config + setConfigName(''); + setProvider('openai'); + setModel(''); + setApiKey(''); + setBaseUrl(''); + setTemperature('0.7'); + setMaxTokens('8192'); + } + }, [isOpen, currentConfig, isNewConfig]); + + if (!isOpen) return null; + + const needsBaseUrl = ['ollama', 'glm', 'qwen', 'kimi', 'custom-openai-base'].includes(provider); + const needsApiKey = provider !== 'ollama'; + + const handleSave = () => { + if (!configName.trim()) { + alert('Please enter a configuration name'); + return; + } + if (!model.trim()) { + alert('Please enter a model name'); + return; + } + if (needsApiKey && !apiKey.trim()) { + alert('Please enter an API key'); + return; + } + if (needsBaseUrl && !baseUrl.trim()) { + alert('Please enter a base URL'); + return; + } + + postMessage({ + type: 'saveModelConfig', + data: { + name: configName.trim(), + provider, + model: model.trim(), + apiKey: apiKey.trim(), + baseUrl: baseUrl.trim() || undefined, + temperature: parseFloat(temperature) || 0.7, + maxTokens: parseInt(maxTokens) || 8192, + isNewConfig + } + }); + + onClose(); + }; + + return ( +
+
e.stopPropagation()}> +
+

{isNewConfig ? 'Add New Model Config' : 'Configure Model'}

+ +
+ +
+
+ + setConfigName(e.target.value)} + placeholder="e.g., my-glm, work-gpt4" + /> +
+ +
+ + +
+ +
+ + setModel(e.target.value)} + placeholder="Enter model name" + /> +
+ + {needsApiKey && ( +
+ +
+ setApiKey(e.target.value)} + placeholder="Enter API key" + /> + +
+
+ )} + + {needsBaseUrl && ( +
+ + setBaseUrl(e.target.value)} + placeholder={ + provider === 'ollama' ? 'http://localhost:11434' : + provider === 'glm' ? 'https://open.bigmodel.cn/api/paas/v4' : + provider === 'qwen' ? 'https://dashscope.aliyuncs.com/api/v1' : + provider === 'kimi' ? 'https://api.moonshot.cn/v1' : + 'https://api.example.com/v1' + } + /> +
+ )} + +
+ +
+ + {showAdvanced && ( + <> +
+ + setTemperature(e.target.value)} + placeholder="0.7" + /> +
+ +
+ + setMaxTokens(e.target.value)} + placeholder="8192" + /> +
+ + )} +
+ +
+ + +
+
+
+ ); +}; + diff --git a/mpp-vscode/webview/src/components/ModelSelector.tsx b/mpp-vscode/webview/src/components/ModelSelector.tsx index 2be82112ed..d90a5beeab 100644 --- a/mpp-vscode/webview/src/components/ModelSelector.tsx +++ b/mpp-vscode/webview/src/components/ModelSelector.tsx @@ -19,13 +19,15 @@ interface ModelSelectorProps { currentConfigName: string | null; onConfigSelect: (config: ModelConfig) => void; onConfigureClick: () => void; + onAddNewConfig?: () => void; } export const ModelSelector: React.FC = ({ availableConfigs, currentConfigName, onConfigSelect, - onConfigureClick + onConfigureClick, + onAddNewConfig }) => { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); @@ -93,6 +95,24 @@ export const ModelSelector: React.FC = ({ )} + {onAddNewConfig && ( + <> + +
+ + )} +
@@ -148,9 +159,14 @@ export const ModelConfigDialog: React.FC = ({ setModel(e.target.value)} + onChange={(e) => { + setModel(e.target.value); + if (errors.model) setErrors({ ...errors, model: '' }); + }} placeholder="Enter model name" + className={errors.model ? 'error' : ''} /> + {errors.model &&
{errors.model}
}
{needsApiKey && ( @@ -160,8 +176,12 @@ export const ModelConfigDialog: React.FC = ({ setApiKey(e.target.value)} + onChange={(e) => { + setApiKey(e.target.value); + if (errors.apiKey) setErrors({ ...errors, apiKey: '' }); + }} placeholder="Enter API key" + className={errors.apiKey ? 'error' : ''} /> + {errors.apiKey &&
{errors.apiKey}
} )} @@ -180,7 +201,10 @@ export const ModelConfigDialog: React.FC = ({ setBaseUrl(e.target.value)} + onChange={(e) => { + setBaseUrl(e.target.value); + if (errors.baseUrl) setErrors({ ...errors, baseUrl: '' }); + }} placeholder={ provider === 'ollama' ? 'http://localhost:11434' : provider === 'glm' ? 'https://open.bigmodel.cn/api/paas/v4' : @@ -188,7 +212,9 @@ export const ModelConfigDialog: React.FC = ({ provider === 'kimi' ? 'https://api.moonshot.cn/v1' : 'https://api.example.com/v1' } + className={errors.baseUrl ? 'error' : ''} /> + {errors.baseUrl &&
{errors.baseUrl}
} )}