From e268e69597177cdb165f55b6c005086f776aec47 Mon Sep 17 00:00:00 2001 From: Phantom Date: Fri, 7 Nov 2025 22:24:05 +0800 Subject: [PATCH 01/15] refactor(config): centralize home directory constant to shared config (#11158) Replace hardcoded '.cherrystudio' directory references with HOME_CHERRY_DIR constant --- packages/shared/config/constant.ts | 3 +++ src/main/services/CodeToolsService.ts | 13 +++++++------ src/main/services/MCPService.ts | 3 ++- src/main/services/OvmsManager.ts | 17 +++++++++-------- src/main/services/SpanCacheService.ts | 3 ++- src/main/services/ocr/builtin/OvOcrService.ts | 5 +++-- src/main/utils/file.ts | 6 +++--- src/main/utils/init.ts | 3 ++- src/main/utils/process.ts | 5 +++-- 9 files changed, 34 insertions(+), 24 deletions(-) diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 3b38592005..9d9240223a 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -470,3 +470,6 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [ }) } ] + +// resources/scripts should be maintained manually +export const HOME_CHERRY_DIR = '.cherrystudio' diff --git a/src/main/services/CodeToolsService.ts b/src/main/services/CodeToolsService.ts index 3a93a40d79..82c9c64f87 100644 --- a/src/main/services/CodeToolsService.ts +++ b/src/main/services/CodeToolsService.ts @@ -10,6 +10,7 @@ import { getBinaryName } from '@main/utils/process' import type { TerminalConfig, TerminalConfigWithCommand } from '@shared/config/constant' import { codeTools, + HOME_CHERRY_DIR, MACOS_TERMINALS, MACOS_TERMINALS_WITH_COMMANDS, terminalApps, @@ -66,7 +67,7 @@ class CodeToolsService { } public async getBunPath() { - const dir = path.join(os.homedir(), '.cherrystudio', 'bin') + const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const bunName = await getBinaryName('bun') const bunPath = path.join(dir, bunName) return bunPath @@ -362,7 +363,7 @@ class CodeToolsService { private async isPackageInstalled(cliTool: string): Promise { const executableName = await this.getCliExecutableName(cliTool) - const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) // Ensure bin directory exists @@ -389,7 +390,7 @@ class CodeToolsService { logger.info(`${cliTool} is installed, getting current version`) try { const executableName = await this.getCliExecutableName(cliTool) - const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) const { stdout } = await execAsync(`"${executablePath}" --version`, { @@ -500,7 +501,7 @@ class CodeToolsService { try { const packageName = await this.getPackageName(cliTool) const bunPath = await this.getBunPath() - const bunInstallPath = path.join(os.homedir(), '.cherrystudio') + const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR) const registryUrl = await this.getNpmRegistryUrl() const installEnvPrefix = isWin @@ -550,7 +551,7 @@ class CodeToolsService { const packageName = await this.getPackageName(cliTool) const bunPath = await this.getBunPath() const executableName = await this.getCliExecutableName(cliTool) - const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) logger.debug(`Package name: ${packageName}`) @@ -652,7 +653,7 @@ class CodeToolsService { baseCommand = `${baseCommand} ${configParams}` } - const bunInstallPath = path.join(os.homedir(), '.cherrystudio') + const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR) if (isInstalled) { // If already installed, run executable directly (with optional update message) diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index ba3340780b..3831d0af1e 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -30,6 +30,7 @@ import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js' import { nanoid } from '@reduxjs/toolkit' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import type { MCPProgressEvent } from '@shared/config/types' import { IpcChannel } from '@shared/IpcChannel' import { defaultAppHeaders } from '@shared/utils' @@ -715,7 +716,7 @@ class McpService { } public async getInstallInfo() { - const dir = path.join(os.homedir(), '.cherrystudio', 'bin') + const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const uvName = await getBinaryName('uv') const bunName = await getBinaryName('bun') const uvPath = path.join(dir, uvName) diff --git a/src/main/services/OvmsManager.ts b/src/main/services/OvmsManager.ts index f319200ac3..3a32d74ecf 100644 --- a/src/main/services/OvmsManager.ts +++ b/src/main/services/OvmsManager.ts @@ -3,6 +3,7 @@ import { homedir } from 'node:os' import { promisify } from 'node:util' import { loggerService } from '@logger' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import * as fs from 'fs-extra' import * as path from 'path' @@ -145,7 +146,7 @@ class OvmsManager { */ public async runOvms(): Promise<{ success: boolean; message?: string }> { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') const runBatPath = path.join(ovmsDir, 'run.bat') @@ -195,7 +196,7 @@ class OvmsManager { */ public async getOvmsStatus(): Promise<'not-installed' | 'not-running' | 'running'> { const homeDir = homedir() - const ovmsPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'ovms.exe') + const ovmsPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'ovms.exe') try { // Check if OVMS executable exists @@ -273,7 +274,7 @@ class OvmsManager { } const homeDir = homedir() - const configPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'models', 'config.json') + const configPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'models', 'config.json') try { if (!(await fs.pathExists(configPath))) { logger.warn(`Config file does not exist: ${configPath}`) @@ -304,7 +305,7 @@ class OvmsManager { private async applyModelPath(modelDirPath: string): Promise { const homeDir = homedir() - const patchDir = path.join(homeDir, '.cherrystudio', 'ovms', 'patch') + const patchDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'patch') if (!(await fs.pathExists(patchDir))) { return true } @@ -355,7 +356,7 @@ class OvmsManager { logger.info(`Adding model: ${modelName} with ID: ${modelId}, Source: ${modelSource}, Task: ${task}`) const homeDir = homedir() - const ovdndDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovdndDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const pathModel = path.join(ovdndDir, 'models', modelId) try { @@ -468,7 +469,7 @@ class OvmsManager { */ public async checkModelExists(modelId: string): Promise { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') try { @@ -495,7 +496,7 @@ class OvmsManager { */ public async updateModelConfig(modelName: string, modelId: string): Promise { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') try { @@ -548,7 +549,7 @@ class OvmsManager { */ public async getModels(): Promise { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') try { diff --git a/src/main/services/SpanCacheService.ts b/src/main/services/SpanCacheService.ts index 62707388a4..47a89d4327 100644 --- a/src/main/services/SpanCacheService.ts +++ b/src/main/services/SpanCacheService.ts @@ -3,6 +3,7 @@ import type { Attributes, SpanEntity, TokenUsage, TraceCache } from '@mcp-trace/ import { convertSpanToSpanEntity } from '@mcp-trace/trace-core' import { SpanStatusCode } from '@opentelemetry/api' import type { ReadableSpan } from '@opentelemetry/sdk-trace-base' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import fs from 'fs/promises' import * as os from 'os' import * as path from 'path' @@ -18,7 +19,7 @@ class SpanCacheService implements TraceCache { pri constructor() { - this.fileDir = path.join(os.homedir(), '.cherrystudio', 'trace') + this.fileDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'trace') } createSpan: (span: ReadableSpan) => void = (span: ReadableSpan) => { diff --git a/src/main/services/ocr/builtin/OvOcrService.ts b/src/main/services/ocr/builtin/OvOcrService.ts index 6e0eee1c37..052682be64 100644 --- a/src/main/services/ocr/builtin/OvOcrService.ts +++ b/src/main/services/ocr/builtin/OvOcrService.ts @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import { isWin } from '@main/constant' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import type { OcrOvConfig, OcrResult, SupportedOcrFile } from '@types' import { isImageFileMetadata } from '@types' import { exec } from 'child_process' @@ -13,7 +14,7 @@ import { OcrBaseService } from './OcrBaseService' const logger = loggerService.withContext('OvOcrService') const execAsync = promisify(exec) -const PATH_BAT_FILE = path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr', 'run.npu.bat') +const PATH_BAT_FILE = path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr', 'run.npu.bat') export class OvOcrService extends OcrBaseService { constructor() { @@ -30,7 +31,7 @@ export class OvOcrService extends OcrBaseService { } private getOvOcrPath(): string { - return path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr') } private getImgDir(): string { diff --git a/src/main/utils/file.ts b/src/main/utils/file.ts index 17155f423b..1432dccc8a 100644 --- a/src/main/utils/file.ts +++ b/src/main/utils/file.ts @@ -5,7 +5,7 @@ import os from 'node:os' import path from 'node:path' import { loggerService } from '@logger' -import { audioExts, documentExts, imageExts, MB, textExts, videoExts } from '@shared/config/constant' +import { audioExts, documentExts, HOME_CHERRY_DIR, imageExts, MB, textExts, videoExts } from '@shared/config/constant' import type { FileMetadata, NotesTreeNode } from '@types' import { FileTypes } from '@types' import chardet from 'chardet' @@ -160,7 +160,7 @@ export function getNotesDir() { } export function getConfigDir() { - return path.join(os.homedir(), '.cherrystudio', 'config') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'config') } export function getCacheDir() { @@ -172,7 +172,7 @@ export function getAppConfigDir(name: string) { } export function getMcpDir() { - return path.join(os.homedir(), '.cherrystudio', 'mcp') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'mcp') } /** diff --git a/src/main/utils/init.ts b/src/main/utils/init.ts index 63cf69e89b..20884b1eeb 100644 --- a/src/main/utils/init.ts +++ b/src/main/utils/init.ts @@ -3,6 +3,7 @@ import os from 'node:os' import path from 'node:path' import { isLinux, isPortable, isWin } from '@main/constant' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import { app } from 'electron' // Please don't import any other modules which is not node/electron built-in modules @@ -17,7 +18,7 @@ function hasWritePermission(path: string) { } function getConfigDir() { - return path.join(os.homedir(), '.cherrystudio', 'config') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'config') } export function initAppDataDir() { diff --git a/src/main/utils/process.ts b/src/main/utils/process.ts index f028f2d3c7..f36e86861d 100644 --- a/src/main/utils/process.ts +++ b/src/main/utils/process.ts @@ -1,4 +1,5 @@ import { loggerService } from '@logger' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import { spawn } from 'child_process' import fs from 'fs' import os from 'os' @@ -46,11 +47,11 @@ export async function getBinaryName(name: string): Promise { export async function getBinaryPath(name?: string): Promise { if (!name) { - return path.join(os.homedir(), '.cherrystudio', 'bin') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') } const binaryName = await getBinaryName(name) - const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binariesDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const binariesDirExists = fs.existsSync(binariesDir) return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName } From 9a10516b52496b20b52e7d319ac07a060cfe07c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 00:31:00 +0800 Subject: [PATCH 02/15] chore: update bun and uv versions (#11193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update bun and uv versions - Update bun from 1.2.17 to 1.3.1 - Update uv from 0.7.13 to 0.9.5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * refactor: update UV installer to support tar.gz format - Update UV package mappings from .zip to .tar.gz for macOS and Linux - Add RISCV64 Linux platform support - Implement dual extraction logic: - tar.gz extraction for macOS/Linux using tar command - zip extraction for Windows using StreamZip - Flatten directory structure during extraction - Maintain executable permissions on Unix-like systems 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * 🐛 fix: correct error handling in UV installer Remove ineffective error code 102 return from nested function. Chmod errors now properly propagate to outer try-catch block. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- resources/scripts/install-bun.js | 2 +- resources/scripts/install-uv.js | 96 ++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/resources/scripts/install-bun.js b/resources/scripts/install-bun.js index 1467a4cde4..33ee18d732 100644 --- a/resources/scripts/install-bun.js +++ b/resources/scripts/install-bun.js @@ -7,7 +7,7 @@ const { downloadWithRedirects } = require('./download') // Base URL for downloading bun binaries const BUN_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/bun/releases/download' -const DEFAULT_BUN_VERSION = '1.2.17' // Default fallback version +const DEFAULT_BUN_VERSION = '1.3.1' // Default fallback version // Mapping of platform+arch to binary package name const BUN_PACKAGES = { diff --git a/resources/scripts/install-uv.js b/resources/scripts/install-uv.js index 3dc8b3e477..c3d34efc33 100644 --- a/resources/scripts/install-uv.js +++ b/resources/scripts/install-uv.js @@ -7,28 +7,29 @@ const { downloadWithRedirects } = require('./download') // Base URL for downloading uv binaries const UV_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/uv/releases/download' -const DEFAULT_UV_VERSION = '0.7.13' +const DEFAULT_UV_VERSION = '0.9.5' // Mapping of platform+arch to binary package name const UV_PACKAGES = { - 'darwin-arm64': 'uv-aarch64-apple-darwin.zip', - 'darwin-x64': 'uv-x86_64-apple-darwin.zip', + 'darwin-arm64': 'uv-aarch64-apple-darwin.tar.gz', + 'darwin-x64': 'uv-x86_64-apple-darwin.tar.gz', 'win32-arm64': 'uv-aarch64-pc-windows-msvc.zip', 'win32-ia32': 'uv-i686-pc-windows-msvc.zip', 'win32-x64': 'uv-x86_64-pc-windows-msvc.zip', - 'linux-arm64': 'uv-aarch64-unknown-linux-gnu.zip', - 'linux-ia32': 'uv-i686-unknown-linux-gnu.zip', - 'linux-ppc64': 'uv-powerpc64-unknown-linux-gnu.zip', - 'linux-ppc64le': 'uv-powerpc64le-unknown-linux-gnu.zip', - 'linux-s390x': 'uv-s390x-unknown-linux-gnu.zip', - 'linux-x64': 'uv-x86_64-unknown-linux-gnu.zip', - 'linux-armv7l': 'uv-armv7-unknown-linux-gnueabihf.zip', + 'linux-arm64': 'uv-aarch64-unknown-linux-gnu.tar.gz', + 'linux-ia32': 'uv-i686-unknown-linux-gnu.tar.gz', + 'linux-ppc64': 'uv-powerpc64-unknown-linux-gnu.tar.gz', + 'linux-ppc64le': 'uv-powerpc64le-unknown-linux-gnu.tar.gz', + 'linux-riscv64': 'uv-riscv64gc-unknown-linux-gnu.tar.gz', + 'linux-s390x': 'uv-s390x-unknown-linux-gnu.tar.gz', + 'linux-x64': 'uv-x86_64-unknown-linux-gnu.tar.gz', + 'linux-armv7l': 'uv-armv7-unknown-linux-gnueabihf.tar.gz', // MUSL variants - 'linux-musl-arm64': 'uv-aarch64-unknown-linux-musl.zip', - 'linux-musl-ia32': 'uv-i686-unknown-linux-musl.zip', - 'linux-musl-x64': 'uv-x86_64-unknown-linux-musl.zip', - 'linux-musl-armv6l': 'uv-arm-unknown-linux-musleabihf.zip', - 'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.zip' + 'linux-musl-arm64': 'uv-aarch64-unknown-linux-musl.tar.gz', + 'linux-musl-ia32': 'uv-i686-unknown-linux-musl.tar.gz', + 'linux-musl-x64': 'uv-x86_64-unknown-linux-musl.tar.gz', + 'linux-musl-armv6l': 'uv-arm-unknown-linux-musleabihf.tar.gz', + 'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.tar.gz' } /** @@ -56,6 +57,7 @@ async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, is const downloadUrl = `${UV_RELEASE_BASE_URL}/${version}/${packageName}` const tempdir = os.tmpdir() const tempFilename = path.join(tempdir, packageName) + const isTarGz = packageName.endsWith('.tar.gz') try { console.log(`Downloading uv ${version} for ${platformKey}...`) @@ -65,34 +67,58 @@ async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, is console.log(`Extracting ${packageName} to ${binDir}...`) - const zip = new StreamZip.async({ file: tempFilename }) + if (isTarGz) { + // Use tar command to extract tar.gz files (macOS and Linux) + const tempExtractDir = path.join(tempdir, `uv-extract-${Date.now()}`) + fs.mkdirSync(tempExtractDir, { recursive: true }) - // Get all entries in the zip file - const entries = await zip.entries() + execSync(`tar -xzf "${tempFilename}" -C "${tempExtractDir}"`, { stdio: 'inherit' }) - // Extract files directly to binDir, flattening the directory structure - for (const entry of Object.values(entries)) { - if (!entry.isDirectory) { - // Get just the filename without path - const filename = path.basename(entry.name) - const outputPath = path.join(binDir, filename) - - console.log(`Extracting ${entry.name} -> ${filename}`) - await zip.extract(entry.name, outputPath) - // Make executable files executable on Unix-like systems - if (platform !== 'win32') { - try { + // Find all files in the extracted directory and move them to binDir + const findAndMoveFiles = (dir) => { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + findAndMoveFiles(fullPath) + } else { + const filename = path.basename(entry.name) + const outputPath = path.join(binDir, filename) + fs.copyFileSync(fullPath, outputPath) + console.log(`Extracted ${entry.name} -> ${outputPath}`) + // Make executable on Unix-like systems fs.chmodSync(outputPath, 0o755) - } catch (chmodError) { - console.error(`Warning: Failed to set executable permissions on ${filename}`) - return 102 } } - console.log(`Extracted ${entry.name} -> ${outputPath}`) } + + findAndMoveFiles(tempExtractDir) + + // Clean up temporary extraction directory + fs.rmSync(tempExtractDir, { recursive: true }) + } else { + // Use StreamZip for zip files (Windows) + const zip = new StreamZip.async({ file: tempFilename }) + + // Get all entries in the zip file + const entries = await zip.entries() + + // Extract files directly to binDir, flattening the directory structure + for (const entry of Object.values(entries)) { + if (!entry.isDirectory) { + // Get just the filename without path + const filename = path.basename(entry.name) + const outputPath = path.join(binDir, filename) + + console.log(`Extracting ${entry.name} -> ${filename}`) + await zip.extract(entry.name, outputPath) + console.log(`Extracted ${entry.name} -> ${outputPath}`) + } + } + + await zip.close() } - await zip.close() fs.unlinkSync(tempFilename) console.log(`Successfully installed uv ${version} for ${platform}-${arch}`) return 0 From 58afbe8a797b72b18d7c2160487145a9d8941c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 00:31:20 +0800 Subject: [PATCH 03/15] refactor(config): optimize oxlint configuration by removing redundant default rules (#11192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove ~60 redundant rule declarations that match oxlint's default behavior. This reduces the config file by 28% (211 -> 152 lines) while maintaining identical linting behavior. Changes: - Remove default error-level rules (constructor-super, no-debugger, etc.) - Retain only custom configurations that differ from defaults - Keep all environment overrides and plugin settings unchanged - Preserve all modified severity levels (warn) and disabled rules (off) Benefits: - Improved readability: clearly shows project-specific lint strategy - Reduced maintenance: no need to sync with oxlint default updates - Smaller config: 46% fewer rule declarations (130 -> 70 rules) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- .oxlintrc.json | 83 ++++++-------------------------------------------- 1 file changed, 10 insertions(+), 73 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 5d63538e2c..0b2e1a2ab5 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -22,7 +22,6 @@ "eslint.config.mjs" ], "overrides": [ - // set different env { "env": { "node": true @@ -55,74 +54,16 @@ "files": ["src/preload/**"] } ], - // We don't use the React plugin here because its behavior differs slightly from that of ESLint's React plugin. "plugins": ["unicorn", "typescript", "oxc", "import"], "rules": { - "constructor-super": "error", - "for-direction": "error", - "getter-return": "error", "no-array-constructor": "off", - // "import/no-cycle": "error", // tons of error, bro - "no-async-promise-executor": "error", "no-caller": "warn", - "no-case-declarations": "error", - "no-class-assign": "error", - "no-compare-neg-zero": "error", - "no-cond-assign": "error", - "no-const-assign": "error", - "no-constant-binary-expression": "error", - "no-constant-condition": "error", - "no-control-regex": "error", - "no-debugger": "error", - "no-delete-var": "error", - "no-dupe-args": "error", - "no-dupe-class-members": "error", - "no-dupe-else-if": "error", - "no-dupe-keys": "error", - "no-duplicate-case": "error", - "no-empty": "error", - "no-empty-character-class": "error", - "no-empty-pattern": "error", - "no-empty-static-block": "error", "no-eval": "warn", - "no-ex-assign": "error", - "no-extra-boolean-cast": "error", "no-fallthrough": "warn", - "no-func-assign": "error", - "no-global-assign": "error", - "no-import-assign": "error", - "no-invalid-regexp": "error", - "no-irregular-whitespace": "error", - "no-loss-of-precision": "error", - "no-misleading-character-class": "error", - "no-new-native-nonconstructor": "error", - "no-nonoctal-decimal-escape": "error", - "no-obj-calls": "error", - "no-octal": "error", - "no-prototype-builtins": "error", - "no-redeclare": "error", - "no-regex-spaces": "error", - "no-self-assign": "error", - "no-setter-return": "error", - "no-shadow-restricted-names": "error", - "no-sparse-arrays": "error", - "no-this-before-super": "error", "no-unassigned-vars": "warn", - "no-undef": "error", - "no-unexpected-multiline": "error", - "no-unreachable": "error", - "no-unsafe-finally": "error", - "no-unsafe-negation": "error", - "no-unsafe-optional-chaining": "error", - "no-unused-expressions": "off", // this rule disallow us to use expression to call function, like `condition && fn()` - "no-unused-labels": "error", - "no-unused-private-class-members": "error", + "no-unused-expressions": "off", "no-unused-vars": ["warn", { "caughtErrors": "none" }], - "no-useless-backreference": "error", - "no-useless-catch": "error", - "no-useless-escape": "error", "no-useless-rename": "warn", - "no-with": "error", "oxc/bad-array-method-on-arguments": "warn", "oxc/bad-char-at-comparison": "warn", "oxc/bad-comparison-sequence": "warn", @@ -134,19 +75,17 @@ "oxc/erasing-op": "warn", "oxc/missing-throw": "warn", "oxc/number-arg-out-of-range": "warn", - "oxc/only-used-in-recursion": "off", // manually off bacause of existing warning. may turn it on in the future + "oxc/only-used-in-recursion": "off", "oxc/uninvoked-array-callback": "warn", - "require-yield": "error", "typescript/await-thenable": "warn", - // "typescript/ban-ts-comment": "error", - "typescript/no-array-constructor": "error", "typescript/consistent-type-imports": "error", + "typescript/no-array-constructor": "error", "typescript/no-array-delete": "warn", "typescript/no-base-to-string": "warn", "typescript/no-duplicate-enum-values": "error", "typescript/no-duplicate-type-constituents": "warn", "typescript/no-empty-object-type": "off", - "typescript/no-explicit-any": "off", // not safe but too many errors + "typescript/no-explicit-any": "off", "typescript/no-extra-non-null-assertion": "error", "typescript/no-floating-promises": "warn", "typescript/no-for-in-array": "warn", @@ -155,7 +94,7 @@ "typescript/no-misused-new": "error", "typescript/no-misused-spread": "warn", "typescript/no-namespace": "error", - "typescript/no-non-null-asserted-optional-chain": "off", // it's off now. but may turn it on. + "typescript/no-non-null-asserted-optional-chain": "off", "typescript/no-redundant-type-constituents": "warn", "typescript/no-require-imports": "off", "typescript/no-this-alias": "error", @@ -173,20 +112,18 @@ "typescript/triple-slash-reference": "error", "typescript/unbound-method": "warn", "unicorn/no-await-in-promise-methods": "warn", - "unicorn/no-empty-file": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-empty-file": "off", "unicorn/no-invalid-fetch-options": "warn", "unicorn/no-invalid-remove-event-listener": "warn", - "unicorn/no-new-array": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-new-array": "off", "unicorn/no-single-promise-in-promise-methods": "warn", - "unicorn/no-thenable": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-thenable": "off", "unicorn/no-unnecessary-await": "warn", "unicorn/no-useless-fallback-in-spread": "warn", "unicorn/no-useless-length-check": "warn", - "unicorn/no-useless-spread": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-useless-spread": "off", "unicorn/prefer-set-size": "warn", - "unicorn/prefer-string-starts-ends-with": "warn", - "use-isnan": "error", - "valid-typeof": "error" + "unicorn/prefer-string-starts-ends-with": "warn" }, "settings": { "jsdoc": { From 57d9a31c0f5c43def814bf187d7b3a6031c9ea71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 00:31:35 +0800 Subject: [PATCH 04/15] refactor(migrate): consolidate migrations into version 172 (#11194) * refactor(migrate): consolidate migrations into version 172 Consolidates migrations 162-166 into a single migration 172 to fix data inconsistencies between release/v1.6.x and v1.7.0-x versions. This ensures a single, consistent migration path and corrects data deviations that occurred during version upgrades. Changes: - Remove separate migrations 162-166 - Add consolidated migration 172 that includes: - Mini app additions (ling, huggingchat) - OCR provider updates (ovocr) - Agent to preset migration - Sidebar icon updates (agents -> store) - LLM provider Anthropic API host configurations - Assistant preset settings initialization Co-Authored-By: Claude * refactor(store): update persist version to 172 Update the redux-persist version number from 171 to 172 to match the consolidated migration version. Co-Authored-By: Claude * fix(migrate): add missing break statement in switch case Add missing break statement after 'grok' case to prevent fall-through to 'cherryin' case. Also add break statement for 'longcat' case. Co-Authored-By: Claude --------- Co-authored-by: Claude --- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 218 +++++++++++++----------------- 2 files changed, 93 insertions(+), 127 deletions(-) diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index e68ada058f..14adb6cde0 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -67,7 +67,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 171, + version: 172, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index ba9bf21f45..d15fe05cb0 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2623,132 +2623,6 @@ const migrateConfig = { return state } }, - '162': (state: RootState) => { - try { - // @ts-ignore - if (state?.agents?.agents) { - // @ts-ignore - state.assistants.presets = [...state.agents.agents] - // @ts-ignore - delete state.agents.agents - } - - if (state.settings.sidebarIcons) { - state.settings.sidebarIcons.visible = state.settings.sidebarIcons.visible.map((icon) => { - // @ts-ignore - return icon === 'agents' ? 'store' : icon - }) - state.settings.sidebarIcons.disabled = state.settings.sidebarIcons.disabled.map((icon) => { - // @ts-ignore - return icon === 'agents' ? 'store' : icon - }) - } - - state.llm.providers.forEach((provider) => { - if (provider.anthropicApiHost) { - return - } - - switch (provider.id) { - case 'deepseek': - provider.anthropicApiHost = 'https://api.deepseek.com/anthropic' - break - case 'moonshot': - provider.anthropicApiHost = 'https://api.moonshot.cn/anthropic' - break - case 'zhipu': - provider.anthropicApiHost = 'https://open.bigmodel.cn/api/anthropic' - break - case 'dashscope': - provider.anthropicApiHost = 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy' - break - case 'modelscope': - provider.anthropicApiHost = 'https://api-inference.modelscope.cn' - break - case 'aihubmix': - provider.anthropicApiHost = 'https://aihubmix.com' - break - case 'new-api': - provider.anthropicApiHost = 'http://localhost:3000' - break - case 'grok': - provider.anthropicApiHost = 'https://api.x.ai' - } - }) - return state - } catch (error) { - logger.error('migrate 162 error', error as Error) - return state - } - }, - '163': (state: RootState) => { - try { - addOcrProvider(state, BUILTIN_OCR_PROVIDERS_MAP.ovocr) - state.llm.providers.forEach((provider) => { - if (provider.id === 'cherryin') { - provider.anthropicApiHost = 'https://open.cherryin.net' - } - }) - state.paintings.ovms_paintings = [] - return state - } catch (error) { - logger.error('migrate 163 error', error as Error) - return state - } - }, - '164': (state: RootState) => { - try { - addMiniApp(state, 'ling') - return state - } catch (error) { - logger.error('migrate 164 error', error as Error) - return state - } - }, - '165': (state: RootState) => { - try { - addMiniApp(state, 'huggingchat') - return state - } catch (error) { - logger.error('migrate 165 error', error as Error) - return state - } - }, - '166': (state: RootState) => { - try { - if (state.assistants.presets === undefined) { - state.assistants.presets = [] - } - state.assistants.presets.forEach((preset) => { - if (!preset.settings) { - preset.settings = DEFAULT_ASSISTANT_SETTINGS - } else if (!preset.settings.toolUseMode) { - preset.settings.toolUseMode = DEFAULT_ASSISTANT_SETTINGS.toolUseMode - } - }) - // 更新阿里云百炼的 Anthropic API 地址 - const dashscopeProvider = state.llm.providers.find((provider) => provider.id === 'dashscope') - if (dashscopeProvider) { - dashscopeProvider.anthropicApiHost = 'https://dashscope.aliyuncs.com/apps/anthropic' - } - - state.llm.providers.forEach((provider) => { - if (provider.id === SystemProviderIds['new-api'] && provider.type !== 'new-api') { - provider.type = 'new-api' - } - if (provider.id === SystemProviderIds.longcat) { - // https://longcat.chat/platform/docs/zh/#anthropic-api-%E6%A0%BC%E5%BC%8F - if (!provider.anthropicApiHost) { - provider.anthropicApiHost = 'https://api.longcat.chat/anthropic' - } - } - }) - return state - } catch (error) { - logger.error('migrate 166 error', error as Error) - return state - } - }, '167': (state: RootState) => { try { addProvider(state, 'huggingface') @@ -2817,6 +2691,98 @@ const migrateConfig = { logger.error('migrate 171 error', error as Error) return state } + }, + '172': (state: RootState) => { + try { + // Add ling and huggingchat mini apps + addMiniApp(state, 'ling') + addMiniApp(state, 'huggingchat') + + // Add ovocr provider and clear ovms paintings + addOcrProvider(state, BUILTIN_OCR_PROVIDERS_MAP.ovocr) + if (isEmpty(state.paintings.ovms_paintings)) { + state.paintings.ovms_paintings = [] + } + + // Migrate agents to assistants presets + // @ts-ignore + if (state?.agents?.agents) { + // @ts-ignore + state.assistants.presets = [...state.agents.agents] + // @ts-ignore + delete state.agents.agents + } + + // Initialize assistants presets + if (state.assistants.presets === undefined) { + state.assistants.presets = [] + } + + // Migrate assistants presets + state.assistants.presets.forEach((preset) => { + if (!preset.settings) { + preset.settings = DEFAULT_ASSISTANT_SETTINGS + } else if (!preset.settings.toolUseMode) { + preset.settings.toolUseMode = DEFAULT_ASSISTANT_SETTINGS.toolUseMode + } + }) + + // Migrate sidebar icons + if (state.settings.sidebarIcons) { + state.settings.sidebarIcons.visible = state.settings.sidebarIcons.visible.map((icon) => { + // @ts-ignore + return icon === 'agents' ? 'store' : icon + }) + state.settings.sidebarIcons.disabled = state.settings.sidebarIcons.disabled.map((icon) => { + // @ts-ignore + return icon === 'agents' ? 'store' : icon + }) + } + + // Migrate llm providers + state.llm.providers.forEach((provider) => { + if (provider.id === SystemProviderIds['new-api'] && provider.type !== 'new-api') { + provider.type = 'new-api' + } + + switch (provider.id) { + case 'deepseek': + provider.anthropicApiHost = 'https://api.deepseek.com/anthropic' + break + case 'moonshot': + provider.anthropicApiHost = 'https://api.moonshot.cn/anthropic' + break + case 'zhipu': + provider.anthropicApiHost = 'https://open.bigmodel.cn/api/anthropic' + break + case 'dashscope': + provider.anthropicApiHost = 'https://dashscope.aliyuncs.com/apps/anthropic' + break + case 'modelscope': + provider.anthropicApiHost = 'https://api-inference.modelscope.cn' + break + case 'aihubmix': + provider.anthropicApiHost = 'https://aihubmix.com' + break + case 'new-api': + provider.anthropicApiHost = 'http://localhost:3000' + break + case 'grok': + provider.anthropicApiHost = 'https://api.x.ai' + break + case 'cherryin': + provider.anthropicApiHost = 'https://open.cherryin.net' + break + case 'longcat': + provider.anthropicApiHost = 'https://api.longcat.chat/anthropic' + break + } + }) + return state + } catch (error) { + logger.error('migrate 172 error', error as Error) + return state + } } } From ed453750fea208b0b536a1875d67df305f0177a1 Mon Sep 17 00:00:00 2001 From: cheng chao Date: Sun, 9 Nov 2025 01:45:25 +0800 Subject: [PATCH 05/15] fix(mcp): resolve OAuth callback page hanging and add i18n support (#11195) - Fix OAuth callback server not sending HTTP response, causing browser to hang - Add internationalization support for OAuth callback page (10 languages) - Simplify callback page design with clean white background - Improve user experience with localized success messages Changes: - src/main/services/mcp/oauth/callback.ts: Add HTTP response to OAuth callback - src/renderer/src/i18n/: Add callback page translations for all supported languages Signed-off-by: charles --- src/main/services/mcp/oauth/callback.ts | 81 ++++++++++++++++++++++ src/renderer/src/i18n/locales/en-us.json | 6 ++ src/renderer/src/i18n/locales/zh-cn.json | 6 ++ src/renderer/src/i18n/locales/zh-tw.json | 6 ++ src/renderer/src/i18n/translate/de-de.json | 6 ++ src/renderer/src/i18n/translate/el-gr.json | 6 ++ src/renderer/src/i18n/translate/es-es.json | 6 ++ src/renderer/src/i18n/translate/fr-fr.json | 6 ++ src/renderer/src/i18n/translate/ja-jp.json | 6 ++ src/renderer/src/i18n/translate/pt-pt.json | 6 ++ src/renderer/src/i18n/translate/ru-ru.json | 6 ++ 11 files changed, 141 insertions(+) diff --git a/src/main/services/mcp/oauth/callback.ts b/src/main/services/mcp/oauth/callback.ts index 81d435f867..c13ecd5c07 100644 --- a/src/main/services/mcp/oauth/callback.ts +++ b/src/main/services/mcp/oauth/callback.ts @@ -1,4 +1,6 @@ import { loggerService } from '@logger' +import { configManager } from '@main/services/ConfigManager' +import { locales } from '@main/utils/locales' import type EventEmitter from 'events' import http from 'http' import { URL } from 'url' @@ -7,6 +9,36 @@ import type { OAuthCallbackServerOptions } from './types' const logger = loggerService.withContext('MCP:OAuthCallbackServer') +function getTranslation(key: string): string { + const language = configManager.getLanguage() + const localeData = locales[language] + + if (!localeData) { + logger.warn(`No locale data found for language: ${language}`) + return key + } + + const translations = localeData.translation as any + if (!translations) { + logger.warn(`No translations found for language: ${language}`) + return key + } + + const keys = key.split('.') + let value = translations + + for (const k of keys) { + if (value && typeof value === 'object' && k in value) { + value = value[k] + } else { + logger.warn(`Translation key not found: ${key} (failed at: ${k})`) + return key // fallback to key if translation not found + } + } + + return typeof value === 'string' ? value : key +} + export class CallBackServer { private server: Promise private events: EventEmitter @@ -28,6 +60,55 @@ export class CallBackServer { if (code) { // Emit the code event this.events.emit('auth-code-received', code) + // Send success response to browser + const title = getTranslation('settings.mcp.oauth.callback.title') + const message = getTranslation('settings.mcp.oauth.callback.message') + + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) + res.end(` + + + + + ${title} + + + +
+

${title}

+

${message}

+
+ + + `) + } else { + res.writeHead(400, { 'Content-Type': 'text/plain' }) + res.end('Missing authorization code') } } catch (error) { logger.error('Error processing OAuth callback:', error as Error) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index fbffb92777..0695075051 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -3863,6 +3863,12 @@ "usage": "Usage", "version": "Version" }, + "oauth": { + "callback": { + "message": "You can close this page and return to Cherry Studio", + "title": "Authentication Successful" + } + }, "prompts": { "arguments": "Arguments", "availablePrompts": "Available Prompts", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 26fb5dbe75..37659a7dd7 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -3863,6 +3863,12 @@ "usage": "用法", "version": "版本" }, + "oauth": { + "callback": { + "message": "您可以关闭此页面并返回 Cherry Studio", + "title": "认证成功" + } + }, "prompts": { "arguments": "参数", "availablePrompts": "可用提示", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 8b16b3e94e..5016bcfe1d 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -3863,6 +3863,12 @@ "usage": "用法", "version": "版本" }, + "oauth": { + "callback": { + "message": "您可以關閉此頁面並返回 Cherry Studio", + "title": "認證成功" + } + }, "prompts": { "arguments": "參數", "availablePrompts": "可用提示", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index d94e74422b..59a1f8489e 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -3863,6 +3863,12 @@ "usage": "Verwendung", "version": "Version" }, + "oauth": { + "callback": { + "message": "Sie können diese Seite schließen und zu Cherry Studio zurückkehren", + "title": "Authentifizierung erfolgreich" + } + }, "prompts": { "arguments": "Parameter", "availablePrompts": "Verfügbare Prompts", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 069cd8da8b..c77b757c05 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -3863,6 +3863,12 @@ "usage": "Χρήση", "version": "Έκδοση" }, + "oauth": { + "callback": { + "message": "Μπορείτε να κλείσετε αυτήν τη σελίδα και να επιστρέψετε στο Cherry Studio", + "title": "Επιτυχής Ταυτοποίηση" + } + }, "prompts": { "arguments": "Ορίσματα", "availablePrompts": "Διαθέσιμες Υποδείξεις", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index f3c7342b21..722730c645 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -3863,6 +3863,12 @@ "usage": "Uso", "version": "Versión" }, + "oauth": { + "callback": { + "message": "Puede cerrar esta página y volver a Cherry Studio", + "title": "Autenticación Exitosa" + } + }, "prompts": { "arguments": "Argumentos", "availablePrompts": "Indicaciones disponibles", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 79dd7c4141..47ceca85e5 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -3863,6 +3863,12 @@ "usage": "Utilisation", "version": "Version" }, + "oauth": { + "callback": { + "message": "Vous pouvez fermer cette page et retourner à Cherry Studio", + "title": "Authentification Réussie" + } + }, "prompts": { "arguments": "Arguments", "availablePrompts": "Invites disponibles", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 9655d8fb7b..409a5ba997 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -3863,6 +3863,12 @@ "usage": "使用法", "version": "バージョン" }, + "oauth": { + "callback": { + "message": "このページを閉じてCherry Studioに戻ることができます", + "title": "認証成功" + } + }, "prompts": { "arguments": "引数", "availablePrompts": "利用可能なプロンプト", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 4be4a3dd97..b7cd03ceca 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -3863,6 +3863,12 @@ "usage": "Uso", "version": "Versão" }, + "oauth": { + "callback": { + "message": "Você pode fechar esta página e retornar ao Cherry Studio", + "title": "Autenticação Bem-Sucedida" + } + }, "prompts": { "arguments": "Argumentos", "availablePrompts": "Dicas disponíveis", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 241fde8fd3..87921d9c5e 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -3863,6 +3863,12 @@ "usage": "Использование", "version": "Версия" }, + "oauth": { + "callback": { + "message": "Вы можете закрыть эту страницу и вернуться в Cherry Studio", + "title": "Аутентификация Успешна" + } + }, "prompts": { "arguments": "Аргументы", "availablePrompts": "Доступные подсказки", From 85a628f8dd211682b3fd235c5ba676fc94e22b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 12:06:50 +0800 Subject: [PATCH 06/15] style(ui): center plugin browser tabs (#11205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💄 style(ui): center plugin browser tabs Center the tab items in the plugin browser component for better visual alignment and improved user experience. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- .../pages/settings/AgentSettings/components/PluginBrowser.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/src/pages/settings/AgentSettings/components/PluginBrowser.tsx b/src/renderer/src/pages/settings/AgentSettings/components/PluginBrowser.tsx index 9251a47ae6..6d3c21e4dc 100644 --- a/src/renderer/src/pages/settings/AgentSettings/components/PluginBrowser.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/components/PluginBrowser.tsx @@ -263,6 +263,7 @@ export const PluginBrowser: FC = ({ items={pluginTypeTabItems} className="w-full" size="small" + centered /> From d5826c2dc7bd626eadc00a9743a38bf2ab363fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 12:27:15 +0800 Subject: [PATCH 07/15] fix(ui): truncate long Bash command in tag with popover (#11200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix(ui): truncate long Bash command in tag with popover Add automatic truncation for Bash commands exceeding 200 characters in the tag display. When truncated, users can hover over the tag to view the full command in a popover. - Add MAX_TAG_LENGTH constant (200 chars) - Implement command truncation logic - Add Popover component for full command display on hover - Prevent UI overflow issues with long commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * ♻️ refactor(ui): reduce MAX_TAG_LENGTH to 100 for smaller screens Reduce the command truncation threshold from 200 to 100 characters to better support smaller screen sizes and improve readability. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * docs: remove emoji requirement from conventional commits Update commit message guidelines to use standard Conventional Commit format without emoji prefixes for better compatibility and consistency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- CLAUDE.md | 3 +-- .../Tools/MessageAgentTools/BashTool.tsx | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0728605824..372bff256c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,8 +10,7 @@ This file provides guidance to AI coding assistants when working with code in th - **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`. - **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references. - **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications. -- **Write conventional commits with emoji**: Commit small, focused changes using emoji-prefixed Conventional Commit messages (e.g., `✨ feat:`, `🐛 fix:`, `♻️ refactor:`, ` -📝 docs:`). +- **Write conventional commits**: Commit small, focused changes using Conventional Commit messages (e.g., `feat:`, `fix:`, `refactor:`, `docs:`). ## Development Commands diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx index d92b6461a4..9b9d98054d 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx @@ -1,10 +1,12 @@ import type { CollapseProps } from 'antd' -import { Tag } from 'antd' +import { Popover, Tag } from 'antd' import { Terminal } from 'lucide-react' import { ToolTitle } from './GenericTools' import type { BashToolInput as BashToolInputType, BashToolOutput as BashToolOutputType } from './types' +const MAX_TAG_LENGTH = 100 + export function BashTool({ input, output @@ -15,6 +17,13 @@ export function BashTool({ // 如果有输出,计算输出行数 const outputLines = output ? output.split('\n').length : 0 + // 处理命令字符串的截断 + const command = input.command + const needsTruncate = command.length > MAX_TAG_LENGTH + const displayCommand = needsTruncate ? `${command.slice(0, MAX_TAG_LENGTH)}...` : command + + const tagContent = {displayCommand} + return { key: 'tool', label: ( @@ -26,7 +35,15 @@ export function BashTool({ stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined} />
- {input.command} + {needsTruncate ? ( + {command}
} + trigger="hover"> + {tagContent} + + ) : ( + tagContent + )} ), From 66f66fe08ea8ebaa3d8561746b14b29b787e4c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 17:50:41 +0800 Subject: [PATCH 08/15] fix: prevent MCP card description text from overflowing dialog width (#11203) * fix: prevent MCP card description text from overflowing dialog width Add whitespace-pre-wrap and break-all classes to the MCP server description text in Agent Settings to ensure long descriptions wrap properly within the dialog bounds instead of causing layout overflow issues. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * feat: display MCP server logo in Agent Settings tooling section Add logo display support for MCP servers in the Agent Settings tooling section. When a server has a logoUrl defined, it will now be shown next to the server name as a 20x20px rounded image, matching the design pattern used in MCPSettings. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- .../AgentSettings/ToolingSettings.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx index e353799c5b..40dc2249a6 100644 --- a/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx @@ -459,11 +459,22 @@ export const ToolingSettings: FC = ({ agentBase, upda key={server.id} className="border border-default-200" title={ -
-
- {server.name} +
+
+
+ {server.logoUrl && ( + {`${server.name} + )} + {server.name} +
{server.description ? ( - {server.description} + + {server.description} + ) : null}
Date: Sun, 9 Nov 2025 17:53:05 +0800 Subject: [PATCH 09/15] fix(ErrorBlock): reorder field (#11057) feat(ErrorBlock): add responseBody display above requestBodyValues Move responseBody display to appear before requestBodyValues for better error flow readability --- .../pages/home/Messages/Blocks/ErrorBlock.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx index adf38d9aed..f3ed182aee 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx @@ -303,7 +303,7 @@ const BuiltinError = ({ error }: { error: SerializedError }) => { ) } -// 作为 base,渲染公共字段,应当在 ErrorDetailList 中渲染 +// Base component to render common fields, should be rendered inside ErrorDetailList const AiSdkErrorBase = ({ error }: { error: SerializedAiSdkError }) => { const { t } = useTranslation() const { highlightCode } = useCodeStyle() @@ -368,6 +368,13 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => { {isSerializedAiSdkAPICallError(error) && ( <> + {error.responseBody && ( + + {t('error.responseBody')}: + + + )} + {error.requestBodyValues && ( {t('error.requestBodyValues')}: @@ -392,13 +399,6 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => { )} - {error.responseBody && ( - - {t('error.responseBody')}: - - - )} - {error.data && ( {t('error.data')}: From 9013fcba14ccdfe3fd8605c5001a3e5f532555e8 Mon Sep 17 00:00:00 2001 From: Phantom Date: Sun, 9 Nov 2025 18:17:34 +0800 Subject: [PATCH 10/15] fix(useMessageOperations): skip timestamp update for UI-only changes (#10927) Prevent unnecessary message updates when only UI-related states change by checking the update keys and skipping timestamp updates in those cases --- src/renderer/src/hooks/useMessageOperations.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/hooks/useMessageOperations.ts b/src/renderer/src/hooks/useMessageOperations.ts index b1836f3fa7..b3b920085a 100644 --- a/src/renderer/src/hooks/useMessageOperations.ts +++ b/src/renderer/src/hooks/useMessageOperations.ts @@ -20,11 +20,11 @@ import { updateMessageAndBlocksThunk, updateTranslationBlockThunk } from '@renderer/store/thunk/messageThunk' -import type { Assistant, Model, Topic, TranslateLanguageCode } from '@renderer/types' +import { type Assistant, type Model, objectKeys, type Topic, type TranslateLanguageCode } from '@renderer/types' import type { Message, MessageBlock } from '@renderer/types/newMessage' import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { abortCompletion } from '@renderer/utils/abortController' -import { throttle } from 'lodash' +import { difference, throttle } from 'lodash' import { useCallback } from 'react' const logger = loggerService.withContext('UseMessageOperations') @@ -82,10 +82,12 @@ export function useMessageOperations(topic: Topic) { logger.error('[editMessage] Topic prop is not valid.') return } - + const uiStates = ['multiModelMessageStyle', 'foldSelected'] as const satisfies (keyof Message)[] + const extraUpdate = difference(objectKeys(updates), uiStates) + const isUiUpdateOnly = extraUpdate.length === 0 const messageUpdates: Partial & Pick = { id: messageId, - updatedAt: new Date().toISOString(), + updatedAt: isUiUpdateOnly ? undefined : new Date().toISOString(), ...updates } From 120ac122ebbf09f3bca9299ebb8a062a744ed8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 23:24:35 +0800 Subject: [PATCH 11/15] fix(ui): resolve sidebar tooltip overlap with window controls on macOS (#11216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #11125 Add placement="right" to sidebar toggle tooltips in ChatNavbar, Navbar, and Notes HeaderNavbar to prevent tooltips from overlapping with macOS window control buttons (minimize, maximize, close) in the top-left corner. This ensures tooltips appear to the right of the toggle buttons rather than above them, avoiding overlap with native window controls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- src/renderer/src/pages/home/ChatNavbar.tsx | 2 +- src/renderer/src/pages/home/Navbar.tsx | 2 +- src/renderer/src/pages/notes/HeaderNavbar.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/pages/home/ChatNavbar.tsx b/src/renderer/src/pages/home/ChatNavbar.tsx index bdc000f223..17b45ad189 100644 --- a/src/renderer/src/pages/home/ChatNavbar.tsx +++ b/src/renderer/src/pages/home/ChatNavbar.tsx @@ -84,7 +84,7 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo )} {isTopNavbar && !showAssistants && ( - + toggleShowAssistants()} style={{ marginRight: 8 }}> diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index dd3a215922..0d93a4bd01 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -98,7 +98,7 @@ const HeaderNavbar: FC = ({ paddingRight: 0, minWidth: 'auto' }}> - + toggleShowAssistants()}> diff --git a/src/renderer/src/pages/notes/HeaderNavbar.tsx b/src/renderer/src/pages/notes/HeaderNavbar.tsx index 044e87bbe6..a2d6e66039 100644 --- a/src/renderer/src/pages/notes/HeaderNavbar.tsx +++ b/src/renderer/src/pages/notes/HeaderNavbar.tsx @@ -181,7 +181,7 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand )} {!showWorkspace && ( - + From e43562423e7b639e1d0e4cf55dfa1c552ffc4610 Mon Sep 17 00:00:00 2001 From: fullex <106392080+0xfullex@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:14:32 +0800 Subject: [PATCH 12/15] refactor: remove unused files and configurations (#11176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor: remove unused resources/js directory and references Remove legacy resources/js directory (bridge.js and utils.js) that was left over after minapp.html removal in commit 461458e5e. Also update .oxlintrc.json to remove the unused resources/js/** file pattern. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * ♻️ refactor: remove additional unused files - Remove duplicate ipService.js (superseded by TypeScript version in src/main/utils/) - Remove unused components.json (shadcn config with non-existent target directory) - Remove unused context-menu.tsx component (no imports found) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- .oxlintrc.json | 3 +- components.json | 21 --- resources/js/bridge.js | 36 ----- resources/js/utils.js | 5 - resources/scripts/ipService.js | 88 ------------ src/renderer/src/ui/context-menu.tsx | 207 --------------------------- 6 files changed, 1 insertion(+), 359 deletions(-) delete mode 100644 components.json delete mode 100644 resources/js/bridge.js delete mode 100644 resources/js/utils.js delete mode 100644 resources/scripts/ipService.js delete mode 100644 src/renderer/src/ui/context-menu.tsx diff --git a/.oxlintrc.json b/.oxlintrc.json index 0b2e1a2ab5..329d08c043 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -35,8 +35,7 @@ "files": [ "src/renderer/**/*.{ts,tsx}", "packages/aiCore/**", - "packages/extension-table-plus/**", - "resources/js/**" + "packages/extension-table-plus/**" ] }, { diff --git a/components.json b/components.json deleted file mode 100644 index c5aceeb3ce..0000000000 --- a/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "aliases": { - "components": "@renderer/ui/third-party", - "hooks": "@renderer/hooks", - "lib": "@renderer/lib", - "ui": "@renderer/ui", - "utils": "@renderer/utils" - }, - "iconLibrary": "lucide", - "rsc": false, - "style": "new-york", - "tailwind": { - "baseColor": "zinc", - "config": "", - "css": "src/renderer/src/assets/styles/tailwind.css", - "cssVariables": true, - "prefix": "" - }, - "tsx": true -} diff --git a/resources/js/bridge.js b/resources/js/bridge.js deleted file mode 100644 index f6c0021a63..0000000000 --- a/resources/js/bridge.js +++ /dev/null @@ -1,36 +0,0 @@ -;(() => { - let messageId = 0 - const pendingCalls = new Map() - - function api(method, ...args) { - const id = messageId++ - return new Promise((resolve, reject) => { - pendingCalls.set(id, { resolve, reject }) - window.parent.postMessage({ id, type: 'api-call', method, args }, '*') - }) - } - - window.addEventListener('message', (event) => { - if (event.data.type === 'api-response') { - const { id, result, error } = event.data - const pendingCall = pendingCalls.get(id) - if (pendingCall) { - if (error) { - pendingCall.reject(new Error(error)) - } else { - pendingCall.resolve(result) - } - pendingCalls.delete(id) - } - } - }) - - window.api = new Proxy( - {}, - { - get: (target, prop) => { - return (...args) => api(prop, ...args) - } - } - ) -})() diff --git a/resources/js/utils.js b/resources/js/utils.js deleted file mode 100644 index 36981ac44f..0000000000 --- a/resources/js/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -export function getQueryParam(paramName) { - const url = new URL(window.location.href) - const params = new URLSearchParams(url.search) - return params.get(paramName) -} diff --git a/resources/scripts/ipService.js b/resources/scripts/ipService.js deleted file mode 100644 index 8e997659a7..0000000000 --- a/resources/scripts/ipService.js +++ /dev/null @@ -1,88 +0,0 @@ -const https = require('https') -const { loggerService } = require('@logger') - -const logger = loggerService.withContext('IpService') - -/** - * 获取用户的IP地址所在国家 - * @returns {Promise} 返回国家代码,默认为'CN' - */ -async function getIpCountry() { - return new Promise((resolve) => { - // 添加超时控制 - const timeout = setTimeout(() => { - logger.info('IP Address Check Timeout, default to China Mirror') - resolve('CN') - }, 5000) - - const options = { - hostname: 'ipinfo.io', - path: '/json', - method: 'GET', - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', - 'Accept-Language': 'en-US,en;q=0.9' - } - } - - const req = https.request(options, (res) => { - clearTimeout(timeout) - let data = '' - - res.on('data', (chunk) => { - data += chunk - }) - - res.on('end', () => { - try { - const parsed = JSON.parse(data) - const country = parsed.country || 'CN' - logger.info(`Detected user IP address country: ${country}`) - resolve(country) - } catch (error) { - logger.error('Failed to parse IP address information:', error.message) - resolve('CN') - } - }) - }) - - req.on('error', (error) => { - clearTimeout(timeout) - logger.error('Failed to get IP address information:', error.message) - resolve('CN') - }) - - req.end() - }) -} - -/** - * 检查用户是否在中国 - * @returns {Promise} 如果用户在中国返回true,否则返回false - */ -async function isUserInChina() { - const country = await getIpCountry() - return country.toLowerCase() === 'cn' -} - -/** - * 根据用户位置获取适合的npm镜像URL - * @returns {Promise} 返回npm镜像URL - */ -async function getNpmRegistryUrl() { - const inChina = await isUserInChina() - if (inChina) { - logger.info('User in China, using Taobao npm mirror') - return 'https://registry.npmmirror.com' - } else { - logger.info('User not in China, using default npm mirror') - return 'https://registry.npmjs.org' - } -} - -module.exports = { - getIpCountry, - isUserInChina, - getNpmRegistryUrl -} diff --git a/src/renderer/src/ui/context-menu.tsx b/src/renderer/src/ui/context-menu.tsx deleted file mode 100644 index 7fdd27c38f..0000000000 --- a/src/renderer/src/ui/context-menu.tsx +++ /dev/null @@ -1,207 +0,0 @@ -'use client' - -import * as React from 'react' -import * as ContextMenuPrimitive from '@radix-ui/react-context-menu' -import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react' -import { cn } from '@renderer/utils' - -function ContextMenu({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuTrigger({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuGroup({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuPortal({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuSub({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuRadioGroup({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuSubTrigger({ - className, - inset, - children, - ...props -}: React.ComponentProps & { - inset?: boolean -}) { - return ( - - {children} - - - ) -} - -function ContextMenuSubContent({ className, ...props }: React.ComponentProps) { - return ( - - ) -} - -function ContextMenuContent({ className, ...props }: React.ComponentProps) { - return ( - - - - ) -} - -function ContextMenuItem({ - className, - inset, - variant = 'default', - ...props -}: React.ComponentProps & { - inset?: boolean - variant?: 'default' | 'destructive' -}) { - return ( - - ) -} - -function ContextMenuCheckboxItem({ - className, - children, - checked, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ) -} - -function ContextMenuRadioItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ) -} - -function ContextMenuLabel({ - className, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean -}) { - return ( - - ) -} - -function ContextMenuSeparator({ className, ...props }: React.ComponentProps) { - return ( - - ) -} - -function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { - return ( - - ) -} - -export { - ContextMenu, - ContextMenuTrigger, - ContextMenuContent, - ContextMenuItem, - ContextMenuCheckboxItem, - ContextMenuRadioItem, - ContextMenuLabel, - ContextMenuSeparator, - ContextMenuShortcut, - ContextMenuGroup, - ContextMenuPortal, - ContextMenuSub, - ContextMenuSubContent, - ContextMenuSubTrigger, - ContextMenuRadioGroup -} From bc8b0a8d5322e94aad50d23e12fe59253f14c117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Mon, 10 Nov 2025 11:26:36 +0800 Subject: [PATCH 13/15] feat(agent): add permission mode display component for empty session state (#11204) Replace empty state text with a visual permission mode display card that shows: - Permission mode icon with unique colors for each mode (default, plan, acceptEdits, bypassPermissions) - Permission mode title and description - Clickable to navigate directly to tooling settings tab Replace loading text with Ant Design Spin component for better UX. --- .../home/Messages/AgentSessionMessages.tsx | 16 ++-- .../home/Messages/PermissionModeDisplay.tsx | 82 +++++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 src/renderer/src/pages/home/Messages/PermissionModeDisplay.tsx diff --git a/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx b/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx index 90b284d6c4..611216919a 100644 --- a/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx +++ b/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx @@ -5,11 +5,13 @@ import { useTopicMessages } from '@renderer/hooks/useMessageOperations' import { getGroupedMessages } from '@renderer/services/MessagesService' import { type Topic, TopicType } from '@renderer/types' import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' +import { Spin } from 'antd' import { memo, useMemo } from 'react' import styled from 'styled-components' import MessageGroup from './MessageGroup' import NarrowLayout from './NarrowLayout' +import PermissionModeDisplay from './PermissionModeDisplay' import { MessagesContainer, ScrollContainer } from './shared' const logger = loggerService.withContext('AgentSessionMessages') @@ -67,8 +69,12 @@ const AgentSessionMessages: React.FC = ({ agentId, sessionId }) => { groupedMessages.map(([key, groupMessages]) => ( )) + ) : session ? ( + ) : ( - {session ? 'No messages yet.' : 'Loading session...'} + + + )} @@ -77,10 +83,10 @@ const AgentSessionMessages: React.FC = ({ agentId, sessionId }) => { ) } -const EmptyState = styled.div` - color: var(--color-text-3); - font-size: 12px; - text-align: center; +const LoadingState = styled.div` + display: flex; + justify-content: center; + align-items: center; padding: 20px 0; ` diff --git a/src/renderer/src/pages/home/Messages/PermissionModeDisplay.tsx b/src/renderer/src/pages/home/Messages/PermissionModeDisplay.tsx new file mode 100644 index 0000000000..c8ad773484 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/PermissionModeDisplay.tsx @@ -0,0 +1,82 @@ +import { permissionModeCards } from '@renderer/config/agent' +import SessionSettingsPopup from '@renderer/pages/settings/AgentSettings/SessionSettingsPopup' +import type { GetAgentSessionResponse, PermissionMode } from '@renderer/types' +import { FileEdit, Lightbulb, Shield, ShieldOff } from 'lucide-react' +import type { FC } from 'react' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +interface Props { + session: GetAgentSessionResponse + agentId: string +} + +const getPermissionModeConfig = (mode: PermissionMode) => { + switch (mode) { + case 'default': + return { + icon: + } + case 'plan': + return { + icon: + } + case 'acceptEdits': + return { + icon: + } + case 'bypassPermissions': + return { + icon: + } + default: + return { + icon: + } + } +} + +const PermissionModeDisplay: FC = ({ session, agentId }) => { + const { t } = useTranslation() + + const permissionMode = session?.configuration?.permission_mode ?? 'default' + + const modeCard = useMemo(() => { + return permissionModeCards.find((card) => card.mode === permissionMode) + }, [permissionMode]) + + const modeConfig = useMemo(() => getPermissionModeConfig(permissionMode), [permissionMode]) + + const handleClick = () => { + SessionSettingsPopup.show({ + agentId, + sessionId: session.id, + tab: 'tooling' + }) + } + + if (!modeCard) { + return null + } + + return ( +
+
+
{modeConfig.icon}
+
+
+ {t(modeCard.titleKey, modeCard.titleFallback)} +
+
+ {t(modeCard.descriptionKey, modeCard.descriptionFallback)}{' '} + {t(modeCard.behaviorKey, modeCard.behaviorFallback)} +
+
+
+
+ ) +} + +export default PermissionModeDisplay From 5e0a66fa1f9ef768ba55345c086646a3fb39f091 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Mon, 10 Nov 2025 15:41:17 +0800 Subject: [PATCH 14/15] docs(README): update AI Web Service Integration section and remove public beta notice - Added a hyperlink to Poe in the AI Web Service Integration list for better accessibility. - Removed the public beta notice for the Enterprise Edition to streamline the documentation. - Updated the cost section to include a link to the AGPL-3.0 License for clarity. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c3d3f915a1..1223f73ed0 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Cherry Studio is a desktop client that supports multiple LLM providers, availabl 1. **Diverse LLM Provider Support**: - ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more -- 🔗 AI Web Service Integration: Claude, Perplexity, Poe, and others +- 🔗 AI Web Service Integration: Claude, Perplexity, [Poe](https://poe.com/), and others - 💻 Local Model Support with Ollama, LM Studio 2. **AI Assistants & Conversations**: @@ -238,10 +238,6 @@ The Enterprise Edition addresses core challenges in team collaboration by centra ## ✨ Online Demo -> 🚧 **Public Beta Notice** -> -> The Enterprise Edition is currently in its early public beta stage, and we are actively iterating and optimizing its features. We are aware that it may not be perfectly stable yet. If you encounter any issues or have valuable suggestions during your trial, we would be very grateful if you could contact us via email to provide feedback. - **🔗 [Cherry Studio Enterprise](https://www.cherry-ai.com/enterprise)** ## Version Comparison @@ -249,7 +245,7 @@ The Enterprise Edition addresses core challenges in team collaboration by centra | Feature | Community Edition | Enterprise Edition | | :---------------- | :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | | **Open Source** | ✅ Yes | ⭕️ Partially released to customers | -| **Cost** | Free for Personal Use / Commercial License | Buyout / Subscription Fee | +| **Cost** | [AGPL-3.0 License](https://github.com/CherryHQ/cherry-studio?tab=AGPL-3.0-1-ov-file) | Buyout / Subscription Fee | | **Admin Backend** | — | ● Centralized **Model** Access
● **Employee** Management
● Shared **Knowledge Base**
● **Access** Control
● **Data** Backup | | **Server** | — | ✅ Dedicated Private Deployment | @@ -262,8 +258,12 @@ We believe the Enterprise Edition will become your team's AI productivity engine # 🔗 Related Projects +- [new-api](https://github.com/QuantumNous/new-api): The next-generation LLM gateway and AI asset management system supports multiple languages. + - [one-api](https://github.com/songquanpeng/one-api): LLM API management and distribution system supporting mainstream models like OpenAI, Azure, and Anthropic. Features a unified API interface, suitable for key management and secondary distribution. +- [Poe](https://poe.com/): Poe gives you access to the best AI, all in one place. Explore GPT-5, Claude Opus 4.1, DeepSeek-R1, Veo 3, ElevenLabs, and millions of others. + - [ublacklist](https://github.com/iorate/ublacklist): Blocks specific sites from appearing in Google search results # 🚀 Contributors From e2c8edab61239365530d6d415e517f2ed0831c9f Mon Sep 17 00:00:00 2001 From: Konjac-XZ <71483384+Konjac-XZ@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:42:34 +0800 Subject: [PATCH 15/15] =?UTF-8?q?fix:=20incorrect=20spelling=20caused=20Ge?= =?UTF-8?q?mini=20endpoint=E2=80=99s=20thinking=20budget=20to=20fail=20(#1?= =?UTF-8?q?1217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/aiCore/utils/reasoning.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/aiCore/utils/reasoning.ts b/src/renderer/src/aiCore/utils/reasoning.ts index 3a36fb658a..1d7123a47b 100644 --- a/src/renderer/src/aiCore/utils/reasoning.ts +++ b/src/renderer/src/aiCore/utils/reasoning.ts @@ -418,6 +418,8 @@ export function getAnthropicReasoningParams(assistant: Assistant, model: Model): /** * 获取 Gemini 推理参数 * 从 GeminiAPIClient 中提取的逻辑 + * 注意:Gemini/GCP 端点所使用的 thinkingBudget 等参数应该按照驼峰命名法传递 + * 而在 Google 官方提供的 OpenAI 兼容端点中则使用蛇形命名法 thinking_budget */ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Record { if (!isReasoningModel(model)) { @@ -431,8 +433,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re if (reasoningEffort === undefined) { return { thinkingConfig: { - include_thoughts: false, - ...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinking_budget: 0 } : {}) + includeThoughts: false, + ...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinkingBudget: 0 } : {}) } } } @@ -442,7 +444,7 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re if (effortRatio > 1) { return { thinkingConfig: { - include_thoughts: true + includeThoughts: true } } } @@ -452,8 +454,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re return { thinkingConfig: { - ...(budget > 0 ? { thinking_budget: budget } : {}), - include_thoughts: true + ...(budget > 0 ? { thinkingBudget: budget } : {}), + includeThoughts: true } } }