microsoft/vscode · #336340

sessions: keep Codicon confetti behind conversations

TylerLeonhardt · merged Sep 15, 20263 files · 213 + / 117
src/vs/sessions/contrib/chat/test/browser/chatView.test.ts127 + / 46
@@ -620,6 +620,7 @@ suite('Sessions - Chat View', () => {  	test('makes one random codicon an accessible confetti button', () => { 		const workbench = dom.$('.monaco-workbench.agent-sessions-workbench');+		workbench.style.setProperty('--vscode-spacing-size240', '24px'); 		const part = dom.append(workbench, dom.$('.part.sessionspart')); 		dom.getWindow(workbench).document.body.appendChild(workbench); 		disposables.add(toDisposable(() => workbench.remove()));@@ -639,12 +640,18 @@ suite('Sessions - Chat View', () => { 			top: cell.style.top, 			iconTransform: cell.querySelector<HTMLElement>('.codicon')?.style.transform, 		}));-		const initialButton = layer?.querySelector<HTMLElement>('.sessions-chat-codicon-button');+		const initialButton = part.querySelector<HTMLElement>(':scope > .sessions-chat-codicon-hit-target');+		const initialCell = cellsBefore.find(cell => cell.style.left === initialButton?.style.left && cell.style.top === initialButton?.style.top); 		const buttonStyle = initialButton ? dom.getWindow(initialButton).getComputedStyle(initialButton) : undefined;+		const backgroundStyle = backgroundLayer ? dom.getWindow(backgroundLayer).getComputedStyle(backgroundLayer) : undefined;+		const initialButtonPosition = `${initialButton?.style.left}:${initialButton?.style.top}`; 		initialButton?.focus(); 		initialButton?.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Enter', keyCode: 13 })); 		const cellsAfter = layer ? [...layer.querySelectorAll<HTMLElement>('.sessions-chat-codicon-cell')] : [];-		const nextButton = layer?.querySelector<HTMLElement>('.sessions-chat-codicon-button');+		const nextButton = part.querySelector<HTMLElement>(':scope > .sessions-chat-codicon-hit-target');+		const activeCells = cellsAfter.filter(cell => cell.classList.contains('sessions-chat-codicon-button-active'));+		const focusedButtonStyle = nextButton ? dom.getWindow(nextButton).getComputedStyle(nextButton) : undefined;+		const activationParent = activationTarget?.parentElement; 		const layoutAfter = cellsAfter.map(cell => ({ 			left: cell.style.left, 			top: cell.style.top,@@ -654,52 +661,61 @@ suite('Sessions - Chat View', () => { 		assert.deepStrictEqual({ 			backgroundAriaHidden: backgroundLayer?.ariaHidden, 			layerAriaHidden: layer?.ariaHidden,-			buttonCount: layer?.querySelectorAll('.sessions-chat-codicon-button').length,+			buttonCount: part.querySelectorAll(':scope > .sessions-chat-codicon-hit-target').length,+			buttonIsOutsideBackground: !backgroundLayer?.contains(nextButton ?? null),+			buttonPrecedesBackground: initialButton?.nextElementSibling === backgroundLayer,+			buttonChildCount: nextButton?.childElementCount, 			role: nextButton?.getAttribute('role'), 			tabIndex: nextButton?.tabIndex, 			ariaLabel: nextButton?.getAttribute('aria-label'),-			iconAriaHidden: nextButton?.querySelector<HTMLElement>('.codicon')?.ariaHidden,-			display: buttonStyle?.display,-			placeItems: buttonStyle?.placeItems,+			buttonOpacity: buttonStyle?.opacity,+			buttonZIndex: buttonStyle?.zIndex,+			backgroundZIndex: backgroundStyle?.zIndex,+			visualCellSize: { width: initialCell?.offsetWidth, height: initialCell?.offsetHeight },+			activeCellCount: activeCells.length,+			activeCellMatchesButton: activeCells[0]?.style.left === nextButton?.style.left && activeCells[0]?.style.top === nextButton?.style.top,+			iconsAriaHidden: cellsAfter.every(cell => cell.ariaHidden === 'true' && cell.querySelector<HTMLElement>('.codicon')?.ariaHidden === 'true'), 			activationTarget: activationTarget?.className,-			activationTargetRetained: activationTarget?.parentElement === initialButton,+			activationTargetRetained: activationParent ? cellsBefore.includes(activationParent) : false, 			activations,-			targetChanged: nextButton !== initialButton,+			buttonReused: nextButton === initialButton,+			targetPositionChanged: `${nextButton?.style.left}:${nextButton?.style.top}` !== initialButtonPosition, 			focusTransferred: dom.getWindow(part).document.activeElement === nextButton,+			focusedButtonOpacity: focusedButtonStyle?.opacity, 			cellElementsRetained: cellsAfter.length === cellsBefore.length && cellsAfter.every((cell, index) => cell === cellsBefore[index]), 			layoutStable: layoutAfter,-			initialButtonAfterActivation: {-				ariaHidden: initialButton?.ariaHidden,-				role: initialButton?.getAttribute('role'),-				tabIndex: initialButton?.tabIndex,-			}, 		}, {-			backgroundAriaHidden: null,-			layerAriaHidden: null,+			backgroundAriaHidden: 'true',+			layerAriaHidden: 'true', 			buttonCount: 1,+			buttonIsOutsideBackground: true,+			buttonPrecedesBackground: true,+			buttonChildCount: 0, 			role: 'button', 			tabIndex: 0, 			ariaLabel: 'Celebrate',-			iconAriaHidden: 'true',-			display: 'grid',-			placeItems: 'center',+			buttonOpacity: '0',+			buttonZIndex: '3',+			backgroundZIndex: '0',+			visualCellSize: { width: 24, height: 24 },+			activeCellCount: 1,+			activeCellMatchesButton: true,+			iconsAriaHidden: true, 			activationTarget: 'sessions-chat-codicon-button-animation', 			activationTargetRetained: true, 			activations: 1,-			targetChanged: true,+			buttonReused: true,+			targetPositionChanged: true, 			focusTransferred: true,+			focusedButtonOpacity: '0', 			cellElementsRetained: true, 			layoutStable: layoutBefore,-			initialButtonAfterActivation: {-				ariaHidden: 'true',-				role: null,-				tabIndex: -1,-			}, 		}); 	}); -	test('keeps the confetti button fully inside the viewport', async () => {+	test('keeps the confetti button fully inside the viewport and clear of conversation content', async () => { 		const workbench = dom.$('.monaco-workbench.agent-sessions-workbench');+		workbench.style.setProperty('--vscode-spacing-size240', '24px'); 		const part = dom.append(workbench, dom.$('.part.sessionspart')); 		part.style.width = '850px'; 		part.style.height = '641px';@@ -723,28 +739,19 @@ suite('Sessions - Chat View', () => { 		const renderer = disposables.add(new SessionsChatBackgroundRenderer(part, true, () => 0.999)); 		renderer.setBackground({ kind: 'codicons' }); 		const layer = part.querySelector<HTMLElement>(':scope > .sessions-chat-background > .sessions-chat-codicon-background');-		const initialButton = layer?.querySelector<HTMLElement>('.sessions-chat-codicon-button');+		const initialButton = part.querySelector<HTMLElement>(':scope > .sessions-chat-codicon-hit-target');+		const initialButtonPosition = `${initialButton?.style.left}:${initialButton?.style.top}`; 		const initialViewport = { width: part.clientWidth, height: part.clientHeight }; 		const initialCells = layer ? [...layer.querySelectorAll<HTMLElement>('.sessions-chat-codicon-cell')] : [];-		const overlaps = (button: HTMLElement | null | undefined, left: number, width: number) => {+		const overlapsBounds = (button: HTMLElement | null | undefined, left: number, top: number, width: number, height: number) => { 			const centerX = Number.parseFloat(button?.style.left ?? ''); 			const centerY = Number.parseFloat(button?.style.top ?? ''); 			const horizontalRadius = (button?.offsetWidth ?? 0) / 2; 			const verticalRadius = (button?.offsetHeight ?? 0) / 2; 			return centerX - horizontalRadius < left + width && centerX + horizontalRadius > left-				&& centerY - verticalRadius < 641 && centerY + verticalRadius > 0;+				&& centerY - verticalRadius < top + height && centerY + verticalRadius > top; 		};-		const initialButtonOverlapsOccluder = overlaps(initialButton, occluderLeft, occluderWidth);-		const hasOccludedCells = initialCells.some(cell => overlaps(cell, occluderLeft, occluderWidth));--		occluderLeft = 0;-		occluderWidth = 400;-		occluder.style.left = '0';-		occluder.style.width = '400px';-		await timeout(20);-		const refreshedButton = layer?.querySelector<HTMLElement>('.sessions-chat-codicon-button');-		refreshedButton?.click();-		const nextButton = layer?.querySelector<HTMLElement>('.sessions-chat-codicon-button');+		const overlaps = (button: HTMLElement | null | undefined, left: number, width: number) => overlapsBounds(button, left, 0, width, 641); 		const isFullyVisible = (button: HTMLElement | null | undefined, viewport: { width: number; height: number }) => { 			const centerX = Number.parseFloat(button?.style.left ?? ''); 			const centerY = Number.parseFloat(button?.style.top ?? '');@@ -755,31 +762,105 @@ suite('Sessions - Chat View', () => { 				&& centerY - verticalRadius >= 0 				&& centerY + verticalRadius <= viewport.height; 		};+		const initialButtonFullyVisible = isFullyVisible(initialButton, initialViewport);+		const initialButtonOverlapsOccluder = overlaps(initialButton, occluderLeft, occluderWidth);+		const hasOccludedCells = initialCells.some(cell => overlaps(cell, occluderLeft, occluderWidth));++		occluderLeft = 0;+		occluderWidth = 400;+		occluder.style.left = '0';+		occluder.style.width = '400px';+		await timeout(20);+		const refreshedButton = part.querySelector<HTMLElement>(':scope > .sessions-chat-codicon-hit-target');+		const refreshedButtonPosition = `${refreshedButton?.style.left}:${refreshedButton?.style.top}`;+		const refreshedButtonFullyVisible = isFullyVisible(refreshedButton, initialViewport);+		const refreshedButtonOverlapsOccluder = overlaps(refreshedButton, occluderLeft, occluderWidth);+		refreshedButton?.click();+		const nextButton = part.querySelector<HTMLElement>(':scope > .sessions-chat-codicon-hit-target');+		const nextButtonPosition = `${nextButton?.style.left}:${nextButton?.style.top}`;+		const nextButtonFullyVisible = isFullyVisible(nextButton, initialViewport);+		const nextButtonOverlapsOccluder = overlaps(nextButton, occluderLeft, occluderWidth);+		const dynamicOccluderRelocations = [];+		for (const selector of ['.interactive-item-container.interactive-response', '.scrollbar', '.monaco-sash']) {+			const currentButton = part.querySelector<HTMLElement>(':scope > .sessions-chat-codicon-hit-target');+			const currentButtonPosition = `${currentButton?.style.left}:${currentButton?.style.top}`;+			const left = Number.parseFloat(currentButton?.style.left ?? '') - 12;+			const top = Number.parseFloat(currentButton?.style.top ?? '') - 12;+			const dynamicOccluder = dom.append(part, dom.$(selector));+			dynamicOccluder.style.position = 'absolute';+			dynamicOccluder.style.left = `${left}px`;+			dynamicOccluder.style.top = `${top}px`;+			dynamicOccluder.style.width = '24px';+			dynamicOccluder.style.height = '24px';+			dynamicOccluder.getBoundingClientRect = () => DOMRect.fromRect({+				x: partBounds.left + left,+				y: partBounds.top + top,+				width: 24,+				height: 24,+			});+			await timeout(20);+			const relocatedButton = part.querySelector<HTMLElement>(':scope > .sessions-chat-codicon-hit-target');+			dynamicOccluderRelocations.push({+				className: dynamicOccluder.className,+				buttonReused: relocatedButton === currentButton,+				targetChanged: `${relocatedButton?.style.left}:${relocatedButton?.style.top}` !== currentButtonPosition,+				buttonFullyVisible: isFullyVisible(relocatedButton, initialViewport),+				buttonOverlapsOccluder: overlapsBounds(relocatedButton, left, top, 24, 24),+			});+		} 		const cells = layer ? [...layer.querySelectorAll<HTMLElement>('.sessions-chat-codicon-cell')] : []; 		const refreshedViewport = { width: part.clientWidth, height: part.clientHeight };  		assert.deepStrictEqual({ 			initialButtonSize: { width: initialButton?.offsetWidth, height: initialButton?.offsetHeight },-			initialButtonFullyVisible: isFullyVisible(initialButton, initialViewport),+			initialButtonFullyVisible, 			initialButtonOverlapsOccluder,-			targetChangedAfterOccluderMove: refreshedButton !== initialButton,-			refreshedButtonFullyVisible: isFullyVisible(refreshedButton, refreshedViewport),-			refreshedButtonOverlapsOccluder: overlaps(refreshedButton, occluderLeft, occluderWidth),-			targetChangedAfterActivation: nextButton !== refreshedButton,-			nextButtonFullyVisible: isFullyVisible(nextButton, refreshedViewport),-			nextButtonOverlapsOccluder: overlaps(nextButton, occluderLeft, occluderWidth),+			buttonReusedAfterOccluderMove: refreshedButton === initialButton,+			targetChangedAfterOccluderMove: refreshedButtonPosition !== initialButtonPosition,+			refreshedButtonFullyVisible,+			refreshedButtonOverlapsOccluder,+			buttonReusedAfterActivation: nextButton === refreshedButton,+			targetChangedAfterActivation: nextButtonPosition !== refreshedButtonPosition,+			nextButtonFullyVisible,+			nextButtonOverlapsOccluder,+			dynamicOccluderRelocations, 			hasClippedCells: cells.some(cell => !isFullyVisible(cell, refreshedViewport)), 			hasOccludedCells, 		}, { 			initialButtonSize: { width: 24, height: 24 }, 			initialButtonFullyVisible: true, 			initialButtonOverlapsOccluder: false,+			buttonReusedAfterOccluderMove: true, 			targetChangedAfterOccluderMove: true, 			refreshedButtonFullyVisible: true, 			refreshedButtonOverlapsOccluder: false,+			buttonReusedAfterActivation: true, 			targetChangedAfterActivation: true, 			nextButtonFullyVisible: true, 			nextButtonOverlapsOccluder: false,+			dynamicOccluderRelocations: [+				{+					className: 'interactive-item-container interactive-response',+					buttonReused: true,+					targetChanged: true,+					buttonFullyVisible: true,+					buttonOverlapsOccluder: false,+				},+				{+					className: 'scrollbar',+					buttonReused: true,+					targetChanged: true,+					buttonFullyVisible: true,+					buttonOverlapsOccluder: false,+				},+				{+					className: 'monaco-sash',+					buttonReused: true,+					targetChanged: true,+					buttonFullyVisible: true,+					buttonOverlapsOccluder: false,+				},+			], 			hasClippedCells: true, 			hasOccludedCells: true, 		});
src/vs/sessions/services/chatBackground/browser/chatBackgroundRenderer.ts75 + / 63
@@ -18,7 +18,14 @@ import { ISessionsChatBackground } from './chatBackgroundService.js'; const codiconCellSize = 80; const codiconButtonSize = 24; const codiconDefaults = { width: 960, height: 800 };-const codiconButtonOccluderClasses = ['new-chat-input-container', 'new-chat-bottom-container', 'interactive-input-part'];+const codiconButtonOccluderClasses = [+	'interactive-item-container',+	'monaco-sash',+	'new-chat-input-container',+	'new-chat-bottom-container',+	'interactive-input-part',+	'scrollbar',+]; const codiconButtonOccluderTags = new Set(['A', 'BUTTON', 'INPUT', 'SELECT', 'SUMMARY', 'TEXTAREA']); const codiconButtonOccluderRoles = new Set(['button', 'checkbox', 'combobox', 'link', 'menuitem', 'option', 'radio', 'slider', 'spinbutton', 'switch', 'tab', 'textbox', 'treeitem']); const codiconChoices = [@@ -103,7 +110,9 @@ function isCodiconButtonOccluder(element: HTMLElement): boolean {  function* getCodiconButtonOccluders(element: HTMLElement): Iterable<HTMLElement> { 	for (const child of element.children) {-		if (!isHTMLElement(child) || child.classList.contains('sessions-chat-background')) {+		if (!isHTMLElement(child)+			|| child.classList.contains('sessions-chat-background')+			|| child.classList.contains('sessions-chat-codicon-hit-target')) { 			continue; 		} 		if (isCodiconButtonOccluder(child)) {@@ -123,7 +132,6 @@ interface ICodiconCell { 	readonly element: HTMLElement; 	readonly icon: HTMLElement; 	readonly animationElement?: HTMLElement;-	readonly disposable?: IDisposable; }  export class SessionsChatBackgroundRenderer extends Disposable {@@ -133,6 +141,7 @@ export class SessionsChatBackgroundRenderer extends Disposable { 	private readonly codiconCells = new Map<string, ICodiconCell>(); 	private readonly confettiCandidates = new Set<string>(); 	private readonly foregroundResizeObservations = new Map<HTMLElement, IDisposable>();+	private readonly confettiButton: Button | undefined; 	private readonly _onDidActivateCodicon = this._register(new Emitter<HTMLElement>()); 	readonly onDidActivateCodicon: Event<HTMLElement> = this._onDidActivateCodicon.event; 	private readonly refreshScheduler: RunOnceScheduler;@@ -153,17 +162,11 @@ export class SessionsChatBackgroundRenderer extends Disposable { 		));  		this.backgroundLayer = $('.sessions-chat-background');-		if (!this.interactive) {-			this.backgroundLayer.ariaHidden = 'true';-		} else {-			this.backgroundLayer.classList.add('sessions-chat-background-interactive');-		}+		this.backgroundLayer.ariaHidden = 'true'; 		this.backgroundLayer.hidden = true;  		this.codiconLayer = $('.sessions-chat-codicon-background');-		if (!this.interactive) {-			this.codiconLayer.ariaHidden = 'true';-		}+		this.codiconLayer.ariaHidden = 'true'; 		this.codiconLayer.hidden = true; 		this.backgroundLayer.appendChild(this.codiconLayer); 		this.element.prepend(this.backgroundLayer);@@ -173,6 +176,17 @@ export class SessionsChatBackgroundRenderer extends Disposable { 			this.backgroundLayer.remove(); 		})); +		if (this.interactive) {+			const label = localize('sessionsChatBackground.confettiButton', "Celebrate");+			this.confettiButton = this._register(new Button(this.element, { ariaLabel: label, title: label }));+			this.confettiButton.element.classList.add('sessions-chat-codicon-hit-target');+			this.element.insertBefore(this.confettiButton.element, this.backgroundLayer);+			this.confettiButton.element.hidden = true;+			this._register(this.confettiButton.onDidClick(() => this.activateConfettiCell()));+		} else {+			this.confettiButton = undefined;+		}+ 		this.resizeObserver = this._register(new DisposableResizeObserver( 			'SessionsChatBackgroundRenderer', 			() => this.refreshScheduler.schedule(),@@ -188,7 +202,7 @@ export class SessionsChatBackgroundRenderer extends Disposable { 				childList: true, 				subtree: true, 			})(mutations => {-				if (mutations.some(mutation => !this.backgroundLayer.contains(mutation.target))) {+				if (mutations.some(mutation => !this.backgroundLayer.contains(mutation.target) && !this.confettiButton?.element.contains(mutation.target))) { 					this.refreshScheduler.schedule(); 				} 			}));@@ -244,22 +258,19 @@ export class SessionsChatBackgroundRenderer extends Disposable { 			} 		} -		const preserveFocus = !!this.confettiCell-			&& this.codiconCells.get(this.confettiCell)?.element === getWindow(this.element).document.activeElement; 		if (this.interactive && (!this.confettiCell || !this.confettiCandidates.has(this.confettiCell))) { 			const candidates = [...this.confettiCandidates]; 			this.confettiCell = candidates.length ? candidates[Math.min(candidates.length - 1, Math.floor(this.random() * candidates.length))] : undefined; 		}  		if (gridSize === this.codiconGridSize) {-			this.updateConfettiButtons(preserveFocus);+			this.updateConfettiButton(); 			return; 		} 		this.codiconGridSize = gridSize;  		for (const [cell, codiconCell] of this.codiconCells) { 			if (!visibleCells.has(cell)) {-				codiconCell.disposable?.dispose(); 				codiconCell.element.remove(); 				this.codiconCells.delete(cell); 			}@@ -273,7 +284,7 @@ export class SessionsChatBackgroundRenderer extends Disposable { 			}  			const icon = codiconChoices[hashCodiconCell(row, column, 1) % codiconChoices.length];-			const codiconCell = this.interactive ? this.createCodiconButton(cell, icon) : this.createDecorativeCodicon(icon);+			const codiconCell = this.interactive ? this.createInteractiveCodicon(icon) : this.createDecorativeCodicon(icon); 			if (this.interactive) { 				codiconCell.element.style.left = `${layout.left}px`; 				codiconCell.element.style.top = `${layout.top}px`;@@ -289,7 +300,7 @@ export class SessionsChatBackgroundRenderer extends Disposable { 			this.codiconLayer.appendChild(codiconCell.element); 		} -		this.updateConfettiButtons(preserveFocus);+		this.updateConfettiButton(); 	}  	private getConfettiCandidateGeometry(): IConfettiCandidateGeometry {@@ -338,64 +349,64 @@ export class SessionsChatBackgroundRenderer extends Disposable { 		return { element, icon: element }; 	} -	private createCodiconButton(cell: string, icon: ThemeIcon): ICodiconCell {-		const disposables = new DisposableStore();-		const button = disposables.add(new Button(this.codiconLayer, {}));-		button.element.classList.add('sessions-chat-codicon-cell');-		button.element.style.width = `${codiconButtonSize}px`;-		button.element.style.height = `${codiconButtonSize}px`;+	private createInteractiveCodicon(icon: ThemeIcon): ICodiconCell {+		const element = $('.sessions-chat-codicon-cell');+		element.ariaHidden = 'true'; 		const animationElement = $('.sessions-chat-codicon-button-animation');-		const buttonIcon = renderIcon(icon);-		buttonIcon.ariaHidden = 'true';-		animationElement.appendChild(buttonIcon);-		button.element.appendChild(animationElement);-		disposables.add(button.onDidClick(event => {-			if (this.confettiCell !== cell) {-				return;-			}+		const iconElement = renderIcon(icon);+		iconElement.ariaHidden = 'true';+		animationElement.appendChild(iconElement);+		element.appendChild(animationElement);+		return { element, icon: iconElement, animationElement };+	} -			this._onDidActivateCodicon.fire(animationElement);-			this.selectNextConfettiCell(cell, event.type === EventType.KEY_DOWN);-		}));-		return { element: button.element, icon: buttonIcon, animationElement, disposable: disposables };+	private activateConfettiCell(): void {+		if (!this.confettiCell) {+			return;+		}++		const cell = this.codiconCells.get(this.confettiCell);+		if (!cell?.animationElement) {+			return;+		}++		this._onDidActivateCodicon.fire(cell.animationElement);+		this.selectNextConfettiCell(this.confettiCell); 	} -	private selectNextConfettiCell(currentCell: string, preserveFocus: boolean): void {+	private selectNextConfettiCell(currentCell: string): void { 		const candidates = [...this.confettiCandidates].filter(cell => cell !== currentCell); 		if (!candidates.length) { 			return; 		}  		const candidateIndex = Math.min(candidates.length - 1, Math.floor(this.random() * candidates.length)); 		this.confettiCell = candidates[candidateIndex];-		this.updateConfettiButtons(preserveFocus);+		this.updateConfettiButton(); 	} -	private updateConfettiButtons(preserveFocus = false): void {-		const label = localize('sessionsChatBackground.confettiButton', "Celebrate");-		for (const [cell, codiconCell] of this.codiconCells) {-			if (!codiconCell.animationElement) {-				continue;-			}+	private updateConfettiButton(): void {+		if (!this.confettiButton) {+			return;+		} -			const active = cell === this.confettiCell;-			codiconCell.element.classList.toggle('sessions-chat-codicon-button', active);-			codiconCell.element.tabIndex = active ? 0 : -1;-			if (active) {-				codiconCell.element.removeAttribute('aria-hidden');-				codiconCell.element.setAttribute('role', 'button');-				codiconCell.element.setAttribute('aria-label', label);-				codiconCell.element.title = label;-			} else {-				codiconCell.element.blur();-				codiconCell.element.ariaHidden = 'true';-				codiconCell.element.removeAttribute('role');-				codiconCell.element.removeAttribute('aria-label');-				codiconCell.element.removeAttribute('title');-			}+		const confettiCell = this.confettiCell;+		const cell = confettiCell ? this.codiconCells.get(confettiCell) : undefined;+		if (!confettiCell || !cell) {+			this.confettiButton.element.hidden = true;+			this.updateActiveConfettiCell();+			return; 		}-		if (preserveFocus && this.confettiCell) {-			this.codiconCells.get(this.confettiCell)?.element.focus();++		this.confettiButton.element.style.left = cell.element.style.left;+		this.confettiButton.element.style.top = cell.element.style.top;+		this.confettiButton.element.hidden = false;+		this.updateActiveConfettiCell();+	}++	private updateActiveConfettiCell(): void {+		for (const [cellId, cell] of this.codiconCells) {+			cell.element.classList.toggle('sessions-chat-codicon-button-active', cellId === this.confettiCell); 		} 	} @@ -404,8 +415,9 @@ export class SessionsChatBackgroundRenderer extends Disposable { 			observation.dispose(); 		} 		this.foregroundResizeObservations.clear();-		for (const cell of this.codiconCells.values()) {-			cell.disposable?.dispose();+		if (this.confettiButton) {+			this.confettiButton.element.blur();+			this.confettiButton.element.hidden = true; 		} 		this.codiconCells.clear(); 		this.confettiCandidates.clear();
src/vs/sessions/services/chatBackground/browser/media/chatBackground.css11 + / 8
@@ -18,10 +18,6 @@ 	pointer-events: none; } -.sessions-chat-background.sessions-chat-background-interactive {-	z-index: auto;-}- .sessions-chat-background, .sessions-chat-background-replica-viewport { 	inset: 0;@@ -47,11 +43,14 @@ }  .sessions-chat-background > .sessions-chat-codicon-background > .codicon,-.sessions-chat-codicon-cell {+.sessions-chat-codicon-cell,+.sessions-chat-codicon-hit-target { 	position: absolute; }  .sessions-chat-codicon-cell {+	width: var(--vscode-spacing-size240);+	height: var(--vscode-spacing-size240); 	border-radius: var(--vscode-cornerRadius-small); 	color: inherit; 	transform: translate(-50%, -50%);@@ -60,21 +59,25 @@ 	place-items: center; } -.sessions-chat-codicon-button {+.monaco-workbench .part.sessionspart > .sessions-chat-codicon-hit-target {+	width: var(--vscode-spacing-size240);+	height: var(--vscode-spacing-size240); 	z-index: 3;+	opacity: 0; 	pointer-events: auto;+	transform: translate(-50%, -50%); }  .sessions-chat-codicon-button-animation { 	display: flex; 	line-height: 1; } -.sessions-chat-codicon-button:hover {+.monaco-workbench .part.sessionspart > .sessions-chat-codicon-hit-target:hover ~ .sessions-chat-background .sessions-chat-codicon-button-active { 	background-color: var(--vscode-toolbar-hoverBackground); } -.sessions-chat-codicon-button:focus-visible {+.monaco-workbench .part.sessionspart > .sessions-chat-codicon-hit-target:focus-visible ~ .sessions-chat-background .sessions-chat-codicon-button-active { 	background-color: var(--vscode-toolbar-hoverBackground); 	outline: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); 	outline-offset: calc(-1 * var(--vscode-strokeThickness));