mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 19:30:17 +08:00
feat: add GitHub Copilot CLI integration to coding tools (#10403)
* 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 <leaderonepro@outlook.com> * 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 <leaderonepro@outlook.com> * 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 <noreply@anthropic.com> --------- Signed-off-by: LeaderOnePro <leaderonepro@outlook.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d11a2cd95c
commit
38ac42af8c
@ -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 {
|
||||
|
||||
@ -31,7 +31,10 @@ interface VersionInfo {
|
||||
|
||||
class CodeToolsService {
|
||||
private versionCache: Map<string, { version: string; timestamp: number }> = new Map()
|
||||
private terminalsCache: { terminals: TerminalConfig[]; timestamp: number } | null = null
|
||||
private terminalsCache: {
|
||||
terminals: TerminalConfig[]
|
||||
timestamp: number
|
||||
} | null = null
|
||||
private customTerminalPaths: Map<string, string> = 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)
|
||||
|
||||
@ -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 {
|
||||
// 状态
|
||||
|
||||
@ -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<Record<string, string> | 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<string, string>) => {
|
||||
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={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<span>{t('code.bun_required_message')}</span>
|
||||
<Button
|
||||
type="primary"
|
||||
@ -345,46 +361,64 @@ const CodeToolsPage: FC = () => {
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem>
|
||||
<div className="settings-label">
|
||||
{t('code.model')}
|
||||
{selectedCliTool === 'claude-code' && (
|
||||
<Popover
|
||||
content={
|
||||
<div style={{ width: 200 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>{t('code.supported_providers')}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{getClaudeSupportedProviders(allProviders).map((provider) => {
|
||||
return (
|
||||
<Link
|
||||
key={provider.id}
|
||||
style={{ color: 'var(--color-text)', display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
to={`/settings/provider?id=${provider.id}`}>
|
||||
<ProviderLogo shape="square" src={getProviderLogo(provider.id)} size={20} />
|
||||
{getProviderLabel(provider.id)}
|
||||
<ArrowUpRight size={14} />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
{selectedCliTool !== codeTools.githubCopilotCli && (
|
||||
<SettingsItem>
|
||||
<div className="settings-label">
|
||||
{t('code.model')}
|
||||
{selectedCliTool === 'claude-code' && (
|
||||
<Popover
|
||||
content={
|
||||
<div style={{ width: 200 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>{t('code.supported_providers')}</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8
|
||||
}}>
|
||||
{getClaudeSupportedProviders(allProviders).map((provider) => {
|
||||
return (
|
||||
<Link
|
||||
key={provider.id}
|
||||
style={{
|
||||
color: 'var(--color-text)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
}}
|
||||
to={`/settings/provider?id=${provider.id}`}>
|
||||
<ProviderLogo shape="square" src={getProviderLogo(provider.id)} size={20} />
|
||||
{getProviderLabel(provider.id)}
|
||||
<ArrowUpRight size={14} />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
trigger="hover"
|
||||
placement="right">
|
||||
<HelpCircle size={14} style={{ color: 'var(--color-text-3)', cursor: 'pointer' }} />
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
<ModelSelector
|
||||
providers={availableProviders}
|
||||
predicate={modelPredicate}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('code.model_placeholder')}
|
||||
value={selectedModel ? getModelUniqId(selectedModel) : undefined}
|
||||
onChange={handleModelChange}
|
||||
allowClear
|
||||
/>
|
||||
</SettingsItem>
|
||||
}
|
||||
trigger="hover"
|
||||
placement="right">
|
||||
<HelpCircle
|
||||
size={14}
|
||||
style={{
|
||||
color: 'var(--color-text-3)',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
<ModelSelector
|
||||
providers={availableProviders}
|
||||
predicate={modelPredicate}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('code.model_placeholder')}
|
||||
value={selectedModel ? getModelUniqId(selectedModel) : undefined}
|
||||
onChange={handleModelChange}
|
||||
allowClear
|
||||
/>
|
||||
</SettingsItem>
|
||||
)}
|
||||
|
||||
<SettingsItem>
|
||||
<div className="settings-label">{t('code.working_directory')}</div>
|
||||
@ -403,11 +437,27 @@ const CodeToolsPage: FC = () => {
|
||||
options={directories.map((dir) => ({
|
||||
value: dir,
|
||||
label: (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>{dir}</span>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>
|
||||
{dir}
|
||||
</span>
|
||||
<X
|
||||
size={14}
|
||||
style={{ marginLeft: 8, cursor: 'pointer', color: '#999' }}
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
cursor: 'pointer',
|
||||
color: '#999'
|
||||
}}
|
||||
onClick={(e) => handleRemoveDirectory(dir, e)}
|
||||
/>
|
||||
</div>
|
||||
@ -429,7 +479,14 @@ const CodeToolsPage: FC = () => {
|
||||
rows={2}
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
/>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-3)', marginTop: 4 }}>{t('code.env_vars_help')}</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: 'var(--color-text-3)',
|
||||
marginTop: 4
|
||||
}}>
|
||||
{t('code.env_vars_help')}
|
||||
</div>
|
||||
</SettingsItem>
|
||||
|
||||
{/* 终端选择 (macOS 和 Windows) */}
|
||||
@ -464,7 +521,12 @@ const CodeToolsPage: FC = () => {
|
||||
selectedTerminal !== terminalApps.cmd &&
|
||||
selectedTerminal !== terminalApps.powershell &&
|
||||
selectedTerminal !== terminalApps.windowsTerminal && (
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-3)', marginTop: 4 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: 'var(--color-text-3)',
|
||||
marginTop: 4
|
||||
}}>
|
||||
{terminalCustomPaths[selectedTerminal]
|
||||
? `${t('code.custom_path')}: ${terminalCustomPaths[selectedTerminal]}`
|
||||
: t('code.custom_path_required')}
|
||||
|
||||
@ -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<string, (providers: Provider[]) => 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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user