diff --git a/.changeset/cuddly-vans-judge.md b/.changeset/cuddly-vans-judge.md new file mode 100644 index 00000000000..ba9a25dc3e7 --- /dev/null +++ b/.changeset/cuddly-vans-judge.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Add a preferred language dropdown diff --git a/.clinerules b/.clinerules index 9eaef00abef..8c679894987 100644 --- a/.clinerules +++ b/.clinerules @@ -1 +1,162 @@ -- Before attempting completion, always make sure that any code changes have test coverage and that the tests pass. \ No newline at end of file +# 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 + setMultisearchDiffEnabled(e.target.checked)} + > + Enable multi-search diff matching + + ``` + +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 + + ``` + +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 diff --git a/README.md b/README.md index 81561419409..1c81e98fef3 100644 --- a/README.md +++ b/README.md @@ -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 - Quick prompt copying from history - 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 - Runs alongside the original Cline diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 63704c22f8b..cc1062dc02a 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -769,8 +769,8 @@ export class Cline { throw new Error("MCP hub not available") } - const { browserLargeViewport } = 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 { 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, 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 (previousApiReqIndex >= 0) { diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index e7ae452f00c..1e6920ed6d1 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -772,9 +772,17 @@ async function loadRuleFiles(cwd: string): Promise { return combinedRules } -export async function addCustomInstructions(customInstructions: string, cwd: string): Promise { +export async function addCustomInstructions(customInstructions: string, cwd: string, preferredLanguage?: string): Promise { 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()) { allInstructions.push(ruleFileContent.trim()) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6789d9abe3a..e502d08df0e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -71,6 +71,7 @@ type GlobalStateKey = | "alwaysAllowMcp" | "browserLargeViewport" | "fuzzyMatchThreshold" + | "preferredLanguage" // Language setting for Cline's communication export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -622,6 +623,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("fuzzyMatchThreshold", message.value) await this.postStateToWebview() break + case "preferredLanguage": + await this.updateGlobalState("preferredLanguage", message.text) + await this.postStateToWebview() + break } }, null, @@ -951,6 +956,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { taskHistory, soundVolume, browserLargeViewport, + preferredLanguage, } = await this.getState() const allowedCommands = vscode.workspace @@ -977,6 +983,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { allowedCommands, soundVolume: soundVolume ?? 0.5, browserLargeViewport: browserLargeViewport ?? false, + preferredLanguage: preferredLanguage ?? 'English', } } @@ -1072,6 +1079,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { soundVolume, browserLargeViewport, fuzzyMatchThreshold, + preferredLanguage, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1112,6 +1120,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("soundVolume") as Promise, this.getGlobalState("browserLargeViewport") as Promise, this.getGlobalState("fuzzyMatchThreshold") as Promise, + this.getGlobalState("preferredLanguage") as Promise, ]) let apiProvider: ApiProvider @@ -1170,6 +1179,27 @@ export class ClineProvider implements vscode.WebviewViewProvider { soundVolume, browserLargeViewport: browserLargeViewport ?? false, 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'; + })(), } } diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index b2ab983bc04..d3d2c025296 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -73,7 +73,8 @@ jest.mock('vscode', () => ({ onDidCloseTextDocument: jest.fn(() => ({ dispose: jest.fn() })) }, env: { - uriScheme: 'vscode' + uriScheme: 'vscode', + language: 'en' } })) @@ -235,6 +236,7 @@ describe('ClineProvider', () => { const mockState: ExtensionState = { version: '1.0.0', + preferredLanguage: 'English', clineMessages: [], taskHistory: [], shouldShowAnnouncement: false, @@ -248,7 +250,7 @@ describe('ClineProvider', () => { alwaysAllowBrowser: false, uriScheme: 'vscode', soundEnabled: false, - diffEnabled: false + diffEnabled: false, } const message: ExtensionMessage = { @@ -300,6 +302,22 @@ describe('ClineProvider', () => { 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 () => { // Mock globalState.get to return undefined for diffEnabled (mockContext.globalState.get as jest.Mock).mockReturnValue(undefined) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index dcd352f8154..faef35c6394 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -55,6 +55,7 @@ export interface ExtensionState { diffEnabled?: boolean browserLargeViewport?: boolean fuzzyMatchThreshold?: number + preferredLanguage: string } export interface ClineMessage { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 7a0983afdba..d662475435a 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -40,6 +40,7 @@ export interface WebviewMessage { | "toggleToolAlwaysAllow" | "toggleMcpServer" | "fuzzyMatchThreshold" + | "preferredLanguage" text?: string disabled?: boolean askResponse?: ClineAskResponse diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 465fd5a9750..9a124a21394 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -40,6 +40,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { allowedCommands, fuzzyMatchThreshold, setFuzzyMatchThreshold, + preferredLanguage, + setPreferredLanguage, } = useExtensionState() const [apiErrorMessage, setApiErrorMessage] = useState(undefined) const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined) @@ -67,6 +69,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { vscode.postMessage({ type: "diffEnabled", bool: diffEnabled }) vscode.postMessage({ type: "browserLargeViewport", bool: browserLargeViewport }) vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 }) + vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage }) onDone() } } @@ -136,6 +139,42 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
+
+ + +

+ Select the language that Cline should use for communication. +

+
+ void setBrowserLargeViewport: (value: boolean) => void setFuzzyMatchThreshold: (value: number) => void + preferredLanguage: string + setPreferredLanguage: (value: string) => void } const ExtensionStateContext = createContext(undefined) @@ -48,6 +50,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode soundVolume: 0.5, diffEnabled: false, fuzzyMatchThreshold: 1.0, + preferredLanguage: 'English', }) const [didHydrateState, setDidHydrateState] = 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 })), setBrowserLargeViewport: (value) => setState((prevState) => ({ ...prevState, browserLargeViewport: value })), setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })), + setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })), } return {children}