Skip to content

Commit 6b56e8a

Browse files
committed
feat(mpp-vscode): add ModelSelector and unified toolbar layout
- Add ModelSelector component similar to IdeaModelSelector.kt - Update ChatInput with toolbar: ModelSelector on left, Stop/Send on right - Add config state management in App.tsx - Add sendConfigUpdate, stopExecution, selectConfig in chat-view.ts - Add new message types: configUpdate, stopExecution, selectConfig All 63 tests passing. Refs #31
1 parent caca9a3 commit 6b56e8a

File tree

7 files changed

+497
-41
lines changed

7 files changed

+497
-41
lines changed

mpp-vscode/src/providers/chat-view.ts

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
/**
22
* Chat View Provider - Webview for chat interface
33
*
4-
* Uses mpp-core's CodingAgent for agent-based interactions
5-
* Mirrors mpp-ui's AgentChatInterface and IdeaAgentViewModel architecture
6-
*
4+
* Uses mpp-core's JsCodingAgent for agent-based interactions
75
* Configuration is loaded from ~/.autodev/config.yaml (same as CLI and Desktop)
6+
*
7+
* Architecture mirrors:
8+
* - mpp-ui/src/jsMain/typescript/modes/AgentMode.ts
9+
* - mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt
810
*/
911

1012
import * as vscode from 'vscode';
1113
import { ConfigManager, AutoDevConfigWrapper, LLMConfig } from '../services/config-manager';
1214

13-
// Import mpp-core bridge
1415
// @ts-ignore - Kotlin/JS generated module
1516
import MppCore from '@autodev/mpp-core';
1617

17-
const { JsKoogLLMService, JsModelConfig } = MppCore.cc.unitmesh.llm;
18-
const { JsCodingAgent, JsAgentTask } = MppCore.cc.unitmesh.agent;
18+
// Access Kotlin/JS exports - same pattern as AgentMode.ts
19+
const KotlinCC = MppCore.cc.unitmesh;
1920

2021
/**
2122
* Chat View Provider for the sidebar webview
@@ -64,13 +65,56 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
6465
case 'openConfig':
6566
await this.openConfigFile();
6667
break;
68+
case 'stopExecution':
69+
this.stopExecution();
70+
break;
71+
case 'selectConfig':
72+
await this.selectConfig(message.data?.configName as string);
73+
break;
6774
}
6875
});
6976

7077
// Initialize agent from config file
7178
this.initializeFromConfig();
7279
}
7380

81+
/**
82+
* Stop current execution
83+
*/
84+
private stopExecution(): void {
85+
if (this.isExecuting) {
86+
this.isExecuting = false;
87+
this.postMessage({ type: 'taskComplete', data: { success: false, message: 'Stopped by user' } });
88+
this.log('Execution stopped by user');
89+
}
90+
}
91+
92+
/**
93+
* Select a different config
94+
*/
95+
private async selectConfig(configName: string): Promise<void> {
96+
if (!this.configWrapper || !configName) return;
97+
98+
const configs = this.configWrapper.getAllConfigs();
99+
const selectedConfig = configs.find(c => c.name === configName);
100+
101+
if (selectedConfig) {
102+
// Recreate LLM service with new config
103+
this.llmService = this.createLLMService(selectedConfig);
104+
this.codingAgent = null; // Reset agent to use new LLM service
105+
106+
this.log(`Switched to config: ${configName}`);
107+
108+
// Send updated config state to webview
109+
this.sendConfigUpdate(configName);
110+
111+
this.postMessage({
112+
type: 'responseChunk',
113+
content: `✨ Switched to: \`${selectedConfig.name}\` (${selectedConfig.provider}/${selectedConfig.model})`
114+
});
115+
}
116+
}
117+
74118
/**
75119
* Send a message programmatically
76120
*/
@@ -88,6 +132,28 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
88132
this.webviewView?.webview.postMessage(message);
89133
}
90134

135+
/**
136+
* Send config update to webview
137+
*/
138+
private sendConfigUpdate(currentConfigName?: string): void {
139+
if (!this.configWrapper) return;
140+
141+
const configs = this.configWrapper.getAllConfigs();
142+
const availableConfigs = configs.map(c => ({
143+
name: c.name,
144+
provider: c.provider,
145+
model: c.model
146+
}));
147+
148+
this.postMessage({
149+
type: 'configUpdate',
150+
data: {
151+
availableConfigs,
152+
currentConfigName: currentConfigName || this.configWrapper.getActiveConfig()?.name || null
153+
}
154+
});
155+
}
156+
91157
/**
92158
* Initialize from ~/.autodev/config.yaml
93159
* Mirrors IdeaAgentViewModel.loadConfiguration()
@@ -97,6 +163,9 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
97163
this.configWrapper = await ConfigManager.load();
98164
const activeConfig = this.configWrapper.getActiveConfig();
99165

166+
// Send config state to webview
167+
this.sendConfigUpdate();
168+
100169
if (!activeConfig || !this.configWrapper.isValid()) {
101170
this.log('No valid configuration found in ~/.autodev/config.yaml');
102171
// Show welcome message with config instructions
@@ -125,23 +194,25 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
125194

126195
/**
127196
* Create LLM service from config
197+
* Same pattern as AgentMode.ts
128198
*/
129199
private createLLMService(config: LLMConfig): any {
130-
const modelConfig = new JsModelConfig(
131-
config.provider.toUpperCase(),
200+
const modelConfig = new KotlinCC.llm.JsModelConfig(
201+
config.provider, // Use lowercase provider name like AgentMode.ts
132202
config.model,
133203
config.apiKey || '',
134204
config.temperature ?? 0.7,
135205
config.maxTokens ?? 8192,
136206
config.baseUrl || ''
137207
);
138-
return new JsKoogLLMService(modelConfig);
208+
return new KotlinCC.llm.JsKoogLLMService(modelConfig);
139209
}
140210

141211
/**
142212
* Initialize CodingAgent (lazy initialization)
213+
* Same pattern as AgentMode.ts
143214
*/
144-
private async initializeCodingAgent(): Promise<any> {
215+
private initializeCodingAgent(): any {
145216
if (this.codingAgent) {
146217
return this.codingAgent;
147218
}
@@ -150,13 +221,14 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
150221
throw new Error('LLM service not configured. Please configure ~/.autodev/config.yaml');
151222
}
152223

153-
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
224+
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
154225
const mcpServers = this.configWrapper?.getEnabledMcpServers() || {};
155226

156227
// Create renderer that forwards events to webview
157228
const renderer = this.createRenderer();
158229

159-
this.codingAgent = new JsCodingAgent(
230+
// Create CodingAgent - same constructor as AgentMode.ts
231+
this.codingAgent = new KotlinCC.agent.JsCodingAgent(
160232
workspacePath,
161233
this.llmService,
162234
10, // maxIterations
@@ -258,15 +330,15 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
258330

259331
try {
260332
// Initialize agent if needed
261-
const agent = await this.initializeCodingAgent();
262-
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
333+
const agent = this.initializeCodingAgent();
334+
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
263335

264-
// Create task and execute
265-
const task = new JsAgentTask(trimmedContent, workspacePath);
336+
// Create task and execute - same pattern as AgentMode.ts
337+
const task = new KotlinCC.agent.JsAgentTask(trimmedContent, workspacePath);
266338
const result = await agent.executeTask(task);
267339

268340
// Add completion message
269-
if (result.message) {
341+
if (result && result.message) {
270342
this.messages.push({ role: 'assistant', content: result.message });
271343
}
272344

mpp-vscode/webview/src/App.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@
88
import React, { useState, useEffect, useCallback } from 'react';
99
import { Timeline } from './components/Timeline';
1010
import { ChatInput } from './components/ChatInput';
11+
import { ModelConfig } from './components/ModelSelector';
1112
import { useVSCode, ExtensionMessage } from './hooks/useVSCode';
1213
import type { AgentState, ToolCallInfo, TerminalOutput, ToolCallTimelineItem } from './types/timeline';
1314
import './App.css';
1415

16+
interface ConfigState {
17+
availableConfigs: ModelConfig[];
18+
currentConfigName: string | null;
19+
}
20+
1521
const App: React.FC = () => {
1622
// Agent state - mirrors ComposeRenderer's state
1723
const [agentState, setAgentState] = useState<AgentState>({
@@ -23,6 +29,12 @@ const App: React.FC = () => {
2329
tasks: [],
2430
});
2531

32+
// Config state - mirrors IdeaAgentViewModel's config management
33+
const [configState, setConfigState] = useState<ConfigState>({
34+
availableConfigs: [],
35+
currentConfigName: null
36+
});
37+
2638
const { postMessage, onMessage, isVSCode } = useVSCode();
2739

2840
// Handle messages from extension
@@ -183,6 +195,16 @@ const App: React.FC = () => {
183195
maxIterations: Number(msg.data?.max) || prev.maxIterations
184196
}));
185197
break;
198+
199+
// Config update from extension
200+
case 'configUpdate':
201+
if (msg.data) {
202+
setConfigState({
203+
availableConfigs: (msg.data.availableConfigs as ModelConfig[]) || [],
204+
currentConfigName: (msg.data.currentConfigName as string) || null
205+
});
206+
}
207+
break;
186208
}
187209
}, []);
188210

@@ -211,6 +233,16 @@ const App: React.FC = () => {
211233
postMessage({ type: 'openConfig' });
212234
}, [postMessage]);
213235

236+
// Handle stop execution
237+
const handleStop = useCallback(() => {
238+
postMessage({ type: 'stopExecution' });
239+
}, [postMessage]);
240+
241+
// Handle config selection
242+
const handleConfigSelect = useCallback((config: ModelConfig) => {
243+
postMessage({ type: 'selectConfig', data: { configName: config.name } });
244+
}, [postMessage]);
245+
214246
// Check if we need to show config prompt
215247
const needsConfig = agentState.timeline.length === 0 &&
216248
agentState.currentStreamingContent.includes('No configuration found') ||
@@ -262,8 +294,14 @@ const App: React.FC = () => {
262294
<ChatInput
263295
onSend={handleSend}
264296
onClear={handleClear}
297+
onStop={handleStop}
298+
onConfigSelect={handleConfigSelect}
299+
onConfigureClick={handleOpenConfig}
265300
disabled={agentState.isProcessing}
301+
isExecuting={agentState.isProcessing}
266302
placeholder="Ask AutoDev anything... (use / for commands, @ for agents)"
303+
availableConfigs={configState.availableConfigs}
304+
currentConfigName={configState.currentConfigName}
267305
/>
268306
</div>
269307
);

mpp-vscode/webview/src/components/ChatInput.css

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,57 @@
11
.chat-input-container {
2-
padding: 12px;
2+
padding: 8px 12px 12px;
33
border-top: 1px solid var(--panel-border);
44
background: var(--background);
55
}
66

7+
/* Toolbar */
8+
.input-toolbar {
9+
display: flex;
10+
justify-content: space-between;
11+
align-items: center;
12+
margin-bottom: 8px;
13+
padding: 0 4px;
14+
}
15+
16+
.toolbar-left {
17+
display: flex;
18+
align-items: center;
19+
gap: 8px;
20+
}
21+
22+
.toolbar-right {
23+
display: flex;
24+
align-items: center;
25+
gap: 4px;
26+
}
27+
28+
.toolbar-button {
29+
display: flex;
30+
align-items: center;
31+
justify-content: center;
32+
width: 28px;
33+
height: 28px;
34+
padding: 0;
35+
background: transparent;
36+
border: none;
37+
border-radius: 4px;
38+
color: var(--foreground);
39+
opacity: 0.6;
40+
cursor: pointer;
41+
transition: all 0.15s;
42+
}
43+
44+
.toolbar-button:hover:not(:disabled) {
45+
opacity: 1;
46+
background: var(--selection-background);
47+
}
48+
49+
.toolbar-button:disabled {
50+
opacity: 0.3;
51+
cursor: not-allowed;
52+
}
53+
54+
/* Input Wrapper */
755
.input-wrapper {
856
display: flex;
957
gap: 8px;
@@ -47,15 +95,22 @@
4795
}
4896

4997
.action-button {
50-
width: 36px;
51-
height: 36px;
98+
height: 32px;
5299
display: flex;
53100
align-items: center;
54101
justify-content: center;
102+
gap: 6px;
103+
padding: 0 12px;
55104
border: none;
56105
border-radius: 6px;
57106
cursor: pointer;
58-
transition: all 0.2s;
107+
font-size: 13px;
108+
font-weight: 500;
109+
transition: all 0.15s;
110+
}
111+
112+
.action-button span {
113+
display: inline-block;
59114
}
60115

61116
.send-button {
@@ -72,6 +127,16 @@
72127
cursor: not-allowed;
73128
}
74129

130+
.stop-button {
131+
background: var(--vscode-inputValidation-errorBackground, #5a1d1d);
132+
color: var(--vscode-inputValidation-errorForeground, #f48771);
133+
border: 1px solid var(--vscode-inputValidation-errorBorder, #be1100);
134+
}
135+
136+
.stop-button:hover {
137+
background: var(--vscode-inputValidation-errorBackground, #6a2d2d);
138+
}
139+
75140
.clear-button {
76141
background: transparent;
77142
color: var(--foreground);

0 commit comments

Comments
 (0)