From 849d97e1bcf0cd949cf430fe2590cb597f72144f Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Mon, 9 Feb 2026 12:51:04 -0800 Subject: [PATCH 01/72] smooth panel animations --- src/vs/base/browser/ui/grid/grid.ts | 16 +- src/vs/base/browser/ui/grid/gridview.ts | 43 +++++ src/vs/base/browser/ui/motion/motion.css | 9 ++ src/vs/base/browser/ui/motion/motion.ts | 125 +++++++++++++++ src/vs/base/browser/ui/splitview/splitview.ts | 150 ++++++++++++++++++ .../browser/quickInputController.ts | 39 ++++- src/vs/workbench/browser/layout.ts | 99 +++++++----- src/vs/workbench/browser/media/motion.css | 22 +++ src/vs/workbench/browser/style.ts | 1 + 9 files changed, 465 insertions(+), 39 deletions(-) create mode 100644 src/vs/base/browser/ui/motion/motion.css create mode 100644 src/vs/base/browser/ui/motion/motion.ts create mode 100644 src/vs/workbench/browser/media/motion.css diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index 60b2666cf26..a060fdae9fc 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -6,7 +6,7 @@ import { IBoundarySashes, Orientation } from '../sash/sash.js'; import { equals, tail } from '../../../common/arrays.js'; import { Event } from '../../../common/event.js'; -import { Disposable } from '../../../common/lifecycle.js'; +import { Disposable, IDisposable } from '../../../common/lifecycle.js'; import './gridview.css'; import { Box, GridView, IGridViewOptions, IGridViewStyles, IView as IGridViewView, IViewSize, orthogonal, Sizing as GridViewSizing, GridLocation } from './gridview.js'; import type { SplitView, AutoSizing as SplitViewAutoSizing } from '../splitview/splitview.js'; @@ -655,6 +655,20 @@ export class Grid extends Disposable { this.gridview.setViewVisible(location, visible); } + /** + * Set the visibility state of a {@link IView view} with a smooth animation. + * + * @param view The {@link IView view}. + * @param visible Whether the view should be visible. + * @param duration The transition duration in milliseconds. + * @param easing The CSS easing function string. + * @returns A disposable that cancels the animation. + */ + setViewVisibleAnimated(view: T, visible: boolean, duration: number, easing: string, onComplete?: () => void): IDisposable { + const location = this.getViewLocation(view); + return this.gridview.setViewVisibleAnimated(location, visible, duration, easing, onComplete); + } + /** * Returns a descriptor for the entire grid. */ diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 46821d6fcb8..29bb93c67de 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -633,6 +633,24 @@ class BranchNode implements ISplitView, IDisposable { } } + setChildVisibleAnimated(index: number, visible: boolean, duration: number, easing: string, onComplete?: () => void): IDisposable { + index = validateIndex(index, this.children.length); + + if (this.splitview.isViewVisible(index) === visible) { + return { dispose: () => { } }; + } + + const wereAllChildrenHidden = this.splitview.contentSize === 0; + const disposable = this.splitview.setViewVisibleAnimated(index, visible, duration, easing, onComplete); + const areAllChildrenHidden = this.splitview.contentSize === 0; + + if ((visible && wereAllChildrenHidden) || (!visible && areAllChildrenHidden)) { + this._onDidVisibilityChange.fire(visible); + } + + return disposable; + } + getChildCachedVisibleSize(index: number): number | undefined { index = validateIndex(index, this.children.length); @@ -1677,6 +1695,31 @@ export class GridView implements IDisposable { parent.setChildVisible(index, visible); } + /** + * Set the visibility state of a {@link IView view} with a smooth animation. + * + * @param location The {@link GridLocation location} of the view. + * @param visible Whether the view should be visible. + * @param duration The transition duration in milliseconds. + * @param easing The CSS easing function string. + * @returns A disposable that cancels the animation. + */ + setViewVisibleAnimated(location: GridLocation, visible: boolean, duration: number, easing: string, onComplete?: () => void): IDisposable { + if (this.hasMaximizedView()) { + this.exitMaximizedView(); + return { dispose: () => { } }; + } + + const [rest, index] = tail(location); + const [, parent] = this.getNode(rest); + + if (!(parent instanceof BranchNode)) { + throw new Error('Invalid from location'); + } + + return parent.setChildVisibleAnimated(index, visible, duration, easing, onComplete); + } + /** * Returns a descriptor for the entire grid. */ diff --git a/src/vs/base/browser/ui/motion/motion.css b/src/vs/base/browser/ui/motion/motion.css new file mode 100644 index 00000000000..69e257be2d3 --- /dev/null +++ b/src/vs/base/browser/ui/motion/motion.css @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Utility class applied during panel animations to prevent content overflow */ +.monaco-split-view2 > .split-view-container > .split-view-view.motion-animating { + overflow: hidden; +} diff --git a/src/vs/base/browser/ui/motion/motion.ts b/src/vs/base/browser/ui/motion/motion.ts new file mode 100644 index 00000000000..cd7a9a05ab4 --- /dev/null +++ b/src/vs/base/browser/ui/motion/motion.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './motion.css'; + +//#region Duration Constants + +/** + * Duration in milliseconds for panel open (entrance) animations. + * Per Fluent 2 Enter/Exit pattern - entrance should feel smooth but not sluggish. + */ +export const PANEL_OPEN_DURATION = 300; + +/** + * Duration in milliseconds for panel close (exit) animations. + * Exits are faster than entrances - feels snappy and responsive. + */ +export const PANEL_CLOSE_DURATION = 200; + +/** + * Duration in milliseconds for quick input open (entrance) animations. + */ +export const QUICK_INPUT_OPEN_DURATION = 250; + +/** + * Duration in milliseconds for quick input close (exit) animations. + */ +export const QUICK_INPUT_CLOSE_DURATION = 150; + +//#endregion + +//#region Easing Curves + +/** + * Fluent 2 ease-out curve - default for entrances and expansions. + * Starts fast and decelerates to a stop. + */ +export const EASE_OUT = 'cubic-bezier(0.1, 0.9, 0.2, 1)'; + +/** + * Fluent 2 ease-in curve - for exits and collapses. + * Starts slow and accelerates out. + */ +export const EASE_IN = 'cubic-bezier(0.9, 0.1, 1, 0.2)'; + +//#endregion + +//#region Cubic Bezier Evaluation + +/** + * Parses a CSS `cubic-bezier(x1, y1, x2, y2)` string into its four control + * point values. Returns `[0, 0, 1, 1]` (linear) on parse failure. + */ +export function parseCubicBezier(css: string): [number, number, number, number] { + const match = css.match(/cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/); + if (!match) { + return [0, 0, 1, 1]; + } + return [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3]), parseFloat(match[4])]; +} + +/** + * Evaluates a cubic bezier curve at time `t` (0-1). + * + * Given control points `(x1, y1)` and `(x2, y2)` (the CSS `cubic-bezier` + * parameters), this finds the bezier parameter `u` such that `Bx(u) = t` + * using Newton's method, then returns `By(u)`. + */ +export function solveCubicBezier(x1: number, y1: number, x2: number, y2: number, t: number): number { + if (t <= 0) { + return 0; + } + if (t >= 1) { + return 1; + } + + // Newton's method to find u where Bx(u) = t + let u = t; // initial guess + for (let i = 0; i < 8; i++) { + const currentX = bezierComponent(u, x1, x2); + const error = currentX - t; + if (Math.abs(error) < 1e-6) { + break; + } + const dx = bezierComponentDerivative(u, x1, x2); + if (Math.abs(dx) < 1e-6) { + break; + } + u -= error / dx; + } + + u = Math.max(0, Math.min(1, u)); + return bezierComponent(u, y1, y2); +} + +/** Evaluates one component of a cubic bezier: B(u) with control points p1, p2, endpoints 0 and 1. */ +function bezierComponent(u: number, p1: number, p2: number): number { + // B(u) = 3(1-u)^2*u*p1 + 3(1-u)*u^2*p2 + u^3 + const oneMinusU = 1 - u; + return 3 * oneMinusU * oneMinusU * u * p1 + 3 * oneMinusU * u * u * p2 + u * u * u; +} + +/** First derivative of a bezier component: B'(u). */ +function bezierComponentDerivative(u: number, p1: number, p2: number): number { + // B'(u) = 3(1-u)^2*p1 + 6(1-u)*u*(p2-p1) + 3*u^2*(1-p2) + const oneMinusU = 1 - u; + return 3 * oneMinusU * oneMinusU * p1 + 6 * oneMinusU * u * (p2 - p1) + 3 * u * u * (1 - p2); +} + +//#endregion + +//#region Utility Functions + +/** + * Checks whether motion is reduced by looking for the `monaco-reduce-motion` + * class on an ancestor element. This integrates with VS Code's existing + * accessibility infrastructure in {@link AccessibilityService}. + */ +export function isMotionReduced(element: HTMLElement): boolean { + return element.closest('.monaco-reduce-motion') !== null; +} + +//#endregion diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 35f2724c1a8..faca36926fc 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -14,6 +14,7 @@ import { combinedDisposable, Disposable, dispose, IDisposable, toDisposable } fr import { clamp } from '../../../common/numbers.js'; import { Scrollable, ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js'; import * as types from '../../../common/types.js'; +import { isMotionReduced, parseCubicBezier, solveCubicBezier } from '../motion/motion.js'; import './splitview.css'; export { Orientation } from '../sash/sash.js'; @@ -818,6 +819,155 @@ export class SplitView void): IDisposable { + if (index < 0 || index >= this.viewItems.length) { + throw new Error('Index out of bounds'); + } + + // Cancel any in-flight animation + this._cleanupMotion?.(); + this._cleanupMotion = undefined; + + const viewItem = this.viewItems[index]; + + // If motion is reduced or already in target state, use instant path + if (viewItem.visible === visible || isMotionReduced(this.el)) { + this.setViewVisible(index, visible); + return toDisposable(() => { }); + } + + const container = this.viewContainer.children[index] as HTMLElement; + const window = getWindow(this.el); + let disposed = false; + let rafId: number | undefined; + + // 1. Snapshot sizes BEFORE the visibility change (the animation start state) + const startSizes = this.viewItems.map(v => v.size); + + // 2. Apply the target visibility to the model instantly. + // This computes final sizes, fires events, updates sashes, etc. + this.setViewVisible(index, visible); + + // 3. Snapshot sizes AFTER the visibility change (the animation end state) + const finalSizes = this.viewItems.map(v => v.size); + + // 4. Restore start sizes so we can animate FROM them + for (let i = 0; i < this.viewItems.length; i++) { + this.viewItems[i].size = startSizes[i]; + } + + // 5. For hiding: the target container lost .visible class (→ display:none). + // Restore it so content stays visible during the animation. + if (!visible) { + container.classList.add('visible'); + } + + // 6. Clip overflow on the target container while it shrinks. + // Only apply for HIDE animations - for SHOW, we leave overflow alone + // so that box-shadow / visual effects on the child Part are not clipped + // by the parent container during the animation. + if (!visible) { + container.style.overflow = 'hidden'; + } + + // 7. Render the start state + this.layoutViews(); + + // 8. Parse easing for JS evaluation + const [x1, y1, x2, y2] = parseCubicBezier(easing); + + // Helper: snap all sizes to final state and clean up + const applyFinalState = () => { + for (let i = 0; i < this.viewItems.length; i++) { + this.viewItems[i].size = finalSizes[i]; + } + if (!visible) { + container.classList.remove('visible'); + container.style.overflow = ''; + } + this.layoutViews(); + this.saveProportions(); + }; + + const cleanup = (completed: boolean) => { + if (disposed) { + return; + } + disposed = true; + if (rafId !== undefined) { + window.cancelAnimationFrame(rafId); + rafId = undefined; + } + applyFinalState(); + this._cleanupMotion = undefined; + if (completed) { + onComplete?.(); + } + }; + this._cleanupMotion = () => cleanup(false); + + // 9. Animate via requestAnimationFrame + const startTime = performance.now(); + const totalSize = this.size; + + const animate = () => { + if (disposed) { + return; + } + + const elapsed = performance.now() - startTime; + const t = Math.min(elapsed / duration, 1); + const easedT = solveCubicBezier(x1, y1, x2, y2, t); + + // Interpolate all view sizes + let runningTotal = 0; + for (let i = 0; i < this.viewItems.length; i++) { + if (i === this.viewItems.length - 1) { + // Last item absorbs rounding errors to maintain total = this.size + this.viewItems[i].size = totalSize - runningTotal; + } else { + const size = Math.round( + startSizes[i] + (finalSizes[i] - startSizes[i]) * easedT + ); + this.viewItems[i].size = size; + runningTotal += size; + } + } + + this.layoutViews(); + + if (t < 1) { + rafId = window.requestAnimationFrame(animate); + } else { + cleanup(true); + } + }; + + rafId = window.requestAnimationFrame(animate); + + return toDisposable(() => cleanup(false)); + } + + private _cleanupMotion: (() => void) | undefined; + /** * Returns the {@link IView view}'s size previously to being hidden. * diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index fae2f7ffb45..17ab3d2f9b2 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -37,6 +37,7 @@ import { TriStateCheckbox, createToggleActionViewItemProvider } from '../../../b import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; import { QuickInputTreeController } from './tree/quickInputTreeController.js'; import { QuickTree } from './tree/quickTree.js'; +import { isMotionReduced, QUICK_INPUT_OPEN_DURATION, QUICK_INPUT_CLOSE_DURATION, EASE_OUT, EASE_IN } from '../../../base/browser/ui/motion/motion.js'; const $ = dom.$; @@ -713,6 +714,23 @@ export class QuickInputController extends Disposable { this.dndController?.layoutContainer(); ui.inputBox.setFocus(); this.quickInputTypeContext.set(controller.type); + + // Animate entrance: fade in + slide down + if (!isMotionReduced(ui.container)) { + ui.container.style.opacity = '0'; + ui.container.style.transform = 'translateY(-8px)'; + ui.container.style.transition = `opacity ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}, transform ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}`; + // Trigger reflow so the initial state is registered before applying target + void ui.container.offsetHeight; + ui.container.style.opacity = '1'; + ui.container.style.transform = 'translateY(0)'; + const onDone = () => { + ui.container.style.transition = ''; + ui.container.style.willChange = ''; + ui.container.removeEventListener('transitionend', onDone); + }; + ui.container.addEventListener('transitionend', onDone); + } } isVisible(): boolean { @@ -779,7 +797,26 @@ export class QuickInputController extends Disposable { this.controller = null; this.onHideEmitter.fire(); if (container) { - container.style.display = 'none'; + // Animate exit: fade out + slide up (faster than open) + if (!isMotionReduced(container)) { + container.style.transition = `opacity ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN}, transform ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN}`; + container.style.opacity = '0'; + container.style.transform = 'translateY(-8px)'; + const onDone = () => { + container.style.display = 'none'; + container.style.transition = ''; + container.style.opacity = ''; + container.style.transform = ''; + container.style.willChange = ''; + container.removeEventListener('transitionend', onDone); + }; + container.addEventListener('transitionend', onDone); + // Safety timeout in case transitionend doesn't fire + const window = dom.getWindow(container); + window.setTimeout(onDone, QUICK_INPUT_CLOSE_DURATION + 50); + } else { + container.style.display = 'none'; + } } if (!focusChanged) { let currentElement = this.previousFocusElement; diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 784c9edfadc..b7bf628b882 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -47,6 +47,7 @@ import { AuxiliaryBarPart } from './parts/auxiliarybar/auxiliaryBarPart.js'; import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js'; import { IAuxiliaryWindowService } from '../services/auxiliaryWindow/browser/auxiliaryWindowService.js'; import { CodeWindow, mainWindow } from '../../base/browser/window.js'; +import { PANEL_OPEN_DURATION, PANEL_CLOSE_DURATION, EASE_OUT, EASE_IN } from '../../base/browser/ui/motion/motion.js'; //#region Layout Implementation @@ -1864,27 +1865,33 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.stateModel.setRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN, hidden); - // Adjust CSS - if (hidden) { - this.mainContainer.classList.add(LayoutClasses.SIDEBAR_HIDDEN); - } else { + // Adjust CSS - for hiding, defer adding the class until animation + // completes so the part stays visible during the exit animation. + if (!hidden) { this.mainContainer.classList.remove(LayoutClasses.SIDEBAR_HIDDEN); } // Propagate to grid - this.workbenchGrid.setViewVisible(this.sideBarPartView, !hidden); + this.workbenchGrid.setViewVisibleAnimated( + this.sideBarPartView, + !hidden, + hidden ? PANEL_CLOSE_DURATION : PANEL_OPEN_DURATION, + hidden ? EASE_IN : EASE_OUT, + hidden ? () => { + // Deferred to after close animation + this.mainContainer.classList.add(LayoutClasses.SIDEBAR_HIDDEN); + if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Sidebar); - // If sidebar becomes hidden, also hide the current active Viewlet if any - if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { - this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Sidebar); - - if (!this.isAuxiliaryBarMaximized()) { - this.focusPanelOrEditor(); // do not auto focus when auxiliary bar is maximized - } - } + if (!this.isAuxiliaryBarMaximized()) { + this.focusPanelOrEditor(); // do not auto focus when auxiliary bar is maximized + } + } + } : undefined + ); // If sidebar becomes visible, show last active Viewlet or default viewlet - else if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { + if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { const viewletToOpen = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.Sidebar); if (viewletToOpen) { this.openViewContainer(ViewContainerLocation.Sidebar, viewletToOpen); @@ -2008,13 +2015,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const panelOpensMaximized = this.panelOpensMaximized(); - // Adjust CSS - if (hidden) { - this.mainContainer.classList.add(LayoutClasses.PANEL_HIDDEN); - } else { - this.mainContainer.classList.remove(LayoutClasses.PANEL_HIDDEN); - } - // If maximized and in process of hiding, unmaximize FIRST before // changing visibility to prevent conflict with setEditorHidden // which would force panel visible again (fixes #281772) @@ -2022,13 +2022,31 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.toggleMaximizedPanel(); } - // Propagate layout changes to grid - this.workbenchGrid.setViewVisible(this.panelPartView, !hidden); + // Adjust CSS - for hiding, defer adding the class until animation + // completes because `.nopanel .part.panel { display: none !important }` + // would instantly hide the panel content mid-animation. + if (!hidden) { + this.mainContainer.classList.remove(LayoutClasses.PANEL_HIDDEN); + } - // If panel part becomes hidden, also hide the current active panel if any + // Propagate layout changes to grid + this.workbenchGrid.setViewVisibleAnimated( + this.panelPartView, + !hidden, + hidden ? PANEL_CLOSE_DURATION : PANEL_OPEN_DURATION, + hidden ? EASE_IN : EASE_OUT, + hidden ? () => { + // Deferred to after close animation + this.mainContainer.classList.add(LayoutClasses.PANEL_HIDDEN); + if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Panel); + } + } : undefined + ); + + // If panel part becomes hidden, focus the editor after animation starts let focusEditor = false; if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)) { - this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Panel); if ( !isIOS && // do not auto focus on iOS (https://github.com/microsoft/vscode/issues/127832) !this.isAuxiliaryBarMaximized() // do not auto focus when auxiliary bar is maximized @@ -2202,24 +2220,31 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN, hidden); - // Adjust CSS - if (hidden) { - this.mainContainer.classList.add(LayoutClasses.AUXILIARYBAR_HIDDEN); - } else { + // Adjust CSS - for hiding, defer adding the class until animation + // completes because `.noauxiliarybar .part.auxiliarybar { display: none !important }` + // would instantly hide the content mid-animation. + if (!hidden) { this.mainContainer.classList.remove(LayoutClasses.AUXILIARYBAR_HIDDEN); } // Propagate to grid - this.workbenchGrid.setViewVisible(this.auxiliaryBarPartView, !hidden); - - // If auxiliary bar becomes hidden, also hide the current active pane composite if any - if (hidden && this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { - this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.AuxiliaryBar); - this.focusPanelOrEditor(); - } + this.workbenchGrid.setViewVisibleAnimated( + this.auxiliaryBarPartView, + !hidden, + hidden ? PANEL_CLOSE_DURATION : PANEL_OPEN_DURATION, + hidden ? EASE_IN : EASE_OUT, + hidden ? () => { + // Deferred to after close animation + this.mainContainer.classList.add(LayoutClasses.AUXILIARYBAR_HIDDEN); + if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { + this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.AuxiliaryBar); + this.focusPanelOrEditor(); + } + } : undefined + ); // If auxiliary bar becomes visible, show last active pane composite or default pane composite - else if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { + if (!hidden && !this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { let viewletToOpen: string | undefined = this.paneCompositeService.getLastActivePaneCompositeId(ViewContainerLocation.AuxiliaryBar); // verify that the viewlet we try to open has views before we default to it diff --git a/src/vs/workbench/browser/media/motion.css b/src/vs/workbench/browser/media/motion.css new file mode 100644 index 00000000000..5d22a5e4677 --- /dev/null +++ b/src/vs/workbench/browser/media/motion.css @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Motion custom properties -- only active when motion is enabled */ +.monaco-workbench.monaco-enable-motion { + --vscode-motion-panel-open-duration: 300ms; + --vscode-motion-panel-close-duration: 200ms; + --vscode-motion-quick-input-open-duration: 250ms; + --vscode-motion-quick-input-close-duration: 150ms; + --vscode-motion-ease-out: cubic-bezier(0.1, 0.9, 0.2, 1); + --vscode-motion-ease-in: cubic-bezier(0.9, 0.1, 1, 0.2); +} + +/* Disable all motion durations when reduced motion is active */ +.monaco-workbench.monaco-reduce-motion { + --vscode-motion-panel-open-duration: 0ms; + --vscode-motion-panel-close-duration: 0ms; + --vscode-motion-quick-input-open-duration: 0ms; + --vscode-motion-quick-input-close-duration: 0ms; +} diff --git a/src/vs/workbench/browser/style.ts b/src/vs/workbench/browser/style.ts index 9250ef3f280..de344e7e46b 100644 --- a/src/vs/workbench/browser/style.ts +++ b/src/vs/workbench/browser/style.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import './media/style.css'; +import './media/motion.css'; import { registerThemingParticipant } from '../../platform/theme/common/themeService.js'; import { WORKBENCH_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND } from '../common/theme.js'; import { isWeb, isIOS } from '../../base/common/platform.js'; From ba7ffdb523dcdfc6fb04e32586557fd8b6b517c6 Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Mon, 9 Feb 2026 13:26:50 -0800 Subject: [PATCH 02/72] faster transitions --- src/vs/base/browser/ui/motion/motion.ts | 8 ++++---- src/vs/workbench/browser/media/motion.css | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vs/base/browser/ui/motion/motion.ts b/src/vs/base/browser/ui/motion/motion.ts index cd7a9a05ab4..5a1cfa28a5a 100644 --- a/src/vs/base/browser/ui/motion/motion.ts +++ b/src/vs/base/browser/ui/motion/motion.ts @@ -11,23 +11,23 @@ import './motion.css'; * Duration in milliseconds for panel open (entrance) animations. * Per Fluent 2 Enter/Exit pattern - entrance should feel smooth but not sluggish. */ -export const PANEL_OPEN_DURATION = 300; +export const PANEL_OPEN_DURATION = 200; /** * Duration in milliseconds for panel close (exit) animations. * Exits are faster than entrances - feels snappy and responsive. */ -export const PANEL_CLOSE_DURATION = 200; +export const PANEL_CLOSE_DURATION = 75; /** * Duration in milliseconds for quick input open (entrance) animations. */ -export const QUICK_INPUT_OPEN_DURATION = 250; +export const QUICK_INPUT_OPEN_DURATION = 125; /** * Duration in milliseconds for quick input close (exit) animations. */ -export const QUICK_INPUT_CLOSE_DURATION = 150; +export const QUICK_INPUT_CLOSE_DURATION = 75; //#endregion diff --git a/src/vs/workbench/browser/media/motion.css b/src/vs/workbench/browser/media/motion.css index 5d22a5e4677..75581b1bb93 100644 --- a/src/vs/workbench/browser/media/motion.css +++ b/src/vs/workbench/browser/media/motion.css @@ -5,10 +5,10 @@ /* Motion custom properties -- only active when motion is enabled */ .monaco-workbench.monaco-enable-motion { - --vscode-motion-panel-open-duration: 300ms; - --vscode-motion-panel-close-duration: 200ms; - --vscode-motion-quick-input-open-duration: 250ms; - --vscode-motion-quick-input-close-duration: 150ms; + --vscode-motion-panel-open-duration: 200ms; + --vscode-motion-panel-close-duration: 75ms; + --vscode-motion-quick-input-open-duration: 125ms; + --vscode-motion-quick-input-close-duration: 75ms; --vscode-motion-ease-out: cubic-bezier(0.1, 0.9, 0.2, 1); --vscode-motion-ease-in: cubic-bezier(0.9, 0.1, 1, 0.2); } From 09eede573c2e27d36410cbc5b0f91feac9e9a49f Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Mon, 9 Feb 2026 13:32:26 -0800 Subject: [PATCH 03/72] fixed command palette animation --- src/vs/base/browser/ui/motion/motion.ts | 2 +- src/vs/platform/quickinput/browser/quickInputController.ts | 7 +++++-- src/vs/workbench/browser/media/motion.css | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/ui/motion/motion.ts b/src/vs/base/browser/ui/motion/motion.ts index 5a1cfa28a5a..455825510eb 100644 --- a/src/vs/base/browser/ui/motion/motion.ts +++ b/src/vs/base/browser/ui/motion/motion.ts @@ -22,7 +22,7 @@ export const PANEL_CLOSE_DURATION = 75; /** * Duration in milliseconds for quick input open (entrance) animations. */ -export const QUICK_INPUT_OPEN_DURATION = 125; +export const QUICK_INPUT_OPEN_DURATION = 200; /** * Duration in milliseconds for quick input close (exit) animations. diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 17ab3d2f9b2..6e4e11e17ec 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -717,11 +717,14 @@ export class QuickInputController extends Disposable { // Animate entrance: fade in + slide down if (!isMotionReduced(ui.container)) { + // Set initial state without transition + ui.container.style.transition = 'none'; ui.container.style.opacity = '0'; ui.container.style.transform = 'translateY(-8px)'; - ui.container.style.transition = `opacity ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}, transform ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}`; - // Trigger reflow so the initial state is registered before applying target + // Trigger reflow so the browser registers the initial state void ui.container.offsetHeight; + // Now apply transition and target state + ui.container.style.transition = `opacity ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}, transform ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}`; ui.container.style.opacity = '1'; ui.container.style.transform = 'translateY(0)'; const onDone = () => { diff --git a/src/vs/workbench/browser/media/motion.css b/src/vs/workbench/browser/media/motion.css index 75581b1bb93..79730b812c7 100644 --- a/src/vs/workbench/browser/media/motion.css +++ b/src/vs/workbench/browser/media/motion.css @@ -7,7 +7,7 @@ .monaco-workbench.monaco-enable-motion { --vscode-motion-panel-open-duration: 200ms; --vscode-motion-panel-close-duration: 75ms; - --vscode-motion-quick-input-open-duration: 125ms; + --vscode-motion-quick-input-open-duration: 200ms; --vscode-motion-quick-input-close-duration: 75ms; --vscode-motion-ease-out: cubic-bezier(0.1, 0.9, 0.2, 1); --vscode-motion-ease-in: cubic-bezier(0.9, 0.1, 1, 0.2); From c6a54123f2f6ef811849b0056b570f6510355bc9 Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Mon, 9 Feb 2026 14:02:51 -0800 Subject: [PATCH 04/72] fixed full screen bug --- src/vs/base/browser/ui/splitview/splitview.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index faca36926fc..8a457624e3e 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -811,6 +811,14 @@ export class SplitView Date: Mon, 9 Feb 2026 15:29:00 +0800 Subject: [PATCH 05/72] fix: stop unbounded websocket inflate-byte recording --- src/vs/base/parts/ipc/node/ipc.net.ts | 40 +++++++++++++++++- .../base/parts/ipc/test/node/ipc.net.test.ts | 41 +++++++++++++++++++ src/vs/server/node/extensionHostConnection.ts | 7 ++++ .../node/remoteExtensionHostAgentServer.ts | 6 +++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/vs/base/parts/ipc/node/ipc.net.ts b/src/vs/base/parts/ipc/node/ipc.net.ts index 9a508286e23..5ada7242262 100644 --- a/src/vs/base/parts/ipc/node/ipc.net.ts +++ b/src/vs/base/parts/ipc/node/ipc.net.ts @@ -318,6 +318,10 @@ export class WebSocketNodeSocket extends Disposable implements ISocket, ISocketT return this._flowManager.recordedInflateBytes; } + public setRecordInflateBytes(record: boolean): void { + this._flowManager.setRecordInflateBytes(record); + } + public traceSocketEvent(type: SocketDiagnosticsEventType, data?: VSBuffer | Uint8Array | ArrayBuffer | ArrayBufferView | unknown): void { this.socket.traceSocketEvent(type, data); } @@ -598,6 +602,10 @@ class WebSocketFlowManager extends Disposable { return VSBuffer.alloc(0); } + public setRecordInflateBytes(record: boolean): void { + this._zlibInflateStream?.setRecordInflateBytes(record); + } + constructor( private readonly _tracer: ISocketTracer, permessageDeflate: boolean, @@ -714,6 +722,7 @@ class ZlibInflateStream extends Disposable { private readonly _zlibInflate: InflateRaw; private readonly _recordedInflateBytes: VSBuffer[] = []; private readonly _pendingInflateData: VSBuffer[] = []; + private _recordInflateBytes: boolean; public get recordedInflateBytes(): VSBuffer { if (this._recordInflateBytes) { @@ -724,11 +733,12 @@ class ZlibInflateStream extends Disposable { constructor( private readonly _tracer: ISocketTracer, - private readonly _recordInflateBytes: boolean, + recordInflateBytes: boolean, inflateBytes: VSBuffer | null, options: ZlibOptions ) { super(); + this._recordInflateBytes = recordInflateBytes; this._zlibInflate = createInflateRaw(options); this._zlibInflate.on('error', (err: Error) => { this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibInflateError, { message: err?.message, code: (err as NodeJS.ErrnoException)?.code }); @@ -756,6 +766,13 @@ class ZlibInflateStream extends Disposable { this._zlibInflate.write(buffer.buffer); } + public setRecordInflateBytes(record: boolean): void { + this._recordInflateBytes = record; + if (!record) { + this._recordedInflateBytes.length = 0; + } + } + public flush(callback: (data: VSBuffer) => void): void { this._zlibInflate.flush(() => { this._tracer.traceSocketEvent(SocketDiagnosticsEventType.zlibInflateFlushFired); @@ -764,6 +781,17 @@ class ZlibInflateStream extends Disposable { callback(data); }); } + + public override dispose(): void { + this._recordedInflateBytes.length = 0; + this._pendingInflateData.length = 0; + try { + this._zlibInflate.close(); + } catch { + // ignore errors while disposing + } + super.dispose(); + } } class ZlibDeflateStream extends Disposable { @@ -812,6 +840,16 @@ class ZlibDeflateStream extends Disposable { callback(data); }); } + + public override dispose(): void { + this._pendingDeflateData.length = 0; + try { + this._zlibDeflate.close(); + } catch { + // ignore errors while disposing + } + super.dispose(); + } } function unmask(buffer: VSBuffer, mask: number): void { diff --git a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts index a1bc9a5749a..6c96decef45 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.net.test.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.net.test.ts @@ -711,6 +711,47 @@ suite('WebSocketNodeSocket', () => { assert.deepStrictEqual(actual, 'Hello'); }); + test('setRecordInflateBytes(false) clears and stops recording', async () => { + const disposables = new DisposableStore(); + const socket = disposables.add(new FakeNodeSocket()); + // eslint-disable-next-line local/code-no-any-casts + const webSocket = disposables.add(new WebSocketNodeSocket(socket, true, null, true)); + + const compressedHelloFrame = [0xc1, 0x07, 0xf2, 0x48, 0xcd, 0xc9, 0xc9, 0x07, 0x00]; + const waitForOneData = () => new Promise(resolve => { + const d = webSocket.onData(data => { + d.dispose(); + resolve(data); + }); + }); + + const firstPromise = waitForOneData(); + socket.fireData(compressedHelloFrame); + const first = await firstPromise; + assert.strictEqual(fromCharCodeArray(fromUint8Array(first.buffer)), 'Hello'); + assert.ok(webSocket.recordedInflateBytes.byteLength > 0); + + webSocket.setRecordInflateBytes(false); + assert.strictEqual(webSocket.recordedInflateBytes.byteLength, 0); + + const secondPromise = waitForOneData(); + socket.fireData(compressedHelloFrame); + const second = await secondPromise; + assert.strictEqual(fromCharCodeArray(fromUint8Array(second.buffer)), 'Hello'); + assert.strictEqual(webSocket.recordedInflateBytes.byteLength, 0); + + webSocket.setRecordInflateBytes(true); + assert.strictEqual(webSocket.recordedInflateBytes.byteLength, 0); + + const thirdPromise = waitForOneData(); + socket.fireData(compressedHelloFrame); + const third = await thirdPromise; + assert.strictEqual(fromCharCodeArray(fromUint8Array(third.buffer)), 'Hello'); + assert.ok(webSocket.recordedInflateBytes.byteLength > 0); + + disposables.dispose(); + }); + test('A fragmented compressed text message', async () => { // contains "Hello" const frames = [ // contains "Hello" diff --git a/src/vs/server/node/extensionHostConnection.ts b/src/vs/server/node/extensionHostConnection.ts index 6ae4edd84b9..0daf9ee7031 100644 --- a/src/vs/server/node/extensionHostConnection.ts +++ b/src/vs/server/node/extensionHostConnection.ts @@ -94,6 +94,7 @@ class ConnectionData { skipWebSocketFrames = false; permessageDeflate = this.socket.permessageDeflate; inflateBytes = this.socket.recordedInflateBytes; + this.socket.setRecordInflateBytes(false); } return { @@ -133,6 +134,9 @@ export class ExtensionHostConnection extends Disposable { this._remoteAddress = remoteAddress; this._extensionHostProcess = null; this._connectionData = new ConnectionData(socket, initialDataChunk); + if (!this._canSendSocket && socket instanceof WebSocketNodeSocket) { + socket.setRecordInflateBytes(false); + } this._log(`New connection established.`); } @@ -209,6 +213,9 @@ export class ExtensionHostConnection extends Disposable { public acceptReconnection(remoteAddress: string, _socket: NodeSocket | WebSocketNodeSocket, initialDataChunk: VSBuffer): void { this._remoteAddress = remoteAddress; this._log(`The client has reconnected.`); + if (!this._canSendSocket && _socket instanceof WebSocketNodeSocket) { + _socket.setRecordInflateBytes(false); + } const connectionData = new ConnectionData(_socket, initialDataChunk); if (!this._extensionHostProcess) { diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 269cc3878eb..da7e417cd5c 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -395,6 +395,9 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { if (msg.desiredConnectionType === ConnectionType.Management) { // This should become a management connection + if (socket instanceof WebSocketNodeSocket) { + socket.setRecordInflateBytes(false); + } if (isReconnection) { // This is a reconnection @@ -484,6 +487,9 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { } } else if (msg.desiredConnectionType === ConnectionType.Tunnel) { + if (socket instanceof WebSocketNodeSocket) { + socket.setRecordInflateBytes(false); + } const tunnelStartParams = msg.args; this._createTunnel(protocol, tunnelStartParams); From f5827dab395d8bcffda3f4c6a5dec14c37c5470d Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:39:41 -0800 Subject: [PATCH 06/72] Adopt esbuild for bundling more builtin extensions Follow up on #294208 --- eslint.config.js | 6 +++-- extensions/markdown-math/esbuild-browser.ts | 26 +++++++++++++++++++ .../esbuild.ts} | 21 ++++++++------- .../extension-browser.webpack.config.js | 13 ---------- extensions/media-preview/esbuild-browser.ts | 26 +++++++++++++++++++ .../esbuild.ts} | 21 ++++++++------- .../media-preview/extension.webpack.config.js | 16 ------------ .../mermaid-chat-features/esbuild-browser.ts | 26 +++++++++++++++++++ .../esbuild.ts} | 18 ++++++++----- .../extension-browser.webpack.config.js | 13 ---------- 10 files changed, 115 insertions(+), 71 deletions(-) create mode 100644 extensions/markdown-math/esbuild-browser.ts rename extensions/{mermaid-chat-features/extension.webpack.config.js => markdown-math/esbuild.ts} (53%) delete mode 100644 extensions/markdown-math/extension-browser.webpack.config.js create mode 100644 extensions/media-preview/esbuild-browser.ts rename extensions/{markdown-math/extension.webpack.config.js => media-preview/esbuild.ts} (53%) delete mode 100644 extensions/media-preview/extension.webpack.config.js create mode 100644 extensions/mermaid-chat-features/esbuild-browser.ts rename extensions/{media-preview/extension-browser.webpack.config.js => mermaid-chat-features/esbuild.ts} (53%) delete mode 100644 extensions/mermaid-chat-features/extension-browser.webpack.config.js diff --git a/eslint.config.js b/eslint.config.js index fa55c74032c..21c0ae56151 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2067,8 +2067,10 @@ export default tseslint.config( 'extensions/markdown-language-features/src/**/*.ts', 'extensions/markdown-language-features/notebook/**/*.ts', 'extensions/markdown-language-features/preview-src/**/*.ts', - 'extensions/mermaid-chat-features/**/*.ts', - 'extensions/media-preview/**/*.ts', + 'extensions/mermaid-chat-features/chat-webview-src/*.ts', + 'extensions/mermaid-chat-features/src/*.ts', + 'extensions/media-preview/preview-src/*.ts', + 'extensions/media-preview/src/*.ts', 'extensions/simple-browser/**/*.ts', 'extensions/typescript-language-features/**/*.ts', ], diff --git a/extensions/markdown-math/esbuild-browser.ts b/extensions/markdown-math/esbuild-browser.ts new file mode 100644 index 00000000000..a2659e5ff46 --- /dev/null +++ b/extensions/markdown-math/esbuild-browser.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.ts'; + +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist', 'browser'); + +run({ + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), + }, + srcDir, + outdir: outDir, + additionalOptions: { + platform: 'browser', + format: 'cjs', + define: { + 'process.platform': JSON.stringify('web'), + 'process.env': JSON.stringify({}), + 'process.env.BROWSER_ENV': JSON.stringify('true'), + }, + }, +}, process.argv); diff --git a/extensions/mermaid-chat-features/extension.webpack.config.js b/extensions/markdown-math/esbuild.ts similarity index 53% rename from extensions/mermaid-chat-features/extension.webpack.config.js rename to extensions/markdown-math/esbuild.ts index 4928186ae55..232f589197b 100644 --- a/extensions/mermaid-chat-features/extension.webpack.config.js +++ b/extensions/markdown-math/esbuild.ts @@ -2,15 +2,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check -import withDefaults from '../shared.webpack.config.mjs'; +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.ts'; -export default withDefaults({ - context: import.meta.dirname, - resolve: { - mainFields: ['module', 'main'] +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist'); + +run({ + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), }, - entry: { - extension: './src/extension.ts', - } -}); + srcDir, + outdir: outDir, +}, process.argv); diff --git a/extensions/markdown-math/extension-browser.webpack.config.js b/extensions/markdown-math/extension-browser.webpack.config.js deleted file mode 100644 index b758f2d8155..00000000000 --- a/extensions/markdown-math/extension-browser.webpack.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -import { browser as withBrowserDefaults } from '../shared.webpack.config.mjs'; - -export default withBrowserDefaults({ - context: import.meta.dirname, - entry: { - extension: './src/extension.ts' - } -}); diff --git a/extensions/media-preview/esbuild-browser.ts b/extensions/media-preview/esbuild-browser.ts new file mode 100644 index 00000000000..a2659e5ff46 --- /dev/null +++ b/extensions/media-preview/esbuild-browser.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.ts'; + +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist', 'browser'); + +run({ + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), + }, + srcDir, + outdir: outDir, + additionalOptions: { + platform: 'browser', + format: 'cjs', + define: { + 'process.platform': JSON.stringify('web'), + 'process.env': JSON.stringify({}), + 'process.env.BROWSER_ENV': JSON.stringify('true'), + }, + }, +}, process.argv); diff --git a/extensions/markdown-math/extension.webpack.config.js b/extensions/media-preview/esbuild.ts similarity index 53% rename from extensions/markdown-math/extension.webpack.config.js rename to extensions/media-preview/esbuild.ts index 4928186ae55..232f589197b 100644 --- a/extensions/markdown-math/extension.webpack.config.js +++ b/extensions/media-preview/esbuild.ts @@ -2,15 +2,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check -import withDefaults from '../shared.webpack.config.mjs'; +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.ts'; -export default withDefaults({ - context: import.meta.dirname, - resolve: { - mainFields: ['module', 'main'] +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist'); + +run({ + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), }, - entry: { - extension: './src/extension.ts', - } -}); + srcDir, + outdir: outDir, +}, process.argv); diff --git a/extensions/media-preview/extension.webpack.config.js b/extensions/media-preview/extension.webpack.config.js deleted file mode 100644 index 4928186ae55..00000000000 --- a/extensions/media-preview/extension.webpack.config.js +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -import withDefaults from '../shared.webpack.config.mjs'; - -export default withDefaults({ - context: import.meta.dirname, - resolve: { - mainFields: ['module', 'main'] - }, - entry: { - extension: './src/extension.ts', - } -}); diff --git a/extensions/mermaid-chat-features/esbuild-browser.ts b/extensions/mermaid-chat-features/esbuild-browser.ts new file mode 100644 index 00000000000..a2659e5ff46 --- /dev/null +++ b/extensions/mermaid-chat-features/esbuild-browser.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.ts'; + +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist', 'browser'); + +run({ + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), + }, + srcDir, + outdir: outDir, + additionalOptions: { + platform: 'browser', + format: 'cjs', + define: { + 'process.platform': JSON.stringify('web'), + 'process.env': JSON.stringify({}), + 'process.env.BROWSER_ENV': JSON.stringify('true'), + }, + }, +}, process.argv); diff --git a/extensions/media-preview/extension-browser.webpack.config.js b/extensions/mermaid-chat-features/esbuild.ts similarity index 53% rename from extensions/media-preview/extension-browser.webpack.config.js rename to extensions/mermaid-chat-features/esbuild.ts index 6c86474b4e5..232f589197b 100644 --- a/extensions/media-preview/extension-browser.webpack.config.js +++ b/extensions/mermaid-chat-features/esbuild.ts @@ -2,12 +2,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check -import { browser as withBrowserDefaults } from '../shared.webpack.config.mjs'; +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.ts'; -export default withBrowserDefaults({ - context: import.meta.dirname, - entry: { - extension: './src/extension.ts' +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist'); + +run({ + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), }, -}); + srcDir, + outdir: outDir, +}, process.argv); diff --git a/extensions/mermaid-chat-features/extension-browser.webpack.config.js b/extensions/mermaid-chat-features/extension-browser.webpack.config.js deleted file mode 100644 index b758f2d8155..00000000000 --- a/extensions/mermaid-chat-features/extension-browser.webpack.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -import { browser as withBrowserDefaults } from '../shared.webpack.config.mjs'; - -export default withBrowserDefaults({ - context: import.meta.dirname, - entry: { - extension: './src/extension.ts' - } -}); From d81b46203cb433c7b7bd8528de023e8a04eb5cfd Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:08:14 -0800 Subject: [PATCH 07/72] Fix globs --- eslint.config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 21c0ae56151..0142f1d183c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2067,10 +2067,10 @@ export default tseslint.config( 'extensions/markdown-language-features/src/**/*.ts', 'extensions/markdown-language-features/notebook/**/*.ts', 'extensions/markdown-language-features/preview-src/**/*.ts', - 'extensions/mermaid-chat-features/chat-webview-src/*.ts', - 'extensions/mermaid-chat-features/src/*.ts', - 'extensions/media-preview/preview-src/*.ts', - 'extensions/media-preview/src/*.ts', + 'extensions/mermaid-chat-features/chat-webview-src/**/*.ts', + 'extensions/mermaid-chat-features/src/**/*.ts', + 'extensions/media-preview/preview-src/**/*.ts', + 'extensions/media-preview/src/**/*.ts', 'extensions/simple-browser/**/*.ts', 'extensions/typescript-language-features/**/*.ts', ], From 70e58babb6e498b17da5a47aaf5c3627f2d40c9d Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:09:07 -0800 Subject: [PATCH 08/72] Remove file that doesn't exist --- eslint.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 0142f1d183c..f60ff793db5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2069,7 +2069,6 @@ export default tseslint.config( 'extensions/markdown-language-features/preview-src/**/*.ts', 'extensions/mermaid-chat-features/chat-webview-src/**/*.ts', 'extensions/mermaid-chat-features/src/**/*.ts', - 'extensions/media-preview/preview-src/**/*.ts', 'extensions/media-preview/src/**/*.ts', 'extensions/simple-browser/**/*.ts', 'extensions/typescript-language-features/**/*.ts', From f3349e3a7ed4e024a9c7e1468291619e9ad6c6c4 Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Tue, 10 Feb 2026 15:54:18 -0800 Subject: [PATCH 09/72] added subtle fade, fixed command palette --- src/vs/base/browser/ui/motion/motion.ts | 42 +++++++++++++++++-- src/vs/base/browser/ui/splitview/splitview.ts | 19 +++++++-- .../browser/quickInputController.ts | 22 +++++++++- src/vs/workbench/browser/media/motion.css | 8 ++-- 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/vs/base/browser/ui/motion/motion.ts b/src/vs/base/browser/ui/motion/motion.ts index 455825510eb..817ad0b0213 100644 --- a/src/vs/base/browser/ui/motion/motion.ts +++ b/src/vs/base/browser/ui/motion/motion.ts @@ -11,23 +11,23 @@ import './motion.css'; * Duration in milliseconds for panel open (entrance) animations. * Per Fluent 2 Enter/Exit pattern - entrance should feel smooth but not sluggish. */ -export const PANEL_OPEN_DURATION = 200; +export const PANEL_OPEN_DURATION = 150; /** * Duration in milliseconds for panel close (exit) animations. * Exits are faster than entrances - feels snappy and responsive. */ -export const PANEL_CLOSE_DURATION = 75; +export const PANEL_CLOSE_DURATION = 50; /** * Duration in milliseconds for quick input open (entrance) animations. */ -export const QUICK_INPUT_OPEN_DURATION = 200; +export const QUICK_INPUT_OPEN_DURATION = 150; /** * Duration in milliseconds for quick input close (exit) animations. */ -export const QUICK_INPUT_CLOSE_DURATION = 75; +export const QUICK_INPUT_CLOSE_DURATION = 50; //#endregion @@ -111,6 +111,40 @@ function bezierComponentDerivative(u: number, p1: number, p2: number): number { //#endregion +//#region Duration Scaling + +/** + * Reference pixel distance at which the base duration constants apply. + * Duration scales linearly: a 600px animation takes twice as long as a 300px + * one, keeping perceived velocity constant. + */ +const REFERENCE_DISTANCE = 300; + +/** Minimum animation duration in milliseconds (avoids sub-frame flickers). */ +const MIN_DURATION = 50; + +/** Maximum animation duration in milliseconds (avoids sluggish feel). */ +const MAX_DURATION = 300; + +/** + * Scales a base animation duration proportionally to the pixel distance + * being animated, so that perceived velocity stays constant regardless of + * panel width. + * + * @param baseDuration The duration (ms) that applies at {@link REFERENCE_DISTANCE} pixels. + * @param pixelDistance The actual number of pixels the view will resize. + * @returns The scaled duration, clamped to [{@link MIN_DURATION}, {@link MAX_DURATION}]. + */ +export function scaleDuration(baseDuration: number, pixelDistance: number): number { + if (pixelDistance <= 0) { + return baseDuration; + } + const scaled = baseDuration * (pixelDistance / REFERENCE_DISTANCE); + return Math.round(Math.max(MIN_DURATION, Math.min(MAX_DURATION, scaled))); +} + +//#endregion + //#region Utility Functions /** diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 8a457624e3e..d83124decb2 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -14,7 +14,7 @@ import { combinedDisposable, Disposable, dispose, IDisposable, toDisposable } fr import { clamp } from '../../../common/numbers.js'; import { Scrollable, ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js'; import * as types from '../../../common/types.js'; -import { isMotionReduced, parseCubicBezier, solveCubicBezier } from '../motion/motion.js'; +import { isMotionReduced, parseCubicBezier, scaleDuration, solveCubicBezier } from '../motion/motion.js'; import './splitview.css'; export { Orientation } from '../sash/sash.js'; @@ -896,10 +896,17 @@ export class SplitView cleanup(false); - // 9. Animate via requestAnimationFrame + // 10. Animate via requestAnimationFrame const startTime = performance.now(); const totalSize = this.size; @@ -945,6 +953,9 @@ export class SplitView void) | undefined; private readonly inQuickInputContext: IContextKey; private readonly quickInputTypeContext: IContextKey; @@ -712,15 +713,19 @@ export class QuickInputController extends Disposable { const backKeybindingLabel = this.options.backKeybindingLabel(); backButton.tooltip = backKeybindingLabel ? localize('quickInput.backWithKeybinding', "Back ({0})", backKeybindingLabel) : localize('quickInput.back', "Back"); + const wasVisible = ui.container.style.display !== 'none'; ui.container.style.display = ''; + // Cancel any in-flight exit animation that would set display:none + this._cancelExitAnimation?.(); + this._cancelExitAnimation = undefined; this.updateLayout(); this.dndController?.setEnabled(!controller.anchor); this.dndController?.layoutContainer(); ui.inputBox.setFocus(); this.quickInputTypeContext.set(controller.type); - // Animate entrance: fade in + slide down - if (!isMotionReduced(ui.container)) { + // Animate entrance: fade in + slide down (only when first appearing) + if (!wasVisible && !isMotionReduced(ui.container)) { // Set initial state without transition ui.container.style.transition = 'none'; ui.container.style.opacity = '0'; @@ -806,10 +811,15 @@ export class QuickInputController extends Disposable { if (container) { // Animate exit: fade out + slide up (faster than open) if (!isMotionReduced(container)) { + let exitCancelled = false; container.style.transition = `opacity ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN}, transform ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN}`; container.style.opacity = '0'; container.style.transform = 'translateY(-8px)'; const onDone = () => { + if (exitCancelled) { + return; + } + exitCancelled = true; container.style.display = 'none'; container.style.transition = ''; container.style.opacity = ''; @@ -817,6 +827,14 @@ export class QuickInputController extends Disposable { container.style.willChange = ''; container.removeEventListener('transitionend', onDone); }; + this._cancelExitAnimation = () => { + exitCancelled = true; + container.style.transition = ''; + container.style.opacity = ''; + container.style.transform = ''; + container.style.willChange = ''; + container.removeEventListener('transitionend', onDone); + }; container.addEventListener('transitionend', onDone); // Safety timeout in case transitionend doesn't fire const window = dom.getWindow(container); diff --git a/src/vs/workbench/browser/media/motion.css b/src/vs/workbench/browser/media/motion.css index 79730b812c7..182d420add4 100644 --- a/src/vs/workbench/browser/media/motion.css +++ b/src/vs/workbench/browser/media/motion.css @@ -5,10 +5,10 @@ /* Motion custom properties -- only active when motion is enabled */ .monaco-workbench.monaco-enable-motion { - --vscode-motion-panel-open-duration: 200ms; - --vscode-motion-panel-close-duration: 75ms; - --vscode-motion-quick-input-open-duration: 200ms; - --vscode-motion-quick-input-close-duration: 75ms; + --vscode-motion-panel-open-duration: 150ms; + --vscode-motion-panel-close-duration: 50ms; + --vscode-motion-quick-input-open-duration: 150ms; + --vscode-motion-quick-input-close-duration: 50ms; --vscode-motion-ease-out: cubic-bezier(0.1, 0.9, 0.2, 1); --vscode-motion-ease-in: cubic-bezier(0.9, 0.1, 1, 0.2); } From 42f2854b85b10b9b6d10424d902875588981b1ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:57:50 +0000 Subject: [PATCH 10/72] Initial plan From a650799f9acd91fd3d16eed1fd6e9d03fd3283ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:04:09 +0000 Subject: [PATCH 11/72] Fix transitionend event filtering for quick input animations - Filter transitionend events by e.target to ignore bubbling from descendant elements in both entrance and exit animations - Add safety timeout fallback to entrance animation (matching exit pattern) - Clear safety timeouts when animations complete or are cancelled - Reorder variable declarations for better readability Co-authored-by: eli-w-king <201316543+eli-w-king@users.noreply.github.com> --- .../browser/quickInputController.ts | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 33d160f9bf9..ef2dfaf11ab 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -734,14 +734,34 @@ export class QuickInputController extends Disposable { void ui.container.offsetHeight; // Now apply transition and target state ui.container.style.transition = `opacity ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}, transform ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}`; + ui.container.style.willChange = 'opacity, transform'; ui.container.style.opacity = '1'; ui.container.style.transform = 'translateY(0)'; - const onDone = () => { + let entranceCleanedUp = false; + const window = dom.getWindow(ui.container); + let entranceCleanupTimeout: number | undefined; + const cleanupEntranceTransition = () => { + if (entranceCleanedUp) { + return; + } + entranceCleanedUp = true; ui.container.style.transition = ''; ui.container.style.willChange = ''; - ui.container.removeEventListener('transitionend', onDone); + ui.container.removeEventListener('transitionend', onTransitionEnd); + if (entranceCleanupTimeout !== undefined) { + clearTimeout(entranceCleanupTimeout); + entranceCleanupTimeout = undefined; + } }; - ui.container.addEventListener('transitionend', onDone); + const onTransitionEnd = (e: TransitionEvent) => { + if (e.target !== ui.container) { + return; // Ignore transitions from descendant elements + } + cleanupEntranceTransition(); + }; + ui.container.addEventListener('transitionend', onTransitionEnd); + // Safety timeout in case transitionend doesn't fire + entranceCleanupTimeout = window.setTimeout(cleanupEntranceTransition, QUICK_INPUT_OPEN_DURATION + 50); } } @@ -812,6 +832,8 @@ export class QuickInputController extends Disposable { // Animate exit: fade out + slide up (faster than open) if (!isMotionReduced(container)) { let exitCancelled = false; + const window = dom.getWindow(container); + let exitCleanupTimeout: number | undefined; container.style.transition = `opacity ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN}, transform ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN}`; container.style.opacity = '0'; container.style.transform = 'translateY(-8px)'; @@ -825,7 +847,17 @@ export class QuickInputController extends Disposable { container.style.opacity = ''; container.style.transform = ''; container.style.willChange = ''; - container.removeEventListener('transitionend', onDone); + container.removeEventListener('transitionend', onTransitionEnd); + if (exitCleanupTimeout !== undefined) { + clearTimeout(exitCleanupTimeout); + exitCleanupTimeout = undefined; + } + }; + const onTransitionEnd = (e: TransitionEvent) => { + if (e.target !== container) { + return; // Ignore transitions from descendant elements + } + onDone(); }; this._cancelExitAnimation = () => { exitCancelled = true; @@ -833,12 +865,15 @@ export class QuickInputController extends Disposable { container.style.opacity = ''; container.style.transform = ''; container.style.willChange = ''; - container.removeEventListener('transitionend', onDone); + container.removeEventListener('transitionend', onTransitionEnd); + if (exitCleanupTimeout !== undefined) { + clearTimeout(exitCleanupTimeout); + exitCleanupTimeout = undefined; + } }; - container.addEventListener('transitionend', onDone); + container.addEventListener('transitionend', onTransitionEnd); // Safety timeout in case transitionend doesn't fire - const window = dom.getWindow(container); - window.setTimeout(onDone, QUICK_INPUT_CLOSE_DURATION + 50); + exitCleanupTimeout = window.setTimeout(onDone, QUICK_INPUT_CLOSE_DURATION + 50); } else { container.style.display = 'none'; } From 638483047965fe8c2cda9ae88c547a27e9b22b2e Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 11 Feb 2026 10:52:23 +0100 Subject: [PATCH 12/72] Refactor view visibility management and centralize animation logic --- src/vs/base/browser/ui/grid/grid.ts | 23 +--- src/vs/base/browser/ui/grid/gridview.ts | 53 +------ src/vs/base/browser/ui/motion/motion.ts | 130 +++++++++--------- src/vs/base/browser/ui/splitview/splitview.ts | 107 ++++++++------ .../browser/quickInputController.ts | 12 +- src/vs/workbench/browser/layout.ts | 47 ++++--- 6 files changed, 177 insertions(+), 195 deletions(-) diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index a060fdae9fc..b343a0e3bc2 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -6,12 +6,13 @@ import { IBoundarySashes, Orientation } from '../sash/sash.js'; import { equals, tail } from '../../../common/arrays.js'; import { Event } from '../../../common/event.js'; -import { Disposable, IDisposable } from '../../../common/lifecycle.js'; +import { Disposable } from '../../../common/lifecycle.js'; import './gridview.css'; import { Box, GridView, IGridViewOptions, IGridViewStyles, IView as IGridViewView, IViewSize, orthogonal, Sizing as GridViewSizing, GridLocation } from './gridview.js'; -import type { SplitView, AutoSizing as SplitViewAutoSizing } from '../splitview/splitview.js'; +import type { SplitView, AutoSizing as SplitViewAutoSizing, IViewVisibilityAnimationOptions } from '../splitview/splitview.js'; export type { IViewSize }; +export type { IViewVisibilityAnimationOptions } from '../splitview/splitview.js'; export { LayoutPriority, Orientation, orthogonal } from './gridview.js'; export const enum Direction { @@ -649,24 +650,12 @@ export class Grid extends Disposable { * Set the visibility state of a {@link IView view}. * * @param view The {@link IView view}. - */ - setViewVisible(view: T, visible: boolean): void { - const location = this.getViewLocation(view); - this.gridview.setViewVisible(location, visible); - } - - /** - * Set the visibility state of a {@link IView view} with a smooth animation. - * - * @param view The {@link IView view}. * @param visible Whether the view should be visible. - * @param duration The transition duration in milliseconds. - * @param easing The CSS easing function string. - * @returns A disposable that cancels the animation. + * @param animation Optional animation options. */ - setViewVisibleAnimated(view: T, visible: boolean, duration: number, easing: string, onComplete?: () => void): IDisposable { + setViewVisible(view: T, visible: boolean, animation?: IViewVisibilityAnimationOptions): void { const location = this.getViewLocation(view); - return this.gridview.setViewVisibleAnimated(location, visible, duration, easing, onComplete); + this.gridview.setViewVisible(location, visible, animation); } /** diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 29bb93c67de..f7137f0342c 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -5,7 +5,7 @@ import { $ } from '../../dom.js'; import { IBoundarySashes, Orientation, Sash } from '../sash/sash.js'; -import { DistributeSizing, ISplitViewStyles, IView as ISplitView, LayoutPriority, Sizing, AutoSizing, SplitView } from '../splitview/splitview.js'; +import { DistributeSizing, ISplitViewStyles, IView as ISplitView, IViewVisibilityAnimationOptions, LayoutPriority, Sizing, AutoSizing, SplitView } from '../splitview/splitview.js'; import { equals as arrayEquals, tail } from '../../../common/arrays.js'; import { Color } from '../../../common/color.js'; import { Emitter, Event, Relay } from '../../../common/event.js'; @@ -615,7 +615,7 @@ class BranchNode implements ISplitView, IDisposable { return this.splitview.isViewVisible(index); } - setChildVisible(index: number, visible: boolean): void { + setChildVisible(index: number, visible: boolean, animation?: IViewVisibilityAnimationOptions): void { index = validateIndex(index, this.children.length); if (this.splitview.isViewVisible(index) === visible) { @@ -623,7 +623,7 @@ class BranchNode implements ISplitView, IDisposable { } const wereAllChildrenHidden = this.splitview.contentSize === 0; - this.splitview.setViewVisible(index, visible); + this.splitview.setViewVisible(index, visible, animation); const areAllChildrenHidden = this.splitview.contentSize === 0; // If all children are hidden then the parent should hide the entire splitview @@ -633,24 +633,6 @@ class BranchNode implements ISplitView, IDisposable { } } - setChildVisibleAnimated(index: number, visible: boolean, duration: number, easing: string, onComplete?: () => void): IDisposable { - index = validateIndex(index, this.children.length); - - if (this.splitview.isViewVisible(index) === visible) { - return { dispose: () => { } }; - } - - const wereAllChildrenHidden = this.splitview.contentSize === 0; - const disposable = this.splitview.setViewVisibleAnimated(index, visible, duration, easing, onComplete); - const areAllChildrenHidden = this.splitview.contentSize === 0; - - if ((visible && wereAllChildrenHidden) || (!visible && areAllChildrenHidden)) { - this._onDidVisibilityChange.fire(visible); - } - - return disposable; - } - getChildCachedVisibleSize(index: number): number | undefined { index = validateIndex(index, this.children.length); @@ -1679,7 +1661,7 @@ export class GridView implements IDisposable { * * @param location The {@link GridLocation location} of the view. */ - setViewVisible(location: GridLocation, visible: boolean): void { + setViewVisible(location: GridLocation, visible: boolean, animation?: IViewVisibilityAnimationOptions): void { if (this.hasMaximizedView()) { this.exitMaximizedView(); return; @@ -1692,32 +1674,7 @@ export class GridView implements IDisposable { throw new Error('Invalid from location'); } - parent.setChildVisible(index, visible); - } - - /** - * Set the visibility state of a {@link IView view} with a smooth animation. - * - * @param location The {@link GridLocation location} of the view. - * @param visible Whether the view should be visible. - * @param duration The transition duration in milliseconds. - * @param easing The CSS easing function string. - * @returns A disposable that cancels the animation. - */ - setViewVisibleAnimated(location: GridLocation, visible: boolean, duration: number, easing: string, onComplete?: () => void): IDisposable { - if (this.hasMaximizedView()) { - this.exitMaximizedView(); - return { dispose: () => { } }; - } - - const [rest, index] = tail(location); - const [, parent] = this.getNode(rest); - - if (!(parent instanceof BranchNode)) { - throw new Error('Invalid from location'); - } - - return parent.setChildVisibleAnimated(index, visible, duration, easing, onComplete); + parent.setChildVisible(index, visible, animation); } /** diff --git a/src/vs/base/browser/ui/motion/motion.ts b/src/vs/base/browser/ui/motion/motion.ts index 817ad0b0213..c2e8a045d41 100644 --- a/src/vs/base/browser/ui/motion/motion.ts +++ b/src/vs/base/browser/ui/motion/motion.ts @@ -5,94 +5,90 @@ import './motion.css'; -//#region Duration Constants - -/** - * Duration in milliseconds for panel open (entrance) animations. - * Per Fluent 2 Enter/Exit pattern - entrance should feel smooth but not sluggish. - */ -export const PANEL_OPEN_DURATION = 150; - -/** - * Duration in milliseconds for panel close (exit) animations. - * Exits are faster than entrances - feels snappy and responsive. - */ -export const PANEL_CLOSE_DURATION = 50; - -/** - * Duration in milliseconds for quick input open (entrance) animations. - */ -export const QUICK_INPUT_OPEN_DURATION = 150; - -/** - * Duration in milliseconds for quick input close (exit) animations. - */ -export const QUICK_INPUT_CLOSE_DURATION = 50; - -//#endregion - //#region Easing Curves +/** + * A pre-parsed cubic bezier easing curve that can be evaluated directly + * without reparsing a CSS string on every frame. + * + * Given control points `(x1, y1)` and `(x2, y2)` (the CSS `cubic-bezier` + * parameters), {@link solve} finds the bezier parameter `u` such that + * `Bx(u) = t` using Newton's method, then returns `By(u)`. + */ +export class CubicBezierCurve { + + constructor( + readonly x1: number, + readonly y1: number, + readonly x2: number, + readonly y2: number, + ) { } + + /** + * Evaluate the curve at time `t` (0-1), returning the eased value. + */ + solve(t: number): number { + if (t <= 0) { + return 0; + } + if (t >= 1) { + return 1; + } + + // Newton's method to find u where Bx(u) = t + let u = t; // initial guess + for (let i = 0; i < 8; i++) { + const currentX = bezierComponent(u, this.x1, this.x2); + const error = currentX - t; + if (Math.abs(error) < 1e-6) { + break; + } + const dx = bezierComponentDerivative(u, this.x1, this.x2); + if (Math.abs(dx) < 1e-6) { + break; + } + u -= error / dx; + } + + u = Math.max(0, Math.min(1, u)); + return bezierComponent(u, this.y1, this.y2); + } + + /** + * Returns the CSS `cubic-bezier(…)` string representation, for use in + * CSS `transition` or `animation` properties. + */ + toCssString(): string { + return `cubic-bezier(${this.x1}, ${this.y1}, ${this.x2}, ${this.y2})`; + } +} + /** * Fluent 2 ease-out curve - default for entrances and expansions. * Starts fast and decelerates to a stop. */ -export const EASE_OUT = 'cubic-bezier(0.1, 0.9, 0.2, 1)'; +export const EASE_OUT = new CubicBezierCurve(0.1, 0.9, 0.2, 1); /** * Fluent 2 ease-in curve - for exits and collapses. * Starts slow and accelerates out. */ -export const EASE_IN = 'cubic-bezier(0.9, 0.1, 1, 0.2)'; +export const EASE_IN = new CubicBezierCurve(0.9, 0.1, 1, 0.2); //#endregion //#region Cubic Bezier Evaluation /** - * Parses a CSS `cubic-bezier(x1, y1, x2, y2)` string into its four control - * point values. Returns `[0, 0, 1, 1]` (linear) on parse failure. + * Parses a CSS `cubic-bezier(x1, y1, x2, y2)` string into a + * {@link CubicBezierCurve}. Returns a linear curve on parse failure. */ -export function parseCubicBezier(css: string): [number, number, number, number] { +export function parseCubicBezier(css: string): CubicBezierCurve { const match = css.match(/cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/); if (!match) { - return [0, 0, 1, 1]; + return new CubicBezierCurve(0, 0, 1, 1); } - return [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3]), parseFloat(match[4])]; -} - -/** - * Evaluates a cubic bezier curve at time `t` (0-1). - * - * Given control points `(x1, y1)` and `(x2, y2)` (the CSS `cubic-bezier` - * parameters), this finds the bezier parameter `u` such that `Bx(u) = t` - * using Newton's method, then returns `By(u)`. - */ -export function solveCubicBezier(x1: number, y1: number, x2: number, y2: number, t: number): number { - if (t <= 0) { - return 0; - } - if (t >= 1) { - return 1; - } - - // Newton's method to find u where Bx(u) = t - let u = t; // initial guess - for (let i = 0; i < 8; i++) { - const currentX = bezierComponent(u, x1, x2); - const error = currentX - t; - if (Math.abs(error) < 1e-6) { - break; - } - const dx = bezierComponentDerivative(u, x1, x2); - if (Math.abs(dx) < 1e-6) { - break; - } - u -= error / dx; - } - - u = Math.max(0, Math.min(1, u)); - return bezierComponent(u, y1, y2); + return new CubicBezierCurve(parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3]), parseFloat(match[4])); } /** Evaluates one component of a cubic bezier: B(u) with control points p1, p2, endpoints 0 and 1. */ diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index d83124decb2..c6ac50b2ded 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -8,16 +8,41 @@ import { DomEmitter } from '../../event.js'; import { ISashEvent as IBaseSashEvent, Orientation, Sash, SashState } from '../sash/sash.js'; import { SmoothScrollableElement } from '../scrollbar/scrollableElement.js'; import { pushToEnd, pushToStart, range } from '../../../common/arrays.js'; +import { CancellationToken } from '../../../common/cancellation.js'; import { Color } from '../../../common/color.js'; import { Emitter, Event } from '../../../common/event.js'; import { combinedDisposable, Disposable, dispose, IDisposable, toDisposable } from '../../../common/lifecycle.js'; import { clamp } from '../../../common/numbers.js'; import { Scrollable, ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js'; import * as types from '../../../common/types.js'; -import { isMotionReduced, parseCubicBezier, scaleDuration, solveCubicBezier } from '../motion/motion.js'; +import { CubicBezierCurve, isMotionReduced, scaleDuration } from '../motion/motion.js'; import './splitview.css'; export { Orientation } from '../sash/sash.js'; +/** + * Options for animating a view visibility change in a {@link SplitView}. + */ +export interface IViewVisibilityAnimationOptions { + + /** Transition duration in milliseconds. */ + readonly duration: number; + + /** The easing curve applied to the animation. */ + readonly easing: CubicBezierCurve; + + /** + * Optional callback invoked when the animation finishes naturally. + * NOT called if the animation is cancelled via the {@link token}. + */ + readonly onComplete?: () => void; + + /** + * A cancellation token that allows the caller to stop the animation. + * When cancellation is requested the animation snaps to its final state. + */ + readonly token: CancellationToken; +} + export interface ISplitViewStyles { readonly separatorBorder: Color; } @@ -803,22 +828,38 @@ export class SplitView= this.viewItems.length) { throw new Error('Index out of bounds'); } // Cancel any in-flight animation before changing visibility. - // An animated setViewVisibleAnimated interpolates ALL view sizes each - // frame, so a concurrent non-animated visibility change on a different - // view in this splitview would be overwritten on the next frame. - // Snapping the animation to its final state first prevents that. + // An animated visibility change interpolates ALL view sizes each + // frame, so a concurrent change on a different view would be + // overwritten on the next frame. Snapping first prevents that. this._cleanupMotion?.(); this._cleanupMotion = undefined; + if (animation && !animation.token.isCancellationRequested && !isMotionReduced(this.el) && this.viewItems[index].visible !== visible) { + this._setViewVisibleAnimated(index, visible, animation); + } else { + this._setViewVisibleInstant(index, visible); + } + } + + /** + * Apply the visibility change to the model without animation. + */ + private _setViewVisibleInstant(index: number, visible: boolean): void { const viewItem = this.viewItems[index]; viewItem.setVisible(visible); @@ -828,39 +869,18 @@ export class SplitView void): IDisposable { - if (index < 0 || index >= this.viewItems.length) { - throw new Error('Index out of bounds'); - } - - // Cancel any in-flight animation - this._cleanupMotion?.(); - this._cleanupMotion = undefined; - - const viewItem = this.viewItems[index]; - - // If motion is reduced or already in target state, use instant path - if (viewItem.visible === visible || isMotionReduced(this.el)) { - this.setViewVisible(index, visible); - return toDisposable(() => { }); - } + private _setViewVisibleAnimated(index: number, visible: boolean, animation: IViewVisibilityAnimationOptions): void { + const { duration: baseDuration, easing, onComplete, token } = animation; const container = this.viewContainer.children[index] as HTMLElement; const window = getWindow(this.el); @@ -872,7 +892,7 @@ export class SplitView v.size); @@ -901,13 +921,12 @@ export class SplitView { @@ -928,6 +947,7 @@ export class SplitView cleanup(false); + // Listen to the cancellation token so the caller can stop the animation + const tokenListener = token.onCancellationRequested(() => cleanup(false)); + // 10. Animate via requestAnimationFrame const startTime = performance.now(); const totalSize = this.size; @@ -951,7 +974,7 @@ export class SplitView cleanup(false)); } private _cleanupMotion: (() => void) | undefined; diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index ef2dfaf11ab..a9dcd8203e2 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -37,10 +37,16 @@ import { TriStateCheckbox, createToggleActionViewItemProvider } from '../../../b import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; import { QuickInputTreeController } from './tree/quickInputTreeController.js'; import { QuickTree } from './tree/quickTree.js'; -import { isMotionReduced, QUICK_INPUT_OPEN_DURATION, QUICK_INPUT_CLOSE_DURATION, EASE_OUT, EASE_IN } from '../../../base/browser/ui/motion/motion.js'; +import { isMotionReduced, EASE_OUT, EASE_IN } from '../../../base/browser/ui/motion/motion.js'; import { AnchorAlignment, AnchorPosition, layout2d } from '../../../base/common/layout.js'; import { getAnchorRect } from '../../../base/browser/ui/contextview/contextview.js'; +/** Duration (ms) for quick input open (entrance) animations. */ +const QUICK_INPUT_OPEN_DURATION = 150; + +/** Duration (ms) for quick input close (exit) animations. */ +const QUICK_INPUT_CLOSE_DURATION = 50; + const $ = dom.$; const VIEWSTATE_STORAGE_KEY = 'workbench.quickInput.viewState'; @@ -733,7 +739,7 @@ export class QuickInputController extends Disposable { // Trigger reflow so the browser registers the initial state void ui.container.offsetHeight; // Now apply transition and target state - ui.container.style.transition = `opacity ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}, transform ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT}`; + ui.container.style.transition = `opacity ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT.toCssString()}, transform ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT.toCssString()}`; ui.container.style.willChange = 'opacity, transform'; ui.container.style.opacity = '1'; ui.container.style.transform = 'translateY(0)'; @@ -834,7 +840,7 @@ export class QuickInputController extends Disposable { let exitCancelled = false; const window = dom.getWindow(container); let exitCleanupTimeout: number | undefined; - container.style.transition = `opacity ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN}, transform ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN}`; + container.style.transition = `opacity ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN.toCssString()}, transform ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN.toCssString()}`; container.style.opacity = '0'; container.style.transform = 'translateY(-8px)'; const onDone = () => { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index b7bf628b882..10c0e42f532 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -23,7 +23,7 @@ import { IHostService } from '../services/host/browser/host.js'; import { IBrowserWorkbenchEnvironmentService } from '../services/environment/browser/environmentService.js'; import { IEditorService } from '../services/editor/common/editorService.js'; import { EditorGroupLayout, GroupOrientation, GroupsOrder, IEditorGroupsService } from '../services/editor/common/editorGroupsService.js'; -import { SerializableGrid, ISerializableView, ISerializedGrid, Orientation, ISerializedNode, ISerializedLeafNode, Direction, IViewSize, Sizing } from '../../base/browser/ui/grid/grid.js'; +import { SerializableGrid, ISerializableView, ISerializedGrid, Orientation, ISerializedNode, ISerializedLeafNode, Direction, IViewSize, Sizing, IViewVisibilityAnimationOptions } from '../../base/browser/ui/grid/grid.js'; import { Part } from './part.js'; import { IStatusbarService } from '../services/statusbar/browser/statusbar.js'; import { IFileService } from '../../platform/files/common/files.js'; @@ -47,7 +47,8 @@ import { AuxiliaryBarPart } from './parts/auxiliarybar/auxiliaryBarPart.js'; import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js'; import { IAuxiliaryWindowService } from '../services/auxiliaryWindow/browser/auxiliaryWindowService.js'; import { CodeWindow, mainWindow } from '../../base/browser/window.js'; -import { PANEL_OPEN_DURATION, PANEL_CLOSE_DURATION, EASE_OUT, EASE_IN } from '../../base/browser/ui/motion/motion.js'; +import { EASE_OUT, EASE_IN } from '../../base/browser/ui/motion/motion.js'; +import { CancellationToken } from '../../base/common/cancellation.js'; //#region Layout Implementation @@ -1872,12 +1873,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } // Propagate to grid - this.workbenchGrid.setViewVisibleAnimated( + this.workbenchGrid.setViewVisible( this.sideBarPartView, !hidden, - hidden ? PANEL_CLOSE_DURATION : PANEL_OPEN_DURATION, - hidden ? EASE_IN : EASE_OUT, - hidden ? () => { + createViewVisibilityAnimation(hidden, () => { + if (!hidden) { return; } // Deferred to after close animation this.mainContainer.classList.add(LayoutClasses.SIDEBAR_HIDDEN); if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Sidebar)) { @@ -1887,7 +1887,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.focusPanelOrEditor(); // do not auto focus when auxiliary bar is maximized } } - } : undefined + }) ); // If sidebar becomes visible, show last active Viewlet or default viewlet @@ -2030,18 +2030,17 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } // Propagate layout changes to grid - this.workbenchGrid.setViewVisibleAnimated( + this.workbenchGrid.setViewVisible( this.panelPartView, !hidden, - hidden ? PANEL_CLOSE_DURATION : PANEL_OPEN_DURATION, - hidden ? EASE_IN : EASE_OUT, - hidden ? () => { + createViewVisibilityAnimation(hidden, () => { + if (!hidden) { return; } // Deferred to after close animation this.mainContainer.classList.add(LayoutClasses.PANEL_HIDDEN); if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.Panel)) { this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.Panel); } - } : undefined + }) ); // If panel part becomes hidden, focus the editor after animation starts @@ -2228,19 +2227,18 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } // Propagate to grid - this.workbenchGrid.setViewVisibleAnimated( + this.workbenchGrid.setViewVisible( this.auxiliaryBarPartView, !hidden, - hidden ? PANEL_CLOSE_DURATION : PANEL_OPEN_DURATION, - hidden ? EASE_IN : EASE_OUT, - hidden ? () => { + createViewVisibilityAnimation(hidden, () => { + if (!hidden) { return; } // Deferred to after close animation this.mainContainer.classList.add(LayoutClasses.AUXILIARYBAR_HIDDEN); if (this.paneCompositeService.getActivePaneComposite(ViewContainerLocation.AuxiliaryBar)) { this.paneCompositeService.hideActivePaneComposite(ViewContainerLocation.AuxiliaryBar); this.focusPanelOrEditor(); } - } : undefined + }) ); // If auxiliary bar becomes visible, show last active pane composite or default pane composite @@ -2737,6 +2735,21 @@ function getZenModeConfiguration(configurationService: IConfigurationService): Z return configurationService.getValue(WorkbenchLayoutSettings.ZEN_MODE_CONFIG); } +/** Duration (ms) for panel/sidebar open (entrance) animations. */ +const PANEL_OPEN_DURATION = 150; + +/** Duration (ms) for panel/sidebar close (exit) animations. */ +const PANEL_CLOSE_DURATION = 50; + +function createViewVisibilityAnimation(hidden: boolean, onComplete?: () => void, token: CancellationToken = CancellationToken.None): IViewVisibilityAnimationOptions { + return { + duration: hidden ? PANEL_CLOSE_DURATION : PANEL_OPEN_DURATION, + easing: hidden ? EASE_IN : EASE_OUT, + token, + onComplete, + }; +} + //#endregion //#region Layout State Model From c91fbe4b39ef2f329e2b4113a0d9ecb18d42043b Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 11 Feb 2026 12:17:00 +0000 Subject: [PATCH 13/72] Update menu separator color and enhance backdrop-filter for context menus --- extensions/theme-2026/themes/2026-dark.json | 2 +- extensions/theme-2026/themes/styles.css | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 2aca9467744..44d0d3273f3 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -94,7 +94,7 @@ "menu.foreground": "#bfbfbf", "menu.selectionBackground": "#3994BC26", "menu.selectionForeground": "#bfbfbf", - "menu.separatorBackground": "#838485", + "menu.separatorBackground": "#2A2B2C", "menu.border": "#2A2B2CFF", "commandCenter.foreground": "#bfbfbf", "commandCenter.activeForeground": "#bfbfbf", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 035e15149e7..c6393ee9a98 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -283,10 +283,19 @@ /* Context Menus */ .monaco-workbench .monaco-menu .monaco-action-bar.vertical { - box-shadow: var(--shadow-lg); border-radius: var(--radius-lg); +} + +/* Use ::before for backdrop-filter so it doesn't create a containing block + for position:fixed submenu children (which would clip them via overflow:hidden). */ +.monaco-workbench .monaco-menu .monaco-action-bar.vertical::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; backdrop-filter: var(--backdrop-blur-md); -webkit-backdrop-filter: var(--backdrop-blur-md); + z-index: -1; } .monaco-workbench .context-view .monaco-menu { @@ -295,6 +304,11 @@ border-radius: var(--radius-lg); } +.monaco-workbench .monaco-menu-container > .monaco-scrollable-element { + border-radius: var(--radius-lg) !important; + box-shadow: var(--shadow-lg) !important; +} + .monaco-workbench .action-widget { background: color-mix(in srgb, var(--vscode-menu-background) 60%, transparent) !important; backdrop-filter: var(--backdrop-blur-md); From 1cd08a4237099cfa09dc70f1067de5b7825530cf Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 11 Feb 2026 14:58:27 +0000 Subject: [PATCH 14/72] Refactor context menu styles by removing unnecessary backdrop-filter rules --- extensions/theme-2026/themes/styles.css | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index c6393ee9a98..da8961d2042 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -284,18 +284,7 @@ /* Context Menus */ .monaco-workbench .monaco-menu .monaco-action-bar.vertical { border-radius: var(--radius-lg); -} - -/* Use ::before for backdrop-filter so it doesn't create a containing block - for position:fixed submenu children (which would clip them via overflow:hidden). */ -.monaco-workbench .monaco-menu .monaco-action-bar.vertical::before { - content: ''; - position: absolute; - inset: 0; border-radius: inherit; - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); - z-index: -1; } .monaco-workbench .context-view .monaco-menu { From c0c8316db87374fbba2df8951d5842c4c0620446 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 11 Feb 2026 15:06:40 +0000 Subject: [PATCH 15/72] Remove redundant border-radius inheritance from vertical action bars in context menus --- extensions/theme-2026/themes/styles.css | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index da8961d2042..7c46dad4509 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -284,7 +284,6 @@ /* Context Menus */ .monaco-workbench .monaco-menu .monaco-action-bar.vertical { border-radius: var(--radius-lg); - border-radius: inherit; } .monaco-workbench .context-view .monaco-menu { From 66bcaa0aea629c73b0f08cd497098856efffa943 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 11 Feb 2026 12:31:21 -0500 Subject: [PATCH 16/72] Ensure gulp min writes the ISO date file (#294615) --- build/gulpfile.vscode.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 92ae01fd01c..19504aaf7c7 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -15,7 +15,7 @@ import electron from '@vscode/gulp-electron'; import jsonEditor from 'gulp-json-editor'; import * as util from './lib/util.ts'; import { getVersion } from './lib/getVersion.ts'; -import { readISODate } from './lib/date.ts'; +import { readISODate, writeISODate } from './lib/date.ts'; import * as task from './lib/task.ts'; import buildfile from './buildfile.ts'; import * as optimize from './lib/optimize.ts'; @@ -253,6 +253,7 @@ const coreCIEsbuild = task.define('core-ci-esbuild', task.series( cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask, + writeISODate('out-build'), // Type-check with tsgo (no emit) task.define('tsgo-typecheck', () => runTsGoTypeCheck()), // Transpile individual files to out-build first (for unit tests) @@ -638,6 +639,7 @@ BUILD_TARGETS.forEach(buildTarget => { cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask, + writeISODate('out-build'), esbuildBundleTask, vscodeTaskCI )); From 1628f21c8fcabb29a5166d0c640f52f6d2118944 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 11 Feb 2026 11:32:32 -0600 Subject: [PATCH 17/72] make chat tip screen reader friendly (#294205) --- .../browser/actions/chatAccessibilityHelp.ts | 1 + .../chat/browser/actions/chatActions.ts | 31 +++++++++++++++++ src/vs/workbench/contrib/chat/browser/chat.ts | 6 ++++ .../chatContentParts/chatTipContentPart.ts | 33 +++++++++++++++++-- .../chat/browser/widget/chatListRenderer.ts | 25 ++++++++++++++ .../chat/browser/widget/chatListWidget.ts | 15 +++++++++ .../contrib/chat/browser/widget/chatWidget.ts | 9 +++++ .../chat/common/actions/chatContextKeys.ts | 1 + 8 files changed, 119 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 4a6166dceea..3c23b729b2a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -88,6 +88,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'age content.push(localize('chat.focusMostRecentTerminal', 'To focus the last chat terminal that ran a tool, invoke the Focus Most Recent Chat Terminal command{0}.', ``)); content.push(localize('chat.focusMostRecentTerminalOutput', 'To focus the output from the last chat terminal tool, invoke the Focus Most Recent Chat Terminal Output command{0}.', ``)); content.push(localize('chat.focusQuestionCarousel', 'When a chat question appears, toggle focus between the question and the chat input{0}.', '')); + content.push(localize('chat.focusTip', 'When a tip appears, toggle focus between the tip and the chat input{0}.', '')); } if (type === 'editsView' || type === 'agentView') { if (type === 'agentView') { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 980ba420f12..dc57a988509 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -809,6 +809,37 @@ export function registerChatActions() { } }); + registerAction2(class FocusTipAction extends Action2 { + static readonly ID = 'workbench.action.chat.focusTip'; + + constructor() { + super({ + id: FocusTipAction.ID, + title: localize2('interactiveSession.focusTip.label', "Chat: Toggle Focus Between Tip and Input"), + category: CHAT_CATEGORY, + f1: true, + precondition: ChatContextKeys.inChatSession, + keybinding: [{ + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Slash, + when: ContextKeyExpr.or( + ChatContextKeys.inChatSession, + ChatContextKeys.inChatTip + ), + }] + }); + } + + run(accessor: ServicesAccessor): void { + const widgetService = accessor.get(IChatWidgetService); + const widget = widgetService.lastFocusedWidget; + + if (!widget || !widget.toggleTipFocus()) { + alert(localize('chat.tip.focusUnavailable', "No chat tip.")); + } + } + }); + registerAction2(class ShowContextUsageAction extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 2d2b307f23e..fc4423e96d1 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -386,6 +386,12 @@ export interface IChatWidget { * @returns Whether the operation succeeded (i.e., the focus was toggled). */ toggleQuestionCarouselFocus(): boolean; + /** + * Toggles focus between the tip widget and the chat input. + * Returns false if no tip is visible. + * @returns Whether the operation succeeded (i.e., the focus was toggled). + */ + toggleTipFocus(): boolean; hasInputFocus(): boolean; getModeRequestOptions(): Partial; getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts index df83eb5d194..7a5ba17dcaa 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts @@ -6,17 +6,19 @@ import './media/chatTipContent.css'; import * as dom from '../../../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; +import { status } from '../../../../../../base/browser/ui/aria/aria.js'; import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; -import { localize2 } from '../../../../../../nls.js'; +import { localize, localize2 } from '../../../../../../nls.js'; import { getFlatContextMenuActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; import { IChatTip, IChatTipService } from '../../chatTipService.js'; const $ = dom.$; @@ -29,6 +31,8 @@ export class ChatTipContentPart extends Disposable { private readonly _renderedContent = this._register(new MutableDisposable()); + private readonly _inChatTipContextKey: IContextKey; + constructor( tip: IChatTip, private readonly _renderer: IMarkdownRenderer, @@ -41,6 +45,16 @@ export class ChatTipContentPart extends Disposable { super(); this.domNode = $('.chat-tip-widget'); + this.domNode.tabIndex = 0; + this.domNode.setAttribute('role', 'region'); + this.domNode.setAttribute('aria-roledescription', localize('chatTipRoleDescription', "tip")); + + this._inChatTipContextKey = ChatContextKeys.inChatTip.bindTo(this._contextKeyService); + const focusTracker = this._register(dom.trackFocus(this.domNode)); + this._register(focusTracker.onDidFocus(() => this._inChatTipContextKey.set(true))); + this._register(focusTracker.onDidBlur(() => this._inChatTipContextKey.set(false))); + this._register({ dispose: () => this._inChatTipContextKey.reset() }); + this._renderTip(tip); this._register(this._chatTipService.onDidDismissTip(() => { @@ -69,12 +83,27 @@ export class ChatTipContentPart extends Disposable { })); } + hasFocus(): boolean { + return dom.isAncestorOfActiveElement(this.domNode); + } + + focus(): void { + this.domNode.focus(); + } + private _renderTip(tip: IChatTip): void { dom.clearNode(this.domNode); this.domNode.appendChild(renderIcon(Codicon.lightbulb)); const markdownContent = this._renderer.render(tip.content); this._renderedContent.value = markdownContent; this.domNode.appendChild(markdownContent.element); + const textContent = markdownContent.element.textContent ?? localize('chatTip', "Chat tip"); + const hasLink = /\[.*?\]\(.*?\)/.test(tip.content.value); + const ariaLabel = hasLink + ? localize('chatTipWithAction', "{0} Tab to the action.", textContent) + : textContent; + this.domNode.setAttribute('aria-label', ariaLabel); + status(ariaLabel); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 57267238df8..c533f527c54 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -184,6 +184,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer>(); + private _activeTipPart: ChatTipContentPart | undefined; + private readonly _notifiedQuestionCarousels = new WeakSet(); private readonly _questionCarouselToast = this._register(new DisposableStore()); @@ -301,6 +303,18 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.chatTipService.getNextTip(element.id, element.timestamp, this.contextKeyService), ); templateData.value.appendChild(tipPart.domNode); + this._activeTipPart = tipPart; templateData.elementDisposables.add(tipPart); templateData.elementDisposables.add(tipPart.onDidHide(() => { tipPart.domNode.remove(); + if (this._activeTipPart === tipPart) { + this._activeTipPart = undefined; + } })); + templateData.elementDisposables.add({ + dispose: () => { + if (this._activeTipPart === tipPart) { + this._activeTipPart = undefined; + } + } + }); } let inlineSlashCommandRendered = false; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts index 4e33ef0c176..50b40d59419 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts @@ -789,6 +789,21 @@ export class ChatListWidget extends Disposable { return this._renderer.editorsInUse(); } + /** + * Whether the active tip currently has focus. + */ + hasTipFocus(): boolean { + return this._renderer.hasTipFocus(); + } + + /** + * Focus the active tip, if any. + * @returns Whether a tip was focused. + */ + focusTip(): boolean { + return this._renderer.focusTip(); + } + /** * Get template data for a request ID. */ diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 8fa0fbb402f..13c0fb84ae1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -742,6 +742,15 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.input.focusQuestionCarousel(); } + toggleTipFocus(): boolean { + if (this.listWidget.hasTipFocus()) { + this.focusInput(); + return true; + } + + return this.listWidget.focusTip(); + } + hasInputFocus(): boolean { return this.input.hasFocus(); } diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index cb507daf273..94bb20e75af 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -41,6 +41,7 @@ export namespace ChatContextKeys { export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); export const inChatEditor = new RawContextKey('inChatEditor', false, { type: 'boolean', description: localize('inChatEditor', "Whether focus is in a chat editor.") }); export const inChatTodoList = new RawContextKey('inChatTodoList', false, { type: 'boolean', description: localize('inChatTodoList', "True when focus is in the chat todo list.") }); + export const inChatTip = new RawContextKey('inChatTip', false, { type: 'boolean', description: localize('inChatTip', "True when focus is in a chat tip.") }); export const inChatTerminalToolOutput = new RawContextKey('inChatTerminalToolOutput', false, { type: 'boolean', description: localize('inChatTerminalToolOutput', "True when focus is in the chat terminal output region.") }); export const chatModeKind = new RawContextKey('chatAgentKind', ChatModeKind.Ask, { type: 'string', description: localize('agentKind', "The 'kind' of the current agent.") }); export const chatModeName = new RawContextKey('chatModeName', '', { type: 'string', description: localize('chatModeName', "The name of the current chat mode (e.g. 'Plan' for custom modes).") }); From f179dbde128627f2091c9f49e4171c3ae3396a22 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 11 Feb 2026 18:37:27 +0100 Subject: [PATCH 18/72] fix: resolve watcherPath definition for compatibility with ES modules (#294625) --- build/lib/watch/watch-win32.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/lib/watch/watch-win32.ts b/build/lib/watch/watch-win32.ts index 12b8ffc0ac3..5b5a1197762 100644 --- a/build/lib/watch/watch-win32.ts +++ b/build/lib/watch/watch-win32.ts @@ -10,8 +10,9 @@ import File from 'vinyl'; import es from 'event-stream'; import filter from 'gulp-filter'; import { Stream } from 'stream'; +import { fileURLToPath } from 'url'; -const watcherPath = path.join(import.meta.dirname, 'watcher.exe'); +const watcherPath = path.join(typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)), 'watcher.exe'); function toChangeType(type: '0' | '1' | '2'): 'change' | 'add' | 'unlink' { switch (type) { From 95fb438758c10100e55ff1832f372c893d773753 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:50:19 +0000 Subject: [PATCH 19/72] Maintain chat accessible view on queued requests (#293936) --- .../chatResponseAccessibleView.ts | 13 ++++- .../chatResponseAccessibleView.test.ts | 51 ++++++++++++++++++- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts index 1f88332b739..0cd00519f0e 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts @@ -39,8 +39,17 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplementation } const verifiedWidget: IChatWidget = widget; - const focusedItem = verifiedWidget.getFocus(); - if (!focusedItem) { + let focusedItem = verifiedWidget.getFocus(); + if (!focusedItem || !isResponseVM(focusedItem)) { + const responseItems = verifiedWidget.viewModel?.getItems().filter(isResponseVM); + const lastResponse = responseItems?.at(-1); + if (lastResponse) { + focusedItem = lastResponse; + verifiedWidget.focus(lastResponse); + } + } + + if (!focusedItem || !isResponseVM(focusedItem)) { return; } diff --git a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts index 9aca92e5856..d756c702087 100644 --- a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts @@ -4,15 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { Event } from '../../../../../../base/common/event.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { URI } from '../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { Location } from '../../../../../../editor/common/languages.js'; -import { getToolSpecificDataDescription, getResultDetailsDescription, getToolInvocationA11yDescription } from '../../../browser/accessibility/chatResponseAccessibleView.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ChatResponseAccessibleView, getToolSpecificDataDescription, getResultDetailsDescription, getToolInvocationA11yDescription } from '../../../browser/accessibility/chatResponseAccessibleView.js'; +import { IChatWidget, IChatWidgetService } from '../../../browser/chat.js'; import { IChatExtensionsContent, IChatPullRequestContent, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolResourcesInvocationData } from '../../../common/chatService/chatService.js'; suite('ChatResponseAccessibleView', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const store = ensureNoDisposablesAreLeakedInTestSuite(); suite('getToolSpecificDataDescription', () => { test('returns empty string for undefined', () => { @@ -388,4 +392,47 @@ suite('ChatResponseAccessibleView', () => { assert.ok(result.includes('test.ts')); }); }); + + suite('getProvider', () => { + test('prefers the latest response when focus is on a queued request', () => { + const instantiationService = store.add(new TestInstantiationService()); + const responseItem = { + response: { value: [{ kind: 'markdownContent', content: new MarkdownString('Response content') }] }, + model: { onDidChange: Event.None }, + setVote: () => undefined + }; + const queuedRequest = { message: 'Queued request' }; + const items = [responseItem, queuedRequest]; + let focusedItem: unknown = queuedRequest; + + const widget = { + hasInputFocus: () => true, + focusResponseItem: () => { focusedItem = queuedRequest; }, + getFocus: () => focusedItem, + focus: (item: unknown) => { focusedItem = item; }, + viewModel: { getItems: () => items } + } as unknown as IChatWidget; + + const widgetService = { + _serviceBrand: undefined, + lastFocusedWidget: widget, + onDidAddWidget: Event.None, + onDidBackgroundSession: Event.None, + reveal: async () => true, + revealWidget: async () => widget, + getAllWidgets: () => [widget], + getWidgetByInputUri: () => widget, + openSession: async () => widget, + getWidgetBySessionResource: () => widget + } as unknown as IChatWidgetService; + + instantiationService.stub(IChatWidgetService, widgetService); + + const accessibleView = new ChatResponseAccessibleView(); + const provider = instantiationService.invokeFunction(accessor => accessibleView.getProvider(accessor)); + assert.ok(provider); + store.add(provider); + assert.ok(provider.provideContent().includes('Response content')); + }); + }); }); From 53b52ea8bb64086e754bbdf03190efe3ed00fb07 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 11 Feb 2026 12:08:11 -0600 Subject: [PATCH 20/72] tweak tips (#294607) --- src/vs/workbench/contrib/chat/browser/chatTipService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 5c55c8159ad..08f1c161b3a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -164,13 +164,13 @@ const TIP_CATALOG: ITipDefinition[] = [ }, { id: 'tip.messageQueueing', - message: localize('tip.messageQueueing', "Tip: You can send follow-up messages and steering while the agent is working. They'll be queued and processed in order."), + message: localize('tip.messageQueueing', "Tip: You can send follow-up and steering messages while the agent is working. They'll be queued and processed in order."), when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), excludeWhenCommandsExecuted: ['workbench.action.chat.queueMessage', 'workbench.action.chat.steerWithMessage'], }, { id: 'tip.yoloMode', - message: localize('tip.yoloMode', "Tip: Enable [auto approval](command:workbench.action.openSettings?%5B%22chat.tools.global.autoApprove%22%5D) to let the agent run tools without manual confirmation."), + message: localize('tip.yoloMode', "Tip: Enable [auto approve](command:workbench.action.openSettings?%5B%22chat.tools.global.autoApprove%22%5D) to let the agent run tools without manual confirmation."), when: ContextKeyExpr.and( ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), ContextKeyExpr.notEquals('config.chat.tools.global.autoApprove', true), From fe023c8dde1ba2ed24687994b9e2b42cb16d939c Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:09:02 +0100 Subject: [PATCH 21/72] Fix contributed chat context issues found by CCR (#294622) --- src/vs/workbench/api/common/extHostChatContext.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatContext.ts b/src/vs/workbench/api/common/extHostChatContext.ts index f0c6509f8a5..74a9fab7e5f 100644 --- a/src/vs/workbench/api/common/extHostChatContext.ts +++ b/src/vs/workbench/api/common/extHostChatContext.ts @@ -50,7 +50,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext throw new Error('Workspace context provider not found'); } const provider = entry.provider as vscode.ChatWorkspaceContextProvider; - const result = (await provider.provideWorkspaceChatContext(token)) ?? (await provider.provideChatContext?.(token)) ?? []; + const result = (await provider.provideWorkspaceChatContext?.(token)) ?? (await provider.provideChatContext?.(token)) ?? []; return this._convertItems(handle, result); } @@ -63,7 +63,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext throw new Error('Explicit context provider not found'); } const provider = entry.provider as vscode.ChatExplicitContextProvider; - const result = (await provider.provideExplicitChatContext(token)) ?? (await provider.provideChatContext?.(token)) ?? []; + const result = (await provider.provideExplicitChatContext?.(token)) ?? (await provider.provideChatContext?.(token)) ?? []; return this._convertItems(handle, result); } @@ -77,7 +77,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (!extItem) { throw new Error('Chat context item not found'); } - return this._doResolve((provider.resolveExplicitChatContext ?? provider.resolveChatContext).bind(provider), context, extItem, token); + return this._doResolve((provider.resolveExplicitChatContext ?? provider.resolveChatContext)?.bind(provider), context, extItem, token); } // Resource context provider methods @@ -89,7 +89,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext } const provider = entry.provider as vscode.ChatResourceContextProvider; - const result = (await provider.provideResourceChatContext({ resource: URI.revive(options.resource) }, token)) ?? (await provider.provideChatContext?.({ resource: URI.revive(options.resource) }, token)); + const result = (await provider.provideResourceChatContext?.({ resource: URI.revive(options.resource) }, token)) ?? (await provider.provideChatContext?.({ resource: URI.revive(options.resource) }, token)); if (!result) { return undefined; } @@ -109,7 +109,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext command: result.command ? { id: result.command.command } : undefined }; if (options.withValue && !item.value) { - const resolved = await (provider.resolveResourceChatContext ?? provider.resolveChatContext).bind(provider)(result, token); + const resolved = await (provider.resolveResourceChatContext ?? provider.resolveChatContext)?.bind(provider)(result, token); item.value = resolved?.value; item.tooltip = resolved?.tooltip ? MarkdownString.from(resolved.tooltip) : item.tooltip; } @@ -127,7 +127,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (!extItem) { throw new Error('Chat context item not found'); } - return this._doResolve((provider.resolveResourceChatContext ?? provider.resolveChatContext).bind(provider), context, extItem, token); + return this._doResolve((provider.resolveResourceChatContext ?? provider.resolveChatContext)?.bind(provider), context, extItem, token); } // Command execution @@ -315,7 +315,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext return; } const provideWorkspaceContext = async () => { - const workspaceContexts = await provider.provideWorkspaceChatContext(CancellationToken.None); + const workspaceContexts = (await provider.provideWorkspaceChatContext?.(CancellationToken.None) ?? await provider.provideChatContext?.(CancellationToken.None)); const resolvedContexts = this._convertItems(handle, workspaceContexts ?? []); return this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts); }; From bd0b2d369cf0866fe0b5c3acccc2c609c25280cb Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 11 Feb 2026 13:09:17 -0500 Subject: [PATCH 22/72] Fix a few rendering issues with the chat context widget (#294630) --- .../browser/widgetHosts/viewPane/chatContextUsageWidget.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts index 422e3a3e905..aa6c0db52aa 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts @@ -117,6 +117,7 @@ export class ChatContextUsageWidget extends Disposable { private currentData: IChatContextUsageData | undefined; private static readonly _OPENED_STORAGE_KEY = 'chat.contextUsage.hasBeenOpened'; + private static readonly _HOVER_ID = 'chat.contextUsage'; private readonly _contextUsageOpenedKey: IContextKey; @@ -170,6 +171,7 @@ export class ChatContextUsageWidget extends Disposable { } private readonly _hoverOptions: Omit = { + id: ChatContextUsageWidget._HOVER_ID, appearance: { showPointer: true, compact: true }, persistence: { hideOnHover: false }, trapFocus: true @@ -179,7 +181,9 @@ export class ChatContextUsageWidget extends Disposable { if (!this._isVisible.get() || !this.currentData) { return undefined; } - this._contextUsageDetails.value = this.instantiationService.createInstance(ChatContextUsageDetails); + if (!this._contextUsageDetails.value) { + this._contextUsageDetails.value = this.instantiationService.createInstance(ChatContextUsageDetails); + } this._contextUsageDetails.value.update(this.currentData); return this._contextUsageDetails.value; } From abfdc25d69c70195d5552f33d910f538f235904b Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:27:28 -0800 Subject: [PATCH 23/72] Fix welcome page session grid layout to use ITEM_HEIGHT (#294309) Fix welcome page session grid layout to match ITEM_HEIGHT (44px) --- .../agentSessions/media/agentsessionsviewer.css | 2 +- .../browser/agentSessionsWelcome.ts | 9 +++++---- .../browser/media/agentSessionsWelcome.css | 14 +++++++------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 2bd4108b727..5db250de5b3 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -9,7 +9,7 @@ height: 100%; min-height: 0; - .monaco-scrollable-element { + .pane-body & .monaco-scrollable-element { padding: 0 6px; } diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 6801e3c8237..cebab4bb5db 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -47,6 +47,7 @@ import { ChatSessionPosition, getResourceForNewChatSession } from '../../chat/br import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; import { AgentSessionsControl, IAgentSessionsControlOptions } from '../../chat/browser/agentSessions/agentSessionsControl.js'; import { AgentSessionsFilter } from '../../chat/browser/agentSessions/agentSessionsFilter.js'; +import { AgentSessionsListDelegate } from '../../chat/browser/agentSessions/agentSessionsViewer.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { IResolvedWalkthrough, IWalkthroughsService } from '../../welcomeGettingStarted/browser/gettingStartedService.js'; import { GettingStartedEditorOptions, GettingStartedInput } from '../../welcomeGettingStarted/browser/gettingStartedInput.js'; @@ -815,19 +816,19 @@ export class AgentSessionsWelcomePage extends EditorPane { // TODO: @osortega this is a weird way of doing this, maybe we handle the 2-colum layout in the control itself? const sessionsWidth = Math.min(800, this.lastDimension.width - 80); // Calculate height based on actual visible sessions (capped at MAX_SESSIONS) - // Use 54px per item from AgentSessionsListDelegate.ITEM_HEIGHT + // Use ITEM_HEIGHT per item from AgentSessionsListDelegate // Give the list FULL height so virtualization renders all items // CSS transforms handle the 2-column visual layout const visibleSessions = Math.min( this.agentSessionsService.model.sessions.filter(s => !s.isArchived()).length, MAX_SESSIONS ); - const sessionsHeight = visibleSessions * 56; + const sessionsHeight = visibleSessions * AgentSessionsListDelegate.ITEM_HEIGHT; this.sessionsControl.layout(sessionsHeight, sessionsWidth); // Set margin offset for 2-column layout: actual height - visual height - // Visual height = ceil(n/2) * 52, so offset = floor(n/2) * 52 - const marginOffset = Math.floor(visibleSessions / 2) * 52; + // Visual height = ceil(n/2) * ITEM_HEIGHT, so offset = floor(n/2) * ITEM_HEIGHT + const marginOffset = Math.floor(visibleSessions / 2) * AgentSessionsListDelegate.ITEM_HEIGHT; this.sessionsControl.element!.style.marginBottom = `-${marginOffset}px`; } diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css index 3021512101f..edc3e1d193d 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/media/agentSessionsWelcome.css @@ -168,31 +168,31 @@ * Each pair forms a visual row. * Left column items need to move up by floor((index-1)/2) rows * Right column items need to move right and up by (index/2) rows - * Row height is 52px. + * Row height is 44px. */ /* Left column items (odd positions): move up to form 2-column layout */ /* Item 3: move up 1 row */ .agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(3) { - transform: translateY(-52px); + transform: translateY(-44px); } /* Item 5: move up 2 rows */ .agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(5) { - transform: translateY(-104px); + transform: translateY(-88px); } /* Right column items (even positions): move right and up */ /* Item 2: move right, up 1 row */ .agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(2) { - transform: translateX(100%) translateY(-52px); + transform: translateX(100%) translateY(-44px); } /* Item 4: move right, up 2 rows */ .agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(4) { - transform: translateX(100%) translateY(-104px); + transform: translateX(100%) translateY(-88px); } /* Item 6: move right, up 3 rows */ .agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(6) { - transform: translateX(100%) translateY(-156px); + transform: translateX(100%) translateY(-132px); } /* Style individual session items in the welcome page */ @@ -482,7 +482,7 @@ gap: 12px; padding: 8px 12px 8px 8px; border-radius: 4px; - height: 52px; + height: 44px; box-sizing: border-box; } From 04c6ff22be8ab268ab2e6c2de025337b5789ab7a Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:27:39 -0800 Subject: [PATCH 24/72] More verbose conpty, winpty related error message (#294193) --- src/vs/platform/terminal/node/terminalProcess.ts | 6 +++++- .../contrib/terminal/test/browser/terminalInstance.test.ts | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 968009e20e6..707bfa7fc0f 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -244,7 +244,11 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return undefined; } catch (err) { this._logService.trace('node-pty.node-pty.IPty#spawn native exception', err); - return { message: `A native exception occurred during launch (${err.message})` }; + const errorMessage = err.message; + if (errorMessage?.includes('Cannot launch conpty')) { + return { message: localize('conptyLaunchFailed', "A native exception occurred during launch (Cannot launch conpty). Winpty has been removed, see {0} for more details. You can also try enabling the `{1}` setting.", 'https://code.visualstudio.com/updates/v1_109#_removal-of-winpty-support', 'terminal.integrated.windowsUseConptyDll') }; + } + return { message: `A native exception occurred during launch (${errorMessage})` }; } } diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts index 81bc2516e21..0eb0cf84c48 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts @@ -288,6 +288,12 @@ suite('Workbench - TerminalInstance', () => { { code: 1260, message: `The terminal process failed to launch: Windows cannot open this program because it has been prevented by a software restriction policy. For more information, open Event Viewer or contact your system Administrator.` } ); }); + test('should format conpty launch failure', () => { + deepStrictEqual( + parseExitResult({ message: 'A native exception occurred during launch (Cannot launch conpty). Winpty has been removed, see https://code.visualstudio.com/updates/v1_109#_removal-of-winpty-support for more details. You can also try enabling the `terminal.integrated.windowsUseConptyDll` setting.' }, {}, ProcessState.KilledDuringLaunch, undefined), + { code: undefined, message: `The terminal process failed to launch: A native exception occurred during launch (Cannot launch conpty). Winpty has been removed, see https://code.visualstudio.com/updates/v1_109#_removal-of-winpty-support for more details. You can also try enabling the \`terminal.integrated.windowsUseConptyDll\` setting..` } + ); + }); test('should format generic failures', () => { deepStrictEqual( parseExitResult({ code: 123, message: 'A native exception occurred during launch (Cannot create process, error code: 123)' }, {}, ProcessState.KilledDuringLaunch, undefined), From 9b64d3b8aeba845e38595076949b7515fc48f880 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:38:21 -0800 Subject: [PATCH 25/72] Use sequence as default terminal tab title This aligns us with how most terminals use this information. It is a pretty big change though and I expect some users to not be happy with the new behavior. The user can always go back to the old way, but this way is technically more correct and there's really good reason to make the change now since agentic CLIs use the title to signal progress or required user confirmations. Fixes #291275 --- .../workbench/contrib/terminal/common/terminalConfiguration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 87c3d392b20..046f3dcc712 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -342,7 +342,7 @@ const terminalConfiguration: IStringDictionary = { }, [TerminalSettingId.TerminalTitle]: { 'type': 'string', - 'default': '${process}', + 'default': '${sequence}', 'markdownDescription': terminalTitle }, [TerminalSettingId.TerminalDescription]: { From de73ae31fe01948f6c781439138f8245cc4c9657 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:43:21 -0800 Subject: [PATCH 26/72] feat: remove workbench.settings.scrollBehavior setting (#294641) * chore: add experimental tag * feat: remove workbench.settings.scrollBehavior setting --- .../preferences/browser/settingsEditor2.ts | 156 ++---------------- .../common/preferencesContribution.ts | 11 -- 2 files changed, 15 insertions(+), 152 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 9a33bfea060..4c7e17c9cc3 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -93,13 +93,8 @@ export function createGroupIterator(group: SettingsTreeGroupElement): Iterable(SCROLL_BEHAVIOR_KEY); - if (this.searchResultModel || scrollBehavior === 'paginated') { - // In search mode or paginated mode, filter to show only the selected category - if (this.viewState.categoryFilter !== element) { - this.viewState.categoryFilter = element ?? undefined; - // Force render in this case, because - // onDidClickSetting relies on the updated view. - this.renderTree(undefined, true); - this.settingsTree.scrollTop = 0; - } - } else { - // In continuous mode, clear any category filter that may have been set in paginated mode - if (this.viewState.categoryFilter) { - this.viewState.categoryFilter = undefined; - this.renderTree(undefined, true); - } - if (element && (!e.browserEvent || !(e.browserEvent).fromScroll)) { - let targetElement = element; - // Searches equivalent old Object currently living in the Tree nodes. - if (!this.settingsTree.hasElement(targetElement)) { - if (element instanceof SettingsTreeGroupElement) { - const targetId = element.id; - const findInViewNodes = (nodes: any[]): SettingsTreeGroupElement | undefined => { - for (const node of nodes) { - if (node.element instanceof SettingsTreeGroupElement && node.element.id === targetId) { - return node.element; - } - if (node.children && node.children.length > 0) { - const found = findInViewNodes(node.children); - if (found) { - return found; - } - } - } - return undefined; - }; - - try { - const rootNode = this.settingsTree.getNode(null); - if (rootNode && rootNode.children) { - const foundOldElement = findInViewNodes(rootNode.children); - if (foundOldElement) { - // Now we don't reveal the New Object, reveal the Old Object" - targetElement = foundOldElement; - } - } - } catch (err) { - // Tree might be in an invalid state, ignore - } - } - } - - if (this.settingsTree.hasElement(targetElement)) { - this.settingsTree.reveal(targetElement, 0); - this.settingsTree.setFocus([targetElement]); - } - } + // Filter to show only the selected category + if (this.viewState.categoryFilter !== element) { + this.viewState.categoryFilter = element ?? undefined; + // Force render in this case, because + // onDidClickSetting relies on the updated view. + this.renderTree(undefined, true); + this.settingsTree.scrollTop = 0; } })); @@ -1263,74 +1208,6 @@ export class SettingsEditor2 extends EditorPane { private updateTreeScrollSync(): void { this.settingRenderers.cancelSuggesters(); - if (this.searchResultModel) { - return; - } - - // In paginated mode, we don't sync scroll position since categories are filtered - const scrollBehavior = this.configurationService.getValue<'paginated' | 'continuous'>(SCROLL_BEHAVIOR_KEY); - if (scrollBehavior === 'paginated') { - return; - } - - if (!this.tocTreeModel) { - return; - } - - const elementToSync = this.settingsTree.firstVisibleElement; - const element = elementToSync instanceof SettingsTreeSettingElement ? elementToSync.parent : - elementToSync instanceof SettingsTreeGroupElement ? elementToSync : - null; - - // It's possible for this to be called when the TOC and settings tree are out of sync - e.g. when the settings tree has deferred a refresh because - // it is focused. So, bail if element doesn't exist in the TOC. - let nodeExists = true; - try { this.tocTree.getNode(element); } catch (e) { nodeExists = false; } - if (!nodeExists) { - return; - } - - if (element && this.tocTree.getSelection()[0] !== element) { - const ancestors = this.getAncestors(element); - ancestors.forEach(e => this.tocTree.expand(e)); - - this.tocTree.reveal(element); - const elementTop = this.tocTree.getRelativeTop(element); - if (typeof elementTop !== 'number') { - return; - } - - this.tocTree.collapseAll(); - - ancestors.forEach(e => this.tocTree.expand(e)); - if (elementTop < 0 || elementTop > 1) { - this.tocTree.reveal(element); - } else { - this.tocTree.reveal(element, elementTop); - } - - this.tocTree.expand(element); - - this.tocTree.setSelection([element]); - - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - (fakeKeyboardEvent).fromScroll = true; - this.tocTree.setFocus([element], fakeKeyboardEvent); - } - } - - private getAncestors(element: SettingsTreeElement): SettingsTreeElement[] { - const ancestors: SettingsTreeElement[] = []; - - while (element.parent) { - if (element.parent.id !== 'root') { - ancestors.push(element.parent); - } - - element = element.parent; - } - - return ancestors.reverse(); } private updateChangedSetting(key: string, value: unknown, manualReset: boolean, languageFilter: string | undefined, scope: ConfigurationScope | undefined): Promise { @@ -1710,17 +1587,14 @@ export class SettingsEditor2 extends EditorPane { } else { this.refreshTOCTree(); - // In paginated mode, set initial category to the first one (Commonly Used) - const scrollBehavior = this.configurationService.getValue<'paginated' | 'continuous'>(SCROLL_BEHAVIOR_KEY); - if (scrollBehavior === 'paginated') { - const rootChildren = this.settingsTreeModel.value.root.children; - if (Array.isArray(rootChildren) && rootChildren.length > 0) { - const firstCategory = rootChildren[0]; - if (firstCategory instanceof SettingsTreeGroupElement) { - this.viewState.categoryFilter = firstCategory; - this.tocTree.setFocus([firstCategory]); - this.tocTree.setSelection([firstCategory]); - } + // Set initial category to the first one (Commonly Used) + const rootChildren = this.settingsTreeModel.value.root.children; + if (Array.isArray(rootChildren) && rootChildren.length > 0) { + const firstCategory = rootChildren[0]; + if (firstCategory instanceof SettingsTreeGroupElement) { + this.viewState.categoryFilter = firstCategory; + this.tocTree.setFocus([firstCategory]); + this.tocTree.setSelection([firstCategory]); } } diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index 1b1e55f4529..e5cec6f206a 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -125,17 +125,6 @@ registry.registerConfiguration({ 'description': nls.localize('settingsSearchTocBehavior', "Controls the behavior of the Settings editor Table of Contents while searching. If this setting is being changed in the Settings editor, the setting will take effect after the search query is modified."), 'default': 'filter', 'scope': ConfigurationScope.WINDOW - }, - 'workbench.settings.scrollBehavior': { - 'type': 'string', - 'enum': ['paginated', 'continuous'], - 'enumDescriptions': [ - nls.localize('settingsScrollBehavior.paginated', "Show only settings from the selected category. Clicking a category in the Table of Contents filters the view to that category."), - nls.localize('settingsScrollBehavior.continuous', "Show all settings in a continuous scrolling list. Clicking a category scrolls to that section."), - ], - 'description': nls.localize('settingsScrollBehavior', "Controls whether the Settings editor shows one category at a time (paginated) or allows continuous scrolling through all settings."), - 'default': 'paginated', - 'scope': ConfigurationScope.WINDOW } } }); From 8c3168c967e0100348cb498bd5e770344821151c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:55:32 -0800 Subject: [PATCH 27/72] Enable kitty keyboard protocol by default --- .../contrib/terminal/common/terminalConfiguration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 87c3d392b20..43925690a4a 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -589,9 +589,9 @@ const terminalConfiguration: IStringDictionary = { }, [TerminalSettingId.EnableKittyKeyboardProtocol]: { restricted: true, - markdownDescription: localize('terminal.integrated.enableKittyKeyboardProtocol', "Whether to enable the kitty keyboard protocol, which provides more detailed keyboard input reporting to the terminal."), + markdownDescription: localize('terminal.integrated.enableKittyKeyboardProtocol', "Whether to enable the kitty keyboard protocol, which allows a program in the terminal to request more detailed keyboard input reporting. This can for example enable shift+enter to be handled by the program."), type: 'boolean', - default: false, + default: true, tags: ['experimental', 'advanced'], experiment: { mode: 'auto' From f02acde95c9f5faec630e4a016d6083d69461b68 Mon Sep 17 00:00:00 2001 From: Aaron Munger <2019016+amunger@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:00:18 -0800 Subject: [PATCH 28/72] track and dispose listener (#294643) --- src/vs/workbench/api/common/extHostNotebookKernels.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index 52eaf027866..eecdf625b00 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -150,6 +150,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { let _executeHandler = handler ?? _defaultExecutHandler; let _interruptHandler: ((this: vscode.NotebookController, notebook: vscode.NotebookDocument) => void | Thenable) | undefined; let _variableProvider: vscode.NotebookVariableProvider | undefined; + let _variableProviderDisposable: IDisposable | undefined; this._proxy.$addKernel(handle, data).catch(err => { // this can happen when a kernel with that ID is already registered @@ -234,9 +235,10 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { }, set variableProvider(value) { checkProposedApiEnabled(extension, 'notebookVariableProvider'); + _variableProviderDisposable?.dispose(); _variableProvider = value; data.hasVariableProvider = !!value; - value?.onDidChangeVariables(e => that._proxy.$variablesUpdated(e.uri)); + _variableProviderDisposable = value?.onDidChangeVariables(e => that._proxy.$variablesUpdated(e.uri)); _update(); }, get variableProvider() { @@ -270,6 +272,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { this._kernelData.delete(handle); onDidChangeSelection.dispose(); onDidReceiveMessage.dispose(); + _variableProviderDisposable?.dispose(); this._proxy.$removeKernel(handle); } }, From 742dd34ff5c58c772b70a91ffb743b2b366733d5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:02:01 -0800 Subject: [PATCH 29/72] Remove experiments code from kitty setting --- .../contrib/terminal/common/terminalConfiguration.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 43925690a4a..ad89c6a6179 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -592,10 +592,7 @@ const terminalConfiguration: IStringDictionary = { markdownDescription: localize('terminal.integrated.enableKittyKeyboardProtocol', "Whether to enable the kitty keyboard protocol, which allows a program in the terminal to request more detailed keyboard input reporting. This can for example enable shift+enter to be handled by the program."), type: 'boolean', default: true, - tags: ['experimental', 'advanced'], - experiment: { - mode: 'auto' - } + tags: ['advanced'] }, [TerminalSettingId.EnableWin32InputMode]: { restricted: true, From 98280cf5c34b3dd5082653f30a944f4ed0a059e4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:06:27 -0800 Subject: [PATCH 30/72] Update src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../workbench/contrib/terminal/common/terminalConfiguration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index ad89c6a6179..a99f9628499 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -589,7 +589,7 @@ const terminalConfiguration: IStringDictionary = { }, [TerminalSettingId.EnableKittyKeyboardProtocol]: { restricted: true, - markdownDescription: localize('terminal.integrated.enableKittyKeyboardProtocol', "Whether to enable the kitty keyboard protocol, which allows a program in the terminal to request more detailed keyboard input reporting. This can for example enable shift+enter to be handled by the program."), + markdownDescription: localize('terminal.integrated.enableKittyKeyboardProtocol', "Whether to enable the kitty keyboard protocol, which allows a program in the terminal to request more detailed keyboard input reporting. This can, for example, enable `Shift+Enter` to be handled by the program."), type: 'boolean', default: true, tags: ['advanced'] From 17ecf08cbf3e045360ffecb3901142ae637dac02 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 11 Feb 2026 11:15:44 -0800 Subject: [PATCH 31/72] Hook menu title rename (#294645) --- .../workbench/contrib/chat/browser/promptSyntax/hookActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts index babfb20a486..d59d0c04949 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts @@ -611,7 +611,7 @@ export async function showConfigureHooksQuickPick( const inputBox = inputDisposables.add(quickInputService.createInputBox()); inputBox.prompt = localize('commands.hook.filename.prompt', "Enter hook file name"); inputBox.placeholder = localize('commands.hook.filename.placeholder', "e.g., hooks, diagnostics, security"); - inputBox.title = localize('commands.hook.selectFolder.title', 'Hook File Location'); + inputBox.title = localize('commands.hook.filename.title', "Hook File Name"); inputBox.buttons = [backButton]; inputBox.ignoreFocusOut = true; From 375ea3d393098c03f9fa64a2bd9d1a539566df69 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 11 Feb 2026 14:22:01 -0500 Subject: [PATCH 32/72] Remove some legacy fallbacks (#294649) --- .../widget/chatContentParts/chatThinkingContentPart.ts | 5 +---- .../contrib/chat/browser/widget/input/chatInputPart.ts | 2 +- .../browser/tools/monitoring/outputMonitor.ts | 8 +------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 4f926a701d4..e3ddd1312ad 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -718,10 +718,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen const timeout = setTimeout(() => cts.cancel(), 5000); try { - let models = await this.languageModelsService.selectLanguageModels({ vendor: 'copilot', id: 'copilot-fast' }); - if (!models.length) { - models = await this.languageModelsService.selectLanguageModels({ vendor: 'copilot', family: 'gpt-4o-mini' }); - } + const models = await this.languageModelsService.selectLanguageModels({ vendor: 'copilot', id: 'copilot-fast' }); if (!models.length) { this.setFallbackTitle(); return; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index b0fd053396d..518c735c5d3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -665,7 +665,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private initSelectedModel() { const persistedSelection = this.storageService.get(this.getSelectedModelStorageKey(), StorageScope.APPLICATION); - const persistedAsDefault = this.storageService.getBoolean(this.getSelectedModelIsDefaultStorageKey(), StorageScope.APPLICATION, persistedSelection === 'copilot/gpt-4.1'); + const persistedAsDefault = this.storageService.getBoolean(this.getSelectedModelIsDefaultStorageKey(), StorageScope.APPLICATION, true); if (persistedSelection) { const model = this.getModels().find(m => m.identifier === persistedSelection); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts index 8c70eb496af..081534d5031 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts @@ -877,13 +877,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { } private async _getLanguageModel(): Promise { - let models = await this._languageModelsService.selectLanguageModels({ vendor: 'copilot', id: 'copilot-fast' }); - - // Fallback to gpt-4o-mini if copilot-fast is not available for backwards compatibility - if (!models.length) { - models = await this._languageModelsService.selectLanguageModels({ vendor: 'copilot', family: 'gpt-4o-mini' }); - } - + const models = await this._languageModelsService.selectLanguageModels({ vendor: 'copilot', id: 'copilot-fast' }); return models.length ? models[0] : undefined; } } From f60e8ae4b31f8efce27a773acd3b141d5e8d3241 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:54:32 -0800 Subject: [PATCH 33/72] Improve js/ts code lens settings descriptions For #292934 Adding some links and removing extra keywords --- .../typescript-language-features/package.json | 17 +++++++---------- .../package.nls.json | 8 ++++---- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 4cd0263a1a0..ecc66db7e38 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -209,7 +209,7 @@ "js/ts.referencesCodeLens.showOnAllFunctions": { "type": "boolean", "default": false, - "description": "%configuration.referencesCodeLens.showOnAllFunctions%", + "markdownDescription": "%configuration.referencesCodeLens.showOnAllFunctions%", "scope": "language-overridable", "tags": [ "JavaScript", @@ -219,14 +219,14 @@ "javascript.referencesCodeLens.showOnAllFunctions": { "type": "boolean", "default": false, - "description": "%configuration.referencesCodeLens.showOnAllFunctions%", + "markdownDescription": "%configuration.referencesCodeLens.showOnAllFunctions%", "markdownDeprecationMessage": "%configuration.referencesCodeLens.showOnAllFunctions.unifiedDeprecationMessage%", "scope": "window" }, "typescript.referencesCodeLens.showOnAllFunctions": { "type": "boolean", "default": false, - "description": "%configuration.referencesCodeLens.showOnAllFunctions%", + "markdownDescription": "%configuration.referencesCodeLens.showOnAllFunctions%", "markdownDeprecationMessage": "%configuration.referencesCodeLens.showOnAllFunctions.unifiedDeprecationMessage%", "scope": "window" }, @@ -236,7 +236,6 @@ "description": "%configuration.implementationsCodeLens.enabled%", "scope": "language-overridable", "tags": [ - "JavaScript", "TypeScript" ] }, @@ -250,34 +249,32 @@ "js/ts.implementationsCodeLens.showOnInterfaceMethods": { "type": "boolean", "default": false, - "description": "%configuration.implementationsCodeLens.showOnInterfaceMethods%", + "markdownDescription": "%configuration.implementationsCodeLens.showOnInterfaceMethods%", "scope": "language-overridable", "tags": [ - "JavaScript", "TypeScript" ] }, "typescript.implementationsCodeLens.showOnInterfaceMethods": { "type": "boolean", "default": false, - "description": "%configuration.implementationsCodeLens.showOnInterfaceMethods%", + "markdownDescription": "%configuration.implementationsCodeLens.showOnInterfaceMethods%", "markdownDeprecationMessage": "%configuration.implementationsCodeLens.showOnInterfaceMethods.unifiedDeprecationMessage%", "scope": "window" }, "js/ts.implementationsCodeLens.showOnAllClassMethods": { "type": "boolean", "default": false, - "description": "%configuration.implementationsCodeLens.showOnAllClassMethods%", + "markdownDescription": "%configuration.implementationsCodeLens.showOnAllClassMethods%", "scope": "language-overridable", "tags": [ - "JavaScript", "TypeScript" ] }, "typescript.implementationsCodeLens.showOnAllClassMethods": { "type": "boolean", "default": false, - "description": "%configuration.implementationsCodeLens.showOnAllClassMethods%", + "markdownDescription": "%configuration.implementationsCodeLens.showOnAllClassMethods%", "markdownDeprecationMessage": "%configuration.implementationsCodeLens.showOnAllClassMethods.unifiedDeprecationMessage%", "scope": "window" }, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index ed640ef85f0..536eab3ce03 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -51,13 +51,13 @@ "typescript.goToProjectConfig.title": "Go to Project Configuration (tsconfig)", "configuration.referencesCodeLens.enabled": "Enable/disable references CodeLens in JavaScript and TypeScript files. This CodeLens shows the number of references for classes and exported functions and allows you to peek or navigate to them.", "configuration.referencesCodeLens.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.referencesCodeLens.enabled#` instead.", - "configuration.referencesCodeLens.showOnAllFunctions": "Enable/disable the references CodeLens on all functions in JavaScript and TypeScript files.", + "configuration.referencesCodeLens.showOnAllFunctions": "Enable/disable the [references CodeLens](#js/ts.referencesCodeLens.enabled) on all functions in JavaScript and TypeScript files.", "configuration.referencesCodeLens.showOnAllFunctions.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.referencesCodeLens.showOnAllFunctions#` instead.", - "configuration.implementationsCodeLens.enabled": "Enable/disable implementations CodeLens in TypeScript files. This CodeLens shows the implementers of a TypeScript interface.", + "configuration.implementationsCodeLens.enabled": "Enable/disable implementations CodeLens in TypeScript files. This CodeLens shows the implementers of TypeScript interfaces.", "configuration.implementationsCodeLens.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.implementationsCodeLens.enabled#` instead.", - "configuration.implementationsCodeLens.showOnInterfaceMethods": "Enable/disable implementations CodeLens on TypeScript interface methods.", + "configuration.implementationsCodeLens.showOnInterfaceMethods": "Enable/disable [implementations CodeLens](#js/ts.implementationsCodeLens.enabled) on TypeScript interface methods.", "configuration.implementationsCodeLens.showOnInterfaceMethods.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.implementationsCodeLens.showOnInterfaceMethods#` instead.", - "configuration.implementationsCodeLens.showOnAllClassMethods": "Enable/disable showing implementations CodeLens above all TypeScript class methods instead of only on abstract methods.", + "configuration.implementationsCodeLens.showOnAllClassMethods": "Enable/disable showing [implementations CodeLens](#js/ts.implementationsCodeLens.enabled) above all TypeScript class methods instead of only on abstract methods.", "configuration.implementationsCodeLens.showOnAllClassMethods.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.implementationsCodeLens.showOnAllClassMethods#` instead.", "typescript.openTsServerLog.title": "Open TS Server log", "typescript.restartTsServer": "Restart TS Server", From 71e1f05e3eb2bf741a684d3329bfef3ffc521f35 Mon Sep 17 00:00:00 2001 From: Robo Date: Thu, 12 Feb 2026 05:11:55 +0900 Subject: [PATCH 34/72] fix: restore context menu for explicit win10 mode (#294661) --- build/win32/code.iss | 51 +++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/build/win32/code.iss b/build/win32/code.iss index 0d47e15103f..05c8465a17b 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -86,7 +86,7 @@ Type: files; Name: "{app}\updating_version" Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 Name: "addcontextmenufiles"; Description: "{cm:AddContextMenuFiles,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked -Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked; Check: not IsWindows11OrLater +Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked; Check: not ShouldUseWindows11ContextMenu Name: "associatewithfiles"; Description: "{cm:AssociateWithFiles,{#NameShort}}"; GroupDescription: "{cm:Other}" Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent @@ -105,8 +105,8 @@ Source: "bin\{#ApplicationName}.cmd"; DestDir: "{code:GetDestDir}\bin"; DestName Source: "bin\{#ApplicationName}"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirApplicationFilename}"; Flags: ignoreversion Source: "{#ProductJsonPath}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\resources\app"; Flags: ignoreversion #ifdef AppxPackageName -Source: "appx\{#AppxPackage}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\appx"; BeforeInstall: RemoveAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater -Source: "appx\{#AppxPackageDll}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\appx"; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater +Source: "appx\{#AppxPackage}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\appx"; BeforeInstall: RemoveAppxPackage; Flags: ignoreversion; Check: ShouldUseWindows11ContextMenu +Source: "appx\{#AppxPackageDll}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\appx"; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: ShouldUseWindows11ContextMenu #endif [Icons] @@ -1272,19 +1272,19 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBas Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe""" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}ContextMenu"; ValueType: expandsz; ValueName: "Title"; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: not IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}ContextMenu"; ValueType: expandsz; ValueName: "Title"; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufiles; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: addcontextmenufiles; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not ShouldUseWindows11ContextMenu ; Environment #if "user" == InstallTarget @@ -1472,6 +1472,23 @@ begin Result := (GetWindowsVersion >= $0A0055F0); end; +function IsWindows10ContextMenuForced(): Boolean; +var + SubKey: String; +begin + // Check if the user has forced Windows 10 style context menus on Windows 11 + SubKey := 'Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32'; + Result := RegKeyExists(HKEY_CURRENT_USER, SubKey) or RegKeyExists(HKEY_LOCAL_MACHINE, SubKey); +end; + +function ShouldUseWindows11ContextMenu(): Boolean; +begin + // Use Windows 11 context menu only if: + // 1. Running on Windows 11 or later + // 2. User has NOT forced Windows 10 style context menus + Result := IsWindows11OrLater() and not IsWindows10ContextMenuForced(); +end; + function GetAppMutex(Value: string): string; begin if IsBackgroundUpdate() then @@ -1629,7 +1646,7 @@ begin begin #ifdef AppxPackageName // Remove the old context menu registry keys - if IsWindows11OrLater() then begin + if ShouldUseWindows11ContextMenu() then begin RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\*\shell\{#RegValueName}'); RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\shell\{#RegValueName}'); RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\background\shell\{#RegValueName}'); From a5f53a88d0eeb8a235aa9082680bc5a8f949d2bf Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 11 Feb 2026 14:35:14 -0600 Subject: [PATCH 35/72] add tip for welcome view, first request per session, improve styling (#294653) --- .../contrib/chat/browser/chatTipService.ts | 144 ++++++++++++++---- .../chatContentParts/media/chatTipContent.css | 49 +++++- .../contrib/chat/browser/widget/chatWidget.ts | 116 +++++++++++++- .../browser/widget/input/chatInputPart.ts | 6 + .../chat/browser/widget/media/chat.css | 6 + .../chat/test/browser/chatTipService.test.ts | 33 ++++ 6 files changed, 325 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 08f1c161b3a..bcc4e30c205 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -43,14 +43,28 @@ export interface IChatTipService { /** * Gets a tip to show for a request, or undefined if a tip has already been shown this session. - * Only one tip is shown per VS Code session (resets on reload). - * Tips are only shown for requests created after the service was instantiated. + * Only one tip is shown per conversation session (resets when switching conversations). + * Tips are suppressed if a welcome tip was already shown in this session. + * Tips are only shown for requests created after the current session started. * @param requestId The unique ID of the request (used for stable rerenders). * @param requestTimestamp The timestamp when the request was created. * @param contextKeyService The context key service to evaluate tip eligibility. */ getNextTip(requestId: string, requestTimestamp: number, contextKeyService: IContextKeyService): IChatTip | undefined; + /** + * Gets a tip to show on the welcome/getting-started view. + * Unlike {@link getNextTip}, this does not require a request and skips request-timestamp checks. + * Returns the same tip on repeated calls for stable rerenders. + */ + getWelcomeTip(contextKeyService: IContextKeyService): IChatTip | undefined; + + /** + * Resets tip state for a new conversation. + * Call this when the chat widget binds to a new model. + */ + resetSession(): void; + /** * Dismisses the current tip and allows a new one to be picked for the same request. * The dismissed tip will not be shown again in this workspace. @@ -133,7 +147,7 @@ const TIP_CATALOG: ITipDefinition[] = [ }, { id: 'tip.undoChanges', - message: localize('tip.undoChanges', "Tip: You can undo Copilot's changes to any point by clicking Restore Checkpoint."), + message: localize('tip.undoChanges', "Tip: You can undo chat's changes to any point by clicking Restore Checkpoint."), when: ContextKeyExpr.or( ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Edit), @@ -142,7 +156,7 @@ const TIP_CATALOG: ITipDefinition[] = [ }, { id: 'tip.customInstructions', - message: localize('tip.customInstructions', "Tip: [Generate workspace instructions](command:workbench.action.chat.generateInstructions) so Copilot always has the context it needs when starting a task."), + message: localize('tip.customInstructions', "Tip: [Generate workspace instructions](command:workbench.action.chat.generateInstructions) so chat always has the context it needs when starting a task."), enabledCommands: ['workbench.action.chat.generateInstructions'], excludeWhenPromptFilesExist: { promptType: PromptsType.instructions, agentFileType: AgentFileType.copilotInstructionsMd, excludeUntilChecked: true }, }, @@ -468,16 +482,17 @@ export class ChatTipService extends Disposable implements IChatTipService { readonly onDidDisableTips = this._onDidDisableTips.event; /** - * Timestamp when this service was instantiated. + * Timestamp when the current session started. * Used to only show tips for requests created after this time. + * Resets on each {@link resetSession} call. */ - private readonly _createdAt = Date.now(); + private _sessionStartedAt = Date.now(); /** - * Whether a tip has already been shown in this window session. - * Only one tip is shown per session. + * Whether a chatResponse tip has already been shown in this conversation + * session. Only one response tip is shown per session. */ - private _hasShownTip = false; + private _hasShownRequestTip = false; /** * The request ID that was assigned a tip (for stable rerenders). @@ -490,6 +505,7 @@ export class ChatTipService extends Disposable implements IChatTipService { private _shownTip: ITipDefinition | undefined; private static readonly _DISMISSED_TIP_KEY = 'chat.tip.dismissed'; + private static readonly _LAST_TIP_ID_KEY = 'chat.tip.lastTipId'; private readonly _tracker: TipEligibilityTracker; constructor( @@ -503,13 +519,20 @@ export class ChatTipService extends Disposable implements IChatTipService { this._tracker = this._register(instantiationService.createInstance(TipEligibilityTracker, TIP_CATALOG)); } + resetSession(): void { + this._hasShownRequestTip = false; + this._shownTip = undefined; + this._tipRequestId = undefined; + this._sessionStartedAt = Date.now(); + } + dismissTip(): void { if (this._shownTip) { const dismissed = this._getDismissedTipIds(); dismissed.push(this._shownTip.id); this._storageService.store(ChatTipService._DISMISSED_TIP_KEY, JSON.stringify(dismissed), StorageScope.WORKSPACE, StorageTarget.MACHINE); } - this._hasShownTip = false; + this._hasShownRequestTip = false; this._shownTip = undefined; this._tipRequestId = undefined; this._onDidDismissTip.fire(); @@ -523,14 +546,25 @@ export class ChatTipService extends Disposable implements IChatTipService { try { const parsed = JSON.parse(raw); this._logService.debug('#ChatTips dismissed:', parsed); - return Array.isArray(parsed) ? parsed : []; + if (!Array.isArray(parsed)) { + return []; + } + + // Safety valve: if every known tip has been dismissed (for example, due to a + // past bug that dismissed the current tip on every new session), treat this + // as "no tips dismissed" so the feature can recover. + if (parsed.length >= TIP_CATALOG.length) { + return []; + } + + return parsed; } catch { return []; } } async disableTips(): Promise { - this._hasShownTip = false; + this._hasShownRequestTip = false; this._shownTip = undefined; this._tipRequestId = undefined; await this._configurationService.updateValue('chat.tips.enabled', false); @@ -554,33 +588,89 @@ export class ChatTipService extends Disposable implements IChatTipService { } // Only show one tip per session - if (this._hasShownTip) { + if (this._hasShownRequestTip) { return undefined; } - // Only show tips for requests created after the service was instantiated - // This prevents showing tips for old requests being re-rendered after reload - if (requestTimestamp < this._createdAt) { + // Only show tips for requests created after the current session started. + // This prevents showing tips for old requests being re-rendered. + if (requestTimestamp < this._sessionStartedAt) { return undefined; } - // Find eligible tips (excluding dismissed ones) - const dismissedIds = new Set(this._getDismissedTipIds()); - const eligibleTips = TIP_CATALOG.filter(tip => !dismissedIds.has(tip.id) && this._isEligible(tip, contextKeyService)); - // Record the current mode for future eligibility decisions + return this._pickTip(requestId, contextKeyService); + } + + getWelcomeTip(contextKeyService: IContextKeyService): IChatTip | undefined { + // Check if tips are enabled + if (!this._configurationService.getValue('chat.tips.enabled')) { + return undefined; + } + + // Only show tips for Copilot + if (!this._isCopilotEnabled()) { + return undefined; + } + + // Return the already-shown tip for stable rerenders + if (this._tipRequestId === 'welcome' && this._shownTip) { + return this._createTip(this._shownTip); + } + + const tip = this._pickTip('welcome', contextKeyService); + + return tip; + } + + private _pickTip(sourceId: string, contextKeyService: IContextKeyService): IChatTip | undefined { + // Record the current mode for future eligibility decisions. this._tracker.recordCurrentMode(contextKeyService); - if (eligibleTips.length === 0) { - return undefined; + const dismissedIds = new Set(this._getDismissedTipIds()); + let selectedTip: ITipDefinition | undefined; + + // Determine where to start in the catalog based on the last-shown tip. + const lastTipId = this._storageService.get(ChatTipService._LAST_TIP_ID_KEY, StorageScope.WORKSPACE); + const lastCatalogIndex = lastTipId ? TIP_CATALOG.findIndex(tip => tip.id === lastTipId) : -1; + const startIndex = lastCatalogIndex === -1 ? 0 : (lastCatalogIndex + 1) % TIP_CATALOG.length; + + // Pass 1: walk TIP_CATALOG in a ring, picking the first tip that is both + // not dismissed and eligible for the current context. + for (let i = 0; i < TIP_CATALOG.length; i++) { + const idx = (startIndex + i) % TIP_CATALOG.length; + const candidate = TIP_CATALOG[idx]; + if (!dismissedIds.has(candidate.id) && this._isEligible(candidate, contextKeyService)) { + selectedTip = candidate; + break; + } } - // Pick a random tip from eligible tips - const randomIndex = Math.floor(Math.random() * eligibleTips.length); - const selectedTip = eligibleTips[randomIndex]; + // Pass 2: if everything was ineligible (e.g., user has already done all + // the suggested actions), still advance through the catalog but only skip + // tips that were explicitly dismissed. + if (!selectedTip) { + for (let i = 0; i < TIP_CATALOG.length; i++) { + const idx = (startIndex + i) % TIP_CATALOG.length; + const candidate = TIP_CATALOG[idx]; + if (!dismissedIds.has(candidate.id)) { + selectedTip = candidate; + break; + } + } + } + + // Final fallback: if even that fails (all tips dismissed), stick with the + // catalog order so rotation still progresses. + if (!selectedTip) { + selectedTip = TIP_CATALOG[startIndex]; + } + + // Persist the selected tip id so the next use advances to the following one. + this._storageService.store(ChatTipService._LAST_TIP_ID_KEY, selectedTip.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); // Record that we've shown a tip this session - this._hasShownTip = true; - this._tipRequestId = requestId; + this._hasShownRequestTip = sourceId !== 'welcome'; + this._tipRequestId = sourceId; this._shownTip = selectedTip; return this._createTip(selectedTip); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css index cc9cfd47f39..d99c14f550b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css @@ -8,6 +8,10 @@ align-items: center; gap: 4px; margin-bottom: 8px; + padding: 6px 10px; + border-radius: 4px; + border: 1px solid var(--vscode-focusBorder); + background-color: var(--vscode-editorWidget-background); font-size: var(--vscode-chat-font-size-body-s); font-family: var(--vscode-chat-font-family, inherit); color: var(--vscode-descriptionForeground); @@ -15,13 +19,56 @@ .interactive-item-container .chat-tip-widget .codicon-lightbulb { font-size: 12px; - color: var(--vscode-descriptionForeground); + color: var(--vscode-notificationsWarningIcon-foreground); } .interactive-item-container .chat-tip-widget .rendered-markdown p { margin: 0; } +.chat-getting-started-tip-container { + position: absolute; + left: 12px; + right: 12px; + /* Initial vertical position; updated dynamically to align with input top */ + bottom: 96px; + display: flex; + justify-content: center; + /* No horizontal padding so width exactly matches input */ + padding: 0; + pointer-events: none; + z-index: 2; +} + +.chat-getting-started-tip-container .chat-tip-widget { + pointer-events: auto; + display: flex; + align-items: center; + gap: 4px; + width: 100%; + max-width: 100%; + box-sizing: border-box; + /* Match input inner padding (6px per side) so content edges align */ + padding: 6px; + /* Darker background for welcome-state tip, distinct from input */ + background-color: var(--vscode-editorWidget-background); + border-radius: 4px 4px 0 0; + border: 1px solid var(--vscode-editorWidget-border, var(--vscode-input-border, transparent)); + border-bottom: none; /* Seamless attachment to input below */ + box-shadow: none; + font-size: var(--vscode-chat-font-size-body-s); + font-family: var(--vscode-chat-font-family, inherit); + color: var(--vscode-descriptionForeground); +} + +.chat-getting-started-tip-container .chat-tip-widget .codicon-lightbulb { + display: none; +} + +.chat-getting-started-tip-container .chat-tip-widget .rendered-markdown p { + margin: 0; +} + /* Override bubble styling for tip widget's rendered markdown in chat editor */ .interactive-session:not(.chat-widget > .interactive-session) { .interactive-item-container.interactive-request .value .chat-tip-widget .rendered-markdown { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 13c0fb84ae1..9ed052f6af2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -78,6 +78,9 @@ import { IChatListItemTemplate } from './chatListRenderer.js'; import { ChatListWidget } from './chatListWidget.js'; import { ChatEditorOptions } from './chatOptions.js'; import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from '../viewsWelcome/chatViewWelcomeController.js'; +import { IChatTipService } from '../chatTipService.js'; +import { ChatTipContentPart } from './chatContentParts/chatTipContentPart.js'; +import { ChatContentMarkdownRenderer } from './chatContentMarkdownRenderer.js'; import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js'; const $ = dom.$; @@ -233,6 +236,9 @@ export class ChatWidget extends Disposable implements IChatWidget { private welcomeMessageContainer!: HTMLElement; private readonly welcomePart: MutableDisposable = this._register(new MutableDisposable()); + private gettingStartedTipContainer: HTMLElement | undefined; + private readonly _gettingStartedTipPart = this._register(new MutableDisposable()); + private readonly chatSuggestNextWidget: ChatSuggestNextWidget; private bodyDimension: dom.Dimension | undefined; @@ -368,6 +374,7 @@ export class ChatWidget extends Disposable implements IChatWidget { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IChatAttachmentResolveService private readonly chatAttachmentResolveService: IChatAttachmentResolveService, + @IChatTipService private readonly chatTipService: IChatTipService, ) { super(); @@ -622,6 +629,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } else { this.listContainer = dom.append(this.container, $(`.interactive-list`)); dom.append(this.container, this.chatSuggestNextWidget.domNode); + this.gettingStartedTipContainer = dom.append(this.container, $('.chat-getting-started-tip-container', { style: 'display: none' })); this.createInput(this.container, { renderFollowups, renderStyle, renderInputToolbarBelowInput }); } @@ -840,9 +848,33 @@ export class ChatWidget extends Disposable implements IChatWidget { */ private updateChatViewVisibility(): void { if (this.viewModel) { + const isStandardLayout = this.viewOptions.renderStyle !== 'compact' && this.viewOptions.renderStyle !== 'minimal'; const numItems = this.viewModel.getItems().length; dom.setVisibility(numItems === 0, this.welcomeMessageContainer); dom.setVisibility(numItems !== 0, this.listContainer); + + // Show/hide the getting-started tip container based on empty state. + // Only use this in the standard chat layout where the welcome view is shown. + if (isStandardLayout && this.gettingStartedTipContainer) { + if (numItems === 0) { + this.renderGettingStartedTipIfNeeded(); + this.container.classList.toggle('chat-has-getting-started-tip', !!this._gettingStartedTipPart.value); + } else { + // Dispose the cached tip part so the next empty state picks a + // fresh (rotated) tip instead of re-showing the stale one. + this._gettingStartedTipPart.clear(); + dom.clearNode(this.gettingStartedTipContainer); + // Reset inline positioning from layoutGettingStartedTipPosition + // so the next render starts from the CSS defaults. + this.gettingStartedTipContainer.style.top = ''; + this.gettingStartedTipContainer.style.bottom = ''; + this.gettingStartedTipContainer.style.left = ''; + this.gettingStartedTipContainer.style.right = ''; + this.gettingStartedTipContainer.style.width = ''; + dom.setVisibility(false, this.gettingStartedTipContainer); + this.container.classList.toggle('chat-has-getting-started-tip', false); + } + } } // Only show welcome getting started until extension is installed @@ -904,6 +936,84 @@ export class ChatWidget extends Disposable implements IChatWidget { } } + private renderGettingStartedTipIfNeeded(): void { + const tipContainer = this.gettingStartedTipContainer; + if (!tipContainer) { + return; + } + + // Already showing a tip + if (this._gettingStartedTipPart.value) { + dom.setVisibility(true, tipContainer); + return; + } + + const tip = this.chatTipService.getWelcomeTip(this.contextKeyService); + if (!tip) { + dom.setVisibility(false, tipContainer); + return; + } + + const store = new DisposableStore(); + const renderer = this.instantiationService.createInstance(ChatContentMarkdownRenderer); + const tipPart = store.add(this.instantiationService.createInstance(ChatTipContentPart, + tip, + renderer, + () => this.chatTipService.getWelcomeTip(this.contextKeyService), + )); + tipContainer.appendChild(tipPart.domNode); + + store.add(tipPart.onDidHide(() => { + tipPart.domNode.remove(); + this._gettingStartedTipPart.clear(); + dom.setVisibility(false, tipContainer); + this.container.classList.toggle('chat-has-getting-started-tip', false); + })); + + this._gettingStartedTipPart.value = store; + dom.setVisibility(true, tipContainer); + + // Best-effort synchronous position (works when layout is already settled, + // e.g. the very first render after page load). + this.layoutGettingStartedTipPosition(); + + // Also schedule a deferred correction for cases where the browser + // hasn't finished layout yet (e.g. returning to the welcome view + // after a conversation). + store.add(dom.scheduleAtNextAnimationFrame(dom.getWindow(tipContainer), () => { + this.layoutGettingStartedTipPosition(); + })); + } + + private layoutGettingStartedTipPosition(): void { + if (!this.container || !this.gettingStartedTipContainer || !this.inputPart) { + return; + } + + const inputContainer = this.inputPart.inputContainerElement; + if (!inputContainer) { + return; + } + + const containerRect = this.container.getBoundingClientRect(); + const inputRect = inputContainer.getBoundingClientRect(); + const tipRect = this.gettingStartedTipContainer.getBoundingClientRect(); + + // Align the tip horizontally with the input container. + const left = inputRect.left - containerRect.left; + this.gettingStartedTipContainer.style.left = `${left}px`; + this.gettingStartedTipContainer.style.right = 'auto'; + this.gettingStartedTipContainer.style.width = `${inputRect.width}px`; + + // Position the tip so its bottom edge sits flush against the input's + // top edge for a seamless visual connection. + const topOffset = inputRect.top - containerRect.top - tipRect.height; + if (topOffset > 0) { + this.gettingStartedTipContainer.style.top = `${topOffset}px`; + this.gettingStartedTipContainer.style.bottom = 'auto'; + } + } + private _getGenerateInstructionsMessage(): IMarkdownString { // Start checking for instruction files immediately if not already done if (!this._instructionFilesCheckPromise) { @@ -990,7 +1100,7 @@ export class ChatWidget extends Disposable implements IChatWidget { message: new MarkdownString(DISCLAIMER), icon: Codicon.chatSparkle, additionalMessage, - suggestedPrompts: this.getPromptFileSuggestions() + suggestedPrompts: this.getPromptFileSuggestions(), }; } @@ -1743,6 +1853,9 @@ export class ChatWidget extends Disposable implements IChatWidget { this.layout(this.bodyDimension.height, this.bodyDimension.width); } + // Keep getting-started tip aligned with the top of the input + this.layoutGettingStartedTipPosition(); + this._onDidChangeContentHeight.fire(); })); } @@ -1866,6 +1979,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } this.inputPart.clearTodoListWidget(model.sessionResource, false); this.chatSuggestNextWidget.hide(); + this.chatTipService.resetSession(); this._codeBlockModelCollection.clear(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 518c735c5d3..5d93d052632 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -289,12 +289,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatInputTodoListWidgetContainer!: HTMLElement; private chatQuestionCarouselContainer!: HTMLElement; private chatInputWidgetsContainer!: HTMLElement; + private inputContainer!: HTMLElement; private readonly _widgetController = this._register(new MutableDisposable()); private contextUsageWidget?: ChatContextUsageWidget; private contextUsageWidgetContainer!: HTMLElement; private readonly _contextUsageDisposables = this._register(new MutableDisposable()); + get inputContainerElement(): HTMLElement | undefined { + return this.inputContainer; + } + readonly height = observableValue(this, 0); private _inputEditor!: CodeEditorWidget; @@ -1794,6 +1799,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.followupsContainer = elements.followupsContainer; const inputAndSideToolbar = elements.inputAndSideToolbar; // The chat input and toolbar to the right const inputContainer = elements.inputContainer; // The chat editor, attachments, and toolbars + this.inputContainer = inputContainer; const editorContainer = elements.editorContainer; this.attachmentsContainer = elements.attachmentsContainer; this.attachedContextContainer = elements.attachedContextContainer; diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 4de68fd4633..f3c634ff743 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -805,6 +805,12 @@ have to be updated for changes to the rules above, or to support more deeply nes position: relative; } +/* When a getting-started tip is shown, visually attach it to the input */ +.interactive-session.chat-has-getting-started-tip .chat-input-container { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + /* Context usage widget container - positioned at top right of chat input */ .interactive-session .chat-input-container .chat-context-usage-container { position: absolute; diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts index 3ee8fdae9f6..442293c1998 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -479,6 +479,39 @@ suite('ChatTipService', () => { assert.strictEqual(tracker2.isExcluded(tip), true, 'New tracker should read persisted mode exclusion from workspace storage'); }); + test('resetSession allows tips in a new conversation', () => { + const service = createService(); + const now = Date.now(); + + // Show a tip in the first conversation + const tip1 = service.getNextTip('request-1', now + 1000, contextKeyService); + assert.ok(tip1, 'First request should get a tip'); + + // Second request — no tip (one per session) + const tip2 = service.getNextTip('request-2', now + 2000, contextKeyService); + assert.strictEqual(tip2, undefined, 'Second request should not get a tip'); + + // Start a new conversation + service.resetSession(); + + // New request after reset should get a tip + const tip3 = service.getNextTip('request-3', Date.now() + 1000, contextKeyService); + assert.ok(tip3, 'First request after resetSession should get a tip'); + }); + + test('chatResponse tip shows regardless of welcome tip', () => { + const service = createService(); + const now = Date.now(); + + // Show a welcome tip (simulating the getting-started view) + const welcomeTip = service.getWelcomeTip(contextKeyService); + assert.ok(welcomeTip, 'Welcome tip should be shown'); + + // First new request should still get a chatResponse tip + const tip = service.getNextTip('request-1', now + 1000, contextKeyService); + assert.ok(tip, 'ChatResponse tip should show even when welcome tip was shown'); + }); + test('excludes tip when tracked tool has been invoked', () => { const mockToolsService = createMockToolsService(); const tip: ITipDefinition = { From d5354dfbe6e76acc9f3ee8c607ade4137700bcb2 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:46:02 -0800 Subject: [PATCH 36/72] Fix provider to controller adapter --- src/vs/workbench/api/common/extHostChatSessions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index febaa3a413d..cec3c62b015 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -361,7 +361,10 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio if (provider.onDidChangeChatSessionItems) { disposables.add(provider.onDidChangeChatSessionItems(() => { this._logService.trace(`ExtHostChatSessions. Provider items changed for ${chatSessionType}`); - this._proxy.$onDidChangeChatSessionItems(handle); + // When a provider fires this, we treat it the same as triggering a refresh in the new controller based model. + // This is because with providers, firing this event would signal that `provide` should be called again. + // With controllers, it instead signals that you should read the current items again. + controller.refreshHandler(CancellationToken.None); })); } From 86aab8a08bf128ee385bfbd6f2ca6f1140889e0c Mon Sep 17 00:00:00 2001 From: Robo Date: Thu, 12 Feb 2026 06:05:35 +0900 Subject: [PATCH 37/72] fix: customize user-agent sent via macOS updater path (#294646) --- .../electron-main/abstractUpdateService.ts | 28 +++++++++++++++-- .../electron-main/updateService.darwin.ts | 30 ++++++++++++++++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index ed54d90f383..05c4489758b 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as os from 'os'; import { IntervalTimer, timeout } from '../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../base/common/event.js'; +import { isMacintosh } from '../../../base/common/platform.js'; import { IMeteredConnectionService } from '../../meteredConnection/common/meteredConnection.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; @@ -29,6 +31,23 @@ export function createUpdateURL(baseUpdateUrl: string, platform: string, quality return url.toString(); } +/** + * Builds common headers for macOS update requests, including those issued + * via Electron's auto-updater (e.g. setFeedURL({ url, headers })) and + * manual HTTP requests that bypass the auto-updater. On macOS, this includes + * the Darwin kernel version which the update server uses for EOL detection. + */ +export function getUpdateRequestHeaders(productVersion: string): Record | undefined { + if (isMacintosh) { + const darwinVersion = os.release(); + return { + 'User-Agent': `Code/${productVersion} Darwin/${darwinVersion}` + }; + } + + return undefined; +} + export type UpdateErrorClassification = { owner: 'joaomoreno'; messageHash: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The hash of the error message.' }; @@ -288,11 +307,16 @@ export abstract class AbstractUpdateService implements IUpdateService { return undefined; } + const headers = getUpdateRequestHeaders(this.productService.version); + this.logService.trace('update#isLatestVersion() - checking update server', { url, headers }); + try { - const context = await this.requestService.request({ url }, token); + const context = await this.requestService.request({ url, headers }, token); + const statusCode = context.res.statusCode; + this.logService.trace('update#isLatestVersion() - response', { statusCode }); // The update server replies with 204 (No Content) when no // update is available - that's all we want to know. - return context.res.statusCode === 204; + return statusCode === 204; } catch (error) { this.logService.error('update#isLatestVersion(): failed to check for updates'); diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index fe133e702b9..e65a9823839 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -18,13 +18,14 @@ import { asJson, IRequestService } from '../../request/common/request.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AvailableForDownload, IUpdate, State, StateType, UpdateType } from '../common/update.js'; import { IMeteredConnectionService } from '../../meteredConnection/common/meteredConnection.js'; -import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js'; +import { AbstractUpdateService, createUpdateURL, getUpdateRequestHeaders, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js'; export class DarwinUpdateService extends AbstractUpdateService implements IRelaunchHandler { private readonly disposables = new DisposableStore(); @memoize private get onRawError(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } + @memoize private get onRawCheckingForUpdate(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'checking-for-update'); } @memoize private get onRawUpdateNotAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available'); } @memoize private get onRawUpdateDownloaded(): Event { @@ -68,11 +69,16 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau protected override async initialize(): Promise { await super.initialize(); this.onRawError(this.onError, this, this.disposables); + this.onRawCheckingForUpdate(this.onCheckingForUpdate, this, this.disposables); this.onRawUpdateAvailable(this.onUpdateAvailable, this, this.disposables); this.onRawUpdateDownloaded(this.onUpdateDownloaded, this, this.disposables); this.onRawUpdateNotAvailable(this.onUpdateNotAvailable, this, this.disposables); } + private onCheckingForUpdate(): void { + this.logService.trace('update#onCheckingForUpdate - Electron autoUpdater is checking for updates'); + } + private onError(err: string): void { this.telemetryService.publicLog2<{ messageHash: string }, UpdateErrorClassification>('update:error', { messageHash: String(hash(String(err))) }); this.logService.error('UpdateService error:', err); @@ -85,8 +91,10 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined { const assetID = this.productService.darwinUniversalAssetId ?? (process.arch === 'x64' ? 'darwin' : 'darwin-arm64'); const url = createUpdateURL(this.productService.updateUrl!, assetID, quality, commit, options); + const headers = getUpdateRequestHeaders(this.productService.version); try { - electron.autoUpdater.setFeedURL({ url }); + this.logService.trace('update#buildUpdateFeedUrl - setting feed URL for Electron autoUpdater', { url, assetID, quality, commit, headers }); + electron.autoUpdater.setFeedURL({ url, headers }); } catch (e) { // application is very likely not signed this.logService.error('Failed to set update feed URL', e); @@ -126,6 +134,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return; } + this.logService.trace('update#doCheckForUpdates - using Electron autoUpdater', { url, explicit, background }); electron.autoUpdater.checkForUpdates(); } @@ -134,20 +143,31 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau * Used when connection is metered to show update availability without downloading. */ private async checkForUpdateNoDownload(url: string): Promise { + const headers = getUpdateRequestHeaders(this.productService.version); + this.logService.trace('update#checkForUpdateNoDownload - checking update server', { url, headers }); + try { - const update = await asJson(await this.requestService.request({ url }, CancellationToken.None)); + const context = await this.requestService.request({ url, headers }, CancellationToken.None); + const statusCode = context.res.statusCode; + this.logService.trace('update#checkForUpdateNoDownload - response', { statusCode }); + + const update = await asJson(context); if (!update || !update.url || !update.version || !update.productVersion) { + this.logService.trace('update#checkForUpdateNoDownload - no update available'); this.setState(State.Idle(UpdateType.Archive)); } else { + this.logService.trace('update#checkForUpdateNoDownload - update available', { version: update.version, productVersion: update.productVersion }); this.setState(State.AvailableForDownload(update)); } } catch (err) { - this.logService.error(err); + this.logService.error('update#checkForUpdateNoDownload - failed to check for update', err); this.setState(State.Idle(UpdateType.Archive)); } } private onUpdateAvailable(): void { + this.logService.trace('update#onUpdateAvailable - Electron autoUpdater reported update available'); + if (this.state.type !== StateType.CheckingForUpdates && this.state.type !== StateType.Overwriting) { return; } @@ -167,6 +187,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau } private onUpdateNotAvailable(): void { + this.logService.trace('update#onUpdateNotAvailable - Electron autoUpdater reported no update available'); + if (this.state.type !== StateType.CheckingForUpdates) { return; } From 7b6fd2e5f8f1f9d5971f157039d35f93a8da8fb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:07:39 +0000 Subject: [PATCH 38/72] Initial plan From f6fbcd535abf6ee1ffb38cf93a8d815d25ed2df5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:12:10 +0000 Subject: [PATCH 39/72] Fix terminal persistence test by setting stable title format Set terminal.integrated.tabs.title to ${process} to ensure consistent terminal names for detach/attach operations in the smoke test. This prevents name mismatches on Windows where the default title format may include dynamic content. Fixes failing test: Terminal Persistence -> detach/attach -> should support basic reconnection Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com> --- test/smoke/src/areas/terminal/terminal-persistence.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/smoke/src/areas/terminal/terminal-persistence.test.ts b/test/smoke/src/areas/terminal/terminal-persistence.test.ts index 455d1f016d4..c4cb0ca0fd0 100644 --- a/test/smoke/src/areas/terminal/terminal-persistence.test.ts +++ b/test/smoke/src/areas/terminal/terminal-persistence.test.ts @@ -16,7 +16,10 @@ export function setup(options?: { skipSuite: boolean }) { const app = this.app as Application; terminal = app.workbench.terminal; settingsEditor = app.workbench.settingsEditor; - await setTerminalTestSettings(app); + await setTerminalTestSettings(app, [ + // Use ${process} for terminal title to ensure stable names for detach/attach + ['terminal.integrated.tabs.title', '"${process}"'] + ]); }); after(async function () { From 70658519dc8b1cc5b312ca0c3b7f232b7aea8140 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:17:47 -0800 Subject: [PATCH 40/72] Enable tsgo by default in the workspace Tsgo language tooling should be feature complete at this point. There still may be bugs or more minor gaps, but we want more dogfooding to find these Enabling tsgo to help with this. It's still easy to switch back to tsc if you run into any blockers --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index da775f21244..3e903f64675 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -94,6 +94,7 @@ "**/*.snap": true, }, // --- TypeScript --- + "typescript.experimental.useTsgo": true, "typescript.tsdk": "node_modules/typescript/lib", "typescript.preferences.importModuleSpecifier": "relative", "typescript.preferences.quoteStyle": "single", From f103973f700e95b1906802b117d23f5f691c4772 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 11 Feb 2026 15:30:01 -0600 Subject: [PATCH 41/72] add `requiresAnyToolSetRegistered` condition for `tips` (#294680) fix #294675 --- .../browser/ui/codicons/codicon/codicon.ttf | Bin 125648 -> 125696 bytes .../contrib/chat/browser/chatTipService.ts | 17 +++++++- .../chat/test/browser/chatTipService.test.ts | 41 ++++++++++++++++++ .../tools/mockLanguageModelToolsService.ts | 17 ++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index ff46f210b8ab96e49e12c17db2d6794ed6b5eecd..a3098e1715c529ea8fa52ad57586c686ca2fd629 100644 GIT binary patch delta 559 zcmXAlO=uHQ5Xawa-tK1K?!LF1x0|>LiIPZ)(L_n&D)>n$9*T#Gp2UL%58f<(Ts#<| z;vp4jK)i^hpm>m61fhYX+cviOXq(WBJ@)9ygGdg=le^6vW`_U#X8tgHKjoME^5S*v zcD}iQP<|P~$5!oT8Rw?G<=U%^c@H5toO|@->B5IBK`^*eYn;PhXXayI~1Zai<3_UOm^O}croHMQ02WINT) z-)^ou*FEfA?!D+o`w#m2+jP6Jb8KgCH@Ev`aAHs&y2E$FgZZWW%2FOaJ*$L}9}tH) za?yQMMTi%pu@u;rB&Q$=mc)R>Z>ilS483)nFP zjmt9E1jE4scc`u?aV|IpiAYj}Oc5qxSu>bMuq0uXSzMPwI7CTEHCZq=HGwbzha}yy znHoT(qA^30vB(HGwgCyQ`VMi1t)w8b2nmNtgo#4Z!YuAPt2_aQt;n*%DZyAYVx|-p z!8S1EqOPc>RwtB_x@M}1E^>+u8^o|=#tac-LJ=B&TSgd~rO>SLkEA6Z8gZKN}cef8ebauR*w;i>!*!k8q zx);0Od->kA-rxRQKkOfVT-!yv8+*pyqk%YhGRzF`3_JH%i|eaJcyVhPqexH$4~a-e zNmN4%2vsfOxaAtK|3~06T6P^`F+ZqUDole4j$0r(X9gc0T|B diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index bcc4e30c205..726d06a3df5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -105,6 +105,11 @@ export interface ITipDefinition { * The tip won't be shown if the tool it describes has already been used. */ readonly excludeWhenToolsInvoked?: string[]; + /** + * Tool set reference names where at least one must be registered for the tip to be eligible. + * If none of the listed tool sets are registered, the tip is not shown. + */ + readonly requiresAnyToolSetRegistered?: string[]; /** * If set, exclude this tip when prompt files of the specified type exist in the workspace. */ @@ -205,6 +210,7 @@ const TIP_CATALOG: ITipDefinition[] = [ ContextKeyExpr.notEquals('gitOpenRepositoryCount', '0'), ), excludeWhenToolsInvoked: ['github-pull-request_doSearch', 'github-pull-request_issue_fetch', 'github-pull-request_formSearchQuery'], + requiresAnyToolSetRegistered: ['github', 'github-pull-request'], }, { id: 'tip.subagents', @@ -271,7 +277,7 @@ export class TipEligibilityTracker extends Disposable { @ICommandService commandService: ICommandService, @IStorageService private readonly _storageService: IStorageService, @IPromptsService private readonly _promptsService: IPromptsService, - @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, + @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, @ILogService private readonly _logService: ILogService, ) { super(); @@ -335,7 +341,7 @@ export class TipEligibilityTracker extends Disposable { // --- Set up tool listener (auto-disposes when all seen) ----------------- if (this._pendingTools.size > 0) { - this._toolListener.value = languageModelToolsService.onDidInvokeTool(e => { + this._toolListener.value = this._languageModelToolsService.onDidInvokeTool(e => { if (this._pendingTools.has(e.toolId)) { this._invokedTools.add(e.toolId); this._persistSet(TipEligibilityTracker._TOOLS_STORAGE_KEY, this._invokedTools); @@ -428,6 +434,13 @@ export class TipEligibilityTracker extends Disposable { this._logService.debug('#ChatTips: tip excluded because prompt files exist', tip.id); return true; } + if (tip.requiresAnyToolSetRegistered) { + const hasAny = tip.requiresAnyToolSetRegistered.some(name => this._languageModelToolsService.getToolSetByName(name)); + if (!hasAny) { + this._logService.debug('#ChatTips: tip excluded because no required tool sets are registered', tip.id); + return true; + } + } return false; } diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts index 442293c1998..a1f7741a4b7 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -613,6 +613,47 @@ suite('ChatTipService', () => { assert.strictEqual(tracker.isExcluded(tip), false, 'Should not be excluded when no skill files exist'); }); + test('excludes tip when requiresAnyToolSetRegistered tool sets are not registered', () => { + const tip: ITipDefinition = { + id: 'tip.githubRepo', + message: 'test', + requiresAnyToolSetRegistered: ['github', 'github-pull-request'], + }; + + const tracker = testDisposables.add(new TipEligibilityTracker( + [tip], + { onDidExecuteCommand: Event.None, onWillExecuteCommand: Event.None } as Partial as ICommandService, + storageService, + createMockPromptsService() as IPromptsService, + createMockToolsService(), + new NullLogService(), + )); + + assert.strictEqual(tracker.isExcluded(tip), true, 'Should be excluded when no required tool sets are registered'); + }); + + test('does not exclude tip when at least one requiresAnyToolSetRegistered tool set is registered', () => { + const mockToolsService = createMockToolsService(); + mockToolsService.addRegisteredToolSetName('github'); + + const tip: ITipDefinition = { + id: 'tip.githubRepo', + message: 'test', + requiresAnyToolSetRegistered: ['github', 'github-pull-request'], + }; + + const tracker = testDisposables.add(new TipEligibilityTracker( + [tip], + { onDidExecuteCommand: Event.None, onWillExecuteCommand: Event.None } as Partial as ICommandService, + storageService, + createMockPromptsService() as IPromptsService, + mockToolsService, + new NullLogService(), + )); + + assert.strictEqual(tracker.isExcluded(tip), false, 'Should not be excluded when at least one required tool set is registered'); + }); + test('re-checks agent file exclusion when onDidChangeCustomAgents fires', async () => { const agentChangeEmitter = testDisposables.add(new Emitter()); let agentFiles: IPromptPath[] = []; diff --git a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts index ea248c48bd2..665bd2a4618 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts @@ -27,6 +27,9 @@ export class MockLanguageModelToolsService extends Disposable implements ILangua private readonly _onDidInvokeTool = this._register(new Emitter()); + private readonly _registeredToolIds = new Set(); + private readonly _registeredToolSetNames = new Set(); + constructor() { super(); } @@ -88,7 +91,14 @@ export class MockLanguageModelToolsService extends Disposable implements ILangua return []; } + addRegisteredToolId(id: string): void { + this._registeredToolIds.add(id); + } + getTool(id: string): IToolData | undefined { + if (this._registeredToolIds.has(id)) { + return { id, source: ToolDataSource.Internal, displayName: id, modelDescription: id }; + } return undefined; } @@ -125,7 +135,14 @@ export class MockLanguageModelToolsService extends Disposable implements ILangua return []; } + addRegisteredToolSetName(name: string): void { + this._registeredToolSetNames.add(name); + } + getToolSetByName(name: string): IToolSet | undefined { + if (this._registeredToolSetNames.has(name)) { + return { id: name, referenceName: name, icon: ThemeIcon.fromId(Codicon.tools.id), source: ToolDataSource.Internal, getTools: () => [] }; + } return undefined; } From 797e995e24366ddeb967e1347a041810e0892e13 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:12:00 +0000 Subject: [PATCH 42/72] Initial plan From 83c7589b4167ee4752673c926ddadbcbd330483c Mon Sep 17 00:00:00 2001 From: cwebster-99 Date: Wed, 11 Feb 2026 16:12:35 -0600 Subject: [PATCH 43/72] Updating no auth widget for agent welcome --- .../welcomeAgentSessions/browser/agentSessionsWelcome.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index 30789ae673c..9564ad3f431 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -753,7 +753,7 @@ export class AgentSessionsWelcomePage extends EditorPane { // Content const content = append(tosCard, $('.agentSessionsWelcome-walkthroughCard-content')); const title = append(content, $('.agentSessionsWelcome-walkthroughCard-title')); - title.textContent = localize('tosTitle', "Your GitHub Copilot trial is active"); + title.textContent = localize('tosTitle', "Try GitHub Copilot for free, no sign-in required!"); const desc = append(content, $('.agentSessionsWelcome-walkthroughCard-description')); const descriptionMarkdown = new MarkdownString( From f8aa99fcc09bfad525783edfd8ad50cbc9512f37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:18:43 +0000 Subject: [PATCH 44/72] Refactor quick input animations to use CSS classes instead of inline styles Co-authored-by: eli-w-king <201316543+eli-w-king@users.noreply.github.com> --- .../quickinput/browser/media/quickInput.css | 20 ++++++++++++ .../browser/quickInputController.ts | 32 ++++++------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index bd509719a3c..5bdbb5fc743 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -525,3 +525,23 @@ outline: 1px solid var(--vscode-list-focusOutline) !important; outline-offset: -1px; } + +/* Entrance animation */ +.quick-input-widget.animating-entrance { + opacity: 0; + transform: translateY(-8px); + transition: opacity 150ms cubic-bezier(0.1, 0.9, 0.2, 1), transform 150ms cubic-bezier(0.1, 0.9, 0.2, 1); + will-change: opacity, transform; +} + +.quick-input-widget.animating-entrance.visible { + opacity: 1; + transform: translateY(0); +} + +/* Exit animation */ +.quick-input-widget.animating-exit { + opacity: 0; + transform: translateY(-8px); + transition: opacity 50ms cubic-bezier(0.9, 0.1, 1, 0.2), transform 50ms cubic-bezier(0.9, 0.1, 1, 0.2); +} diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index a9dcd8203e2..32754f1b6ab 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -37,7 +37,7 @@ import { TriStateCheckbox, createToggleActionViewItemProvider } from '../../../b import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js'; import { QuickInputTreeController } from './tree/quickInputTreeController.js'; import { QuickTree } from './tree/quickTree.js'; -import { isMotionReduced, EASE_OUT, EASE_IN } from '../../../base/browser/ui/motion/motion.js'; +import { isMotionReduced } from '../../../base/browser/ui/motion/motion.js'; import { AnchorAlignment, AnchorPosition, layout2d } from '../../../base/common/layout.js'; import { getAnchorRect } from '../../../base/browser/ui/contextview/contextview.js'; @@ -732,17 +732,12 @@ export class QuickInputController extends Disposable { // Animate entrance: fade in + slide down (only when first appearing) if (!wasVisible && !isMotionReduced(ui.container)) { - // Set initial state without transition - ui.container.style.transition = 'none'; - ui.container.style.opacity = '0'; - ui.container.style.transform = 'translateY(-8px)'; + ui.container.classList.add('animating-entrance'); // Trigger reflow so the browser registers the initial state void ui.container.offsetHeight; - // Now apply transition and target state - ui.container.style.transition = `opacity ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT.toCssString()}, transform ${QUICK_INPUT_OPEN_DURATION}ms ${EASE_OUT.toCssString()}`; - ui.container.style.willChange = 'opacity, transform'; - ui.container.style.opacity = '1'; - ui.container.style.transform = 'translateY(0)'; + // Now add the visible class to trigger the transition + ui.container.classList.add('visible'); + let entranceCleanedUp = false; const window = dom.getWindow(ui.container); let entranceCleanupTimeout: number | undefined; @@ -751,8 +746,7 @@ export class QuickInputController extends Disposable { return; } entranceCleanedUp = true; - ui.container.style.transition = ''; - ui.container.style.willChange = ''; + ui.container.classList.remove('animating-entrance', 'visible'); ui.container.removeEventListener('transitionend', onTransitionEnd); if (entranceCleanupTimeout !== undefined) { clearTimeout(entranceCleanupTimeout); @@ -840,19 +834,14 @@ export class QuickInputController extends Disposable { let exitCancelled = false; const window = dom.getWindow(container); let exitCleanupTimeout: number | undefined; - container.style.transition = `opacity ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN.toCssString()}, transform ${QUICK_INPUT_CLOSE_DURATION}ms ${EASE_IN.toCssString()}`; - container.style.opacity = '0'; - container.style.transform = 'translateY(-8px)'; + container.classList.add('animating-exit'); const onDone = () => { if (exitCancelled) { return; } exitCancelled = true; container.style.display = 'none'; - container.style.transition = ''; - container.style.opacity = ''; - container.style.transform = ''; - container.style.willChange = ''; + container.classList.remove('animating-exit'); container.removeEventListener('transitionend', onTransitionEnd); if (exitCleanupTimeout !== undefined) { clearTimeout(exitCleanupTimeout); @@ -867,10 +856,7 @@ export class QuickInputController extends Disposable { }; this._cancelExitAnimation = () => { exitCancelled = true; - container.style.transition = ''; - container.style.opacity = ''; - container.style.transform = ''; - container.style.willChange = ''; + container.classList.remove('animating-exit'); container.removeEventListener('transitionend', onTransitionEnd); if (exitCleanupTimeout !== undefined) { clearTimeout(exitCleanupTimeout); From 69cd1ec09593925e04887ec150979c7f3de36a0e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:22:09 +0000 Subject: [PATCH 45/72] Fix entrance animation cleanup to preserve visibility and remove trailing whitespace Co-authored-by: eli-w-king <201316543+eli-w-king@users.noreply.github.com> --- src/vs/platform/quickinput/browser/media/quickInput.css | 2 +- src/vs/platform/quickinput/browser/quickInputController.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 5bdbb5fc743..16d185b3f87 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -534,7 +534,7 @@ will-change: opacity, transform; } -.quick-input-widget.animating-entrance.visible { +.quick-input-widget.animating-entrance.animating-entrance-target { opacity: 1; transform: translateY(0); } diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 32754f1b6ab..ee5369bf373 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -735,9 +735,8 @@ export class QuickInputController extends Disposable { ui.container.classList.add('animating-entrance'); // Trigger reflow so the browser registers the initial state void ui.container.offsetHeight; - // Now add the visible class to trigger the transition - ui.container.classList.add('visible'); - + // Now add the target class to trigger the transition + ui.container.classList.add('animating-entrance-target'); let entranceCleanedUp = false; const window = dom.getWindow(ui.container); let entranceCleanupTimeout: number | undefined; @@ -746,7 +745,7 @@ export class QuickInputController extends Disposable { return; } entranceCleanedUp = true; - ui.container.classList.remove('animating-entrance', 'visible'); + ui.container.classList.remove('animating-entrance', 'animating-entrance-target'); ui.container.removeEventListener('transitionend', onTransitionEnd); if (entranceCleanupTimeout !== undefined) { clearTimeout(entranceCleanupTimeout); From bd3b73648c4ce0dc1671be8ec3895f72fc6b5b96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:22:58 +0000 Subject: [PATCH 46/72] Add comments to document CSS animation duration constants and display property usage Co-authored-by: eli-w-king <201316543+eli-w-king@users.noreply.github.com> --- src/vs/platform/quickinput/browser/media/quickInput.css | 2 ++ src/vs/platform/quickinput/browser/quickInputController.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 16d185b3f87..b424a9e2377 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -527,6 +527,7 @@ } /* Entrance animation */ +/* Duration matches QUICK_INPUT_OPEN_DURATION constant (150ms) in quickInputController.ts */ .quick-input-widget.animating-entrance { opacity: 0; transform: translateY(-8px); @@ -540,6 +541,7 @@ } /* Exit animation */ +/* Duration matches QUICK_INPUT_CLOSE_DURATION constant (50ms) in quickInputController.ts */ .quick-input-widget.animating-exit { opacity: 0; transform: translateY(-8px); diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index ee5369bf373..8b5f998f9e6 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -839,6 +839,7 @@ export class QuickInputController extends Disposable { return; } exitCancelled = true; + // Set display after animation completes to actually hide the element container.style.display = 'none'; container.classList.remove('animating-exit'); container.removeEventListener('transitionend', onTransitionEnd); From c4f5414e3d408f06433c832e4604d204c0703c54 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Wed, 11 Feb 2026 17:24:42 -0500 Subject: [PATCH 47/72] Advertise full context rather than just prompt (#294696) --- .../viewPane/chatContextUsageDetails.ts | 27 ++++++++++++++----- .../viewPane/chatContextUsageWidget.ts | 13 +++++---- .../chat/common/participants/chatAgents.ts | 12 --------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts index 306f8b863c9..43fc982f081 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts @@ -21,8 +21,8 @@ export interface IChatContextUsagePromptTokenDetail { } export interface IChatContextUsageData { - promptTokens: number; - maxInputTokens: number; + usedTokens: number; + totalContextWindow: number; percentage: number; promptTokenDetails?: readonly IChatContextUsagePromptTokenDetail[]; } @@ -102,14 +102,14 @@ export class ChatContextUsageDetails extends Disposable { } update(data: IChatContextUsageData): void { - const { percentage, promptTokens, maxInputTokens, promptTokenDetails } = data; + const { percentage, usedTokens, totalContextWindow, promptTokenDetails } = data; // Update token count and percentage on same line this.tokenCountLabel.textContent = localize( 'tokenCount', "{0} / {1} tokens", - this.formatTokenCount(promptTokens, 1), - this.formatTokenCount(maxInputTokens, 0) + this.formatTokenCount(usedTokens, 1), + this.formatTokenCount(totalContextWindow, 0) ); this.percentageLabel.textContent = `• ${percentage.toFixed(0)}%`; @@ -132,7 +132,10 @@ export class ChatContextUsageDetails extends Disposable { } private formatTokenCount(count: number, decimals: number): string { - if (count >= 1000000) { + // Use M when count is >= 1M, or when K representation would round to 1000K + const mThreshold = 1000000 - 500 * Math.pow(10, -decimals); + + if (count >= mThreshold) { return `${(count / 1000000).toFixed(decimals)}M`; } else if (count >= 1000) { return `${(count / 1000).toFixed(decimals)}K`; @@ -172,6 +175,16 @@ export class ChatContextUsageDetails extends Disposable { // Render each category for (const [category, items] of categoryMap) { + // Filter out items with 0% usage + const visibleItems = items.filter(item => { + const contextRelativePercentage = (item.percentageOfPrompt / 100) * contextWindowPercentage; + return contextRelativePercentage >= 0.05; // Show if at least 0.1% when rounded + }); + + if (visibleItems.length === 0) { + continue; + } + const categorySection = this.tokenDetailsContainer.appendChild($('.token-category')); // Category header @@ -179,7 +192,7 @@ export class ChatContextUsageDetails extends Disposable { categoryHeader.textContent = category; // Category items - for (const item of items) { + for (const item of visibleItems) { const itemRow = categorySection.appendChild($('.token-detail-item')); const itemLabel = itemRow.appendChild($('.token-detail-label')); diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts index aa6c0db52aa..5b48266f16a 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts @@ -248,23 +248,26 @@ export class ChatContextUsageWidget extends Disposable { const usage = response.usage; const modelMetadata = this.languageModelsService.lookupLanguageModel(modelId); const maxInputTokens = modelMetadata?.maxInputTokens; + const maxOutputTokens = modelMetadata?.maxOutputTokens; - if (!usage || !maxInputTokens || maxInputTokens <= 0) { + if (!usage || !maxInputTokens || maxInputTokens <= 0 || !maxOutputTokens || maxOutputTokens <= 0) { this.hide(); return; } const promptTokens = usage.promptTokens; const promptTokenDetails = usage.promptTokenDetails; - const percentage = Math.min(100, (promptTokens / maxInputTokens) * 100); + const totalContextWindow = maxInputTokens + maxOutputTokens; + const usedTokens = promptTokens + maxOutputTokens; + const percentage = Math.min(100, (usedTokens / totalContextWindow) * 100); - this.render(percentage, promptTokens, maxInputTokens, promptTokenDetails); + this.render(percentage, usedTokens, totalContextWindow, promptTokenDetails); this.show(); } - private render(percentage: number, promptTokens: number, maxTokens: number, promptTokenDetails?: readonly { category: string; label: string; percentageOfPrompt: number }[]): void { + private render(percentage: number, usedTokens: number, totalContextWindow: number, promptTokenDetails?: readonly { category: string; label: string; percentageOfPrompt: number }[]): void { // Store current data for use in details popup - this.currentData = { promptTokens, maxInputTokens: maxTokens, percentage, promptTokenDetails }; + this.currentData = { usedTokens, totalContextWindow, percentage, promptTokenDetails }; // Update pie chart progress this.progressIndicator.setProgress(percentage); diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index e0dd7ca927a..94097d98c96 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -186,18 +186,6 @@ export interface IChatAgentResultTimings { totalElapsed: number; } -export interface IChatAgentPromptTokenDetail { - category: string; - label: string; - percentageOfPrompt: number; -} - -export interface IChatAgentResultUsage { - promptTokens: number; - completionTokens: number; - promptTokenDetails?: readonly IChatAgentPromptTokenDetail[]; -} - export interface IChatAgentResult { errorDetails?: IChatResponseErrorDetails; timings?: IChatAgentResultTimings; From 03561955b791d218e612e36a34599295ff99ad89 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:57:16 -0800 Subject: [PATCH 48/72] Clean up esbuilding of extensions - Make sure we still always type check using `tsgo --noEmit` - Align names of esbuild files - Convert all esbuild files to typescript. We use the `.mts` extension to avoid annoying node warnings about using pacakges --- build/lib/extensions.ts | 44 ++++++++++------- build/lib/tsgo.ts | 11 +++-- ...common.ts => esbuild-extension-common.mts} | 0 ...-common.mjs => esbuild-webview-common.mts} | 48 ++++++++----------- .../{esbuild.mjs => esbuild.notebook.mts} | 3 +- extensions/ipynb/package.json | 2 +- ...esbuild-browser.ts => esbuild.browser.mts} | 2 +- .../{esbuild.ts => esbuild.mts} | 2 +- ...uild-notebook.mjs => esbuild.notebook.mts} | 3 +- ...sbuild-preview.mjs => esbuild.webview.mts} | 3 +- .../markdown-language-features/package.json | 22 +++++---- .../tsconfig.browser.json | 3 ++ .../esbuild.browser.mts} | 2 +- .../esbuild.ts => markdown-math/esbuild.mts} | 2 +- .../{esbuild.mjs => esbuild.notebook.mts} | 10 ++-- extensions/markdown-math/package.json | 2 +- .../markdown-math/tsconfig.browser.json | 9 ++++ .../esbuild.browser.mts} | 2 +- .../esbuild.ts => media-preview/esbuild.mts} | 2 +- ...esbuild-browser.ts => esbuild.browser.mts} | 2 +- .../esbuild.mts} | 2 +- ...d-chat-webview.mjs => esbuild.webview.mts} | 3 +- extensions/mermaid-chat-features/package.json | 2 +- .../{esbuild.mjs => esbuild.notebook.mts} | 3 +- extensions/notebook-renderers/package.json | 2 +- ...sbuild-preview.mjs => esbuild.webview.mts} | 3 +- extensions/simple-browser/package.json | 2 +- 27 files changed, 103 insertions(+), 88 deletions(-) rename extensions/{esbuild-extension-common.ts => esbuild-extension-common.mts} (100%) rename extensions/{esbuild-webview-common.mjs => esbuild-webview-common.mts} (62%) rename extensions/ipynb/{esbuild.mjs => esbuild.notebook.mts} (90%) rename extensions/markdown-language-features/{esbuild-browser.ts => esbuild.browser.mts} (96%) rename extensions/markdown-language-features/{esbuild.ts => esbuild.mts} (95%) rename extensions/markdown-language-features/{esbuild-notebook.mjs => esbuild.notebook.mts} (90%) rename extensions/markdown-language-features/{esbuild-preview.mjs => esbuild.webview.mts} (90%) rename extensions/{media-preview/esbuild-browser.ts => markdown-math/esbuild.browser.mts} (93%) rename extensions/{mermaid-chat-features/esbuild.ts => markdown-math/esbuild.mts} (91%) rename extensions/markdown-math/{esbuild.mjs => esbuild.notebook.mts} (89%) create mode 100644 extensions/markdown-math/tsconfig.browser.json rename extensions/{markdown-math/esbuild-browser.ts => media-preview/esbuild.browser.mts} (93%) rename extensions/{markdown-math/esbuild.ts => media-preview/esbuild.mts} (91%) rename extensions/mermaid-chat-features/{esbuild-browser.ts => esbuild.browser.mts} (93%) rename extensions/{media-preview/esbuild.ts => mermaid-chat-features/esbuild.mts} (91%) rename extensions/mermaid-chat-features/{esbuild-chat-webview.mjs => esbuild.webview.mts} (92%) rename extensions/notebook-renderers/{esbuild.mjs => esbuild.notebook.mts} (90%) rename extensions/simple-browser/{esbuild-preview.mjs => esbuild.webview.mts} (92%) diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index cea54bff8b9..3e83cab64f9 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -25,6 +25,7 @@ import { getProductionDependencies } from './dependencies.ts'; import { type IExtensionDefinition, getExtensionStream } from './builtInExtensions.ts'; import { getVersion } from './getVersion.ts'; import { fetchUrls, fetchGithub } from './fetch.ts'; +import { createTsgoStream } from './tsgo.ts'; import vzip from 'gulp-vinyl-zip'; import { createRequire } from 'module'; @@ -67,23 +68,27 @@ function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolean): Stream { const esbuildConfigFileName = forWeb - ? 'esbuild-browser.ts' - : 'esbuild.ts'; + ? 'esbuild.browser.mts' + : 'esbuild.mts'; const webpackConfigFileName = forWeb ? `extension-browser.webpack.config.js` : `extension.webpack.config.js`; const hasEsbuild = fs.existsSync(path.join(extensionPath, esbuildConfigFileName)); - const isWebPacked = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); + const hasWebpack = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); let input: Stream; let isBundled = false; if (hasEsbuild) { - input = fromLocalEsbuild(extensionPath, esbuildConfigFileName); + // Unlike webpack, esbuild only does bundling so we still want to run a separate type check step + input = es.merge( + fromLocalEsbuild(extensionPath, esbuildConfigFileName), + typeCheckExtension(extensionPath, forWeb), + ); isBundled = true; - } else if (isWebPacked) { + } else if (hasWebpack) { input = fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle); isBundled = true; } else { @@ -105,6 +110,11 @@ function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolea return input; } +function typeCheckExtension(extensionPath: string, forWeb: boolean): Stream { + const tsconfigFileName = forWeb ? 'tsconfig.browser.json' : 'tsconfig.json'; + const tsconfigPath = path.join(extensionPath, tsconfigFileName); + return createTsgoStream(tsconfigPath, { reporterId: 'extensions', noEmit: true }); +} function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, disableMangle: boolean): Stream { const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce'); @@ -267,6 +277,7 @@ function fromLocalEsbuild(extensionPath: string, esbuildConfigFileName: string): if (error) { return reject(error); } + const matches = (stderr || '').match(/\> (.+): error: (.+)?/g); fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), esbuildConfigFileName))} with ${matches ? matches.length : 0} errors.`); for (const match of matches || []) { @@ -632,17 +643,6 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string const extensionsPath = path.join(root, 'extensions'); -// Additional projects to run esbuild on. These typically build code for webviews -const esbuildMediaScripts = [ - 'ipynb/esbuild.mjs', - 'markdown-language-features/esbuild-notebook.mjs', - 'markdown-language-features/esbuild-preview.mjs', - 'markdown-math/esbuild.mjs', - 'mermaid-chat-features/esbuild-chat-webview.mjs', - 'notebook-renderers/esbuild.mjs', - 'simple-browser/esbuild-preview.mjs', -]; - export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string; outputRoot?: string }[]) { const webpack = require('webpack') as typeof import('webpack'); @@ -742,6 +742,18 @@ export async function esbuildExtensions(taskName: string, isWatch: boolean, scri await Promise.all(tasks); } + +// Additional projects to run esbuild on. These typically build code for webviews +const esbuildMediaScripts = [ + 'ipynb/esbuild.notebook.mts', + 'markdown-language-features/esbuild.notebook.mts', + 'markdown-language-features/esbuild.webview.mts', + 'markdown-math/esbuild.notebook.mts', + 'mermaid-chat-features/esbuild.webview.mts', + 'notebook-renderers/esbuild.notebook.mts', + 'simple-browser/esbuild.webview.mts', +]; + export function buildExtensionMedia(isWatch: boolean, outputRoot?: string): Promise { return esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ script: path.join(extensionsPath, p), diff --git a/build/lib/tsgo.ts b/build/lib/tsgo.ts index 3a245fe5cb6..9427b397926 100644 --- a/build/lib/tsgo.ts +++ b/build/lib/tsgo.ts @@ -12,7 +12,7 @@ const root = path.dirname(path.dirname(import.meta.dirname)); const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx'; const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; -export function spawnTsgo(projectPath: string, config: { reporterId: string }, onComplete?: () => Promise | void): Promise { +export function spawnTsgo(projectPath: string, config: { reporterId: string; noEmit?: boolean }, onComplete?: () => Promise | void): Promise { const reporter = createReporter(config.reporterId); let report: NodeJS.ReadWriteStream | undefined; @@ -33,7 +33,12 @@ export function spawnTsgo(projectPath: string, config: { reporterId: string }, o beginReport(false); - const args = ['tsgo', '--project', projectPath, '--pretty', 'false', '--sourceMap', '--inlineSources']; + const args = ['tsgo', '--project', projectPath, '--pretty', 'false']; + if (config.noEmit) { + args.push('--noEmit'); + } else { + args.push('--sourceMap', '--inlineSources'); + } const child = cp.spawn(npx, args, { cwd: root, stdio: ['ignore', 'pipe', 'pipe'], @@ -99,7 +104,7 @@ export function spawnTsgo(projectPath: string, config: { reporterId: string }, o }); } -export function createTsgoStream(projectPath: string, config: { reporterId: string }, onComplete?: () => Promise | void): NodeJS.ReadWriteStream { +export function createTsgoStream(projectPath: string, config: { reporterId: string; noEmit?: boolean }, onComplete?: () => Promise | void): NodeJS.ReadWriteStream { const stream = es.through(); spawnTsgo(projectPath, config, onComplete).then(() => { diff --git a/extensions/esbuild-extension-common.ts b/extensions/esbuild-extension-common.mts similarity index 100% rename from extensions/esbuild-extension-common.ts rename to extensions/esbuild-extension-common.mts diff --git a/extensions/esbuild-webview-common.mjs b/extensions/esbuild-webview-common.mts similarity index 62% rename from extensions/esbuild-webview-common.mjs rename to extensions/esbuild-webview-common.mts index 76d03abad7d..a170e5e344f 100644 --- a/extensions/esbuild-webview-common.mjs +++ b/extensions/esbuild-webview-common.mts @@ -2,27 +2,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check + /** - * @fileoverview Common build script for extension scripts used in in webviews. + * Common build script for extension scripts used in in webviews. */ import path from 'node:path'; import esbuild from 'esbuild'; -/** - * @typedef {Partial & { - * entryPoints: string[] | Record | { in: string, out: string }[]; - * outdir: string; - * }} BuildOptions - */ +export type BuildOptions = Partial & { + entryPoints: string[] | Record | { in: string; out: string }[]; + outdir: string; +}; /** * Build the source code once using esbuild. - * - * @param {BuildOptions} options - * @param {(outDir: string) => unknown} [didBuild] */ -async function build(options, didBuild) { +async function build(options: BuildOptions, didBuild?: (outDir: string) => unknown): Promise { await esbuild.build({ bundle: true, minify: true, @@ -38,11 +33,8 @@ async function build(options, didBuild) { /** * Build the source code once using esbuild, logging errors instead of throwing. - * - * @param {BuildOptions} options - * @param {(outDir: string) => unknown} [didBuild] */ -async function tryBuild(options, didBuild) { +async function tryBuild(options: BuildOptions, didBuild?: (outDir: string) => unknown): Promise { try { await build(options, didBuild); } catch (err) { @@ -50,17 +42,16 @@ async function tryBuild(options, didBuild) { } } -/** - * @param {{ - * srcDir: string; - * outdir: string; - * entryPoints: string[] | Record | { in: string, out: string }[]; - * additionalOptions?: Partial - * }} config - * @param {string[]} args - * @param {(outDir: string) => unknown} [didBuild] - */ -export async function run(config, args, didBuild) { +export async function run( + config: { + srcDir: string; + outdir: string; + entryPoints: BuildOptions['entryPoints']; + additionalOptions?: Partial; + }, + args: string[], + didBuild?: (outDir: string) => unknown +): Promise { let outdir = config.outdir; const outputRootIndex = args.indexOf('--outputRoot'); if (outputRootIndex >= 0) { @@ -69,8 +60,7 @@ export async function run(config, args, didBuild) { outdir = path.join(outputRoot, outputDirName); } - /** @type {BuildOptions} */ - const resolvedOptions = { + const resolvedOptions: BuildOptions = { entryPoints: config.entryPoints, outdir, logOverride: { diff --git a/extensions/ipynb/esbuild.mjs b/extensions/ipynb/esbuild.notebook.mts similarity index 90% rename from extensions/ipynb/esbuild.mjs rename to extensions/ipynb/esbuild.notebook.mts index 3003959c1eb..4d45f388574 100644 --- a/extensions/ipynb/esbuild.mjs +++ b/extensions/ipynb/esbuild.notebook.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'node:path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'notebook-src'); const outDir = path.join(import.meta.dirname, 'notebook-out'); diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index 89a24e5cc15..7396e270a47 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -158,7 +158,7 @@ "scripts": { "compile": "npx gulp compile-extension:ipynb && npm run build-notebook", "watch": "npx gulp watch-extension:ipynb", - "build-notebook": "node ./esbuild.mjs" + "build-notebook": "node ./esbuild.notebook.mts" }, "dependencies": { "@enonic/fnv-plus": "^1.3.0", diff --git a/extensions/markdown-language-features/esbuild-browser.ts b/extensions/markdown-language-features/esbuild.browser.mts similarity index 96% rename from extensions/markdown-language-features/esbuild-browser.ts rename to extensions/markdown-language-features/esbuild.browser.mts index 2c46e390c06..ddf0c5a99dc 100644 --- a/extensions/markdown-language-features/esbuild-browser.ts +++ b/extensions/markdown-language-features/esbuild.browser.mts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist', 'browser'); diff --git a/extensions/markdown-language-features/esbuild.ts b/extensions/markdown-language-features/esbuild.mts similarity index 95% rename from extensions/markdown-language-features/esbuild.ts rename to extensions/markdown-language-features/esbuild.mts index 67835c9a1d7..a1cf6eb5fa8 100644 --- a/extensions/markdown-language-features/esbuild.ts +++ b/extensions/markdown-language-features/esbuild.mts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist'); diff --git a/extensions/markdown-language-features/esbuild-notebook.mjs b/extensions/markdown-language-features/esbuild.notebook.mts similarity index 90% rename from extensions/markdown-language-features/esbuild-notebook.mjs rename to extensions/markdown-language-features/esbuild.notebook.mts index 933e77d21a5..d9d511c5e82 100644 --- a/extensions/markdown-language-features/esbuild-notebook.mjs +++ b/extensions/markdown-language-features/esbuild.notebook.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'notebook'); const outDir = path.join(import.meta.dirname, 'notebook-out'); diff --git a/extensions/markdown-language-features/esbuild-preview.mjs b/extensions/markdown-language-features/esbuild.webview.mts similarity index 90% rename from extensions/markdown-language-features/esbuild-preview.mjs rename to extensions/markdown-language-features/esbuild.webview.mts index 1d3fc48b9bc..c4141cf50a5 100644 --- a/extensions/markdown-language-features/esbuild-preview.mjs +++ b/extensions/markdown-language-features/esbuild.webview.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'preview-src'); const outDir = path.join(import.meta.dirname, 'media'); diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index edffec39d74..c9d0de68d86 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -757,14 +757,20 @@ ] }, "scripts": { - "compile": "gulp compile-extension:markdown-language-features-languageService && gulp compile-extension:markdown-language-features && npm run build-preview && npm run build-notebook", - "watch": "npm run build-preview && gulp watch-extension:markdown-language-features watch-extension:markdown-language-features-languageService", - "vscode:prepublish": "npm run build-ext && npm run build-preview", - "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:markdown-language-features ./tsconfig.json", - "build-notebook": "node ./esbuild-notebook.mjs", - "build-preview": "node ./esbuild-preview.mjs", - "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", - "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch" + "compile": "npm-run-all2 -lp build-ext build-webview build-notebook", + "watch": "npm-run-all2 -lp watch-ext watch-webview watch-notebook", + "build-ext": "gulp compile-extension:markdown-language-features", + "watch-ext": "gulp watch-extension:markdown-language-features", + "build-notebook": "node ./esbuild.notebook.mts", + "watch-notebook": "node ./esbuild.notebook.mts --watch", + "build-webview": "node ./esbuild.webview.mts", + "watch-webview": "node ./esbuild.webview.mts --watch", + "compile-web": "npm-run-all2 -lp bundle-web typecheck-web", + "bundle-web": "node ./esbuild.browser.mts", + "typecheck-web": "tsgo --project ./tsconfig.browser.json --noEmit", + "watch-web": "npm-run-all2 -lp watch-bundle-web watch-typecheck-web", + "watch-bundle-web": "node ./esbuild.browser.mts --watch", + "watch-typecheck-web": "tsgo --project ./tsconfig.browser.json --noEmit --watch" }, "dependencies": { "@vscode/extension-telemetry": "^0.9.8", diff --git a/extensions/markdown-language-features/tsconfig.browser.json b/extensions/markdown-language-features/tsconfig.browser.json index dbacbb22fdf..790349e7fec 100644 --- a/extensions/markdown-language-features/tsconfig.browser.json +++ b/extensions/markdown-language-features/tsconfig.browser.json @@ -3,5 +3,8 @@ "compilerOptions": {}, "exclude": [ "./src/test/**" + ], + "files": [ + "./src/extension.browser.ts" ] } diff --git a/extensions/media-preview/esbuild-browser.ts b/extensions/markdown-math/esbuild.browser.mts similarity index 93% rename from extensions/media-preview/esbuild-browser.ts rename to extensions/markdown-math/esbuild.browser.mts index a2659e5ff46..e3fa7792d05 100644 --- a/extensions/media-preview/esbuild-browser.ts +++ b/extensions/markdown-math/esbuild.browser.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist', 'browser'); diff --git a/extensions/mermaid-chat-features/esbuild.ts b/extensions/markdown-math/esbuild.mts similarity index 91% rename from extensions/mermaid-chat-features/esbuild.ts rename to extensions/markdown-math/esbuild.mts index 232f589197b..5fafb57ab75 100644 --- a/extensions/mermaid-chat-features/esbuild.ts +++ b/extensions/markdown-math/esbuild.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist'); diff --git a/extensions/markdown-math/esbuild.mjs b/extensions/markdown-math/esbuild.notebook.mts similarity index 89% rename from extensions/markdown-math/esbuild.mjs rename to extensions/markdown-math/esbuild.notebook.mts index 910acbb06a8..c5ac472b3bd 100644 --- a/extensions/markdown-math/esbuild.mjs +++ b/extensions/markdown-math/esbuild.notebook.mts @@ -2,18 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -//@ts-check - -import path from 'path'; import fse from 'fs-extra'; -import { run } from '../esbuild-webview-common.mjs'; - -const args = process.argv.slice(2); +import path from 'path'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'notebook'); const outDir = path.join(import.meta.dirname, 'notebook-out'); -function postBuild(outDir) { +function postBuild(outDir: string) { fse.copySync( path.join(import.meta.dirname, 'node_modules', 'katex', 'dist', 'katex.min.css'), path.join(outDir, 'katex.min.css')); diff --git a/extensions/markdown-math/package.json b/extensions/markdown-math/package.json index 5af72e0b513..4a9be208ea0 100644 --- a/extensions/markdown-math/package.json +++ b/extensions/markdown-math/package.json @@ -108,7 +108,7 @@ "scripts": { "compile": "npm run build-notebook", "watch": "npm run build-notebook", - "build-notebook": "node ./esbuild.mjs" + "build-notebook": "node ./esbuild.mts" }, "devDependencies": { "@types/markdown-it": "^0.0.0", diff --git a/extensions/markdown-math/tsconfig.browser.json b/extensions/markdown-math/tsconfig.browser.json new file mode 100644 index 00000000000..24f4ba27a3d --- /dev/null +++ b/extensions/markdown-math/tsconfig.browser.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": [], + "typeRoots": [ + "./node_modules/@types" + ] + } +} diff --git a/extensions/markdown-math/esbuild-browser.ts b/extensions/media-preview/esbuild.browser.mts similarity index 93% rename from extensions/markdown-math/esbuild-browser.ts rename to extensions/media-preview/esbuild.browser.mts index a2659e5ff46..e3fa7792d05 100644 --- a/extensions/markdown-math/esbuild-browser.ts +++ b/extensions/media-preview/esbuild.browser.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist', 'browser'); diff --git a/extensions/markdown-math/esbuild.ts b/extensions/media-preview/esbuild.mts similarity index 91% rename from extensions/markdown-math/esbuild.ts rename to extensions/media-preview/esbuild.mts index 232f589197b..5fafb57ab75 100644 --- a/extensions/markdown-math/esbuild.ts +++ b/extensions/media-preview/esbuild.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist'); diff --git a/extensions/mermaid-chat-features/esbuild-browser.ts b/extensions/mermaid-chat-features/esbuild.browser.mts similarity index 93% rename from extensions/mermaid-chat-features/esbuild-browser.ts rename to extensions/mermaid-chat-features/esbuild.browser.mts index a2659e5ff46..e3fa7792d05 100644 --- a/extensions/mermaid-chat-features/esbuild-browser.ts +++ b/extensions/mermaid-chat-features/esbuild.browser.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist', 'browser'); diff --git a/extensions/media-preview/esbuild.ts b/extensions/mermaid-chat-features/esbuild.mts similarity index 91% rename from extensions/media-preview/esbuild.ts rename to extensions/mermaid-chat-features/esbuild.mts index 232f589197b..5fafb57ab75 100644 --- a/extensions/media-preview/esbuild.ts +++ b/extensions/mermaid-chat-features/esbuild.mts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.ts'; +import { run } from '../esbuild-extension-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'dist'); diff --git a/extensions/mermaid-chat-features/esbuild-chat-webview.mjs b/extensions/mermaid-chat-features/esbuild.webview.mts similarity index 92% rename from extensions/mermaid-chat-features/esbuild-chat-webview.mjs rename to extensions/mermaid-chat-features/esbuild.webview.mts index e242585b1c3..41cfa12139e 100644 --- a/extensions/mermaid-chat-features/esbuild-chat-webview.mjs +++ b/extensions/mermaid-chat-features/esbuild.webview.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'chat-webview-src'); const outDir = path.join(import.meta.dirname, 'chat-webview-out'); diff --git a/extensions/mermaid-chat-features/package.json b/extensions/mermaid-chat-features/package.json index 16b6a03ce48..64c31782461 100644 --- a/extensions/mermaid-chat-features/package.json +++ b/extensions/mermaid-chat-features/package.json @@ -117,7 +117,7 @@ "watch": "npm run build-chat-webview && gulp watch-extension:mermaid-chat-features", "vscode:prepublish": "npm run build-ext && npm run build-chat-webview", "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:mermaid-chat-features", - "build-chat-webview": "node ./esbuild-chat-webview.mjs", + "build-chat-webview": "node ./esbuild.webview.mts", "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, diff --git a/extensions/notebook-renderers/esbuild.mjs b/extensions/notebook-renderers/esbuild.notebook.mts similarity index 90% rename from extensions/notebook-renderers/esbuild.mjs rename to extensions/notebook-renderers/esbuild.notebook.mts index 890aacd19bf..ab241d8601d 100644 --- a/extensions/notebook-renderers/esbuild.mjs +++ b/extensions/notebook-renderers/esbuild.notebook.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'src'); const outDir = path.join(import.meta.dirname, 'renderer-out'); diff --git a/extensions/notebook-renderers/package.json b/extensions/notebook-renderers/package.json index 77c042ee663..715cfc03e85 100644 --- a/extensions/notebook-renderers/package.json +++ b/extensions/notebook-renderers/package.json @@ -44,7 +44,7 @@ "scripts": { "compile": "npx gulp compile-extension:notebook-renderers && npm run build-notebook", "watch": "npx gulp compile-watch:notebook-renderers", - "build-notebook": "node ./esbuild.mjs" + "build-notebook": "node ./esbuild.notebook.mts" }, "devDependencies": { "@types/jsdom": "^21.1.0", diff --git a/extensions/simple-browser/esbuild-preview.mjs b/extensions/simple-browser/esbuild.webview.mts similarity index 92% rename from extensions/simple-browser/esbuild-preview.mjs rename to extensions/simple-browser/esbuild.webview.mts index 3ce58360a30..0f91843610b 100644 --- a/extensions/simple-browser/esbuild-preview.mjs +++ b/extensions/simple-browser/esbuild.webview.mts @@ -2,9 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check import path from 'path'; -import { run } from '../esbuild-webview-common.mjs'; +import { run } from '../esbuild-webview-common.mts'; const srcDir = path.join(import.meta.dirname, 'preview-src'); const outDir = path.join(import.meta.dirname, 'media'); diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 0d558eeebf6..5fc5cee96f0 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -71,7 +71,7 @@ "watch": "npm run build-preview && gulp watch-extension:simple-browser", "vscode:prepublish": "npm run build-ext && npm run build-preview", "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:simple-browser ./tsconfig.json", - "build-preview": "node ./esbuild-preview.mjs", + "build-preview": "node ./esbuild-preview.mts", "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, From 438d64aded1d8f512d7c09e94611a40efa16c929 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 11 Feb 2026 18:00:08 -0600 Subject: [PATCH 49/72] Improve tips styling and behavior (#294702) --- .../chat/browser/actions/chatTitleActions.ts | 12 ----------- .../contrib/chat/browser/chat.contribution.ts | 6 +----- .../contrib/chat/browser/chatTipService.ts | 20 ++++++++++++++----- .../chatContentParts/media/chatTipContent.css | 9 +++++++-- .../preferences/browser/settingsLayout.ts | 3 +-- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 56b2429570c..a442be07b03 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -25,9 +25,7 @@ import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatService } from '. import { isResponseVM } from '../../common/model/chatViewModel.js'; import { ChatModeKind } from '../../common/constants.js'; import { IChatAccessibilityService, IChatWidgetService } from '../chat.js'; -import { triggerConfetti } from '../widget/chatConfetti.js'; import { CHAT_CATEGORY } from './chatActions.js'; -import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; export const MarkUnhelpfulActionId = 'workbench.action.chat.markUnhelpful'; const enableFeedbackConfig = 'config.telemetry.feedback.enabled'; @@ -77,16 +75,6 @@ export function registerChatTitleActions() { }); item.setVote(ChatAgentVoteDirection.Up); item.setVoteDownReason(undefined); - - const configurationService = accessor.get(IConfigurationService); - const accessibilityService = accessor.get(IAccessibilityService); - if (configurationService.getValue('chat.confettiOnThumbsUp') && !accessibilityService.isMotionReduced()) { - const chatWidgetService = accessor.get(IChatWidgetService); - const widget = chatWidgetService.getWidgetBySessionResource(item.session.sessionResource); - if (widget) { - triggerConfetti(widget.domNode); - } - } } }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index f51de3f44f1..d9a0dbe7a6a 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -287,11 +287,7 @@ configurationRegistry.registerConfiguration({ mode: 'auto' } }, - 'chat.confettiOnThumbsUp': { - type: 'boolean', - description: nls.localize('chat.confettiOnThumbsUp', "Controls whether a confetti animation is shown when clicking the thumbs up button on a chat response."), - default: false, - }, + 'chat.experimental.detectParticipant.enabled': { type: 'boolean', deprecationMessage: nls.localize('chat.experimental.detectParticipant.enabled.deprecated', "This setting is deprecated. Please use `chat.detectParticipant.enabled` instead."), diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 726d06a3df5..bea60daa877 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -26,6 +26,7 @@ export const IChatTipService = createDecorator('chatTipService' export interface IChatTip { readonly id: string; readonly content: MarkdownString; + readonly enabledCommands?: readonly string[]; } export interface IChatTipService { @@ -67,7 +68,7 @@ export interface IChatTipService { /** * Dismisses the current tip and allows a new one to be picked for the same request. - * The dismissed tip will not be shown again in this workspace. + * The dismissed tip will not be shown again in this profile. */ dismissTip(): void; @@ -543,7 +544,7 @@ export class ChatTipService extends Disposable implements IChatTipService { if (this._shownTip) { const dismissed = this._getDismissedTipIds(); dismissed.push(this._shownTip.id); - this._storageService.store(ChatTipService._DISMISSED_TIP_KEY, JSON.stringify(dismissed), StorageScope.WORKSPACE, StorageTarget.MACHINE); + this._storageService.store(ChatTipService._DISMISSED_TIP_KEY, JSON.stringify(dismissed), StorageScope.PROFILE, StorageTarget.MACHINE); } this._hasShownRequestTip = false; this._shownTip = undefined; @@ -552,7 +553,7 @@ export class ChatTipService extends Disposable implements IChatTipService { } private _getDismissedTipIds(): string[] { - const raw = this._storageService.get(ChatTipService._DISMISSED_TIP_KEY, StorageScope.WORKSPACE); + const raw = this._storageService.get(ChatTipService._DISMISSED_TIP_KEY, StorageScope.PROFILE); if (!raw) { return []; } @@ -600,6 +601,14 @@ export class ChatTipService extends Disposable implements IChatTipService { return this._createTip(this._shownTip); } + // A new request arrived while we already showed a tip, hide the old one + if (this._hasShownRequestTip && this._tipRequestId && this._tipRequestId !== requestId) { + this._shownTip = undefined; + this._tipRequestId = undefined; + this._onDidDismissTip.fire(); + return undefined; + } + // Only show one tip per session if (this._hasShownRequestTip) { return undefined; @@ -643,7 +652,7 @@ export class ChatTipService extends Disposable implements IChatTipService { let selectedTip: ITipDefinition | undefined; // Determine where to start in the catalog based on the last-shown tip. - const lastTipId = this._storageService.get(ChatTipService._LAST_TIP_ID_KEY, StorageScope.WORKSPACE); + const lastTipId = this._storageService.get(ChatTipService._LAST_TIP_ID_KEY, StorageScope.PROFILE); const lastCatalogIndex = lastTipId ? TIP_CATALOG.findIndex(tip => tip.id === lastTipId) : -1; const startIndex = lastCatalogIndex === -1 ? 0 : (lastCatalogIndex + 1) % TIP_CATALOG.length; @@ -679,7 +688,7 @@ export class ChatTipService extends Disposable implements IChatTipService { } // Persist the selected tip id so the next use advances to the following one. - this._storageService.store(ChatTipService._LAST_TIP_ID_KEY, selectedTip.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); + this._storageService.store(ChatTipService._LAST_TIP_ID_KEY, selectedTip.id, StorageScope.PROFILE, StorageTarget.USER); // Record that we've shown a tip this session this._hasShownRequestTip = sourceId !== 'welcome'; @@ -713,6 +722,7 @@ export class ChatTipService extends Disposable implements IChatTipService { return { id: tipDef.id, content: markdown, + enabledCommands: tipDef.enabledCommands, }; } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css index d99c14f550b..30d8e85c097 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css @@ -10,11 +10,13 @@ margin-bottom: 8px; padding: 6px 10px; border-radius: 4px; - border: 1px solid var(--vscode-focusBorder); + border: 1px solid var(--vscode-editorWidget-border, var(--vscode-input-border, transparent)); background-color: var(--vscode-editorWidget-background); font-size: var(--vscode-chat-font-size-body-s); font-family: var(--vscode-chat-font-family, inherit); color: var(--vscode-descriptionForeground); + position: relative; + overflow: hidden; } .interactive-item-container .chat-tip-widget .codicon-lightbulb { @@ -59,10 +61,13 @@ font-size: var(--vscode-chat-font-size-body-s); font-family: var(--vscode-chat-font-family, inherit); color: var(--vscode-descriptionForeground); + position: relative; + overflow: hidden; } .chat-getting-started-tip-container .chat-tip-widget .codicon-lightbulb { - display: none; + font-size: 12px; + color: var(--vscode-notificationsWarningIcon-foreground); } .chat-getting-started-tip-container .chat-tip-widget .rendered-markdown p { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index bc3a037c246..4b8f27cdd9b 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -209,8 +209,7 @@ export const tocData: ITOCEntry = { 'chat.notifyWindow*', 'chat.statusWidget.*', 'chat.tips.*', - 'chat.unifiedAgentsBar.*', - 'chat.confettiOnThumbsUp' + 'chat.unifiedAgentsBar.*' ] }, { From dd8c734defdb6252a24f0f5e92a76fc5b09e17ce Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:38:02 -0800 Subject: [PATCH 50/72] Simplify quick input animations with CSS @keyframes (#294737) * Initial plan * Simplify animations using CSS @keyframes instead of transitions Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> * Remove accidentally committed codicon.ttf binary Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> --- .../quickinput/browser/media/quickInput.css | 36 +++++++---- .../browser/quickInputController.ts | 63 +++---------------- 2 files changed, 32 insertions(+), 67 deletions(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index b424a9e2377..b9fe8c98023 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -528,22 +528,34 @@ /* Entrance animation */ /* Duration matches QUICK_INPUT_OPEN_DURATION constant (150ms) in quickInputController.ts */ -.quick-input-widget.animating-entrance { - opacity: 0; - transform: translateY(-8px); - transition: opacity 150ms cubic-bezier(0.1, 0.9, 0.2, 1), transform 150ms cubic-bezier(0.1, 0.9, 0.2, 1); - will-change: opacity, transform; +@keyframes quick-input-entrance { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } } -.quick-input-widget.animating-entrance.animating-entrance-target { - opacity: 1; - transform: translateY(0); +.quick-input-widget.animating-entrance { + animation: quick-input-entrance 150ms cubic-bezier(0.1, 0.9, 0.2, 1) forwards; } /* Exit animation */ /* Duration matches QUICK_INPUT_CLOSE_DURATION constant (50ms) in quickInputController.ts */ -.quick-input-widget.animating-exit { - opacity: 0; - transform: translateY(-8px); - transition: opacity 50ms cubic-bezier(0.9, 0.1, 1, 0.2), transform 50ms cubic-bezier(0.9, 0.1, 1, 0.2); +@keyframes quick-input-exit { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-8px); + } +} + +.quick-input-widget.animating-exit { + animation: quick-input-exit 50ms cubic-bezier(0.9, 0.1, 1, 0.2) forwards; } diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 8b5f998f9e6..2059c921f7c 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -733,34 +733,11 @@ export class QuickInputController extends Disposable { // Animate entrance: fade in + slide down (only when first appearing) if (!wasVisible && !isMotionReduced(ui.container)) { ui.container.classList.add('animating-entrance'); - // Trigger reflow so the browser registers the initial state - void ui.container.offsetHeight; - // Now add the target class to trigger the transition - ui.container.classList.add('animating-entrance-target'); - let entranceCleanedUp = false; - const window = dom.getWindow(ui.container); - let entranceCleanupTimeout: number | undefined; - const cleanupEntranceTransition = () => { - if (entranceCleanedUp) { - return; - } - entranceCleanedUp = true; - ui.container.classList.remove('animating-entrance', 'animating-entrance-target'); - ui.container.removeEventListener('transitionend', onTransitionEnd); - if (entranceCleanupTimeout !== undefined) { - clearTimeout(entranceCleanupTimeout); - entranceCleanupTimeout = undefined; - } + const onAnimationEnd = () => { + ui.container.classList.remove('animating-entrance'); + ui.container.removeEventListener('animationend', onAnimationEnd); }; - const onTransitionEnd = (e: TransitionEvent) => { - if (e.target !== ui.container) { - return; // Ignore transitions from descendant elements - } - cleanupEntranceTransition(); - }; - ui.container.addEventListener('transitionend', onTransitionEnd); - // Safety timeout in case transitionend doesn't fire - entranceCleanupTimeout = window.setTimeout(cleanupEntranceTransition, QUICK_INPUT_OPEN_DURATION + 50); + ui.container.addEventListener('animationend', onAnimationEnd); } } @@ -830,42 +807,18 @@ export class QuickInputController extends Disposable { if (container) { // Animate exit: fade out + slide up (faster than open) if (!isMotionReduced(container)) { - let exitCancelled = false; - const window = dom.getWindow(container); - let exitCleanupTimeout: number | undefined; container.classList.add('animating-exit'); - const onDone = () => { - if (exitCancelled) { - return; - } - exitCancelled = true; + const onAnimationEnd = () => { // Set display after animation completes to actually hide the element container.style.display = 'none'; container.classList.remove('animating-exit'); - container.removeEventListener('transitionend', onTransitionEnd); - if (exitCleanupTimeout !== undefined) { - clearTimeout(exitCleanupTimeout); - exitCleanupTimeout = undefined; - } - }; - const onTransitionEnd = (e: TransitionEvent) => { - if (e.target !== container) { - return; // Ignore transitions from descendant elements - } - onDone(); + container.removeEventListener('animationend', onAnimationEnd); }; this._cancelExitAnimation = () => { - exitCancelled = true; container.classList.remove('animating-exit'); - container.removeEventListener('transitionend', onTransitionEnd); - if (exitCleanupTimeout !== undefined) { - clearTimeout(exitCleanupTimeout); - exitCleanupTimeout = undefined; - } + container.removeEventListener('animationend', onAnimationEnd); }; - container.addEventListener('transitionend', onTransitionEnd); - // Safety timeout in case transitionend doesn't fire - exitCleanupTimeout = window.setTimeout(onDone, QUICK_INPUT_CLOSE_DURATION + 50); + container.addEventListener('animationend', onAnimationEnd); } else { container.style.display = 'none'; } From cb714c7c9fe9ed391c24d92b290047dfeffbf3f3 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:38:06 -0800 Subject: [PATCH 51/72] Addressing code review comments --- build/gulpfile.extensions.ts | 14 ++++-- build/lib/extensions.ts | 14 ++++-- build/lib/tsgo.ts | 47 +++++-------------- extensions/markdown-math/package.json | 2 +- .../markdown-math/tsconfig.browser.json | 2 +- .../media-preview/tsconfig.browser.json | 3 ++ .../tsconfig.browser.json | 3 ++ extensions/simple-browser/package.json | 8 ++-- 8 files changed, 43 insertions(+), 50 deletions(-) create mode 100644 extensions/media-preview/tsconfig.browser.json create mode 100644 extensions/mermaid-chat-features/tsconfig.browser.json diff --git a/build/gulpfile.extensions.ts b/build/gulpfile.extensions.ts index f48738be53a..cae158ea590 100644 --- a/build/gulpfile.extensions.ts +++ b/build/gulpfile.extensions.ts @@ -166,7 +166,7 @@ const tasks = compilations.map(function (tsconfigFile) { const compileTask = task.define(`compile-extension:${name}`, task.series(cleanTask, async () => { const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'], { dot: true })); const copyNonTs = util.streamToPromise(nonts.pipe(gulp.dest(out))); - const tsgo = spawnTsgo(absolutePath, { reporterId: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl)); + const tsgo = spawnTsgo(absolutePath, { taskName: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl)); await Promise.all([copyNonTs, tsgo]); })); @@ -175,7 +175,7 @@ const tasks = compilations.map(function (tsconfigFile) { const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'], { dot: true })); const watchInput = watcher(src, { ...srcOpts, ...{ readDelay: 200 } }); const watchNonTs = watchInput.pipe(filter(['**', '!**/*.ts'], { dot: true })).pipe(gulp.dest(out)); - const tsgoStream = watchInput.pipe(util.debounce(() => createTsgoStream(absolutePath, { reporterId: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl)), 200)); + const tsgoStream = watchInput.pipe(util.debounce(() => createTsgoStream(absolutePath, { taskName: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl)), 200)); const watchStream = es.merge(nonts.pipe(gulp.dest(out)), watchNonTs, tsgoStream); return watchStream; @@ -276,9 +276,9 @@ gulp.task(watchWebExtensionsTask); async function buildWebExtensions(isWatch: boolean): Promise { const extensionsPath = path.join(root, 'extensions'); - // Find all esbuild-browser.ts files + // Find all esbuild.browser.mts files const esbuildConfigLocations = await nodeUtil.promisify(glob)( - path.join(extensionsPath, '**', 'esbuild-browser.ts'), + path.join(extensionsPath, '**', 'esbuild.browser.mts'), { ignore: ['**/node_modules'] } ); @@ -293,7 +293,11 @@ async function buildWebExtensions(isWatch: boolean): Promise { // Esbuild for extensions if (esbuildConfigLocations.length > 0) { - promises.push(ext.esbuildExtensions('packaging web extension (esbuild)', isWatch, esbuildConfigLocations.map(script => ({ script })))); + promises.push( + ext.esbuildExtensions('packaging web extension (esbuild)', isWatch, esbuildConfigLocations.map(script => ({ script }))), + // Also run type check on extensions + ...esbuildConfigLocations.map(script => ext.typeCheckExtension(path.dirname(script), true)) + ); } // Run webpack for remaining extensions diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 3e83cab64f9..fac7946fc98 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -25,7 +25,7 @@ import { getProductionDependencies } from './dependencies.ts'; import { type IExtensionDefinition, getExtensionStream } from './builtInExtensions.ts'; import { getVersion } from './getVersion.ts'; import { fetchUrls, fetchGithub } from './fetch.ts'; -import { createTsgoStream } from './tsgo.ts'; +import { createTsgoStream, spawnTsgo } from './tsgo.ts'; import vzip from 'gulp-vinyl-zip'; import { createRequire } from 'module'; @@ -85,7 +85,7 @@ function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolea // Unlike webpack, esbuild only does bundling so we still want to run a separate type check step input = es.merge( fromLocalEsbuild(extensionPath, esbuildConfigFileName), - typeCheckExtension(extensionPath, forWeb), + typeCheckExtensionStream(extensionPath, forWeb), ); isBundled = true; } else if (hasWebpack) { @@ -110,10 +110,16 @@ function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolea return input; } -function typeCheckExtension(extensionPath: string, forWeb: boolean): Stream { +export function typeCheckExtension(extensionPath: string, forWeb: boolean): Promise { const tsconfigFileName = forWeb ? 'tsconfig.browser.json' : 'tsconfig.json'; const tsconfigPath = path.join(extensionPath, tsconfigFileName); - return createTsgoStream(tsconfigPath, { reporterId: 'extensions', noEmit: true }); + return spawnTsgo(tsconfigPath, { taskName: 'typechecking extension (tsgo)', noEmit: true }); +} + +export function typeCheckExtensionStream(extensionPath: string, forWeb: boolean): Stream { + const tsconfigFileName = forWeb ? 'tsconfig.browser.json' : 'tsconfig.json'; + const tsconfigPath = path.join(extensionPath, tsconfigFileName); + return createTsgoStream(tsconfigPath, { taskName: 'typechecking extension (tsgo)', noEmit: true }); } function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, disableMangle: boolean): Stream { diff --git a/build/lib/tsgo.ts b/build/lib/tsgo.ts index 9427b397926..421f4c1cc1b 100644 --- a/build/lib/tsgo.ts +++ b/build/lib/tsgo.ts @@ -3,35 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import ansiColors from 'ansi-colors'; import * as cp from 'child_process'; import es from 'event-stream'; +import fancyLog from 'fancy-log'; import * as path from 'path'; -import { createReporter } from './reporter.ts'; const root = path.dirname(path.dirname(import.meta.dirname)); const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx'; const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; -export function spawnTsgo(projectPath: string, config: { reporterId: string; noEmit?: boolean }, onComplete?: () => Promise | void): Promise { - const reporter = createReporter(config.reporterId); - let report: NodeJS.ReadWriteStream | undefined; - - const beginReport = (emitError: boolean) => { - if (report) { - report.end(); +export function spawnTsgo(projectPath: string, config: { taskName: string; noEmit?: boolean }, onComplete?: () => Promise | void): Promise { + function reporter(stdError: string) { + const matches = (stdError || '').match(/^error \w+: (.+)?/g); + fancyLog(`Finished ${ansiColors.green(config.taskName)} ${projectPath} with ${matches ? matches.length : 0} errors.`); + for (const match of matches || []) { + fancyLog.error(match); } - report = reporter.end(emitError); - }; - - const endReport = () => { - if (!report) { - return; - } - report.end(); - report = undefined; - }; - - beginReport(false); + } const args = ['tsgo', '--project', projectPath, '--pretty', 'false']; if (config.noEmit) { @@ -52,23 +41,13 @@ export function spawnTsgo(projectPath: string, config: { reporterId: string; noE return; } if (/Starting compilation|File change detected/i.test(trimmed)) { - beginReport(false); return; } if (/Compilation complete/i.test(trimmed)) { - endReport(); return; } - const match = /(.*\(\d+,\d+\): )(.*: )(.*)/.exec(trimmed); - - if (match) { - const fullpath = path.isAbsolute(match[1]) ? match[1] : path.join(root, match[1]); - const message = match[3]; - reporter(fullpath + message); - } else { - reporter(trimmed); - } + reporter(trimmed); }; const handleData = (data: Buffer) => { @@ -89,7 +68,7 @@ export function spawnTsgo(projectPath: string, config: { reporterId: string; noE handleLine(buffer); buffer = ''; } - endReport(); + if (code === 0) { Promise.resolve(onComplete?.()).then(() => resolve(), reject); } else { @@ -98,15 +77,13 @@ export function spawnTsgo(projectPath: string, config: { reporterId: string; noE }); child.on('error', err => { - endReport(); reject(err); }); }); } -export function createTsgoStream(projectPath: string, config: { reporterId: string; noEmit?: boolean }, onComplete?: () => Promise | void): NodeJS.ReadWriteStream { +export function createTsgoStream(projectPath: string, config: { taskName: string; noEmit?: boolean }, onComplete?: () => Promise | void): NodeJS.ReadWriteStream { const stream = es.through(); - spawnTsgo(projectPath, config, onComplete).then(() => { stream.emit('end'); }).catch(() => { diff --git a/extensions/markdown-math/package.json b/extensions/markdown-math/package.json index 4a9be208ea0..19f20fcd04a 100644 --- a/extensions/markdown-math/package.json +++ b/extensions/markdown-math/package.json @@ -108,7 +108,7 @@ "scripts": { "compile": "npm run build-notebook", "watch": "npm run build-notebook", - "build-notebook": "node ./esbuild.mts" + "build-notebook": "node ./esbuild.notebook.mts" }, "devDependencies": { "@types/markdown-it": "^0.0.0", diff --git a/extensions/markdown-math/tsconfig.browser.json b/extensions/markdown-math/tsconfig.browser.json index 24f4ba27a3d..715a07ebfb8 100644 --- a/extensions/markdown-math/tsconfig.browser.json +++ b/extensions/markdown-math/tsconfig.browser.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.json", + "extends": "./tsconfig.json", "compilerOptions": { "types": [], "typeRoots": [ diff --git a/extensions/media-preview/tsconfig.browser.json b/extensions/media-preview/tsconfig.browser.json new file mode 100644 index 00000000000..3694afc77ee --- /dev/null +++ b/extensions/media-preview/tsconfig.browser.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/extensions/mermaid-chat-features/tsconfig.browser.json b/extensions/mermaid-chat-features/tsconfig.browser.json new file mode 100644 index 00000000000..3694afc77ee --- /dev/null +++ b/extensions/mermaid-chat-features/tsconfig.browser.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig" +} diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 5fc5cee96f0..d372992c897 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -67,11 +67,11 @@ ] }, "scripts": { - "compile": "gulp compile-extension:simple-browser && npm run build-preview", - "watch": "npm run build-preview && gulp watch-extension:simple-browser", - "vscode:prepublish": "npm run build-ext && npm run build-preview", + "compile": "gulp compile-extension:simple-browser && npm run build-webview", + "watch": "npm run build-webview && gulp watch-extension:simple-browser", + "vscode:prepublish": "npm run build-ext && npm run build-webview", "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:simple-browser ./tsconfig.json", - "build-preview": "node ./esbuild-preview.mts", + "build-webview": "node ./esbuild.webview.mts", "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, From 5423e1a2a56f27991232797f64741cda5ec1d854 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:42:08 -0800 Subject: [PATCH 52/72] Update ignore file list --- extensions/markdown-language-features/.vscodeignore | 8 ++------ extensions/markdown-math/.vscodeignore | 7 ++----- extensions/media-preview/.vscodeignore | 5 ++--- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/extensions/markdown-language-features/.vscodeignore b/extensions/markdown-language-features/.vscodeignore index a5b7a3ec72c..315b1d78770 100644 --- a/extensions/markdown-language-features/.vscodeignore +++ b/extensions/markdown-language-features/.vscodeignore @@ -2,16 +2,12 @@ test/** test-workspace/** src/** notebook/** -tsconfig.json -tsconfig.*.json +tsconfig*.json +esbuild* out/test/** out/** -extension.webpack.config.js -extension-browser.webpack.config.js cgmanifest.json package-lock.json preview-src/** -webpack.config.js -esbuild-* .gitignore **/*.d.ts diff --git a/extensions/markdown-math/.vscodeignore b/extensions/markdown-math/.vscodeignore index 5df4a1cb8ab..90098845502 100644 --- a/extensions/markdown-math/.vscodeignore +++ b/extensions/markdown-math/.vscodeignore @@ -1,10 +1,7 @@ src/** notebook/** -extension-browser.webpack.config.js -extension.webpack.config.js -esbuild.* +tsconfig*.json +esbuild* cgmanifest.json package-lock.json -webpack.config.js -tsconfig.json .gitignore diff --git a/extensions/media-preview/.vscodeignore b/extensions/media-preview/.vscodeignore index 532c87f6f2e..8621eb9e9f4 100644 --- a/extensions/media-preview/.vscodeignore +++ b/extensions/media-preview/.vscodeignore @@ -1,10 +1,9 @@ test/** src/** -tsconfig.json +tsconfig*.json +esbuild* out/test/** out/** -extension.webpack.config.js -extension-browser.webpack.config.js cgmanifest.json package-lock.json preview-src/** From 5cf6aa6a20cbc2789d9a54f9835d07b617fc1582 Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Wed, 11 Feb 2026 16:43:12 -0800 Subject: [PATCH 53/72] slowed animations by 25ms --- src/vs/workbench/browser/media/motion.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/media/motion.css b/src/vs/workbench/browser/media/motion.css index 182d420add4..fbd8215265a 100644 --- a/src/vs/workbench/browser/media/motion.css +++ b/src/vs/workbench/browser/media/motion.css @@ -5,10 +5,10 @@ /* Motion custom properties -- only active when motion is enabled */ .monaco-workbench.monaco-enable-motion { - --vscode-motion-panel-open-duration: 150ms; - --vscode-motion-panel-close-duration: 50ms; - --vscode-motion-quick-input-open-duration: 150ms; - --vscode-motion-quick-input-close-duration: 50ms; + --vscode-motion-panel-open-duration: 175ms; + --vscode-motion-panel-close-duration: 75ms; + --vscode-motion-quick-input-open-duration: 175ms; + --vscode-motion-quick-input-close-duration: 75ms; --vscode-motion-ease-out: cubic-bezier(0.1, 0.9, 0.2, 1); --vscode-motion-ease-in: cubic-bezier(0.9, 0.1, 1, 0.2); } From 252f81c0f1a909b082d01f4d8525ec2909a1f0a1 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 11 Feb 2026 16:51:35 -0800 Subject: [PATCH 54/72] Update hook settings (#294732) --- build/lib/policies/policyData.jsonc | 14 +++++++++++++ .../contrib/chat/browser/chat.contribution.ts | 20 +++++++++++++++---- .../chat/common/promptSyntax/config/config.ts | 2 +- .../service/promptsServiceImpl.ts | 10 +++++++++- .../preferences/browser/settingsLayout.ts | 2 +- 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index 53387c4fa34..7489336e814 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -169,6 +169,20 @@ "type": "boolean", "default": true }, + { + "key": "chat.useHooks", + "name": "ChatHooks", + "category": "InteractiveSession", + "minimumVersion": "1.109", + "localization": { + "description": { + "key": "chat.useHooks.description", + "value": "Controls whether chat hooks are executed at strategic points during an agent's workflow. Hooks are loaded from the files configured in `#chat.hookFilesLocations#`." + } + }, + "type": "boolean", + "default": true + }, { "key": "chat.tools.terminal.enableAutoApprove", "name": "ChatToolsTerminalEnableAutoApprove", diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d9a0dbe7a6a..13e9b83a523 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -958,7 +958,7 @@ configurationRegistry.registerConfiguration({ patternErrorMessage: nls.localize('chat.hookFilesLocations.invalidPath', "Paths must be relative or start with '~/'. Absolute paths and '\\' separators are not supported."), }, restricted: true, - tags: ['prompts', 'hooks', 'agent'], + tags: ['preview', 'prompts', 'hooks', 'agent'], examples: [ { [DEFAULT_HOOK_FILE_PATHS[0].path]: true, @@ -971,12 +971,24 @@ configurationRegistry.registerConfiguration({ }, [PromptsConfig.USE_CHAT_HOOKS]: { type: 'boolean', - title: nls.localize('chat.useChatHooks.title', "Use Chat Hooks",), - markdownDescription: nls.localize('chat.useChatHooks.description', "Controls whether chat hooks are executed at strategic points during an agent's workflow. Hooks are loaded from the files configured in `#chat.hookFilesLocations#`.",), + title: nls.localize('chat.useHooks.title', "Use Chat Hooks",), + markdownDescription: nls.localize('chat.useHooks.description', "Controls whether chat hooks are executed at strategic points during an agent's workflow. Hooks are loaded from the files configured in `#chat.hookFilesLocations#`.",), default: true, restricted: true, disallowConfigurationDefault: true, - tags: ['prompts', 'hooks', 'agent'] + tags: ['preview', 'prompts', 'hooks', 'agent'], + policy: { + name: 'ChatHooks', + category: PolicyCategory.InteractiveSession, + minimumVersion: '1.109', + value: (policyData) => policyData.chat_preview_features_enabled === false ? false : undefined, + localization: { + description: { + key: 'chat.useHooks.description', + value: nls.localize('chat.useHooks.description', "Controls whether chat hooks are executed at strategic points during an agent's workflow. Hooks are loaded from the files configured in `#chat.hookFilesLocations#`.",) + } + }, + } }, [PromptsConfig.PROMPT_FILES_SUGGEST_KEY]: { type: 'object', diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts index 4b9621c5547..89d415a99ef 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts @@ -108,7 +108,7 @@ export namespace PromptsConfig { /** * Configuration key for chat hooks usage. */ - export const USE_CHAT_HOOKS = 'chat.useChatHooks'; + export const USE_CHAT_HOOKS = 'chat.useHooks'; /** * Configuration key for enabling stronger skill adherence prompt (experimental). diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 7d4cb5cc352..e32abc667f0 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -177,7 +177,10 @@ export class PromptsService extends Disposable implements IPromptsService { this.cachedHooks = this._register(new CachedPromise( (token) => this.computeHooks(token), - () => this.getFileLocatorEvent(PromptsType.hook) + () => Event.any( + this.getFileLocatorEvent(PromptsType.hook), + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(PromptsConfig.USE_CHAT_HOOKS)), + ) )); // Hack: Subscribe to activate caching (CachedPromise only caches when onDidChange has listeners) @@ -1000,6 +1003,11 @@ export class PromptsService extends Disposable implements IPromptsService { } private async computeHooks(token: CancellationToken): Promise { + const useChatHooks = this.configurationService.getValue(PromptsConfig.USE_CHAT_HOOKS); + if (!useChatHooks) { + return undefined; + } + const hookFiles = await this.listPromptFiles(PromptsType.hook, token); if (hookFiles.length === 0) { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 4b8f27cdd9b..63a328d454b 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -257,7 +257,7 @@ export const tocData: ITOCEntry = { 'chat.useNestedAgentsMdFiles', 'chat.useAgentSkills', 'chat.experimental.useSkillAdherencePrompt', - 'chat.useChatHooks', + 'chat.useHooks', 'chat.includeApplyingInstructions', 'chat.includeReferencedInstructions', 'chat.sendElementsToChat.*', From ff2c0466296cd9e029191b7e3dd89734c58ac6d4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:54:18 -0800 Subject: [PATCH 55/72] Refactor quick input exit animation cleanup to eliminate duplication (#294739) * Initial plan * Refactor exit animation cleanup to be DRY and properly reset cancel function Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> * Remove accidentally committed codicon.ttf build artifact Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Co-authored-by: Elijah King --- .../browser/ui/codicons/codicon/codicon.ttf | Bin 125696 -> 0 bytes .../quickinput/browser/quickInputController.ts | 13 +++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 src/vs/base/browser/ui/codicons/codicon/codicon.ttf diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf deleted file mode 100644 index a3098e1715c529ea8fa52ad57586c686ca2fd629..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 125696 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 z zi4?%z{-rSYK%vq_U?p0t_&|P~%3m)DwWyL~W*;iXUy6%LZz@i6i{uv1yQRimOiOKE zbKo~W5f#G+ij?=G7E`xUcmP{wVI&4jRjZdI*(^EXDS1Q``LUPCpZ=Axzd!b6W>wH$&hn3Nu_H_q6G8C-VRza#I4Koe zw|rD>eCFwC%@FLp%3-bKdG$O+5k6y}MEx`HJ1Iu)7q{RymJWQgw1rgF=ZU&eH>>Yr zUnT6C1GVoA;O@)L=$DTm=X{VwEbxwmR7}Bfu&8H_1XjwOoSwhz{4@VEJ{C$Yq8Sw{ zId2B_vxu*NdW~_!Jz_7IEq(!uB~G2tn_yUF41P<)OA@x^s9OF_D|nqINf%z0c@t)X zUEI){oI&c3(Vo61P$Q-bOG|}mH7huRV)UtEm9_u|>E1A>=M@Y_1YnCota^-el{gLx zIaIy#%6JWq)VdZyX8edf2d-ov@1;Fz1`QxrTj-DOv$DlqmFJOnrr+sblG&O_>8FR; z*9^L_CPlb-ebUvfFxFFy zHBUbHjQ{|8)~Rs@ZClfi^eR1-3iKKh`!xz*AAaMMkyGjA$ZcG{MeuF@Gr3c^2X~13 zST>5&8`)}{oG_|6rk;S*M`ZR1%Ogu)x?Xsp(NR@DaCWB0FR<%>F3mkA0r__6?Xxq; z*!E(M%h_4x?A$4&iLfc;5Zzj$9|p}|jf zO1XHy+<&Li-{@W{lih;vdzGmBA3}Fycai+%gy1Ug3U_7?wm+WC7UZz{u|6QLlRkl+ zK>~9YaiQx)Dg#V^E>(Ut|0^{b%8}1n9`2ZDNnu0ww=Vs06qXAuQVoI@It9K?qLpJ1 zrsFt0Sf*kt8`e{mxBz!Twhprr0ojp3&4AyE6$9XtPQ%2t7GNHT;}>M+4s3!LAg|vl zl%YlL$YUz`7OrOB?{XE7w4>G@RF0qJw)BzaR3n~@T+X9mhooBjekCdxkYV5^OE3uQQTnrSVD{?aXKoHzXmO3z9%c!30tgj*!Fbvu`~PP~>veEG*xLWR|NCF>{N0DFTmZkTznzxYbeiRA@IArDgT}K@$R%si zwqzvA0(JSE!~|v`F0T(NA&cdAB~~c z!76HUlHf~d)eG5?+~Bkx!M!Qm&m-8C46?I?AuxVTBAcrC%T=ovnJ&91L-~vA8KV4; ze*W{L4QZLGIFTnvyNZOwyLi!i`(qm$j{zb>8(o&>}XsXnIQek)l&C?V?hevS4faXFAU0_3v z@b6jDbSP}a@p67ALFOlI>_p?$#*;0Es-^Y ze_yt*dP=avfunJuNaIIE;R~H1Q7-twFpZsz5X#zv3*9X~xlaxJ9{kLpnh?xp2s6p2 zDFSBDStX-AfLKN|2Lz;H|{bgEU9L zqd-F0L>X~wXVAx)I$@J|IZhK8kMJ^<4#Ed<;^F}~O@W|9KQnYGua^(VoyM}tP>C|H zECwdKitJ04#^B|858+gKbaqCyjNSDaBu+9Ir#e23s8u#PMn9pgK4McN^x||A=fwoc1+>RC5hD4;k5C}Kaxho+ zk~%P!nDgY(Z#k$vY!!}9Qgw$Q9(OepDP5eTH>OGU|v!H z1$#x{MjTu0o3Ueh?Z7$XI{}7~aJX0o0JQ~t7t6~YnuU@%7(j}9^xzvBoIyyL-ciA)#47fy*Jd&V3D)Yww*RDdl2Nx;|beNYOWSqP~< z)P0qVqva7qzEMD8cn5S7GDBz^;qaCaNFkk-2t}g(5(F)ZLy1&@RUl`a2m&``5K>*> z3y$}iM=G;|`nl1{MrNyOh@HYJ5tt~>AulPh2VuQ+1`QJ2*zpv^3zst)W4*pVLz*O3 zQYuAI;_=0IK^3Hn@J8G@h<+;aWGYFjEb`ZvCGWlCBvX&2O3)7GD&jFPpRjhu&Rvoy zL0u{1P5a}FtT@0$$vDDR4*OpYU~X;T`+>G0koAS{6c(uh3=|+Ky`Lr_mWWd+;&mi+ z;`G!sbgaNI;Gh6q(YOLE4b-EEcu2_1#f3$HsDQPqFXM*OvOq~uh-Iu)9OoX$V$pix z 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 M^vo|k6s_$42P$3o;Q#;t diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 2059c921f7c..cdfa00b7f82 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -808,16 +808,17 @@ export class QuickInputController extends Disposable { // Animate exit: fade out + slide up (faster than open) if (!isMotionReduced(container)) { container.classList.add('animating-exit'); + const cleanupAnimation = () => { + container.classList.remove('animating-exit'); + container.removeEventListener('animationend', onAnimationEnd); + this._cancelExitAnimation = undefined; + }; const onAnimationEnd = () => { // Set display after animation completes to actually hide the element container.style.display = 'none'; - container.classList.remove('animating-exit'); - container.removeEventListener('animationend', onAnimationEnd); - }; - this._cancelExitAnimation = () => { - container.classList.remove('animating-exit'); - container.removeEventListener('animationend', onAnimationEnd); + cleanupAnimation(); }; + this._cancelExitAnimation = cleanupAnimation; container.addEventListener('animationend', onAnimationEnd); } else { container.style.display = 'none'; From 849d9387b6d52efcec9efa01c210d58bb987db23 Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Wed, 11 Feb 2026 17:01:46 -0800 Subject: [PATCH 56/72] fixed unused error sped animation back up by 25ms :) --- src/vs/platform/quickinput/browser/quickInputController.ts | 6 ------ src/vs/workbench/browser/layout.ts | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index cdfa00b7f82..11964ea5a30 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -41,12 +41,6 @@ import { isMotionReduced } from '../../../base/browser/ui/motion/motion.js'; import { AnchorAlignment, AnchorPosition, layout2d } from '../../../base/common/layout.js'; import { getAnchorRect } from '../../../base/browser/ui/contextview/contextview.js'; -/** Duration (ms) for quick input open (entrance) animations. */ -const QUICK_INPUT_OPEN_DURATION = 150; - -/** Duration (ms) for quick input close (exit) animations. */ -const QUICK_INPUT_CLOSE_DURATION = 50; - const $ = dom.$; const VIEWSTATE_STORAGE_KEY = 'workbench.quickInput.viewState'; diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 10c0e42f532..e092dad8d54 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -2736,10 +2736,10 @@ function getZenModeConfiguration(configurationService: IConfigurationService): Z } /** Duration (ms) for panel/sidebar open (entrance) animations. */ -const PANEL_OPEN_DURATION = 150; +const PANEL_OPEN_DURATION = 125; /** Duration (ms) for panel/sidebar close (exit) animations. */ -const PANEL_CLOSE_DURATION = 50; +const PANEL_CLOSE_DURATION = 25; function createViewVisibilityAnimation(hidden: boolean, onComplete?: () => void, token: CancellationToken = CancellationToken.None): IViewVisibilityAnimationOptions { return { From e8cc00bea16fa76d84506282f1a3e1fcd985e351 Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Wed, 11 Feb 2026 17:05:15 -0800 Subject: [PATCH 57/72] restored codicon.ttf --- .../browser/ui/codicons/codicon/codicon.ttf | Bin 0 -> 125696 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/vs/base/browser/ui/codicons/codicon/codicon.ttf diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a3098e1715c529ea8fa52ad57586c686ca2fd629 GIT binary patch literal 125696 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 z zi4?%z{-rSYK%vq_U?p0t_&|P~%3m)DwWyL~W*;iXUy6%LZz@i6i{uv1yQRimOiOKE zbKo~W5f#G+ij?=G7E`xUcmP{wVI&4jRjZdI*(^EXDS1Q``LUPCpZ=Axzd!b6W>wH$&hn3Nu_H_q6G8C-VRza#I4Koe zw|rD>eCFwC%@FLp%3-bKdG$O+5k6y}MEx`HJ1Iu)7q{RymJWQgw1rgF=ZU&eH>>Yr zUnT6C1GVoA;O@)L=$DTm=X{VwEbxwmR7}Bfu&8H_1XjwOoSwhz{4@VEJ{C$Yq8Sw{ zId2B_vxu*NdW~_!Jz_7IEq(!uB~G2tn_yUF41P<)OA@x^s9OF_D|nqINf%z0c@t)X zUEI){oI&c3(Vo61P$Q-bOG|}mH7huRV)UtEm9_u|>E1A>=M@Y_1YnCota^-el{gLx zIaIy#%6JWq)VdZyX8edf2d-ov@1;Fz1`QxrTj-DOv$DlqmFJOnrr+sblG&O_>8FR; z*9^L_CPlb-ebUvfFxFFy zHBUbHjQ{|8)~Rs@ZClfi^eR1-3iKKh`!xz*AAaMMkyGjA$ZcG{MeuF@Gr3c^2X~13 zST>5&8`)}{oG_|6rk;S*M`ZR1%Ogu)x?Xsp(NR@DaCWB0FR<%>F3mkA0r__6?Xxq; z*!E(M%h_4x?A$4&iLfc;5Zzj$9|p}|jf zO1XHy+<&Li-{@W{lih;vdzGmBA3}Fycai+%gy1Ug3U_7?wm+WC7UZz{u|6QLlRkl+ zK>~9YaiQx)Dg#V^E>(Ut|0^{b%8}1n9`2ZDNnu0ww=Vs06qXAuQVoI@It9K?qLpJ1 zrsFt0Sf*kt8`e{mxBz!Twhprr0ojp3&4AyE6$9XtPQ%2t7GNHT;}>M+4s3!LAg|vl zl%YlL$YUz`7OrOB?{XE7w4>G@RF0qJw)BzaR3n~@T+X9mhooBjekCdxkYV5^OE3uQQTnrSVD{?aXKoHzXmO3z9%c!30tgj*!Fbvu`~PP~>veEG*xLWR|NCF>{N0DFTmZkTznzxYbeiRA@IArDgT}K@$R%si zwqzvA0(JSE!~|v`F0T(NA&cdAB~~c z!76HUlHf~d)eG5?+~Bkx!M!Qm&m-8C46?I?AuxVTBAcrC%T=ovnJ&91L-~vA8KV4; ze*W{L4QZLGIFTnvyNZOwyLi!i`(qm$j{zb>8(o&>}XsXnIQek)l&C?V?hevS4faXFAU0_3v z@b6jDbSP}a@p67ALFOlI>_p?$#*;0Es-^Y ze_yt*dP=avfunJuNaIIE;R~H1Q7-twFpZsz5X#zv3*9X~xlaxJ9{kLpnh?xp2s6p2 zDFSBDStX-AfLKN|2Lz;H|{bgEU9L zqd-F0L>X~wXVAx)I$@J|IZhK8kMJ^<4#Ed<;^F}~O@W|9KQnYGua^(VoyM}tP>C|H zECwdKitJ04#^B|858+gKbaqCyjNSDaBu+9Ir#e23s8u#PMn9pgK4McN^x||A=fwoc1+>RC5hD4;k5C}Kaxho+ zk~%P!nDgY(Z#k$vY!!}9Qgw$Q9(OepDP5eTH>OGU|v!H z1$#x{MjTu0o3Ueh?Z7$XI{}7~aJX0o0JQ~t7t6~YnuU@%7(j}9^xzvBoIyyL-ciA)#47fy*Jd&V3D)Yww*RDdl2Nx;|beNYOWSqP~< z)P0qVqva7qzEMD8cn5S7GDBz^;qaCaNFkk-2t}g(5(F)ZLy1&@RUl`a2m&``5K>*> z3y$}iM=G;|`nl1{MrNyOh@HYJ5tt~>AulPh2VuQ+1`QJ2*zpv^3zst)W4*pVLz*O3 zQYuAI;_=0IK^3Hn@J8G@h<+;aWGYFjEb`ZvCGWlCBvX&2O3)7GD&jFPpRjhu&Rvoy zL0u{1P5a}FtT@0$$vDDR4*OpYU~X;T`+>G0koAS{6c(uh3=|+Ky`Lr_mWWd+;&mi+ z;`G!sbgaNI;Gh6q(YOLE4b-EEcu2_1#f3$HsDQPqFXM*OvOq~uh-Iu)9OoX$V$pix z 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 M^vo|k6s_$42P$3o;Q#;t literal 0 HcmV?d00001 From d04bc913f0fc346c8c66f71eb82394d1d207654b Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:10:29 -0800 Subject: [PATCH 58/72] Apply suggestion from @TylerLeonhardt --- src/vs/platform/quickinput/browser/media/quickInput.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index b9fe8c98023..642e0df0626 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -527,7 +527,6 @@ } /* Entrance animation */ -/* Duration matches QUICK_INPUT_OPEN_DURATION constant (150ms) in quickInputController.ts */ @keyframes quick-input-entrance { from { opacity: 0; From 3ce73d45334b1d7227d102b626622fc12871faab Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:11:04 -0800 Subject: [PATCH 59/72] Apply suggestion from @TylerLeonhardt --- src/vs/platform/quickinput/browser/media/quickInput.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 642e0df0626..9afd0964022 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -543,7 +543,6 @@ } /* Exit animation */ -/* Duration matches QUICK_INPUT_CLOSE_DURATION constant (50ms) in quickInputController.ts */ @keyframes quick-input-exit { from { opacity: 1; From 7566dfb8577091e4bcaf4f735bfb89988c53bfcb Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:12:25 -0800 Subject: [PATCH 60/72] Switch to unified `js/ts` settings for inlay hints For #292934 --- .../typescript-language-features/package.json | 192 ++++++++++++++---- .../package.nls.json | 8 + .../fileConfigurationManager.ts | 44 ++-- .../src/languageFeatures/inlayHints.ts | 19 +- 4 files changed, 186 insertions(+), 77 deletions(-) diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index ecc66db7e38..03d98f3efe7 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -1296,7 +1296,7 @@ "title": "%configuration.inlayHints%", "order": 24, "properties": { - "typescript.inlayHints.parameterNames.enabled": { + "js/ts.inlayHints.parameterNames.enabled": { "type": "string", "enum": [ "none", @@ -1310,49 +1310,11 @@ ], "default": "none", "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", - "scope": "resource" - }, - "typescript.inlayHints.parameterTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.variableTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { - "type": "boolean", - "default": true, - "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", - "scope": "resource" - }, - "typescript.inlayHints.propertyDeclarationTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.functionLikeReturnTypes.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", - "scope": "resource" - }, - "typescript.inlayHints.enumMemberValues.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%configuration.inlayHints.enumMemberValues.enabled%", - "scope": "resource" + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] }, "javascript.inlayHints.parameterNames.enabled": { "type": "string", @@ -1368,42 +1330,184 @@ ], "default": "none", "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterNames.enabled.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.parameterNames.enabled": { + "type": "string", + "enum": [ + "none", + "literals", + "all" + ], + "enumDescriptions": [ + "%inlayHints.parameterNames.none%", + "%inlayHints.parameterNames.literals%", + "%inlayHints.parameterNames.all%" + ], + "default": "none", + "markdownDescription": "%configuration.inlayHints.parameterNames.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterNames.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { "type": "boolean", "default": true, "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.parameterTypes.enabled": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterTypes.enabled.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.parameterTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.parameterTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.parameterTypes.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.variableTypes.enabled": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.variableTypes.enabled.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.variableTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.variableTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.variableTypes.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.variableTypes.suppressWhenTypeMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { "type": "boolean", "default": true, "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", + "markdownDeprecationMessage": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": { + "type": "boolean", + "default": true, + "markdownDescription": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName%", + "markdownDeprecationMessage": "%configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.propertyDeclarationTypes.enabled": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.propertyDeclarationTypes.enabled.unifiedDeprecationMessage%", "scope": "resource" }, + "typescript.inlayHints.propertyDeclarationTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.propertyDeclarationTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.propertyDeclarationTypes.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", + "scope": "language-overridable", + "tags": [ + "JavaScript", + "TypeScript" + ] + }, "javascript.inlayHints.functionLikeReturnTypes.enabled": { "type": "boolean", "default": false, "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.functionLikeReturnTypes.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "typescript.inlayHints.functionLikeReturnTypes.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.functionLikeReturnTypes.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.functionLikeReturnTypes.enabled.unifiedDeprecationMessage%", + "scope": "resource" + }, + "js/ts.inlayHints.enumMemberValues.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.enumMemberValues.enabled%", + "scope": "language-overridable", + "tags": [ + "TypeScript" + ] + }, + "typescript.inlayHints.enumMemberValues.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.inlayHints.enumMemberValues.enabled%", + "markdownDeprecationMessage": "%configuration.inlayHints.enumMemberValues.enabled.unifiedDeprecationMessage%", "scope": "resource" } } diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 536eab3ce03..cdbce28c5a9 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -103,38 +103,46 @@ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.parameterNames.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.parameterNames.enabled#` instead.", "configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName": "Suppress parameter name hints on arguments whose text is identical to the parameter name.", + "configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.parameterNames.suppressWhenArgumentMatchesName#` instead.", "configuration.inlayHints.parameterTypes.enabled": { "message": "Enable/disable inlay hints for implicit parameter types:\n```typescript\n\nel.addEventListener('click', e /* :MouseEvent */ => ...)\n \n```", "comment": [ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.parameterTypes.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.parameterTypes.enabled#` instead.", "configuration.inlayHints.variableTypes.enabled": { "message": "Enable/disable inlay hints for implicit variable types:\n```typescript\n\nconst foo /* :number */ = Date.now();\n \n```", "comment": [ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.variableTypes.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.variableTypes.enabled#` instead.", "configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName": "Suppress type hints on variables whose name is identical to the type name.", + "configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.variableTypes.suppressWhenTypeMatchesName#` instead.", "configuration.inlayHints.propertyDeclarationTypes.enabled": { "message": "Enable/disable inlay hints for implicit types on property declarations:\n```typescript\n\nclass Foo {\n\tprop /* :number */ = Date.now();\n}\n \n```", "comment": [ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.propertyDeclarationTypes.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.propertyDeclarationTypes.enabled#` instead.", "configuration.inlayHints.functionLikeReturnTypes.enabled": { "message": "Enable/disable inlay hints for implicit return types on function signatures:\n```typescript\n\nfunction foo() /* :number */ {\n\treturn Date.now();\n} \n \n```", "comment": [ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.functionLikeReturnTypes.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.functionLikeReturnTypes.enabled#` instead.", "configuration.inlayHints.enumMemberValues.enabled": { "message": "Enable/disable inlay hints for member values in enum declarations:\n```typescript\n\nenum MyValue {\n\tA /* = 0 */;\n\tB /* = 1 */;\n}\n \n```", "comment": [ "The text inside the ``` block is code and should not be localized." ] }, + "configuration.inlayHints.enumMemberValues.enabled.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.inlayHints.enumMemberValues.enabled#` instead.", "taskDefinition.tsconfig.description": "The tsconfig file that defines the TS build.", "javascript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for JavaScript files in the editor.", "typescript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for TypeScript files in the editor.", diff --git a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index 6da5bb74cd7..f6ede823fcd 100644 --- a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -10,6 +10,7 @@ import { isTypeScriptDocument } from '../configuration/languageIds'; import { API } from '../tsServer/api'; import type * as Proto from '../tsServer/protocol/protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; +import { readUnifiedConfig } from '../utils/configuration'; import { Disposable } from '../utils/dispose'; import { equals } from '../utils/objects'; import { ResourceMap } from '../utils/resourceMap'; @@ -206,7 +207,7 @@ export default class FileConfigurationManager extends Disposable { disableLineTextInReferences: true, interactiveInlayHints: true, includeCompletionsForModuleExports: config.get('suggest.autoImports'), - ...getInlayHintsPreferences(config), + ...getInlayHintsPreferences(document, isTypeScriptDocument(document) ? 'typescript' : 'javascript'), ...this.getOrganizeImportsPreferences(preferencesConfig), maximumHoverLength: this.getMaximumHoverLength(document), }; @@ -274,31 +275,32 @@ function withDefaultAsUndefined(value: T, def: O): Exclude return value === def ? undefined : value as Exclude; } -export class InlayHintSettingNames { - static readonly parameterNamesSuppressWhenArgumentMatchesName = 'inlayHints.parameterNames.suppressWhenArgumentMatchesName'; - static readonly parameterNamesEnabled = 'inlayHints.parameterTypes.enabled'; - static readonly variableTypesEnabled = 'inlayHints.variableTypes.enabled'; - static readonly variableTypesSuppressWhenTypeMatchesName = 'inlayHints.variableTypes.suppressWhenTypeMatchesName'; - static readonly propertyDeclarationTypesEnabled = 'inlayHints.propertyDeclarationTypes.enabled'; - static readonly functionLikeReturnTypesEnabled = 'inlayHints.functionLikeReturnTypes.enabled'; - static readonly enumMemberValuesEnabled = 'inlayHints.enumMemberValues.enabled'; -} +export const InlayHintSettingNames = Object.freeze({ + parameterNamesEnabled: 'inlayHints.parameterNames.enabled', + parameterNamesSuppressWhenArgumentMatchesName: 'inlayHints.parameterNames.suppressWhenArgumentMatchesName', + parameterTypesEnabled: 'inlayHints.parameterTypes.enabled', + variableTypesEnabled: 'inlayHints.variableTypes.enabled', + variableTypesSuppressWhenTypeMatchesName: 'inlayHints.variableTypes.suppressWhenTypeMatchesName', + propertyDeclarationTypesEnabled: 'inlayHints.propertyDeclarationTypes.enabled', + functionLikeReturnTypesEnabled: 'inlayHints.functionLikeReturnTypes.enabled', + enumMemberValuesEnabled: 'inlayHints.enumMemberValues.enabled', +}); -export function getInlayHintsPreferences(config: vscode.WorkspaceConfiguration) { +export function getInlayHintsPreferences(scope: vscode.ConfigurationScope, fallbackSection: string) { return { - includeInlayParameterNameHints: getInlayParameterNameHintsPreference(config), - includeInlayParameterNameHintsWhenArgumentMatchesName: !config.get(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true), - includeInlayFunctionParameterTypeHints: config.get(InlayHintSettingNames.parameterNamesEnabled, false), - includeInlayVariableTypeHints: config.get(InlayHintSettingNames.variableTypesEnabled, false), - includeInlayVariableTypeHintsWhenTypeMatchesName: !config.get(InlayHintSettingNames.variableTypesSuppressWhenTypeMatchesName, true), - includeInlayPropertyDeclarationTypeHints: config.get(InlayHintSettingNames.propertyDeclarationTypesEnabled, false), - includeInlayFunctionLikeReturnTypeHints: config.get(InlayHintSettingNames.functionLikeReturnTypesEnabled, false), - includeInlayEnumMemberValueHints: config.get(InlayHintSettingNames.enumMemberValuesEnabled, false), + includeInlayParameterNameHints: getInlayParameterNameHintsPreference(scope, fallbackSection), + includeInlayParameterNameHintsWhenArgumentMatchesName: !readUnifiedConfig(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true, { scope, fallbackSection }), + includeInlayFunctionParameterTypeHints: readUnifiedConfig(InlayHintSettingNames.parameterTypesEnabled, false, { scope, fallbackSection }), + includeInlayVariableTypeHints: readUnifiedConfig(InlayHintSettingNames.variableTypesEnabled, false, { scope, fallbackSection }), + includeInlayVariableTypeHintsWhenTypeMatchesName: !readUnifiedConfig(InlayHintSettingNames.variableTypesSuppressWhenTypeMatchesName, true, { scope, fallbackSection }), + includeInlayPropertyDeclarationTypeHints: readUnifiedConfig(InlayHintSettingNames.propertyDeclarationTypesEnabled, false, { scope, fallbackSection }), + includeInlayFunctionLikeReturnTypeHints: readUnifiedConfig(InlayHintSettingNames.functionLikeReturnTypesEnabled, false, { scope, fallbackSection }), + includeInlayEnumMemberValueHints: readUnifiedConfig(InlayHintSettingNames.enumMemberValuesEnabled, false, { scope, fallbackSection }), } as const; } -function getInlayParameterNameHintsPreference(config: vscode.WorkspaceConfiguration) { - switch (config.get('inlayHints.parameterNames.enabled')) { +function getInlayParameterNameHintsPreference(scope: vscode.ConfigurationScope, fallbackSection: string) { + switch (readUnifiedConfig(InlayHintSettingNames.parameterNamesEnabled, 'none', { scope, fallbackSection })) { case 'none': return 'none'; case 'literals': return 'literals'; case 'all': return 'all'; diff --git a/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts b/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts index 4fa38e4986b..16bf7dd62db 100644 --- a/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts +++ b/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts @@ -11,20 +11,13 @@ import { API } from '../tsServer/api'; import type * as Proto from '../tsServer/protocol/protocol'; import { Location, Position } from '../typeConverters'; import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { unifiedConfigSection } from '../utils/configuration'; import { Disposable } from '../utils/dispose'; import FileConfigurationManager, { InlayHintSettingNames, getInlayHintsPreferences } from './fileConfigurationManager'; import { conditionalRegistration, requireMinVersion, requireSomeCapability } from './util/dependentRegistration'; -const inlayHintSettingNames = Object.freeze([ - InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, - InlayHintSettingNames.parameterNamesEnabled, - InlayHintSettingNames.variableTypesEnabled, - InlayHintSettingNames.variableTypesSuppressWhenTypeMatchesName, - InlayHintSettingNames.propertyDeclarationTypesEnabled, - InlayHintSettingNames.functionLikeReturnTypesEnabled, - InlayHintSettingNames.enumMemberValuesEnabled, -]); +const inlayHintSettingNames = Object.values(InlayHintSettingNames); class TypeScriptInlayHintsProvider extends Disposable implements vscode.InlayHintsProvider { @@ -44,7 +37,10 @@ class TypeScriptInlayHintsProvider extends Disposable implements vscode.InlayHin super(); this._register(vscode.workspace.onDidChangeConfiguration(e => { - if (inlayHintSettingNames.some(settingName => e.affectsConfiguration(language.id + '.' + settingName))) { + if (inlayHintSettingNames.some(settingName => + e.affectsConfiguration(unifiedConfigSection + '.' + settingName) || + e.affectsConfiguration(language.id + '.' + settingName) + )) { this._onDidChangeInlayHints.fire(); } })); @@ -131,8 +127,7 @@ function fromProtocolInlayHintKind(kind: Proto.InlayHintKind): vscode.InlayHintK } function areInlayHintsEnabledForFile(language: LanguageDescription, document: vscode.TextDocument) { - const config = vscode.workspace.getConfiguration(language.id, document); - const preferences = getInlayHintsPreferences(config); + const preferences = getInlayHintsPreferences(document, language.id); return preferences.includeInlayParameterNameHints === 'literals' || preferences.includeInlayParameterNameHints === 'all' || From 91340306abb65bde72a837d4791e6c09404e0b27 Mon Sep 17 00:00:00 2001 From: eli-w-king Date: Wed, 11 Feb 2026 17:20:28 -0800 Subject: [PATCH 61/72] adjusted speed --- src/vs/workbench/browser/layout.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index e092dad8d54..97a74fcf8fa 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -2736,10 +2736,10 @@ function getZenModeConfiguration(configurationService: IConfigurationService): Z } /** Duration (ms) for panel/sidebar open (entrance) animations. */ -const PANEL_OPEN_DURATION = 125; +const PANEL_OPEN_DURATION = 135; /** Duration (ms) for panel/sidebar close (exit) animations. */ -const PANEL_CLOSE_DURATION = 25; +const PANEL_CLOSE_DURATION = 35; function createViewVisibilityAnimation(hidden: boolean, onComplete?: () => void, token: CancellationToken = CancellationToken.None): IViewVisibilityAnimationOptions { return { From 2354a3ce50a74eff725af893da85fd8d2122cad3 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 11 Feb 2026 19:33:29 -0600 Subject: [PATCH 62/72] add `chat.autoReply` (#294715) --- .../chat/browser/actions/chatActions.ts | 89 +++- .../contrib/chat/browser/chat.contribution.ts | 7 + .../chat/browser/widget/chatListRenderer.ts | 89 +++- .../widget/chatQuestionCarouselAutoReply.ts | 455 ++++++++++++++++++ .../contrib/chat/common/constants.ts | 1 + .../contrib/chat/common/languageModels.ts | 30 ++ .../browser/tools/monitoring/outputMonitor.ts | 7 +- .../browser/tools/monitoring/utils.ts | 29 +- 8 files changed, 663 insertions(+), 44 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/chatQuestionCarouselAutoReply.ts diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index dc57a988509..158ece0a481 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -51,7 +51,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; import { IChatModel, IChatResponseModel } from '../../common/model/chatModel.js'; import { ChatMode, IChatMode, IChatModeService } from '../../common/chatModes.js'; -import { IChatService } from '../../common/chatService/chatService.js'; +import { ElicitationState, IChatService, IChatToolInvocation } from '../../common/chatService/chatService.js'; import { ISCMHistoryItemChangeRangeVariableEntry, ISCMHistoryItemChangeVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/model/chatViewModel.js'; import { IChatWidgetHistoryService } from '../../common/widget/chatWidgetHistoryService.js'; @@ -212,6 +212,7 @@ abstract class OpenChatGlobalAction extends Action2 { const languageModelService = accessor.get(ILanguageModelsService); const scmService = accessor.get(ISCMService); const logService = accessor.get(ILogService); + const configurationService = accessor.get(IConfigurationService); let chatWidget = widgetService.lastFocusedWidget; // When this was invoked to switch to a mode via keybinding, and some chat widget is focused, use that one. @@ -388,16 +389,38 @@ abstract class OpenChatGlobalAction extends Action2 { if (opts?.blockOnResponse) { const response = await resp; if (response) { + const autoReplyEnabled = configurationService.getValue(ChatConfiguration.AutoReply); await new Promise(resolve => { const d = response.onDidChange(async () => { - if (response.isComplete || response.isPendingConfirmation.get()) { + if (response.isComplete) { + d.dispose(); + resolve(); + return; + } + + const pendingConfirmation = response.isPendingConfirmation.get(); + if (pendingConfirmation) { + // Check if the pending confirmation is a question carousel that will be auto-replied. + // Only question carousels are auto-replied; other confirmation types (tool approvals, + // elicitations, etc.) should cause us to resolve immediately. + const hasPendingQuestionCarousel = response.response.value.some( + part => part.kind === 'questionCarousel' && !part.isUsed + ); + if (autoReplyEnabled && hasPendingQuestionCarousel) { + // Auto-reply will handle this question carousel, keep waiting + return; + } d.dispose(); resolve(); } }); }); - return { ...response.result, type: response.isPendingConfirmation.get() ? 'confirmation' : undefined }; + const confirmationInfo = getPendingConfirmationInfo(response); + if (confirmationInfo) { + return { ...response.result, ...confirmationInfo }; + } + return { ...response.result }; } } @@ -437,6 +460,66 @@ async function waitForDefaultAgent(chatAgentService: IChatAgentService, mode: Ch ]); } +/** + * Information about a pending confirmation in a chat response. + */ +export type IChatPendingConfirmationInfo = + | { type: 'confirmation'; kind: 'toolInvocation'; toolId: string } + | { type: 'confirmation'; kind: 'toolPostApproval'; toolId: string } + | { type: 'confirmation'; kind: 'confirmation'; title: string; data: unknown } + | { type: 'confirmation'; kind: 'questionCarousel'; questions: unknown[] } + | { type: 'confirmation'; kind: 'elicitation'; title: string }; + +/** + * Extracts detailed information about the pending confirmation from a chat response. + * Returns undefined if there is no pending confirmation. + */ +function getPendingConfirmationInfo(response: IChatResponseModel): IChatPendingConfirmationInfo | undefined { + for (const part of response.response.value) { + if (part.kind === 'toolInvocation') { + const state = part.state.get(); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { + return { + type: 'confirmation', + kind: 'toolInvocation', + toolId: part.toolId, + }; + } + if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + return { + type: 'confirmation', + kind: 'toolPostApproval', + toolId: part.toolId, + }; + } + } + if (part.kind === 'confirmation' && !part.isUsed) { + return { + type: 'confirmation', + kind: 'confirmation', + title: part.title, + data: part.data, + }; + } + if (part.kind === 'questionCarousel' && !part.isUsed) { + return { + type: 'confirmation', + kind: 'questionCarousel', + questions: part.questions, + }; + } + if (part.kind === 'elicitation2' && part.state.get() === ElicitationState.Pending) { + const title = part.title; + return { + type: 'confirmation', + kind: 'elicitation', + title: typeof title === 'string' ? title : title.value, + }; + } + } + return undefined; +} + class PrimaryOpenChatGlobalAction extends OpenChatGlobalAction { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 13e9b83a523..ff1604d72b9 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -323,6 +323,13 @@ configurationRegistry.registerConfiguration({ description: nls.localize('chat.notifyWindowOnConfirmation', "Controls whether a chat session should present the user with an OS notification when a confirmation is needed while the window is not in focus. This includes a window badge as well as notification toast."), default: true, }, + [ChatConfiguration.AutoReply]: { + default: false, + markdownDescription: nls.localize('chat.autoReply.description', "Automatically answer chat question carousels using the current model. This is an advanced setting and can lead to unintended choices or actions based on incomplete context."), + type: 'boolean', + scope: ConfigurationScope.APPLICATION_MACHINE, + tags: ['experimental', 'advanced'], + }, [ChatConfiguration.GlobalAutoApprove]: { default: false, markdownDescription: globalAutoApproveDescription.value, diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index c533f527c54..dd074702d2b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -109,6 +109,7 @@ import { IAccessibilityService } from '../../../../../platform/accessibility/com import { ChatHookContentPart } from './chatContentParts/chatHookContentPart.js'; import { ChatPendingDragController } from './chatPendingDragAndDrop.js'; import { HookType } from '../../common/promptSyntax/hookSchema.js'; +import { ChatQuestionCarouselAutoReply } from './chatQuestionCarouselAutoReply.js'; const $ = dom.$; @@ -183,10 +184,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer>(); + private readonly _autoRepliedQuestionCarousels = new Set(); + private readonly _autoReply: ChatQuestionCarouselAutoReply; private _activeTipPart: ChatTipContentPart | undefined; - private readonly _notifiedQuestionCarousels = new WeakSet(); + private readonly _notifiedQuestionCarousels = new Set(); private readonly _questionCarouselToast = this._register(new DisposableStore()); private readonly chatContentMarkdownRenderer: IMarkdownRenderer; @@ -272,6 +275,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { @@ -358,6 +362,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer | undefined, part: ChatQuestionCarouselPart) => { // Mark the carousel as used and store the answers @@ -2141,13 +2149,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer handleSubmit(answers, fallbackPart) }); + this.maybeAutoReplyToQuestionCarousel(context, carousel, fallbackPart, answers => handleSubmit(answers, fallbackPart), modelName, requestMessageText); return fallbackPart; } - // If global auto-approve (yolo mode) is enabled, skip with defaults immediately - if (!carousel.isUsed && this.configService.getValue(ChatConfiguration.GlobalAutoApprove)) { - part.skip(); - } + this.maybeAutoReplyToQuestionCarousel(context, carousel, part, answers => handleSubmit(answers, part), modelName, requestMessageText); // Track the carousel for auto-skip when user submits a new message // Only add tracking if not already tracked (prevents duplicate tracking on re-render) @@ -2186,13 +2192,23 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('chat.notifyWindowOnConfirmation')) { @@ -2261,6 +2279,61 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer | undefined) => Promise, + modelName: string | undefined, + requestMessageText: string | undefined, + ): void { + if (carousel.isUsed) { + return; + } + + // Use a stable key based on requestId + resolveId to prevent duplicate + // auto-replies across re-renders of the same logical carousel. + const stableKey = this._getCarouselStableKey(context, carousel); + if (stableKey) { + if (this._autoRepliedQuestionCarousels.has(stableKey)) { + return; + } + // Mark as in-progress before the async opt-in check to prevent + // duplicate prompts/requests from concurrent re-renders. + this._autoRepliedQuestionCarousels.add(stableKey); + } + + void this._autoReply.shouldAutoReply().then(shouldAutoReply => { + if (!shouldAutoReply) { + // Roll back the in-progress mark if auto-reply is not enabled. + if (stableKey) { + this._autoRepliedQuestionCarousels.delete(stableKey); + } + return; + } + + const cts = new CancellationTokenSource(); + part.addDisposable(toDisposable(() => { + cts.cancel(); + cts.dispose(); + })); + + this._autoReply.autoReply(carousel, submit, modelName, requestMessageText, cts.token).catch(err => { + this.logService.debug('#ChatQuestionCarousel: Auto reply failed', toErrorMessage(err)); + }); + }); + } + + + private getRequestMessageText(response: IChatResponseViewModel): string | undefined { + const requestId = response.requestId; + const items = response.session.getItems(); + const request = items.find(item => isRequestVM(item) && item.id === requestId) as IChatRequestViewModel | undefined; + return request?.messageText; + } + + + private removeCarouselFromTracking(context: IChatContentPartRenderContext, part: ChatQuestionCarouselPart): void { if (isResponseVM(context.element)) { const carousels = this.pendingQuestionCarousels.get(context.element.sessionResource); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatQuestionCarouselAutoReply.ts b/src/vs/workbench/contrib/chat/browser/widget/chatQuestionCarouselAutoReply.ts new file mode 100644 index 00000000000..eca6e160814 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatQuestionCarouselAutoReply.ts @@ -0,0 +1,455 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../../base/common/codicons.js'; +import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import Severity from '../../../../../base/common/severity.js'; +import { hasKey } from '../../../../../base/common/types.js'; +import { localize } from '../../../../../nls.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { IChatQuestion, IChatQuestionCarousel } from '../../common/chatService/chatService.js'; +import { ChatConfiguration } from '../../common/constants.js'; +import { ChatMessageRole, getTextResponseFromStream, ILanguageModelsService } from '../../common/languageModels.js'; +import { Event } from '../../../../../base/common/event.js'; + +const enum AutoReplyStorageKeys { + AutoReplyOptIn = 'chat.autoReply.optIn' +} + +/** + * Encapsulates the logic for automatically replying to question carousels, + * including opt-in state management, LLM-based answer resolution, fallback + * answer generation, and answer parsing/merging. + */ +export class ChatQuestionCarouselAutoReply extends Disposable { + + constructor( + @IConfigurationService private readonly configService: IConfigurationService, + @IDialogService private readonly dialogService: IDialogService, + @ILogService private readonly logService: ILogService, + @IStorageService private readonly storageService: IStorageService, + @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + ) { + super(); + + // Clear out warning accepted state if the setting is disabled + this._register(Event.runAndSubscribe(this.configService.onDidChangeConfiguration, e => { + if (!e || e.affectsConfiguration(ChatConfiguration.AutoReply)) { + if (this.configService.getValue(ChatConfiguration.AutoReply) !== true) { + this.storageService.remove(AutoReplyStorageKeys.AutoReplyOptIn, StorageScope.APPLICATION); + } + } + })); + } + + async shouldAutoReply(): Promise { + if (!this.configService.getValue(ChatConfiguration.AutoReply)) { + return false; + } + return this.checkOptIn(); + } + + async autoReply( + carousel: IChatQuestionCarousel, + submit: (answers: Map | undefined) => Promise, + modelName: string | undefined, + requestMessageText: string | undefined, + token: CancellationToken, + ): Promise { + if (token.isCancellationRequested || carousel.isUsed || carousel.questions.length === 0) { + return; + } + + const fallbackAnswers = this.buildFallbackCarouselAnswers(carousel, requestMessageText); + let resolvedAnswers = fallbackAnswers; + + const modelId = await this.getModelId(modelName); + if (modelId && !token.isCancellationRequested) { + try { + const parsedAnswers = await this.requestAnswers(modelId, carousel, requestMessageText, token); + if (parsedAnswers.size > 0) { + resolvedAnswers = this.mergeAnswers(carousel, parsedAnswers, fallbackAnswers); + } + } catch (err) { + this.logService.debug('#ChatQuestionCarousel: Failed to resolve auto reply', toErrorMessage(err)); + } + } + + if (token.isCancellationRequested || carousel.isUsed) { + return; + } + + await submit(resolvedAnswers); + } + + // #region Opt-in + + private async checkOptIn(): Promise { + const optedIn = this.storageService.getBoolean(AutoReplyStorageKeys.AutoReplyOptIn, StorageScope.APPLICATION, false); + if (optedIn) { + return true; + } + + const promptResult = await this.dialogService.prompt({ + type: Severity.Warning, + message: localize('chat.autoReply.enable.title', 'Enable chat auto reply?'), + buttons: [ + { + label: localize('chat.autoReply.enable', 'Enable'), + run: () => true + }, + { + label: localize('chat.autoReply.disable', 'Disable'), + run: () => false + }, + ], + custom: { + icon: Codicon.warning, + disableCloseAction: true, + markdownDetails: [{ + markdown: new MarkdownString(localize('chat.autoReply.enable.details', 'Chat auto reply answers question carousels using the current model and may make unintended choices. Review your settings and outputs carefully.')), + }], + } + }); + + if (promptResult.result !== true) { + await this.configService.updateValue(ChatConfiguration.AutoReply, false); + return false; + } + + this.storageService.store(AutoReplyStorageKeys.AutoReplyOptIn, true, StorageScope.APPLICATION, StorageTarget.USER); + return true; + } + + // #endregion + + // #region LLM interaction + + private async getModelId(modelName: string | undefined): Promise { + if (!modelName) { + return undefined; + } + + let models = await this.languageModelsService.selectLanguageModels({ id: modelName }); + if (models.length > 0) { + return models[0]; + } + + if (modelName.startsWith('copilot/')) { + models = await this.languageModelsService.selectLanguageModels({ vendor: 'copilot', family: modelName.replace(/^copilot\//, '') }); + return models[0]; + } + + return undefined; + } + + private buildPrompt(carousel: IChatQuestionCarousel, requestMessageText: string | undefined, strict: boolean): string { + const questions = carousel.questions.map(question => ({ + id: question.id, + type: question.type, + title: question.title, + message: typeof question.message === 'string' ? question.message : question.message?.value, + options: question.options?.map(option => ({ id: option.id, label: option.label })) ?? [], + allowFreeformInput: question.allowFreeformInput ?? false, + })); + + const contextLines: string[] = []; + if (requestMessageText) { + contextLines.push(`Original user request: ${JSON.stringify(requestMessageText)}`); + } + + return [ + 'Choose default answers for the following questions.', + 'Return a JSON object keyed by question id.', + 'For text questions, the value should be a string.', + 'For singleSelect questions, the value should be { "selectedId": string } or { "freeform": string }.', + 'For multiSelect questions, the value should be { "selectedIds": string[] } and may include { "freeform": string }.', + 'If a question allows freeform input and has no options, return a freeform answer based on the user request when possible.', + 'Use option ids from the provided options.', + ...contextLines, + 'Questions:', + JSON.stringify(questions), + strict ? 'Return ONLY valid JSON. Do not include markdown or explanations.' : undefined, + ].filter(Boolean).join('\n'); + } + + private async requestAnswers( + modelId: string, + carousel: IChatQuestionCarousel, + requestMessageText: string | undefined, + token: CancellationToken, + ): Promise> { + const prompt = this.buildPrompt(carousel, requestMessageText, false); + const response = await this.languageModelsService.sendChatRequest( + modelId, + new ExtensionIdentifier('core'), + [{ role: ChatMessageRole.User, content: [{ type: 'text', value: prompt }] }], + {}, + token, + ); + const responseText = await getTextResponseFromStream(response); + const parsedAnswers = this.parseAnswers(responseText, carousel); + if (parsedAnswers.size > 0 || token.isCancellationRequested) { + return parsedAnswers; + } + + const retryPrompt = this.buildPrompt(carousel, requestMessageText, true); + const retryResponse = await this.languageModelsService.sendChatRequest( + modelId, + new ExtensionIdentifier('core'), + [{ role: ChatMessageRole.User, content: [{ type: 'text', value: retryPrompt }] }], + {}, + token, + ); + const retryText = await getTextResponseFromStream(retryResponse); + return this.parseAnswers(retryText, carousel); + } + + // #endregion + + // #region Answer parsing and resolution + + private parseAnswers(responseText: string, carousel: IChatQuestionCarousel): Map { + const parsed = this.tryParseJsonObject(responseText); + if (!parsed) { + return new Map(); + } + + const answers = new Map(); + for (const question of carousel.questions) { + const rawAnswer = parsed[question.id]; + const resolved = this.resolveAnswerFromRaw(question, rawAnswer); + if (resolved !== undefined) { + answers.set(question.id, resolved); + } + } + return answers; + } + + private mergeAnswers( + carousel: IChatQuestionCarousel, + resolvedAnswers: Map, + fallbackAnswers: Map, + ): Map { + const merged = new Map(); + for (const question of carousel.questions) { + const fallback = fallbackAnswers.get(question.id); + if (this.hasDefaultValue(question) && fallback !== undefined) { + merged.set(question.id, fallback); + continue; + } + if (resolvedAnswers.has(question.id)) { + merged.set(question.id, resolvedAnswers.get(question.id)!); + continue; + } + if (fallback !== undefined) { + merged.set(question.id, fallback); + } + } + return merged; + } + + private hasDefaultValue(question: IChatQuestion): boolean { + switch (question.type) { + case 'text': + return question.defaultValue !== undefined; + case 'singleSelect': + return typeof question.defaultValue === 'string'; + case 'multiSelect': + return Array.isArray(question.defaultValue) + ? question.defaultValue.length > 0 + : typeof question.defaultValue === 'string'; + } + } + + private resolveAnswerFromRaw(question: IChatQuestion, raw: unknown): unknown | undefined { + switch (question.type) { + case 'text': { + if (typeof raw === 'string') { + const value = raw.trim(); + return value.length > 0 ? value : undefined; + } + if (raw && typeof raw === 'object' && hasKey(raw, { value: true }) && typeof (raw as { value: unknown }).value === 'string') { + const value = (raw as { value: string }).value.trim(); + return value.length > 0 ? value : undefined; + } + return undefined; + } + case 'singleSelect': { + let selectedInput: string | undefined; + let freeformInput: string | undefined; + if (typeof raw === 'string') { + selectedInput = raw; + } else if (raw && typeof raw === 'object') { + if (hasKey(raw, { selectedId: true }) && typeof (raw as { selectedId: unknown }).selectedId === 'string') { + selectedInput = (raw as { selectedId: string }).selectedId; + } else if (hasKey(raw, { selectedLabel: true }) && typeof (raw as { selectedLabel: unknown }).selectedLabel === 'string') { + selectedInput = (raw as { selectedLabel: string }).selectedLabel; + } + if (hasKey(raw, { freeform: true }) && typeof (raw as { freeform: unknown }).freeform === 'string') { + freeformInput = (raw as { freeform: string }).freeform; + } + } + + if (freeformInput && freeformInput.trim().length > 0) { + return { selectedValue: undefined, freeformValue: freeformInput.trim() }; + } + + const match = selectedInput ? this.matchQuestionOption(question, selectedInput) : undefined; + if (match) { + return { selectedValue: match.value, freeformValue: undefined }; + } + return undefined; + } + case 'multiSelect': { + let selectedInputs: string[] = []; + let freeformInput: string | undefined; + if (Array.isArray(raw)) { + selectedInputs = raw.filter(item => typeof item === 'string') as string[]; + } else if (typeof raw === 'string') { + selectedInputs = raw.split(',').map(item => item.trim()).filter(item => item.length > 0); + } else if (raw && typeof raw === 'object') { + if (hasKey(raw, { selectedIds: true })) { + const selectedIdsValue = (raw as { selectedIds?: unknown }).selectedIds; + if (Array.isArray(selectedIdsValue)) { + selectedInputs = selectedIdsValue.filter((item: unknown): item is string => typeof item === 'string'); + } + } + if (hasKey(raw, { freeform: true }) && typeof (raw as { freeform?: unknown }).freeform === 'string') { + freeformInput = (raw as { freeform: string }).freeform; + } + } + + const selectedValues = selectedInputs + .map(input => this.matchQuestionOption(question, input)?.value) + .filter(value => value !== undefined); + const freeformValue = freeformInput?.trim(); + + if (selectedValues.length > 0 || (freeformValue && freeformValue.length > 0)) { + return { selectedValues, freeformValue }; + } + return undefined; + } + } + } + + private matchQuestionOption(question: IChatQuestion, rawInput: string): { id: string; value: unknown } | undefined { + const options = question.options ?? []; + if (!options.length) { + return undefined; + } + + const normalized = rawInput.trim().toLowerCase(); + const numeric = Number.parseInt(normalized, 10); + if (!Number.isNaN(numeric) && numeric > 0 && numeric <= options.length) { + const option = options[numeric - 1]; + return { id: option.id, value: option.value }; + } + + const exactId = options.find(option => option.id.toLowerCase() === normalized); + if (exactId) { + return { id: exactId.id, value: exactId.value }; + } + const exactLabel = options.find(option => option.label.toLowerCase() === normalized); + if (exactLabel) { + return { id: exactLabel.id, value: exactLabel.value }; + } + const partialLabel = options.find(option => option.label.toLowerCase().includes(normalized)); + if (partialLabel) { + return { id: partialLabel.id, value: partialLabel.value }; + } + + return undefined; + } + + // #endregion + + // #region Fallback answers + + buildFallbackCarouselAnswers(carousel: IChatQuestionCarousel, requestMessageText: string | undefined): Map { + const answers = new Map(); + for (const question of carousel.questions) { + const answer = this.getFallbackAnswerForQuestion(question, requestMessageText); + if (answer !== undefined) { + answers.set(question.id, answer); + } + } + return answers; + } + + private getFallbackAnswerForQuestion(question: IChatQuestion, requestMessageText: string | undefined): unknown { + const fallbackFreeform = requestMessageText?.trim() || localize('chat.questionCarousel.autoReplyFallback', 'OK'); + + switch (question.type) { + case 'text': + return question.defaultValue ?? fallbackFreeform; + case 'singleSelect': { + const defaultOptionId = typeof question.defaultValue === 'string' ? question.defaultValue : undefined; + const defaultOption = defaultOptionId ? question.options?.find(opt => opt.id === defaultOptionId) : undefined; + if (defaultOption) { + return { selectedValue: defaultOption.value, freeformValue: undefined }; + } + if (question.options && question.options.length > 0) { + return { selectedValue: question.options[0].value, freeformValue: undefined }; + } + if (question.allowFreeformInput) { + return { selectedValue: undefined, freeformValue: fallbackFreeform }; + } + return undefined; + } + case 'multiSelect': { + const defaultIds = Array.isArray(question.defaultValue) + ? question.defaultValue + : (typeof question.defaultValue === 'string' ? [question.defaultValue] : []); + const selectedValues = question.options + ?.filter(opt => defaultIds.includes(opt.id)) + .map(opt => opt.value) + .filter(value => value !== undefined) ?? []; + if (selectedValues.length > 0) { + return { selectedValues, freeformValue: undefined }; + } + if (question.options && question.options.length > 0) { + return { selectedValues: [question.options[0].value], freeformValue: undefined }; + } + if (question.allowFreeformInput) { + return { selectedValues: [], freeformValue: fallbackFreeform }; + } + return undefined; + } + } + } + + // #endregion + + // #region Utilities + + private tryParseJsonObject(text: string): Record | undefined { + const trimmed = text.trim(); + if (!trimmed) { + return undefined; + } + const start = trimmed.indexOf('{'); + const end = trimmed.lastIndexOf('}'); + const candidate = start >= 0 && end > start ? trimmed.slice(start, end + 1) : trimmed; + try { + const parsed = JSON.parse(candidate) as unknown; + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + return parsed as Record; + } + } catch { + return undefined; + } + return undefined; + } + + // #endregion +} diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index a5eec8a6ad8..2ac63ee13ea 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -25,6 +25,7 @@ export enum ChatConfiguration { RepoInfoEnabled = 'chat.repoInfo.enabled', EditRequests = 'chat.editRequests', InlineReferencesStyle = 'chat.inlineReferences.style', + AutoReply = 'chat.autoReply', GlobalAutoApprove = 'chat.tools.global.autoApprove', AutoApproveEdits = 'chat.tools.edits.autoApprove', AutoApprovedUrls = 'chat.tools.urls.autoApprove', diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 4067c77f7e7..f01c341dd9a 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -221,6 +221,36 @@ export interface ILanguageModelChatResponse { result: Promise; } +export async function getTextResponseFromStream(response: ILanguageModelChatResponse): Promise { + let responseText = ''; + const streaming = (async () => { + if (!response?.stream) { + return; + } + for await (const part of response.stream) { + if (Array.isArray(part)) { + for (const item of part) { + if (item.type === 'text') { + responseText += item.value; + } + } + } else if (part.type === 'text') { + responseText += part.value; + } + } + })(); + + try { + await Promise.all([response.result, streaming]); + return responseText; + } catch (err) { + if (responseText) { + return responseText; + } + throw err; + } +} + export interface ILanguageModelChatProvider { readonly onDidChange: Event; provideLanguageModelChatInfo(options: ILanguageModelChatInfoOptions, token: CancellationToken): Promise; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts index 081534d5031..fdda1dc2b01 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts @@ -19,12 +19,11 @@ import { ChatElicitationRequestPart } from '../../../../../chat/common/model/cha import { ChatModel } from '../../../../../chat/common/model/chatModel.js'; import { ElicitationState, IChatService } from '../../../../../chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../../../../chat/common/constants.js'; -import { ChatMessageRole, ILanguageModelsService } from '../../../../../chat/common/languageModels.js'; +import { ChatMessageRole, getTextResponseFromStream, ILanguageModelsService } from '../../../../../chat/common/languageModels.js'; import { IToolInvocationContext } from '../../../../../chat/common/tools/languageModelToolsService.js'; import { ITaskService } from '../../../../../tasks/common/taskService.js'; import { ILinkLocation } from '../../taskHelpers.js'; import { IConfirmationPrompt, IExecution, IPollingResult, OutputMonitorState, PollingConsts } from './types.js'; -import { getTextResponseFromStream } from './utils.js'; import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; import { TerminalChatAgentToolsSettingId } from '../../../common/terminalChatAgentToolsConfiguration.js'; import { ITerminalService } from '../../../../../terminal/browser/terminal.js'; @@ -470,9 +469,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { ); try { - const responseFromStream = getTextResponseFromStream(response); - await Promise.all([response.result, responseFromStream]); - return await responseFromStream; + return await getTextResponseFromStream(response); } catch (err) { return 'Error occurred ' + err; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/utils.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/utils.ts index 3a22b30f9fa..0ec550b0961 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/utils.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/utils.ts @@ -3,31 +3,4 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILanguageModelChatResponse } from '../../../../../chat/common/languageModels.js'; - -export async function getTextResponseFromStream(response: ILanguageModelChatResponse): Promise { - let responseText = ''; - const streaming = (async () => { - if (!response || !response.stream) { - return; - } - for await (const part of response.stream) { - if (Array.isArray(part)) { - for (const p of part) { - if (p.type === 'text') { - responseText += p.value; - } - } - } else if (part.type === 'text') { - responseText += part.value; - } - } - })(); - - try { - await Promise.all([response.result, streaming]); - return responseText; - } catch (err) { - return 'Error occurred ' + err; - } -} +export { getTextResponseFromStream } from '../../../../../chat/common/languageModels.js'; From 386be2607ecbcdc59c5881f78f58335116faceb0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 01:34:00 +0000 Subject: [PATCH 63/72] Fix vertical alignment of Last Synced label in Settings header (#294663) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- .../contrib/preferences/browser/media/settingsEditor2.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 2c59b893326..982fbc1d50b 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -109,7 +109,6 @@ } .settings-editor > .settings-header > .settings-header-controls .last-synced-label { - padding-top: 7px; opacity: 0.9; } From 6c1af3f302f1857b7b37ba3282143b8028056e0d Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 11 Feb 2026 17:49:34 -0800 Subject: [PATCH 64/72] Add customizations telemetry (#294741) --- .../browser/actions/chatExecuteActions.ts | 9 +- .../computeAutomaticInstructions.ts | 23 ++- .../config/promptFileLocations.ts | 8 + .../computeAutomaticInstructions.test.ts | 186 ++++++++++++++++-- 4 files changed, 205 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index d1938adf33f..2192b382727 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -29,6 +29,7 @@ import { ChatAgentLocation, ChatConfiguration, ChatModeKind, } from '../../commo import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; +import { isInClaudeAgentsFolder } from '../../common/promptSyntax/config/promptFileLocations.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { getAgentSessionProvider, AgentSessionProviders } from '../agentSessions/agentSessions.js'; @@ -260,6 +261,7 @@ type ChatModeChangeClassification = { extensionId?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension ID if the target mode is from an extension' }; toolsCount?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of custom tools in the target mode'; 'isMeasurement': true }; handoffsCount?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of handoffs in the target mode'; 'isMeasurement': true }; + isClaudeAgent?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the target mode is a Claude agent file from .claude/agents/' }; }; type ChatModeChangeEvent = { @@ -270,6 +272,7 @@ type ChatModeChangeEvent = { extensionId?: string; toolsCount?: number; handoffsCount?: number; + isClaudeAgent?: boolean; }; class ToggleChatModeAction extends Action2 { @@ -337,6 +340,9 @@ class ToggleChatModeAction extends Action2 { return mode.name.get(); }; + const modeUri = switchToMode.uri?.get(); + const isClaudeAgent = modeUri ? isInClaudeAgentsFolder(modeUri) : undefined; + telemetryService.publicLog2('chat.modeChange', { fromMode: getModeNameForTelemetry(currentMode), mode: getModeNameForTelemetry(switchToMode), @@ -344,7 +350,8 @@ class ToggleChatModeAction extends Action2 { storage, extensionId, toolsCount, - handoffsCount + handoffsCount, + isClaudeAgent }); widget.input.setChatMode(switchToMode.id); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 7a603df5251..2ca88074c26 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -19,7 +19,7 @@ import { IWorkspaceContextService } from '../../../../../platform/workspace/comm import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, toPromptFileVariableEntry, toPromptTextVariableEntry, PromptFileVariableKind, IPromptTextVariableEntry, ChatRequestToolReferenceEntry, toToolVariableEntry } from '../attachments/chatVariableEntries.js'; import { ILanguageModelToolsService, IToolData, VSCodeToolReference } from '../tools/languageModelToolsService.js'; import { PromptsConfig } from './config/config.js'; -import { isInClaudeRulesFolder, isPromptOrInstructionsFile } from './config/promptFileLocations.js'; +import { isInClaudeAgentsFolder, isInClaudeRulesFolder, isPromptOrInstructionsFile } from './config/promptFileLocations.js'; import { PromptsType } from './promptTypes.js'; import { ParsedPromptFile } from './promptFileParser.js'; import { AgentFileType, ICustomAgent, IPromptPath, IPromptsService } from './service/promptsService.js'; @@ -33,9 +33,12 @@ export type InstructionsCollectionEvent = { agentInstructionsCount: number; listedInstructionsCount: number; totalInstructionsCount: number; + claudeRulesCount: number; + claudeMdCount: number; + claudeAgentsCount: number; }; export function newInstructionsCollectionEvent(): InstructionsCollectionEvent { - return { applyingInstructionsCount: 0, referencedInstructionsCount: 0, agentInstructionsCount: 0, listedInstructionsCount: 0, totalInstructionsCount: 0 }; + return { applyingInstructionsCount: 0, referencedInstructionsCount: 0, agentInstructionsCount: 0, listedInstructionsCount: 0, totalInstructionsCount: 0, claudeRulesCount: 0, claudeMdCount: 0, claudeAgentsCount: 0 }; } type InstructionsCollectionClassification = { @@ -44,6 +47,9 @@ type InstructionsCollectionClassification = { agentInstructionsCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of agent instructions added (copilot-instructions.md and agents.md).' }; listedInstructionsCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of instruction patterns added.' }; totalInstructionsCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Total number of instruction entries added to variables.' }; + claudeRulesCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of Claude rules files (.claude/rules/) added via pattern matching.' }; + claudeMdCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of CLAUDE.md agent instruction files added.' }; + claudeAgentsCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of Claude agent files (.claude/agents/) listed as subagents.' }; owner: 'digitarald'; comment: 'Tracks automatic instruction collection usage in chat prompt system.'; }; @@ -100,7 +106,7 @@ export class ComputeAutomaticInstructions { // get copilot instructions await this._addAgentInstructions(variables, telemetryEvent, token); - const instructionsListVariable = await this._getInstructionsWithPatternsList(instructionFiles, variables, token); + const instructionsListVariable = await this._getInstructionsWithPatternsList(instructionFiles, variables, telemetryEvent, token); if (instructionsListVariable) { variables.add(instructionsListVariable); telemetryEvent.listedInstructionsCount++; @@ -159,6 +165,9 @@ export class ComputeAutomaticInstructions { variables.add(toPromptFileVariableEntry(uri, PromptFileVariableKind.Instruction, reason, true)); telemetryEvent.applyingInstructionsCount++; + if (isClaudeRules) { + telemetryEvent.claudeRulesCount++; + } } else { this._logService.trace(`[InstructionsContextComputer] No match for ${uri} with ${pattern}`); } @@ -199,6 +208,9 @@ export class ComputeAutomaticInstructions { } telemetryEvent.agentInstructionsCount++; + if (type === AgentFileType.claudeMd) { + telemetryEvent.claudeMdCount++; + } logger.logInfo(`Agent instruction file added: ${uri.toString()}`); } @@ -278,7 +290,7 @@ export class ComputeAutomaticInstructions { return undefined; } - private async _getInstructionsWithPatternsList(instructionFiles: readonly IPromptPath[], _existingVariables: ChatRequestVariableSet, token: CancellationToken): Promise { + private async _getInstructionsWithPatternsList(instructionFiles: readonly IPromptPath[], _existingVariables: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise { const readTool = this._getTool('readFile'); const runSubagentTool = this._getTool(VSCodeToolReference.runSubagent); @@ -398,6 +410,9 @@ export class ComputeAutomaticInstructions { entries.push(`${agent.argumentHint}`); } entries.push(''); + if (isInClaudeAgentsFolder(agent.uri)) { + telemetryEvent.claudeAgentsCount++; + } } } entries.push('', '', ''); // add trailing newline diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts index 186fee36470..db61519b2b8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts @@ -204,6 +204,14 @@ function isInAgentsFolder(fileUri: URI): boolean { return dir.endsWith('/' + AGENTS_SOURCE_FOLDER) || dir.endsWith('/' + CLAUDE_AGENTS_SOURCE_FOLDER); } +/** + * Helper function to check if a file is directly in the .claude/agents/ folder. + */ +export function isInClaudeAgentsFolder(fileUri: URI): boolean { + const dir = dirname(fileUri.path); + return dir.endsWith('/' + CLAUDE_AGENTS_SOURCE_FOLDER); +} + /** * Helper function to check if a file is inside the .claude/rules/ folder (including subfolders). * Claude rules files (.md) in this folder are treated as instruction files. diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts index b53e9e8fd1b..5733ddff827 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts @@ -27,7 +27,7 @@ import { testWorkspace } from '../../../../../../platform/workspace/test/common/ import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; import { TestContextService, TestUserDataProfileService } from '../../../../../test/common/workbenchTestServices.js'; import { ChatRequestVariableSet, isPromptFileVariableEntry, isPromptTextVariableEntry, toFileVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; -import { ComputeAutomaticInstructions } from '../../../common/promptSyntax/computeAutomaticInstructions.js'; +import { ComputeAutomaticInstructions, InstructionsCollectionEvent } from '../../../common/promptSyntax/computeAutomaticInstructions.js'; import { PromptsConfig } from '../../../common/promptSyntax/config/config.js'; import { AGENTS_SOURCE_FOLDER, CLAUDE_RULES_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../common/promptSyntax/config/promptFileLocations.js'; import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/promptTypes.js'; @@ -871,13 +871,9 @@ suite('ComputeAutomaticInstructions', () => { '---', 'applyTo: "**/*.ts"', '---', - 'TS instructions [](./referenced.instructions.md)', + 'TS instructions', ] }, - { - path: `${rootFolder}/.github/instructions/referenced.instructions.md`, - contents: ['Referenced content'], - }, { path: `${rootFolder}/.github/copilot-instructions.md`, contents: ['Copilot instructions'], @@ -908,16 +904,174 @@ suite('ComputeAutomaticInstructions', () => { const telemetryEvent = telemetryEvents.find(e => e.eventName === 'instructionsCollected'); assert.ok(telemetryEvent, 'Should emit telemetry event'); - const data = telemetryEvent.data as { - applyingInstructionsCount: number; - referencedInstructionsCount: number; - agentInstructionsCount: number; - totalInstructionsCount: number; - }; - assert.ok(data.applyingInstructionsCount >= 0, 'Should have applying count'); - assert.ok(data.referencedInstructionsCount >= 0, 'Should have referenced count'); - assert.ok(data.agentInstructionsCount >= 0, 'Should have agent count'); - assert.ok(data.totalInstructionsCount >= 0, 'Should have total count'); + const data = telemetryEvent.data as InstructionsCollectionEvent; + assert.deepStrictEqual(data, { + applyingInstructionsCount: 1, + referencedInstructionsCount: 0, + agentInstructionsCount: 2, + listedInstructionsCount: 0, + totalInstructionsCount: 3, + claudeRulesCount: 0, + claudeMdCount: 0, + claudeAgentsCount: 0, + }); + }); + + test('should track Claude rules in telemetry', async () => { + const rootFolderName = 'telemetry-claude-rules-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.claude/rules/code-style.md`, + contents: ['Code style guidelines'], + }, + { + path: `${rootFolder}/.claude/rules/testing.md`, + contents: [ + '---', + 'paths:', + ' - "**/*.test.ts"', + '---', + 'Testing guidelines', + ], + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['code'], + }, + ]); + + const telemetryEvents: { eventName: string; data: unknown }[] = []; + const mockTelemetryService = { + publicLog2: (eventName: string, data: unknown) => { + telemetryEvents.push({ eventName, data }); + } + } as unknown as ITelemetryService; + instaService.stub(ITelemetryService, mockTelemetryService); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatModeKind.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const telemetryEvent = telemetryEvents.find(e => e.eventName === 'instructionsCollected'); + assert.ok(telemetryEvent, 'Should emit telemetry event'); + const data = telemetryEvent.data as InstructionsCollectionEvent; + // code-style.md defaults to ** so should match; testing.md only matches *.test.ts so should not match + assert.strictEqual(data.claudeRulesCount, 1, 'Should count 1 Claude rules file (code-style.md matches **)'); + assert.strictEqual(data.applyingInstructionsCount, 1, 'Claude rules count as applying instructions'); + assert.strictEqual(data.claudeMdCount, 0, 'Should have no CLAUDE.md count'); + }); + + test('should track CLAUDE.md in telemetry', async () => { + const rootFolderName = 'telemetry-claudemd-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + testConfigService.setUserConfiguration(PromptsConfig.USE_CLAUDE_MD, true); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/CLAUDE.md`, + contents: ['Claude guidelines'], + }, + { + path: `${rootFolder}/.claude/CLAUDE.md`, + contents: ['More Claude guidelines'], + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['code'], + }, + ]); + + const telemetryEvents: { eventName: string; data: unknown }[] = []; + const mockTelemetryService = { + publicLog2: (eventName: string, data: unknown) => { + telemetryEvents.push({ eventName, data }); + } + } as unknown as ITelemetryService; + instaService.stub(ITelemetryService, mockTelemetryService); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatModeKind.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const telemetryEvent = telemetryEvents.find(e => e.eventName === 'instructionsCollected'); + assert.ok(telemetryEvent, 'Should emit telemetry event'); + const data = telemetryEvent.data as InstructionsCollectionEvent; + assert.strictEqual(data.claudeMdCount, 2, 'Should count both CLAUDE.md files'); + assert.strictEqual(data.claudeRulesCount, 0, 'Should have no Claude rules count'); + }); + + test('should track Claude agents in telemetry', async () => { + const rootFolderName = 'telemetry-claude-agents-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + testConfigService.setUserConfiguration('chat.customAgentInSubagent.enabled', true); + testConfigService.setUserConfiguration(PromptsConfig.AGENTS_LOCATION_KEY, { + [AGENTS_SOURCE_FOLDER]: true, + '.claude/agents': true, + }); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.claude/agents/claude-agent.agent.md`, + contents: [ + '---', + 'description: \'A Claude agent\'', + '---', + 'Claude agent content', + ] + }, + { + path: `${rootFolder}/.github/agents/gh-agent.agent.md`, + contents: [ + '---', + 'description: \'A GitHub agent\'', + '---', + 'GitHub agent content', + ] + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['code'], + }, + ]); + + const telemetryEvents: { eventName: string; data: unknown }[] = []; + const mockTelemetryService = { + publicLog2: (eventName: string, data: unknown) => { + telemetryEvents.push({ eventName, data }); + } + } as unknown as ITelemetryService; + instaService.stub(ITelemetryService, mockTelemetryService); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatModeKind.Agent, + { 'vscode_runSubagent': true }, + ['*'] + ); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const telemetryEvent = telemetryEvents.find(e => e.eventName === 'instructionsCollected'); + assert.ok(telemetryEvent, 'Should emit telemetry event'); + const data = telemetryEvent.data as InstructionsCollectionEvent; + assert.strictEqual(data.claudeAgentsCount, 1, 'Should count 1 Claude agent'); }); }); From 182401098564c84f2ff76906bce37c73e6f17d82 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 11 Feb 2026 18:28:13 -0800 Subject: [PATCH 65/72] Fix restoring non-local chat session --- .../common/chatService/chatServiceImpl.ts | 27 +++++++++++-------- .../contrib/chat/common/model/chatModel.ts | 8 +++--- .../chat/common/model/chatModelStore.ts | 1 - 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index b55b824cd7d..5740a3345c7 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -437,15 +437,14 @@ export class ChatService extends Disposable implements IChatService { initialData: undefined, location, sessionResource, - sessionId, canUseTools: options?.canUseTools ?? true, disableBackgroundKeepAlive: options?.disableBackgroundKeepAlive }); } private _startSession(props: IStartSessionProps): ChatModel { - const { initialData, location, sessionResource, sessionId, canUseTools, transferEditingSession, disableBackgroundKeepAlive, inputState } = props; - const model = this.instantiationService.createInstance(ChatModel, initialData, { initialLocation: location, canUseTools, resource: sessionResource, sessionId, disableBackgroundKeepAlive, inputState }); + const { initialData, location, sessionResource, canUseTools, transferEditingSession, disableBackgroundKeepAlive, inputState } = props; + const model = this.instantiationService.createInstance(ChatModel, initialData, { initialLocation: location, canUseTools, resource: sessionResource, disableBackgroundKeepAlive, inputState }); if (location === ChatAgentLocation.Chat) { model.startEditingSession(true, transferEditingSession); } @@ -503,17 +502,17 @@ export class ChatService extends Disposable implements IChatService { return existingRef; } - const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); - if (!sessionId) { - throw new Error(`Cannot restore non-local session ${sessionResource}`); - } - let sessionData: ISerializedChatDataReference | undefined; if (isEqual(this.transferredSessionResource, sessionResource)) { this._transferredSessionResource = undefined; sessionData = await this._chatSessionStore.readTransferredSession(sessionResource); } else { - sessionData = await this._chatSessionStore.readSession(sessionId); + const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + if (localSessionId) { + sessionData = await this._chatSessionStore.readSession(localSessionId); + } else { + return this.loadSessionForResource(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); + } } if (!sessionData) { @@ -524,7 +523,6 @@ export class ChatService extends Disposable implements IChatService { initialData: sessionData, location: sessionData.value.initialLocation ?? ChatAgentLocation.Chat, sessionResource, - sessionId, canUseTools: true, }); @@ -550,7 +548,6 @@ export class ChatService extends Disposable implements IChatService { initialData: { value: data, serializer: new ChatSessionOperationLog() }, location: data.initialLocation ?? ChatAgentLocation.Chat, sessionResource, - sessionId, canUseTools: true, }); } @@ -568,6 +565,14 @@ export class ChatService extends Disposable implements IChatService { } const providedSession = await this.chatSessionService.getOrCreateChatSession(chatSessionResource, CancellationToken.None); + + // Make sure we haven't created this in the meantime + const existingRefAfterProvision = this._sessionModels.acquireExisting(chatSessionResource); + if (existingRefAfterProvision) { + providedSession.dispose(); + return existingRefAfterProvision; + } + const chatSessionType = chatSessionResource.scheme; // Contributed sessions do not use UI tools diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 7f9170a04f2..1c0abf70509 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -37,7 +37,7 @@ import { IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../languageModels.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, UserSelectedTools, reviveSerializedAgent } from '../participants/chatAgents.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from '../requestParser/chatParserTypes.js'; -import { LocalChatSessionUri } from './chatUri.js'; +import { chatSessionResourceToId, LocalChatSessionUri } from './chatUri.js'; import { ObjectMutationLog } from './objectMutationLog.js'; @@ -2102,7 +2102,7 @@ export class ChatModel extends Disposable implements IChatModel { constructor( dataRef: ISerializedChatDataReference | undefined, - initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource?: URI; sessionId?: string; disableBackgroundKeepAlive?: boolean }, + initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource: URI; disableBackgroundKeepAlive?: boolean }, @ILogService private readonly logService: ILogService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IChatEditingService private readonly chatEditingService: IChatEditingService, @@ -2118,8 +2118,8 @@ export class ChatModel extends Disposable implements IChatModel { } this._isImported = !!initialData && isValidExportedData && !isValidFullData; - this._sessionId = (isValidFullData && initialData.sessionId) || initialModelProps.sessionId || generateUuid(); - this._sessionResource = initialModelProps.resource ?? LocalChatSessionUri.forSession(this._sessionId); + this._sessionResource = initialModelProps.resource; + this._sessionId = (isValidFullData && initialData.sessionId) || chatSessionResourceToId(initialModelProps.resource); this._disableBackgroundKeepAlive = initialModelProps.disableBackgroundKeepAlive ?? false; this._requests = initialData ? this._deserialize(initialData) : []; diff --git a/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts b/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts index 42305065ed5..268dba2ec10 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts @@ -16,7 +16,6 @@ export interface IStartSessionProps { readonly initialData?: ISerializedChatDataReference; readonly location: ChatAgentLocation; readonly sessionResource: URI; - readonly sessionId?: string; readonly canUseTools: boolean; readonly transferEditingSession?: IChatEditingSession; readonly disableBackgroundKeepAlive?: boolean; From 962b547dffae5d07bdf731012abe9f64fe004712 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 11 Feb 2026 18:31:49 -0800 Subject: [PATCH 66/72] Hook parsing fixes (#294745) --- .../chat/browser/promptSyntax/hookActions.ts | 33 ++++--- .../common/promptSyntax/hookClaudeCompat.ts | 85 +++++++++++------- .../common/promptSyntax/hookCompatibility.ts | 11 ++- .../promptSyntax/hookClaudeCompat.test.ts | 87 +++++++++++++++++- .../promptSyntax/hookCompatibility.test.ts | 88 +++++++++++++++++++ 5 files changed, 253 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts index d59d0c04949..e37b11c22cd 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { parse as parseJSONC } from '../../../../../base/common/jsonc.js'; +import { setProperty, applyEdits } from '../../../../../base/common/jsonEdit.js'; +import { FormattingOptions } from '../../../../../base/common/jsonFormatter.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; @@ -102,7 +105,7 @@ async function addHookToFile( if (fileExists) { const existingContent = await fileService.readFile(hookFileUri); try { - hooksContent = JSON.parse(existingContent.value.toString()); + hooksContent = parseJSONC(existingContent.value.toString()); // Ensure hooks object exists if (!hooksContent.hooks) { hooksContent.hooks = {}; @@ -144,19 +147,25 @@ async function addHookToFile( // Use existing key if found, otherwise use the detected naming convention const keyToUse = existingKeyForType ?? hookTypeKeyName; - // Add the new hook entry (append if hook type already exists) + // Determine the new hook index (append if hook type already exists) const newHookEntry = buildNewHookEntry(sourceFormat); - let newHookIndex: number; - if (!hooksContent.hooks[keyToUse]) { - hooksContent.hooks[keyToUse] = [newHookEntry]; - newHookIndex = 0; - } else { - hooksContent.hooks[keyToUse].push(newHookEntry); - newHookIndex = hooksContent.hooks[keyToUse].length - 1; - } + const existingHooks = hooksContent.hooks[keyToUse]; + const newHookIndex = Array.isArray(existingHooks) ? existingHooks.length : 0; - // Write the file - const jsonContent = JSON.stringify(hooksContent, null, '\t'); + // Generate the new JSON content using setProperty to preserve comments + let jsonContent: string; + if (fileExists) { + // Use setProperty to make targeted edits that preserve comments + const originalText = (await fileService.readFile(hookFileUri)).value.toString(); + const detectedEol = originalText.includes('\r\n') ? '\r\n' : '\n'; + const formattingOptions: FormattingOptions = { tabSize: 1, insertSpaces: false, eol: detectedEol }; + const edits = setProperty(originalText, ['hooks', keyToUse, newHookIndex], newHookEntry, formattingOptions); + jsonContent = applyEdits(originalText, edits); + } else { + // New file - use JSON.stringify since there are no comments to preserve + const newContent = { hooks: { [keyToUse]: [newHookEntry] } }; + jsonContent = JSON.stringify(newContent, null, '\t'); + } // Check if the file is already open in an editor const existingEditor = editorService.editors.find(e => isEqual(e.resource, hookFileUri)); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts index c159acfa4c3..eb567363a8e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts @@ -128,28 +128,9 @@ export function parseClaudeHooks( const commands: IHookCommand[] = []; for (const item of hookArray) { - if (!item || typeof item !== 'object') { - continue; - } - - const itemObj = item as Record; - - // Claude can have nested hooks with matchers: { matcher: "Bash", hooks: [...] } - const nestedHooks = (itemObj as { hooks?: unknown }).hooks; - if (nestedHooks !== undefined && Array.isArray(nestedHooks)) { - for (const nestedHook of nestedHooks) { - const resolved = resolveClaudeCommand(nestedHook as Record, workspaceRootUri, userHome); - if (resolved) { - commands.push(resolved); - } - } - } else { - // Direct hook command - const resolved = resolveClaudeCommand(itemObj, workspaceRootUri, userHome); - if (resolved) { - commands.push(resolved); - } - } + // Use shared helper that handles both direct commands and nested matcher structures + const extracted = extractHookCommandsFromItem(item, workspaceRootUri, userHome); + commands.push(...extracted); } if (commands.length > 0) { @@ -166,19 +147,59 @@ export function parseClaudeHooks( } /** - * Resolves a Claude hook command to our IHookCommand format. - * Claude commands can be: { type: "command", command: "..." } or { command: "..." } + * Helper to extract hook commands from an item that could be: + * 1. A direct command object: { type: 'command', command: '...' } + * 2. A nested structure with matcher (Claude style): { matcher: '...', hooks: [{ type: 'command', command: '...' }] } + * + * This allows Copilot format to handle Claude-style entries if pasted. + * Also handles Claude's leniency where 'type' field can be omitted. */ -function resolveClaudeCommand( - raw: Record, +export function extractHookCommandsFromItem( + item: unknown, workspaceRootUri: URI | undefined, userHome: string -): IHookCommand | undefined { - // Claude might not require 'type' field, so we're more lenient - const hasValidType = raw.type === undefined || raw.type === 'command'; - if (!hasValidType) { - return undefined; +): IHookCommand[] { + if (!item || typeof item !== 'object') { + return []; } - return resolveHookCommand(raw, workspaceRootUri, userHome); + const itemObj = item as Record; + const commands: IHookCommand[] = []; + + // Check for nested hooks with matcher (Claude style): { matcher: "...", hooks: [...] } + const nestedHooks = itemObj.hooks; + if (nestedHooks !== undefined && Array.isArray(nestedHooks)) { + for (const nestedHook of nestedHooks) { + if (!nestedHook || typeof nestedHook !== 'object') { + continue; + } + const normalized = normalizeForResolve(nestedHook as Record); + const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); + if (resolved) { + commands.push(resolved); + } + } + } else { + // Direct command object + const normalized = normalizeForResolve(itemObj); + const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); + if (resolved) { + commands.push(resolved); + } + } + + return commands; +} + +/** + * Normalizes a hook command object for resolving. + * Claude format allows omitting the 'type' field, treating it as 'command'. + * This ensures compatibility when Claude-style hooks are pasted into Copilot format. + */ +function normalizeForResolve(raw: Record): Record { + // If type is missing or already 'command', ensure it's set to 'command' + if (raw.type === undefined || raw.type === 'command') { + return { ...raw, type: 'command' }; + } + return raw; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts index 6bdf4afdc89..d00fd26cb1e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookCompatibility.ts @@ -5,8 +5,8 @@ import { URI } from '../../../../../base/common/uri.js'; import { basename, dirname } from '../../../../../base/common/path.js'; -import { HookType, IHookCommand, toHookType, resolveHookCommand } from './hookSchema.js'; -import { parseClaudeHooks } from './hookClaudeCompat.js'; +import { HookType, IHookCommand, toHookType } from './hookSchema.js'; +import { parseClaudeHooks, extractHookCommandsFromItem } from './hookClaudeCompat.js'; import { resolveCopilotCliHookType } from './hookCopilotCliCompat.js'; /** @@ -97,10 +97,9 @@ export function parseCopilotHooks( const commands: IHookCommand[] = []; for (const item of hookArray) { - const resolved = resolveHookCommand(item as Record, workspaceRootUri, userHome); - if (resolved) { - commands.push(resolved); - } + // Use helper that handles both direct commands and Claude-style nested matcher structures + const extracted = extractHookCommandsFromItem(item, workspaceRootUri, userHome); + commands.push(...extracted); } if (commands.length > 0) { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts index 6f852ed7285..76b2ac54b07 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookClaudeCompat.test.ts @@ -6,13 +6,98 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { HookType } from '../../../common/promptSyntax/hookSchema.js'; -import { parseClaudeHooks, resolveClaudeHookType, getClaudeHookTypeName } from '../../../common/promptSyntax/hookClaudeCompat.js'; +import { parseClaudeHooks, resolveClaudeHookType, getClaudeHookTypeName, extractHookCommandsFromItem } from '../../../common/promptSyntax/hookClaudeCompat.js'; import { getHookSourceFormat, HookSourceFormat, buildNewHookEntry } from '../../../common/promptSyntax/hookCompatibility.js'; import { URI } from '../../../../../../base/common/uri.js'; suite('HookClaudeCompat', () => { ensureNoDisposablesAreLeakedInTestSuite(); + suite('extractHookCommandsFromItem', () => { + const workspaceRoot = URI.file('/workspace'); + const userHome = '/home/user'; + + test('extracts direct command object', () => { + const item = { type: 'command', command: 'echo "test"' }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].command, 'echo "test"'); + }); + + test('extracts from nested matcher structure', () => { + const item = { + matcher: 'Bash', + hooks: [ + { type: 'command', command: 'echo "nested"' } + ] + }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].command, 'echo "nested"'); + }); + + test('extracts multiple hooks from matcher structure', () => { + const item = { + matcher: 'Write', + hooks: [ + { type: 'command', command: 'echo "first"' }, + { type: 'command', command: 'echo "second"' } + ] + }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].command, 'echo "first"'); + assert.strictEqual(result[1].command, 'echo "second"'); + }); + + test('handles command without type field (Claude format)', () => { + const item = { command: 'echo "no type"' }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].command, 'echo "no type"'); + }); + + test('handles nested command without type field', () => { + const item = { + matcher: 'Bash', + hooks: [ + { command: 'echo "no type nested"' } + ] + }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].command, 'echo "no type nested"'); + }); + + test('returns empty array for null item', () => { + const result = extractHookCommandsFromItem(null, workspaceRoot, userHome); + assert.strictEqual(result.length, 0); + }); + + test('returns empty array for undefined item', () => { + const result = extractHookCommandsFromItem(undefined, workspaceRoot, userHome); + assert.strictEqual(result.length, 0); + }); + + test('returns empty array for invalid type', () => { + const item = { type: 'script', command: 'echo "wrong type"' }; + + const result = extractHookCommandsFromItem(item, workspaceRoot, userHome); + + assert.strictEqual(result.length, 0); + }); + }); + suite('resolveClaudeHookType', () => { test('resolves PreToolUse', () => { assert.strictEqual(resolveClaudeHookType('PreToolUse'), HookType.PreToolUse); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts index 7d4ba6ffe51..ede5eeb5e52 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookCompatibility.test.ts @@ -52,6 +52,94 @@ suite('HookCompatibility', () => { assert.strictEqual(result.size, 0); }); }); + + suite('Claude-style matcher compatibility', () => { + test('parses Claude-style nested matcher structure', () => { + // When Claude format is pasted into Copilot hooks file + const json = { + hooks: { + PreToolUse: [ + { + matcher: 'Bash', + hooks: [ + { type: 'command', command: 'echo "from matcher"' } + ] + } + ] + } + }; + + const result = parseCopilotHooks(json, workspaceRoot, userHome); + + assert.strictEqual(result.size, 1); + const entry = result.get(HookType.PreToolUse)!; + assert.strictEqual(entry.hooks.length, 1); + assert.strictEqual(entry.hooks[0].command, 'echo "from matcher"'); + }); + + test('parses Claude-style nested matcher with multiple hooks', () => { + const json = { + hooks: { + PostToolUse: [ + { + matcher: 'Write', + hooks: [ + { type: 'command', command: 'echo "first"' }, + { type: 'command', command: 'echo "second"' } + ] + } + ] + } + }; + + const result = parseCopilotHooks(json, workspaceRoot, userHome); + + const entry = result.get(HookType.PostToolUse)!; + assert.strictEqual(entry.hooks.length, 2); + assert.strictEqual(entry.hooks[0].command, 'echo "first"'); + assert.strictEqual(entry.hooks[1].command, 'echo "second"'); + }); + + test('handles mixed direct and nested matcher entries', () => { + const json = { + hooks: { + PreToolUse: [ + { type: 'command', command: 'echo "direct"' }, + { + matcher: 'Bash', + hooks: [ + { type: 'command', command: 'echo "nested"' } + ] + } + ] + } + }; + + const result = parseCopilotHooks(json, workspaceRoot, userHome); + + const entry = result.get(HookType.PreToolUse)!; + assert.strictEqual(entry.hooks.length, 2); + assert.strictEqual(entry.hooks[0].command, 'echo "direct"'); + assert.strictEqual(entry.hooks[1].command, 'echo "nested"'); + }); + + test('handles Claude-style hook without type field', () => { + // Claude allows omitting the type field + const json = { + hooks: { + SessionStart: [ + { command: 'echo "no type"' } + ] + } + }; + + const result = parseCopilotHooks(json, workspaceRoot, userHome); + + const entry = result.get(HookType.SessionStart)!; + assert.strictEqual(entry.hooks.length, 1); + assert.strictEqual(entry.hooks[0].command, 'echo "no type"'); + }); + }); }); suite('parseHooksFromFile', () => { From b9ed3e0402ed2b991cb56bef2b69d5f0fc85f475 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:59:26 -0800 Subject: [PATCH 67/72] Fix tests --- .../contrib/chat/common/model/chatModel.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 1c0abf70509..3b741cb825a 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -2102,7 +2102,7 @@ export class ChatModel extends Disposable implements IChatModel { constructor( dataRef: ISerializedChatDataReference | undefined, - initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource: URI; disableBackgroundKeepAlive?: boolean }, + initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource?: URI; disableBackgroundKeepAlive?: boolean }, @ILogService private readonly logService: ILogService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IChatEditingService private readonly chatEditingService: IChatEditingService, @@ -2118,8 +2118,23 @@ export class ChatModel extends Disposable implements IChatModel { } this._isImported = !!initialData && isValidExportedData && !isValidFullData; - this._sessionResource = initialModelProps.resource; - this._sessionId = (isValidFullData && initialData.sessionId) || chatSessionResourceToId(initialModelProps.resource); + + // Set the session resource and id + if (initialModelProps.resource) { + // prefer using the provided resource if provided + this._sessionId = chatSessionResourceToId(initialModelProps.resource); + this._sessionResource = initialModelProps.resource; + } else if (isValidFullData) { + // Otherwise use the serialized id. This is only valid for local chat sessions + this._sessionId = initialData.sessionId; + this._sessionResource = LocalChatSessionUri.forSession(initialData.sessionId); + } else { + // Finally fall back to generating a new id for a local session. This is used in the case where a + // chat has been exported (but not serialized) + this._sessionId = generateUuid(); + this._sessionResource = LocalChatSessionUri.forSession(this._sessionId); + } + this._disableBackgroundKeepAlive = initialModelProps.disableBackgroundKeepAlive ?? false; this._requests = initialData ? this._deserialize(initialData) : []; From 4fbcb2b5de98af53d62927cf9b2e4b7ccade2d4a Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:12:54 -0800 Subject: [PATCH 68/72] Slight chat session controller optimization - Add should not retransmit all items - Skip updates if they don't do anything --- .../api/common/extHostChatSessions.ts | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index cec3c62b015..1e184be05e2 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -176,12 +176,17 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { } } +interface SessionCollectionListeners { + onItemsChanged(): void; + onItemChanged(item: vscode.ChatSessionItem): void; +} + class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection { readonly #items = new ResourceMap(); - #onItemsChanged: () => void; + readonly #callbacks: SessionCollectionListeners; - constructor(onItemsChanged: () => void) { - this.#onItemsChanged = onItemsChanged; + constructor(callbacks: SessionCollectionListeners) { + this.#callbacks = callbacks; } get size(): number { @@ -197,7 +202,7 @@ class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection for (const item of items) { this.#items.set(item.resource, item); } - this.#onItemsChanged(); + this.#callbacks.onItemsChanged(); } forEach(callback: (item: vscode.ChatSessionItem, collection: vscode.ChatSessionItemCollection) => unknown, thisArg?: any): void { @@ -207,13 +212,20 @@ class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection } add(item: vscode.ChatSessionItem): void { + const existing = this.#items.get(item.resource); + if (existing && existing === item) { + // We're adding the same item again + return; + } + this.#items.set(item.resource, item); - this.#onItemsChanged(); + this.#callbacks.onItemChanged(item); } delete(resource: vscode.Uri): void { - this.#items.delete(resource); - this.#onItemsChanged(); + if (this.#items.delete(resource)) { + this.#callbacks.onItemsChanged(); + } } get(resource: vscode.Uri): vscode.ChatSessionItem | undefined { @@ -324,8 +336,10 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio const onDidChangeChatSessionItemStateEmitter = disposables.add(new Emitter()); - const collection = new ChatSessionItemCollectionImpl(() => { + const collection = new ChatSessionItemCollectionImpl({ // Noop for providers + onItemsChanged: () => { }, + onItemChanged: () => { } }); // Helper to push items to main thread @@ -400,7 +414,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio void this._proxy.$setChatSessionItems(controllerHandle, items); }; - const collection = new ChatSessionItemCollectionImpl(onItemsChanged); + const onItemChanged = (item: vscode.ChatSessionItem) => { + void this._proxy.$updateChatSessionItem(controllerHandle, typeConvert.ChatSessionItem.from(item)); + }; + + const collection = new ChatSessionItemCollectionImpl({ onItemsChanged, onItemChanged }); const controller = Object.freeze({ id, @@ -420,7 +438,9 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } const item = new ChatSessionItemImpl(resource, label, () => { - void this._proxy.$updateChatSessionItem(controllerHandle, typeConvert.ChatSessionItem.from(item)); + if (collection.get(resource) === item) { + onItemChanged(item); + } }); return item; }, From 8eefef2a87f3dff55aa6dc6590ff9e5514b19662 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:26:44 -0800 Subject: [PATCH 69/72] Make chat session item timing properties readonly Clarifying that timing properties can't be updated individually on managed chat session items. If you want the change to apply, you need to re-set the whole property --- .../vscode.proposed.chatSessionsProvider.d.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index df078abc002..c3641a37061 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -102,7 +102,7 @@ declare module 'vscode' { * * This is also called on first load to get the initial set of items. */ - refreshHandler: (token: CancellationToken) => Thenable; + readonly refreshHandler: (token: CancellationToken) => Thenable; /** * Fired when an item's archived state changes. @@ -204,33 +204,33 @@ declare module 'vscode' { /** * Timestamp when the session was created in milliseconds elapsed since January 1, 1970 00:00:00 UTC. */ - created: number; + readonly created: number; /** * Timestamp when the most recent request started in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * * Should be undefined if no requests have been made yet. */ - lastRequestStarted?: number; + readonly lastRequestStarted?: number; /** * Timestamp when the most recent request completed in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * * Should be undefined if the most recent request is still in progress or if no requests have been made yet. */ - lastRequestEnded?: number; + readonly lastRequestEnded?: number; /** * Session start timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * @deprecated Use `created` and `lastRequestStarted` instead. */ - startTime?: number; + readonly startTime?: number; /** * Session end timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * @deprecated Use `lastRequestEnded` instead. */ - endTime?: number; + readonly endTime?: number; }; /** From a413cd55102f18036bc5f8bbcd9fa3c3ef9e0c24 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:38:39 -0800 Subject: [PATCH 70/72] Fix add vs update confusion --- .../api/browser/mainThreadChatSessions.ts | 44 ++++++++----------- .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/common/extHostChatSessions.ts | 20 +++++---- 3 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index da9a664e813..47bff4e1d83 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -354,13 +354,9 @@ class MainThreadChatSessionItemController extends Disposable implements IChatSes this._onDidChangeChatSessionItems.fire(); } - updateItem(item: IChatSessionItem): void { - if (this._items.has(item.resource)) { - this._items.set(item.resource, item); - this._onDidChangeChatSessionItems.fire(); - } else { - console.warn(`Item with resource ${item.resource.toString()} does not exist. Skipping update.`); - } + addOrUpdateItem(item: IChatSessionItem): void { + this._items.set(item.resource, item); + this._onDidChangeChatSessionItems.fire(); } fireOnDidChangeChatSessionItems(): void { @@ -440,8 +436,17 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat )); } + private getController(handle: number): MainThreadChatSessionItemController { + const registration = this._itemControllerRegistrations.get(handle); + if (!registration) { + throw new Error(`No chat session controller registered for handle ${handle}`); + } + return registration.controller; + } + $onDidChangeChatSessionItems(handle: number): void { - this._itemControllerRegistrations.get(handle)?.controller.fireOnDidChangeChatSessionItems(); + const controller = this.getController(handle); + controller.fireOnDidChangeChatSessionItems(); } private async _resolveSessionItem(item: Dto): Promise { @@ -474,31 +479,20 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat }; } - async $setChatSessionItems(handle: number, items: Dto[]): Promise { - const registration = this._itemControllerRegistrations.get(handle); - if (!registration) { - this._logService.warn(`No chat session controller registered for handle ${handle}`); - return; - } - + async $setChatSessionItems(controllerHandle: number, items: Dto[]): Promise { + const controller = this.getController(controllerHandle); const resolvedItems = await Promise.all(items.map(item => this._resolveSessionItem(item))); - registration.controller.setItems(resolvedItems); + controller.setItems(resolvedItems); } - async $updateChatSessionItem(controllerHandle: number, item: Dto): Promise { - const registration = this._itemControllerRegistrations.get(controllerHandle); - if (!registration) { - this._logService.warn(`No chat session controller registered for handle ${controllerHandle}`); - return; - } - + async $addOrUpdateChatSessionItem(controllerHandle: number, item: Dto): Promise { + const controller = this.getController(controllerHandle); const resolvedItem = await this._resolveSessionItem(item); - registration.controller.updateItem(resolvedItem); + controller.addOrUpdateItem(resolvedItem); } $onDidChangeChatSessionOptions(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string }>): void { const sessionResource = URI.revive(sessionResourceComponents); - this._chatSessionsService.notifySessionOptionsChange(sessionResource, updates); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 422aa1b5827..d12f99c0051 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3418,7 +3418,7 @@ export interface MainThreadChatSessionsShape extends IDisposable { $registerChatSessionItemController(handle: number, chatSessionType: string): void; $unregisterChatSessionItemController(handle: number): void; $setChatSessionItems(handle: number, items: Dto[]): Promise; - $updateChatSessionItem(handle: number, item: Dto): Promise; + $addOrUpdateChatSessionItem(handle: number, item: Dto): Promise; $onDidChangeChatSessionItems(handle: number): void; $onDidCommitChatSessionItem(handle: number, original: UriComponents, modified: UriComponents): void; $registerChatSessionContentProvider(handle: number, chatSessionScheme: string): void; diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 1e184be05e2..8868ad66110 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -178,7 +178,7 @@ class ChatSessionItemImpl implements vscode.ChatSessionItem { interface SessionCollectionListeners { onItemsChanged(): void; - onItemChanged(item: vscode.ChatSessionItem): void; + onItemAddedOrUpdated(item: vscode.ChatSessionItem): void; } class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection { @@ -219,7 +219,7 @@ class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection } this.#items.set(item.resource, item); - this.#callbacks.onItemChanged(item); + this.#callbacks.onItemAddedOrUpdated(item); } delete(resource: vscode.Uri): void { @@ -339,7 +339,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio const collection = new ChatSessionItemCollectionImpl({ // Noop for providers onItemsChanged: () => { }, - onItemChanged: () => { } + onItemAddedOrUpdated: () => { } }); // Helper to push items to main thread @@ -414,11 +414,12 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio void this._proxy.$setChatSessionItems(controllerHandle, items); }; - const onItemChanged = (item: vscode.ChatSessionItem) => { - void this._proxy.$updateChatSessionItem(controllerHandle, typeConvert.ChatSessionItem.from(item)); - }; - - const collection = new ChatSessionItemCollectionImpl({ onItemsChanged, onItemChanged }); + const collection = new ChatSessionItemCollectionImpl({ + onItemsChanged, + onItemAddedOrUpdated: (item: vscode.ChatSessionItem) => { + void this._proxy.$addOrUpdateChatSessionItem(controllerHandle, typeConvert.ChatSessionItem.from(item)); + } + }); const controller = Object.freeze({ id, @@ -438,8 +439,9 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } const item = new ChatSessionItemImpl(resource, label, () => { + // Make sure the item really is in the collection. If not we don't need to transmit it to the main thread yet if (collection.get(resource) === item) { - onItemChanged(item); + void this._proxy.$addOrUpdateChatSessionItem(controllerHandle, typeConvert.ChatSessionItem.from(item)); } }); return item; From 010803cf24098915faf4bd9c225b02975adc82bc Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 12 Feb 2026 10:15:54 +0100 Subject: [PATCH 71/72] Chat maximzed, settings view on top, esc closes both (fix #294544) (#294785) --- src/vs/workbench/browser/layout.ts | 8 +++- .../parts/editor/auxiliaryEditorPart.ts | 17 ++++--- .../workbench/browser/parts/editor/editor.ts | 4 +- .../browser/parts/editor/editorPart.ts | 12 ++--- .../browser/parts/editor/editorParts.ts | 6 +-- .../browser/parts/editor/modalEditorPart.ts | 44 +++++++++++++------ .../editor/common/editorGroupsService.ts | 20 ++++++++- .../test/browser/editorGroupsService.test.ts | 33 +++++++++++++- .../test/browser/workbenchTestServices.ts | 4 +- 9 files changed, 112 insertions(+), 36 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 97a74fcf8fa..c7679ab51cb 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -22,7 +22,7 @@ import { getMenuBarVisibility, IPath, hasNativeTitlebar, hasCustomTitlebar, Titl import { IHostService } from '../services/host/browser/host.js'; import { IBrowserWorkbenchEnvironmentService } from '../services/environment/browser/environmentService.js'; import { IEditorService } from '../services/editor/common/editorService.js'; -import { EditorGroupLayout, GroupOrientation, GroupsOrder, IEditorGroupsService } from '../services/editor/common/editorGroupsService.js'; +import { EditorGroupLayout, GroupActivationReason, GroupOrientation, GroupsOrder, IEditorGroupsService } from '../services/editor/common/editorGroupsService.js'; import { SerializableGrid, ISerializableView, ISerializedGrid, Orientation, ISerializedNode, ISerializedLeafNode, Direction, IViewSize, Sizing, IViewVisibilityAnimationOptions } from '../../base/browser/ui/grid/grid.js'; import { Part } from './part.js'; import { IStatusbarService } from '../services/statusbar/browser/statusbar.js'; @@ -382,7 +382,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi showEditorIfHidden(); } })); - this._register(this.editorGroupService.mainPart.onDidActivateGroup(showEditorIfHidden)); + this._register(this.editorGroupService.mainPart.onDidActivateGroup(e => { + if (e.reason !== GroupActivationReason.PART_CLOSE) { + showEditorIfHidden(); // only show unless a modal/auxiliary part closes + } + })); // Revalidate center layout when active editor changes: diff editor quits centered mode this._register(this.mainPartEditorService.onDidActiveEditorChange(() => this.centerMainEditorLayout(this.stateModel.getRuntimeValue(LayoutStateKeys.MAIN_EDITOR_CENTERED)))); diff --git a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts index ec1cf50a7be..327b8bd85dc 100644 --- a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts @@ -5,6 +5,7 @@ import { onDidChangeFullscreen } from '../../../../base/browser/browser.js'; import { $, getActiveWindow, hide, show } from '../../../../base/browser/dom.js'; +import { mainWindow } from '../../../../base/browser/window.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, markAsSingleton, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { isNative } from '../../../../base/common/platform.js'; @@ -20,10 +21,10 @@ import { EditorPart, IEditorPartUIState } from './editorPart.js'; import { IAuxiliaryTitlebarPart } from '../titlebar/titlebarPart.js'; import { WindowTitle } from '../titlebar/windowTitle.js'; import { IAuxiliaryWindowOpenOptions, IAuxiliaryWindowService } from '../../../services/auxiliaryWindow/browser/auxiliaryWindowService.js'; -import { GroupDirection, GroupsOrder, IAuxiliaryEditorPart } from '../../../services/editor/common/editorGroupsService.js'; +import { GroupDirection, GroupsOrder, IAuxiliaryEditorPart, GroupActivationReason } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IHostService } from '../../../services/host/browser/host.js'; -import { IWorkbenchLayoutService, shouldShowCustomTitleBar } from '../../../services/layout/browser/layoutService.js'; +import { IWorkbenchLayoutService, Parts, shouldShowCustomTitleBar } from '../../../services/layout/browser/layoutService.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { IStatusbarService } from '../../../services/statusbar/browser/statusbar.js'; import { ITitleService } from '../../../services/title/browser/titleService.js'; @@ -409,13 +410,19 @@ class AuxiliaryEditorPartImpl extends EditorPart implements IAuxiliaryEditorPart private doRemoveLastGroup(preserveFocus?: boolean): void { const restoreFocus = !preserveFocus && this.shouldRestoreFocus(this.container); - // Activate next group + // Activate next group when closing const mostRecentlyActiveGroups = this.editorPartsView.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); const nextActiveGroup = mostRecentlyActiveGroups[1]; // [0] will be the current group we are about to dispose if (nextActiveGroup) { - nextActiveGroup.groupsView.activateGroup(nextActiveGroup); + nextActiveGroup.groupsView.activateGroup(nextActiveGroup, undefined, GroupActivationReason.PART_CLOSE); + } - if (restoreFocus) { + // Deal with focus: focus the next recently used group but skip + // this if the next group is in the main part and the main part + // is currently hidden, as that would make it visible. + if (nextActiveGroup && restoreFocus) { + const nextGroupInHiddenMainPart = nextActiveGroup.groupsView === this.editorPartsView.mainPart && !this.layoutService.isVisible(Parts.EDITOR_PART, mainWindow); + if (!nextGroupInHiddenMainPart) { nextActiveGroup.focus(); } } diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 96779124403..9a1b9ab092f 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -5,7 +5,7 @@ import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, SideBySideEditor, EditorCloseContext, IEditorPane, IEditorPartLimitOptions, IEditorPartDecorationOptions, IEditorWillOpenEvent, EditorInputWithOptions } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; -import { IEditorGroup, GroupDirection, IMergeGroupOptions, GroupsOrder, GroupsArrangement, IAuxiliaryEditorPart, IEditorPart, IModalEditorPart } from '../../../services/editor/common/editorGroupsService.js'; +import { IEditorGroup, GroupDirection, IMergeGroupOptions, GroupsOrder, GroupsArrangement, IAuxiliaryEditorPart, IEditorPart, IModalEditorPart, GroupActivationReason } from '../../../services/editor/common/editorGroupsService.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { Dimension } from '../../../../base/browser/dom.js'; import { Event } from '../../../../base/common/event.js'; @@ -218,7 +218,7 @@ export interface IEditorGroupsView { getGroup(identifier: GroupIdentifier): IEditorGroupView | undefined; getGroups(order: GroupsOrder): IEditorGroupView[]; - activateGroup(identifier: IEditorGroupView | GroupIdentifier, preserveWindowOrder?: boolean): IEditorGroupView; + activateGroup(identifier: IEditorGroupView | GroupIdentifier, preserveWindowOrder?: boolean, reason?: GroupActivationReason): IEditorGroupView; restoreGroup(identifier: IEditorGroupView | GroupIdentifier): IEditorGroupView; addGroup(location: IEditorGroupView | GroupIdentifier, direction: GroupDirection, groupToCopy?: IEditorGroupView): IEditorGroupView; diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 56ca88b06d6..4326662a9ab 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -8,7 +8,7 @@ import { Part } from '../../part.js'; import { Dimension, $, EventHelper, addDisposableGenericMouseDownListener, getWindow, isAncestorOfActiveElement, getActiveElement, isHTMLElement } from '../../../../base/browser/dom.js'; import { Event, Emitter, Relay, PauseableEmitter } from '../../../../base/common/event.js'; import { contrastBorder, editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; -import { GroupDirection, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, GroupsOrder, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument, IEditorSideGroup, IEditorDropTargetDelegate, IEditorPart } from '../../../services/editor/common/editorGroupsService.js'; +import { GroupDirection, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, GroupsOrder, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument, IEditorSideGroup, IEditorDropTargetDelegate, IEditorPart, GroupActivationReason, IEditorGroupActivationEvent } from '../../../services/editor/common/editorGroupsService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGrid, Sizing, ISerializedGrid, ISerializedNode, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from '../../../../base/browser/ui/grid/grid.js'; import { GroupIdentifier, EditorInputWithOptions, IEditorPartOptions, IEditorPartOptionsChangeEvent, GroupModelChangeKind } from '../../../common/editor.js'; @@ -116,7 +116,7 @@ export class EditorPart extends Part implements IEditorPart, private readonly _onDidChangeGroupMaximized = this._register(new Emitter()); readonly onDidChangeGroupMaximized = this._onDidChangeGroupMaximized.event; - private readonly _onDidActivateGroup = this._register(new Emitter()); + private readonly _onDidActivateGroup = this._register(new Emitter()); readonly onDidActivateGroup = this._onDidActivateGroup.event; private readonly _onDidAddGroup = this._register(new PauseableEmitter()); @@ -365,9 +365,9 @@ export class EditorPart extends Part implements IEditorPart, } } - activateGroup(group: IEditorGroupView | GroupIdentifier, preserveWindowOrder?: boolean): IEditorGroupView { + activateGroup(group: IEditorGroupView | GroupIdentifier, preserveWindowOrder?: boolean, reason?: GroupActivationReason): IEditorGroupView { const groupView = this.assertGroupView(group); - this.doSetGroupActive(groupView); + this.doSetGroupActive(groupView, reason); // Ensure window on top unless disabled if (!preserveWindowOrder) { @@ -684,7 +684,7 @@ export class EditorPart extends Part implements IEditorPart, return groupView; } - private doSetGroupActive(group: IEditorGroupView): void { + private doSetGroupActive(group: IEditorGroupView, reason = GroupActivationReason.DEFAULT): void { if (this._activeGroup !== group) { const previousActiveGroup = this._activeGroup; this._activeGroup = group; @@ -710,7 +710,7 @@ export class EditorPart extends Part implements IEditorPart, // Always fire the event that a group has been activated // even if its the same group that is already active to // signal the intent even when nothing has changed. - this._onDidActivateGroup.fire(group); + this._onDidActivateGroup.fire({ group, reason }); } private doRestoreGroup(group: IEditorGroupView): void { diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 61703e493fd..85651989603 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../nls.js'; -import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions, IEditorPart, IModalEditorPart } from '../../../services/editor/common/editorGroupsService.js'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions, IEditorPart, IModalEditorPart, IEditorGroupActivationEvent } from '../../../services/editor/common/editorGroupsService.js'; import { Emitter } from '../../../../base/common/event.js'; import { DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { GroupIdentifier, IEditorPartOptions } from '../../../common/editor.js'; @@ -234,7 +234,7 @@ export class EditorParts extends MultiWindowParts this._onDidAddGroup.fire(group))); disposables.add(part.onDidRemoveGroup(group => this._onDidRemoveGroup.fire(group))); disposables.add(part.onDidMoveGroup(group => this._onDidMoveGroup.fire(group))); - disposables.add(part.onDidActivateGroup(group => this._onDidActivateGroup.fire(group))); + disposables.add(part.onDidActivateGroup(e => this._onDidActivateGroup.fire(e))); disposables.add(part.onDidChangeGroupMaximized(maximized => this._onDidChangeGroupMaximized.fire(maximized))); disposables.add(part.onDidChangeGroupIndex(group => this._onDidChangeGroupIndex.fire(group))); @@ -558,7 +558,7 @@ export class EditorParts extends MultiWindowParts()); readonly onDidMoveGroup = this._onDidMoveGroup.event; - private readonly _onDidActivateGroup = this._register(new Emitter()); + private readonly _onDidActivateGroup = this._register(new Emitter()); readonly onDidActivateGroup = this._onDidActivateGroup.event; private readonly _onDidChangeGroupIndex = this._register(new Emitter()); diff --git a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts index 6e65299f400..8d2c6acb592 100644 --- a/src/vs/workbench/browser/parts/editor/modalEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/modalEditorPart.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import './media/modalEditorPart.css'; -import { $, addDisposableListener, append, EventHelper, EventType } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, append, EventHelper, EventType, isHTMLElement } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -20,12 +20,12 @@ import { IStorageService } from '../../../../platform/storage/common/storage.js' import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IEditorGroupView, IEditorPartsView } from './editor.js'; import { EditorPart } from './editorPart.js'; -import { GroupDirection, GroupsOrder, IModalEditorPart } from '../../../services/editor/common/editorGroupsService.js'; +import { GroupDirection, GroupsOrder, IModalEditorPart, GroupActivationReason } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { EditorPartModalContext, EditorPartModalMaximizedContext } from '../../../common/contextkeys.js'; import { Verbosity } from '../../../common/editor.js'; import { IHostService } from '../../../services/host/browser/host.js'; -import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; +import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { localize } from '../../../../nls.js'; @@ -208,6 +208,8 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { private readonly optionsDisposable = this._register(new MutableDisposable()); + private previousMainWindowActiveElement: Element | null = null; + constructor( windowId: number, editorPartsView: IEditorPartsView, @@ -226,6 +228,12 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { this.enforceModalPartOptions(); } + override create(parent: HTMLElement, options?: object): void { + this.previousMainWindowActiveElement = mainWindow.document.activeElement; + + super.create(parent, options); + } + private enforceModalPartOptions(): void { const editorCount = this.groups.reduce((count, group) => count + group.count, 0); this.optionsDisposable.value = this.enforcePartOptions({ @@ -264,7 +272,7 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { // Close modal when last group removed const groupView = this.assertGroupView(group); if (this.count === 1 && this.activeGroup === groupView) { - this.doRemoveLastGroup(preserveFocus); + this.doRemoveLastGroup(); } // Otherwise delegate to parent implementation @@ -273,18 +281,26 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart { } } - private doRemoveLastGroup(preserveFocus?: boolean): void { - const restoreFocus = !preserveFocus && this.shouldRestoreFocus(this.container); + private doRemoveLastGroup(): void { - // Activate next group - const mostRecentlyActiveGroups = this.editorPartsView.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); - const nextActiveGroup = mostRecentlyActiveGroups[1]; // [0] will be the current group we are about to dispose - if (nextActiveGroup) { - nextActiveGroup.groupsView.activateGroup(nextActiveGroup); + // Activate main editor group when closing + const activeMainGroup = this.editorPartsView.mainPart.activeGroup; + this.editorPartsView.mainPart.activateGroup(activeMainGroup, undefined, GroupActivationReason.PART_CLOSE); - if (restoreFocus) { - nextActiveGroup.focus(); - } + // Deal with focus: removing the last modal group + // means we return back to the main editor part. + // But we only want to focus that if it was focused + // before to prevent revealing the editor part if + // it was maybe hidden before. + const mainEditorPartContainer = this.layoutService.getContainer(mainWindow, Parts.EDITOR_PART); + if ( + !isHTMLElement(this.previousMainWindowActiveElement) || // invalid previous element + !this.previousMainWindowActiveElement.isConnected || // previous element no longer in the DOM + mainEditorPartContainer?.contains(this.previousMainWindowActiveElement) // previous element is inside main editor part + ) { + activeMainGroup.focus(); + } else { + this.previousMainWindowActiveElement.focus(); } this.doClose({ mergeConfirmingEditorsToMainPart: false }); diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index d7a24923b9e..d782d8df7ba 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -20,6 +20,24 @@ import { DeepPartial } from '../../../../base/common/types.js'; export const IEditorGroupsService = createDecorator('editorGroupsService'); +export const enum GroupActivationReason { + + /** + * Group was activated explicitly by user or programmatic action. + */ + DEFAULT = 0, + + /** + * Group was activated because a modal or auxiliary editor part was closing. + */ + PART_CLOSE = 1 +} + +export interface IEditorGroupActivationEvent { + readonly group: IEditorGroup; + readonly reason: GroupActivationReason; +} + export const enum GroupDirection { UP, DOWN, @@ -212,7 +230,7 @@ export interface IEditorGroupsContainer { /** * An event for when a group gets activated. */ - readonly onDidActivateGroup: Event; + readonly onDidActivateGroup: Event; /** * An event for when the index of a group changes. diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 8c44dd65409..3a42f865c96 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, ITestInstantiationService, workbenchTeardown, createEditorParts, TestEditorParts } from '../../../../test/browser/workbenchTestServices.js'; -import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement, IEditorGroupContextKeyProvider } from '../../common/editorGroupsService.js'; +import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement, IEditorGroupContextKeyProvider, GroupActivationReason, IEditorGroupActivationEvent } from '../../common/editorGroupsService.js'; import { CloseDirection, IEditorPartOptions, EditorsOrder, EditorInputCapabilities, GroupModelChangeKind, SideBySideEditor, IEditorFactoryRegistry, EditorExtensions } from '../../../../common/editor.js'; import { URI } from '../../../../../base/common/uri.js'; import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; @@ -2197,5 +2197,36 @@ suite('EditorGroupsService', () => { disposables.dispose(); }); + test('onDidActivateGroup carries activation reason', async function () { + const [part] = await createPart(); + + const activationEvents: IEditorGroupActivationEvent[] = []; + disposables.add(part.onDidActivateGroup(e => activationEvents.push(e))); + + const rootGroup = part.groups[0]; + const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + + // Activate a group explicitly - should carry DEFAULT reason + activationEvents.length = 0; + part.activateGroup(rightGroup); + assert.strictEqual(activationEvents.length, 1); + assert.strictEqual(activationEvents[0].group, rightGroup); + assert.strictEqual(activationEvents[0].reason, GroupActivationReason.DEFAULT); + + // Activate the same group again - should still fire with DEFAULT reason + activationEvents.length = 0; + part.activateGroup(rightGroup); + assert.strictEqual(activationEvents.length, 1); + assert.strictEqual(activationEvents[0].group, rightGroup); + assert.strictEqual(activationEvents[0].reason, GroupActivationReason.DEFAULT); + + // Activate root group back + activationEvents.length = 0; + part.activateGroup(rootGroup); + assert.strictEqual(activationEvents.length, 1); + assert.strictEqual(activationEvents[0].group, rootGroup); + assert.strictEqual(activationEvents[0].reason, GroupActivationReason.DEFAULT); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index ba91f4f6ac8..4d70e2e4a4b 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -149,7 +149,7 @@ import { CodeEditorService } from '../../services/editor/browser/codeEditorServi import { EditorPaneService } from '../../services/editor/browser/editorPaneService.js'; import { EditorResolverService } from '../../services/editor/browser/editorResolverService.js'; import { CustomEditorLabelService, ICustomEditorLabelService } from '../../services/editor/common/customEditorLabelService.js'; -import { EditorGroupLayout, GroupDirection, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, ICloseAllEditorsOptions, ICloseEditorOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorGroup, IEditorGroupContextKeyProvider, IEditorGroupsContainer, IEditorGroupsService, IEditorPart, IEditorReplacement, IEditorWorkingSet, IEditorWorkingSetOptions, IFindGroupScope, IMergeGroupOptions, IModalEditorPart } from '../../services/editor/common/editorGroupsService.js'; +import { EditorGroupLayout, GroupDirection, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, ICloseAllEditorsOptions, ICloseEditorOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorGroup, IEditorGroupActivationEvent, IEditorGroupContextKeyProvider, IEditorGroupsContainer, IEditorGroupsService, IEditorPart, IEditorReplacement, IEditorWorkingSet, IEditorWorkingSetOptions, IFindGroupScope, IMergeGroupOptions, IModalEditorPart } from '../../services/editor/common/editorGroupsService.js'; import { IEditorPaneService } from '../../services/editor/common/editorPaneService.js'; import { IEditorResolverService } from '../../services/editor/common/editorResolverService.js'; import { IEditorsChangeEvent, IEditorService, IRevertAllEditorsOptions, ISaveEditorsOptions, ISaveEditorsResult, PreferredGroup } from '../../services/editor/common/editorService.js'; @@ -863,7 +863,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { readonly onDidCreateAuxiliaryEditorPart: Event = Event.None; readonly onDidChangeActiveGroup: Event = Event.None; - readonly onDidActivateGroup: Event = Event.None; + readonly onDidActivateGroup: Event = Event.None; readonly onDidAddGroup: Event = Event.None; readonly onDidRemoveGroup: Event = Event.None; readonly onDidMoveGroup: Event = Event.None; From 3d7ca2537dee3a5585cfc5d7536e264e26ae978b Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 12 Feb 2026 01:18:26 -0800 Subject: [PATCH 72/72] Update update action button styles to match primary VS Code buttons (#294811) --- .../contrib/update/browser/releaseNotesEditor.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 7d207e48c41..550329a85ef 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -558,13 +558,12 @@ export class ReleaseNotesManager extends Disposable { font-size: var(--vscode-font-size); font-family: var(--vscode-font-family); white-space: nowrap; - box-shadow: 1px 1px 1px rgba(0,0,0,.25); + box-shadow: 0 2px 8px var(--vscode-widget-shadow); z-index: 100; overflow: hidden; display: flex; align-items: center; justify-content: center; - transition: border-radius 0.25s ease, padding 0.25s ease, width 0.25s ease; } #update-action-btn .icon { @@ -587,16 +586,18 @@ export class ReleaseNotesManager extends Disposable { max-width: 0; opacity: 0; margin-left: 0; - transition: max-width 0.25s ease, opacity 0.2s ease, margin-left 0.25s ease; } #update-action-btn:hover, #update-action-btn.expanded { background-color: var(--vscode-button-hoverBackground); - box-shadow: 2px 2px 2px rgba(0,0,0,.25); + box-shadow: 0 2px 8px var(--vscode-widget-shadow); width: auto; - border-radius: 20px; - padding: 0 14px; + height: auto; + max-height: 40px; + border-radius: var(--vscode-cornerRadius-small); + padding: 6px 10px; + line-height: 16px; } #update-action-btn:hover .label, @@ -608,6 +609,7 @@ export class ReleaseNotesManager extends Disposable { #update-action-btn.expanded { background-color: var(--vscode-button-background); + box-shadow: 0 2px 8px var(--vscode-widget-shadow); } body.vscode-high-contrast #update-action-btn {