Add a preferred language dropdown

This commit is contained in:
Matt Rubens
2024-12-21 10:10:05 -05:00
parent 6ad6949265
commit 8b4c52fb51
11 changed files with 275 additions and 7 deletions

View File

@@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Add a preferred language dropdown

View File

@@ -1 +1,162 @@
- Before attempting completion, always make sure that any code changes have test coverage and that the tests pass. # Code Quality Rules
1. Test Coverage:
- Before attempting completion, always make sure that any code changes have test coverage
- Ensure all tests pass before submitting changes
2. Git Commits:
- When finishing a task, always output a git commit command
- Include a descriptive commit message that follows conventional commit format
3. Documentation:
- Update README.md when making significant changes, such as:
* Adding new features or settings
* Changing existing functionality
* Updating system requirements
* Adding new dependencies
- Include clear descriptions of new features and how to use them
- Keep the documentation in sync with the codebase
- Add examples where appropriate
# Adding a New Setting
To add a new setting that persists its state, follow these steps:
## For All Settings
1. Add the setting to ExtensionMessage.ts:
- Add the setting to the ExtensionState interface
- Make it required if it has a default value, optional if it can be undefined
- Example: `preferredLanguage: string`
2. Add test coverage:
- Add the setting to mockState in ClineProvider.test.ts
- Add test cases for setting persistence and state updates
- Ensure all tests pass before submitting changes
## For Checkbox Settings
1. Add the message type to WebviewMessage.ts:
- Add the setting name to the WebviewMessage type's type union
- Example: `| "multisearchDiffEnabled"`
2. Add the setting to ExtensionStateContext.tsx:
- Add the setting to the ExtensionStateContextType interface
- Add the setter function to the interface
- Add the setting to the initial state in useState
- Add the setting to the contextValue object
- Example:
```typescript
interface ExtensionStateContextType {
multisearchDiffEnabled: boolean;
setMultisearchDiffEnabled: (value: boolean) => void;
}
```
3. Add the setting to ClineProvider.ts:
- Add the setting name to the GlobalStateKey type union
- Add the setting to the Promise.all array in getState
- Add the setting to the return value in getState with a default value
- Add the setting to the destructured variables in getStateToPostToWebview
- Add the setting to the return value in getStateToPostToWebview
- Add a case in setWebviewMessageListener to handle the setting's message type
- Example:
```typescript
case "multisearchDiffEnabled":
await this.updateGlobalState("multisearchDiffEnabled", message.bool)
await this.postStateToWebview()
break
```
4. Add the checkbox UI to SettingsView.tsx:
- Import the setting and its setter from ExtensionStateContext
- Add the VSCodeCheckbox component with the setting's state and onChange handler
- Add appropriate labels and description text
- Example:
```typescript
<VSCodeCheckbox
checked={multisearchDiffEnabled}
onChange={(e: any) => setMultisearchDiffEnabled(e.target.checked)}
>
<span style={{ fontWeight: "500" }}>Enable multi-search diff matching</span>
</VSCodeCheckbox>
```
5. Add the setting to handleSubmit in SettingsView.tsx:
- Add a vscode.postMessage call to send the setting's value when clicking Done
- Example:
```typescript
vscode.postMessage({ type: "multisearchDiffEnabled", bool: multisearchDiffEnabled })
```
## For Select/Dropdown Settings
1. Add the message type to WebviewMessage.ts:
- Add the setting name to the WebviewMessage type's type union
- Example: `| "preferredLanguage"`
2. Add the setting to ExtensionStateContext.tsx:
- Add the setting to the ExtensionStateContextType interface
- Add the setter function to the interface
- Add the setting to the initial state in useState with a default value
- Add the setting to the contextValue object
- Example:
```typescript
interface ExtensionStateContextType {
preferredLanguage: string;
setPreferredLanguage: (value: string) => void;
}
```
3. Add the setting to ClineProvider.ts:
- Add the setting name to the GlobalStateKey type union
- Add the setting to the Promise.all array in getState
- Add the setting to the return value in getState with a default value
- Add the setting to the destructured variables in getStateToPostToWebview
- Add the setting to the return value in getStateToPostToWebview
- Add a case in setWebviewMessageListener to handle the setting's message type
- Example:
```typescript
case "preferredLanguage":
await this.updateGlobalState("preferredLanguage", message.text)
await this.postStateToWebview()
break
```
4. Add the select UI to SettingsView.tsx:
- Import the setting and its setter from ExtensionStateContext
- Add the select element with appropriate styling to match VSCode's theme
- Add options for the dropdown
- Add appropriate labels and description text
- Example:
```typescript
<select
value={preferredLanguage}
onChange={(e) => setPreferredLanguage(e.target.value)}
style={{
width: "100%",
padding: "4px 8px",
backgroundColor: "var(--vscode-input-background)",
color: "var(--vscode-input-foreground)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "2px"
}}>
<option value="English">English</option>
<option value="Spanish">Spanish</option>
...
</select>
```
5. Add the setting to handleSubmit in SettingsView.tsx:
- Add a vscode.postMessage call to send the setting's value when clicking Done
- Example:
```typescript
vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage })
```
These steps ensure that:
- The setting's state is properly typed throughout the application
- The setting persists between sessions
- The setting's value is properly synchronized between the webview and extension
- The setting has a proper UI representation in the settings view
- Test coverage is maintained for the new setting

View File

@@ -13,6 +13,7 @@ A fork of Cline, an autonomous coding agent, tweaked for more speed and flexibil
- Option to use a larger 1280x800 browser - Option to use a larger 1280x800 browser
- Quick prompt copying from history - Quick prompt copying from history
- OpenRouter compression support - OpenRouter compression support
- Language selection for Cline's communication (English, Japanese, Spanish, French, German, and more)
- Support for newer Gemini models (gemini-exp-1206, gemini-2.0-flash-exp, gemini-2.0-flash-thinking-exp-1219) and Meta 3, 3.1, and 3.2 models via AWS Bedrock - Support for newer Gemini models (gemini-exp-1206, gemini-2.0-flash-exp, gemini-2.0-flash-thinking-exp-1219) and Meta 3, 3.1, and 3.2 models via AWS Bedrock
- Runs alongside the original Cline - Runs alongside the original Cline

View File

@@ -769,8 +769,8 @@ export class Cline {
throw new Error("MCP hub not available") throw new Error("MCP hub not available")
} }
const { browserLargeViewport } = await this.providerRef.deref()?.getState() ?? {} const { browserLargeViewport, preferredLanguage } = await this.providerRef.deref()?.getState() ?? {}
const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub, this.diffStrategy, browserLargeViewport) + await addCustomInstructions(this.customInstructions ?? '', cwd) const systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub, this.diffStrategy, browserLargeViewport) + await addCustomInstructions(this.customInstructions ?? '', cwd, preferredLanguage)
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request // If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) { if (previousApiReqIndex >= 0) {

View File

@@ -772,9 +772,17 @@ async function loadRuleFiles(cwd: string): Promise<string> {
return combinedRules return combinedRules
} }
export async function addCustomInstructions(customInstructions: string, cwd: string): Promise<string> { export async function addCustomInstructions(customInstructions: string, cwd: string, preferredLanguage?: string): Promise<string> {
const ruleFileContent = await loadRuleFiles(cwd) const ruleFileContent = await loadRuleFiles(cwd)
const allInstructions = [customInstructions.trim()] const allInstructions = []
if (preferredLanguage) {
allInstructions.push(`You should always speak and think in the ${preferredLanguage} language.`)
}
if (customInstructions.trim()) {
allInstructions.push(customInstructions.trim())
}
if (ruleFileContent && ruleFileContent.trim()) { if (ruleFileContent && ruleFileContent.trim()) {
allInstructions.push(ruleFileContent.trim()) allInstructions.push(ruleFileContent.trim())

View File

@@ -71,6 +71,7 @@ type GlobalStateKey =
| "alwaysAllowMcp" | "alwaysAllowMcp"
| "browserLargeViewport" | "browserLargeViewport"
| "fuzzyMatchThreshold" | "fuzzyMatchThreshold"
| "preferredLanguage" // Language setting for Cline's communication
export const GlobalFileNames = { export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json", apiConversationHistory: "api_conversation_history.json",
@@ -622,6 +623,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("fuzzyMatchThreshold", message.value) await this.updateGlobalState("fuzzyMatchThreshold", message.value)
await this.postStateToWebview() await this.postStateToWebview()
break break
case "preferredLanguage":
await this.updateGlobalState("preferredLanguage", message.text)
await this.postStateToWebview()
break
} }
}, },
null, null,
@@ -951,6 +956,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
taskHistory, taskHistory,
soundVolume, soundVolume,
browserLargeViewport, browserLargeViewport,
preferredLanguage,
} = await this.getState() } = await this.getState()
const allowedCommands = vscode.workspace const allowedCommands = vscode.workspace
@@ -977,6 +983,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
allowedCommands, allowedCommands,
soundVolume: soundVolume ?? 0.5, soundVolume: soundVolume ?? 0.5,
browserLargeViewport: browserLargeViewport ?? false, browserLargeViewport: browserLargeViewport ?? false,
preferredLanguage: preferredLanguage ?? 'English',
} }
} }
@@ -1072,6 +1079,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundVolume, soundVolume,
browserLargeViewport, browserLargeViewport,
fuzzyMatchThreshold, fuzzyMatchThreshold,
preferredLanguage,
] = await Promise.all([ ] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>, this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>, this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1112,6 +1120,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("soundVolume") as Promise<number | undefined>, this.getGlobalState("soundVolume") as Promise<number | undefined>,
this.getGlobalState("browserLargeViewport") as Promise<boolean | undefined>, this.getGlobalState("browserLargeViewport") as Promise<boolean | undefined>,
this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>, this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
this.getGlobalState("preferredLanguage") as Promise<string | undefined>,
]) ])
let apiProvider: ApiProvider let apiProvider: ApiProvider
@@ -1170,6 +1179,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundVolume, soundVolume,
browserLargeViewport: browserLargeViewport ?? false, browserLargeViewport: browserLargeViewport ?? false,
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0, fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
preferredLanguage: preferredLanguage ?? (() => {
// Get VSCode's locale setting
const vscodeLang = vscode.env.language;
// Map VSCode locale to our supported languages
const langMap: { [key: string]: string } = {
'en': 'English',
'es': 'Spanish',
'fr': 'French',
'de': 'German',
'it': 'Italian',
'pt': 'Portuguese',
'zh': 'Chinese',
'ja': 'Japanese',
'ko': 'Korean',
'ru': 'Russian',
'ar': 'Arabic',
'hi': 'Hindi'
};
// Return mapped language or default to English
return langMap[vscodeLang.split('-')[0]] ?? 'English';
})(),
} }
} }

View File

@@ -73,7 +73,8 @@ jest.mock('vscode', () => ({
onDidCloseTextDocument: jest.fn(() => ({ dispose: jest.fn() })) onDidCloseTextDocument: jest.fn(() => ({ dispose: jest.fn() }))
}, },
env: { env: {
uriScheme: 'vscode' uriScheme: 'vscode',
language: 'en'
} }
})) }))
@@ -235,6 +236,7 @@ describe('ClineProvider', () => {
const mockState: ExtensionState = { const mockState: ExtensionState = {
version: '1.0.0', version: '1.0.0',
preferredLanguage: 'English',
clineMessages: [], clineMessages: [],
taskHistory: [], taskHistory: [],
shouldShowAnnouncement: false, shouldShowAnnouncement: false,
@@ -248,7 +250,7 @@ describe('ClineProvider', () => {
alwaysAllowBrowser: false, alwaysAllowBrowser: false,
uriScheme: 'vscode', uriScheme: 'vscode',
soundEnabled: false, soundEnabled: false,
diffEnabled: false diffEnabled: false,
} }
const message: ExtensionMessage = { const message: ExtensionMessage = {
@@ -300,6 +302,22 @@ describe('ClineProvider', () => {
expect(state).toHaveProperty('diffEnabled') expect(state).toHaveProperty('diffEnabled')
}) })
test('preferredLanguage defaults to VSCode language when not set', async () => {
// Mock VSCode language as Spanish
(vscode.env as any).language = 'es-ES';
const state = await provider.getState();
expect(state.preferredLanguage).toBe('Spanish');
});
test('preferredLanguage defaults to English for unsupported VSCode language', async () => {
// Mock VSCode language as an unsupported language
(vscode.env as any).language = 'unsupported-LANG';
const state = await provider.getState();
expect(state.preferredLanguage).toBe('English');
});
test('diffEnabled defaults to true when not set', async () => { test('diffEnabled defaults to true when not set', async () => {
// Mock globalState.get to return undefined for diffEnabled // Mock globalState.get to return undefined for diffEnabled
(mockContext.globalState.get as jest.Mock).mockReturnValue(undefined) (mockContext.globalState.get as jest.Mock).mockReturnValue(undefined)

View File

@@ -55,6 +55,7 @@ export interface ExtensionState {
diffEnabled?: boolean diffEnabled?: boolean
browserLargeViewport?: boolean browserLargeViewport?: boolean
fuzzyMatchThreshold?: number fuzzyMatchThreshold?: number
preferredLanguage: string
} }
export interface ClineMessage { export interface ClineMessage {

View File

@@ -40,6 +40,7 @@ export interface WebviewMessage {
| "toggleToolAlwaysAllow" | "toggleToolAlwaysAllow"
| "toggleMcpServer" | "toggleMcpServer"
| "fuzzyMatchThreshold" | "fuzzyMatchThreshold"
| "preferredLanguage"
text?: string text?: string
disabled?: boolean disabled?: boolean
askResponse?: ClineAskResponse askResponse?: ClineAskResponse

View File

@@ -40,6 +40,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
allowedCommands, allowedCommands,
fuzzyMatchThreshold, fuzzyMatchThreshold,
setFuzzyMatchThreshold, setFuzzyMatchThreshold,
preferredLanguage,
setPreferredLanguage,
} = useExtensionState() } = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined) const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined) const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@@ -67,6 +69,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled }) vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
vscode.postMessage({ type: "browserLargeViewport", bool: browserLargeViewport }) vscode.postMessage({ type: "browserLargeViewport", bool: browserLargeViewport })
vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 }) vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage })
onDone() onDone()
} }
} }
@@ -136,6 +139,42 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</div> </div>
<div style={{ marginBottom: 5 }}> <div style={{ marginBottom: 5 }}>
<div style={{ marginBottom: 15 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Preferred Language</label>
<select
value={preferredLanguage}
onChange={(e) => setPreferredLanguage(e.target.value)}
style={{
width: "100%",
padding: "4px 8px",
backgroundColor: "var(--vscode-input-background)",
color: "var(--vscode-input-foreground)",
border: "1px solid var(--vscode-input-border)",
borderRadius: "2px",
height: "28px"
}}>
<option value="English">English</option>
<option value="Spanish">Spanish - Español</option>
<option value="French">French - Français</option>
<option value="German">German - Deutsch</option>
<option value="Italian">Italian - Italiano</option>
<option value="Portuguese">Portuguese - Português</option>
<option value="Chinese">Chinese - </option>
<option value="Japanese">Japanese - </option>
<option value="Korean">Korean - </option>
<option value="Russian">Russian - Русский</option>
<option value="Arabic">Arabic - العربية</option>
<option value="Hindi">Hindi - ि</option>
</select>
<p style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Select the language that Cline should use for communication.
</p>
</div>
<VSCodeTextArea <VSCodeTextArea
value={customInstructions ?? ""} value={customInstructions ?? ""}
style={{ width: "100%" }} style={{ width: "100%" }}

View File

@@ -33,6 +33,8 @@ export interface ExtensionStateContextType extends ExtensionState {
setDiffEnabled: (value: boolean) => void setDiffEnabled: (value: boolean) => void
setBrowserLargeViewport: (value: boolean) => void setBrowserLargeViewport: (value: boolean) => void
setFuzzyMatchThreshold: (value: number) => void setFuzzyMatchThreshold: (value: number) => void
preferredLanguage: string
setPreferredLanguage: (value: string) => void
} }
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined) const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -48,6 +50,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
soundVolume: 0.5, soundVolume: 0.5,
diffEnabled: false, diffEnabled: false,
fuzzyMatchThreshold: 1.0, fuzzyMatchThreshold: 1.0,
preferredLanguage: 'English',
}) })
const [didHydrateState, setDidHydrateState] = useState(false) const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false) const [showWelcome, setShowWelcome] = useState(false)
@@ -153,6 +156,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })), setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
setBrowserLargeViewport: (value) => setState((prevState) => ({ ...prevState, browserLargeViewport: value })), setBrowserLargeViewport: (value) => setState((prevState) => ({ ...prevState, browserLargeViewport: value })),
setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })), setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })),
} }
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider> return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>