From 38ac42af8c52de5f85809f955835ac8cf1dea82b Mon Sep 17 00:00:00 2001 From: LeaderOnePro Date: Tue, 30 Sep 2025 23:43:19 +0800 Subject: [PATCH] feat: add GitHub Copilot CLI integration to coding tools (#10403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add GitHub Copilot CLI integration to coding tools - Add githubCopilotCli to codeTools enum - Support @github/copilot package installation - Add 'copilot' executable command mapping - Update Redux store to include GitHub Copilot CLI state - Add GitHub Copilot CLI option to UI with proper provider mapping - Implement environment variable handling for GitHub authentication - Fix model selection logic to disable model choice for GitHub Copilot CLI - Update launch validation to not require model selection for GitHub Copilot CLI - Fix prepareLaunchEnvironment and executeLaunch to handle no-model scenario This enables users to launch GitHub Copilot CLI directly from Cherry Studio's code tools interface without needing to select a model, as GitHub Copilot CLI uses GitHub's built-in models and authentication. Signed-off-by: LeaderOnePro * style: apply code formatting for GitHub Copilot CLI integration Auto-fix code style inconsistencies using project's Biome formatter. Resolves semicolon, comma, and quote style issues to match project standards. Signed-off-by: LeaderOnePro * feat: conditionally render model selector for GitHub Copilot CLI - Hide model selector component when GitHub Copilot CLI is selected - Maintain validation logic to allow GitHub Copilot CLI without model selection - Improve UX by removing empty model dropdown for GitHub Copilot CLI 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Signed-off-by: LeaderOnePro Co-authored-by: Claude --- packages/shared/config/constant.ts | 3 +- src/main/services/CodeToolsService.ts | 22 ++- src/renderer/src/hooks/useCodeTools.ts | 6 +- src/renderer/src/pages/code/CodeToolsPage.tsx | 156 ++++++++++++------ src/renderer/src/pages/code/index.ts | 10 +- src/renderer/src/store/codeTools.ts | 14 +- 6 files changed, 153 insertions(+), 58 deletions(-) diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 3ffe88f08a..3b38592005 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -217,7 +217,8 @@ export enum codeTools { claudeCode = 'claude-code', geminiCli = 'gemini-cli', openaiCodex = 'openai-codex', - iFlowCli = 'iflow-cli' + iFlowCli = 'iflow-cli', + githubCopilotCli = 'github-copilot-cli' } export enum terminalApps { diff --git a/src/main/services/CodeToolsService.ts b/src/main/services/CodeToolsService.ts index 486e58c212..d6eea8a9e6 100644 --- a/src/main/services/CodeToolsService.ts +++ b/src/main/services/CodeToolsService.ts @@ -31,7 +31,10 @@ interface VersionInfo { class CodeToolsService { private versionCache: Map = new Map() - private terminalsCache: { terminals: TerminalConfig[]; timestamp: number } | null = null + 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 @@ -82,6 +85,8 @@ class CodeToolsService { return '@qwen-code/qwen-code' case codeTools.iFlowCli: return '@iflow-ai/iflow-cli' + case codeTools.githubCopilotCli: + return '@github/copilot' default: throw new Error(`Unsupported CLI tool: ${cliTool}`) } @@ -99,6 +104,8 @@ class CodeToolsService { return 'qwen' case codeTools.iFlowCli: return 'iflow' + case codeTools.githubCopilotCli: + return 'copilot' default: throw new Error(`Unsupported CLI tool: ${cliTool}`) } @@ -144,7 +151,9 @@ class CodeToolsService { case terminalApps.powershell: // Check for PowerShell in PATH try { - await execAsync('powershell -Command "Get-Host"', { timeout: 3000 }) + await execAsync('powershell -Command "Get-Host"', { + timeout: 3000 + }) return terminal } catch { try { @@ -384,7 +393,9 @@ class CodeToolsService { const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) - const { stdout } = await execAsync(`"${executablePath}" --version`, { timeout: 10000 }) + const { stdout } = await execAsync(`"${executablePath}" --version`, { + timeout: 10000 + }) // Extract version number from output (format may vary by tool) const versionMatch = stdout.trim().match(/\d+\.\d+\.\d+/) installedVersion = versionMatch ? versionMatch[0] : stdout.trim().split(' ')[0] @@ -425,7 +436,10 @@ class CodeToolsService { logger.info(`${packageName} latest version: ${latestVersion}`) // Cache the result - this.versionCache.set(cacheKey, { version: latestVersion!, timestamp: now }) + this.versionCache.set(cacheKey, { + version: latestVersion!, + timestamp: now + }) logger.debug(`Cached latest version for ${packageName}`) } catch (error) { logger.warn(`Failed to get latest version for ${packageName}:`, error as Error) diff --git a/src/renderer/src/hooks/useCodeTools.ts b/src/renderer/src/hooks/useCodeTools.ts index 4d1527ed98..44ffd29d9c 100644 --- a/src/renderer/src/hooks/useCodeTools.ts +++ b/src/renderer/src/hooks/useCodeTools.ts @@ -108,7 +108,11 @@ export const useCodeTools = () => { const environmentVariables = codeToolsState?.environmentVariables?.[codeToolsState.selectedCliTool] || '' // 检查是否可以启动(所有必需字段都已填写) - const canLaunch = Boolean(codeToolsState.selectedCliTool && selectedModel && codeToolsState.currentDirectory) + const canLaunch = Boolean( + codeToolsState.selectedCliTool && + codeToolsState.currentDirectory && + (codeToolsState.selectedCliTool === codeTools.githubCopilotCli || selectedModel) + ) return { // 状态 diff --git a/src/renderer/src/pages/code/CodeToolsPage.tsx b/src/renderer/src/pages/code/CodeToolsPage.tsx index b64833f6d6..06f2ef064b 100644 --- a/src/renderer/src/pages/code/CodeToolsPage.tsx +++ b/src/renderer/src/pages/code/CodeToolsPage.tsx @@ -98,6 +98,10 @@ const CodeToolsPage: FC = () => { return m.id.includes('openai') || OPENAI_CODEX_SUPPORTED_PROVIDERS.includes(m.provider) } + if (selectedCliTool === codeTools.githubCopilotCli) { + return false + } + if (selectedCliTool === codeTools.qwenCode || selectedCliTool === codeTools.iFlowCli) { if (m.supported_endpoint_types) { return ['openai', 'openai-response'].some((type) => @@ -196,7 +200,7 @@ const CodeToolsPage: FC = () => { } } - if (!selectedModel) { + if (!selectedModel && selectedCliTool !== codeTools.githubCopilotCli) { return { isValid: false, message: t('code.model_required') } } @@ -205,6 +209,11 @@ const CodeToolsPage: FC = () => { // 准备启动环境 const prepareLaunchEnvironment = async (): Promise | null> => { + if (selectedCliTool === codeTools.githubCopilotCli) { + const userEnv = parseEnvironmentVariables(environmentVariables) + return userEnv + } + if (!selectedModel) return null const modelProvider = getProviderByModel(selectedModel) @@ -229,7 +238,9 @@ const CodeToolsPage: FC = () => { // 执行启动操作 const executeLaunch = async (env: Record) => { - window.api.codeTools.run(selectedCliTool, selectedModel?.id!, currentDirectory, env, { + const modelId = selectedCliTool === codeTools.githubCopilotCli ? '' : selectedModel?.id! + + window.api.codeTools.run(selectedCliTool, modelId, currentDirectory, env, { autoUpdateToLatest, terminal: selectedTerminal }) @@ -316,7 +327,12 @@ const CodeToolsPage: FC = () => { banner style={{ borderRadius: 'var(--list-item-border-radius)' }} message={ -
+
{t('code.bun_required_message')}
+ + + )}
{t('code.working_directory')}
@@ -403,11 +437,27 @@ const CodeToolsPage: FC = () => { options={directories.map((dir) => ({ value: dir, label: ( -
- {dir} +
+ + {dir} + handleRemoveDirectory(dir, e)} />
@@ -429,7 +479,14 @@ const CodeToolsPage: FC = () => { rows={2} style={{ fontFamily: 'monospace' }} /> -
{t('code.env_vars_help')}
+
+ {t('code.env_vars_help')} +
{/* 终端选择 (macOS 和 Windows) */} @@ -464,7 +521,12 @@ const CodeToolsPage: FC = () => { selectedTerminal !== terminalApps.cmd && selectedTerminal !== terminalApps.powershell && selectedTerminal !== terminalApps.windowsTerminal && ( -
+
{terminalCustomPaths[selectedTerminal] ? `${t('code.custom_path')}: ${terminalCustomPaths[selectedTerminal]}` : t('code.custom_path_required')} diff --git a/src/renderer/src/pages/code/index.ts b/src/renderer/src/pages/code/index.ts index 531a7f5f01..06c7991039 100644 --- a/src/renderer/src/pages/code/index.ts +++ b/src/renderer/src/pages/code/index.ts @@ -20,7 +20,8 @@ export const CLI_TOOLS = [ { value: codeTools.qwenCode, label: 'Qwen Code' }, { value: codeTools.geminiCli, label: 'Gemini CLI' }, { value: codeTools.openaiCodex, label: 'OpenAI Codex' }, - { value: codeTools.iFlowCli, label: 'iFlow CLI' } + { value: codeTools.iFlowCli, label: 'iFlow CLI' }, + { value: codeTools.githubCopilotCli, label: 'GitHub Copilot CLI' } ] export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api', 'cherryin'] @@ -43,7 +44,8 @@ export const CLI_TOOL_PROVIDER_MAP: Record Pr [codeTools.qwenCode]: (providers) => providers.filter((p) => p.type.includes('openai')), [codeTools.openaiCodex]: (providers) => providers.filter((p) => p.id === 'openai' || OPENAI_CODEX_SUPPORTED_PROVIDERS.includes(p.id)), - [codeTools.iFlowCli]: (providers) => providers.filter((p) => p.type.includes('openai')) + [codeTools.iFlowCli]: (providers) => providers.filter((p) => p.type.includes('openai')), + [codeTools.githubCopilotCli]: () => [] } export const getCodeToolsApiBaseUrl = (model: Model, type: EndpointType) => { @@ -158,6 +160,10 @@ export const generateToolEnvironment = ({ env.IFLOW_BASE_URL = baseUrl env.IFLOW_MODEL_NAME = model.id break + + case codeTools.githubCopilotCli: + env.GITHUB_TOKEN = apiKey || '' + break } return env diff --git a/src/renderer/src/store/codeTools.ts b/src/renderer/src/store/codeTools.ts index 471a31113e..fd23d9fff8 100644 --- a/src/renderer/src/store/codeTools.ts +++ b/src/renderer/src/store/codeTools.ts @@ -26,12 +26,17 @@ export const initialState: CodeToolsState = { [codeTools.qwenCode]: null, [codeTools.claudeCode]: null, [codeTools.geminiCli]: null, - [codeTools.openaiCodex]: null + [codeTools.openaiCodex]: null, + [codeTools.iFlowCli]: null, + [codeTools.githubCopilotCli]: null }, environmentVariables: { 'qwen-code': '', 'claude-code': '', - 'gemini-cli': '' + 'gemini-cli': '', + 'openai-codex': '', + 'iflow-cli': '', + 'github-copilot-cli': '' }, directories: [], currentDirectory: '', @@ -63,7 +68,10 @@ const codeToolsSlice = createSlice({ state.environmentVariables = { 'qwen-code': '', 'claude-code': '', - 'gemini-cli': '' + 'gemini-cli': '', + 'openai-codex': '', + 'iflow-cli': '', + 'github-copilot-cli': '' } } state.environmentVariables[state.selectedCliTool] = action.payload