@@ -20,6 +20,7 @@ import {
2020 CHIP_FIELD_SHELL ,
2121} from '@/app/workspace/[workspaceId]/components/credential-detail/components/chip-field'
2222import { BYOKKeySkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton'
23+ import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
2324
2425const logger = createLogger ( 'BYOKKeyManager' )
2526
@@ -31,6 +32,16 @@ export interface BYOKManagerProvider {
3132 placeholder : string
3233}
3334
35+ /**
36+ * Optional provider grouping. Each provider id should belong to exactly one
37+ * section; rows keep their {@link BYOKKeyManagerProps.providers} order within a
38+ * group. When omitted, providers render as a single flat list.
39+ */
40+ export interface BYOKProviderSection {
41+ label : string
42+ ids : string [ ]
43+ }
44+
3445interface BYOKKeyManagerProps {
3546 /** Providers to render, in display order. */
3647 providers : BYOKManagerProvider [ ]
@@ -43,7 +54,9 @@ interface BYOKKeyManagerProps {
4354 onDelete : ( providerId : string ) => Promise < void >
4455 isSaving ?: boolean
4556 isDeleting ?: boolean
46- /** Subtitle shown above the provider list. */
57+ /** Labeled provider groups. When omitted, renders a single flat list. */
58+ sections ?: BYOKProviderSection [ ]
59+ /** Optional subtitle shown above the provider list. */
4760 description ?: string
4861 /** Show the provider search box (hidden when there are only a couple). */
4962 showSearch ?: boolean
@@ -53,6 +66,9 @@ interface BYOKKeyManagerProps {
5366 * Shared BYOK key list + add/update/delete modals. Used by both the workspace
5467 * BYOK settings page and the enterprise mothership BYOK tab so the two stay
5568 * visually identical; only the provider set and the backing store differ.
69+ *
70+ * Renders content only (search, provider sections, modals) — the caller owns
71+ * the page chrome (background, scroll container, and `max-w` centering).
5672 */
5773export function BYOKKeyManager ( {
5874 providers,
@@ -62,7 +78,8 @@ export function BYOKKeyManager({
6278 onDelete,
6379 isSaving = false ,
6480 isDeleting = false ,
65- description = 'Use your own API keys for hosted model providers.' ,
81+ sections,
82+ description,
6683 showSearch = true ,
6784} : BYOKKeyManagerProps ) {
6885 const [ searchTerm , setSearchTerm ] = useState ( '' )
@@ -82,7 +99,12 @@ export function BYOKKeyManager({
8299 )
83100 } , [ searchTerm , providers ] )
84101
85- const showNoResults = searchTerm . trim ( ) && filteredProviders . length === 0
102+ const filteredIds = useMemo (
103+ ( ) => new Set ( filteredProviders . map ( ( p ) => p . id ) ) ,
104+ [ filteredProviders ]
105+ )
106+
107+ const showNoResults = searchTerm . trim ( ) !== '' && filteredProviders . length === 0
86108 const editingMeta = providers . find ( ( p ) => p . id === editingProvider )
87109 const deleteMeta = providers . find ( ( p ) => p . id === deleteConfirmProvider )
88110
@@ -124,9 +146,41 @@ export function BYOKKeyManager({
124146 }
125147 }
126148
149+ const renderRow = ( provider : BYOKManagerProvider ) => {
150+ const hasKey = configuredProviderIds . has ( provider . id )
151+ const Icon = provider . icon
152+
153+ return (
154+ < div key = { provider . id } className = 'flex items-center justify-between gap-2.5' >
155+ < div className = 'flex min-w-0 items-center gap-2.5' >
156+ < div className = 'flex size-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border-1)] bg-[var(--bg)]' >
157+ < Icon className = 'size-5' />
158+ </ div >
159+ < div className = 'flex min-w-0 flex-col justify-center gap-[1px]' >
160+ < span className = 'truncate text-[14px] text-[var(--text-body)]' > { provider . name } </ span >
161+ < span className = 'truncate text-[12px] text-[var(--text-muted)]' >
162+ { provider . description }
163+ </ span >
164+ </ div >
165+ </ div >
166+
167+ { hasKey ? (
168+ < div className = 'flex flex-shrink-0 items-center gap-2' >
169+ < Chip onClick = { ( ) => openEditModal ( provider . id ) } > Update</ Chip >
170+ < Chip onClick = { ( ) => setDeleteConfirmProvider ( provider . id ) } > Delete</ Chip >
171+ </ div >
172+ ) : (
173+ < Chip variant = 'primary' onClick = { ( ) => openEditModal ( provider . id ) } >
174+ Add Key
175+ </ Chip >
176+ ) }
177+ </ div >
178+ )
179+ }
180+
127181 return (
128182 < >
129- < div className = 'flex h-full flex-col gap-4.5' >
183+ < div className = 'flex flex-col gap-4.5' >
130184 { showSearch && (
131185 < div className = { CHIP_FIELD_SHELL } >
132186 < Search
@@ -143,56 +197,36 @@ export function BYOKKeyManager({
143197 </ div >
144198 ) }
145199
146- < p className = 'text-[var(--text-secondary)] text-sm' > { description } </ p >
200+ { description && < p className = 'text-[var(--text-secondary)] text-sm' > { description } </ p > }
147201
148- < div className = 'min-h-0 flex-1 overflow-y-auto' >
149- { isLoading ? (
150- < div className = 'flex flex-col gap-2' >
151- { providers . map ( ( p ) => (
152- < BYOKKeySkeleton key = { p . id } />
153- ) ) }
154- </ div >
155- ) : (
156- < div className = 'flex flex-col gap-2' >
157- { filteredProviders . map ( ( provider ) => {
158- const hasKey = configuredProviderIds . has ( provider . id )
159- const Icon = provider . icon
160-
161- return (
162- < div key = { provider . id } className = 'flex items-center justify-between gap-3' >
163- < div className = 'flex items-center gap-3' >
164- < div className = 'flex size-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-[var(--surface-6)]' >
165- < Icon className = 'size-4' />
166- </ div >
167- < div className = 'flex min-w-0 flex-col justify-center gap-[1px]' >
168- < span className = 'font-medium text-base' > { provider . name } </ span >
169- < p className = 'truncate text-[var(--text-muted)] text-sm' >
170- { provider . description }
171- </ p >
172- </ div >
173- </ div >
202+ { isLoading ? (
203+ < div className = 'flex flex-col gap-2' >
204+ { providers . map ( ( p ) => (
205+ < BYOKKeySkeleton key = { p . id } />
206+ ) ) }
207+ </ div >
208+ ) : showNoResults ? (
209+ < div className = 'py-4 text-center text-[var(--text-muted)] text-sm' >
210+ No providers found matching "{ searchTerm } "
211+ </ div >
212+ ) : sections ? (
213+ < div className = 'flex flex-col gap-7' >
214+ { sections . map ( ( section ) => {
215+ const rows = providers . filter (
216+ ( p ) => section . ids . includes ( p . id ) && filteredIds . has ( p . id )
217+ )
218+ if ( rows . length === 0 ) return null
174219
175- { hasKey ? (
176- < div className = 'flex flex-shrink-0 items-center gap-2' >
177- < Chip onClick = { ( ) => openEditModal ( provider . id ) } > Update</ Chip >
178- < Chip onClick = { ( ) => setDeleteConfirmProvider ( provider . id ) } > Delete</ Chip >
179- </ div >
180- ) : (
181- < Chip variant = 'primary' onClick = { ( ) => openEditModal ( provider . id ) } >
182- Add Key
183- </ Chip >
184- ) }
185- </ div >
186- )
187- } ) }
188- { showNoResults && (
189- < div className = 'py-4 text-center text-[var(--text-muted)] text-sm' >
190- No providers found matching "{ searchTerm } "
191- </ div >
192- ) }
193- </ div >
194- ) }
195- </ div >
220+ return (
221+ < SettingsSection key = { section . label } label = { section . label } >
222+ < div className = 'flex flex-col gap-2' > { rows . map ( renderRow ) } </ div >
223+ </ SettingsSection >
224+ )
225+ } ) }
226+ </ div >
227+ ) : (
228+ < div className = 'flex flex-col gap-2' > { filteredProviders . map ( renderRow ) } </ div >
229+ ) }
196230 </ div >
197231
198232 < ChipModal
0 commit comments