Skip to content
Merged
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
9 changes: 3 additions & 6 deletions packages/vsce/e2e/demo/model-dialog.demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,13 @@ test.describe('Model Dialog Demo', () => {

// Verify model select is pre-selected
await expect(webviewPage.locator('#model-select')).toHaveValue('claude-sonnet-4-20250514');
await expect(webviewPage.locator('#fast-model-select')).toHaveValue('claude-haiku-4-20250514');

// Take screenshot
const dialog = webviewPage.locator('.configuration-dialog');
await dialog.screenshot({ path: '../../docs/public/screenshots/spec-model-dialog.png' });
});

test('should show model dialog with empty values and env placeholders', async ({ webviewPage }) => {
test('should show model dialog with empty values', async ({ webviewPage }) => {
await webviewPage.evaluate(() => {
window.simulateExtensionMessage({
command: 'showDialog',
Expand All @@ -54,14 +53,12 @@ test.describe('Model Dialog Demo', () => {
command: 'configurationResponse',
configurationData: {
model: '',
fastModel: '',
envModel: 'WAVE_MODEL',
envFastModel: 'WAVE_FAST_MODEL'
fastModel: ''
}
});
});

// Simulate configured models (even with env vars, models list comes from SDK)
// Simulate configured models
await webviewPage.evaluate(() => {
window.simulateExtensionMessage({
command: 'configuredModelsResponse',
Expand Down
5 changes: 5 additions & 0 deletions packages/vsce/src/chatProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ export class ChatProvider implements vscode.WebviewViewProvider {
this.sidebarSession.updateConfig(cfg, this.context.extensionMode);
this.tabSessions.forEach(session => session.updateConfig(cfg, this.context.extensionMode));
this.windowSessions.forEach(session => session.updateConfig(cfg, this.context.extensionMode));
},
updateAllSessionsModel: (model: string) => {
this.sidebarSession.setModel(model);
this.tabSessions.forEach(session => session.setModel(model));
this.windowSessions.forEach(session => session.setModel(model));
}
}
);
Expand Down
21 changes: 1 addition & 20 deletions packages/vsce/src/services/configurationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,6 @@ export interface ConfigurationData {
model?: string;
fastModel?: string;
language?: string;
/** Environment variable values (read-only, for placeholder display) */
envServerUrl?: string;
envApiKey?: string;
envHeaders?: string;
envBaseUrl?: string;
envModel?: string;
envFastModel?: string;
}

/** Mask a secret value, keeping first 4 and last 4 characters visible */
function maskSecret(value: string): string {
if (value.length <= 10) return '****';
return value.slice(0, 4) + '****' + value.slice(-4);
}

export class ConfigurationService {
Expand All @@ -34,13 +21,7 @@ export class ConfigurationService {
baseURL: this.context.globalState.get<string>('baseURL') || '',
model: this.context.globalState.get<string>('model') || '',
fastModel: this.context.globalState.get<string>('fastModel') || '',
language: this.context.globalState.get<string>('language') || 'Chinese',
envServerUrl: process.env.WAVE_SERVER_URL || undefined,
envApiKey: process.env.WAVE_API_KEY ? maskSecret(process.env.WAVE_API_KEY) : undefined,
envHeaders: process.env.WAVE_CUSTOM_HEADERS || undefined,
envBaseUrl: process.env.WAVE_BASE_URL || undefined,
envModel: process.env.WAVE_MODEL || undefined,
envFastModel: process.env.WAVE_FAST_MODEL || undefined
language: this.context.globalState.get<string>('language') || 'Chinese'
};
}

Expand Down
4 changes: 4 additions & 0 deletions packages/vsce/src/session/chatSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ export class ChatSession {
this.clearQueue();
}

public setModel(model: string): void {
this.agent?.setModel(model);
}

public async updateConfig(config: ConfigurationData, extensionMode: vscode.ExtensionMode) {
if (this.agent) {
const currentSessionId = this.sessionId;
Expand Down
20 changes: 6 additions & 14 deletions packages/vsce/src/session/messageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface MessageHandlerContext {
initializeAgent: (viewType: 'sidebar' | 'tab' | 'window', windowId?: string, restoreSessionId?: string) => Promise<void>;
listSessions: (viewType?: 'sidebar' | 'tab' | 'window', windowId?: string) => Promise<void>;
updateAllSessionsConfig: (config: unknown) => void;
updateAllSessionsModel: (model: string) => void;
}

export class MessageHandler {
Expand Down Expand Up @@ -738,12 +739,8 @@ export class MessageHandler {

private async handleSetModel(configData: unknown, viewType?: 'sidebar' | 'tab' | 'window', windowId?: string) {
try {
const currentConfig = await this.configService.loadConfiguration();
const mergedConfig = { ...currentConfig, ...(configData as Record<string, unknown>) };
await this.configService.saveConfiguration(mergedConfig);
const config = await this.configService.loadConfiguration();

this.context.updateAllSessionsConfig(config);
const { model } = configData as { model: string };
this.context.updateAllSessionsModel(model);

this.context.postMessage({ command: 'configurationUpdated' }, viewType, windowId);
this.context.postMessage({ command: 'focusInput' }, viewType, windowId);
Expand All @@ -759,24 +756,19 @@ export class MessageHandler {
private async handleGetConfiguredModels(viewType?: 'sidebar' | 'tab' | 'window', windowId?: string) {
try {
const session = this.context.getChatSession(viewType || 'tab', windowId);
// Get models from the agent instance (like wave-agent does)
// The agent's getConfiguredModels() reads from SDK's ConfigurationService which has remote models
const models = session.agent?.getConfiguredModels() || [];
// Also get the current model values from the agent (these include remote config)
const modelConfig = session.agent?.getModelConfig() || { model: '', fastModel: '' };
const modelConfig = session.agent?.getModelConfig() || { model: '' };
this.context.postMessage({
command: 'configuredModelsResponse',
models,
currentModel: modelConfig.model || '',
currentFastModel: modelConfig.fastModel || ''
currentModel: modelConfig.model || ''
}, viewType, windowId);
} catch (error) {
console.error(`Failed to get configured models for ${viewType}:`, error);
this.context.postMessage({
command: 'configuredModelsResponse',
models: [],
currentModel: '',
currentFastModel: ''
currentModel: ''
}, viewType, windowId);
}
}
Expand Down
35 changes: 9 additions & 26 deletions packages/vsce/tests/webview/modelDropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ describe('Model Dialog', () => {
await act(async () => {
sendCommand('configurationResponse', {
configurationData: {
model: 'gpt-4',
fastModel: 'gpt-4-mini'
model: 'gpt-4'
}
});
sendCommand('configuredModelsResponse', {
models: ['gpt-4', 'gpt-3.5-turbo', 'claude-3-opus', 'gpt-4-mini'],
models: ['gpt-4', 'gpt-3.5-turbo', 'claude-3-opus'],
});
});

Expand All @@ -43,12 +42,8 @@ describe('Model Dialog', () => {
expect(modelSelect.value).toBe('gpt-4');
});

// Verify fast model select exists and has the current value
const fastModelSelect = document.querySelector('#fast-model-select') as HTMLSelectElement;
expect(fastModelSelect).toBeInTheDocument();
await waitFor(() => {
expect(fastModelSelect.value).toBe('gpt-4-mini');
});
// Fast model select should not exist
expect(document.querySelector('#fast-model-select')).not.toBeInTheDocument();
});

it('should populate select options from configured models response', async () => {
Expand All @@ -57,8 +52,7 @@ describe('Model Dialog', () => {
await act(async () => {
sendCommand('configurationResponse', {
configurationData: {
model: 'gpt-4',
fastModel: 'gpt-4-mini'
model: 'gpt-4'
}
});
sendCommand('configuredModelsResponse', {
Expand All @@ -78,13 +72,6 @@ describe('Model Dialog', () => {
expect(modelValues).toContain('gpt-4');
expect(modelValues).toContain('gpt-3.5-turbo');
expect(modelValues).toContain('claude-3-opus');

const fastModelSelect = document.querySelector('#fast-model-select') as HTMLSelectElement;
const fastOptions = Array.from(fastModelSelect.querySelectorAll('option'));
const fastValues = fastOptions.map(o => o.value);
expect(fastValues).toContain('gpt-4');
expect(fastValues).toContain('gpt-3.5-turbo');
expect(fastValues).toContain('claude-3-opus');
});

it('should send setModel message when save is clicked', async () => {
Expand All @@ -93,13 +80,11 @@ describe('Model Dialog', () => {
await act(async () => {
sendCommand('configurationResponse', {
configurationData: {
model: 'gpt-4',
fastModel: 'gpt-4-mini'
model: 'gpt-4'
}
});
sendCommand('configuredModelsResponse', {
models: ['gpt-4', 'gpt-3.5-turbo'],
fastModels: ['gpt-4-mini', 'gpt-3.5-turbo']
models: ['gpt-4', 'gpt-3.5-turbo']
});
});

Expand Down Expand Up @@ -138,13 +123,11 @@ describe('Model Dialog', () => {
await act(async () => {
sendCommand('configurationResponse', {
configurationData: {
model: 'gpt-4',
fastModel: 'gpt-4-mini'
model: 'gpt-4'
}
});
sendCommand('configuredModelsResponse', {
models: ['gpt-4'],
fastModels: ['gpt-4-mini']
models: ['gpt-4']
});
});

Expand Down
10 changes: 3 additions & 7 deletions packages/vsce/webview/src/components/ChatApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,10 @@ export const ChatApp: React.FC<ChatAppProps> = ({ vscode }) => {
type: 'SET_CONFIGURED_MODELS',
payload: message.models || []
});
// Also update current model values from agent
if (message.currentModel !== undefined || message.currentFastModel !== undefined) {
if (message.currentModel !== undefined) {
dispatch({
type: 'SET_CURRENT_MODELS',
payload: {
model: message.currentModel || '',
fastModel: message.currentFastModel || ''
}
type: 'SET_CURRENT_MODEL',
payload: message.currentModel || ''
});
}
break;
Expand Down
10 changes: 5 additions & 5 deletions packages/vsce/webview/src/components/ConfigDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const ConfigDialog: React.FC<ConfigDialogProps & { vscode: VsCodeApi }> = ({
type="url"
value={formData.baseURL || ''}
onChange={(e) => handleInputChange('baseURL', e.target.value)}
placeholder={configurationData?.envBaseUrl || 'https://api.example.com/v1 (或设置 WAVE_BASE_URL)'}
placeholder="https://api.example.com/v1"
disabled={isLoading}
/>
</div>
Expand All @@ -101,7 +101,7 @@ const ConfigDialog: React.FC<ConfigDialogProps & { vscode: VsCodeApi }> = ({
type="password"
value={formData.apiKey || ''}
onChange={(e) => handleInputChange('apiKey', e.target.value)}
placeholder={configurationData?.envApiKey || '输入 API Key (或设置 WAVE_API_KEY 环境变量)'}
placeholder="输入 API Key"
disabled={isLoading}
/>
</div>
Expand All @@ -112,7 +112,7 @@ const ConfigDialog: React.FC<ConfigDialogProps & { vscode: VsCodeApi }> = ({
id="headers"
value={formData.headers || ''}
onChange={(e) => handleInputChange('headers', e.target.value)}
placeholder={configurationData?.envHeaders || `Authorization: Bearer ...\n(或设置 WAVE_CUSTOM_HEADERS)`}
placeholder="Authorization: Bearer ..."
disabled={isLoading}
className="configuration-textarea"
rows={3}
Expand All @@ -126,7 +126,7 @@ const ConfigDialog: React.FC<ConfigDialogProps & { vscode: VsCodeApi }> = ({
type="text"
value={formData.model || ''}
onChange={(e) => handleInputChange('model', e.target.value)}
placeholder={configurationData?.envModel || '请输入模型名称 (或设置 WAVE_MODEL)'}
placeholder="请输入模型名称"
disabled={isLoading}
/>
</div>
Expand All @@ -138,7 +138,7 @@ const ConfigDialog: React.FC<ConfigDialogProps & { vscode: VsCodeApi }> = ({
type="text"
value={formData.fastModel || ''}
onChange={(e) => handleInputChange('fastModel', e.target.value)}
placeholder={configurationData?.envFastModel || '请输入快速模型名称 (或设置 WAVE_FAST_MODEL)'}
placeholder="请输入快速模型名称"
disabled={isLoading}
/>
</div>
Expand Down
4 changes: 2 additions & 2 deletions packages/vsce/webview/src/components/LoginDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const LoginDialog: React.FC<LoginDialogProps & { vscode: VsCodeApi }> = ({
};
}, [onClose]);

const serverUrl = serverUrlSaved ? serverUrlInput : (configurationData?.serverUrl || configurationData?.envServerUrl || '');
const serverUrl = serverUrlSaved ? serverUrlInput : (configurationData?.serverUrl || '');

return (
<div className="configuration-dialog-overlay">
Expand All @@ -124,7 +124,7 @@ const LoginDialog: React.FC<LoginDialogProps & { vscode: VsCodeApi }> = ({
type="url"
value={serverUrlInput}
onChange={(e) => { setServerUrlInput(e.target.value); setServerUrlSaved(false); }}
placeholder={configurationData?.envServerUrl || 'WAVE_SERVER_URL'}
placeholder="https://wave.example.com"
/>
<button
type="button"
Expand Down
47 changes: 15 additions & 32 deletions packages/vsce/webview/src/components/ModelDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* ModelDialog - Model selection dialog
*
* Opened via the /model slash command. Shows dropdown selects for model and fast model,
* Opened via the /model slash command. Shows a dropdown select for model,
* populated with configured models from the SDK.
*/

Expand All @@ -16,13 +16,11 @@ const ModelDialog: React.FC<ModelDialogProps & { vscode: VsCodeApi }> = ({
onClose,
}) => {
const [model, setModel] = useState('');
const [fastModel, setFastModel] = useState('');
const [saving, setSaving] = useState(false);
const dialogRef = useRef<HTMLDivElement>(null);

useEffect(() => {
setModel(configurationData?.model || '');
setFastModel(configurationData?.fastModel || '');
}, [configurationData]);

// Click outside / Escape to close
Expand All @@ -48,7 +46,7 @@ const ModelDialog: React.FC<ModelDialogProps & { vscode: VsCodeApi }> = ({
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
onSave({ model, fastModel });
onSave({ model });
};

return (
Expand All @@ -61,34 +59,19 @@ const ModelDialog: React.FC<ModelDialogProps & { vscode: VsCodeApi }> = ({
<form onSubmit={handleSubmit} className="configuration-form">
<div className="configuration-fields-scroll-area">
{configuredModels.length > 0 ? (
<>
<div className="configuration-field">
<label htmlFor="model-select">Model:</label>
<select
id="model-select"
value={model}
onChange={(e) => setModel(e.target.value)}
autoFocus
>
{configuredModels.map((m) => (
<option key={m} value={m}>{m}</option>
))}
</select>
</div>

<div className="configuration-field">
<label htmlFor="fast-model-select">Fast Model:</label>
<select
id="fast-model-select"
value={fastModel}
onChange={(e) => setFastModel(e.target.value)}
>
{configuredModels.map((m) => (
<option key={m} value={m}>{m}</option>
))}
</select>
</div>
</>
<div className="configuration-field">
<label htmlFor="model-select">Model:</label>
<select
id="model-select"
value={model}
onChange={(e) => setModel(e.target.value)}
autoFocus
>
{configuredModels.map((m) => (
<option key={m} value={m}>{m}</option>
))}
</select>
</div>
) : (
<div className="model-empty-hint">
没有可用的模型,请检查配置。
Expand Down
Loading