Skip to content

Commit b3f7ce2

Browse files
authored
fix mcp server icon in extensions view (microsoft#250727)
1 parent 7d8b338 commit b3f7ce2

File tree

2 files changed

+72
-26
lines changed

2 files changed

+72
-26
lines changed

src/vs/workbench/contrib/mcp/browser/mcpServerWidgets.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js';
1818
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
1919
import { verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js';
2020
import { installCountIcon, starEmptyIcon, starFullIcon, starHalfIcon } from '../../extensions/browser/extensionsIcons.js';
21-
import { IMcpServerContainer, IWorkbenchMcpServer } from '../common/mcpTypes.js';
21+
import { IMcpServerContainer, IWorkbenchMcpServer, mcpServerIcon } from '../common/mcpTypes.js';
2222

2323
export abstract class McpServerWidget extends Disposable implements IMcpServerContainer {
2424
private _mcpServer: IWorkbenchMcpServer | null = null;
@@ -42,6 +42,71 @@ export function onClick(element: HTMLElement, callback: () => void): IDisposable
4242
return disposables;
4343
}
4444

45+
export class McpServerIconWidget extends McpServerWidget {
46+
47+
private readonly disposables = this._register(new DisposableStore());
48+
private readonly element: HTMLElement;
49+
private readonly iconElement: HTMLImageElement;
50+
private readonly defaultIconElement: HTMLElement;
51+
52+
private iconUrl: string | undefined;
53+
54+
constructor(
55+
container: HTMLElement,
56+
) {
57+
super();
58+
this.element = dom.append(container, dom.$('.extension-icon'));
59+
60+
this.iconElement = dom.append(this.element, dom.$('img.icon', { alt: '' }));
61+
this.iconElement.style.display = 'none';
62+
63+
this.defaultIconElement = dom.append(this.element, dom.$(ThemeIcon.asCSSSelector(mcpServerIcon)));
64+
this.defaultIconElement.style.display = 'none';
65+
66+
this.render();
67+
this._register(toDisposable(() => this.clear()));
68+
}
69+
70+
private clear(): void {
71+
this.iconUrl = undefined;
72+
this.iconElement.src = '';
73+
this.iconElement.style.display = 'none';
74+
this.defaultIconElement.style.display = 'none';
75+
this.disposables.clear();
76+
}
77+
78+
render(): void {
79+
if (!this.mcpServer) {
80+
this.clear();
81+
return;
82+
}
83+
84+
if (this.mcpServer.iconUrl) {
85+
this.iconElement.style.display = 'inherit';
86+
this.defaultIconElement.style.display = 'none';
87+
if (this.iconUrl !== this.mcpServer.iconUrl) {
88+
this.iconUrl = this.mcpServer.iconUrl;
89+
this.disposables.add(dom.addDisposableListener(this.iconElement, 'error', () => {
90+
this.iconElement.style.display = 'none';
91+
this.defaultIconElement.style.display = 'inherit';
92+
}, { once: true }));
93+
this.iconElement.src = this.iconUrl;
94+
if (!this.iconElement.complete) {
95+
this.iconElement.style.visibility = 'hidden';
96+
this.iconElement.onload = () => this.iconElement.style.visibility = 'inherit';
97+
} else {
98+
this.iconElement.style.visibility = 'inherit';
99+
}
100+
}
101+
} else {
102+
this.iconUrl = undefined;
103+
this.iconElement.style.display = 'none';
104+
this.iconElement.src = '';
105+
this.defaultIconElement.style.display = 'inherit';
106+
}
107+
}
108+
}
109+
45110
export class PublisherWidget extends McpServerWidget {
46111

47112
private element: HTMLElement | undefined;

src/vs/workbench/contrib/mcp/browser/mcpServersView.ts

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,11 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js
2323
import { getLocationBasedViewColors, ViewPane } from '../../../browser/parts/views/viewPane.js';
2424
import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';
2525
import { IViewDescriptorService } from '../../../common/views.js';
26-
import { IMcpWorkbenchService, IWorkbenchMcpServer, McpServerContainers, mcpServerIcon } from '../common/mcpTypes.js';
26+
import { IMcpWorkbenchService, IWorkbenchMcpServer, McpServerContainers } from '../common/mcpTypes.js';
2727
import { DropDownAction, InstallAction, ManageMcpServerAction } from './mcpServerActions.js';
28-
import { PublisherWidget, InstallCountWidget, RatingsWidget } from './mcpServerWidgets.js';
28+
import { PublisherWidget, InstallCountWidget, RatingsWidget, McpServerIconWidget } from './mcpServerWidgets.js';
2929
import { ActionRunner, IAction, Separator } from '../../../../base/common/actions.js';
3030
import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
31-
import { ThemeIcon } from '../../../../base/common/themables.js';
3231

3332
export class McpServersListView extends ViewPane {
3433

@@ -136,7 +135,6 @@ export class McpServersListView extends ViewPane {
136135
interface IMcpServerTemplateData {
137136
root: HTMLElement;
138137
element: HTMLElement;
139-
icon: HTMLElement;
140138
name: HTMLElement;
141139
description: HTMLElement;
142140
installCount: HTMLElement;
@@ -159,7 +157,8 @@ class McpServerRenderer implements IListRenderer<IWorkbenchMcpServer, IMcpServer
159157

160158
renderTemplate(root: HTMLElement): IMcpServerTemplateData {
161159
const element = dom.append(root, dom.$('.mcp-server-item.extension-list-item'));
162-
const icon = dom.append(element, dom.$('.icon-container'));
160+
const iconContainer = dom.append(element, dom.$('.icon-container'));
161+
const iconWidget = this.instantiationService.createInstance(McpServerIconWidget, iconContainer);
163162
const details = dom.append(element, dom.$('.details'));
164163
const headerContainer = dom.append(details, dom.$('.header-container'));
165164
const header = dom.append(headerContainer, dom.$('.header'));
@@ -188,6 +187,7 @@ class McpServerRenderer implements IListRenderer<IWorkbenchMcpServer, IMcpServer
188187
];
189188

190189
const widgets = [
190+
iconWidget,
191191
publisherWidget,
192192
this.instantiationService.createInstance(InstallCountWidget, installCount, true),
193193
this.instantiationService.createInstance(RatingsWidget, ratings, true),
@@ -198,7 +198,7 @@ class McpServerRenderer implements IListRenderer<IWorkbenchMcpServer, IMcpServer
198198
const disposable = combinedDisposable(...actions, ...widgets, actionbar, actionBarListener, extensionContainers);
199199

200200
return {
201-
root, element, icon, name, description, installCount, ratings, disposables: [disposable], actionbar,
201+
root, element, name, description, installCount, ratings, disposables: [disposable], actionbar,
202202
mcpServerDisposables: [],
203203
set mcpServer(mcpServer: IWorkbenchMcpServer) {
204204
extensionContainers.mcpServer = mcpServer;
@@ -210,25 +210,6 @@ class McpServerRenderer implements IListRenderer<IWorkbenchMcpServer, IMcpServer
210210
data.element.classList.remove('loading');
211211
data.mcpServerDisposables = dispose(data.mcpServerDisposables);
212212
data.root.setAttribute('data-mcp-server-id', mcpServer.id);
213-
214-
dom.clearNode(data.icon);
215-
if (mcpServer.iconUrl) {
216-
const icon = dom.append(data.icon, dom.$<HTMLImageElement>('img.icon', { alt: '' }));
217-
data.mcpServerDisposables.push(dom.addDisposableListener(icon, 'error', () => {
218-
dom.clearNode(data.icon);
219-
dom.append(data.icon, dom.$(ThemeIcon.asCSSSelector(mcpServerIcon)));
220-
}, { once: true }));
221-
icon.src = mcpServer.iconUrl;
222-
if (!icon.complete) {
223-
data.icon.style.visibility = 'hidden';
224-
icon.onload = () => icon.style.visibility = 'inherit';
225-
} else {
226-
icon.style.visibility = 'inherit';
227-
}
228-
} else {
229-
dom.append(data.icon, dom.$(ThemeIcon.asCSSSelector(mcpServerIcon)));
230-
}
231-
232213
data.name.textContent = mcpServer.label;
233214
data.description.textContent = mcpServer.description;
234215

0 commit comments

Comments
 (0)