208 lines
7.2 KiB
TypeScript
208 lines
7.2 KiB
TypeScript
import { useStore } from '@nanostores/react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useCallback, useEffect } from 'react'
|
|
|
|
import { useGatewayRequest } from '@/app/gateway/hooks/use-gateway-request'
|
|
import { $pluginRecords } from '@/contrib/plugins-store'
|
|
import { getEnvVars, getHermesConfigSchema } from '@/hermes'
|
|
import { useI18n } from '@/i18n'
|
|
import { Package, Palette, Settings2, Wrench } from '@/lib/icons'
|
|
import { $agentPlugins, isDesktopRelevantPlugin, loadAgentPlugins } from '@/store/agent-plugins'
|
|
import { $gatewayState } from '@/store/session'
|
|
import { TRANSLUCENCY_SUPPORTED } from '@/store/translucency'
|
|
|
|
import { useHermesConfigRecord } from '../hooks/use-config-record'
|
|
import { useOnProfileSwitch } from '../hooks/use-on-profile-switch'
|
|
|
|
import {
|
|
APPEARANCE_SETTING_IDS,
|
|
buildConfigSearchEntries,
|
|
buildCredentialSearchEntries,
|
|
type SettingsSearchEntry
|
|
} from './settings-search'
|
|
|
|
/**
|
|
* The granular settings-search catalog (appearance controls, config fields,
|
|
* credentials) for the command palette's Settings page. Page destinations stay
|
|
* on the palette side — it already owns section/page rows — this hook only
|
|
* contributes the deep, schema-driven targets.
|
|
*/
|
|
export function useSettingsSearchCatalog(enabled: boolean) {
|
|
const { t } = useI18n()
|
|
const configQuery = useHermesConfigRecord()
|
|
|
|
const schemaQuery = useQuery({
|
|
queryKey: ['hermes-config-schema'],
|
|
queryFn: () => getHermesConfigSchema(),
|
|
enabled,
|
|
staleTime: 5 * 60 * 1000
|
|
})
|
|
|
|
const {
|
|
data: envVars,
|
|
isError: envVarsError,
|
|
isFetching: envVarsFetching,
|
|
refetch: refetchEnvVars
|
|
} = useQuery({
|
|
queryKey: ['desktop-settings-search-env-vars'],
|
|
queryFn: () => getEnvVars(),
|
|
enabled,
|
|
staleTime: 5 * 60 * 1000
|
|
})
|
|
|
|
const refreshCatalog = useCallback(() => {
|
|
void refetchEnvVars()
|
|
}, [refetchEnvVars])
|
|
|
|
useOnProfileSwitch(refreshCatalog)
|
|
|
|
// Plugin rows: desktop plugins are already in their store (discovered at
|
|
// boot); agent plugins ride the gateway, so load them the first time the
|
|
// catalog is wanted — same RPC the Plugins page fires on mount, deduped by
|
|
// the store's own inflight guard.
|
|
const { requestGateway } = useGatewayRequest()
|
|
const gatewayState = useStore($gatewayState)
|
|
const desktopPluginRecords = useStore($pluginRecords)
|
|
const agentPlugins = useStore($agentPlugins)
|
|
|
|
useEffect(() => {
|
|
if (enabled && gatewayState === 'open') {
|
|
void loadAgentPlugins(requestGateway)
|
|
}
|
|
}, [enabled, gatewayState, requestGateway])
|
|
|
|
const pluginContext = t.settings.nav.plugins
|
|
|
|
const pluginEntries: SettingsSearchEntry[] = [
|
|
...Object.values(desktopPluginRecords).map(record => ({
|
|
context: pluginContext,
|
|
description: record.description,
|
|
icon: Package,
|
|
id: `plugin:desktop:${record.id}`,
|
|
keywords: ['plugin', 'extension', record.id],
|
|
label: record.name,
|
|
target: { plugin: record.id, view: 'plugins' as const }
|
|
})),
|
|
...agentPlugins.filter(isDesktopRelevantPlugin).map(row => ({
|
|
context: pluginContext,
|
|
description: row.description || undefined,
|
|
icon: Package,
|
|
id: `plugin:agent:${row.key ?? row.name}`,
|
|
keywords: ['plugin', 'extension', ...(row.key ? [row.key] : [])],
|
|
label: row.name,
|
|
target: { plugin: row.key ?? row.name, view: 'plugins' as const }
|
|
}))
|
|
]
|
|
|
|
// Never expose stale profile-scoped targets while a catalog is refreshing.
|
|
// Field/key results wait for the current profile's data rather than briefly
|
|
// pointing into the previous one.
|
|
const configEntries =
|
|
configQuery.isFetching || schemaQuery.isFetching || configQuery.isError || schemaQuery.isError
|
|
? []
|
|
: buildConfigSearchEntries(schemaQuery.data?.fields, configQuery.data, {
|
|
fieldDescriptions: t.settings.fieldDescriptions,
|
|
fieldLabels: t.settings.fieldLabels,
|
|
sections: t.settings.sections
|
|
})
|
|
|
|
const appearanceContext = t.settings.sections.appearance
|
|
const appearance = t.settings.appearance
|
|
|
|
const appearanceEntries: SettingsSearchEntry[] = [
|
|
{
|
|
context: appearanceContext,
|
|
description: t.language.description,
|
|
icon: Palette,
|
|
id: `setting:${APPEARANCE_SETTING_IDS.language}`,
|
|
keywords: ['locale'],
|
|
label: t.language.label,
|
|
target: { setting: APPEARANCE_SETTING_IDS.language, view: 'config:appearance' }
|
|
},
|
|
{
|
|
context: appearanceContext,
|
|
description: appearance.themeDesc,
|
|
icon: Palette,
|
|
id: `setting:${APPEARANCE_SETTING_IDS.theme}`,
|
|
keywords: ['color mode', 'skin'],
|
|
label: appearance.themeTitle,
|
|
target: { setting: APPEARANCE_SETTING_IDS.theme, view: 'config:appearance' }
|
|
},
|
|
{
|
|
context: appearanceContext,
|
|
icon: Palette,
|
|
id: `setting:${APPEARANCE_SETTING_IDS.uiScale}`,
|
|
keywords: ['zoom', 'size'],
|
|
label: appearance.uiScaleTitle,
|
|
target: { setting: APPEARANCE_SETTING_IDS.uiScale, view: 'config:appearance' }
|
|
},
|
|
// Linux has no translucency row to land on, and a palette hit that scrolls
|
|
// to nothing is worse than no hit.
|
|
...(TRANSLUCENCY_SUPPORTED
|
|
? [
|
|
{
|
|
context: appearanceContext,
|
|
description: appearance.translucencyDesc,
|
|
icon: Palette,
|
|
id: `setting:${APPEARANCE_SETTING_IDS.translucency}`,
|
|
keywords: ['opacity', 'transparent'],
|
|
label: appearance.translucencyTitle,
|
|
target: { setting: APPEARANCE_SETTING_IDS.translucency, view: 'config:appearance' as const }
|
|
}
|
|
]
|
|
: []),
|
|
{
|
|
context: appearanceContext,
|
|
description: appearance.backdropDesc,
|
|
icon: Palette,
|
|
id: `setting:${APPEARANCE_SETTING_IDS.backdrop}`,
|
|
keywords: ['background', 'blur'],
|
|
label: appearance.backdropTitle,
|
|
target: { setting: APPEARANCE_SETTING_IDS.backdrop, view: 'config:appearance' }
|
|
},
|
|
{
|
|
context: appearanceContext,
|
|
description: appearance.introSplashDesc,
|
|
icon: Palette,
|
|
id: `setting:${APPEARANCE_SETTING_IDS.introSplash}`,
|
|
keywords: ['splash', 'wordmark', 'empty chat', 'new chat'],
|
|
label: appearance.introSplashTitle,
|
|
target: { setting: APPEARANCE_SETTING_IDS.introSplash, view: 'config:appearance' }
|
|
},
|
|
{
|
|
context: appearanceContext,
|
|
description: appearance.toolViewDesc,
|
|
icon: Palette,
|
|
id: `setting:${APPEARANCE_SETTING_IDS.toolView}`,
|
|
keywords: ['tool display', 'technical'],
|
|
label: appearance.toolViewTitle,
|
|
target: { setting: APPEARANCE_SETTING_IDS.toolView, view: 'config:appearance' }
|
|
},
|
|
{
|
|
context: appearanceContext,
|
|
description: appearance.embedsDesc,
|
|
icon: Palette,
|
|
id: `setting:${APPEARANCE_SETTING_IDS.embeds}`,
|
|
keywords: ['external content', 'privacy'],
|
|
label: appearance.embedsTitle,
|
|
target: { setting: APPEARANCE_SETTING_IDS.embeds, view: 'config:appearance' }
|
|
}
|
|
]
|
|
|
|
const credentialEntries = buildCredentialSearchEntries(
|
|
envVarsFetching || envVarsError ? null : envVars,
|
|
{
|
|
settings: t.settings.nav.keysSettings,
|
|
tools: t.settings.nav.keysTools
|
|
},
|
|
{ settings: Settings2, tools: Wrench }
|
|
)
|
|
|
|
return {
|
|
appearanceEntries,
|
|
configEntries,
|
|
credentialEntries,
|
|
pluginEntries
|
|
}
|
|
}
|