@@ -12,6 +12,7 @@ import androidStudioIcon from '@/assets/client-icons/android-studio.svg';
1212import opencodeIcon from '@/assets/client-icons/opencode.svg' ;
1313import opencodeIconDark from '@/assets/client-icons/opencode-dark.svg' ;
1414import { ClientBrandIcon } from '@/components/ClientBrandIcon' ;
15+ import { EmojiPickerButton } from '@/components/emoji-picker-button.component' ;
1516import { resolveKnownClientKey } from '@/lib/clientIcons' ;
1617import {
1718 Laptop ,
@@ -62,7 +63,18 @@ const CLIENT_ICON_ASSETS: Record<string, string> = {
6263 'android-studio' : androidStudioIcon ,
6364} ;
6465
65- function ClientIcon ( { logo_uri, client_name } : { logo_uri ?: string | null ; client_name : string } ) {
66+ function ClientIcon ( {
67+ logo_uri,
68+ client_name,
69+ client_icon,
70+ } : {
71+ logo_uri ?: string | null ;
72+ client_name : string ;
73+ client_icon ?: string | null ;
74+ } ) {
75+ if ( client_icon ) {
76+ return < span > { client_icon } </ span > ;
77+ }
6678 const knownKey = resolveKnownClientKey ( client_name ) ;
6779 // opencode ships theme-specific marks; render our bundled official logo
6880 // (overriding any outdated self-reported logo_uri).
@@ -123,6 +135,7 @@ export default function ClientsPage() {
123135 const [ selected , setSelected ] = useState < OAuthClient | null > ( null ) ;
124136 const [ showRegister , setShowRegister ] = useState ( false ) ;
125137 const [ editAlias , setEditAlias ] = useState ( '' ) ;
138+ const [ editIcon , setEditIcon ] = useState ( '' ) ;
126139 const [ isSaving , setIsSaving ] = useState ( false ) ;
127140 const [ gatewayStatus , setGatewayStatus ] = useState < GatewayStatus > ( {
128141 running : false ,
@@ -204,6 +217,7 @@ export default function ClientsPage() {
204217 const openPanel = ( client : OAuthClient ) => {
205218 setSelected ( client ) ;
206219 setEditAlias ( client . client_alias || '' ) ;
220+ setEditIcon ( client . client_icon || '' ) ;
207221 } ;
208222
209223 const handleSaveAlias = async ( ) => {
@@ -212,9 +226,11 @@ export default function ClientsPage() {
212226 try {
213227 const updated = await updateOAuthClient ( selected . client_id , {
214228 client_alias : editAlias || undefined ,
229+ client_icon : editIcon || undefined ,
215230 } ) ;
216231 setClients ( ( prev ) => prev . map ( ( c ) => ( c . client_id === updated . client_id ? updated : c ) ) ) ;
217232 setSelected ( updated ) ;
233+ setEditIcon ( updated . client_icon || '' ) ;
218234 success ( t ( 'toast.saved' ) , t ( 'toast.savedBody' , { name : updated . client_alias || updated . client_name } ) ) ;
219235 } catch ( e ) {
220236 showError ( t ( 'toast.saveFailed' ) , e instanceof Error ? e . message : String ( e ) ) ;
@@ -374,7 +390,11 @@ export default function ClientsPage() {
374390 < CardContent className = "p-6" >
375391 < div className = "mb-4 flex items-start gap-4" >
376392 < div className = "flex h-14 w-14 flex-shrink-0 items-center justify-center rounded-xl border border-[rgb(var(--border-subtle))] bg-[rgb(var(--surface))] text-3xl" >
377- < ClientIcon logo_uri = { client . logo_uri } client_name = { client . client_name } />
393+ < ClientIcon
394+ logo_uri = { client . logo_uri }
395+ client_name = { client . client_name }
396+ client_icon = { client . client_icon }
397+ />
378398 </ div >
379399 < div className = "min-w-0 flex-1" >
380400 < h3 className = "mb-1 truncate text-lg font-semibold" > { displayName } </ h3 >
@@ -417,6 +437,8 @@ export default function ClientsPage() {
417437 client = { selected }
418438 editAlias = { editAlias }
419439 setEditAlias = { setEditAlias }
440+ editIcon = { editIcon }
441+ setEditIcon = { setEditIcon }
420442 isSaving = { isSaving }
421443 defaultSpaceId = { defaultSpace ?. id ?? null }
422444 onClose = { ( ) => setSelected ( null ) }
@@ -517,6 +539,8 @@ interface SidePanelProps {
517539 client : OAuthClient ;
518540 editAlias : string ;
519541 setEditAlias : ( v : string ) => void ;
542+ editIcon : string ;
543+ setEditIcon : ( v : string ) => void ;
520544 isSaving : boolean ;
521545 defaultSpaceId : string | null ;
522546 onClose : ( ) => void ;
@@ -531,6 +555,8 @@ function SidePanel({
531555 client,
532556 editAlias,
533557 setEditAlias,
558+ editIcon,
559+ setEditIcon,
534560 isSaving,
535561 defaultSpaceId,
536562 onClose,
@@ -542,14 +568,19 @@ function SidePanel({
542568} : SidePanelProps ) {
543569 const { t } = useTranslation ( [ 'clients' , 'nav' ] ) ;
544570 const aliasDirty = ( client . client_alias || '' ) !== editAlias ;
571+ const iconDirty = ( client . client_icon || '' ) !== editIcon ;
545572
546573 return (
547574 < div className = "animate-in slide-in-from-right fixed bottom-0 right-0 top-0 z-50 flex w-full min-w-[420px] max-w-[480px] flex-col border-l border-[rgb(var(--border))] bg-[rgb(var(--surface))] shadow-2xl duration-300" >
548575 < div className = "flex-shrink-0 border-b border-[rgb(var(--border))] bg-[rgb(var(--surface-elevated))] p-4" >
549576 < div className = "flex items-start justify-between" >
550577 < div className = "flex min-w-0 flex-1 items-center gap-3" >
551578 < div className = "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-lg border border-[rgb(var(--border-subtle))] bg-[rgb(var(--background))] text-2xl" >
552- < ClientIcon logo_uri = { client . logo_uri } client_name = { client . client_name } />
579+ < ClientIcon
580+ logo_uri = { client . logo_uri }
581+ client_name = { client . client_name }
582+ client_icon = { editIcon }
583+ />
553584 </ div >
554585 < div className = "min-w-0 flex-1" >
555586 < h2 className = "truncate text-lg font-bold" >
@@ -581,19 +612,25 @@ function SidePanel({
581612 < h3 className = "mb-2 text-xs font-semibold uppercase tracking-wide text-[rgb(var(--muted))]" >
582613 { t ( 'panel.displayName' ) }
583614 </ h3 >
584- < div className = "flex gap-2" >
615+ < div className = "flex items-end gap-2" >
616+ < EmojiPickerButton
617+ value = { editIcon }
618+ onChange = { setEditIcon }
619+ disabled = { isSaving }
620+ testId = "client-icon-picker"
621+ />
585622 < input
586623 type = "text"
587624 value = { editAlias }
588625 onChange = { ( e ) => setEditAlias ( e . target . value ) }
589626 placeholder = { client . client_name }
590- className = "focus:ring-primary-500 focus:border-primary-500 flex-1 rounded-lg border border-[rgb(var(--border))] bg-[rgb(var(--background))] px-3 py-2 text-sm focus:outline-none focus:ring-2"
627+ className = "focus:ring-primary-500 focus:border-primary-500 h-10 flex-1 rounded-lg border border-[rgb(var(--border))] bg-[rgb(var(--background))] px-3 text-sm focus:outline-none focus:ring-2"
591628 />
592629 < Button
593630 size = "sm"
594631 variant = "primary"
595632 onClick = { onSaveAlias }
596- disabled = { ! aliasDirty || isSaving }
633+ disabled = { ( ! aliasDirty && ! iconDirty ) || isSaving }
597634 data-testid = "client-save-alias-btn"
598635 >
599636 { isSaving ? (
0 commit comments