Chat modes

This commit is contained in:
Matt Rubens
2025-01-03 22:39:52 -08:00
parent ae9a35b7b1
commit 344c796f2e
52 changed files with 6273 additions and 1500 deletions

View File

@@ -27,6 +27,8 @@ import { checkExistKey } from "../../shared/checkExistApiConfig"
import { enhancePrompt } from "../../utils/enhance-prompt"
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
import { ConfigManager } from "../config/ConfigManager"
import { Mode } from "../prompts/types"
import { codeMode } from "../prompts/system"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -89,6 +91,8 @@ type GlobalStateKey =
| "requestDelaySeconds"
| "currentApiConfigName"
| "listApiConfigMeta"
| "mode"
| "modeApiConfigs"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@@ -234,7 +238,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.outputChannel.appendLine("Webview view resolved")
}
async initClineWithTask(task?: string, images?: string[]) {
public async initClineWithTask(task?: string, images?: string[]) {
await this.clearTask()
const {
apiConfiguration,
@@ -254,7 +258,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
)
}
async initClineWithHistoryItem(historyItem: HistoryItem) {
public async initClineWithHistoryItem(historyItem: HistoryItem) {
await this.clearTask()
const {
apiConfiguration,
@@ -275,8 +279,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
)
}
// Send any JSON serializable data to the react app
async postMessageToWebview(message: ExtensionMessage) {
public async postMessageToWebview(message: ExtensionMessage) {
await this.view?.webview.postMessage(message)
}
@@ -688,6 +691,40 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
case "terminalOutputLineLimit":
await this.updateGlobalState("terminalOutputLineLimit", message.value)
await this.postStateToWebview()
break
case "mode":
const newMode = message.text as Mode
await this.updateGlobalState("mode", newMode)
// Load the saved API config for the new mode if it exists
const savedConfigId = await this.configManager.GetModeConfigId(newMode)
const listApiConfig = await this.configManager.ListConfig()
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
// If this mode has a saved config, use it
if (savedConfigId) {
const config = listApiConfig?.find(c => c.id === savedConfigId)
if (config?.name) {
const apiConfig = await this.configManager.LoadConfig(config.name)
await Promise.all([
this.updateGlobalState("currentApiConfigName", config.name),
this.updateApiConfiguration(apiConfig)
])
}
} else {
// If no saved config for this mode, save current config as default
const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
if (currentApiConfigName) {
const config = listApiConfig?.find(c => c.name === currentApiConfigName)
if (config?.id) {
await this.configManager.SetModeConfig(newMode, config.id)
}
}
}
await this.postStateToWebview()
break
case "deleteMessage": {
@@ -802,16 +839,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
if (message.text && message.apiConfiguration) {
try {
await this.configManager.SaveConfig(message.text, message.apiConfiguration);
let listApiConfig = await this.configManager.ListConfig();
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
await Promise.all([
this.updateApiConfiguration(message.apiConfiguration),
this.updateGlobalState("currentApiConfigName", message.text),
this.updateGlobalState("listApiConfigMeta", listApiConfig),
])
this.postStateToWebview()
await this.postStateToWebview()
} catch (error) {
console.error("Error create new api configuration:", error)
vscode.window.showErrorMessage("Failed to create api configuration")
@@ -821,21 +859,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "renameApiConfiguration":
if (message.values && message.apiConfiguration) {
try {
const { oldName, newName } = message.values
await this.configManager.SaveConfig(newName, message.apiConfiguration);
await this.configManager.DeleteConfig(oldName)
let listApiConfig = await this.configManager.ListConfig();
const config = listApiConfig?.find(c => c.name === newName);
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
await Promise.all([
this.updateGlobalState("currentApiConfigName", newName),
this.updateGlobalState("listApiConfigMeta", listApiConfig),
])
this.postStateToWebview()
await this.postStateToWebview()
} catch (error) {
console.error("Error create new api configuration:", error)
vscode.window.showErrorMessage("Failed to create api configuration")
@@ -846,6 +885,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
if (message.text) {
try {
const apiConfig = await this.configManager.LoadConfig(message.text);
const listApiConfig = await this.configManager.ListConfig();
const config = listApiConfig?.find(c => c.name === message.text);
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
await Promise.all([
this.updateGlobalState("currentApiConfigName", message.text),
@@ -861,7 +905,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
case "deleteApiConfiguration":
if (message.text) {
const answer = await vscode.window.showInformationMessage(
"Are you sure you want to delete this configuration profile?",
{ modal: true },
@@ -874,21 +917,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
try {
await this.configManager.DeleteConfig(message.text);
let listApiConfig = await this.configManager.ListConfig()
const listApiConfig = await this.configManager.ListConfig();
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
// If this was the current config, switch to first available
let currentApiConfigName = await this.getGlobalState("currentApiConfigName")
if (message.text === currentApiConfigName) {
await this.updateGlobalState("currentApiConfigName", listApiConfig?.[0]?.name)
if (listApiConfig?.[0]?.name) {
const apiConfig = await this.configManager.LoadConfig(listApiConfig?.[0]?.name);
await Promise.all([
this.updateGlobalState("listApiConfigMeta", listApiConfig),
this.updateApiConfiguration(apiConfig),
])
await this.postStateToWebview()
}
if (message.text === currentApiConfigName && listApiConfig?.[0]?.name) {
const apiConfig = await this.configManager.LoadConfig(listApiConfig[0].name);
await Promise.all([
this.updateGlobalState("currentApiConfigName", listApiConfig[0].name),
this.updateApiConfiguration(apiConfig),
])
}
await this.postStateToWebview()
} catch (error) {
console.error("Error delete api configuration:", error)
vscode.window.showErrorMessage("Failed to delete api configuration")
@@ -913,6 +957,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
private async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
// Update mode's default config
const { mode } = await this.getState();
if (mode) {
const currentApiConfigName = await this.getGlobalState("currentApiConfigName");
const listApiConfig = await this.configManager.ListConfig();
const config = listApiConfig?.find(c => c.name === currentApiConfigName);
if (config?.id) {
await this.configManager.SetModeConfig(mode, config.id);
}
}
const {
apiProvider,
apiModelId,
@@ -1426,6 +1481,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestDelaySeconds,
currentApiConfigName,
listApiConfigMeta,
mode,
} = await this.getState()
const allowedCommands = vscode.workspace
@@ -1462,6 +1518,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestDelaySeconds: requestDelaySeconds ?? 5,
currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
mode: mode ?? codeMode,
}
}
@@ -1571,6 +1628,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestDelaySeconds,
currentApiConfigName,
listApiConfigMeta,
mode,
modeApiConfigs,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1625,6 +1684,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
this.getGlobalState("mode") as Promise<Mode | undefined>,
this.getGlobalState("modeApiConfigs") as Promise<Record<Mode, string> | undefined>,
])
let apiProvider: ApiProvider
@@ -1691,6 +1752,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
writeDelayMs: writeDelayMs ?? 1000,
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
mode: mode ?? codeMode,
preferredLanguage: preferredLanguage ?? (() => {
// Get VSCode's locale setting
const vscodeLang = vscode.env.language;
@@ -1723,6 +1785,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
requestDelaySeconds: requestDelaySeconds ?? 5,
currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
modeApiConfigs: modeApiConfigs ?? {} as Record<Mode, string>,
}
}

View File

@@ -2,6 +2,7 @@ import { ClineProvider } from '../ClineProvider'
import * as vscode from 'vscode'
import { ExtensionMessage, ExtensionState } from '../../../shared/ExtensionMessage'
import { setSoundEnabled } from '../../../utils/sound'
import { codeMode } from '../../prompts/system';
// Mock delay module
jest.mock('delay', () => {
@@ -171,7 +172,16 @@ describe('ClineProvider', () => {
extensionPath: '/test/path',
extensionUri: {} as vscode.Uri,
globalState: {
get: jest.fn(),
get: jest.fn().mockImplementation((key: string) => {
switch (key) {
case 'mode':
return 'architect'
case 'currentApiConfigName':
return 'new-config'
default:
return undefined
}
}),
update: jest.fn(),
keys: jest.fn().mockReturnValue([]),
},
@@ -263,7 +273,8 @@ describe('ClineProvider', () => {
browserViewportSize: "900x600",
fuzzyMatchThreshold: 1.0,
mcpEnabled: true,
requestDelaySeconds: 5
requestDelaySeconds: 5,
mode: codeMode,
}
const message: ExtensionMessage = {
@@ -404,6 +415,80 @@ describe('ClineProvider', () => {
expect(state.alwaysApproveResubmit).toBe(false)
})
test('loads saved API config when switching modes', async () => {
provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
// Mock ConfigManager methods
provider.configManager = {
GetModeConfigId: jest.fn().mockResolvedValue('test-id'),
ListConfig: jest.fn().mockResolvedValue([
{ name: 'test-config', id: 'test-id', apiProvider: 'anthropic' }
]),
LoadConfig: jest.fn().mockResolvedValue({ apiProvider: 'anthropic' }),
SetModeConfig: jest.fn()
} as any
// Switch to architect mode
await messageHandler({ type: 'mode', text: 'architect' })
// Should load the saved config for architect mode
expect(provider.configManager.GetModeConfigId).toHaveBeenCalledWith('architect')
expect(provider.configManager.LoadConfig).toHaveBeenCalledWith('test-config')
expect(mockContext.globalState.update).toHaveBeenCalledWith('currentApiConfigName', 'test-config')
})
test('saves current config when switching to mode without config', async () => {
provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
// Mock ConfigManager methods
provider.configManager = {
GetModeConfigId: jest.fn().mockResolvedValue(undefined),
ListConfig: jest.fn().mockResolvedValue([
{ name: 'current-config', id: 'current-id', apiProvider: 'anthropic' }
]),
SetModeConfig: jest.fn()
} as any
// Mock current config name
(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
if (key === 'currentApiConfigName') {
return 'current-config'
}
return undefined
})
// Switch to architect mode
await messageHandler({ type: 'mode', text: 'architect' })
// Should save current config as default for architect mode
expect(provider.configManager.SetModeConfig).toHaveBeenCalledWith('architect', 'current-id')
})
test('saves config as default for current mode when loading config', async () => {
provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
provider.configManager = {
LoadConfig: jest.fn().mockResolvedValue({ apiProvider: 'anthropic', id: 'new-id' }),
ListConfig: jest.fn().mockResolvedValue([
{ name: 'new-config', id: 'new-id', apiProvider: 'anthropic' }
]),
SetModeConfig: jest.fn(),
GetModeConfigId: jest.fn().mockResolvedValue(undefined)
} as any
// First set the mode
await messageHandler({ type: 'mode', text: 'architect' })
// Then load the config
await messageHandler({ type: 'loadApiConfiguration', text: 'new-config' })
// Should save new config as default for architect mode
expect(provider.configManager.SetModeConfig).toHaveBeenCalledWith('architect', 'new-id')
})
test('handles request delay settings messages', async () => {
provider.resolveWebviewView(mockWebviewView)
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]