diff --git a/.github/pr-screenshots/39327/providers-collapsed.png b/.github/pr-screenshots/39327/providers-collapsed.png new file mode 100755 index 000000000..523bd1b84 Binary files /dev/null and b/.github/pr-screenshots/39327/providers-collapsed.png differ diff --git a/.github/pr-screenshots/39327/providers-expanded.png b/.github/pr-screenshots/39327/providers-expanded.png new file mode 100755 index 000000000..ab8c4213f Binary files /dev/null and b/.github/pr-screenshots/39327/providers-expanded.png differ diff --git a/.github/pr-screenshots/39327/tools-collapsed.png b/.github/pr-screenshots/39327/tools-collapsed.png new file mode 100755 index 000000000..d45ac3e5e Binary files /dev/null and b/.github/pr-screenshots/39327/tools-collapsed.png differ diff --git a/.github/pr-screenshots/39327/tools-expanded.png b/.github/pr-screenshots/39327/tools-expanded.png new file mode 100755 index 000000000..1f57248e6 Binary files /dev/null and b/.github/pr-screenshots/39327/tools-expanded.png differ diff --git a/apps/desktop/src/app/settings/credential-key-ui.tsx b/apps/desktop/src/app/settings/credential-key-ui.tsx index 4c916b3c0..8003b3487 100644 --- a/apps/desktop/src/app/settings/credential-key-ui.tsx +++ b/apps/desktop/src/app/settings/credential-key-ui.tsx @@ -2,7 +2,7 @@ import { type ChangeEvent, type KeyboardEvent } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -import { ExternalLink, Loader2, Save } from '@/lib/icons' +import { ChevronDown, ExternalLink, Loader2, Save } from '@/lib/icons' import { cn } from '@/lib/utils' import type { EnvVarInfo } from '@/types/hermes' @@ -133,79 +133,196 @@ function CredentialDocsLink({ href }: { href: string }) { ) } -/** One credential row — same ListRow layout as Advanced config fields. */ +/** One credential row — collapsible; description and docs link expand on click. */ export function CredentialKeyCard({ + expanded, info, label, + onExpand, + onToggle, placeholder, rowProps, varKey -}: { - info: EnvVarInfo - label: string - placeholder: string - rowProps: KeyRowProps - varKey: string -}) { +}: CredentialKeyCardProps) { const docsUrl = info.url?.trim() const description = info.description?.trim() + const expandable = Boolean(description || docsUrl) return ( - } - below={docsUrl ? : undefined} - description={description} - title={label} - /> +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onToggle() + } + } + : undefined + } + role={expandable ? 'button' : undefined} + tabIndex={expandable ? 0 : undefined} + > +
+
+ + + + {label} + + + {expandable && ( + + )} +
+ +
e.stopPropagation()} + onFocus={() => { + if (expandable && !expanded) { + onExpand() + } + }} + > + +
+
+ + {expandable && expanded && ( +
e.stopPropagation()}> + {description && ( +

+ {description} +

+ )} + + {docsUrl && } +
+ )} +
) } -/** Provider API key group — primary + optional advanced fields as ListRows. */ -export function ProviderKeyRows({ - group, - rowProps -}: { - group: ProviderKeyRowGroup - rowProps: KeyRowProps -}) { +/** Provider API key group — collapsible card; description, docs link, and advanced fields expand on click. */ +export function ProviderKeyRows({ expanded, group, onExpand, onToggle, rowProps }: ProviderKeyRowsProps) { const docsUrl = group.docsUrl?.trim() const description = group.description?.trim() - const docsBelow = docsUrl ? : undefined + const expandable = Boolean(description || docsUrl || group.advanced.length > 0) return ( - <> - { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onToggle() + } + } + : undefined + } + role={expandable ? 'button' : undefined} + tabIndex={expandable ? 0 : undefined} + > +
+
+ + + + {group.name} + + + {expandable && ( + + )} +
+ +
e.stopPropagation()} + onFocus={() => { + if (expandable && !expanded) { + onExpand() + } + }} + > - } - below={docsBelow} - description={description} - title={group.name} - /> - {group.advanced.map(([key, info]) => { - const fieldLabel = isKeyVar(key, info) ? prettyName(key.replace(/(?:_API_KEY|_TOKEN|_KEY)$/i, '')) : friendlyFieldLabel(key, info) +
+
- return ( - e.stopPropagation()}> + {description && ( +

+ {description} +

+ )} + + {group.advanced.map(([key, info]) => { + const fieldLabel = isKeyVar(key, info) + ? prettyName(key.replace(/(?:_API_KEY|_TOKEN|_KEY)$/i, '')) + : friendlyFieldLabel(key, info) + + return ( + + } + key={key} + title={fieldLabel} /> - } - key={key} - title={fieldLabel} - /> - ) - })} - + ) + })} + + {docsUrl && } + + )} + ) } @@ -217,10 +334,30 @@ export function credentialRowLabel(varKey: string, info: EnvVarInfo): string { return prettyName(varKey) } +interface CredentialKeyCardProps { + expanded: boolean + info: EnvVarInfo + label: string + onExpand: () => void + onToggle: () => void + placeholder: string + rowProps: KeyRowProps + varKey: string +} + +interface ProviderKeyRowsProps { + expanded: boolean + group: ProviderKeyRowGroup + onExpand: () => void + onToggle: () => void + rowProps: KeyRowProps +} + export interface ProviderKeyRowGroup { advanced: [string, EnvVarInfo][] description?: string docsUrl?: string + hasAnySet: boolean name: string primary: [string, EnvVarInfo] } diff --git a/apps/desktop/src/app/settings/keys-settings.tsx b/apps/desktop/src/app/settings/keys-settings.tsx index 9918451f3..89545acc4 100644 --- a/apps/desktop/src/app/settings/keys-settings.tsx +++ b/apps/desktop/src/app/settings/keys-settings.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useEffect, useMemo, useState } from 'react' import type { EnvVarInfo } from '@/types/hermes' @@ -28,6 +28,11 @@ const VIEW_CATEGORIES: Record = { export function KeysSettings({ view }: KeysSettingsProps) { const { rowProps, vars } = useEnvCredentials() + const [openKey, setOpenKey] = useState(null) + + useEffect(() => { + setOpenKey(null) + }, [view]) const groups = useMemo(() => { if (!vars) { @@ -54,15 +59,18 @@ export function KeysSettings({ view }: KeysSettingsProps) { return ( {visible.map(group => ( -
+
{group.entries.map(([key, info]: [string, EnvVarInfo]) => { const label = credentialRowLabel(key, info) return ( setOpenKey(key)} + onToggle={() => setOpenKey(prev => (prev === key ? null : key))} placeholder={credentialPlaceholder(key, info, label)} rowProps={rowProps} varKey={key} diff --git a/apps/desktop/src/app/settings/providers-settings.tsx b/apps/desktop/src/app/settings/providers-settings.tsx index 759d61a44..413ebd282 100644 --- a/apps/desktop/src/app/settings/providers-settings.tsx +++ b/apps/desktop/src/app/settings/providers-settings.tsx @@ -165,6 +165,7 @@ function NoProviderKeys() { export function ProvidersSettings({ onViewChange, view }: ProvidersSettingsProps) { const { rowProps, vars } = useEnvCredentials() const [oauthProviders, setOauthProviders] = useState([]) + const [openProvider, setOpenProvider] = useState(null) // The onboarding overlay owns the OAuth flow. Watch its `manual` flag so we // re-read connection state when the user finishes (or dismisses) a sign-in // they launched from this page — otherwise the cards keep their stale status. @@ -208,9 +209,16 @@ export function ProvidersSettings({ onViewChange, view }: ProvidersSettingsProps return ( {keyGroups.length > 0 ? ( -
+
{keyGroups.map(group => ( - + setOpenProvider(group.name)} + onToggle={() => setOpenProvider(prev => (prev === group.name ? null : group.name))} + rowProps={rowProps} + /> ))}
) : (