`.ts` files under `src/shared` must use camelCase (naming-conventions §3.2);
kebab-case is only sanctioned under `packages/ui/` and `src/renderer/routes/`.
The `presets/` kebab naming came from best-practice-layered-preset-pattern.md,
which predated and conflicted with the authoritative spec.
- Rename presets/{code-cli,default-assistant,file-processing,mini-apps,
translate-languages,web-search-providers}.ts and utils/code-languages.ts
(plus the two matching __tests__ files) to camelCase, and update all importers
- Fix the upstream generator scripts/update-languages.ts to emit
codeLanguages.ts; otherwise `pnpm update:languages` would recreate the
kebab-named file
- Correct best-practice-layered-preset-pattern.md (kebab -> camelCase) and link
it to naming-conventions §3.2 so it cannot drift again
- Fix two stale `types/file` path references in file/architecture.md
11 KiB
Layered Preset Configuration Pattern
The Layered Preset Configuration pattern is the recommended approach for handling scenarios where you have predefined configurations that users can partially customize.
When to Use This Pattern
Use this pattern when:
- You have a list of predefined configurations (e.g., providers, templates, presets)
- Presets can be updated through app updates (add/remove/modify)
- Users can customize some fields of individual presets
- User customizations should persist while still receiving preset updates
Architecture
┌─────────────────────────────┐
│ Runtime Config │ ← Merged complete configuration
├─────────────────────────────┤
│ User Overrides Layer │ ← User modifications (delta only)
├─────────────────────────────┤
│ Default Presets Layer │ ← Predefined configurations
└─────────────────────────────┘
Key benefits:
- Presets can be updated independently
- User modifications are preserved
- Only differences are stored, minimizing storage
- Runtime merging provides the complete picture
Storage Strategy
| Scenario | Recommended Storage |
|---|---|
| Large item count (dozens+) + critical data | Dedicated SQLite table |
| Small item count (< 20) | Preference storage |
For large-scale scenarios, the layered pattern still applies, but implementation details vary based on data characteristics. Refer to DataApi documentation for guidance.
Large-Scale Scenario: SQLite + Registry Service
For large item counts (dozens+) backed by SQLite, the layered merge is
handled by a Registry Service in src/main/data/services/.
The Registry Service reads preset data from a package or shared constants,
obtains user overrides from the owning Entity Service, and returns
merged results. It does not access the database directly.
This document focuses on small-scale scenarios using Preference storage.
Where preset-only fields merge
SQLite-backed entities have three field classes:
| Class | Owns | Runtime location |
|---|---|---|
| User-editable | DB row | Written via PATCH /:resource/:id |
| Runtime default | Code constants | Merged in rowToEntity |
| Preset-only static | Registry package | Merged in rowToEntity |
Preset-only static fields — websites, description, iconUrl, vendor
links — have no DB column. The Registry Service looks them up by preset
key during rowToEntity and folds them into the runtime entity. A single
GET /:resource/:id returns the complete object.
Do not split preset-only fields into a parallel endpoint. A separate
GET /:resource/:id/preset-metadata forces every consumer to issue two
requests for one logical entity, fragments the type (Entity +
EntityPresetMetadata), and duplicates the merge contract. Merge at the
rowToEntity seam — that is where the entity becomes runtime-shaped.
Acceptable exceptions:
- Preset payload is not 1:1 with the entity (e.g.
GET /catalogbrowsed before creating a row — nothing to merge against). - Field set is large and consumed by only one specialised surface — pay
the second request there, named as
GET /:resource/:id:full-metadataso the relationship to the parent stays explicit.
When in doubt: merge.
Preset File Standards
Location
All preset configurations should be placed in:
src/shared/data/presets/
File Format
Use .ts files (not JSON):
- Small configurations typically don't need online update services
- TypeScript provides type safety
- Types and data can be co-located
Naming Convention
File names follow the repo-wide naming-conventions.md §3.2 camelCase rule for .ts source — presets/ is not one of the kebab-case exceptions (those are limited to packages/ui/ and src/renderer/routes/).
| Element | Convention | Example |
|---|---|---|
| File name | camelCase | selectionActions.ts |
| Constant name | SCREAMING_SNAKE_CASE with PRESETS_ prefix |
PRESETS_SELECTION_ACTIONS |
Naming correspondence:
providers.ts→PRESETS_PROVIDERSselectionActions.ts→PRESETS_SELECTION_ACTIONSaiModels.ts→PRESETS_AI_MODELS
File Structure
A preset file should contain both type definitions and preset data:
Note: The
Providerexample below is for illustration purposes only and does not represent the actual provider implementation in Cherry Studio. Your actual data structure will vary based on your specific requirements.
// src/shared/data/presets/providers.ts
// Type definitions
export interface Provider {
id: string
name: string
apiHost: string
models: string[]
}
// Preset data
export const PRESETS_PROVIDERS: Provider[] = [
{
id: 'openai',
name: 'OpenAI',
apiHost: 'https://api.openai.com',
models: ['gpt-4', 'gpt-3.5-turbo'],
},
{
id: 'anthropic',
name: 'Anthropic',
apiHost: 'https://api.anthropic.com',
models: ['claude-3-opus', 'claude-3-sonnet'],
},
// ...
]
Implementation with Preference
Step 1: Define Override Type
In your preset file, define the override type that represents user-customizable fields:
// src/shared/data/presets/providers.ts
// User-overridable fields (exclude id since it's the identifier)
export type ProviderOverride = Partial<Omit<Provider, 'id'>>
// Map of provider id to its overrides
export type ProviderOverrides = Record<string, ProviderOverride>
Step 2: Register Preference Key
Add a preference key to store user overrides:
// src/shared/data/preference/preferenceSchemas.ts
export const DefaultPreferences = {
default: {
// ...existing preferences
'providers.overrides': {} as ProviderOverrides,
},
}
Step 3: Create Merge Hook
Create a custom hook that merges presets with user overrides:
Note: The hook below is a basic example. Your actual implementation should be tailored to your specific data structure and usage patterns. Consider factors like: which fields are user-editable, how merging should work for nested objects, whether you need filtering/sorting, etc.
// src/renderer/hooks/useProviders.ts
import { useCallback, useMemo } from 'react'
import { usePreference } from '@data/hooks/usePreference'
import { PRESETS_PROVIDERS, ProviderOverride } from '@shared/data/presets/providers'
export function useProviders() {
const [overrides, setOverrides] = usePreference('providers.overrides')
// Merge: presets + user overrides
const providers = useMemo(() => {
return PRESETS_PROVIDERS.map((preset) => ({
...preset,
...overrides[preset.id],
}))
}, [overrides])
// Update specific fields of a provider
const updateProvider = useCallback(
(id: string, updates: ProviderOverride) => {
setOverrides({
...overrides,
[id]: { ...overrides[id], ...updates },
})
},
[overrides, setOverrides]
)
// Reset a provider to default values
const resetProvider = useCallback(
(id: string) => {
const { [id]: _, ...rest } = overrides
setOverrides(rest)
},
[overrides, setOverrides]
)
// Check if a provider has been customized
const isCustomized = useCallback((id: string) => id in overrides, [overrides])
return {
providers,
updateProvider,
resetProvider,
isCustomized,
}
}
Usage Example
function ProviderSettings() {
const { providers, updateProvider, resetProvider, isCustomized } = useProviders()
return (
<>
{providers.map((provider) => (
<ProviderCard
key={provider.id}
provider={provider}
isCustomized={isCustomized(provider.id)}
onApiHostChange={(host) => updateProvider(provider.id, { apiHost: host })}
onReset={() => resetProvider(provider.id)}
/>
))}
</>
)
}
Pure Presets (No User Override)
For presets that don't require user customization, simply place them in the presets directory and import directly:
// src/shared/data/presets/languages.ts
export interface Language {
code: string
name: string
nativeName: string
}
export const PRESETS_LANGUAGES: Language[] = [
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'zh', name: 'Chinese', nativeName: '中文' },
{ code: 'ja', name: 'Japanese', nativeName: '日本語' },
// ...
]
Usage:
import { PRESETS_LANGUAGES } from '@shared/data/presets/languages'
function LanguageSelector() {
return (
<select>
{PRESETS_LANGUAGES.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.nativeName}
</option>
))}
</select>
)
}
Update Compatibility
The layered pattern ensures smooth updates:
| Operation | Behavior |
|---|---|
| Add new preset | Automatically appears in the list (no override) |
| Remove preset | Disappears from list; override data retained (harmless) |
| Modify preset field | User-overridden fields keep user values; others update |
Example scenario:
- App ships with
PRESETS_PROVIDERScontaining OpenAI withapiHost: 'https://api.openai.com' - User changes OpenAI's
apiHostto'https://my-proxy.com' - App update changes OpenAI's
modelsarray - Result: User keeps their custom
apiHost, but gets the newmodels
Versioning for Complex Presets
For presets with frequent configuration changes, consider adding a version field to facilitate migration management:
// src/shared/data/presets/complexConfig.ts
export const PRESETS_COMPLEX_CONFIG_VERSION = 2
export interface ComplexConfig {
id: string
// ... other fields
}
export const PRESETS_COMPLEX_CONFIG: ComplexConfig[] = [
// ...
]
Store the version alongside user overrides in a single object:
// preferenceSchemas.ts
'complex_config.overrides': {
version: 0,
data: {}
} as { version: number; data: ComplexConfigOverrides },
When the preset version changes, implement migration logic to transform the stored overrides to the new format.
This approach is recommended when:
- Configuration structure changes frequently
- Field names or types may be renamed/changed
- You need to clean up deprecated override data
Related Documentation
- Preference Usage Guide - How to use usePreference hook
- Preference Schema Guide - Adding new preference keys
- Data System Overview - Choosing the right data system