diff --git a/package.json b/package.json index c6dd51f..f1f3dc0 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,12 @@ ], "order": 2 }, + "claudex.customModel": { + "type": "string", + "markdownDescription": "自定义模型列表,逗号分隔;示例:`my-model-a, my-model-b`", + "default": "", + "order": 2.1 + }, "claudex.permissionMode": { "type": "string", "markdownDescription": "权限模式", diff --git a/src/services/claude/client.ts b/src/services/claude/client.ts index 886b259..8a902e7 100644 --- a/src/services/claude/client.ts +++ b/src/services/claude/client.ts @@ -24,6 +24,7 @@ export class ClaudeClient { private activeBus?: UiMessageBus; private activeSessions: Map = new Map(); private currentSessionId?: string; + private configWatcher?: vscode.Disposable; constructor( private readonly sessionService: IClaudeCodeSessionService, @@ -39,10 +40,47 @@ export class ClaudeClient { this.logService.debug('[ClaudeClient] Webview 已连接'); // 发送初始化消息 + const cfg = vscode.workspace.getConfiguration('claudex'); + const builtInModels = ['claude-4-sonnet', 'claude-4.1-opus']; + const custom = (cfg.get('customModel') || '') + .split(',') + .map(s => s.trim()) + .filter(s => !!s); + // 合并并去重 + const models = Array.from(new Set([...builtInModels, ...custom])); + const selectedModel = cfg.get('model') || models[0] || 'claude-4-sonnet'; + this.sendToWebview({ type: 'system/ready', payload: { - capabilities: ['chat', 'sessions', 'streaming', 'permissions', 'tools'] + capabilities: ['chat', 'sessions', 'streaming', 'permissions', 'tools'], + models, + selectedModel + } + }); + + // 监听配置变化,动态推送模型列表和默认选择 + this.configWatcher?.dispose(); + this.configWatcher = vscode.workspace.onDidChangeConfiguration(e => { + if ( + e.affectsConfiguration('claudex.customModel') || + e.affectsConfiguration('claudex.model') + ) { + const cfg2 = vscode.workspace.getConfiguration('claudex'); + const custom2 = (cfg2.get('customModel') || '') + .split(',') + .map(s => s.trim()) + .filter(Boolean); + const models2 = Array.from(new Set([...builtInModels, ...custom2])); + const selected2 = cfg2.get('model') || models2[0] || 'claude-4-sonnet'; + this.sendToWebview({ + type: 'system/ready', + payload: { + capabilities: ['chat', 'sessions', 'streaming', 'permissions', 'tools'], + models: models2, + selectedModel: selected2 + } + }); } }); } @@ -70,6 +108,9 @@ export class ClaudeClient { case 'chat/send': await this.handleChatSend(message.payload); break; + case 'settings/open': + await vscode.commands.executeCommand('workbench.action.openSettings', message.payload?.query || 'claudex'); + break; case 'chat/interrupt': this.handleChatInterrupt(); break; @@ -195,7 +236,7 @@ export class ClaudeClient { /** * 处理聊天发送 */ - private async handleChatSend(payload: { text: string; sessionId?: string }): Promise { + private async handleChatSend(payload: { text: string; sessionId?: string; model?: string }): Promise { if (!payload?.text?.trim()) { this.sendToWebview({ type: 'system/error', @@ -227,6 +268,11 @@ export class ClaudeClient { try { const request: ClaudeRequest = { prompt: payload.text }; + if (payload.model) { + request.options = { + model: payload.model + } as any; + } const controller = new AbortController(); const result = await this.claudeAgent.handleRequest( @@ -281,6 +327,24 @@ export class ClaudeClient { private async handleUIReady(): Promise { // 初始化时发送会话列表 await this.handleSessionList(); + + // 再次发送系统能力与模型列表,避免早期消息丢失 + const cfg = vscode.workspace.getConfiguration('claudex'); + const builtInModels = ['claude-4-sonnet', 'claude-4.1-opus']; + const custom = (cfg.get('customModel') || '') + .split(',') + .map(s => s.trim()) + .filter(s => !!s); + const models = Array.from(new Set([...builtInModels, ...custom])); + const selectedModel = cfg.get('model') || models[0] || 'claude-4-sonnet'; + this.sendToWebview({ + type: 'system/ready', + payload: { + capabilities: ['chat', 'sessions', 'streaming', 'permissions', 'tools'], + models, + selectedModel + } + }); } /** @@ -353,6 +417,7 @@ export class ClaudeClient { this.activeBus?.complete(); this.activeSessions.clear(); this.webview = undefined; + this.configWatcher?.dispose(); this.logService.debug('[ClaudeClient] 已清理资源'); } -} \ No newline at end of file +} diff --git a/webview/src/components/ButtonArea.vue b/webview/src/components/ButtonArea.vue index 486493e..dda0095 100644 --- a/webview/src/components/ButtonArea.vue +++ b/webview/src/components/ButtonArea.vue @@ -85,28 +85,19 @@ @@ -166,6 +157,7 @@ diff --git a/webview/src/pages/ChatPage.vue b/webview/src/pages/ChatPage.vue index abdce1b..c51a5e7 100644 --- a/webview/src/pages/ChatPage.vue +++ b/webview/src/pages/ChatPage.vue @@ -79,6 +79,7 @@ import ChatInputBox from '../components/ChatInputBox.vue'; import InputExtraBox from '../components/InputExtraBox.vue'; import MessageContainer from '../components/Messages/MessageContainer.vue'; import type { Todo, FileEdit } from '../../types/toolbar'; +import { modelState } from '../services/messageBus'; // 定义事件 const emit = defineEmits<{ @@ -86,7 +87,7 @@ const emit = defineEmits<{ }>(); const progressPercentage = ref(48.7); -const selectedModel = ref('claude-4-sonnet'); +const selectedModel = computed(() => modelState.selected); // 计算聊天标题 const chatTitle = computed(() => { @@ -357,4 +358,4 @@ function handleNewChat() { width: 100%; align-self: center; } - \ No newline at end of file + diff --git a/webview/src/services/messageBus.ts b/webview/src/services/messageBus.ts index cf4ec22..8f8701d 100644 --- a/webview/src/services/messageBus.ts +++ b/webview/src/services/messageBus.ts @@ -46,6 +46,12 @@ export const messageQueueState = reactive({ queuedMessages: [] }); +// 模型状态 +export const modelState = reactive({ + options: [] as string[], + selected: 'claude-4-sonnet' +}); + // ========== 消息总线 ========== class WebviewMessageBus { @@ -213,10 +219,26 @@ class WebviewMessageBus { console.log('[MessageBus] 新会话已创建:', message.payload.sessionId); }); - // 处理系统就绪 + // 处理系统就绪(也作为配置更新使用) this.on('system/ready', (message) => { sessionState.capabilities = message.payload.capabilities; - console.log('[MessageBus] 系统就绪:', message.payload.capabilities); + console.log('[MessageBus] 系统就绪/更新:', message.payload.capabilities); + // 更新模型列表 + if (Array.isArray(message.payload.models)) { + const newOptions = message.payload.models.slice(); + modelState.options = newOptions; + + // 如果当前选择不在新列表里,则回退到推荐或第一个 + if (!newOptions.includes(modelState.selected)) { + const recommended = message.payload.selectedModel && newOptions.includes(message.payload.selectedModel) + ? message.payload.selectedModel + : (newOptions[0] || modelState.selected); + modelState.selected = recommended; + } + } else if (message.payload.selectedModel && !modelState.selected) { + // 初次初始化:没有 options 时保存默认选择 + modelState.selected = message.payload.selectedModel; + } }); // 处理系统错误 @@ -732,7 +754,7 @@ export function sendChatMessage(text: string, sessionId?: string): string { // 发送到 Extension messageBus.send({ type: 'chat/send', - payload: { text, sessionId } + payload: { text, sessionId, model: modelState.selected } }); return requestId; @@ -876,4 +898,14 @@ if (typeof window !== 'undefined') { requestSessionList(); }, 100); }); -} \ No newline at end of file +} + +/** + * 打开设置(可传查询) + */ +export function openSettings(query?: string) { + messageBus.send({ + type: 'settings/open', + payload: { query } + }); +} diff --git a/webview/types/messages.ts b/webview/types/messages.ts index 35f8b8e..de4ab08 100644 --- a/webview/types/messages.ts +++ b/webview/types/messages.ts @@ -91,6 +91,8 @@ export interface SystemReadyMessage extends BaseMessage { type: 'system/ready'; payload: { capabilities: string[]; + models?: string[]; + selectedModel?: string; }; } @@ -144,6 +146,7 @@ export interface ChatSendMessage extends BaseMessage { payload: { text: string; sessionId?: string; + model?: string; }; } @@ -167,6 +170,14 @@ export interface PermissionResponseMessage extends BaseMessage { }; } +// 打开设置 +export interface SettingsOpenMessage extends BaseMessage { + type: 'settings/open'; + payload: { + query?: string; + }; +} + // ========== 联合类型 ========== // Extension发出的所有消息 @@ -187,7 +198,8 @@ export type WebviewMessage = | SessionSwitchMessage | ChatSendMessage | ChatInterruptMessage - | PermissionResponseMessage; + | PermissionResponseMessage + | SettingsOpenMessage; // 所有消息的联合类型 export type Message = ExtensionMessage | WebviewMessage; @@ -300,4 +312,4 @@ export function isSDKSystemMessage(msg: SDKMessage): msg is SDKSystemMessage { export function isSDKStreamEvent(msg: SDKMessage): msg is SDKPartialAssistantMessage { return msg.type === 'stream_event'; -} \ No newline at end of file +}