diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 49966061..9949eee2 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -461,6 +461,8 @@ export const en = { 'settings.providers.toolSearchEnabled': 'Enable Tool Search', 'settings.providers.toolSearchDesc': 'Load MCP and deferred tools on demand to reduce initial tool schema tokens. Disable it for weak models or providers that reject tool references.', 'settings.providers.toolSearchUnsupported': 'Only Anthropic Messages providers support Tool Search here. OpenAI proxy formats keep full tool schemas available.', + 'settings.providers.availableModels': 'Available models — click to select', + 'settings.providers.clickToSelectModel': 'Click to use this model as the main model', // Settings > Permissions 'settings.permissions.title': 'Permission Mode', diff --git a/desktop/src/i18n/locales/jp.ts b/desktop/src/i18n/locales/jp.ts index 777e0d48..9f0792c9 100644 --- a/desktop/src/i18n/locales/jp.ts +++ b/desktop/src/i18n/locales/jp.ts @@ -463,6 +463,8 @@ export const jp: Record = { 'settings.providers.toolSearchEnabled': 'Tool Search を有効にする', 'settings.providers.toolSearchDesc': 'MCP と遅延ツールを必要に応じて読み込み、初回のツール schema トークンを減らします。弱いモデルや tool_reference を拒否するプロバイダーでは無効にできます。', 'settings.providers.toolSearchUnsupported': 'ここでは Anthropic Messages 形式のプロバイダーのみ Tool Search をサポートします。OpenAI プロキシ形式では完全なツール schema を維持します。', + 'settings.providers.availableModels': '利用可能なモデル — クリックして選択', + 'settings.providers.clickToSelectModel': 'クリックしてこのモデルを使用', // Settings > Permissions 'settings.permissions.title': '権限モード', diff --git a/desktop/src/i18n/locales/kr.ts b/desktop/src/i18n/locales/kr.ts index 2a9829d2..4ce7b9ab 100644 --- a/desktop/src/i18n/locales/kr.ts +++ b/desktop/src/i18n/locales/kr.ts @@ -463,6 +463,8 @@ export const kr: Record = { 'settings.providers.toolSearchEnabled': 'Tool Search 사용', 'settings.providers.toolSearchDesc': 'MCP와 지연 도구를 필요할 때 로드해 초기 도구 schema 토큰을 줄입니다. 약한 모델이나 tool_reference를 거부하는 공급자에서는 끌 수 있습니다.', 'settings.providers.toolSearchUnsupported': '여기서는 Anthropic Messages 형식 공급자만 Tool Search를 지원합니다. OpenAI 프록시 형식은 전체 도구 schema를 유지합니다.', + 'settings.providers.availableModels': '사용 가능한 모델 — 클릭하여 선택', + 'settings.providers.clickToSelectModel': '클릭하여 이 모델을 주 모델로 사용', // Settings > Permissions 'settings.permissions.title': '권한 모드', diff --git a/desktop/src/i18n/locales/zh-TW.ts b/desktop/src/i18n/locales/zh-TW.ts index 41962d67..867c0bf3 100644 --- a/desktop/src/i18n/locales/zh-TW.ts +++ b/desktop/src/i18n/locales/zh-TW.ts @@ -463,6 +463,8 @@ export const zh: Record = { 'settings.providers.toolSearchEnabled': '啟用 Tool Search', 'settings.providers.toolSearchDesc': '按需載入 MCP 和延遲工具,減少首輪工具 schema token。弱模型或不支援 tool_reference 的服務商可以關閉。', 'settings.providers.toolSearchUnsupported': '目前僅 Anthropic Messages 格式支援 Tool Search;OpenAI 代理格式會保留完整工具 schema。', + 'settings.providers.availableModels': '可用模型 — 點擊選擇', + 'settings.providers.clickToSelectModel': '點擊使用此模型作為主模型', // Settings > Permissions 'settings.permissions.title': '許可權模式', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index 22bd97d3..f9355931 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -463,6 +463,8 @@ export const zh: Record = { 'settings.providers.toolSearchEnabled': '启用 Tool Search', 'settings.providers.toolSearchDesc': '按需加载 MCP 和延迟工具,减少首轮工具 schema token。弱模型或不支持 tool_reference 的服务商可以关闭。', 'settings.providers.toolSearchUnsupported': '当前仅 Anthropic Messages 格式支持 Tool Search;OpenAI 代理格式会保留完整工具 schema。', + 'settings.providers.availableModels': '可用模型 — 点击选择', + 'settings.providers.clickToSelectModel': '点击使用此模型作为主模型', // Settings > Permissions 'settings.permissions.title': '权限模式', diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx index a259249b..41318c04 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -1794,6 +1794,33 @@ function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderF )} + {/* Available models — auto-fill main model when a chip is clicked */} + {testResult?.availableModels && testResult.availableModels.length > 0 && ( +
+ +
+ {testResult.availableModels.slice(0, 20).map((model) => ( + + ))} + {testResult.availableModels.length > 20 && ( + + +{testResult.availableModels.length - 20} more + + )} +
+
+ )} + {/* Settings JSON — editable, shown for all presets including official */}
diff --git a/desktop/src/stores/settingsStore.ts b/desktop/src/stores/settingsStore.ts index 46d4b4ba..b1c38e10 100644 --- a/desktop/src/stores/settingsStore.ts +++ b/desktop/src/stores/settingsStore.ts @@ -322,8 +322,12 @@ export const useSettingsStore = create((set, get) => ({ }, setLocale: (locale) => { + const prev = get().locale set({ locale }) try { localStorage.setItem(LOCALE_STORAGE_KEY, locale) } catch { /* noop */ } + settingsApi.updateUser({ locale }).catch(() => { + set({ locale: prev }) + }) }, setTheme: async (theme) => { diff --git a/src/server/index.ts b/src/server/index.ts index 9d6926ae..d5c33171 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -21,7 +21,8 @@ import { sessionService } from './services/sessionService.js' import { conversationService } from './services/conversationService.js' import { OPENAI_CODEX_REDIRECT_PATH } from '../services/openaiAuth/client.js' import { ensureDesktopCliLauncherInstalled } from './services/desktopCliLauncherService.js' -import { enableConfigs } from '../utils/config.js' +import { enableConfigs, getGlobalConfig } from '../utils/config.js' +import { setLocale } from '../utils/i18n/index.js' import { diagnosticsService } from './services/diagnosticsService.js' import { ensurePersistentStorageUpgraded } from './services/persistentStorageMigrations.js' import { handleStaticH5Request } from './staticH5.js' @@ -126,6 +127,9 @@ function originFromUrl(value: string | null): string | null { export function startServer(port = PORT, host = HOST) { enableConfigs() + // Apply persisted locale from ~/.claude/settings.json (set by desktop UI language switcher) + const { locale } = getGlobalConfig() + if (locale) setLocale(locale) // Warm the synchronous disconnect-grace cache from managed settings so the // first client disconnect honors the configured value (issue #764). void refreshDisconnectGraceMs() diff --git a/src/tools/BashTool/pathValidation.ts b/src/tools/BashTool/pathValidation.ts index 8da98f8b..7d0769b5 100644 --- a/src/tools/BashTool/pathValidation.ts +++ b/src/tools/BashTool/pathValidation.ts @@ -9,7 +9,8 @@ import { } from '../../utils/bash/commands.js' import { tryParseShellCommand } from '../../utils/bash/shellQuote.js' import { getDirectoryForPath } from '../../utils/path.js' -import { allWorkingDirectories } from '../../utils/permissions/filesystem.js' +import { getOriginalCwd } from '../../bootstrap/state.js' +import { allWorkingDirectories, checkSymlinkPathSafety } from '../../utils/permissions/filesystem.js' import type { PermissionResult } from '../../utils/permissions/PermissionResult.js' import { createReadRuleSuggestion } from '../../utils/permissions/PermissionUpdate.js' import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js' @@ -691,6 +692,23 @@ function validateCommandPaths( decisionReason, } } + + // Symlink TOCTOU check for write/create operations: + // Block commands where the resolved path crosses a symlink outside the session. + // Read-only operations (ls, cat, grep, etc.) are exempt since they don't modify files. + if (operationType === 'write' || operationType === 'create') { + const symlinkResult = checkSymlinkPathSafety(resolvedPath, [getOriginalCwd()]) + if (!symlinkResult.safe) { + return { + behavior: 'deny', + message: `'${command}' to '${resolvedPath}' is blocked: contains a symlink pointing outside the working directory.`, + decisionReason: { + type: 'safetyCheck', + reason: symlinkResult.reason ?? 'Symlink points outside the session working directory', + }, + } + } + } } // All paths are valid - return passthrough @@ -994,6 +1012,21 @@ function validateOutputRedirections( ], } } + + // Symlink TOCTOU check: ensure the resolved path doesn't cross a symlink + // that points outside the session working directory. + // This is defense-in-depth on top of the normal permission flow. + const symlinkResult = checkSymlinkPathSafety(resolvedPath, [getOriginalCwd()]) + if (!symlinkResult.safe) { + return { + behavior: 'deny', + message: `Output redirection to '${resolvedPath}' is blocked: contains a symlink pointing outside the working directory.`, + decisionReason: { + type: 'safetyCheck', + reason: symlinkResult.reason ?? 'Symlink points outside the session working directory', + }, + } + } } return { diff --git a/src/utils/config.ts b/src/utils/config.ts index 8c6741ed..6b087c8c 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -195,6 +195,7 @@ export type GlobalConfig = { doctorShownAtSession?: number userID?: string theme: ThemeSetting + locale?: string // cc-haha i18n locale (en | zh-CN | zh-TW), synced from desktop UI hasCompletedOnboarding?: boolean // Tracks the last version that reset onboarding, used with MIN_VERSION_REQUIRING_ONBOARDING_RESET lastOnboardingVersion?: string diff --git a/src/utils/i18n/index.ts b/src/utils/i18n/index.ts index f04de061..12997bea 100644 --- a/src/utils/i18n/index.ts +++ b/src/utils/i18n/index.ts @@ -54,6 +54,8 @@ export function setLocale(l: Locale | string): void { if (lower === 'zh-tw' || lower === 'zh_hant') currentLocale = 'zh-TW' else if (lower.startsWith('zh')) currentLocale = 'zh-CN' else currentLocale = 'en' + // Sync to env so child processes and the CLI startup path pick it up + process.env.CC_HAHA_LOCALE = currentLocale } export function getLocale(): Locale {