Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import { ICommandService } from '../../../../../platform/commands/common/command
import { IAICustomizationListItem } from './aiCustomizationItemSource.js';
import { IAICustomizationItemsModel, ItemsModelSection } from './aiCustomizationItemsModel.js';
import { createCustomizationCardPrimaryAction, CustomizationCardListController } from './customizationCardList.js';
import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { ScrollbarVisibility } from '../../../../../base/common/scrollable.js';

export { truncateToFirstLine } from './aiCustomizationListWidgetUtils.js';

Expand Down Expand Up @@ -615,7 +617,8 @@ export class AICustomizationListWidget extends Disposable {
private listContainer!: HTMLElement;
private list!: WorkbenchList<IListEntry>;
private cardContainer!: HTMLElement;
private cardScrollElement: HTMLElement | undefined;
private cardScrollable!: DomScrollableElement;
private cardScrollableNode!: HTMLElement;
private firstCardFocusElement: HTMLElement | undefined;
private readonly cardRowsByUri = new Map<string, HTMLElement>();
private readonly cardRowsById = new Map<string, HTMLElement>();
Expand Down Expand Up @@ -781,7 +784,23 @@ export class AICustomizationListWidget extends Disposable {
this._register(this.addButton.onDidClick(() => this.executePrimaryCreateAction()));

this.cardContainer = DOM.append(this.element, $('.plugin-card-container.customization-card-container'));
this.cardContainer.style.display = 'none';
this.cardScrollable = this._register(new DomScrollableElement(this.cardContainer, {
horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.Auto,
useShadows: false,
}));
Comment thread
houghj16 marked this conversation as resolved.
this._register(DOM.addDisposableListener(this.cardContainer, DOM.EventType.SCROLL, () => {
this.cardScrollable.setScrollPosition({ scrollTop: this.cardContainer.scrollTop });
}));
this.cardScrollableNode = this.cardScrollable.getDomNode();
this.cardScrollableNode.classList.add('plugin-card-scrollable');
this.cardScrollableNode.style.display = 'none';
this.element.appendChild(this.cardScrollableNode);
const cardResizeObserver = this._register(new DOM.DisposableResizeObserver(
'AICustomizationListWidget.cardScrollable',
() => this.cardScrollable.scanDomNode(),
));
this._register(cardResizeObserver.observe(this.cardScrollableNode));

// List container
this.listContainer = DOM.append(this.element, $('.list-container'));
Expand Down Expand Up @@ -1572,10 +1591,9 @@ export class AICustomizationListWidget extends Disposable {
this.cardRowsByUri.clear();
this.cardRowsById.clear();
this.cardMenuButtonsById.clear();
this.cardScrollElement = undefined;
this.firstCardFocusElement = undefined;
DOM.clearNode(this.cardContainer);
this.cardContainer.style.display = 'none';
this.cardScrollableNode.style.display = 'none';
this.updateEmptyState();
return;
}
Expand All @@ -1594,8 +1612,8 @@ export class AICustomizationListWidget extends Disposable {
DOM.clearNode(this.cardContainer);
this.listContainer.style.display = 'none';
this.emptyStateContainer.style.display = 'none';
this.cardContainer.style.display = '';
const content = this.cardScrollElement = DOM.append(this.cardContainer, $('.plugin-card-scroll.customization-card-scroll'));
this.cardScrollableNode.style.display = '';
const content = DOM.append(this.cardContainer, $('.plugin-card-scroll.plugin-card-scroll-content.customization-card-scroll'));

for (const group of visibleGroups) {
const section = DOM.append(content, $('.plugin-card-section.customization-card-section'));
Expand Down Expand Up @@ -1628,6 +1646,7 @@ export class AICustomizationListWidget extends Disposable {
}
cardList.finalize();
}
this.cardScrollable.scanDomNode();
if (shouldRestoreFocus) {
DOM.getWindow(this.element).requestAnimationFrame(() => {
(this.cardMenuButtonsById.get(focusItemId ?? '') ?? this.cardRowsById.get(focusItemId ?? '') ?? this.firstCardFocusElement)?.focus();
Expand Down Expand Up @@ -1884,7 +1903,7 @@ export class AICustomizationListWidget extends Disposable {
private updateEmptyState(): void {
const hasItems = this.displayEntries.length > 0;
if (!hasItems) {
this.cardContainer.style.display = 'none';
this.cardScrollableNode.style.display = 'none';
this.emptyStateContainer.style.display = 'flex';
this.listContainer.style.display = 'none';

Expand All @@ -1901,7 +1920,7 @@ export class AICustomizationListWidget extends Disposable {
} else {
this.emptyStateContainer.style.display = 'none';
this.listContainer.style.display = this.usesCardLayout() ? 'none' : '';
this.cardContainer.style.display = this.usesCardLayout() ? '' : 'none';
this.cardScrollableNode.style.display = this.usesCardLayout() ? '' : 'none';
}
}

Expand Down Expand Up @@ -1976,9 +1995,7 @@ export class AICustomizationListWidget extends Disposable {
*/
revealLastItem(): void {
if (this.usesCardLayout()) {
if (this.cardScrollElement) {
this.cardScrollElement.scrollTop = this.cardScrollElement.scrollHeight;
}
this.cardScrollable.setScrollPosition({ scrollTop: this.cardContainer.scrollHeight });
return;
}
if (this.displayEntries.length > 0) {
Expand Down Expand Up @@ -2049,9 +2066,11 @@ export class AICustomizationListWidget extends Disposable {
const availableHeight = this.element.clientHeight || height;
const listHeight = Math.max(0, availableHeight - searchBarHeight - headerHeight);

this.cardContainer.style.height = `${listHeight}px`;
this.cardScrollableNode.style.height = `${listHeight}px`;
this.listContainer.style.height = `${listHeight}px`;
if (!this.usesCardLayout()) {
if (this.usesCardLayout()) {
this.cardScrollable.scanDomNode();
} else {
this.list.layout(listHeight, width);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import { Range } from '../../../../../editor/common/core/range.js';
import { IMcpServerConfiguration, McpServerType } from '../../../../../platform/mcp/common/mcpPlatformTypes.js';
import { createWorkbenchMcpServerDetailInput, IMcpServerDetailInput } from './embeddedMcpServerDetail.js';
import { createCustomizationCardPrimaryAction, CustomizationCardListController } from './customizationCardList.js';
import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { ScrollbarVisibility } from '../../../../../base/common/scrollable.js';

const $ = DOM.$;

Expand Down Expand Up @@ -1060,6 +1062,8 @@ export class McpListWidget extends Disposable {
private searchAndButtonContainer!: HTMLElement;
private searchInput!: InputBox;
private cardContainer!: HTMLElement;
private cardScrollable!: DomScrollableElement;
private cardScrollableNode!: HTMLElement;
private emptyContainer!: HTMLElement;
private emptyText!: HTMLElement;
private emptySubtext!: HTMLElement;
Expand All @@ -1082,7 +1086,6 @@ export class McpListWidget extends Disposable {
private visible = false;
private mcpAccessEnabled = false;
private firstCardFocusElement: HTMLElement | undefined;
private cardScrollElement: HTMLElement | undefined;
private availableSection: HTMLElement | undefined;
private narrowLayout = false;
private wideLayout = false;
Expand Down Expand Up @@ -1234,8 +1237,24 @@ export class McpListWidget extends Disposable {
disabledText.textContent = localize('mcpAccessDisabledTitle', "MCP servers are disabled");
this.disabledMessage = DOM.append(this.disabledContainer, $('.empty-subtext'));

this.cardContainer = DOM.append(this.element, $('.plugin-card-container'));
this.cardContainer.style.display = 'none';
this.cardContainer = $('.plugin-card-container');
this.cardScrollable = this._register(new DomScrollableElement(this.cardContainer, {
horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.Auto,
useShadows: false,
}));
this._register(DOM.addDisposableListener(this.cardContainer, DOM.EventType.SCROLL, () => {
this.cardScrollable.setScrollPosition({ scrollTop: this.cardContainer.scrollTop });
}));
this.cardScrollableNode = this.cardScrollable.getDomNode();
this.cardScrollableNode.classList.add('plugin-card-scrollable');
this.cardScrollableNode.style.display = 'none';
this.element.appendChild(this.cardScrollableNode);
const cardResizeObserver = this._register(new DOM.DisposableResizeObserver(
'McpListWidget.cardScrollable',
() => this.cardScrollable.scanDomNode(),
));
this._register(cardResizeObserver.observe(this.cardScrollableNode));

// Listen to MCP service changes
this._register(this.mcpWorkbenchService.onChange(() => {
Expand Down Expand Up @@ -1403,16 +1422,27 @@ export class McpListWidget extends Disposable {

private showCardSurface(): void {
this.emptyContainer.style.display = 'none';
this.cardContainer.style.display = '';
this.cardScrollableNode.style.display = '';
}

private showEmptySurface(message: string, detail: string): void {
this.cardContainer.style.display = 'none';
this.cardScrollableNode.style.display = 'none';
this.emptyContainer.style.display = 'flex';
this.emptyText.textContent = message;
this.emptySubtext.textContent = detail;
}

private createCardScrollContent(...classNames: string[]): HTMLElement {
const content = DOM.append(this.cardContainer, $('.plugin-card-scroll.plugin-card-scroll-content'));
content.classList.add(...classNames);
const resizeObserver = this.cardDisposables.add(new DOM.DisposableResizeObserver(
'McpListWidget.cardScrollContent',
() => this.cardScrollable.scanDomNode(),
));
this.cardDisposables.add(resizeObserver.observe(content));
return content;
}

private addSurfaceActivation(surface: HTMLElement, label: string, callback: () => void, ...classNames: string[]): HTMLButtonElement {
const primaryAction = createCustomizationCardPrimaryAction(surface, label, ...classNames);
this.firstCardFocusElement ??= primaryAction;
Expand Down Expand Up @@ -1454,7 +1484,7 @@ export class McpListWidget extends Disposable {
DOM.clearNode(this.cardContainer);
this.showCardSurface();

const content = this.cardScrollElement = DOM.append(this.cardContainer, $('.plugin-card-scroll'));
const content = this.createCardScrollContent();
this.renderFeaturedServers(content);

const installedList = this.renderCardSection(
Expand Down Expand Up @@ -1821,7 +1851,7 @@ export class McpListWidget extends Disposable {
this.availableSection = undefined;
DOM.clearNode(this.cardContainer);
this.showCardSurface();
const content = this.cardScrollElement = DOM.append(this.cardContainer, $('.plugin-card-scroll.plugin-search-results'));
const content = this.createCardScrollContent('plugin-search-results');
if (this.installedEntries.length > 0) {
const installedList = this.renderCardSection(content, localize('installedSearchHeader', "Installed"), undefined, 'installed-mcp-servers-section', this.installedEntries.length);
installedList.classList.add('plugin-inventory-list');
Expand Down Expand Up @@ -1986,7 +2016,8 @@ export class McpListWidget extends Disposable {
this.lastHeaderHeight = headerHeight;
const listHeight = Math.max(0, availableHeight - searchBarHeight - headerHeight);

this.cardContainer.style.height = `${listHeight}px`;
this.cardScrollableNode.style.height = `${listHeight}px`;
this.cardScrollable.scanDomNode();
}

/**
Expand All @@ -2000,16 +2031,14 @@ export class McpListWidget extends Disposable {
* Scrolls the list so the last item is visible.
*/
revealLastItem(): void {
if (this.cardScrollElement) {
this.cardScrollElement.scrollTop = this.cardScrollElement.scrollHeight;
}
this.cardScrollable.setScrollPosition({ scrollTop: this.cardContainer.scrollHeight });
}

/**
* Focuses the list.
*/
focus(): void {
if (this.cardContainer.style.display !== 'none') {
if (this.cardScrollableNode.style.display !== 'none') {
this.firstCardFocusElement?.focus();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@
.ai-customization-list-widget .list-container {
flex: 1;
min-height: 0;
overflow: auto;
overflow: hidden;
}

.ai-customization-list-widget .list-empty-message {
Expand Down Expand Up @@ -1336,6 +1336,9 @@ per-word capitalization does not survive translation. */
position: relative;
flex: 1;
min-height: 0;
width: 100%;
max-width: calc(840px + var(--vscode-spacing-size160));
margin: 0 auto;
}

/* The scrolled element: bound it to the wrapper so clientHeight < scrollHeight when content overflows. */
Expand All @@ -1346,7 +1349,7 @@ per-word capitalization does not survive translation. */
display: flex;
flex-direction: column;
gap: var(--vscode-spacing-size240);
padding: 0 max(var(--vscode-spacing-size20), calc((100% - 840px) / 2)) var(--vscode-spacing-size200);
padding: 0 var(--vscode-spacing-size160) var(--vscode-spacing-size200) 0;
box-sizing: border-box;
}

Expand Down Expand Up @@ -2752,13 +2755,33 @@ per-word capitalization does not survive translation. */
overflow: hidden;
}

.plugin-list-widget .plugin-card-scrollable {
position: relative;
flex: 1;
min-height: 0;
width: 100%;
max-width: calc(840px + var(--vscode-spacing-size160));
margin: 0 auto;
}

.plugin-list-widget .plugin-card-scrollable > .plugin-card-container {
position: absolute;
inset: 0;
}

.plugin-list-widget .plugin-card-scroll {
height: 100%;
overflow: auto;
padding: 0 max(var(--vscode-spacing-size20), calc((100% - 840px) / 2)) var(--vscode-spacing-size200);
box-sizing: border-box;
}

.plugin-list-widget .plugin-card-scroll-content {
height: auto;
overflow: visible;
padding: 0 var(--vscode-spacing-size160) var(--vscode-spacing-size200) 0;
}

.plugin-list-widget .plugin-marketplace-back-container {
flex-shrink: 0;
display: flex;
Expand Down
Loading
Loading