From 273475881e9a3cf6d5e306fa9baee25192e82e77 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 17 Sep 2025 17:18:01 +0800 Subject: [PATCH 01/10] refactor: update HeroUIProvider import in MiniWindowApp component - Changed the import of HeroUIProvider from '@heroui/react' to '@renderer/context/HeroUIProvider' for better context management. --- src/renderer/src/windows/mini/MiniWindowApp.tsx | 2 +- src/renderer/src/windows/selection/action/entryPoint.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/windows/mini/MiniWindowApp.tsx b/src/renderer/src/windows/mini/MiniWindowApp.tsx index b459a3e027..0f6d8bc60d 100644 --- a/src/renderer/src/windows/mini/MiniWindowApp.tsx +++ b/src/renderer/src/windows/mini/MiniWindowApp.tsx @@ -1,9 +1,9 @@ import '@renderer/databases' -import { HeroUIProvider } from '@heroui/react' import { ErrorBoundary } from '@renderer/components/ErrorBoundary' import { ToastPortal } from '@renderer/components/ToastPortal' import { getToastUtilities } from '@renderer/components/TopView/toast' +import { HeroUIProvider } from '@renderer/context/HeroUIProvider' import { useSettings } from '@renderer/hooks/useSettings' import store, { persistor } from '@renderer/store' import { useEffect } from 'react' diff --git a/src/renderer/src/windows/selection/action/entryPoint.tsx b/src/renderer/src/windows/selection/action/entryPoint.tsx index 44f24ccb97..5aa8d3f745 100644 --- a/src/renderer/src/windows/selection/action/entryPoint.tsx +++ b/src/renderer/src/windows/selection/action/entryPoint.tsx @@ -2,13 +2,13 @@ import '@renderer/assets/styles/index.css' import '@renderer/assets/styles/tailwind.css' import '@ant-design/v5-patch-for-react-19' -import { HeroUIProvider } from '@heroui/react' import KeyvStorage from '@kangfenmao/keyv-storage' import { loggerService } from '@logger' import { ToastPortal } from '@renderer/components/ToastPortal' import { getToastUtilities } from '@renderer/components/TopView/toast' import AntdProvider from '@renderer/context/AntdProvider' import { CodeStyleProvider } from '@renderer/context/CodeStyleProvider' +import { HeroUIProvider } from '@renderer/context/HeroUIProvider' import { ThemeProvider } from '@renderer/context/ThemeProvider' import storeSyncService from '@renderer/services/StoreSyncService' import store, { persistor } from '@renderer/store' From 6c9fc598d4a35f99c2c343d2e065118ae5ea53bb Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 17 Sep 2025 18:57:08 +0800 Subject: [PATCH 02/10] bump: version 1.6.0-rc.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- electron-builder.yml | 24 ++++++++++-------------- package.json | 2 +- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/electron-builder.yml b/electron-builder.yml index 0660319150..6b7056ed3a 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -125,20 +125,16 @@ afterSign: scripts/notarize.js artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | - ✨ 新功能: - - 集成 Perplexity SDK 和 Anthropic OAuth - - 支持 API 服务器模式,提供外部调用接口 - - 新增字体自定义设置功能 - - 笔记支持文件夹批量上传 - - 集成 HeroUI 和 Tailwind CSS 提升界面体验 + 🐛 问题修复: + - 修复 Anthropic API URL 处理,移除尾部斜杠并添加端点路径处理 + - 修复 MessageEditor 缺少 QuickPanelProvider 包装的问题 + - 修复 MiniWindow 高度问题 🚀 性能优化: - - 优化大文件上传,支持 OpenAI 标准文件服务 - - 重构 MCP 服务,改进错误处理和状态管理 + - 优化输入栏提及模型状态缓存,在渲染间保持状态 + - 重构网络搜索参数支持模型内置搜索,新增 OpenAI Chat 和 OpenRouter 支持 - 🐛 问题修复: - - 修复 WebSearch RAG 并发问题 - - 修复翻译页面长文本渲染布局问题 - - 修复笔记拖拽排序和无限循环问题 - - 修复 macOS CodeTool 工作目录错误 - - 修复多个 UI 组件的响应式设计问题 + 🔧 重构改进: + - 更新 HeroUIProvider 导入路径,改善上下文管理 + - 更新依赖项和 VSCode 开发环境配置 + - 升级 @cherrystudio/ai-core 到 v1.0.0-alpha.17 diff --git a/package.json b/package.json index 964be0e0fd..8873b7a4a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.6.0-rc.1", + "version": "1.6.0-rc.2", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", From 89d5bd817bf4a633fbab89f38a437d5dcee36656 Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 17 Sep 2025 20:01:47 +0800 Subject: [PATCH 03/10] fix: Add AWS Bedrock reasoning extraction middleware (#10231) * Add AWS Bedrock reasoning extraction middleware - Add 'reasoning' tag to tagNameArray for broader reasoning support - Add AWS Bedrock case with gpt-oss model-specific reasoning extraction - Add openai-chat and openrouter cases to provider options switch - Remove unused zod import * Add OpenRouter provider support Updates ai-core to version alpha.18 with OpenRouter integration and improves provider ID resolution for OpenAI API hosts. --- package.json | 2 +- packages/aiCore/package.json | 2 +- packages/aiCore/src/core/providers/schemas.ts | 10 +++++++++- .../src/aiCore/middleware/AiSdkMiddlewareBuilder.ts | 12 +++++++++++- src/renderer/src/aiCore/provider/factory.ts | 3 +++ src/renderer/src/aiCore/utils/options.ts | 5 ++++- yarn.lock | 4 ++-- 7 files changed, 31 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 8873b7a4a4..b76e5f6f21 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "@aws-sdk/client-bedrock-runtime": "^3.840.0", "@aws-sdk/client-s3": "^3.840.0", "@biomejs/biome": "2.2.4", - "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.17", + "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18", "@cherrystudio/embedjs": "^0.1.31", "@cherrystudio/embedjs-libsql": "^0.1.31", "@cherrystudio/embedjs-loader-csv": "^0.1.31", diff --git a/packages/aiCore/package.json b/packages/aiCore/package.json index 642feff7c1..75ed6ea34e 100644 --- a/packages/aiCore/package.json +++ b/packages/aiCore/package.json @@ -1,6 +1,6 @@ { "name": "@cherrystudio/ai-core", - "version": "1.0.0-alpha.17", + "version": "1.0.0-alpha.18", "description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/packages/aiCore/src/core/providers/schemas.ts b/packages/aiCore/src/core/providers/schemas.ts index 73ea4b8c14..83338cf057 100644 --- a/packages/aiCore/src/core/providers/schemas.ts +++ b/packages/aiCore/src/core/providers/schemas.ts @@ -9,7 +9,9 @@ import { createDeepSeek } from '@ai-sdk/deepseek' import { createGoogleGenerativeAI } from '@ai-sdk/google' import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai' import { createOpenAICompatible } from '@ai-sdk/openai-compatible' +import { LanguageModelV2 } from '@ai-sdk/provider' import { createXai } from '@ai-sdk/xai' +import { createOpenRouter } from '@openrouter/ai-sdk-provider' import { customProvider, Provider } from 'ai' import { z } from 'zod' @@ -46,7 +48,7 @@ export const isBaseProvider = (id: ProviderId): id is BaseProviderId => { type BaseProvider = { id: BaseProviderId name: string - creator: (options: any) => Provider + creator: (options: any) => Provider | LanguageModelV2 supportsImageGeneration: boolean } @@ -124,6 +126,12 @@ export const baseProviders = [ name: 'DeepSeek', creator: createDeepSeek, supportsImageGeneration: false + }, + { + id: 'openrouter', + name: 'OpenRouter', + creator: createOpenRouter, + supportsImageGeneration: true } ] as const satisfies BaseProvider[] diff --git a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts index ffbe66da22..eabdf1815f 100644 --- a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts +++ b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts @@ -140,7 +140,7 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo return builder.build() } -const tagNameArray = ['think', 'thought'] +const tagNameArray = ['think', 'thought', 'reasoning'] /** * 添加provider特定的中间件 @@ -167,6 +167,16 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config: case 'gemini': // Gemini特定中间件 break + case 'aws-bedrock': { + if (config.model?.id.includes('gpt-oss')) { + const tagName = tagNameArray[2] + builder.add({ + name: 'thinking-tag-extraction', + middleware: extractReasoningMiddleware({ tagName }) + }) + } + break + } default: // 其他provider的通用处理 break diff --git a/src/renderer/src/aiCore/provider/factory.ts b/src/renderer/src/aiCore/provider/factory.ts index 617758753e..bfcd3da383 100644 --- a/src/renderer/src/aiCore/provider/factory.ts +++ b/src/renderer/src/aiCore/provider/factory.ts @@ -69,6 +69,9 @@ export function getAiSdkProviderId(provider: Provider): ProviderId | 'openai-com return resolvedFromType } } + if (provider.apiHost.includes('api.openai.com')) { + return 'openai-chat' + } // 3. 最后的fallback(通常会成为openai-compatible) return provider.id as ProviderId } diff --git a/src/renderer/src/aiCore/utils/options.ts b/src/renderer/src/aiCore/utils/options.ts index f85c8c7879..dec475fc8b 100644 --- a/src/renderer/src/aiCore/utils/options.ts +++ b/src/renderer/src/aiCore/utils/options.ts @@ -82,6 +82,7 @@ export function buildProviderOptions( // 应该覆盖所有类型 switch (baseProviderId) { case 'openai': + case 'openai-chat': case 'azure': providerSpecificOptions = { ...buildOpenAIProviderOptions(assistant, model, capabilities), @@ -101,13 +102,15 @@ export function buildProviderOptions( providerSpecificOptions = buildXAIProviderOptions(assistant, model, capabilities) break case 'deepseek': - case 'openai-compatible': + case 'openrouter': + case 'openai-compatible': { // 对于其他 provider,使用通用的构建逻辑 providerSpecificOptions = { ...buildGenericProviderOptions(assistant, model, capabilities), serviceTier: serviceTierSetting } break + } default: throw new Error(`Unsupported base provider ${baseProviderId}`) } diff --git a/yarn.lock b/yarn.lock index 23a168d8f9..2393eaec5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2309,7 +2309,7 @@ __metadata: languageName: node linkType: hard -"@cherrystudio/ai-core@workspace:^1.0.0-alpha.17, @cherrystudio/ai-core@workspace:packages/aiCore": +"@cherrystudio/ai-core@workspace:^1.0.0-alpha.18, @cherrystudio/ai-core@workspace:packages/aiCore": version: 0.0.0-use.local resolution: "@cherrystudio/ai-core@workspace:packages/aiCore" dependencies: @@ -13195,7 +13195,7 @@ __metadata: "@aws-sdk/client-bedrock-runtime": "npm:^3.840.0" "@aws-sdk/client-s3": "npm:^3.840.0" "@biomejs/biome": "npm:2.2.4" - "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.17" + "@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18" "@cherrystudio/embedjs": "npm:^0.1.31" "@cherrystudio/embedjs-libsql": "npm:^0.1.31" "@cherrystudio/embedjs-loader-csv": "npm:^0.1.31" From c76df7fb16c8bc726dad39e68e2174615ec4bf2b Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 17 Sep 2025 23:10:58 +0800 Subject: [PATCH 04/10] fix: Remove maxTokens check from Anthropic thinking budget (#10240) Remove maxTokens check from Anthropic thinking budget --- src/renderer/src/aiCore/utils/reasoning.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/aiCore/utils/reasoning.ts b/src/renderer/src/aiCore/utils/reasoning.ts index 385d8183c5..bee07b1e0d 100644 --- a/src/renderer/src/aiCore/utils/reasoning.ts +++ b/src/renderer/src/aiCore/utils/reasoning.ts @@ -312,7 +312,7 @@ export function getOpenAIReasoningParams(assistant: Assistant, model: Model): Re export function getAnthropicThinkingBudget(assistant: Assistant, model: Model): number { const { maxTokens, reasoning_effort: reasoningEffort } = getAssistantSettings(assistant) - if (maxTokens === undefined || reasoningEffort === undefined) { + if (reasoningEffort === undefined) { return 0 } const effortRatio = EFFORT_RATIO[reasoningEffort] From ca597b9b9bbf77b4feb565c2d6a02a682c6d0fe3 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:24:02 +0800 Subject: [PATCH 05/10] CI: improve claude translator for review, quote and email (#10230) * ci(claude-translator): extend workflow to handle pull request review events - Add support for pull_request_review and pull_request_review_comment events - Update condition logic to include new event types - Expand claude_args to include pull request review related API commands - Enhance prompt to handle new event types and more translation scenarios * ci(workflows): update concurrency group in claude-translator workflow Add github.event.review.id as additional fallback for concurrency group naming * fix(workflow): correct API method for pull_request_review event Use PATCH instead of PUT and update body parameter to match API requirements * ci: clarify comment ID label in workflow output Update the label for comment ID in workflow output to explicitly indicate when it refers to review comments * ci: fix syntax error in GitHub workflow file * fix(workflow): correct HTTP method for pull_request_review event Use PUT instead of PATCH for updating pull request reviews as per GitHub API requirements --- .github/workflows/claude-translator.yml | 61 +++++++++++++++++++------ 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/.github/workflows/claude-translator.yml b/.github/workflows/claude-translator.yml index ab2b6f7e4f..ff317f8532 100644 --- a/.github/workflows/claude-translator.yml +++ b/.github/workflows/claude-translator.yml @@ -1,6 +1,6 @@ name: Claude Translator concurrency: - group: translator-${{ github.event.comment.id || github.event.issue.number }} + group: translator-${{ github.event.comment.id || github.event.issue.number || github.event.review.id }} cancel-in-progress: false on: @@ -8,14 +8,18 @@ on: types: [opened] issue_comment: types: [created, edited] + pull_request_review: + types: [submitted, edited] + pull_request_review_comment: + types: [created, edited] jobs: translate: if: | (github.event_name == 'issues') || - (github.event_name == 'issue_comment' && github.event.sender.type != 'Bot') && - ((github.event_name == 'issue_comment' && github.event.action == 'created' && !contains(github.event.comment.body, 'This issue was translated by Claude')) || - (github.event_name == 'issue_comment' && github.event.action == 'edited')) + (github.event_name == 'issue_comment' && github.event.sender.type != 'Bot') || + (github.event_name == 'pull_request_review' && github.event.sender.type != 'Bot') || + (github.event_name == 'pull_request_review_comment' && github.event.sender.type != 'Bot') runs-on: ubuntu-latest permissions: contents: read @@ -37,23 +41,44 @@ jobs: # Now `contents: read` is safe for files, but we could make a fine-grained token to control it. # See: https://github.com/anthropics/claude-code-action/blob/main/docs/security.md github_token: ${{ secrets.TOKEN_GITHUB_WRITE }} - allowed_non_write_users: '*' + allowed_non_write_users: "*" claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - claude_args: '--allowed-tools Bash(gh issue:*),Bash(gh api:repos/*/issues:*)' + claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:repos/*/issues:*),Bash(gh api:repos/*/pulls/*/reviews/*),Bash(gh api:repos/*/pulls/comments/*)" prompt: | - 你是一个多语言翻译助手。请完成以下任务: + 你是一个多语言翻译助手。你需要响应 GitHub Webhooks 中的以下四种事件: + + - issues + - issue_comment + - pull_request_review + - pull_request_review_comment + + 请完成以下任务: + + 1. 获取当前事件的完整信息。 + + - 如果当前事件是 issues,就获取该 issues 的信息。 + - 如果当前事件是 issue_comment,就获取该 comment 的信息。 + - 如果当前事件是 pull_request_review,就获取该 review 的信息。 + - 如果当前事件是 pull_request_review_comment,就获取该 comment 的信息。 - 1. 获取当前issue/comment的完整信息 2. 智能检测内容。 - 1. 如果是已经遵循格式要求翻译过的issue/comment,检查翻译内容和原始内容是否匹配。若不匹配,则重新翻译一次令其匹配,并遵循格式要求;若匹配,则跳过任务。 - 2. 如果是未翻译过的issue/comment,检查其内容语言。若不是英文,则翻译成英文;若已经是英文,则跳过任务。 + + - 如果获取到的信息是已经遵循格式要求翻译过的内容,则检查翻译内容和原始内容是否匹配。若不匹配,则重新翻译一次令其匹配,并遵循格式要求; + - 如果获取到的信息是未翻译过的内容,检查其内容语言。若不是英文,则翻译成英文; + - 如果获取到的信息是部分翻译为英文的内容,则将其翻译为英文; + - 如果获取到的信息包含了对已翻译内容的引用,则将引用内容清理为仅含英文的内容。引用的内容不能够包含"This xxx was translated by Claude"和"Original Content`等内容。 + - 如果获取到的信息包含了其他类型的引用,即对非 Claude 翻译的内容的引用,则直接照原样引用,不进行翻译。 + - 如果获取到的信息是通过邮件回复的内容,则在翻译时应当将邮件内容的引用放到最后。在原始内容和翻译内容中只需要回复的内容本身,不要包含对邮件内容的引用。 + - 如果获取到的信息本身不需要任何处理,则跳过任务。 + 3. 格式要求: + - 标题:英文翻译(如果非英文) - 内容格式: > [!NOTE] - > This issue/comment was translated by Claude. + > This issue/comment/review was translated by Claude. - [英文翻译内容] + [翻译内容] ---
@@ -62,15 +87,21 @@ jobs:
4. 使用gh工具更新: + - 根据环境信息中的Event类型选择正确的命令: - - 如果Event是'issues':gh issue edit [ISSUE_NUMBER] --title "[英文标题]" --body "[翻译内容 + 原始内容]" - - 如果Event是'issue_comment':gh api -X PATCH /repos/[REPO]/issues/comments/[COMMENT_ID] -f body="[翻译内容 + 原始内容]" + - 如果 Event 是 'issues': gh issue edit [ISSUE_NUMBER] --title "[英文标题]" --body "[翻译内容 + 原始内容]" + - 如果 Event 是 'issue_comment': gh api -X PATCH /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }} -f body="[翻译内容 + 原始内容]" + - 如果 Event 是 'pull_request_review': gh api -X PUT /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews/${{ github.event.review.id }} -f body="[翻译内容]" + - 如果 Event 是 'pull_request_review_comment': gh api -X PATCH /repos/${{ github.repository }}/pulls/comments/${{ github.event.comment.id }} -f body="[翻译内容 + 原始内容]" 环境信息: - Event: ${{ github.event_name }} - Issue Number: ${{ github.event.issue.number }} - Repository: ${{ github.repository }} - - Comment ID: ${{ github.event.comment.id || 'N/A' }} (only available for comment events) + - (Review) Comment ID: ${{ github.event.comment.id || 'N/A' }} + - Pull Request Number: ${{ github.event.pull_request.number || 'N/A' }} + - Review ID: ${{ github.event.review.id || 'N/A' }} + 使用以下命令获取完整信息: gh issue view ${{ github.event.issue.number }} --json title,body,comments From 1d0fc26025f1b72b4800b782c16daf6b3d3022bb Mon Sep 17 00:00:00 2001 From: SuYao Date: Thu, 18 Sep 2025 15:43:07 +0800 Subject: [PATCH 06/10] fix formatApiHost (#10236) * Add .codebuddy and .zed to .gitignore and fix formatApiHost Prevent formatApiHost from processing undefined/empty host values and ignore editor-specific directories * Refactor reasoning tag selection logic for providers Move gpt-oss model handling from aws-bedrock case to openai case and consolidate tag selection logic into a single if-else chain. * Extract reasoning tag name into helper function * fix test * Replace array indexing with named object properties for reasoning tags Improves code readability by using descriptive property names instead of magic array indices when selecting reasoning tag names by model type. * Move host validation to start of formatApiHost --- .gitignore | 2 ++ .../middleware/AiSdkMiddlewareBuilder.ts | 21 +++++++++++-------- src/renderer/src/utils/__tests__/api.test.ts | 2 +- src/renderer/src/utils/api.ts | 4 ++++ 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index fcaa2be164..f4cc92ce30 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,8 @@ local .qwen/* .trae/* .claude-code-router/* +.codebuddy/* +.zed/* CLAUDE.local.md # vitest diff --git a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts index eabdf1815f..1f18e49bad 100644 --- a/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts +++ b/src/renderer/src/aiCore/middleware/AiSdkMiddlewareBuilder.ts @@ -140,7 +140,17 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo return builder.build() } -const tagNameArray = ['think', 'thought', 'reasoning'] +const tagName = { + reasoning: 'reasoning', + think: 'think', + thought: 'thought' +} + +function getReasoningTagName(modelId: string | undefined): string { + if (modelId?.includes('gpt-oss')) return tagName.reasoning + if (modelId?.includes('gemini')) return tagName.thought + return tagName.think +} /** * 添加provider特定的中间件 @@ -156,7 +166,7 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config: case 'openai': case 'azure-openai': { if (config.enableReasoning) { - const tagName = config.model?.id.includes('gemini') ? tagNameArray[1] : tagNameArray[0] + const tagName = getReasoningTagName(config.model?.id.toLowerCase()) builder.add({ name: 'thinking-tag-extraction', middleware: extractReasoningMiddleware({ tagName }) @@ -168,13 +178,6 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config: // Gemini特定中间件 break case 'aws-bedrock': { - if (config.model?.id.includes('gpt-oss')) { - const tagName = tagNameArray[2] - builder.add({ - name: 'thinking-tag-extraction', - middleware: extractReasoningMiddleware({ tagName }) - }) - } break } default: diff --git a/src/renderer/src/utils/__tests__/api.test.ts b/src/renderer/src/utils/__tests__/api.test.ts index ee91e2ad2b..f25ac2e68f 100644 --- a/src/renderer/src/utils/__tests__/api.test.ts +++ b/src/renderer/src/utils/__tests__/api.test.ts @@ -25,7 +25,7 @@ describe('api', () => { }) it('should handle empty string gracefully', () => { - expect(formatApiHost('')).toBe('/v1/') + expect(formatApiHost('')).toBe('') }) }) diff --git a/src/renderer/src/utils/api.ts b/src/renderer/src/utils/api.ts index 5e9b8f91a6..62d0db5623 100644 --- a/src/renderer/src/utils/api.ts +++ b/src/renderer/src/utils/api.ts @@ -20,6 +20,10 @@ export function formatApiKeys(value: string): string { * @returns {string} 格式化后的 API 主机地址。 */ export function formatApiHost(host: string, apiVersion: string = 'v1'): string { + if (!host) { + return '' + } + const forceUseOriginalHost = () => { if (host.endsWith('/')) { return true From 5dac1f5867d8a62984ae41bb0b62085c3e9e658c Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Thu, 18 Sep 2025 17:33:06 +0800 Subject: [PATCH 07/10] feat: support more terminal in code tools (#10192) * feat(CodeTools): add support for terminal selection on macOS - Introduced terminal selection functionality in CodeTools, allowing users to choose from available terminal applications. - Implemented caching for terminal availability checks to enhance performance. - Updated CodeToolsService to preload available terminals and check their availability. - Enhanced UI in CodeToolsPage to display terminal options and handle user selection. - Added new IPC channel for retrieving available terminals from the main process. * lint errs * format * support wezterm * support terminal * support ghostty * support warp kitty * fix github scanner issues * fix all github issues * support windows * support windows * suppport hyper * Refactor terminal command execution for macOS applications to use shell scripts instead of AppleScript, improving compatibility and performance. * Remove Hyper terminal configuration from shared constants * update lint * fix(i18n): Auto update translations for PR #10192 * fix platform checking * format * feat: add Tabby terminal configuration for macOS * fix wrap terminal * delete warp --------- Co-authored-by: GitHub Action --- packages/shared/IpcChannel.ts | 4 + packages/shared/config/constant.ts | 239 +++++++++++ src/main/ipc.ts | 10 + src/main/services/CodeToolsService.ts | 375 ++++++++++++++++-- src/preload/index.ts | 14 +- src/renderer/src/hooks/useCodeTools.ts | 13 +- src/renderer/src/i18n/locales/en-us.json | 7 + src/renderer/src/i18n/locales/zh-cn.json | 7 + src/renderer/src/i18n/locales/zh-tw.json | 7 + src/renderer/src/i18n/translate/el-gr.json | 7 + src/renderer/src/i18n/translate/es-es.json | 7 + src/renderer/src/i18n/translate/fr-fr.json | 7 + src/renderer/src/i18n/translate/ja-jp.json | 7 + src/renderer/src/i18n/translate/pt-pt.json | 7 + src/renderer/src/i18n/translate/ru-ru.json | 7 + src/renderer/src/pages/code/CodeToolsPage.tsx | 106 ++++- src/renderer/src/store/codeTools.ts | 14 +- 17 files changed, 802 insertions(+), 36 deletions(-) diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index a2ef66284c..1e925984a8 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -321,6 +321,10 @@ export enum IpcChannel { // CodeTools CodeTools_Run = 'code-tools:run', + CodeTools_GetAvailableTerminals = 'code-tools:get-available-terminals', + CodeTools_SetCustomTerminalPath = 'code-tools:set-custom-terminal-path', + CodeTools_GetCustomTerminalPath = 'code-tools:get-custom-terminal-path', + CodeTools_RemoveCustomTerminalPath = 'code-tools:remove-custom-terminal-path', // OCR OCR_ocr = 'ocr:ocr', diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 3dc2a45a6a..9ce35e3a5d 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -219,3 +219,242 @@ export enum codeTools { openaiCodex = 'openai-codex', iFlowCli = 'iflow-cli' } + +export enum terminalApps { + systemDefault = 'Terminal', + iterm2 = 'iTerm2', + kitty = 'kitty', + alacritty = 'Alacritty', + wezterm = 'WezTerm', + ghostty = 'Ghostty', + tabby = 'Tabby', + // Windows terminals + windowsTerminal = 'WindowsTerminal', + powershell = 'PowerShell', + cmd = 'CMD', + wsl = 'WSL' +} + +export interface TerminalConfig { + id: string + name: string + bundleId?: string + customPath?: string // For user-configured terminal paths on Windows +} + +export interface TerminalConfigWithCommand extends TerminalConfig { + command: (directory: string, fullCommand: string) => { command: string; args: string[] } +} + +export const MACOS_TERMINALS: TerminalConfig[] = [ + { + id: terminalApps.systemDefault, + name: 'Terminal', + bundleId: 'com.apple.Terminal' + }, + { + id: terminalApps.iterm2, + name: 'iTerm2', + bundleId: 'com.googlecode.iterm2' + }, + { + id: terminalApps.kitty, + name: 'kitty', + bundleId: 'net.kovidgoyal.kitty' + }, + { + id: terminalApps.alacritty, + name: 'Alacritty', + bundleId: 'org.alacritty' + }, + { + id: terminalApps.wezterm, + name: 'WezTerm', + bundleId: 'com.github.wez.wezterm' + }, + { + id: terminalApps.ghostty, + name: 'Ghostty', + bundleId: 'com.mitchellh.ghostty' + }, + { + id: terminalApps.tabby, + name: 'Tabby', + bundleId: 'org.tabby' + } +] + +export const WINDOWS_TERMINALS: TerminalConfig[] = [ + { + id: terminalApps.cmd, + name: 'Command Prompt' + }, + { + id: terminalApps.powershell, + name: 'PowerShell' + }, + { + id: terminalApps.windowsTerminal, + name: 'Windows Terminal' + }, + { + id: terminalApps.wsl, + name: 'WSL (Ubuntu/Debian)' + }, + { + id: terminalApps.alacritty, + name: 'Alacritty' + }, + { + id: terminalApps.wezterm, + name: 'WezTerm' + } +] + +export const WINDOWS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [ + { + id: terminalApps.cmd, + name: 'Command Prompt', + command: (_: string, fullCommand: string) => ({ + command: 'cmd', + args: ['/c', 'start', 'cmd', '/k', fullCommand] + }) + }, + { + id: terminalApps.powershell, + name: 'PowerShell', + command: (_: string, fullCommand: string) => ({ + command: 'cmd', + args: ['/c', 'start', 'powershell', '-NoExit', '-Command', `& '${fullCommand}'`] + }) + }, + { + id: terminalApps.windowsTerminal, + name: 'Windows Terminal', + command: (_: string, fullCommand: string) => ({ + command: 'wt', + args: ['cmd', '/k', fullCommand] + }) + }, + { + id: terminalApps.wsl, + name: 'WSL (Ubuntu/Debian)', + command: (_: string, fullCommand: string) => { + // Start WSL in a new window and execute the batch file from within WSL using cmd.exe + // The batch file will run in Windows context but output will be in WSL terminal + return { + command: 'cmd', + args: ['/c', 'start', 'wsl', '-e', 'bash', '-c', `cmd.exe /c '${fullCommand}' ; exec bash`] + } + } + }, + { + id: terminalApps.alacritty, + name: 'Alacritty', + customPath: '', // Will be set by user in settings + command: (_: string, fullCommand: string) => ({ + command: 'alacritty', // Will be replaced with customPath if set + args: ['-e', 'cmd', '/k', fullCommand] + }) + }, + { + id: terminalApps.wezterm, + name: 'WezTerm', + customPath: '', // Will be set by user in settings + command: (_: string, fullCommand: string) => ({ + command: 'wezterm', // Will be replaced with customPath if set + args: ['start', 'cmd', '/k', fullCommand] + }) + } +] + +export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [ + { + id: terminalApps.systemDefault, + name: 'Terminal', + bundleId: 'com.apple.Terminal', + command: (directory: string, fullCommand: string) => ({ + command: 'sh', + args: [ + '-c', + `open -na Terminal && sleep 0.5 && osascript -e 'tell application "Terminal" to activate' -e 'tell application "Terminal" to do script "cd '${directory.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' && clear && ${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}" in front window'` + ] + }) + }, + { + id: terminalApps.iterm2, + name: 'iTerm2', + bundleId: 'com.googlecode.iterm2', + command: (directory: string, fullCommand: string) => ({ + command: 'sh', + args: [ + '-c', + `open -na iTerm && sleep 0.8 && osascript -e 'on waitUntilRunning()\n repeat 50 times\n tell application "System Events"\n if (exists process "iTerm2") then exit repeat\n end tell\n delay 0.1\n end repeat\nend waitUntilRunning\n\nwaitUntilRunning()\n\ntell application "iTerm2"\n if (count of windows) = 0 then\n create window with default profile\n delay 0.3\n else\n tell current window\n create tab with default profile\n end tell\n delay 0.3\n end if\n tell current session of current window to write text "cd '${directory.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' && clear && ${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"\n activate\nend tell'` + ] + }) + }, + { + id: terminalApps.kitty, + name: 'kitty', + bundleId: 'net.kovidgoyal.kitty', + command: (directory: string, fullCommand: string) => ({ + command: 'sh', + args: [ + '-c', + `cd "${directory}" && open -na kitty --args --directory="${directory}" sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "kitty" to activate'` + ] + }) + }, + { + id: terminalApps.alacritty, + name: 'Alacritty', + bundleId: 'org.alacritty', + command: (directory: string, fullCommand: string) => ({ + command: 'sh', + args: [ + '-c', + `open -na Alacritty --args --working-directory "${directory}" -e sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "Alacritty" to activate'` + ] + }) + }, + { + id: terminalApps.wezterm, + name: 'WezTerm', + bundleId: 'com.github.wez.wezterm', + command: (directory: string, fullCommand: string) => ({ + command: 'sh', + args: [ + '-c', + `open -na WezTerm --args start --new-tab --cwd "${directory}" -- sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "WezTerm" to activate'` + ] + }) + }, + { + id: terminalApps.ghostty, + name: 'Ghostty', + bundleId: 'com.mitchellh.ghostty', + command: (directory: string, fullCommand: string) => ({ + command: 'sh', + args: [ + '-c', + `cd "${directory}" && open -na Ghostty --args --working-directory="${directory}" -e sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "Ghostty" to activate'` + ] + }) + }, + { + id: terminalApps.tabby, + name: 'Tabby', + bundleId: 'org.tabby', + command: (directory: string, fullCommand: string) => ({ + command: 'sh', + args: [ + '-c', + `if pgrep -x "Tabby" > /dev/null; then + open -na Tabby --args open && sleep 0.3 + else + open -na Tabby --args open && sleep 2 + fi && osascript -e 'tell application "Tabby" to activate' -e 'set the clipboard to "cd \\"${directory.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}\\" && clear && ${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"' -e 'tell application "System Events" to tell process "Tabby" to keystroke "v" using {command down}' -e 'tell application "System Events" to key code 36'` + ] + }) + } +] diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 9805b7c6e6..d0ef8ec94a 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -824,6 +824,16 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { // CodeTools ipcMain.handle(IpcChannel.CodeTools_Run, codeToolsService.run) + ipcMain.handle(IpcChannel.CodeTools_GetAvailableTerminals, () => codeToolsService.getAvailableTerminalsForPlatform()) + ipcMain.handle(IpcChannel.CodeTools_SetCustomTerminalPath, (_, terminalId: string, path: string) => + codeToolsService.setCustomTerminalPath(terminalId, path) + ) + ipcMain.handle(IpcChannel.CodeTools_GetCustomTerminalPath, (_, terminalId: string) => + codeToolsService.getCustomTerminalPath(terminalId) + ) + ipcMain.handle(IpcChannel.CodeTools_RemoveCustomTerminalPath, (_, terminalId: string) => + codeToolsService.removeCustomTerminalPath(terminalId) + ) // OCR ipcMain.handle(IpcChannel.OCR_ocr, (_, file: SupportedOcrFile, provider: OcrProvider) => diff --git a/src/main/services/CodeToolsService.ts b/src/main/services/CodeToolsService.ts index 1372bf1f88..74fca367fc 100644 --- a/src/main/services/CodeToolsService.ts +++ b/src/main/services/CodeToolsService.ts @@ -3,11 +3,20 @@ import os from 'node:os' import path from 'node:path' import { loggerService } from '@logger' -import { isWin } from '@main/constant' +import { isMac, isWin } from '@main/constant' import { removeEnvProxy } from '@main/utils' import { isUserInChina } from '@main/utils/ipService' import { getBinaryName } from '@main/utils/process' -import { codeTools } from '@shared/config/constant' +import { + codeTools, + MACOS_TERMINALS, + MACOS_TERMINALS_WITH_COMMANDS, + terminalApps, + TerminalConfig, + TerminalConfigWithCommand, + WINDOWS_TERMINALS, + WINDOWS_TERMINALS_WITH_COMMANDS +} from '@shared/config/constant' import { spawn } from 'child_process' import { promisify } from 'util' @@ -22,7 +31,10 @@ interface VersionInfo { class CodeToolsService { private versionCache: Map = new Map() + private terminalsCache: { terminals: TerminalConfig[]; timestamp: number } | null = null + private customTerminalPaths: Map = new Map() // Store user-configured terminal paths private readonly CACHE_DURATION = 1000 * 60 * 30 // 30 minutes cache + private readonly TERMINALS_CACHE_DURATION = 1000 * 60 * 5 // 5 minutes cache for terminals constructor() { this.getBunPath = this.getBunPath.bind(this) @@ -32,6 +44,23 @@ class CodeToolsService { this.getVersionInfo = this.getVersionInfo.bind(this) this.updatePackage = this.updatePackage.bind(this) this.run = this.run.bind(this) + + if (isMac || isWin) { + this.preloadTerminals() + } + } + + /** + * Preload available terminals in background + */ + private async preloadTerminals(): Promise { + try { + logger.info('Preloading available terminals...') + await this.getAvailableTerminals() + logger.info('Terminal preloading completed') + } catch (error) { + logger.warn('Terminal preloading failed:', error as Error) + } } public async getBunPath() { @@ -75,10 +104,258 @@ class CodeToolsService { } } + /** + * Check if a single terminal is available + */ + private async checkTerminalAvailability(terminal: TerminalConfig): Promise { + try { + if (isMac && terminal.bundleId) { + // macOS: Check if application is installed via bundle ID with timeout + const { stdout } = await execAsync(`mdfind "kMDItemCFBundleIdentifier == '${terminal.bundleId}'"`, { + timeout: 3000 + }) + if (stdout.trim()) { + return terminal + } + } else if (isWin) { + // Windows: Check terminal availability + return await this.checkWindowsTerminalAvailability(terminal) + } else { + // TODO: Check if terminal is available in linux + await execAsync(`which ${terminal.id}`, { timeout: 2000 }) + return terminal + } + } catch (error) { + logger.debug(`Terminal ${terminal.id} not available:`, error as Error) + } + return null + } + + /** + * Check Windows terminal availability (simplified - user configured paths) + */ + private async checkWindowsTerminalAvailability(terminal: TerminalConfig): Promise { + try { + switch (terminal.id) { + case terminalApps.cmd: + // CMD is always available on Windows + return terminal + + case terminalApps.powershell: + // Check for PowerShell in PATH + try { + await execAsync('powershell -Command "Get-Host"', { timeout: 3000 }) + return terminal + } catch { + try { + await execAsync('pwsh -Command "Get-Host"', { timeout: 3000 }) + return terminal + } catch { + return null + } + } + + case terminalApps.windowsTerminal: + // Check for Windows Terminal via where command (doesn't launch the terminal) + try { + await execAsync('where wt', { timeout: 3000 }) + return terminal + } catch { + return null + } + + case terminalApps.wsl: + // Check for WSL + try { + await execAsync('wsl --status', { timeout: 3000 }) + return terminal + } catch { + return null + } + + default: + // For other terminals (Alacritty, WezTerm), check if user has configured custom path + return await this.checkCustomTerminalPath(terminal) + } + } catch (error) { + logger.debug(`Windows terminal ${terminal.id} not available:`, error as Error) + return null + } + } + + /** + * Check if user has configured custom path for terminal + */ + private async checkCustomTerminalPath(terminal: TerminalConfig): Promise { + // Check if user has configured custom path + const customPath = this.customTerminalPaths.get(terminal.id) + if (customPath && fs.existsSync(customPath)) { + try { + await execAsync(`"${customPath}" --version`, { timeout: 3000 }) + return { ...terminal, customPath } + } catch { + return null + } + } + + // Fallback to PATH check + try { + const command = terminal.id === terminalApps.alacritty ? 'alacritty' : 'wezterm' + await execAsync(`${command} --version`, { timeout: 3000 }) + return terminal + } catch { + return null + } + } + + /** + * Set custom path for a terminal (called from settings UI) + */ + public setCustomTerminalPath(terminalId: string, path: string): void { + logger.info(`Setting custom path for terminal ${terminalId}: ${path}`) + this.customTerminalPaths.set(terminalId, path) + // Clear terminals cache to force refresh + this.terminalsCache = null + } + + /** + * Get custom path for a terminal + */ + public getCustomTerminalPath(terminalId: string): string | undefined { + return this.customTerminalPaths.get(terminalId) + } + + /** + * Remove custom path for a terminal + */ + public removeCustomTerminalPath(terminalId: string): void { + logger.info(`Removing custom path for terminal ${terminalId}`) + this.customTerminalPaths.delete(terminalId) + // Clear terminals cache to force refresh + this.terminalsCache = null + } + + /** + * Get available terminals (with caching and parallel checking) + */ + private async getAvailableTerminals(): Promise { + const now = Date.now() + + // Check cache first + if (this.terminalsCache && now - this.terminalsCache.timestamp < this.TERMINALS_CACHE_DURATION) { + logger.info(`Using cached terminals list (${this.terminalsCache.terminals.length} terminals)`) + return this.terminalsCache.terminals + } + + logger.info('Checking available terminals in parallel...') + const startTime = Date.now() + + // Get terminal list based on platform + const terminalList = isWin ? WINDOWS_TERMINALS : MACOS_TERMINALS + + // Check all terminals in parallel + const terminalPromises = terminalList.map((terminal) => this.checkTerminalAvailability(terminal)) + + try { + // Wait for all checks to complete with a global timeout + const results = await Promise.allSettled( + terminalPromises.map((p) => + Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))]) + ) + ) + + const availableTerminals: TerminalConfig[] = [] + results.forEach((result, index) => { + if (result.status === 'fulfilled' && result.value) { + availableTerminals.push(result.value as TerminalConfig) + } else if (result.status === 'rejected') { + logger.debug(`Terminal check failed for ${MACOS_TERMINALS[index].id}:`, result.reason) + } + }) + + const endTime = Date.now() + logger.info( + `Terminal availability check completed in ${endTime - startTime}ms, found ${availableTerminals.length} terminals` + ) + + // Cache the results + this.terminalsCache = { + terminals: availableTerminals, + timestamp: now + } + + return availableTerminals + } catch (error) { + logger.error('Error checking terminal availability:', error as Error) + // Return cached result if available, otherwise empty array + return this.terminalsCache?.terminals || [] + } + } + + /** + * Get terminal config by ID, fallback to system default + */ + private async getTerminalConfig(terminalId?: string): Promise { + const availableTerminals = await this.getAvailableTerminals() + const terminalCommands = isWin ? WINDOWS_TERMINALS_WITH_COMMANDS : MACOS_TERMINALS_WITH_COMMANDS + const defaultTerminal = isWin ? terminalApps.cmd : terminalApps.systemDefault + + if (terminalId) { + let requestedTerminal = terminalCommands.find( + (t) => t.id === terminalId && availableTerminals.some((at) => at.id === t.id) + ) + + if (requestedTerminal) { + // Apply custom path if configured + const customPath = this.customTerminalPaths.get(terminalId) + if (customPath && isWin) { + requestedTerminal = this.applyCustomPath(requestedTerminal, customPath) + } + return requestedTerminal + } else { + logger.warn(`Requested terminal ${terminalId} not available, falling back to system default`) + } + } + + // Fallback to system default Terminal + const systemTerminal = terminalCommands.find( + (t) => t.id === defaultTerminal && availableTerminals.some((at) => at.id === t.id) + ) + if (systemTerminal) { + return systemTerminal + } + + // If even system Terminal is not found, return the first available + const firstAvailable = terminalCommands.find((t) => availableTerminals.some((at) => at.id === t.id)) + if (firstAvailable) { + return firstAvailable + } + + // Last resort fallback + return terminalCommands.find((t) => t.id === defaultTerminal)! + } + + /** + * Apply custom path to terminal configuration + */ + private applyCustomPath(terminal: TerminalConfigWithCommand, customPath: string): TerminalConfigWithCommand { + return { + ...terminal, + customPath, + command: (directory: string, fullCommand: string) => { + const originalCommand = terminal.command(directory, fullCommand) + return { + ...originalCommand, + command: customPath // Replace command with custom path + } + } + } + } + private async isPackageInstalled(cliTool: string): Promise { const executableName = await this.getCliExecutableName(cliTool) const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') - const executablePath = path.join(binDir, executableName + (process.platform === 'win32' ? '.exe' : '')) + const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) // Ensure bin directory exists if (!fs.existsSync(binDir)) { @@ -105,7 +382,7 @@ class CodeToolsService { try { const executableName = await this.getCliExecutableName(cliTool) const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') - const executablePath = path.join(binDir, executableName + (process.platform === 'win32' ? '.exe' : '')) + const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) const { stdout } = await execAsync(`"${executablePath}" --version`, { timeout: 10000 }) // Extract version number from output (format may vary by tool) @@ -191,6 +468,17 @@ class CodeToolsService { } } + /** + * Get available terminals for the current platform + */ + public async getAvailableTerminalsForPlatform(): Promise { + if (isMac || isWin) { + return this.getAvailableTerminals() + } + // For other platforms, return empty array for now + return [] + } + /** * Update a CLI tool to the latest version */ @@ -202,10 +490,9 @@ class CodeToolsService { const bunInstallPath = path.join(os.homedir(), '.cherrystudio') const registryUrl = await this.getNpmRegistryUrl() - const installEnvPrefix = - process.platform === 'win32' - ? `set "BUN_INSTALL=${bunInstallPath}" && set "NPM_CONFIG_REGISTRY=${registryUrl}" &&` - : `export BUN_INSTALL="${bunInstallPath}" && export NPM_CONFIG_REGISTRY="${registryUrl}" &&` + const installEnvPrefix = isWin + ? `set "BUN_INSTALL=${bunInstallPath}" && set "NPM_CONFIG_REGISTRY=${registryUrl}" &&` + : `export BUN_INSTALL="${bunInstallPath}" && export NPM_CONFIG_REGISTRY="${registryUrl}" &&` const updateCommand = `${installEnvPrefix} "${bunPath}" install -g ${packageName}` logger.info(`Executing update command: ${updateCommand}`) @@ -241,7 +528,7 @@ class CodeToolsService { _model: string, directory: string, env: Record, - options: { autoUpdateToLatest?: boolean } = {} + options: { autoUpdateToLatest?: boolean; terminal?: string } = {} ) { logger.info(`Starting CLI tool launch: ${cliTool} in directory: ${directory}`) logger.debug(`Environment variables:`, Object.keys(env)) @@ -251,7 +538,7 @@ class CodeToolsService { const bunPath = await this.getBunPath() const executableName = await this.getCliExecutableName(cliTool) const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') - const executablePath = path.join(binDir, executableName + (process.platform === 'win32' ? '.exe' : '')) + const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) logger.debug(`Package name: ${packageName}`) logger.debug(`Bun path: ${bunPath}`) @@ -295,7 +582,13 @@ class CodeToolsService { // Build environment variable prefix (based on platform) const buildEnvPrefix = (isWindows: boolean) => { - if (Object.keys(env).length === 0) return '' + if (Object.keys(env).length === 0) { + logger.info('No environment variables to set') + return '' + } + + logger.info('Setting environment variables:', Object.keys(env)) + logger.info('Environment variable values:', env) if (isWindows) { // Windows uses set command @@ -304,13 +597,29 @@ class CodeToolsService { .join(' && ') } else { // Unix-like systems use export command - return Object.entries(env) - .map(([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`) + const validEntries = Object.entries(env).filter(([key, value]) => { + if (!key || key.trim() === '') { + return false + } + if (value === undefined || value === null) { + return false + } + return true + }) + + const envCommands = validEntries + .map(([key, value]) => { + const sanitizedValue = String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + const exportCmd = `export ${key}="${sanitizedValue}"` + logger.info(`Setting env var: ${key}="${sanitizedValue}"`) + logger.info(`Export command: ${exportCmd}`) + return exportCmd + }) .join(' && ') + return envCommands } } - // Build command to execute let baseCommand = isWin ? `"${executablePath}"` : `"${bunPath}" "${executablePath}"` // Add configuration parameters for OpenAI Codex @@ -351,20 +660,20 @@ class CodeToolsService { switch (platform) { case 'darwin': { - // macOS - Use osascript to launch terminal and execute command directly, without showing startup command + // macOS - Support multiple terminals const envPrefix = buildEnvPrefix(false) + const command = envPrefix ? `${envPrefix} && ${baseCommand}` : baseCommand + // Combine directory change with the main command to ensure they execute in the same shell session const fullCommand = `cd '${directory.replace(/'/g, "\\'")}' && clear && ${command}` - terminalCommand = 'osascript' - terminalArgs = [ - '-e', - `tell application "Terminal" - do script "${fullCommand.replace(/"/g, '\\"')}" - activate -end tell` - ] + const terminalConfig = await this.getTerminalConfig(options.terminal) + logger.info(`Using terminal: ${terminalConfig.name} (${terminalConfig.id})`) + + const { command: cmd, args } = terminalConfig.command(directory, fullCommand) + terminalCommand = cmd + terminalArgs = args break } case 'win32': { @@ -424,9 +733,23 @@ end tell` throw new Error(`Failed to create launch script: ${error}`) } - // Launch bat file - Use safest start syntax, no title parameter - terminalCommand = 'cmd' - terminalArgs = ['/c', 'start', batFilePath] + // Use selected terminal configuration + const terminalConfig = await this.getTerminalConfig(options.terminal) + logger.info(`Using terminal: ${terminalConfig.name} (${terminalConfig.id})`) + + // Get command and args from terminal configuration + // Pass the bat file path as the command to execute + const fullCommand = batFilePath + const { command: cmd, args } = terminalConfig.command(directory, fullCommand) + + // Override if it's a custom terminal with a custom path + if (terminalConfig.customPath) { + terminalCommand = terminalConfig.customPath + terminalArgs = args + } else { + terminalCommand = cmd + terminalArgs = args + } // Set cleanup task (delete temp file after 5 minutes) setTimeout(() => { diff --git a/src/preload/index.ts b/src/preload/index.ts index cd241237b8..af1cac21a1 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,7 +1,7 @@ import { electronAPI } from '@electron-toolkit/preload' import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core' import { SpanContext } from '@opentelemetry/api' -import { UpgradeChannel } from '@shared/config/constant' +import { TerminalConfig, UpgradeChannel } from '@shared/config/constant' import type { LogLevel, LogSourceWithContext } from '@shared/config/logger' import type { FileChangeEvent } from '@shared/config/types' import { IpcChannel } from '@shared/IpcChannel' @@ -439,8 +439,16 @@ const api = { model: string, directory: string, env: Record, - options?: { autoUpdateToLatest?: boolean } - ) => ipcRenderer.invoke(IpcChannel.CodeTools_Run, cliTool, model, directory, env, options) + options?: { autoUpdateToLatest?: boolean; terminal?: string } + ) => ipcRenderer.invoke(IpcChannel.CodeTools_Run, cliTool, model, directory, env, options), + getAvailableTerminals: (): Promise => + ipcRenderer.invoke(IpcChannel.CodeTools_GetAvailableTerminals), + setCustomTerminalPath: (terminalId: string, path: string): Promise => + ipcRenderer.invoke(IpcChannel.CodeTools_SetCustomTerminalPath, terminalId, path), + getCustomTerminalPath: (terminalId: string): Promise => + ipcRenderer.invoke(IpcChannel.CodeTools_GetCustomTerminalPath, terminalId), + removeCustomTerminalPath: (terminalId: string): Promise => + ipcRenderer.invoke(IpcChannel.CodeTools_RemoveCustomTerminalPath, terminalId) }, ocr: { ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise => diff --git a/src/renderer/src/hooks/useCodeTools.ts b/src/renderer/src/hooks/useCodeTools.ts index db8b21bbeb..4d1527ed98 100644 --- a/src/renderer/src/hooks/useCodeTools.ts +++ b/src/renderer/src/hooks/useCodeTools.ts @@ -8,7 +8,8 @@ import { setCurrentDirectory, setEnvironmentVariables, setSelectedCliTool, - setSelectedModel + setSelectedModel, + setSelectedTerminal } from '@renderer/store/codeTools' import { Model } from '@renderer/types' import { codeTools } from '@shared/config/constant' @@ -35,6 +36,14 @@ export const useCodeTools = () => { [dispatch] ) + // 设置选择的终端 + const setTerminal = useCallback( + (terminal: string) => { + dispatch(setSelectedTerminal(terminal)) + }, + [dispatch] + ) + // 设置环境变量 const setEnvVars = useCallback( (envVars: string) => { @@ -105,6 +114,7 @@ export const useCodeTools = () => { // 状态 selectedCliTool: codeToolsState.selectedCliTool, selectedModel: selectedModel, + selectedTerminal: codeToolsState.selectedTerminal, environmentVariables: environmentVariables, directories: codeToolsState.directories, currentDirectory: codeToolsState.currentDirectory, @@ -113,6 +123,7 @@ export const useCodeTools = () => { // 操作函数 setCliTool, setModel, + setTerminal, setEnvVars, addDir, removeDir, diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 768525d9b2..27a3c655df 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -672,6 +672,10 @@ "bun_required_message": "Bun environment is required to run CLI tools", "cli_tool": "CLI Tool", "cli_tool_placeholder": "Select the CLI tool to use", + "custom_path": "Custom path", + "custom_path_error": "Failed to set custom terminal path", + "custom_path_required": "Custom path required for this terminal", + "custom_path_set": "Custom terminal path set successfully", "description": "Quickly launch multiple code CLI tools to improve development efficiency", "env_vars_help": "Enter custom environment variables (one per line, format: KEY=value)", "environment_variables": "Environment Variables", @@ -690,7 +694,10 @@ "model_placeholder": "Select the model to use", "model_required": "Please select a model", "select_folder": "Select Folder", + "set_custom_path": "Set custom terminal path", "supported_providers": "Supported Providers", + "terminal": "Terminal", + "terminal_placeholder": "Select terminal application", "title": "Code Tools", "update_options": "Update Options", "working_directory": "Working Directory" diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 551338e9d5..f0c674c3bd 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -672,6 +672,10 @@ "bun_required_message": "运行 CLI 工具需要安装 Bun 环境", "cli_tool": "CLI 工具", "cli_tool_placeholder": "选择要使用的 CLI 工具", + "custom_path": "自定义路径", + "custom_path_error": "设置自定义终端路径失败", + "custom_path_required": "此终端需要设置自定义路径", + "custom_path_set": "自定义终端路径设置成功", "description": "快速启动多个代码 CLI 工具,提高开发效率", "env_vars_help": "输入自定义环境变量(每行一个,格式:KEY=value)", "environment_variables": "环境变量", @@ -690,7 +694,10 @@ "model_placeholder": "选择要使用的模型", "model_required": "请选择模型", "select_folder": "选择文件夹", + "set_custom_path": "设置自定义终端路径", "supported_providers": "支持的服务商", + "terminal": "终端", + "terminal_placeholder": "选择终端应用", "title": "代码工具", "update_options": "更新选项", "working_directory": "工作目录" diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index fe7bf4b760..ab2fc860d9 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -672,6 +672,10 @@ "bun_required_message": "運行 CLI 工具需要安裝 Bun 環境", "cli_tool": "CLI 工具", "cli_tool_placeholder": "選擇要使用的 CLI 工具", + "custom_path": "自訂路徑", + "custom_path_error": "設定自訂終端機路徑失敗", + "custom_path_required": "此終端機需要設定自訂路徑", + "custom_path_set": "自訂終端機路徑設定成功", "description": "快速啟動多個程式碼 CLI 工具,提高開發效率", "env_vars_help": "輸入自定義環境變數(每行一個,格式:KEY=value)", "environment_variables": "環境變數", @@ -690,7 +694,10 @@ "model_placeholder": "選擇要使用的模型", "model_required": "請選擇模型", "select_folder": "選擇資料夾", + "set_custom_path": "設定自訂終端機路徑", "supported_providers": "支援的供應商", + "terminal": "終端機", + "terminal_placeholder": "選擇終端機應用程式", "title": "程式碼工具", "update_options": "更新選項", "working_directory": "工作目錄" diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 615eda601e..8741347fcf 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -672,6 +672,10 @@ "bun_required_message": "Για τη λειτουργία του εργαλείου CLI πρέπει να εγκαταστήσετε το περιβάλλον Bun", "cli_tool": "Εργαλείο CLI", "cli_tool_placeholder": "Επιλέξτε το CLI εργαλείο που θέλετε να χρησιμοποιήσετε", + "custom_path": "Προσαρμοσμένη διαδρομή", + "custom_path_error": "Η ρύθμιση της προσαρμοσμένης διαδρομής τερματικού απέτυχε", + "custom_path_required": "Αυτό το τερματικό απαιτεί τη ρύθμιση προσαρμοσμένης διαδρομής", + "custom_path_set": "Η προσαρμοσμένη διαδρομή τερματικού ορίστηκε με επιτυχία", "description": "Εκκίνηση γρήγορα πολλών εργαλείων CLI κώδικα, για αύξηση της αποδοτικότητας ανάπτυξης", "env_vars_help": "Εισαγάγετε προσαρμοσμένες μεταβλητές περιβάλλοντος (μία ανά γραμμή, με τη μορφή: KEY=value)", "environment_variables": "Μεταβλητές περιβάλλοντος", @@ -690,7 +694,10 @@ "model_placeholder": "Επιλέξτε το μοντέλο που θα χρησιμοποιήσετε", "model_required": "Επιλέξτε μοντέλο", "select_folder": "Επιλογή φακέλου", + "set_custom_path": "Ρύθμιση προσαρμοσμένης διαδρομής τερματικού", "supported_providers": "υποστηριζόμενοι πάροχοι", + "terminal": "τερματικό", + "terminal_placeholder": "Επιλέξτε εφαρμογή τερματικού", "title": "Εργαλεία κώδικα", "update_options": "Ενημέρωση επιλογών", "working_directory": "κατάλογος εργασίας" diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index a210344a41..7bbb64a11c 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -672,6 +672,10 @@ "bun_required_message": "Se requiere instalar el entorno Bun para ejecutar la herramienta de línea de comandos", "cli_tool": "Herramienta de línea de comandos", "cli_tool_placeholder": "Seleccione la herramienta de línea de comandos que desea utilizar", + "custom_path": "Ruta personalizada", + "custom_path_error": "Error al establecer la ruta de terminal personalizada", + "custom_path_required": "此终端需要设置自定义路径", + "custom_path_set": "Configuración de ruta de terminal personalizada exitosa", "description": "Inicia rápidamente múltiples herramientas de línea de comandos para código, aumentando la eficiencia del desarrollo", "env_vars_help": "Introduzca variables de entorno personalizadas (una por línea, formato: CLAVE=valor)", "environment_variables": "Variables de entorno", @@ -690,7 +694,10 @@ "model_placeholder": "Seleccionar el modelo que se va a utilizar", "model_required": "Seleccione el modelo", "select_folder": "Seleccionar carpeta", + "set_custom_path": "Establecer ruta de terminal personalizada", "supported_providers": "Proveedores de servicios compatibles", + "terminal": "terminal", + "terminal_placeholder": "Seleccionar aplicación de terminal", "title": "Herramientas de código", "update_options": "Opciones de actualización", "working_directory": "directorio de trabajo" diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 3795f5ab01..ab74489903 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -672,6 +672,10 @@ "bun_required_message": "L'exécution de l'outil en ligne de commande nécessite l'installation de l'environnement Bun", "cli_tool": "Outil CLI", "cli_tool_placeholder": "Sélectionnez l'outil CLI à utiliser", + "custom_path": "Chemin personnalisé", + "custom_path_error": "Échec de la définition du chemin de terminal personnalisé", + "custom_path_required": "Ce terminal nécessite la configuration d’un chemin personnalisé", + "custom_path_set": "Paramétrage personnalisé du chemin du terminal réussi", "description": "Lancer rapidement plusieurs outils CLI de code pour améliorer l'efficacité du développement", "env_vars_help": "Saisissez les variables d'environnement personnalisées (une par ligne, format : KEY=value)", "environment_variables": "variables d'environnement", @@ -690,7 +694,10 @@ "model_placeholder": "Sélectionnez le modèle à utiliser", "model_required": "Veuillez sélectionner le modèle", "select_folder": "Sélectionner le dossier", + "set_custom_path": "Définir un chemin de terminal personnalisé", "supported_providers": "fournisseurs pris en charge", + "terminal": "Terminal", + "terminal_placeholder": "Choisir une application de terminal", "title": "Outils de code", "update_options": "Options de mise à jour", "working_directory": "répertoire de travail" diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index f83153d90b..04bac6fbd0 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -672,6 +672,10 @@ "bun_required_message": "CLI ツールを実行するには Bun 環境が必要です", "cli_tool": "CLI ツール", "cli_tool_placeholder": "使用する CLI ツールを選択してください", + "custom_path": "カスタムパス", + "custom_path_error": "カスタムターミナルパスの設定に失敗しました", + "custom_path_required": "この端末にはカスタムパスを設定する必要があります", + "custom_path_set": "カスタムターミナルパスの設定が成功しました", "description": "開発効率を向上させるために、複数のコード CLI ツールを迅速に起動します", "env_vars_help": "環境変数を設定して、CLI ツールの実行時に使用します。各変数は 1 行ごとに設定してください。", "environment_variables": "環境変数", @@ -690,7 +694,10 @@ "model_placeholder": "使用するモデルを選択してください", "model_required": "モデルを選択してください", "select_folder": "フォルダを選択", + "set_custom_path": "カスタムターミナルパスを設定", "supported_providers": "サポートされているプロバイダー", + "terminal": "端末", + "terminal_placeholder": "ターミナルアプリを選択", "title": "コードツール", "update_options": "更新オプション", "working_directory": "作業ディレクトリ" diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 7759fc2706..4f18f55ef7 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -672,6 +672,10 @@ "bun_required_message": "Executar a ferramenta CLI requer a instalação do ambiente Bun", "cli_tool": "Ferramenta de linha de comando", "cli_tool_placeholder": "Selecione a ferramenta de linha de comando a ser utilizada", + "custom_path": "Caminho personalizado", + "custom_path_error": "Falha ao definir caminho de terminal personalizado", + "custom_path_required": "Este terminal requer a definição de um caminho personalizado", + "custom_path_set": "Configuração personalizada do caminho do terminal bem-sucedida", "description": "Inicie rapidamente várias ferramentas de linha de comando de código, aumentando a eficiência do desenvolvimento", "env_vars_help": "Insira variáveis de ambiente personalizadas (uma por linha, formato: CHAVE=valor)", "environment_variables": "variáveis de ambiente", @@ -690,7 +694,10 @@ "model_placeholder": "Selecione o modelo a ser utilizado", "model_required": "Selecione o modelo", "select_folder": "Selecionar pasta", + "set_custom_path": "Definir caminho personalizado do terminal", "supported_providers": "Provedores de serviço suportados", + "terminal": "terminal", + "terminal_placeholder": "Selecionar aplicativo de terminal", "title": "Ferramenta de código", "update_options": "Opções de atualização", "working_directory": "diretório de trabalho" diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index cec3dd0e74..88ce796529 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -672,6 +672,10 @@ "bun_required_message": "Запуск CLI-инструментов требует установки среды Bun", "cli_tool": "Инструмент", "cli_tool_placeholder": "Выберите CLI-инструмент для использования", + "custom_path": "Пользовательский путь", + "custom_path_error": "Не удалось задать пользовательский путь терминала", + "custom_path_required": "Этот терминал требует установки пользовательского пути", + "custom_path_set": "Пользовательский путь терминала успешно установлен", "description": "Быстро запускает несколько CLI-инструментов для кода, повышая эффективность разработки", "env_vars_help": "Установите переменные окружения для использования при запуске CLI-инструментов. Каждая переменная должна быть на отдельной строке в формате KEY=value", "environment_variables": "Переменные окружения", @@ -690,7 +694,10 @@ "model_placeholder": "Выберите модель для использования", "model_required": "Пожалуйста, выберите модель", "select_folder": "Выберите папку", + "set_custom_path": "Настройка пользовательского пути терминала", "supported_providers": "Поддерживаемые поставщики", + "terminal": "терминал", + "terminal_placeholder": "Выбор приложения терминала", "title": "Инструменты кода", "update_options": "Параметры обновления", "working_directory": "Рабочая директория" diff --git a/src/renderer/src/pages/code/CodeToolsPage.tsx b/src/renderer/src/pages/code/CodeToolsPage.tsx index 4dc0f287cf..14f540f5db 100644 --- a/src/renderer/src/pages/code/CodeToolsPage.tsx +++ b/src/renderer/src/pages/code/CodeToolsPage.tsx @@ -1,6 +1,7 @@ import AiProvider from '@renderer/aiCore' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import ModelSelector from '@renderer/components/ModelSelector' +import { isMac, isWin } from '@renderer/config/constant' import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models' import { getProviderLogo } from '@renderer/config/providers' import { useCodeTools } from '@renderer/hooks/useCodeTools' @@ -13,9 +14,9 @@ import { getModelUniqId } from '@renderer/services/ModelService' import { useAppDispatch, useAppSelector } from '@renderer/store' import { setIsBunInstalled } from '@renderer/store/mcp' import { Model } from '@renderer/types' -import { codeTools } from '@shared/config/constant' -import { Alert, Avatar, Button, Checkbox, Input, Popover, Select, Space } from 'antd' -import { ArrowUpRight, Download, HelpCircle, Terminal, X } from 'lucide-react' +import { codeTools, terminalApps, TerminalConfig } from '@shared/config/constant' +import { Alert, Avatar, Button, Checkbox, Input, Popover, Select, Space, Tooltip } from 'antd' +import { ArrowUpRight, Download, FolderOpen, HelpCircle, Terminal, X } from 'lucide-react' import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' @@ -42,12 +43,14 @@ const CodeToolsPage: FC = () => { const { selectedCliTool, selectedModel, + selectedTerminal, environmentVariables, directories, currentDirectory, canLaunch, setCliTool, setModel, + setTerminal, setEnvVars, setCurrentDir, removeDir, @@ -58,6 +61,9 @@ const CodeToolsPage: FC = () => { const [isLaunching, setIsLaunching] = useState(false) const [isInstallingBun, setIsInstallingBun] = useState(false) const [autoUpdateToLatest, setAutoUpdateToLatest] = useState(false) + const [availableTerminals, setAvailableTerminals] = useState([]) + const [isLoadingTerminals, setIsLoadingTerminals] = useState(false) + const [terminalCustomPaths, setTerminalCustomPaths] = useState>({}) const modelPredicate = useCallback( (m: Model) => { @@ -119,6 +125,26 @@ const CodeToolsPage: FC = () => { } }, [dispatch]) + // 获取可用终端 + const loadAvailableTerminals = useCallback(async () => { + if (!isMac && !isWin) return // 仅 macOS 和 Windows 支持 + + try { + setIsLoadingTerminals(true) + const terminals = await window.api.codeTools.getAvailableTerminals() + setAvailableTerminals(terminals) + logger.info( + `Found ${terminals.length} available terminals:`, + terminals.map((t) => t.name) + ) + } catch (error) { + logger.error('Failed to load available terminals:', error as Error) + setAvailableTerminals([]) + } finally { + setIsLoadingTerminals(false) + } + }, []) + // 安装 bun const handleInstallBun = async () => { try { @@ -179,11 +205,37 @@ const CodeToolsPage: FC = () => { // 执行启动操作 const executeLaunch = async (env: Record) => { window.api.codeTools.run(selectedCliTool, selectedModel?.id!, currentDirectory, env, { - autoUpdateToLatest + autoUpdateToLatest, + terminal: selectedTerminal }) window.toast.success(t('code.launch.success')) } + // 设置终端自定义路径 + const handleSetCustomPath = async (terminalId: string) => { + try { + const result = await window.api.file.select({ + properties: ['openFile'], + filters: [ + { name: 'Executable', extensions: ['exe'] }, + { name: 'All Files', extensions: ['*'] } + ] + }) + + if (result && result.length > 0) { + const path = result[0].path + await window.api.codeTools.setCustomTerminalPath(terminalId, path) + setTerminalCustomPaths((prev) => ({ ...prev, [terminalId]: path })) + window.toast.success(t('code.custom_path_set')) + // Reload terminals to reflect changes + loadAvailableTerminals() + } + } catch (error) { + logger.error('Failed to set custom terminal path:', error as Error) + window.toast.error(t('code.custom_path_error')) + } + } + // 处理启动 const handleLaunch = async () => { const validation = validateLaunch() @@ -216,6 +268,11 @@ const CodeToolsPage: FC = () => { checkBunInstallation() }, [checkBunInstallation]) + // 页面加载时获取可用终端 + useEffect(() => { + loadAvailableTerminals() + }, [loadAvailableTerminals]) + return ( @@ -350,6 +407,47 @@ const CodeToolsPage: FC = () => {
{t('code.env_vars_help')}
+ {/* 终端选择 (macOS 和 Windows) */} + {(isMac || isWin) && availableTerminals.length > 0 && ( + +
{t('code.terminal')}
+ +