Skip to content
This repository was archived by the owner on Nov 14, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "权限模式",
Expand Down
71 changes: 68 additions & 3 deletions src/services/claude/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class ClaudeClient {
private activeBus?: UiMessageBus;
private activeSessions: Map<string, UiMessageBus> = new Map();
private currentSessionId?: string;
private configWatcher?: vscode.Disposable;

constructor(
private readonly sessionService: IClaudeCodeSessionService,
Expand All @@ -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<string>('customModel') || '')
.split(',')
.map(s => s.trim())
.filter(s => !!s);
// 合并并去重
const models = Array.from(new Set([...builtInModels, ...custom]));
const selectedModel = cfg.get<string>('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<string>('customModel') || '')
.split(',')
.map(s => s.trim())
.filter(Boolean);
const models2 = Array.from(new Set([...builtInModels, ...custom2]));
const selected2 = cfg2.get<string>('model') || models2[0] || 'claude-4-sonnet';
Comment on lines +70 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider normalizing model names to avoid duplicates due to whitespace or casing.

Trimming and filtering helps, but without normalizing case or whitespace, users may still create duplicate model entries. Consider converting names to lower case or documenting the expected format.

Suggested change
const custom2 = (cfg2.get<string>('customModel') || '')
.split(',')
.map(s => s.trim())
.filter(Boolean);
const models2 = Array.from(new Set([...builtInModels, ...custom2]));
const selected2 = cfg2.get<string>('model') || models2[0] || 'claude-4-sonnet';
const custom2 = (cfg2.get<string>('customModel') || '')
.split(',')
.map(s => s.trim().toLowerCase())
.filter(Boolean);
const models2 = Array.from(new Set([...builtInModels.map(m => m.toLowerCase()), ...custom2]));
const selected2 = (cfg2.get<string>('model') || models2[0] || 'claude-4-sonnet').toLowerCase();

this.sendToWebview({
type: 'system/ready',
payload: {
capabilities: ['chat', 'sessions', 'streaming', 'permissions', 'tools'],
models: models2,
selectedModel: selected2
}
});
}
});
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -195,7 +236,7 @@ export class ClaudeClient {
/**
* 处理聊天发送
*/
private async handleChatSend(payload: { text: string; sessionId?: string }): Promise<void> {
private async handleChatSend(payload: { text: string; sessionId?: string; model?: string }): Promise<void> {
if (!payload?.text?.trim()) {
this.sendToWebview({
type: 'system/error',
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -281,6 +327,24 @@ export class ClaudeClient {
private async handleUIReady(): Promise<void> {
// 初始化时发送会话列表
await this.handleSessionList();

// 再次发送系统能力与模型列表,避免早期消息丢失
const cfg = vscode.workspace.getConfiguration('claudex');
const builtInModels = ['claude-4-sonnet', 'claude-4.1-opus'];
const custom = (cfg.get<string>('customModel') || '')
.split(',')
.map(s => s.trim())
.filter(s => !!s);
const models = Array.from(new Set([...builtInModels, ...custom]));
const selectedModel = cfg.get<string>('model') || models[0] || 'claude-4-sonnet';
this.sendToWebview({
type: 'system/ready',
payload: {
capabilities: ['chat', 'sessions', 'streaming', 'permissions', 'tools'],
models,
selectedModel
}
});
}

/**
Expand Down Expand Up @@ -353,6 +417,7 @@ export class ClaudeClient {
this.activeBus?.complete();
this.activeSessions.clear();
this.webview = undefined;
this.configWatcher?.dispose();
this.logService.debug('[ClaudeClient] 已清理资源');
}
}
}
62 changes: 28 additions & 34 deletions webview/src/components/ButtonArea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,28 +85,19 @@
</template>

<template #content="{ close }">
<DropdownItem
:item="{
id: 'claude-4-sonnet',
label: 'claude-4-sonnet',
checked: selectedModelLocal === 'claude-4-sonnet',
type: 'model'
}"
:is-selected="selectedModelLocal === 'claude-4-sonnet'"
:index="0"
@click="(item) => handleModelSelect(item, close)"
/>
<DropdownItem
:item="{
id: 'claude-4.1-opus',
label: 'claude-4.1-opus',
checked: selectedModelLocal === 'claude-4.1-opus',
type: 'model'
}"
:is-selected="selectedModelLocal === 'claude-4.1-opus'"
:index="1"
@click="(item) => handleModelSelect(item, close)"
/>
<template v-for="(m, idx) in modelOptions" :key="m">
<DropdownItem
:item="{
id: m,
label: m,
checked: selectedModelLocal === m,
type: 'model'
}"
:is-selected="selectedModelLocal === m"
:index="idx"
@click="(item) => handleModelSelect(item, close)"
/>
</template>
<DropdownSeparator />
<DropdownItem
:item="{
Expand All @@ -115,8 +106,8 @@
rightIcon: 'codicon-chevron-right',
type: 'action'
}"
:index="2"
@click="(item) => handleModelSelect(item, close)"
:index="modelOptions.length + 1"
@click="() => handleOpenModelSettings(close)"
/>
</template>
</DropdownTrigger>
Expand Down Expand Up @@ -166,6 +157,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { DropdownTrigger, DropdownItem, DropdownSeparator, type DropdownItemData } from './Dropdown'
import { modelState, openSettings } from '../services/messageBus'

interface Props {
disabled?: boolean
Expand All @@ -192,7 +184,8 @@ const props = withDefaults(defineProps<Props>(), {
hasInputContent: false
})

const selectedModelLocal = ref(props.selectedModel)
const selectedModelLocal = computed(() => modelState.selected || props.selectedModel)
const modelOptions = computed(() => modelState.options.length ? modelState.options : [props.selectedModel])

const emit = defineEmits<Emits>()

Expand Down Expand Up @@ -260,18 +253,19 @@ function handleModelDropdown() {

function handleModelSelect(item: DropdownItemData, close: () => void) {
console.log('Selected model:', item)
selectedModelLocal.value = item.id
// 更新全局选中模型
if (item.type === 'model') {
modelState.selected = String(item.id)
}
close()

// 可以在这里添加模型切换的业务逻辑
switch (item.id) {
case 'claude-4-sonnet':
console.log('Switching to Claude 4 Sonnet')
break
case 'claude-4.1-opus':
console.log('Switching to Claude 4.1 Opus')
break
}
console.log('Switched model to:', item.id)
}

function handleOpenModelSettings(close: () => void) {
openSettings('claudex.customModel')
close()
}
</script>

Expand Down
5 changes: 3 additions & 2 deletions webview/src/pages/ChatPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,15 @@ 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<{
switchToSessions: [];
}>();

const progressPercentage = ref(48.7);
const selectedModel = ref('claude-4-sonnet');
const selectedModel = computed(() => modelState.selected);

// 计算聊天标题
const chatTitle = computed(() => {
Expand Down Expand Up @@ -357,4 +358,4 @@ function handleNewChat() {
width: 100%;
align-self: center;
}
</style>
</style>
40 changes: 36 additions & 4 deletions webview/src/services/messageBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export const messageQueueState = reactive<MessageQueueState>({
queuedMessages: []
});

// 模型状态
export const modelState = reactive({
options: [] as string[],
selected: 'claude-4-sonnet'
});

// ========== 消息总线 ==========

class WebviewMessageBus {
Expand Down Expand Up @@ -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;
Comment on lines +228 to +229

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Direct assignment to reactive array may not trigger reactivity in some cases.

If UI updates are inconsistent, try replacing the array contents with splice: modelState.options.splice(0, modelState.options.length, ...newOptions).

Suggested change
const newOptions = message.payload.models.slice();
modelState.options = newOptions;
const newOptions = message.payload.models.slice();
modelState.options.splice(0, modelState.options.length, ...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;
}
});

// 处理系统错误
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -876,4 +898,14 @@ if (typeof window !== 'undefined') {
requestSessionList();
}, 100);
});
}
}

/**
* 打开设置(可传查询)
*/
export function openSettings(query?: string) {
messageBus.send({
type: 'settings/open',
payload: { query }
});
}
16 changes: 14 additions & 2 deletions webview/types/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export interface SystemReadyMessage extends BaseMessage {
type: 'system/ready';
payload: {
capabilities: string[];
models?: string[];
selectedModel?: string;
};
}

Expand Down Expand Up @@ -144,6 +146,7 @@ export interface ChatSendMessage extends BaseMessage {
payload: {
text: string;
sessionId?: string;
model?: string;
};
}

Expand All @@ -167,6 +170,14 @@ export interface PermissionResponseMessage extends BaseMessage {
};
}

// 打开设置
export interface SettingsOpenMessage extends BaseMessage {
type: 'settings/open';
payload: {
query?: string;
};
}

// ========== 联合类型 ==========

// Extension发出的所有消息
Expand All @@ -187,7 +198,8 @@ export type WebviewMessage =
| SessionSwitchMessage
| ChatSendMessage
| ChatInterruptMessage
| PermissionResponseMessage;
| PermissionResponseMessage
| SettingsOpenMessage;

// 所有消息的联合类型
export type Message = ExtensionMessage | WebviewMessage;
Expand Down Expand Up @@ -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';
}
}