refactor(renderer-config): dissolve config bucket into owning domains

Reorganize src/renderer/config per renderer-architecture §8 so it holds only
app-global constants (constant.ts, env.ts).

- Delete v1 dead code: provider catalog, model defaults, embeddings/translate/
  webSearch presets, the mini-app catalog, and unused agent configs.
- Relocate live config to its owning domain: PROVIDER_URLS -> ProviderSettings,
  builtin MCP servers -> McpSettings, code providers -> code page, OCR ->
  FileProcessingSettings, message menu bar -> chat message frame.
- Sink model predicates (with modelReconcile/modelSearch) into utils/model with
  a curated named-export index; move tab/agent/sidebar helpers into utils.
- Split the sidebar icon map into the component layer (components/app/
  sidebarIcons) to remove the reverse config->component dependency, and drop
  the sidebar exports orphaned by #16413.
This commit is contained in:
fullex
2026-06-27 01:56:28 -07:00
parent 1e1e6c3938
commit 1065cd4dfd
92 changed files with 227 additions and 3101 deletions

View File

@@ -1,5 +1,5 @@
import { Avatar, AvatarFallback } from '@cherrystudio/ui'
import { getModelLogo } from '@renderer/config/models'
import { getModelLogo } from '@renderer/utils/model'
import { cn } from '@renderer/utils/style'
import { first } from 'lodash'
import type { FC } from 'react'

View File

@@ -1,4 +1,4 @@
import { getMiniAppsLogo } from '@renderer/config/miniApps'
import { getMiniAppsLogo } from '@renderer/components/Icons/miniAppsLogo'
import type { MiniApp } from '@shared/data/types/miniApp'
import type { FC } from 'react'

View File

@@ -4,25 +4,7 @@ import { describe, expect, it, vi } from 'vitest'
import MiniAppIcon from '../MiniAppIcon'
vi.mock('@renderer/config/miniApps', () => ({
allMiniApps: [
{
id: 'test-app-1',
name: 'Test App 1',
logo: '/test-logo-1.png',
url: 'https://test1.com',
bordered: true,
background: '#f0f0f0'
},
{
id: 'test-app-2',
name: 'Test App 2',
logo: '/test-logo-2.png',
url: 'https://test2.com',
bordered: false,
background: undefined
}
],
vi.mock('@renderer/components/Icons/miniAppsLogo', () => ({
getMiniAppsLogo: (logo: unknown) => {
if (logo !== 'compound-logo') return logo
const CompoundLogo = ({

View File

@@ -64,45 +64,6 @@ import {
Zhida,
Zhipu
} from '@cherrystudio/ui/icons'
import { PRESETS_MINI_APPS as SHARED_PRESETS } from '@shared/data/presets/miniApps'
/**
* Legacy mini-app entity type used by the deprecated Redux slice and config layer.
* The v2 MiniApp entity lives in @shared/data/types/miniApp.
*/
export type MiniAppType = {
id: string
name: string
nameKey?: string
supportedRegions?: string[]
logo?: string
url: string
bordered?: boolean
background?: string
style?: Record<string, unknown>
addTime?: string
type?: 'Custom' | 'Default'
}
// Renderer preset list, derived from the shared source of truth.
// Custom mini apps are no longer file-backed in v2 — they live in the
// `mini_app` SQLite table and are accessed via DataApi. Anything in v1's
// `custom-miniApps.json` reaches v2 only through MiniAppMigrator.
const PRESETS_MINI_APPS: MiniAppType[] = SHARED_PRESETS.map((app) => ({
id: app.id,
name: app.name,
nameKey: app.nameKey,
url: app.url,
logo: app.logo,
bordered: app.bordered,
background: app.background,
supportedRegions: app.supportedRegions,
style: app.style
}))
const allMiniApps = PRESETS_MINI_APPS
export { allMiniApps, PRESETS_MINI_APPS }
export function getMiniAppsLogo(LogoId: string | undefined): CompoundIcon | undefined {
if (!LogoId) {

View File

@@ -1,3 +1,4 @@
import type * as ModelModule from '@renderer/utils/model'
import { type Model, MODEL_CAPABILITY, type UniqueModelId } from '@shared/data/types/model'
import type { Provider } from '@shared/data/types/provider'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
@@ -59,7 +60,8 @@ vi.mock('@cherrystudio/ui/icons', () => ({
resolveIcon: () => null
}))
vi.mock('@renderer/config/models/reasoning', () => ({
vi.mock('@renderer/utils/model', async (importOriginal) => ({
...(await importOriginal<typeof ModelModule>()),
getModelSupportedReasoningEffortOptions: () => undefined
}))

View File

@@ -1,3 +1,4 @@
import type * as ModelModule from '@renderer/utils/model'
import type { Model, UniqueModelId } from '@shared/data/types/model'
import type { Provider } from '@shared/data/types/provider'
import { render, screen } from '@testing-library/react'
@@ -11,7 +12,8 @@ const { mockGetModelSupportedReasoningEffortOptions } = vi.hoisted(() => ({
mockGetModelSupportedReasoningEffortOptions: vi.fn()
}))
vi.mock('@renderer/config/models/reasoning', () => ({
vi.mock('@renderer/utils/model', async (importOriginal) => ({
...(await importOriginal<typeof ModelModule>()),
getModelSupportedReasoningEffortOptions: mockGetModelSupportedReasoningEffortOptions
}))

View File

@@ -1,6 +1,6 @@
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@cherrystudio/ui'
import { getModelDisplayTags, ModelTag } from '@renderer/components/Tags/Model'
import { getModelSupportedReasoningEffortOptions } from '@renderer/config/models/reasoning'
import { getModelSupportedReasoningEffortOptions } from '@renderer/utils/model'
import type { Model } from '@shared/data/types/model'
import type { Provider } from '@shared/data/types/provider'
import type { TFunction } from 'i18next'

View File

@@ -2,7 +2,7 @@ import { modelMatchesDisplayTag } from '@renderer/components/Tags/Model'
import { useModels } from '@renderer/hooks/useModel'
import { usePins } from '@renderer/hooks/usePins'
import { useProviders } from '@renderer/hooks/useProvider'
import { getSearchMatchScore } from '@renderer/utils/modelSearch'
import { getSearchMatchScore } from '@renderer/utils/model'
import { CHERRYAI_PROVIDER_ID } from '@shared/data/presets/cherryai'
import { isUniqueModelId, type Model, parseUniqueModelId, type UniqueModelId } from '@shared/data/types/model'
import type { Provider } from '@shared/data/types/provider'

View File

@@ -1,20 +1,16 @@
import { usePersistCache } from '@data/hooks/useCache'
import { usePreference } from '@data/hooks/usePreference'
import { SIDEBAR_ICON_COMPONENTS } from '@renderer/components/app/sidebarIcons'
import {
emitResourceListReveal,
type ResourceListRevealSource
} from '@renderer/components/chat/resources/resourceListRevealEvents'
import {
getOrderedVisibleSidebarIcons,
getSidebarMenuPath,
resolveSidebarActiveItem,
SIDEBAR_ICON_COMPONENTS
} from '@renderer/config/sidebar'
import { clearTabInstanceMetadata } from '@renderer/config/tabInstanceMetadata'
import { useTabs } from '@renderer/hooks/tab'
import useAvatar from '@renderer/hooks/useAvatar'
import { getSidebarIconLabelKey } from '@renderer/i18n/label'
import { getDefaultRouteTitle } from '@renderer/utils/routeTitle'
import { getOrderedVisibleSidebarIcons, getSidebarMenuPath, resolveSidebarActiveItem } from '@renderer/utils/sidebar'
import { clearTabInstanceMetadata } from '@renderer/utils/tabInstanceMetadata'
import type { SidebarIcon as SidebarIconType } from '@shared/data/preference/preferenceTypes'
import type { Ref } from 'react'
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'

View File

@@ -193,7 +193,7 @@ vi.mock('react-i18next', () => ({
})
}))
import { resolveSidebarAppTabEntryUrl } from '@renderer/config/sidebar'
import { resolveSidebarAppTabEntryUrl } from '@renderer/utils/sidebar'
import Sidebar from '../Sidebar'

View File

@@ -0,0 +1,35 @@
import { OpenClawSidebarIcon } from '@renderer/components/Icons/SvgIcon'
import type { SidebarMenuItem } from '@renderer/components/Sidebar/types'
import type { SidebarIcon } from '@shared/data/preference/preferenceTypes'
import {
Code,
FileSearch,
Folder,
Languages,
LayoutGrid,
Library,
MessageSquare,
MousePointerClick,
NotepadText,
Palette
} from 'lucide-react'
/**
* Icon component for each sidebar app. Keyed by the `SidebarIcon` union so the
* compiler enforces full coverage — adding a new sidebar app id without an icon
* here is a type error. Kept in the component layer because the values are React
* components; the navigation data and logic live in `@renderer/utils/sidebar`.
*/
export const SIDEBAR_ICON_COMPONENTS: Record<SidebarIcon, SidebarMenuItem['icon']> = {
assistants: MessageSquare,
agents: MousePointerClick,
paintings: Palette,
translate: Languages,
store: Library,
mini_app: LayoutGrid,
knowledge: FileSearch,
files: Folder,
code_tools: Code,
notes: NotepadText,
openclaw: OpenClawSidebarIcon
}

View File

@@ -1,7 +1,7 @@
import { Checkbox, Tooltip } from '@cherrystudio/ui'
import { getModelLogo } from '@renderer/config/models'
import { useTheme } from '@renderer/hooks/useTheme'
import type { Model } from '@renderer/types/model'
import { getModelLogo } from '@renderer/utils/model'
import { firstLetter, removeLeadingEmoji } from '@renderer/utils/naming'
import dayjs from 'dayjs'
import { Sparkle } from 'lucide-react'

View File

@@ -1,5 +1,8 @@
import type { MessageMenuBarScope } from '@renderer/config/registry/messageMenuBar'
import { DEFAULT_MESSAGE_MENUBAR_SCOPE, getMessageMenuBarConfig } from '@renderer/config/registry/messageMenuBar'
import type { MessageMenuBarScope } from '@renderer/components/chat/messages/frame/messageMenuBarConfig'
import {
DEFAULT_MESSAGE_MENUBAR_SCOPE,
getMessageMenuBarConfig
} from '@renderer/components/chat/messages/frame/messageMenuBarConfig'
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
import type { Topic } from '@renderer/types/topic'
import { getComposerTextFromParts } from '@renderer/utils/message/composerTokens'

View File

@@ -19,7 +19,7 @@ vi.mock('@cherrystudio/ui', () => ({
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('@renderer/config/models', () => ({
vi.mock('@renderer/utils/model', () => ({
getModelLogo: () => null
}))

View File

@@ -1,5 +1,8 @@
import {
DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS,
getMessageMenuBarConfig
} from '@renderer/components/chat/messages/frame/messageMenuBarConfig'
import { defaultMessageMenuConfig, type MessageListActions } from '@renderer/components/chat/messages/types'
import { DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS, getMessageMenuBarConfig } from '@renderer/config/registry/messageMenuBar'
import { COMPOSER_CLIPBOARD_FRAGMENT_MIME } from '@renderer/utils/message/composerClipboard'
import { fireEvent, render, screen } from '@testing-library/react'
import type { ComponentProps, MouseEvent, ReactElement, ReactNode } from 'react'

View File

@@ -1,7 +1,7 @@
import { TopicType } from '@renderer/types/topic'
import { describe, expect, it } from 'vitest'
import { getMessageMenuBarConfig } from '../messageMenuBar'
import { getMessageMenuBarConfig } from '../messageMenuBarConfig'
describe('messageMenuBar registry', () => {
// Regression: agent sessions don't mount the translation-overlay provider,

View File

@@ -1,10 +1,10 @@
import { loggerService } from '@logger'
import { CopyIcon, DeleteIcon, EditIcon, RefreshIcon } from '@renderer/components/Icons'
import {
DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS,
type MessageMenuBarButtonId,
STREAMING_DISABLED_BUTTON_IDS
} from '@renderer/config/registry/messageMenuBar'
} from '@renderer/components/chat/messages/frame/messageMenuBarConfig'
import { CopyIcon, DeleteIcon, EditIcon, RefreshIcon } from '@renderer/components/Icons'
import { getMessageTitle } from '@renderer/services/MessagesService'
import type { MessageExportView } from '@renderer/types/messageExport'
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'

View File

@@ -1,9 +1,9 @@
import { Avatar, AvatarFallback, AvatarImage, EmojiAvatar } from '@cherrystudio/ui'
import { getModelLogo } from '@renderer/config/models'
import { useTheme } from '@renderer/hooks/useTheme'
import { useTimer } from '@renderer/hooks/useTimer'
import { scrollIntoView } from '@renderer/utils/dom'
import { getTextFromParts } from '@renderer/utils/message/partsHelpers'
import { getModelLogo } from '@renderer/utils/model'
import { firstLetter, isEmoji, removeLeadingEmoji } from '@renderer/utils/naming'
import { CircleChevronDown } from 'lucide-react'
import { type FC, type Ref, useCallback, useEffect, useRef, useState } from 'react'

View File

@@ -7,7 +7,7 @@ const { mockIsGenerateImageModel, mockIsReasoningModel, mockIsSupportedToolUse }
mockIsSupportedToolUse: vi.fn()
}))
vi.mock('@renderer/config/models', () => ({
vi.mock('@renderer/utils/model', () => ({
isGenerateImageModel: (...args: unknown[]) => mockIsGenerateImageModel(...args),
isReasoningModel: (...args: unknown[]) => mockIsReasoningModel(...args)
}))

View File

@@ -9,6 +9,9 @@ import {
MdiLightbulbOn90,
MdiLightbulbQuestion
} from '@renderer/components/Icons/SvgIcon'
import { cacheService } from '@renderer/data/CacheService'
import { useAssistant } from '@renderer/hooks/useAssistant'
import type { ThinkingOption } from '@renderer/types/reasoning'
import {
getThinkModelType,
isDoubaoThinkingAutoModel,
@@ -17,10 +20,7 @@ import {
isOpenAIWebSearchModel,
isReasoningModel,
MODEL_SUPPORTED_OPTIONS
} from '@renderer/config/models'
import { cacheService } from '@renderer/data/CacheService'
import { useAssistant } from '@renderer/hooks/useAssistant'
import type { ThinkingOption } from '@renderer/types/reasoning'
} from '@renderer/utils/model'
import type { Model } from '@shared/data/types/model'
import type { FC, SVGProps } from 'react'
import { useCallback, useEffect, useMemo } from 'react'

View File

@@ -5,7 +5,7 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
import { useProvider } from '@renderer/hooks/useProvider'
import { useWebSearchProviders } from '@renderer/hooks/useWebSearch'
import { getEffectiveMcpMode } from '@renderer/utils/mcpMode'
import { canModelUseAssistantWebSearch, hasModelBuiltinWebSearch } from '@renderer/utils/modelReconcile'
import { canModelUseAssistantWebSearch, hasModelBuiltinWebSearch } from '@renderer/utils/model'
import { getWebSearchProviderLogo } from '@renderer/utils/webSearchProviderMeta'
import type { WebSearchProviderId } from '@shared/data/preference/preferenceTypes'
import { isGemini3Model, isGeminiModel, isGPT5SeriesReasoningModel, isOpenAIWebSearchModel } from '@shared/utils/model'

View File

@@ -50,7 +50,7 @@ vi.mock('@renderer/data/CacheService', () => ({
}
}))
vi.mock('@renderer/config/models', () => ({
vi.mock('@renderer/utils/model', () => ({
getThinkModelType: (...args: unknown[]) => mocks.getThinkModelType(...args),
isDoubaoThinkingAutoModel: (...args: unknown[]) => mocks.isDoubaoThinkingAutoModel(...args),
isFixedReasoningModel: (...args: unknown[]) => mocks.isFixedReasoningModel(...args),

View File

@@ -66,32 +66,29 @@ vi.mock('@renderer/utils/api', () => ({
splitApiKeyString: (value: string) => value.split(',').map((item) => item.trim())
}))
vi.mock('@renderer/config/models', () => {
const qwenModel = {
id: 'qwen',
name: 'Qwen',
provider: 'cherryai',
group: 'Qwen'
}
vi.mock('@renderer/utils/model', () => {
const isFunctionCallingModel = (model?: Model) =>
model?.capabilities.includes(MODEL_CAPABILITY.FUNCTION_CALL) ?? false
const isOpenRouterBuiltInWebSearchModel = () => false
const isWebSearchModel = (model?: Model) => model?.capabilities.includes(MODEL_CAPABILITY.WEB_SEARCH) ?? false
// Mirror the real reconcile composition over the mocked predicates above.
const hasModelBuiltinWebSearch = (model?: Model) => isWebSearchModel(model) || isOpenRouterBuiltInWebSearchModel()
const canModelUseAssistantWebSearch = (model?: Model) =>
hasModelBuiltinWebSearch(model) || isFunctionCallingModel(model)
return {
qwenModel,
SYSTEM_MODELS: new Proxy(
{ defaultModel: [qwenModel] },
{
get: (target, prop) => (prop in target ? target[prop as keyof typeof target] : [])
}
),
canModelUseAssistantWebSearch,
getThinkModelType: () => 'default',
isFunctionCallingModel: (model?: Model) => model?.capabilities.includes(MODEL_CAPABILITY.FUNCTION_CALL) ?? false,
hasModelBuiltinWebSearch,
isFunctionCallingModel,
isGemini3Model: () => false,
isGeminiModel: () => false,
isGPT5SeriesReasoningModel: () => false,
isOpenRouterBuiltInWebSearchModel: () => false,
isOpenRouterBuiltInWebSearchModel,
isOpenAIWebSearchModel: () => false,
isSupportedReasoningEffortModel: () => false,
isSupportedThinkingTokenModel: () => false,
isWebSearchModel: (model?: Model) => model?.capabilities.includes(MODEL_CAPABILITY.WEB_SEARCH) ?? false,
isWebSearchModel,
MODEL_SUPPORTED_OPTIONS: { default: ['none'] },
MODEL_SUPPORTED_REASONING_EFFORT: { default: ['none'] }
}

View File

@@ -6,7 +6,7 @@ const { mockIsGenerateImageModel } = vi.hoisted(() => ({
mockIsGenerateImageModel: vi.fn()
}))
vi.mock('@renderer/config/models', () => ({
vi.mock('@renderer/utils/model', () => ({
isGenerateImageModel: (...args: unknown[]) => mockIsGenerateImageModel(...args)
}))

View File

@@ -1,5 +1,5 @@
import { defineTool, registerTool, TopicType } from '@renderer/components/composer/tools/types'
import { isGenerateImageModel } from '@renderer/config/models'
import { isGenerateImageModel } from '@renderer/utils/model'
import { Image } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'

View File

@@ -1,9 +1,9 @@
import { defineTool, registerTool, type ToolRenderContext, TopicType } from '@renderer/components/composer/tools/types'
import { permissionModeCards } from '@renderer/config/agent'
import { defaultConfiguration } from '@renderer/hooks/agents/agentConfiguration'
import { useAgent } from '@renderer/hooks/agents/useAgent'
import { useUpdateAgent } from '@renderer/hooks/agents/useAgent'
import type { PermissionMode } from '@renderer/types/agent'
import { permissionModeCards } from '@renderer/utils/agent'
import { FolderPen, Pointer, RefreshCcw, Route } from 'lucide-react'
import type { ReactNode } from 'react'
import { useCallback, useEffect, useMemo } from 'react'

View File

@@ -34,7 +34,7 @@ import { type Topic, TopicType } from '@renderer/types/topic'
import { buildFilePartsForAttachments } from '@renderer/utils/file/buildFileParts'
import { getSendMessageShortcutLabel } from '@renderer/utils/input'
import type { ComposerAttachment } from '@renderer/utils/message/composerAttachment'
import { canModelUseAssistantWebSearch } from '@renderer/utils/modelReconcile'
import { canModelUseAssistantWebSearch } from '@renderer/utils/model'
import { getLeadingEmoji } from '@renderer/utils/naming'
import { cn } from '@renderer/utils/style'
import type { ComposerQueuedMessagePayload } from '@shared/ai/transport'

View File

@@ -327,7 +327,12 @@ vi.mock('@renderer/components/resource', () => ({
)
}))
vi.mock('@renderer/config/models', () => ({
vi.mock('@renderer/utils/model', () => ({
// Mirrors the real reconcile logic using the mocked predicates below:
// canModelUseAssistantWebSearch = isWebSearchModel || isOpenRouterBuiltInWebSearchModel || isFunctionCallingModel.
// The first two predicates are stubbed to false here, so it reduces to the function-call check.
canModelUseAssistantWebSearch: (currentModel?: Model) =>
currentModel?.capabilities.includes(MODEL_CAPABILITY.FUNCTION_CALL) ?? false,
getThinkModelType: () => 'default',
isEmbeddingModel: () => false,
isFunctionCallingModel: (currentModel?: Model) =>

View File

@@ -11,7 +11,7 @@ const mocks = vi.hoisted(() => ({
isGenerateImageModels: vi.fn()
}))
vi.mock('@renderer/config/models', () => mocks)
vi.mock('@renderer/utils/model', () => mocks)
const model = (id: string) => ({ id }) as unknown as Model

View File

@@ -1,4 +1,4 @@
import { isGenerateImageModel, isGenerateImageModels, isVisionModel, isVisionModels } from '@renderer/config/models'
import { isGenerateImageModel, isGenerateImageModels, isVisionModel, isVisionModels } from '@renderer/utils/model'
import type { Model } from '@shared/data/types/model'
import { documentExts, imageExts, textExts } from '@shared/utils/file/fileExtensions'
import { useMemo } from 'react'

View File

@@ -1,10 +1,10 @@
import { clearTabInstanceMetadata } from '@renderer/config/tabInstanceMetadata'
import { usePersistCache } from '@renderer/data/hooks/useCache'
import { useCommandHandler } from '@renderer/hooks/command'
import { useTabs } from '@renderer/hooks/tab'
import useMacTransparentWindow from '@renderer/hooks/useMacTransparentWindow'
import { getDefaultRouteTitle, isPageTitledRoute } from '@renderer/utils/routeTitle'
import { cn } from '@renderer/utils/style'
import { clearTabInstanceMetadata } from '@renderer/utils/tabInstanceMetadata'
import { useCallback, useEffect, useMemo } from 'react'
import Sidebar from '../app/Sidebar'

View File

@@ -1,5 +1,5 @@
import EmojiIcon from '@renderer/components/EmojiIcon'
import { getMiniAppsLogo } from '@renderer/config/miniApps'
import { getMiniAppsLogo } from '@renderer/components/Icons/miniAppsLogo'
import { cn } from '@renderer/utils/style'
import { TAB_ICON_EMOJI_PREFIX } from '@renderer/utils/tabIcons'
import type { FC } from 'react'

View File

@@ -1,9 +1,9 @@
import { loggerService } from '@logger'
import { resolveSidebarAppTabEntryUrl } from '@renderer/config/sidebar'
import { usePersistCache } from '@renderer/data/hooks/useCache'
import { type OpenTabOptions, TabsContext, type TabsContextValue } from '@renderer/hooks/tab'
import { TabLruManager } from '@renderer/services/TabLruManager'
import { getDefaultRouteTitle, isPageTitledRoute, isTopLevelRoute } from '@renderer/utils/routeTitle'
import { resolveSidebarAppTabEntryUrl } from '@renderer/utils/sidebar'
import type { Tab, TabSavedState } from '@shared/data/cache/cacheValueTypes'
import { IpcChannel } from '@shared/IpcChannel'
import type { ReactNode } from 'react'

View File

@@ -38,7 +38,7 @@ vi.mock('@renderer/config/constant', () => ({
platform: 'linux'
}))
vi.mock('@renderer/config/miniApps', () => ({
vi.mock('@renderer/components/Icons/miniAppsLogo', () => ({
getMiniAppsLogo: () => undefined
}))

View File

@@ -1,5 +1,5 @@
import { resolveSidebarAppTabEntryUrl } from '@renderer/config/sidebar'
import type { Tab } from '@renderer/hooks/tab'
import { resolveSidebarAppTabEntryUrl } from '@renderer/utils/sidebar'
import { IpcChannel } from '@shared/IpcChannel'
import { useCallback, useEffect, useRef, useState } from 'react'

View File

@@ -1,67 +0,0 @@
import { resolveProviderIcon } from '@cherrystudio/ui/icons'
import type { PermissionModeCard } from '@renderer/types/agent'
import type { AgentBase, AgentConfiguration, AgentType } from '@shared/data/types/agent'
// Partial defaults — `name` and `model` are user-supplied at create time.
// Workspace defaults belong to session creation, not to the agent blueprint.
type PartialAgentBase = Partial<Omit<AgentBase, 'model'>>
export const DEFAULT_CLAUDE_CODE_CONFIG: PartialAgentBase = {} as const
export const DEFAULT_CHERRY_CLAW_CONFIG: PartialAgentBase & { configuration: AgentConfiguration } = {
configuration: {
permission_mode: 'bypassPermissions',
max_turns: 100,
env_vars: {},
soul_enabled: true,
scheduler_enabled: false,
scheduler_type: 'interval',
heartbeat_enabled: true,
heartbeat_interval: 30
}
}
export const getAgentTypeAvatar = (type: AgentType) => {
switch (type) {
case 'claude-code':
return resolveProviderIcon('anthropic')
default:
return undefined
}
}
export const permissionModeCards: PermissionModeCard[] = [
{
mode: 'default',
// t('agent.settings.tooling.permissionMode.default.title')
titleKey: 'agent.settings.tooling.permissionMode.default.title',
titleFallback: 'Normal Mode',
descriptionKey: 'agent.settings.tooling.permissionMode.default.description',
descriptionFallback: 'Can read files freely. Asks before editing or running commands.'
},
{
mode: 'plan',
// t('agent.settings.tooling.permissionMode.plan.title')
titleKey: 'agent.settings.tooling.permissionMode.plan.title',
titleFallback: 'Plan Mode',
descriptionKey: 'agent.settings.tooling.permissionMode.plan.description',
descriptionFallback: 'Can only read files and make plans. Cannot edit files or run commands.'
},
{
mode: 'acceptEdits',
// t('agent.settings.tooling.permissionMode.acceptEdits.title')
titleKey: 'agent.settings.tooling.permissionMode.acceptEdits.title',
titleFallback: 'Auto-edit Mode',
descriptionKey: 'agent.settings.tooling.permissionMode.acceptEdits.description',
descriptionFallback: 'Can read and edit files freely. Asks before running commands.'
},
{
mode: 'bypassPermissions',
// t('agent.settings.tooling.permissionMode.bypassPermissions.title')
titleKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title',
titleFallback: 'Full Auto Mode',
descriptionKey: 'agent.settings.tooling.permissionMode.bypassPermissions.description',
descriptionFallback: 'Can do everything without asking. Use with caution.',
caution: true
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
export * from './default'
export * from './embedding'
export * from './logo'
export * from './openai'
export * from './qwen'
export * from './reasoning'
export * from './tooluse'
export * from './utils'
export * from './vision'
export * from './websearch'

View File

@@ -1,6 +0,0 @@
import type { Model } from '@shared/data/types/model'
import { isQwen35to39Model as sharedIsQwen35to39Model, isQwenMTModel as sharedIsQwenMTModel } from '@shared/utils/model'
export const isQwenMTModel = (model: Model): boolean => sharedIsQwenMTModel(model)
export const isQwen35to39Model = (model?: Model): boolean => (model ? sharedIsQwen35to39Model(model) : false)

View File

@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useConversationNavigation } from '../useConversationNavigation'
// Drive the boundary over a fake tabs context; config/sidebar (the identity↔url registry)
// Drive the boundary over a fake tabs context; utils/sidebar (the identity↔url registry)
// runs for real, so these tests also lock the assistants/agents instanceKey wiring.
const tabsMock = vi.hoisted(() => ({
ctx: null as ReturnType<typeof makeCtx> | null,

View File

@@ -1,6 +1,6 @@
import { buildTabInstanceMetadata } from '@renderer/config/tabInstanceMetadata'
import { isPageTitledRoute } from '@renderer/utils/routeTitle'
import { emojiTabIcon } from '@renderer/utils/tabIcons'
import { buildTabInstanceMetadata } from '@renderer/utils/tabInstanceMetadata'
import type { Tab } from '@shared/data/cache/cacheValueTypes'
import type { TabInstanceAppId } from '@shared/types/tabInstanceMetadata'
import { useEffect } from 'react'

View File

@@ -21,7 +21,7 @@ import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger'
import { useModelById } from '@renderer/hooks/useModel'
import type { Assistant, AssistantSettings } from '@renderer/types/assistant'
import { reconcileReasoningEffortForModel, reconcileWebSearchForModel } from '@renderer/utils/modelReconcile'
import { reconcileReasoningEffortForModel, reconcileWebSearchForModel } from '@renderer/utils/model'
import type { ConcreteApiPaths } from '@shared/data/api/apiTypes'
import type { CreateAssistantDto, UpdateAssistantDto } from '@shared/data/api/schemas/assistants'
import type { Model } from '@shared/data/types/model'

View File

@@ -3,13 +3,13 @@ import {
type ResourceListRevealSource
} from '@renderer/components/chat/resources/resourceListRevealEvents'
import { useWindowFrame } from '@renderer/components/chat/shell/WindowFrameContext'
import { type TabsContextValue, useOptionalTabsContext } from '@renderer/hooks/tab'
import {
buildSidebarAppOpenMetadata,
getSidebarApp,
getSidebarAppTabInstanceKey,
tabBelongsToApp
} from '@renderer/config/sidebar'
import { type TabsContextValue, useOptionalTabsContext } from '@renderer/hooks/tab'
} from '@renderer/utils/sidebar'
import type { SidebarIcon } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import { useMemo } from 'react'

View File

@@ -8,7 +8,6 @@ import {
createRecentSessionEntryFromSession,
upsertGlobalSearchRecentEntry
} from '@renderer/components/GlobalSearch/globalSearchGroups'
import { getTabInstanceKey } from '@renderer/config/tabInstanceMetadata'
import { usePersistCache } from '@renderer/data/hooks/useCache'
import { useInvalidateCache } from '@renderer/data/hooks/useDataApi'
import { useAgent, useAgents } from '@renderer/hooks/agents/useAgent'
@@ -21,6 +20,7 @@ import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
import { getDefaultRouteTitle } from '@renderer/utils/routeTitle'
import { cn } from '@renderer/utils/style'
import { getTabInstanceKey } from '@renderer/utils/tabInstanceMetadata'
import type { AgentSessionEntity } from '@shared/data/api/schemas/agentSessions'
import { AGENT_WORKSPACE_TYPE, type AgentSessionWorkspaceSource } from '@shared/data/api/schemas/agentWorkspaces'
import { MIN_WINDOW_HEIGHT, SECOND_MIN_WINDOW_WIDTH } from '@shared/utils/window'

View File

@@ -18,7 +18,6 @@ import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
// resolved by tsgo on main's program (resolves on feat's); transitional, reverts
// to the barrel once main converges with feat.
import { ModelSelector } from '@renderer/components/Selector/model'
import { CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS, isSiliconAnthropicCompatibleModel } from '@renderer/config/codeProviders'
import { isMac, isWin } from '@renderer/config/constant'
import { usePersistCache } from '@renderer/data/hooks/useCache'
import { useCodeCli } from '@renderer/hooks/useCodeCli'
@@ -26,6 +25,10 @@ import { useModels } from '@renderer/hooks/useModel'
import { getProviderDisplayName, useProviders } from '@renderer/hooks/useProvider'
import { useTimer } from '@renderer/hooks/useTimer'
import { ipcApi } from '@renderer/ipc'
import {
CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS,
isSiliconAnthropicCompatibleModel
} from '@renderer/pages/code/codeProviders'
import { loggerService } from '@renderer/services/LoggerService'
import { EFFORT_RATIO } from '@renderer/types/reasoning'
import { getThinkingBudget } from '@shared/ai/reasoningBudget'

View File

@@ -9,7 +9,7 @@ import {
QoderCli,
QwenCode
} from '@cherrystudio/ui/icons'
import { CLAUDE_SUPPORTED_PROVIDERS } from '@renderer/config/codeProviders'
import { CLAUDE_SUPPORTED_PROVIDERS } from '@renderer/pages/code/codeProviders'
import { formatApiHost } from '@renderer/utils/api'
import { sanitizeProviderName } from '@renderer/utils/naming'
import type { EndpointType } from '@shared/data/types/model'

View File

@@ -16,7 +16,6 @@ import {
createRecentTopicEntryFromTopic,
upsertGlobalSearchRecentEntry
} from '@renderer/components/GlobalSearch/globalSearchGroups'
import { getTabInstanceKey } from '@renderer/config/tabInstanceMetadata'
import { usePersistCache } from '@renderer/data/hooks/useCache'
import { useCommandHandler } from '@renderer/hooks/command'
import { useCurrentTab, useCurrentTabId, useIsActiveTab, useTabSelfMetadata } from '@renderer/hooks/tab'
@@ -29,6 +28,7 @@ import type { FileMetadata } from '@renderer/types/file'
import type { Topic } from '@renderer/types/topic'
import { getDefaultRouteTitle } from '@renderer/utils/routeTitle'
import { cn } from '@renderer/utils/style'
import { getTabInstanceKey } from '@renderer/utils/tabInstanceMetadata'
import type { CherryMessagePart } from '@shared/data/types/message'
import type { UniqueModelId } from '@shared/data/types/model'
import { MIN_WINDOW_HEIGHT, SECOND_MIN_WINDOW_WIDTH } from '@shared/utils/window'

View File

@@ -62,14 +62,10 @@ vi.mock('@renderer/components/Selector', () => ({
ModelSelector: ({ trigger }: { trigger: ReactNode }) => <>{trigger}</>
}))
vi.mock('@renderer/config/models', () => ({
vi.mock('@renderer/utils/model', () => ({
isVisionModel: vi.fn(() => false)
}))
vi.mock('@renderer/config/models/_bridge', () => ({
toSharedCompatModel: vi.fn(() => undefined)
}))
vi.mock('@renderer/components/chat/messages/editing/MessageEditingContext', () => ({
useMessageEditing: () => ({ editingMessageId: null, editingMessage: null, startEditing: vi.fn() })
}))

View File

@@ -33,7 +33,6 @@ import {
toMessageListItem
} from '@renderer/components/chat/messages/utils/messageListItem'
import { ModelSelector } from '@renderer/components/Selector'
import { isVisionModel } from '@renderer/config/models'
import { useChatWrite } from '@renderer/hooks/chat/ChatWriteContext'
import { useCommandHandler } from '@renderer/hooks/command'
import { SiblingsContext } from '@renderer/hooks/SiblingsContext'
@@ -45,6 +44,7 @@ import { formatErrorMessageWithPrefix, isAbortError } from '@renderer/utils/erro
import { updateCodeBlock } from '@renderer/utils/markdown'
import { createComposerRichClipboardContentFromParts } from '@renderer/utils/message/composerClipboard'
import { getComposerTextFromParts } from '@renderer/utils/message/composerTokens'
import { isVisionModel } from '@renderer/utils/model'
import { translateText } from '@renderer/utils/translate'
import type { TranslateLangCode } from '@shared/data/preference/preferenceTypes'
import type { CherryMessagePart, CherryUIMessage, ModelSnapshot } from '@shared/data/types/message'

View File

@@ -1,17 +1,17 @@
import { usePreference } from '@data/hooks/usePreference'
import { SIDEBAR_ICON_COMPONENTS } from '@renderer/components/app/sidebarIcons'
import { CommandContextMenu, type CommandContextMenuExtraItem } from '@renderer/components/command'
import App from '@renderer/components/MiniApp/MiniApp'
import Scrollbar from '@renderer/components/Scrollbar'
import { useMiniApps } from '@renderer/hooks/useMiniApps'
import { getSidebarIconLabelKey } from '@renderer/i18n/label'
import {
getRequiredSidebarIconsVisible,
getSidebarMenuPath,
REQUIRED_SIDEBAR_ICONS,
sanitizeSidebarIcons,
SIDEBAR_ICON_COMPONENTS,
SIDEBAR_ICON_ORDER
} from '@renderer/config/sidebar'
import { useMiniApps } from '@renderer/hooks/useMiniApps'
import { getSidebarIconLabelKey } from '@renderer/i18n/label'
} from '@renderer/utils/sidebar'
import type { SidebarFavorite, SidebarIcon } from '@shared/data/preference/preferenceTypes'
import type { MiniApp as MiniAppType } from '@shared/data/types/miniApp'
import { useNavigate } from '@tanstack/react-router'

View File

@@ -1,6 +1,6 @@
import { loggerService } from '@logger'
import { LogoAvatar } from '@renderer/components/Icons'
import { getMiniAppsLogo } from '@renderer/config/miniApps'
import { getMiniAppsLogo } from '@renderer/components/Icons/miniAppsLogo'
import { useCurrentTab, useCurrentTabId } from '@renderer/hooks/tab'
import { useOptionalTabsContext } from '@renderer/hooks/tab'
import { useMiniAppPopup } from '@renderer/hooks/useMiniAppPopup'

View File

@@ -1,6 +1,6 @@
import { Scrollbar, Sortable, Tooltip } from '@cherrystudio/ui'
import { LogoAvatar } from '@renderer/components/Icons'
import { getMiniAppsLogo } from '@renderer/config/miniApps'
import { getMiniAppsLogo } from '@renderer/components/Icons/miniAppsLogo'
import type { MiniApp } from '@shared/data/types/miniApp'
import { ArrowLeftToLine, ArrowRightToLine } from 'lucide-react'
import type { FC } from 'react'

View File

@@ -30,7 +30,7 @@ vi.mock('@renderer/components/Icons', () => ({
LogoAvatar: ({ logo }: { logo: string }) => <span data-testid={`logo-${logo}`} />
}))
vi.mock('@renderer/config/miniApps', () => ({
vi.mock('@renderer/components/Icons/miniAppsLogo', () => ({
getMiniAppsLogo: () => undefined
}))

View File

@@ -12,7 +12,7 @@ import {
} from '@cherrystudio/ui'
import { loggerService } from '@logger'
import { LogoAvatar } from '@renderer/components/Icons'
import { getMiniAppsLogo } from '@renderer/config/miniApps'
import { getMiniAppsLogo } from '@renderer/components/Icons/miniAppsLogo'
import { useMiniApps } from '@renderer/hooks/useMiniApps'
import { compressImage, convertToBase64 } from '@renderer/utils/image'
import { uuid } from '@renderer/utils/uuid'

View File

@@ -29,7 +29,7 @@ vi.mock('@renderer/components/Icons', () => ({
LogoAvatar: ({ logo }: { logo: unknown }) => <img alt="miniapp-logo-preview" data-logo={String(logo)} />
}))
vi.mock('@renderer/config/miniApps', () => ({
vi.mock('@renderer/components/Icons/miniAppsLogo', () => ({
getMiniAppsLogo: (logo?: string) => (logo === 'application' ? 'application-icon' : undefined)
}))

View File

@@ -1,3 +1,4 @@
import { isMac, isWin } from '@renderer/config/constant'
import type {
BuiltinOcrProvider,
BuiltinOcrProviderId,
@@ -10,8 +11,6 @@ import type {
} from '@renderer/types/ocr'
import { parseTranslateLangCode } from '@shared/data/preference/preferenceTypes'
import { isMac, isWin } from './constant'
const tesseract: OcrTesseractProvider = {
id: 'tesseract',
name: 'Tesseract',

View File

@@ -1,7 +1,7 @@
import type { CompoundIcon } from '@cherrystudio/ui'
import { Application, Doc2x, Intel, Mineru, Mistral, Paddleocr, TesseractJs } from '@cherrystudio/ui/icons'
import { isWin } from '@renderer/config/constant'
import { TESSERACT_LANG_MAP } from '@renderer/config/ocr'
import { TESSERACT_LANG_MAP } from '@renderer/pages/settings/FileProcessingSettings/ocr'
import type { FileProcessorFeature, FileProcessorId } from '@shared/data/preference/preferenceTypes'
import type { FileProcessorFeatureCapability, FileProcessorMerged } from '@shared/data/presets/fileProcessing'

View File

@@ -1,8 +1,8 @@
import { Badge, Button, Popover, PopoverContent, PopoverTrigger, Tabs, TabsList, TabsTrigger } from '@cherrystudio/ui'
import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar'
import { builtinMcpServers } from '@renderer/config/builtinMcpServers'
import { useMcpServers } from '@renderer/hooks/useMcpServer'
import { getBuiltInMcpServerDescriptionLabelKey } from '@renderer/i18n/label'
import { builtinMcpServers } from '@renderer/pages/settings/McpSettings/builtinMcpServers'
import { cn } from '@renderer/utils/style'
import { Check, Plus } from 'lucide-react'
import type { FC } from 'react'

View File

@@ -10,7 +10,6 @@ import {
Tooltip
} from '@cherrystudio/ui'
import Scrollbar from '@renderer/components/Scrollbar'
import { getModelLogo } from '@renderer/config/models'
import type {
ModelHealthCheckGenerationOutput,
ModelHealthCheckSkipReason,
@@ -18,6 +17,7 @@ import type {
} from '@renderer/pages/settings/ProviderSettings/types/healthCheck'
import { HealthStatus } from '@renderer/pages/settings/ProviderSettings/types/healthCheck'
import { maskApiKey } from '@renderer/utils/api'
import { getModelLogo } from '@renderer/utils/model'
import { cn } from '@renderer/utils/style'
import { CheckCircle2, Info, Loader2, XCircle } from 'lucide-react'
import { type ReactNode, useEffect, useMemo, useState } from 'react'

View File

@@ -1,5 +1,5 @@
import { Avatar, AvatarFallback, Button, RowFlex, Switch, Tooltip } from '@cherrystudio/ui'
import { getModelLogo } from '@renderer/config/models'
import { getModelLogo } from '@renderer/utils/model'
import { cn } from '@renderer/utils/style'
import type { Model } from '@shared/data/types/model'
import { Settings, Trash2 } from 'lucide-react'

View File

@@ -1,5 +1,5 @@
import { Alert, Button, Checkbox } from '@cherrystudio/ui'
import { getModelLogo } from '@renderer/config/models'
import { getModelLogo } from '@renderer/utils/model'
import type { Model } from '@shared/data/types/model'
import { parseUniqueModelId, type UniqueModelId } from '@shared/data/types/model'
import { CheckCircle2, Plus, Trash2 } from 'lucide-react'

View File

@@ -46,7 +46,7 @@ vi.mock('@cherrystudio/ui', async (importOriginal) => {
}
})
vi.mock('@renderer/config/models', async (importOriginal) => ({
vi.mock('@renderer/utils/model', async (importOriginal) => ({
...(await importOriginal<object>()),
getModelLogo: () => null
}))

View File

@@ -1,3 +1,4 @@
import type * as ModelModule from '@renderer/utils/model'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -46,7 +47,8 @@ vi.mock('@cherrystudio/ui', async (importOriginal) => {
}
})
vi.mock('@renderer/config/models', () => ({
vi.mock('@renderer/utils/model', async (importOriginal) => ({
...(await importOriginal<typeof ModelModule>()),
getModelLogo: () => null
}))

View File

@@ -1,4 +1,4 @@
import { getSearchMatchScore } from '@renderer/utils/modelSearch'
import { getSearchMatchScore } from '@renderer/utils/model'
import type { Model } from '@shared/data/types/model'
export const isValidNewApiModel = (model: Model): boolean => !!(model.endpointTypes && model.endpointTypes.length > 0)

View File

@@ -1,10 +1,10 @@
import { Button, RowFlex } from '@cherrystudio/ui'
import { resolveProviderIcon } from '@cherrystudio/ui/icons'
import OauthButton from '@renderer/components/Oauth/OauthButton'
import { PROVIDER_URLS } from '@renderer/config/providers'
import { useProvider } from '@renderer/hooks/useProvider'
import { getProviderLabelKey } from '@renderer/i18n/label'
import { oauthCardClasses } from '@renderer/pages/settings/ProviderSettings/primitives/ProviderSettingsPrimitives'
import { PROVIDER_URLS } from '@renderer/pages/settings/ProviderSettings/providerUrls'
import { providerBills, providerCharge } from '@renderer/utils/oauth'
import { hasApiKeys } from '@shared/utils/provider'
import { CircleDollarSign, ReceiptText } from 'lucide-react'

View File

@@ -1,5 +1,5 @@
import { loggerService } from '@logger'
import { PROVIDER_URLS } from '@renderer/config/providers'
import { PROVIDER_URLS } from '@renderer/pages/settings/ProviderSettings/providerUrls'
import { validateApiHost } from '@renderer/utils/api'
import { ErrorCode, isDataApiError, isSerializedDataApiError, toDataApiError } from '@shared/data/api'
import { ENDPOINT_TYPE } from '@shared/data/types/model'

View File

@@ -1,5 +1,5 @@
import { PROVIDER_URLS } from '@renderer/config/providers'
import { useProviderAuthConfig } from '@renderer/hooks/useProvider'
import { PROVIDER_URLS } from '@renderer/pages/settings/ProviderSettings/providerUrls'
import type { Provider } from '@shared/data/types/provider'
import { getProviderHostTopology } from '@shared/utils/providerTopology'
import { useMemo } from 'react'

View File

@@ -1,686 +1,5 @@
import { OpenAIServiceTiers, type SystemProvider, type SystemProviderId } from '@renderer/types/provider'
import { CHERRYAI_API_BASE_URL, CHERRYAI_PROVIDER_ID, CHERRYAI_PROVIDER_NAME } from '@shared/data/presets/cherryai'
import { TOKENFLUX_HOST } from './constant'
import { qwenModel, SYSTEM_MODELS } from './models'
export const CHERRYAI_PROVIDER: SystemProvider = {
id: CHERRYAI_PROVIDER_ID as SystemProviderId,
name: CHERRYAI_PROVIDER_NAME,
type: 'openai',
apiKey: '',
apiHost: CHERRYAI_API_BASE_URL,
models: [qwenModel],
isSystem: true,
enabled: true
}
export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> = {
cherryin: {
id: 'cherryin',
name: 'CherryIN',
type: 'openai',
apiKey: '',
apiHost: 'https://open.cherryin.cc',
anthropicApiHost: 'https://open.cherryin.cc',
models: [],
isSystem: true,
enabled: true
},
silicon: {
id: 'silicon',
name: 'Silicon',
type: 'openai',
apiKey: '',
apiHost: 'https://api.siliconflow.cn',
anthropicApiHost: 'https://api.siliconflow.cn',
models: SYSTEM_MODELS.silicon,
isSystem: true,
enabled: false
},
aihubmix: {
id: 'aihubmix',
name: 'AiHubMix',
type: 'openai',
apiKey: '',
apiHost: 'https://aihubmix.com',
anthropicApiHost: 'https://aihubmix.com',
models: SYSTEM_MODELS.aihubmix,
isSystem: true,
enabled: false
},
ovms: {
id: 'ovms',
name: 'OpenVINO Model Server',
type: 'openai',
apiKey: '',
apiHost: 'http://localhost:8000/v3/',
models: SYSTEM_MODELS.ovms,
isSystem: true,
enabled: false
},
ocoolai: {
id: 'ocoolai',
name: 'ocoolAI',
type: 'openai',
apiKey: '',
apiHost: 'https://api.ocoolai.com',
models: SYSTEM_MODELS.ocoolai,
isSystem: true,
enabled: false
},
zhipu: {
id: 'zhipu',
name: 'ZhiPu',
type: 'openai',
apiKey: '',
apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
anthropicApiHost: 'https://open.bigmodel.cn/api/anthropic',
models: SYSTEM_MODELS.zhipu,
isSystem: true,
enabled: false
},
zai: {
id: 'zai',
name: 'Z.ai',
type: 'openai',
apiKey: '',
apiHost: 'https://api.z.ai/api/paas/v4/',
anthropicApiHost: 'https://api.z.ai/api/anthropic',
models: SYSTEM_MODELS.zai,
isSystem: true,
enabled: false
},
deepseek: {
id: 'deepseek',
name: 'deepseek',
type: 'openai',
apiKey: '',
apiHost: 'https://api.deepseek.com',
anthropicApiHost: 'https://api.deepseek.com/anthropic',
models: SYSTEM_MODELS.deepseek,
isSystem: true,
enabled: false
},
alayanew: {
id: 'alayanew',
name: 'AlayaNew',
type: 'openai',
apiKey: '',
apiHost: 'https://deepseek.alayanew.com',
models: SYSTEM_MODELS.alayanew,
isSystem: true,
enabled: false
},
dmxapi: {
id: 'dmxapi',
name: 'DMXAPI',
type: 'openai',
apiKey: '',
apiHost: 'https://www.dmxapi.cn',
anthropicApiHost: 'https://www.dmxapi.cn',
models: SYSTEM_MODELS.dmxapi,
isSystem: true,
enabled: false
},
aionly: {
id: 'aionly',
name: 'AIOnly',
type: 'openai',
apiKey: '',
apiHost: 'https://api.aiionly.com',
models: SYSTEM_MODELS.aionly,
isSystem: true,
enabled: false
},
burncloud: {
id: 'burncloud',
name: 'BurnCloud',
type: 'openai',
apiKey: '',
apiHost: 'https://ai.burncloud.com',
models: SYSTEM_MODELS.burncloud,
isSystem: true,
enabled: false
},
tokenflux: {
id: 'tokenflux',
name: 'TokenFlux',
type: 'openai',
apiKey: '',
apiHost: 'https://api.tokenflux.ai/openai/v1',
anthropicApiHost: 'https://api.tokenflux.ai/anthropic',
models: SYSTEM_MODELS.tokenflux,
isSystem: true,
enabled: false
},
'302ai': {
id: '302ai',
name: '302.AI',
type: 'openai',
apiKey: '',
apiHost: 'https://api.302.ai',
anthropicApiHost: 'https://api.302.ai',
models: SYSTEM_MODELS['302ai'],
isSystem: true,
enabled: false
},
cephalon: {
id: 'cephalon',
name: 'Cephalon',
type: 'openai',
apiKey: '',
apiHost: 'https://cephalon.cloud/user-center/v1/model',
models: SYSTEM_MODELS.cephalon,
isSystem: true,
enabled: false
},
lanyun: {
id: 'lanyun',
name: 'LANYUN',
type: 'openai',
apiKey: '',
apiHost: 'https://maas-api.lanyun.net',
models: SYSTEM_MODELS.lanyun,
isSystem: true,
enabled: false
},
ph8: {
id: 'ph8',
name: 'PH8',
type: 'openai',
apiKey: '',
apiHost: 'https://ph8.co',
models: SYSTEM_MODELS.ph8,
isSystem: true,
enabled: false
},
sophnet: {
id: 'sophnet',
name: 'SophNet',
type: 'openai',
apiKey: '',
apiHost: 'https://www.sophnet.com/api/open-apis/v1',
models: [],
isSystem: true,
enabled: false
},
ppio: {
id: 'ppio',
name: 'PPIO',
type: 'openai',
apiKey: '',
apiHost: 'https://api.ppinfra.com/v3/openai/',
models: SYSTEM_MODELS.ppio,
isSystem: true,
enabled: false
},
dashscope: {
id: 'dashscope',
name: 'Bailian',
type: 'openai',
apiKey: '',
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
anthropicApiHost: 'https://dashscope.aliyuncs.com/apps/anthropic',
models: SYSTEM_MODELS.dashscope,
isSystem: true,
enabled: false
},
minimax: {
id: 'minimax',
name: 'MiniMax',
type: 'openai',
apiKey: '',
apiHost: 'https://api.minimaxi.com/v1/',
anthropicApiHost: 'https://api.minimaxi.com/anthropic',
models: SYSTEM_MODELS.minimax,
isSystem: true,
enabled: false
},
'minimax-global': {
id: 'minimax-global',
name: 'MiniMax Global',
type: 'openai',
apiKey: '',
apiHost: 'https://api.minimax.io/v1/',
anthropicApiHost: 'https://api.minimax.io/anthropic',
models: SYSTEM_MODELS['minimax-global'],
isSystem: true,
enabled: false
},
moonshot: {
id: 'moonshot',
name: 'Moonshot AI',
type: 'openai',
apiKey: '',
apiHost: 'https://api.moonshot.cn',
anthropicApiHost: 'https://api.moonshot.cn/anthropic',
models: SYSTEM_MODELS.moonshot,
isSystem: true,
enabled: false
},
qiniu: {
id: 'qiniu',
name: 'Qiniu',
type: 'openai',
apiKey: '',
apiHost: 'https://api.qnaigc.com',
anthropicApiHost: 'https://api.qnaigc.com',
models: SYSTEM_MODELS.qiniu,
isSystem: true,
enabled: false
},
openrouter: {
id: 'openrouter',
name: 'OpenRouter',
type: 'openai',
apiKey: '',
apiHost: 'https://openrouter.ai/api/v1/',
// Anthropic-compatible endpoint for Agent mode (Claude Code SDK)
// https://openrouter.ai/docs/guides/guides/coding-agents/claude-code-integration
anthropicApiHost: 'https://openrouter.ai/api',
models: SYSTEM_MODELS.openrouter,
isSystem: true,
enabled: false
},
'new-api': {
id: 'new-api',
name: 'New API',
type: 'new-api',
apiKey: '',
apiHost: 'http://localhost:3000',
anthropicApiHost: 'http://localhost:3000',
models: SYSTEM_MODELS['new-api'],
isSystem: true,
enabled: false
},
ollama: {
id: 'ollama',
name: 'Ollama',
type: 'ollama',
apiKey: '',
apiHost: 'http://localhost:11434',
anthropicApiHost: 'http://localhost:11434',
models: SYSTEM_MODELS.ollama,
isSystem: true,
enabled: false
},
lmstudio: {
id: 'lmstudio',
name: 'LM Studio',
type: 'openai',
apiKey: '',
apiHost: 'http://localhost:1234',
anthropicApiHost: 'http://localhost:1234',
models: SYSTEM_MODELS.lmstudio,
isSystem: true,
enabled: false
},
anthropic: {
id: 'anthropic',
name: 'Anthropic',
type: 'anthropic',
apiKey: '',
apiHost: 'https://api.anthropic.com',
models: SYSTEM_MODELS.anthropic,
isSystem: true,
enabled: false
},
openai: {
id: 'openai',
name: 'OpenAI',
type: 'openai-response',
apiKey: '',
apiHost: 'https://api.openai.com',
models: SYSTEM_MODELS.openai,
isSystem: true,
enabled: false,
serviceTier: OpenAIServiceTiers.auto
},
'azure-openai': {
id: 'azure-openai',
name: 'Azure OpenAI',
type: 'azure-openai',
apiKey: '',
apiHost: '',
apiVersion: '',
models: SYSTEM_MODELS['azure-openai'],
isSystem: true,
enabled: false
},
gemini: {
id: 'gemini',
name: 'Gemini',
type: 'gemini',
apiKey: '',
apiHost: 'https://generativelanguage.googleapis.com',
models: SYSTEM_MODELS.gemini,
isSystem: true,
enabled: false,
isVertex: false
},
vertexai: {
id: 'vertexai',
name: 'VertexAI',
type: 'vertexai',
apiKey: '',
apiHost: '',
models: SYSTEM_MODELS.vertexai,
isSystem: true,
enabled: false,
isVertex: true
},
github: {
id: 'github',
name: 'Github Models',
type: 'openai',
apiKey: '',
apiHost: 'https://models.github.ai/inference',
models: SYSTEM_MODELS.github,
isSystem: true,
enabled: false
},
copilot: {
id: 'copilot',
name: 'Github Copilot',
type: 'openai',
apiKey: '',
apiHost: 'https://api.githubcopilot.com/',
models: SYSTEM_MODELS.copilot,
isSystem: true,
enabled: false,
isAuthed: false
},
doubao: {
id: 'doubao',
name: 'doubao',
type: 'openai',
apiKey: '',
apiHost: 'https://ark.cn-beijing.volces.com/api/v3/',
models: SYSTEM_MODELS.doubao,
isSystem: true,
enabled: false
},
baichuan: {
id: 'baichuan',
name: 'BAICHUAN AI',
type: 'openai',
apiKey: '',
apiHost: 'https://api.baichuan-ai.com',
models: SYSTEM_MODELS.baichuan,
isSystem: true,
enabled: false
},
stepfun: {
id: 'stepfun',
name: 'StepFun',
type: 'openai',
apiKey: '',
apiHost: 'https://api.stepfun.com',
anthropicApiHost: 'https://api.stepfun.com',
models: SYSTEM_MODELS.stepfun,
isSystem: true,
enabled: false
},
yi: {
id: 'yi',
name: 'Yi',
type: 'openai',
apiKey: '',
apiHost: 'https://api.lingyiwanwu.com',
models: SYSTEM_MODELS.yi,
isSystem: true,
enabled: false
},
infini: {
id: 'infini',
name: 'Infini',
type: 'openai',
apiKey: '',
apiHost: 'https://cloud.infini-ai.com/maas',
models: SYSTEM_MODELS.infini,
isSystem: true,
enabled: false
},
groq: {
id: 'groq',
name: 'Groq',
type: 'openai',
apiKey: '',
apiHost: 'https://api.groq.com/openai',
models: SYSTEM_MODELS.groq,
isSystem: true,
enabled: false
},
together: {
id: 'together',
name: 'Together',
type: 'openai',
apiKey: '',
apiHost: 'https://api.together.xyz',
models: SYSTEM_MODELS.together,
isSystem: true,
enabled: false
},
fireworks: {
id: 'fireworks',
name: 'Fireworks',
type: 'openai',
apiKey: '',
apiHost: 'https://api.fireworks.ai/inference',
models: SYSTEM_MODELS.fireworks,
isSystem: true,
enabled: false
},
nvidia: {
id: 'nvidia',
name: 'nvidia',
type: 'openai',
apiKey: '',
apiHost: 'https://integrate.api.nvidia.com',
models: SYSTEM_MODELS.nvidia,
isSystem: true,
enabled: false
},
grok: {
id: 'grok',
name: 'Grok',
type: 'openai',
apiKey: '',
apiHost: 'https://api.x.ai',
models: SYSTEM_MODELS.grok,
isSystem: true,
enabled: false
},
hyperbolic: {
id: 'hyperbolic',
name: 'Hyperbolic',
type: 'openai',
apiKey: '',
apiHost: 'https://api.hyperbolic.xyz',
models: SYSTEM_MODELS.hyperbolic,
isSystem: true,
enabled: false
},
mistral: {
id: 'mistral',
name: 'Mistral',
type: 'openai',
apiKey: '',
apiHost: 'https://api.mistral.ai',
models: SYSTEM_MODELS.mistral,
isSystem: true,
enabled: false
},
jina: {
id: 'jina',
name: 'Jina',
type: 'openai',
apiKey: '',
apiHost: 'https://api.jina.ai',
models: SYSTEM_MODELS.jina,
isSystem: true,
enabled: false
},
perplexity: {
id: 'perplexity',
name: 'Perplexity',
type: 'openai',
apiKey: '',
apiHost: 'https://api.perplexity.ai/',
models: SYSTEM_MODELS.perplexity,
isSystem: true,
enabled: false
},
modelscope: {
id: 'modelscope',
name: 'ModelScope',
type: 'openai',
apiKey: '',
apiHost: 'https://api-inference.modelscope.cn/v1/',
anthropicApiHost: 'https://api-inference.modelscope.cn',
models: SYSTEM_MODELS.modelscope,
isSystem: true,
enabled: false
},
xirang: {
id: 'xirang',
name: 'Xirang',
type: 'openai',
apiKey: '',
apiHost: 'https://wishub-x1.ctyun.cn',
models: SYSTEM_MODELS.xirang,
isSystem: true,
enabled: false
},
hunyuan: {
id: 'hunyuan',
name: 'hunyuan',
type: 'openai',
apiKey: '',
apiHost: 'https://api.hunyuan.cloud.tencent.com',
models: SYSTEM_MODELS.hunyuan,
isSystem: true,
enabled: false
},
'tencent-cloud-ti': {
id: 'tencent-cloud-ti',
name: 'Tencent Cloud TI',
type: 'openai',
apiKey: '',
apiHost: 'https://api.lkeap.cloud.tencent.com',
models: SYSTEM_MODELS['tencent-cloud-ti'],
isSystem: true,
enabled: false
},
'baidu-cloud': {
id: 'baidu-cloud',
name: 'Baidu Cloud',
type: 'openai',
apiKey: '',
apiHost: 'https://qianfan.baidubce.com/v2/',
models: SYSTEM_MODELS['baidu-cloud'],
isSystem: true,
enabled: false
},
gpustack: {
id: 'gpustack',
name: 'GPUStack',
type: 'openai',
apiKey: '',
apiHost: '',
models: SYSTEM_MODELS.gpustack,
isSystem: true,
enabled: false
},
voyageai: {
id: 'voyageai',
name: 'VoyageAI',
type: 'openai',
apiKey: '',
apiHost: 'https://api.voyageai.com',
models: SYSTEM_MODELS.voyageai,
isSystem: true,
enabled: false
},
'aws-bedrock': {
id: 'aws-bedrock',
name: 'AWS Bedrock',
type: 'aws-bedrock',
apiKey: '',
apiHost: '',
models: SYSTEM_MODELS['aws-bedrock'],
isSystem: true,
enabled: false
},
poe: {
id: 'poe',
name: 'Poe',
type: 'openai',
apiKey: '',
apiHost: 'https://api.poe.com/v1/',
models: SYSTEM_MODELS['poe'],
isSystem: true,
enabled: false
},
longcat: {
id: 'longcat',
name: 'LongCat',
type: 'openai',
apiKey: '',
apiHost: 'https://api.longcat.chat/openai',
anthropicApiHost: 'https://api.longcat.chat/anthropic',
models: SYSTEM_MODELS.longcat,
isSystem: true,
enabled: false
},
huggingface: {
id: 'huggingface',
name: 'Hugging Face',
type: 'openai-response',
apiKey: '',
apiHost: 'https://router.huggingface.co/v1/',
models: [],
isSystem: true,
enabled: false
},
gateway: {
id: 'gateway',
name: 'Vercel AI Gateway',
type: 'gateway',
apiKey: '',
apiHost: 'https://ai-gateway.vercel.sh/v1/ai',
models: [],
isSystem: true,
enabled: false
},
cerebras: {
id: 'cerebras',
name: 'Cerebras AI',
type: 'openai',
apiKey: '',
apiHost: 'https://api.cerebras.ai/v1',
models: SYSTEM_MODELS.cerebras,
isSystem: true,
enabled: false
},
mimo: {
id: 'mimo',
name: 'Xiaomi MiMo',
type: 'openai',
apiKey: '',
apiHost: 'https://api.xiaomimimo.com',
anthropicApiHost: 'https://api.xiaomimimo.com/anthropic',
models: SYSTEM_MODELS.mimo,
isSystem: true,
enabled: false
}
} as const
export const SYSTEM_PROVIDERS: SystemProvider[] = Object.values(SYSTEM_PROVIDERS_CONFIG)
export const NOT_SUPPORTED_RERANK_PROVIDERS = ['ollama', 'lmstudio'] as const satisfies SystemProviderId[]
export const ONLY_SUPPORTED_DIMENSION_PROVIDERS = ['ollama', 'infini'] as const satisfies SystemProviderId[]
import { TOKENFLUX_HOST } from '@renderer/config/constant'
import type { SystemProviderId } from '@renderer/types/provider'
type ProviderUrls = {
api: {

View File

@@ -11,11 +11,6 @@ vi.mock('../ModelService', () => ({
readDefaultModel: vi.fn().mockResolvedValue(undefined)
}))
// Mock CHERRYAI_PROVIDER
vi.mock('@renderer/config/providers', () => ({
CHERRYAI_PROVIDER: { id: 'cherryai', type: 'openai', apiHost: 'https://api.cherry-ai.com', models: [] }
}))
// Mock LoggerService
vi.mock('@renderer/services/LoggerService', () => ({
loggerService: {

View File

@@ -7,14 +7,6 @@ import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'
// --- Mocks Setup ---
// Add this before the test suites
vi.mock('@renderer/config/miniApps', () => {
return {
PRESETS_MINI_APPS: [],
allMiniApps: []
}
})
// Mock window.api
beforeEach(() => {
Object.defineProperty(window, 'api', {

View File

@@ -1,13 +1,12 @@
import { SIDEBAR_ICON_COMPONENTS } from '@renderer/components/app/sidebarIcons'
import { Library } from 'lucide-react'
import { describe, expect, it } from 'vitest'
import {
getDefaultSidebarFavorites,
getOrderedVisibleSidebarIcons,
getRequiredSidebarIconsVisible,
getSidebarMenuPath,
resolveSidebarActiveItem,
SIDEBAR_ICON_COMPONENTS,
SIDEBAR_ICON_ORDER
} from '../sidebar'
@@ -43,12 +42,6 @@ describe('sidebar config helpers', () => {
])
})
it('resets to default sidebar favorites without forcing non-default icons visible', () => {
const reset = getDefaultSidebarFavorites()
expect(reset).toEqual(['assistants', 'agents', 'store', 'translate', 'mini_app'])
})
it('resolves menu paths and active items with the paintings provider route', () => {
expect(getSidebarMenuPath('paintings', 'zhipu')).toBe('/app/paintings/zhipu')
expect(resolveSidebarActiveItem('/app/paintings/zhipu')).toBe('paintings')

View File

@@ -1,3 +1,4 @@
import type { PermissionModeCard } from '@renderer/types/agent'
import type { AgentConfiguration } from '@shared/data/types/agent'
export const DEFAULT_AGENT_AVATAR = '🤖'
@@ -9,3 +10,39 @@ export function getAgentAvatar(avatar?: unknown) {
export function getAgentAvatarFromConfiguration(configuration?: Pick<AgentConfiguration, 'avatar'> | null) {
return getAgentAvatar(configuration?.avatar)
}
export const permissionModeCards: PermissionModeCard[] = [
{
mode: 'default',
// t('agent.settings.tooling.permissionMode.default.title')
titleKey: 'agent.settings.tooling.permissionMode.default.title',
titleFallback: 'Normal Mode',
descriptionKey: 'agent.settings.tooling.permissionMode.default.description',
descriptionFallback: 'Can read files freely. Asks before editing or running commands.'
},
{
mode: 'plan',
// t('agent.settings.tooling.permissionMode.plan.title')
titleKey: 'agent.settings.tooling.permissionMode.plan.title',
titleFallback: 'Plan Mode',
descriptionKey: 'agent.settings.tooling.permissionMode.plan.description',
descriptionFallback: 'Can only read files and make plans. Cannot edit files or run commands.'
},
{
mode: 'acceptEdits',
// t('agent.settings.tooling.permissionMode.acceptEdits.title')
titleKey: 'agent.settings.tooling.permissionMode.acceptEdits.title',
titleFallback: 'Auto-edit Mode',
descriptionKey: 'agent.settings.tooling.permissionMode.acceptEdits.description',
descriptionFallback: 'Can read and edit files freely. Asks before running commands.'
},
{
mode: 'bypassPermissions',
// t('agent.settings.tooling.permissionMode.bypassPermissions.title')
titleKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title',
titleFallback: 'Full Auto Mode',
descriptionKey: 'agent.settings.tooling.permissionMode.bypassPermissions.description',
descriptionFallback: 'Can do everything without asking. Use with caution.',
caution: true
}
]

View File

@@ -1,4 +1,4 @@
import { isFunctionCallingModel } from '@renderer/config/models'
import { isFunctionCallingModel } from '@renderer/utils/model'
import type { Model } from '@shared/data/types/model'
/**

View File

@@ -1,7 +1,7 @@
import { type Model, MODEL_CAPABILITY, type ModelCapability } from '@shared/data/types/model'
import { describe, expect, it } from 'vitest'
import { canModelUseAssistantWebSearch, reconcileWebSearchForModel } from '../modelReconcile'
import { canModelUseAssistantWebSearch, reconcileWebSearchForModel } from '../reconcile'
const createModel = (capabilities: ModelCapability[] = []): Model => ({
id: 'provider::model',
@@ -14,7 +14,7 @@ const createModel = (capabilities: ModelCapability[] = []): Model => ({
isHidden: false
})
describe('modelReconcile web search', () => {
describe('reconcile web search', () => {
it('rejects enabled web search when the next model cannot consume it', () => {
const nextModel = createModel()

View File

@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import { getSearchMatchScore, type ModelSearchField } from '../modelSearch'
import { getSearchMatchScore, type ModelSearchField } from '../search'
describe('modelSearch', () => {
describe('search', () => {
const fields = [
{ value: 'GPT-4o', weight: 0, allowAbbreviation: true },
{ value: 'gpt-4o-mini', weight: 1, allowAbbreviation: true }

View File

@@ -0,0 +1,30 @@
// Curated public surface for the renderer model helpers.
// Named re-exports only (no `export *`) per naming-conventions §5.
export { isEmbeddingModel, isRerankModel } from './embedding'
export { getModelLogo } from './logo'
export { isGPT5SeriesReasoningModel } from './openai'
export {
getModelSupportedReasoningEffortOptions,
getThinkModelType,
isDoubaoThinkingAutoModel,
isFixedReasoningModel,
isQwenReasoningModel,
isReasoningModel,
isSupportedReasoningEffortModel,
isSupportedThinkingTokenModel,
isSupportedThinkingTokenQwenModel,
MODEL_SUPPORTED_OPTIONS,
MODEL_SUPPORTED_REASONING_EFFORT
} from './reasoning'
export {
canModelUseAssistantWebSearch,
hasModelBuiltinWebSearch,
reconcileReasoningEffortForModel,
reconcileWebSearchForModel
} from './reconcile'
export { getSearchMatchScore } from './search'
export { isFunctionCallingModel } from './tooluse'
export { isGenerateImageModels, isVisionModels } from './utils'
export { isGenerateImageModel, isVisionModel } from './vision'
export { isOpenAIWebSearchModel, isOpenRouterBuiltInWebSearchModel, isWebSearchModel } from './websearch'

View File

@@ -13,21 +13,21 @@
* patch needed". Callers compose multiple reconcile fns and only emit a
* settings patch when at least one returned non-null.
*/
import {
getThinkModelType,
isFunctionCallingModel,
isOpenRouterBuiltInWebSearchModel,
isSupportedReasoningEffortModel,
isSupportedThinkingTokenModel,
isWebSearchModel,
MODEL_SUPPORTED_OPTIONS,
MODEL_SUPPORTED_REASONING_EFFORT
} from '@renderer/config/models'
import { cacheService } from '@renderer/data/CacheService'
import type { AssistantSettings } from '@renderer/types/assistant'
import type { ThinkingOption } from '@renderer/types/reasoning'
import type { Model } from '@shared/data/types/model'
import {
getThinkModelType,
isSupportedReasoningEffortModel,
isSupportedThinkingTokenModel,
MODEL_SUPPORTED_OPTIONS,
MODEL_SUPPORTED_REASONING_EFFORT
} from './reasoning'
import { isFunctionCallingModel } from './tooluse'
import { isOpenRouterBuiltInWebSearchModel, isWebSearchModel } from './websearch'
export type ReasoningEffortPatch = {
reasoning_effort?: string
}

View File

@@ -1,26 +1,11 @@
import { OpenClawSidebarIcon } from '@renderer/components/Icons/SvgIcon'
import type { SidebarMenuItem } from '@renderer/components/Sidebar/types'
import {
buildTabInstanceMetadata,
getTabInstanceAppId,
getTabInstanceKey,
hasTabInstanceMetadataForApp
} from '@renderer/config/tabInstanceMetadata'
} from '@renderer/utils/tabInstanceMetadata'
import type { Tab } from '@shared/data/cache/cacheValueTypes'
import type { SidebarIcon } from '@shared/data/preference/preferenceTypes'
import { getDefaultValue } from '@shared/data/preference/preferenceUtils'
import {
Code,
FileSearch,
Folder,
Languages,
LayoutGrid,
Library,
MessageSquare,
MousePointerClick,
NotepadText,
Palette
} from 'lucide-react'
/**
* Context passed to sidebar navigation handlers. Carries per-call state the
@@ -51,7 +36,6 @@ export interface SidebarInstanceKey {
export interface SidebarApp {
id: SidebarIcon
icon: SidebarMenuItem['icon']
routePrefix: string
/** Url to open when no tab exists yet (defaults to `routePrefix`). */
resolveUrl?: (ctx: SidebarNavContext) => string
@@ -85,7 +69,6 @@ function isMessageOnlyConversationUrl(url: string): boolean {
export const SIDEBAR_APPS: readonly SidebarApp[] = [
{
id: 'assistants',
icon: MessageSquare,
routePrefix: '/app/chat',
instanceKey: {
keyFromUrl: (url) => getNormalConversationSearchParamFromUrl(url, 'topicId'),
@@ -95,7 +78,6 @@ export const SIDEBAR_APPS: readonly SidebarApp[] = [
},
{
id: 'agents',
icon: MousePointerClick,
routePrefix: '/app/agents',
instanceKey: {
keyFromUrl: (url) => getNormalConversationSearchParamFromUrl(url, 'sessionId'),
@@ -105,49 +87,40 @@ export const SIDEBAR_APPS: readonly SidebarApp[] = [
},
{
id: 'paintings',
icon: Palette,
routePrefix: '/app/paintings',
resolveUrl: ({ defaultPaintingProvider }) => `/app/paintings/${defaultPaintingProvider}`
},
{
id: 'translate',
icon: Languages,
routePrefix: '/app/translate'
},
{
id: 'store',
icon: Library,
routePrefix: '/app/library'
},
{
id: 'mini_app',
icon: LayoutGrid,
routePrefix: '/app/mini-app',
exactRouteFocus: true
},
{
id: 'knowledge',
icon: FileSearch,
routePrefix: '/app/knowledge'
},
{
id: 'files',
icon: Folder,
routePrefix: '/app/files'
},
{
id: 'code_tools',
icon: Code,
routePrefix: '/app/code'
},
{
id: 'notes',
icon: NotepadText,
routePrefix: '/app/notes'
},
{
id: 'openclaw',
icon: OpenClawSidebarIcon,
routePrefix: '/app/openclaw'
}
]
@@ -196,28 +169,6 @@ export function resolveSidebarAppTabEntryUrl(tab: Pick<Tab, 'metadata' | 'url'>)
return tab.url
}
/**
* The tab id to focus on a sidebar click, or undefined if none exists. Apps with
* sub-instances narrow the match to the last-focused key (so clicking returns to
* that one); keyless apps focus any tab they own.
*/
export function findAppTabToFocus(app: SidebarApp, tabs: Tab[], ctx: SidebarNavContext): string | undefined {
const key = app.instanceKey?.defaultKey(ctx)
const existing = tabs.find(
(t) =>
t.type === 'route' &&
(app.exactRouteFocus ? t.url === app.routePrefix : tabBelongsToApp(app, t.url)) &&
(app.instanceKey && key ? getSidebarAppTabInstanceKey(app, t) === key : true)
)
return existing?.id
}
/** The url to open when no owned tab exists yet (base route, resolveUrl, or routePrefix). */
export function resolveAppOpenUrl(app: SidebarApp, ctx: SidebarNavContext): string {
const key = app.instanceKey?.defaultKey(ctx)
return app.instanceKey && key ? app.routePrefix : (app.resolveUrl?.(ctx) ?? app.routePrefix)
}
export function buildSidebarAppOpenMetadata(app: SidebarApp, key?: string): Tab['metadata'] {
if (!app.instanceKey || !key) return undefined
if (app.id !== 'assistants' && app.id !== 'agents') return undefined
@@ -239,22 +190,6 @@ export const REQUIRED_SIDEBAR_ICONS: SidebarIcon[] = ['assistants']
const sidebarIconSet = new Set<SidebarIcon>(SIDEBAR_ICON_ORDER)
export const SIDEBAR_ROUTE_PREFIX_MAP: Record<SidebarIcon, string> = SIDEBAR_APPS.reduce(
(acc, app) => {
acc[app.id] = app.routePrefix
return acc
},
{} as Record<SidebarIcon, string>
)
export const SIDEBAR_ICON_COMPONENTS: Record<SidebarIcon, SidebarMenuItem['icon']> = SIDEBAR_APPS.reduce(
(acc, app) => {
acc[app.id] = app.icon
return acc
},
{} as Record<SidebarIcon, SidebarMenuItem['icon']>
)
export function getSidebarMenuPath(icon: SidebarIcon, defaultPaintingProvider: string): string {
const app = getSidebarApp(icon)
if (!app) return ''
@@ -302,7 +237,3 @@ export function getOrderedVisibleSidebarIcons(icons: readonly SidebarIcon[] | un
return visible
}
export function getDefaultSidebarFavorites(): SidebarIcon[] {
return getOrderedVisibleSidebarIcons(getDefaultValue('ui.sidebar.favorites'))
}

View File

@@ -5,11 +5,11 @@ import { TabRouter } from '@renderer/components/layout/TabRouter'
import { TITLE_BAR_HEIGHT_CLASS } from '@renderer/components/layout/titleBar'
import MiniAppTabsPool from '@renderer/components/MiniApp/MiniAppTabsPool'
import WindowControls, { useHasWindowControls } from '@renderer/components/WindowControls'
import { clearTabInstanceMetadata } from '@renderer/config/tabInstanceMetadata'
import { useTabs } from '@renderer/hooks/tab'
import { useWindowInitData } from '@renderer/hooks/useWindowInitData'
import { getDefaultRouteTitle, isPageTitledRoute } from '@renderer/utils/routeTitle'
import { cn } from '@renderer/utils/style'
import { clearTabInstanceMetadata } from '@renderer/utils/tabInstanceMetadata'
import type { SubWindowInitData } from '@shared/types/subWindow'
import { Activity, type CSSProperties, useEffect, useRef } from 'react'