diff --git a/package.json b/package.json index 5a6ca82da2..c8772204c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@verboo/code", - "version": "0.15.24", + "version": "0.15.25", "description": "Verboo Code — coding agent for the Verboo platform", "type": "module", "bin": { diff --git a/src/components/LanguagePicker.tsx b/src/components/LanguagePicker.tsx index bf61433fa2..2b07012a12 100644 --- a/src/components/LanguagePicker.tsx +++ b/src/components/LanguagePicker.tsx @@ -68,7 +68,7 @@ export function LanguagePicker(t0) { } let t7; if ($[10] === Symbol.for("react.memo_cache_sentinel")) { - t7 = Leave empty for default (English); + t7 = Leave empty to respond in the user's language; $[10] = t7; } else { t7 = $[10]; diff --git a/src/constants/languageAnchor.test.ts b/src/constants/languageAnchor.test.ts new file mode 100644 index 0000000000..e610f612a9 --- /dev/null +++ b/src/constants/languageAnchor.test.ts @@ -0,0 +1,123 @@ +import { afterEach, beforeEach, expect, test } from 'bun:test' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +// MACRO is replaced at build time by Bun.define but not in test mode. +// Define it globally so tests that import modules using MACRO don't crash. +;(globalThis as Record).MACRO = { + VERSION: '99.0.0', + DISPLAY_VERSION: '0.0.0-test', + BUILD_TIME: new Date().toISOString(), + ISSUES_EXPLAINER: 'report the issue at https://github.com/verbeux-ai/code/issues', + PACKAGE_URL: '@verboo/code', + NATIVE_PACKAGE_URL: undefined, +} + +import { setClaudeConfigHomeDirForTesting } from '../utils/envUtils.js' +import { resetSettingsCache } from '../utils/settings/settingsCache.js' +import { clearSystemPromptSections } from './systemPromptSections.js' +import { getSystemPrompt } from './prompts.js' + +const MIRROR_SNIPPET = + "Always respond in the same language as the user's most recent message" +const SECTION_HEADER = '# Language' + +let dir: string + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'lang-anchor-')) + setClaudeConfigHomeDirForTesting(dir) + resetSettingsCache() + clearSystemPromptSections() +}) + +afterEach(() => { + setClaudeConfigHomeDirForTesting(undefined) + resetSettingsCache() + clearSystemPromptSections() + rmSync(dir, { recursive: true, force: true }) + delete process.env.CLAUDE_CODE_SIMPLE +}) + +test('default (unset language) injects the mirror directive', async () => { + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain(SECTION_HEADER) + expect(text).toContain(MIRROR_SNIPPET) +}) + +test('default (unset language) never leaks an undefined preference', async () => { + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).not.toContain('Always respond in undefined') + expect(text).not.toContain('Use undefined') +}) + +test('the default directive is locale-agnostic — pins no language', async () => { + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).not.toContain('Always respond in English') + expect(text).not.toContain('Always respond in Portuguese') + expect(text).not.toContain('Always respond in Spanish') +}) + +test('explicit language override replaces the mirror directive', async () => { + writeFileSync( + join(dir, 'settings.json'), + JSON.stringify({ language: 'Brazilian Portuguese' }), + ) + resetSettingsCache() + clearSystemPromptSections() + + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain('Always respond in Brazilian Portuguese.') + expect(text).not.toContain(MIRROR_SNIPPET) +}) + +test('any configured language value passes through verbatim', async () => { + writeFileSync( + join(dir, 'settings.json'), + JSON.stringify({ language: 'japanese' }), + ) + resetSettingsCache() + clearSystemPromptSections() + + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain('Always respond in japanese.') + expect(text).not.toContain(MIRROR_SNIPPET) +}) + +test('simple mode system prompt also includes the language anchor', async () => { + process.env.CLAUDE_CODE_SIMPLE = '1' + clearSystemPromptSections() + + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain(MIRROR_SNIPPET) +}) + +test('simple mode respects an explicit language override', async () => { + writeFileSync( + join(dir, 'settings.json'), + JSON.stringify({ language: 'english' }), + ) + resetSettingsCache() + process.env.CLAUDE_CODE_SIMPLE = '1' + clearSystemPromptSections() + + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain('Always respond in english.') + expect(text).not.toContain(MIRROR_SNIPPET) +}) + +test('mirror section keeps technical terms exempt (no forced translation of code)', async () => { + const text = (await getSystemPrompt([], 'test-model')).join('\n') + + expect(text).toContain( + 'Technical terms and code identifiers should remain in their original form', + ) +}) diff --git a/src/constants/prompts.ts b/src/constants/prompts.ts index 006977f082..eaf1d7739a 100644 --- a/src/constants/prompts.ts +++ b/src/constants/prompts.ts @@ -139,10 +139,17 @@ function getAntModelOverrideSection(): string | null { return getAntModelOverrideConfig()?.defaultSystemPromptSuffix || null } +// Locale-agnostic default: mirror the user's language instead of pinning a +// fixed locale — Verboo Code is an international product. +const MIRROR_USER_LANGUAGE_SECTION = `Always respond in the same language as the user's most recent message. Use that language for all explanations, comments, and communications with the user. Technical terms and code identifiers should remain in their original form.` + function getLanguageSection( languagePreference: string | undefined, ): string | null { - if (!languagePreference) return null + if (!languagePreference) { + return `# Language +${MIRROR_USER_LANGUAGE_SECTION}` + } return `# Language Always respond in ${languagePreference}. Use ${languagePreference} for all explanations, comments, and communications with the user. Technical terms and code identifiers should remain in their original form.` @@ -450,7 +457,8 @@ export async function getSystemPrompt( if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { return [ `You are Verboo Code, an open-source coding agent and CLI.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`, - ] + getLanguageSection(getInitialSettings().language), + ].filter(s => s !== null) } const cwd = getCwd()