feat(Sidebar): add 'code_tools' icon and route; enhance CodeToolsPage layout with Navbar and improved provider filtering

This commit is contained in:
kangfenmao 2025-08-19 15:38:03 +08:00
parent e0bc3bb2c5
commit 29d4e37f6b
6 changed files with 169 additions and 113 deletions

View File

@ -16,6 +16,7 @@ import { isEmoji } from '@renderer/utils'
import { Avatar, Tooltip } from 'antd'
import {
CircleHelp,
Code,
FileSearch,
Folder,
Languages,
@ -153,7 +154,8 @@ const MainMenus: FC = () => {
translate: <Languages size={18} className="icon" />,
minapp: <LayoutGrid size={18} className="icon" />,
knowledge: <FileSearch size={18} className="icon" />,
files: <Folder size={17} className="icon" />
files: <Folder size={17} className="icon" />,
code_tools: <Code size={18} className="icon" />
}
const pathMap = {
@ -163,7 +165,8 @@ const MainMenus: FC = () => {
translate: '/translate',
minapp: '/apps',
knowledge: '/knowledge',
files: '/files'
files: '/files',
code_tools: '/code'
}
return sidebarIcons.visible.map((icon) => {

View File

@ -1,4 +1,5 @@
import AiProvider from '@renderer/aiCore'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import ModelSelector from '@renderer/components/ModelSelector'
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
import { useCodeTools } from '@renderer/hooks/useCodeTools'
@ -22,6 +23,8 @@ const CLI_TOOLS = [
{ value: 'gemini-cli', label: 'Gemini CLI' }
]
const SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
const logger = loggerService.withContext('CodeToolsPage')
const CodeToolsPage: FC = () => {
@ -54,12 +57,23 @@ const CodeToolsPage: FC = () => {
}
const openAiProviders = providers.filter((p) => p.type.includes('openai'))
const geminiProviders = providers.filter((p) => p.type === 'gemini')
const claudeProviders = providers.filter((p) => p.type === 'anthropic')
const geminiProviders = providers.filter((p) => p.type === 'gemini' || SUPPORTED_PROVIDERS.includes(p.id))
const claudeProviders = providers.filter((p) => p.type === 'anthropic' || SUPPORTED_PROVIDERS.includes(p.id))
const modelPredicate = useCallback(
(m: Model) => !isEmbeddingModel(m) && !isRerankModel(m) && !isTextToImageModel(m),
[]
(m: Model) => {
if (isEmbeddingModel(m) || isRerankModel(m) || isTextToImageModel(m)) {
return false
}
if (selectedCliTool === 'claude-code') {
return m.id.includes('claude')
}
if (selectedCliTool === 'gemini-cli') {
return m.id.includes('gemini')
}
return true
},
[selectedCliTool]
)
const availableProviders =
@ -176,13 +190,19 @@ const CodeToolsPage: FC = () => {
if (selectedCliTool === 'claude-code') {
env = {
ANTHROPIC_API_KEY: apiKey,
ANTHROPIC_BASE_URL: modelProvider.apiHost,
ANTHROPIC_MODEL: selectedModel.id
}
}
if (selectedCliTool === 'gemini-cli') {
const apiSuffix = modelProvider.id === 'aihubmix' ? '/gemini' : ''
const apiBaseUrl = modelProvider.apiHost + apiSuffix
env = {
GEMINI_API_KEY: apiKey
GEMINI_API_KEY: apiKey,
GEMINI_BASE_URL: apiBaseUrl,
GOOGLE_GEMINI_BASE_URL: apiBaseUrl,
GEMINI_MODEL: selectedModel.id
}
}
@ -228,117 +248,134 @@ const CodeToolsPage: FC = () => {
return (
<Container>
<Title>{t('code.title')}</Title>
<Description>{t('code.description')}</Description>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('code.title')}</NavbarCenter>
</Navbar>
<ContentContainer id="content-container">
<MainContent>
<Title>{t('code.title')}</Title>
<Description>{t('code.description')}</Description>
{/* Bun 安装状态提示 */}
{!isBunInstalled && (
<BunInstallAlert>
<Alert
type="warning"
banner
style={{ borderRadius: 'var(--list-item-border-radius)' }}
message={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{t('code.bun_required_message')}</span>
<Button
type="primary"
size="small"
icon={<Download size={14} />}
onClick={handleInstallBun}
loading={isInstallingBun}
disabled={isInstallingBun}>
{isInstallingBun ? t('code.installing_bun') : t('code.install_bun')}
</Button>
</div>
}
/>
</BunInstallAlert>
)}
<SettingsPanel>
<SettingsItem>
<div className="settings-label">{t('code.cli_tool')}</div>
<Select
style={{ width: '100%' }}
placeholder={t('code.cli_tool_placeholder')}
value={selectedCliTool}
onChange={handleCliToolChange}
options={CLI_TOOLS}
/>
</SettingsItem>
<SettingsItem>
<div className="settings-label">{t('code.model')}</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>
<Space.Compact style={{ width: '100%', display: 'flex' }}>
<Select
style={{ flex: 1, width: 480 }}
placeholder={t('code.folder_placeholder')}
value={currentDirectory || undefined}
onChange={handleDirectoryChange}
allowClear
showSearch
filterOption={(input, option) => {
const label = typeof option?.label === 'string' ? option.label : String(option?.value || '')
return label.toLowerCase().includes(input.toLowerCase())
}}
options={directories.map((dir) => ({
value: dir,
label: (
{/* Bun 安装状态提示 */}
{!isBunInstalled && (
<BunInstallAlert>
<Alert
type="warning"
banner
style={{ borderRadius: 'var(--list-item-border-radius)' }}
message={
<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' }}
onClick={(e) => handleRemoveDirectory(dir, e)}
/>
<span>{t('code.bun_required_message')}</span>
<Button
type="primary"
size="small"
icon={<Download size={14} />}
onClick={handleInstallBun}
loading={isInstallingBun}
disabled={isInstallingBun}>
{isInstallingBun ? t('code.installing_bun') : t('code.install_bun')}
</Button>
</div>
)
}))}
/>
<Button onClick={handleFolderSelect} style={{ width: 120 }}>
{t('code.select_folder')}
</Button>
</Space.Compact>
</SettingsItem>
}
/>
</BunInstallAlert>
)}
<SettingsItem>
<div className="settings-label">{t('code.update_options')}</div>
<Checkbox checked={autoUpdateToLatest} onChange={(e) => setAutoUpdateToLatest(e.target.checked)}>
{t('code.auto_update_to_latest')}
</Checkbox>
</SettingsItem>
</SettingsPanel>
<SettingsPanel>
<SettingsItem>
<div className="settings-label">{t('code.cli_tool')}</div>
<Select
style={{ width: '100%' }}
placeholder={t('code.cli_tool_placeholder')}
value={selectedCliTool}
onChange={handleCliToolChange}
options={CLI_TOOLS}
/>
</SettingsItem>
<Button
type="primary"
icon={<Terminal size={16} />}
size="large"
onClick={handleLaunch}
loading={isLaunching}
disabled={!canLaunch || !isBunInstalled}
block>
{isLaunching ? t('code.launching') : t('code.launch.label')}
</Button>
<SettingsItem>
<div className="settings-label">{t('code.model')}</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>
<Space.Compact style={{ width: '100%', display: 'flex' }}>
<Select
style={{ flex: 1, width: 480 }}
placeholder={t('code.folder_placeholder')}
value={currentDirectory || undefined}
onChange={handleDirectoryChange}
allowClear
showSearch
filterOption={(input, option) => {
const label = typeof option?.label === 'string' ? option.label : String(option?.value || '')
return label.toLowerCase().includes(input.toLowerCase())
}}
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>
<X
size={14}
style={{ marginLeft: 8, cursor: 'pointer', color: '#999' }}
onClick={(e) => handleRemoveDirectory(dir, e)}
/>
</div>
)
}))}
/>
<Button onClick={handleFolderSelect} style={{ width: 120 }}>
{t('code.select_folder')}
</Button>
</Space.Compact>
</SettingsItem>
<SettingsItem>
<div className="settings-label">{t('code.update_options')}</div>
<Checkbox checked={autoUpdateToLatest} onChange={(e) => setAutoUpdateToLatest(e.target.checked)}>
{t('code.auto_update_to_latest')}
</Checkbox>
</SettingsItem>
</SettingsPanel>
<Button
type="primary"
icon={<Terminal size={16} />}
size="large"
onClick={handleLaunch}
loading={isLaunching}
disabled={!canLaunch || !isBunInstalled}
block>
{isLaunching ? t('code.launching') : t('code.launch.label')}
</Button>
</MainContent>
</ContentContainer>
</Container>
)
}
// 样式组件
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
`
const MainContent = styled.div`
width: 600px;
margin: auto;
`
@ -347,7 +384,6 @@ const Title = styled.h1`
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
margin-top: -50px;
color: var(--color-text-1);
`

View File

@ -3,7 +3,7 @@ import { useMinapps } from '@renderer/hooks/useMinapps'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import tabsService from '@renderer/services/TabsService'
import { FileSearch, Folder, Languages, LayoutGrid, Palette, Sparkle, Terminal } from 'lucide-react'
import { Code, FileSearch, Folder, Languages, LayoutGrid, Palette, Sparkle } from 'lucide-react'
import { FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
@ -54,7 +54,7 @@ const LaunchpadPage: FC = () => {
bgColor: 'linear-gradient(135deg, #F59E0B, #FBBF24)' // 文件:金色,代表资源和重要性
},
{
icon: <Terminal size={32} className="icon" />,
icon: <Code size={32} className="icon" />,
text: t('title.code'),
path: '/code',
bgColor: 'linear-gradient(135deg, #1F2937, #374151)' // Code CLI高级暗黑色代表专业和技术

View File

@ -62,7 +62,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 132,
version: 133,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate
},

View File

@ -2119,6 +2119,15 @@ const migrateConfig = {
logger.error('migrate 132 error', error as Error)
return state
}
},
'133': (state: RootState) => {
try {
state.settings.sidebarIcons.visible.push('code_tools')
return state
} catch (error) {
logger.error('migrate 133 error', error as Error)
return state
}
}
}

View File

@ -21,7 +21,15 @@ import { RemoteSyncState } from './backup'
export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter' | 'Alt+Enter'
export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files'
export type SidebarIcon =
| 'assistants'
| 'agents'
| 'paintings'
| 'translate'
| 'minapp'
| 'knowledge'
| 'files'
| 'code_tools'
export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
'assistants',