Skip to content

Commit 0b52f0f

Browse files
committed
clients: allow custom emoji icon override for connections
Adds a user-editable client_icon field so any connection's Connections-page icon can be changed via the shared emoji picker, falling back to the existing logo/known-client-name resolution when unset. Threaded through storage (new migration + repo method), the Tauri command, the GatewayWrites trait (desktop + web-admin HTTP bridge), and the ClientsPage side panel UI. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 8894960 commit 0b52f0f

11 files changed

Lines changed: 106 additions & 12 deletions

File tree

apps/desktop/src-tauri/src/commands/oauth.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ pub async fn get_oauth_clients(
468468
registration_type: client.registration_type.as_str().to_string(),
469469
client_name: client.client_name,
470470
client_alias: client.client_alias,
471+
client_icon: client.client_icon,
471472
redirect_uris: client.redirect_uris,
472473
scope: client.scope,
473474
approved: client.approved,
@@ -526,6 +527,7 @@ pub struct OAuthClientInfo {
526527
pub registration_type: String,
527528
pub client_name: String,
528529
pub client_alias: Option<String>,
530+
pub client_icon: Option<String>,
529531
pub redirect_uris: Vec<String>,
530532
pub scope: Option<String>,
531533

@@ -570,11 +572,13 @@ pub struct OAuthClientInfo {
570572

571573
/// Request to update client settings.
572574
///
573-
/// Only the alias is user-editable now — connection mode / space pin no
574-
/// longer exist.
575+
/// Only the alias and icon are user-editable now — connection mode /
576+
/// space pin no longer exist.
575577
#[derive(Debug, Serialize, Deserialize)]
576578
pub struct UpdateClientSettingsRequest {
577579
pub client_alias: Option<String>,
580+
#[serde(default)]
581+
pub client_icon: Option<String>,
578582
}
579583

580584
/// Update an OAuth client's settings (direct service access)
@@ -599,6 +603,9 @@ pub async fn update_oauth_client(
599603
repo.update_client_alias(&client_id, settings.client_alias)
600604
.await
601605
.map_err(|e| format!("Failed to update client: {}", e))?;
606+
repo.update_client_icon(&client_id, settings.client_icon)
607+
.await
608+
.map_err(|e| format!("Failed to update client: {}", e))?;
602609

603610
info!("[OAuth] Updated client: {}", client_id);
604611

@@ -617,6 +624,7 @@ pub async fn update_oauth_client(
617624
registration_type: updated_client.registration_type.as_str().to_string(),
618625
client_name: updated_client.client_name,
619626
client_alias: updated_client.client_alias,
627+
client_icon: updated_client.client_icon,
620628
redirect_uris: updated_client.redirect_uris,
621629
scope: updated_client.scope,
622630
approved: updated_client.approved,
@@ -759,6 +767,7 @@ pub async fn register_api_key_client(
759767
reports_roots: false,
760768
roots_capability_known: false,
761769
machine_id: None,
770+
client_icon: None,
762771
};
763772
repo.save_client(&client)
764773
.await

apps/desktop/src-tauri/src/services/admin_write_runtime.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,11 +250,15 @@ impl GatewayWriteRuntime for DesktopGatewayWriteRuntime {
250250
&self,
251251
client_id: String,
252252
client_alias: Option<String>,
253+
client_icon: Option<String>,
253254
) -> anyhow::Result<Value> {
254255
let client = update_oauth_client(
255256
self.app_handle.state(),
256257
client_id,
257-
UpdateClientSettingsRequest { client_alias },
258+
UpdateClientSettingsRequest {
259+
client_alias,
260+
client_icon,
261+
},
258262
)
259263
.await
260264
.map_err(|e| anyhow::anyhow!(e))?;

apps/desktop/src/features/clients/ClientsPage.tsx

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidStudioIcon from '@/assets/client-icons/android-studio.svg';
1212
import opencodeIcon from '@/assets/client-icons/opencode.svg';
1313
import opencodeIconDark from '@/assets/client-icons/opencode-dark.svg';
1414
import { ClientBrandIcon } from '@/components/ClientBrandIcon';
15+
import { EmojiPickerButton } from '@/components/emoji-picker-button.component';
1516
import { resolveKnownClientKey } from '@/lib/clientIcons';
1617
import {
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 ? (

apps/desktop/src/lib/api/gateway.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ export interface OAuthClient {
165165
registration_type: RegistrationType;
166166
client_name: string;
167167
client_alias: string | null;
168+
/** User-set emoji override for the Connections-page icon; `null` falls back to logo_uri / known-client resolution. */
169+
client_icon: string | null;
168170
redirect_uris: string[];
169171
scope: string | null;
170172

@@ -204,10 +206,11 @@ export interface OAuthClient {
204206
}
205207

206208
/**
207-
* Update client settings request. Only the display alias is editable.
209+
* Update client settings request. Only the display alias and icon are editable.
208210
*/
209211
export interface UpdateClientRequest {
210212
client_alias?: string;
213+
client_icon?: string;
211214
}
212215

213216
/**

crates/mcpmux-gateway/src/admin/command_bridge/write.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ pub struct MetaToolRevokeBody {
229229
#[derive(Debug, Deserialize)]
230230
pub struct OAuthClientUpdateBody {
231231
pub client_alias: Option<String>,
232+
#[serde(default)]
233+
pub client_icon: Option<String>,
232234
}
233235

234236
#[derive(Debug, Deserialize)]
@@ -1180,7 +1182,7 @@ pub async fn update_oauth_client(
11801182
body: OAuthClientUpdateBody,
11811183
) -> Result<Value> {
11821184
ctx.gateway_writes
1183-
.update_oauth_client(client_id, body.client_alias)
1185+
.update_oauth_client(client_id, body.client_alias, body.client_icon)
11841186
.await
11851187
}
11861188

crates/mcpmux-gateway/src/admin/write_runtime.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub trait GatewayWriteRuntime: Send + Sync {
6161
&self,
6262
client_id: String,
6363
client_alias: Option<String>,
64+
client_icon: Option<String>,
6465
) -> Result<Value>;
6566
async fn delete_oauth_client(&self, client_id: String) -> Result<Value>;
6667
async fn grant_oauth_client_feature_set(
@@ -279,6 +280,7 @@ impl GatewayWriteRuntime for LiveGatewayWriteRuntime {
279280
&self,
280281
_client_id: String,
281282
_client_alias: Option<String>,
283+
_client_icon: Option<String>,
282284
) -> Result<Value> {
283285
Err(gateway_write_unavailable())
284286
}
@@ -436,6 +438,7 @@ impl GatewayWriteRuntime for StubGatewayWriteRuntime {
436438
&self,
437439
_client_id: String,
438440
_client_alias: Option<String>,
441+
_client_icon: Option<String>,
439442
) -> Result<Value> {
440443
Err(gateway_not_running())
441444
}

crates/mcpmux-gateway/src/oauth/dcr.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ fn build_inbound_client_from_request(
141141
reports_roots: false,
142142
roots_capability_known: false,
143143
machine_id: None,
144+
client_icon: None,
144145
}
145146
}
146147

crates/mcpmux-gateway/src/services/client_metadata_service.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ impl ClientMetadataService {
143143
reports_roots: false,
144144
roots_capability_known: false,
145145
machine_id: None,
146+
client_icon: None,
146147
}
147148
}
148149
}

crates/mcpmux-storage/src/database.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@ const MIGRATIONS: &[Migration] = &[
223223
name: "inbound_client_locked_space",
224224
sql: include_str!("migrations/038_inbound_client_locked_space.sql"),
225225
},
226+
Migration {
227+
version: 39,
228+
name: "inbound_client_icon",
229+
sql: include_str!("migrations/039_inbound_client_icon.sql"),
230+
},
226231
];
227232

228233
/// SQLite database wrapper.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- User-editable emoji override for a client's Connections-page icon.
2+
-- NULL means "fall back to logo_uri / known-client-name resolution".
3+
4+
ALTER TABLE inbound_clients ADD COLUMN client_icon TEXT;

0 commit comments

Comments
 (0)