* feat(desktop): dedicated Providers settings with Accounts/API-keys subnav Rework provider configuration in the desktop app into its own Providers page that mirrors the first-run onboarding picker, instead of burying provider keys in the generic Tools & Keys list. - Add a Providers settings page (providers-settings.tsx) reusing the onboarding picker cards/ApiKeyForm so the two surfaces stay identical - Add a sidebar subnav (Accounts vs API keys) backed by a deep-linkable `pview` URL param; nested OverlayNavItem variant for a lighter active state so children don't compete with the parent item - Scope provider search to the active sub-view in its native card format (no more accordion fallback); collapse the API-key grid to the top providers behind a "Show all" toggle to cut scrolling - Launch real in-app OAuth from settings via startManualProviderOAuth; fix the misleading red "reason" banner that showed during an active connect (neutral style, hidden during a flow, omitted for direct per-provider launches) - Expand PROVIDER_GROUPS and add longest-prefix matching so providers like xAI/Ollama group correctly instead of landing under "Other" - Drop redundant messaging API keys from Tools & Keys (channel_managed) Co-authored-by: Cursor <cursoragent@cursor.com> * feat(desktop): Cursor-style provider key list with inline inputs Replace the card-grid API-key form on the Providers page with a per-provider list (mirrors Cursor's API keys section): - One row per vendor with its primary key input inline; rows with extra vars (base URL, region, alt tokens) expand to reveal those on focus - Set keys show their redacted value as the placeholder; Save appears on edit, Remove on a set key - Hide redundant alias key fields (e.g. ANTHROPIC_TOKEN vs ANTHROPIC_API_KEY) unless already set, and label set aliases by env var name so they're unambiguous - Smaller mono input text + compact height Co-authored-by: Cursor <cursoragent@cursor.com> * style(desktop): flatten providers settings UI chrome Tighten the providers settings surface to match the newer desktop style: remove extra card rails/borders in API-key rows, reduce visual noise in the providers subnav, replace bespoke link-like controls with shared text-button variants, and improve key input readability. * feat(desktop): rework providers settings UI - Flatten the shared OAuth picker rows (accounts + onboarding): drop the rounded-2xl/border cards for flat hover-bg rows; Nous hero keeps a subtle tint plus an animated blue→purple arc border. - Key fields collapse to a single input: a set key reads read-only (redacted) and edits in place on focus/click — no Replace/Cancel chrome. Save on type, Esc cancels (without closing the overlay), "Remove or esc to cancel" hint. - Non-key overrides render boxless, content-sized (field-sizing) and right-anchored; advanced fields align under the primary key column. - Add `xs` control size; size fields via padding (no fixed heights). - Cards expand on key-input focus; chevron shows on hover/expanded; expanded state uses a ring + softer bg tier so hover ≠ focus. - Relocate "Get a key" to the bottom-right of the expanded panel; drop the redundant provider description. - Cmd+K: add Providers (accounts) and Provider API keys deep-links. * fix(desktop): flatten provider fields, drop input shadows, fix Cmd+K provider rank - KeyField: collapse to one stacked label-above-input form field (drop the bespoke `naked`/inline/column branches); empty advanced overrides fade until hover/focus/set - styles: kill the resting + focus drop shadow on shared input chrome so form inputs sit flat (composer keeps its own shadow) - Cmd+K: drop stray `providers` keyword from Skills & Tools so the Providers settings entry ranks first for "provider" * fix(desktop): nous portal arc blue → orange * fix(desktop): rank appearance above settings in Cmd+K --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
207 lines
7.1 KiB
TypeScript
207 lines
7.1 KiB
TypeScript
import { IconDownload, IconRefresh, IconUpload } from '@tabler/icons-react'
|
|
import { useRef } from 'react'
|
|
|
|
import { getHermesConfigDefaults, getHermesConfigRecord, saveHermesConfig } from '@/hermes'
|
|
import { triggerHaptic } from '@/lib/haptics'
|
|
import { Archive, Globe, Info, KeyRound, Sparkles, Wrench, Zap } from '@/lib/icons'
|
|
import { notifyError } from '@/store/notifications'
|
|
|
|
import { useRouteEnumParam } from '../hooks/use-route-enum-param'
|
|
import { OverlayIconButton } from '../overlays/overlay-chrome'
|
|
import { OverlayMain, OverlayNavItem, OverlaySidebar, OverlaySplitLayout } from '../overlays/overlay-split-layout'
|
|
import { OverlayView } from '../overlays/overlay-view'
|
|
|
|
import { AboutSettings } from './about-settings'
|
|
import { AppearanceSettings } from './appearance-settings'
|
|
import { ConfigSettings } from './config-settings'
|
|
import { SECTIONS } from './constants'
|
|
import { GatewaySettings } from './gateway-settings'
|
|
import { KeysSettings } from './keys-settings'
|
|
import { McpSettings } from './mcp-settings'
|
|
import { PROVIDER_VIEWS, ProvidersSettings, type ProviderView } from './providers-settings'
|
|
import { SessionsSettings } from './sessions-settings'
|
|
import type { SettingsPageProps, SettingsView as SettingsViewId } from './types'
|
|
|
|
const SETTINGS_VIEWS: readonly SettingsViewId[] = [
|
|
...SECTIONS.map(s => `config:${s.id}` as SettingsViewId),
|
|
'providers',
|
|
'gateway',
|
|
'keys',
|
|
'mcp',
|
|
'sessions',
|
|
'about'
|
|
]
|
|
|
|
export function SettingsView({ gateway, onClose, onConfigSaved, onMainModelChanged }: SettingsPageProps) {
|
|
const [activeView, setActiveView] = useRouteEnumParam('tab', SETTINGS_VIEWS, 'config:model' as SettingsViewId)
|
|
// Providers subnav (Accounts vs API keys) lives in its own param so each
|
|
// sub-view is deep-linkable and survives a refresh.
|
|
const [providerView, setProviderView] = useRouteEnumParam<ProviderView>('pview', PROVIDER_VIEWS, 'accounts')
|
|
|
|
const openProviderView = (view: ProviderView) => {
|
|
setActiveView('providers')
|
|
setProviderView(view)
|
|
}
|
|
|
|
const importInputRef = useRef<HTMLInputElement | null>(null)
|
|
|
|
const exportConfig = async () => {
|
|
try {
|
|
const cfg = await getHermesConfigRecord()
|
|
const blob = new Blob([JSON.stringify(cfg, null, 2)], { type: 'application/json' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = 'hermes-config.json'
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
triggerHaptic('success')
|
|
} catch (err) {
|
|
notifyError(err, 'Export failed')
|
|
}
|
|
}
|
|
|
|
const resetConfig = async () => {
|
|
if (!window.confirm('Reset all settings to Hermes defaults?')) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
await saveHermesConfig(await getHermesConfigDefaults())
|
|
triggerHaptic('success')
|
|
onConfigSaved?.()
|
|
} catch (err) {
|
|
notifyError(err, 'Reset failed')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<OverlayView closeLabel="Close settings" onClose={onClose}>
|
|
<OverlaySplitLayout>
|
|
<OverlaySidebar>
|
|
{SECTIONS.map(s => {
|
|
const view = `config:${s.id}` as SettingsViewId
|
|
|
|
return (
|
|
<OverlayNavItem
|
|
active={activeView === view}
|
|
icon={s.icon}
|
|
key={s.id}
|
|
label={s.label}
|
|
onClick={() => setActiveView(view)}
|
|
/>
|
|
)
|
|
})}
|
|
<div className="my-2 h-px bg-border/30" />
|
|
<OverlayNavItem
|
|
active={activeView === 'providers'}
|
|
icon={Zap}
|
|
label="Providers"
|
|
onClick={() => setActiveView('providers')}
|
|
/>
|
|
{activeView === 'providers' && (
|
|
<div className="ml-3.5 flex flex-col gap-0.5 pl-1.5">
|
|
<OverlayNavItem
|
|
active={providerView === 'accounts'}
|
|
icon={Sparkles}
|
|
label="Accounts"
|
|
nested
|
|
onClick={() => openProviderView('accounts')}
|
|
/>
|
|
<OverlayNavItem
|
|
active={providerView === 'keys'}
|
|
icon={KeyRound}
|
|
label="API keys"
|
|
nested
|
|
onClick={() => openProviderView('keys')}
|
|
/>
|
|
</div>
|
|
)}
|
|
<OverlayNavItem
|
|
active={activeView === 'gateway'}
|
|
icon={Globe}
|
|
label="Gateway"
|
|
onClick={() => setActiveView('gateway')}
|
|
/>
|
|
<OverlayNavItem
|
|
active={activeView === 'keys'}
|
|
icon={KeyRound}
|
|
label="Tools & Keys"
|
|
onClick={() => setActiveView('keys')}
|
|
/>
|
|
<OverlayNavItem
|
|
active={activeView === 'mcp'}
|
|
icon={Wrench}
|
|
label="MCP"
|
|
onClick={() => setActiveView('mcp')}
|
|
/>
|
|
<OverlayNavItem
|
|
active={activeView === 'sessions'}
|
|
icon={Archive}
|
|
label="Archived Chats"
|
|
onClick={() => setActiveView('sessions')}
|
|
/>
|
|
<div className="my-2 h-px bg-border/30" />
|
|
<OverlayNavItem
|
|
active={activeView === 'about'}
|
|
icon={Info}
|
|
label="About"
|
|
onClick={() => setActiveView('about')}
|
|
/>
|
|
<div className="mt-auto flex items-center gap-1 pt-2">
|
|
<OverlayIconButton onClick={() => void exportConfig()} title="Export config">
|
|
<IconDownload className="size-3.5" />
|
|
</OverlayIconButton>
|
|
<OverlayIconButton
|
|
onClick={() => {
|
|
triggerHaptic('open')
|
|
importInputRef.current?.click()
|
|
}}
|
|
title="Import config"
|
|
>
|
|
<IconUpload className="size-3.5" />
|
|
</OverlayIconButton>
|
|
<OverlayIconButton
|
|
className="hover:text-destructive"
|
|
onClick={() => {
|
|
triggerHaptic('warning')
|
|
void resetConfig()
|
|
}}
|
|
title="Reset to defaults"
|
|
>
|
|
<IconRefresh className="size-3.5" />
|
|
</OverlayIconButton>
|
|
</div>
|
|
</OverlaySidebar>
|
|
|
|
<OverlayMain className="px-0 pb-0 pt-[calc(var(--titlebar-height)+1rem)]">
|
|
{activeView === 'config:appearance' ? (
|
|
<AppearanceSettings />
|
|
) : activeView === 'about' ? (
|
|
<AboutSettings />
|
|
) : activeView === 'gateway' ? (
|
|
<GatewaySettings />
|
|
) : activeView.startsWith('config:') ? (
|
|
<ConfigSettings
|
|
activeSectionId={activeView.slice('config:'.length)}
|
|
importInputRef={importInputRef}
|
|
onConfigSaved={onConfigSaved}
|
|
onMainModelChanged={onMainModelChanged}
|
|
/>
|
|
) : activeView === 'providers' ? (
|
|
<ProvidersSettings onViewChange={setProviderView} view={providerView} />
|
|
) : activeView === 'keys' ? (
|
|
<KeysSettings />
|
|
) : activeView === 'mcp' ? (
|
|
<McpSettings gateway={gateway} onConfigSaved={onConfigSaved} />
|
|
) : (
|
|
<SessionsSettings />
|
|
)}
|
|
</OverlayMain>
|
|
</OverlaySplitLayout>
|
|
</OverlayView>
|
|
)
|
|
}
|
|
|
|
export { SettingsView as SettingsPage }
|