From a51055c3a22432a3886caf02417697a16ce7459e Mon Sep 17 00:00:00 2001 From: lewis617 Date: Wed, 24 Jun 2026 17:34:19 +0800 Subject: [PATCH 1/3] feat: replace gear button with /config /plugin /mcp slash commands opening three independent dialogs --- AGENTS.md | 6 +- src/session/messageHandler.ts | 12 +- webview/src/components/ChatApp.tsx | 96 +-- webview/src/components/ConfigDialog.tsx | 291 ++++++++ .../src/components/ConfigurationButton.tsx | 32 - .../src/components/ConfigurationDialog.tsx | 669 ------------------ webview/src/components/McpDialog.tsx | 154 ++++ webview/src/components/MessageInput.tsx | 34 +- webview/src/components/PluginDialog.tsx | 315 +++++++++ webview/src/styles/ConfigurationButton.css | 31 - webview/src/styles/ConfigurationDialog.css | 22 - webview/src/types/index.ts | 43 +- 12 files changed, 859 insertions(+), 846 deletions(-) create mode 100644 webview/src/components/ConfigDialog.tsx delete mode 100644 webview/src/components/ConfigurationButton.tsx delete mode 100644 webview/src/components/ConfigurationDialog.tsx create mode 100644 webview/src/components/McpDialog.tsx create mode 100644 webview/src/components/PluginDialog.tsx delete mode 100644 webview/src/styles/ConfigurationButton.css diff --git a/AGENTS.md b/AGENTS.md index da84ecd..68cdd64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,9 +32,9 @@ This file provides guidance to Wave Code when working with code in this reposito ### 前端 (Webview) - **`webview/src/index.tsx`**: React 应用程序的入口点。 - **`webview/src/components/ChatApp.tsx`**: 使用 `useReducer` 模式管理聊天状态的主要组件。 -- **`webview/src/components/ConfigurationDialog.tsx`**: 用于管理 AI 设置(API 密钥、模型、语言等)和插件管理的 UI。包含三个主要标签页: - - **常规设置**: API 配置、模型选择、语言设置 - - **插件管理**: 探索新插件、已安装插件、插件市场 +- **`webview/src/components/ConfigDialog.tsx`**: 常规设置弹窗(通过 `/config` 命令唤起),管理 AI 设置(API 密钥、模型、语言等)和 SSO 认证。 +- **`webview/src/components/PluginDialog.tsx`**: 插件管理弹窗(通过 `/plugin` 命令唤起),包含探索新插件、已安装插件、插件市场三个子选项卡。 +- **`webview/src/components/McpDialog.tsx`**: MCP 服务器管理弹窗(通过 `/mcp` 命令唤起),显示服务器状态和连接控制。 - **通信 (Communication)**: 使用 `vscode.postMessage` 和 `window.addEventListener('message', ...)` 与后端通信。 - **VS Code API 限制**: `acquireVsCodeApi()` 在整个 Webview 生命周期内只能被调用一次。必须在根组件(如 `index.tsx`)调用并作为 Prop 传递给子组件,严禁在子组件中重复获取,否则会导致 Webview 崩溃。 diff --git a/src/session/messageHandler.ts b/src/session/messageHandler.ts index 8adedd8..1c4f8ec 100644 --- a/src/session/messageHandler.ts +++ b/src/session/messageHandler.ts @@ -593,7 +593,17 @@ export class MessageHandler { private async handleSlashCommandsRequest(filterText: string, viewType?: 'sidebar' | 'tab' | 'window', windowId?: string) { const session = this.context.getChatSession(viewType || 'tab', windowId); try { - const allCommands = session.getSlashCommands(); + const sdkCommands = session.getSlashCommands(); + + // Local UI slash commands (not in SDK, intercepted in webview) + const localCommands = [ + { id: 'config', name: 'config', description: '打开配置设置' }, + { id: 'plugin', name: 'plugin', description: '打开插件管理' }, + { id: 'mcp', name: 'mcp', description: '打开 MCP 服务器管理' } + ]; + + const allCommands = [...sdkCommands, ...localCommands]; + let filteredCommands = allCommands; if (filterText && filterText.trim().length > 0) { const filter = filterText.toLowerCase(); diff --git a/webview/src/components/ChatApp.tsx b/webview/src/components/ChatApp.tsx index abf2be3..c66ed09 100644 --- a/webview/src/components/ChatApp.tsx +++ b/webview/src/components/ChatApp.tsx @@ -5,7 +5,9 @@ import { ChatHeader } from './ChatHeader'; import { TaskList } from './TaskList'; import { QueuedMessageList } from './QueuedMessageList'; import { ConfirmationDialog } from './ConfirmationDialog'; -import ConfigurationDialog from './ConfigurationDialog'; +import ConfigDialog from './ConfigDialog'; +import PluginDialog from './PluginDialog'; +import McpDialog from './McpDialog'; import type { ChatAppProps, ChatState, @@ -31,8 +33,8 @@ const initialState: ChatState = { sessionsLoading: false, pendingConfirmations: [], queuedMessages: [], - // Configuration state - showConfiguration: false, + // Dialog state + activeDialog: null, configurationData: undefined, configurationLoading: false, configurationError: undefined, @@ -119,21 +121,20 @@ function chatReducer(state: ChatState, action: ChatAction): ChatState { ...state, pendingConfirmations: state.pendingConfirmations.filter(c => c.confirmationId !== action.payload) }; - case 'SHOW_CONFIGURATION': + case 'SHOW_DIALOG': return { ...state, - showConfiguration: true, - configurationData: action.payload.data, + activeDialog: action.payload.type, + configurationData: action.payload.data ?? state.configurationData, configurationLoading: false, configurationError: action.payload.error }; - case 'HIDE_CONFIGURATION': - return { - ...state, - showConfiguration: false, - configurationData: action.payload || state.configurationData, - configurationError: undefined - }; + case 'HIDE_DIALOG': + return { + ...state, + activeDialog: null, + configurationError: undefined + }; case 'SET_CONFIGURATION_LOADING': return { ...state, @@ -300,15 +301,19 @@ export const ChatApp: React.FC = ({ vscode }) => { break; case 'showConfiguration': dispatch({ - type: 'SHOW_CONFIGURATION', + type: 'SHOW_DIALOG', payload: { + type: 'config' as const, data: message.configurationData || stateRef.current.configurationData || {}, error: message.error } }); break; + case 'showDialog': + dispatch({ type: 'SHOW_DIALOG', payload: { type: message.dialogType } }); + break; case 'configurationUpdated': - dispatch({ type: 'HIDE_CONFIGURATION' }); + dispatch({ type: 'HIDE_DIALOG' }); break; case 'configurationError': dispatch({ type: 'SET_CONFIGURATION_ERROR', payload: message.error }); @@ -336,6 +341,21 @@ export const ChatApp: React.FC = ({ vscode }) => { const trimmedText = text.trim(); if (!trimmedText && (!images || images.length === 0)) return; + // Intercept local slash commands — open dialogs instead of sending to agent + if (trimmedText === '/config') { + dispatch({ type: 'SHOW_DIALOG', payload: { type: 'config', data: stateRef.current.configurationData || {} } }); + vscode.postMessage({ command: 'getConfiguration' }); + return; + } + if (trimmedText === '/plugin') { + dispatch({ type: 'SHOW_DIALOG', payload: { type: 'plugin' } }); + return; + } + if (trimmedText === '/mcp') { + dispatch({ type: 'SHOW_DIALOG', payload: { type: 'mcp' } }); + return; + } + // Send to extension vscode.postMessage({ command: 'sendMessage', @@ -387,16 +407,6 @@ export const ChatApp: React.FC = ({ vscode }) => { }, [state.queuedMessages, handleSendMessage, handleDeleteQueuedMessage]); // Configuration handlers - const handleConfigurationOpen = useCallback(() => { - dispatch({ - type: 'SHOW_CONFIGURATION', - payload: { data: state.configurationData || {} } - }); - vscode.postMessage({ - command: 'getConfiguration' - }); - }, [vscode, state.configurationData]); - const handleConfigurationSave = useCallback((configData: any) => { dispatch({ type: 'SET_CONFIGURATION_LOADING', payload: true }); vscode.postMessage({ @@ -405,8 +415,8 @@ export const ChatApp: React.FC = ({ vscode }) => { }); }, [vscode]); - const handleConfigurationCancel = useCallback(() => { - dispatch({ type: 'HIDE_CONFIGURATION' }); + const handleDialogClose = useCallback(() => { + dispatch({ type: 'HIDE_DIALOG' }); }, []); const handleToggleTaskList = useCallback(() => { @@ -552,13 +562,6 @@ export const ChatApp: React.FC = ({ vscode }) => { shouldClearInput={state.shouldClearInput} onInputCleared={handleInputCleared} vscode={vscode} - showConfiguration={state.showConfiguration} - configurationData={state.configurationData} - configurationLoading={state.configurationLoading} - configurationError={state.configurationError} - onConfigurationOpen={handleConfigurationOpen} - onConfigurationSave={handleConfigurationSave} - onConfigurationCancel={handleConfigurationCancel} selection={state.selection} inputContent={state.inputContent} permissionMode={state.permissionMode} @@ -579,15 +582,22 @@ export const ChatApp: React.FC = ({ vscode }) => { )} - + {state.activeDialog === 'config' && ( + + )} + {state.activeDialog === 'plugin' && ( + + )} + {state.activeDialog === 'mcp' && ( + + )} ); }; \ No newline at end of file diff --git a/webview/src/components/ConfigDialog.tsx b/webview/src/components/ConfigDialog.tsx new file mode 100644 index 0000000..8ca072f --- /dev/null +++ b/webview/src/components/ConfigDialog.tsx @@ -0,0 +1,291 @@ +/** + * ConfigDialog - General settings dialog for AI configuration + * + * Opened via the /config slash command. Contains SSO authentication + * and AI model/API configuration fields. + */ + +import React, { useState, useEffect, useRef } from 'react'; +import { ConfigDialogProps, ConfigurationData } from '../types'; +import '../styles/ConfigurationDialog.css'; + +const ConfigDialog: React.FC = ({ + configurationData, + isLoading, + error, + onSave, + onCancel, + vscode +}) => { + const [formData, setFormData] = useState({ + serverUrl: '', + baseURL: '', + apiKey: '', + headers: '', + model: '', + fastModel: '', + language: 'Chinese' + }); + + // SSO auth state + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [authUser, setAuthUser] = useState<{ id: string; email?: string } | null>(null); + const [authLoading, setAuthLoading] = useState(false); + const [authMessage, setAuthMessage] = useState(''); + + // Fetch auth status on mount + useEffect(() => { + vscode?.postMessage({ command: 'getAuthStatus' }); + setAuthMessage(''); + }, [vscode]); + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data; + switch (message.command) { + case 'authStatusResponse': + setIsAuthenticated(message.isAuthenticated || false); + setAuthUser(message.user || null); + break; + case 'loginResponse': + if (message.success) { + setIsAuthenticated(true); + setAuthUser(message.user || null); + setAuthLoading(false); + setAuthMessage('登录成功'); + } else { + setAuthLoading(false); + setAuthMessage(message.error || '登录失败'); + } + break; + case 'logoutResponse': + setIsAuthenticated(false); + setAuthUser(null); + setAuthMessage('已登出'); + break; + } + }; + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + }, []); + + const handleLogin = () => { + setAuthLoading(true); + setAuthMessage(''); + vscode?.postMessage({ command: 'login' }); + }; + + const handleLogout = () => { + vscode?.postMessage({ command: 'logout' }); + }; + + const dialogRef = useRef(null); + + // Update form data when configurationData prop changes + useEffect(() => { + if (configurationData) { + setFormData(configurationData); + } + }, [configurationData]); + + // Handle clicking outside to close dialog + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dialogRef.current && !dialogRef.current.contains(event.target as Node)) { + onCancel(); + } + }; + + const handleEscapeKey = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onCancel(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleEscapeKey); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleEscapeKey); + }; + }, [onCancel]); + + const handleInputChange = (field: keyof ConfigurationData, value: string) => { + setFormData(prev => ({ + ...prev, + [field]: value + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave(formData); + }; + + return ( +
+
+
+

配置设置

+
+ +
+
+
+ + handleInputChange('serverUrl', e.target.value)} + placeholder={configurationData?.envServerUrl || 'WAVE_SERVER_URL'} + disabled={isLoading} + /> +
+ +
+ + {isAuthenticated ? ( +
+
+ {authUser?.email && {authUser.email}} + ID: {authUser?.id} +
+ +
+ ) : ( +
+ + {authMessage && ( +
+ {authMessage} +
+ )} +
+ )} +
+ +
+ + handleInputChange('baseURL', e.target.value)} + placeholder={configurationData?.envBaseUrl || 'https://api.example.com/v1 (或设置 WAVE_BASE_URL)'} + disabled={isLoading} + /> +
+ +
+ + handleInputChange('apiKey', e.target.value)} + placeholder={configurationData?.envApiKey || '输入 API Key (或设置 WAVE_API_KEY 环境变量)'} + disabled={isLoading} + /> +
+ +
+ +