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}
+ />
))}
) : (