Merge branch 'main' into benibenj/interested-slug

This commit is contained in:
Benjamin Christopher Simmonds
2026-01-29 17:58:30 +01:00
committed by GitHub
44 changed files with 1387 additions and 671 deletions
@@ -650,8 +650,6 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
async function getSchemaAssociations(forceRefresh: boolean): Promise<ISchemaAssociation[]> {
if (!schemaAssociationsCache || forceRefresh) {
schemaAssociationsCache = computeSchemaAssociations();
runtime.logOutputChannel.info(`Computed schema associations: ${(await schemaAssociationsCache).map(a => `${a.uri} -> [${a.fileMatch.join(', ')}]`).join('\n')}`);
}
return schemaAssociationsCache;
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.109.0",
"distro": "84c8b9580d546487fee8ff25a29c5f3f49d33799",
"distro": "6c9f72a1ba8565301b303ec4314f5a24d585f012",
"author": {
"name": "Microsoft Corporation"
},
@@ -240,4 +240,4 @@
"optionalDependencies": {
"windows-foreground-love": "0.6.1"
}
}
}
+145 -41
View File
@@ -7,13 +7,11 @@ import { BrowserFeatures } from '../../canIUse.js';
import * as DOM from '../../dom.js';
import { StandardMouseEvent } from '../../mouseEvent.js';
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../common/lifecycle.js';
import { AnchorAlignment, AnchorAxisAlignment, AnchorPosition, IRect, layout2d } from '../../../common/layout.js';
import * as platform from '../../../common/platform.js';
import { Range } from '../../../common/range.js';
import { OmitOptional } from '../../../common/types.js';
import './contextview.css';
export { AnchorAlignment, AnchorAxisAlignment, AnchorPosition } from '../../../common/layout.js';
export const enum ContextViewDOMPosition {
ABSOLUTE = 1,
FIXED,
@@ -33,6 +31,18 @@ export function isAnchor(obj: unknown): obj is IAnchor | OmitOptional<IAnchor> {
return !!anchor && typeof anchor.x === 'number' && typeof anchor.y === 'number';
}
export const enum AnchorAlignment {
LEFT, RIGHT
}
export const enum AnchorPosition {
BELOW, ABOVE
}
export const enum AnchorAxisAlignment {
VERTICAL, HORIZONTAL
}
export interface IDelegate {
/**
* The anchor where to position the context view.
@@ -63,40 +73,66 @@ export interface IContextViewProvider {
layout(): void;
}
export function getAnchorRect(anchor: HTMLElement | StandardMouseEvent | IAnchor): IRect {
// Get the element's position and size (to anchor the view)
if (DOM.isHTMLElement(anchor)) {
const elementPosition = DOM.getDomNodePagePosition(anchor);
export interface IPosition {
top: number;
left: number;
}
// In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element
// e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level.
// Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5
const zoom = DOM.getDomNodeZoomLevel(anchor);
export interface ISize {
width: number;
height: number;
}
return {
top: elementPosition.top * zoom,
left: elementPosition.left * zoom,
width: elementPosition.width * zoom,
height: elementPosition.height * zoom
};
} else if (isAnchor(anchor)) {
return {
top: anchor.y,
left: anchor.x,
width: anchor.width || 1,
height: anchor.height || 2
};
export interface IView extends IPosition, ISize { }
export const enum LayoutAnchorPosition {
Before,
After
}
export enum LayoutAnchorMode {
AVOID,
ALIGN
}
export interface ILayoutAnchor {
offset: number;
size: number;
mode?: LayoutAnchorMode; // default: AVOID
position: LayoutAnchorPosition;
}
/**
* Lays out a one dimensional view next to an anchor in a viewport.
*
* @returns The view offset within the viewport.
*/
export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): number {
const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size;
const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset;
if (anchor.position === LayoutAnchorPosition.Before) {
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
return layoutAfterAnchorBoundary; // happy case, lay it out after the anchor
}
if (viewSize <= layoutBeforeAnchorBoundary) {
return layoutBeforeAnchorBoundary - viewSize; // ok case, lay it out before the anchor
}
return Math.max(viewportSize - viewSize, 0); // sad case, lay it over the anchor
} else {
return {
top: anchor.posy,
left: anchor.posx,
// We are about to position the context view where the mouse
// cursor is. To prevent the view being exactly under the mouse
// when showing and thus potentially triggering an action within,
// we treat the mouse location like a small sized block element.
width: 2,
height: 2
};
if (viewSize <= layoutBeforeAnchorBoundary) {
return layoutBeforeAnchorBoundary - viewSize; // happy case, lay it out before the anchor
}
if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) {
return layoutAfterAnchorBoundary; // ok case, lay it out after the anchor
}
return 0; // sad case, lay it over the anchor
}
}
@@ -234,14 +270,82 @@ export class ContextView extends Disposable {
}
// Get anchor
const anchor = getAnchorRect(this.delegate!.getAnchor());
const anchor = this.delegate!.getAnchor();
// Compute around
let around: IView;
// Get the element's position and size (to anchor the view)
if (DOM.isHTMLElement(anchor)) {
const elementPosition = DOM.getDomNodePagePosition(anchor);
// In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element
// e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level.
// Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5
const zoom = DOM.getDomNodeZoomLevel(anchor);
around = {
top: elementPosition.top * zoom,
left: elementPosition.left * zoom,
width: elementPosition.width * zoom,
height: elementPosition.height * zoom
};
} else if (isAnchor(anchor)) {
around = {
top: anchor.y,
left: anchor.x,
width: anchor.width || 1,
height: anchor.height || 2
};
} else {
around = {
top: anchor.posy,
left: anchor.posx,
// We are about to position the context view where the mouse
// cursor is. To prevent the view being exactly under the mouse
// when showing and thus potentially triggering an action within,
// we treat the mouse location like a small sized block element.
width: 2,
height: 2
};
}
const viewSizeWidth = DOM.getTotalWidth(this.view);
const viewSizeHeight = DOM.getTotalHeight(this.view);
const anchorPosition = this.delegate!.anchorPosition ?? AnchorPosition.BELOW;
const anchorAlignment = this.delegate!.anchorAlignment ?? AnchorAlignment.LEFT;
const anchorAxisAlignment = this.delegate!.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL;
let top: number;
let left: number;
const activeWindow = DOM.getActiveWindow();
const viewport = { top: activeWindow.pageYOffset, left: activeWindow.pageXOffset, width: activeWindow.innerWidth, height: activeWindow.innerHeight };
const view = { width: DOM.getTotalWidth(this.view), height: DOM.getTotalHeight(this.view) };
const anchorPosition = this.delegate!.anchorPosition;
const anchorAlignment = this.delegate!.anchorAlignment;
const anchorAxisAlignment = this.delegate!.anchorAxisAlignment;
const { top, left } = layout2d(viewport, view, anchor, { anchorAlignment, anchorPosition, anchorAxisAlignment });
if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) {
const verticalAnchor: ILayoutAnchor = { offset: around.top - activeWindow.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
// if view intersects vertically with anchor, we must avoid the anchor
if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
horizontalAnchor.mode = LayoutAnchorMode.AVOID;
}
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
} else {
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
const verticalAnchor: ILayoutAnchor = { offset: around.top, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
// if view intersects horizontally with anchor, we must avoid the anchor
if (Range.intersects({ start: left, end: left + viewSizeWidth }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) {
verticalAnchor.mode = LayoutAnchorMode.AVOID;
}
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
}
this.view.classList.remove('top', 'bottom', 'left', 'right');
this.view.classList.add(anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');
+3 -3
View File
@@ -11,6 +11,7 @@ import { StandardKeyboardEvent } from '../../keyboardEvent.js';
import { StandardMouseEvent } from '../../mouseEvent.js';
import { ActionBar, ActionsOrientation, IActionViewItemProvider } from '../actionbar/actionbar.js';
import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../actionbar/actionViewItems.js';
import { AnchorAlignment, layout, LayoutAnchorPosition } from '../contextview/contextview.js';
import { DomScrollableElement } from '../scrollbar/scrollableElement.js';
import { EmptySubmenuAction, IAction, IActionRunner, Separator, SubmenuAction } from '../../../common/actions.js';
import { RunOnceScheduler } from '../../../common/async.js';
@@ -25,7 +26,6 @@ import { DisposableStore } from '../../../common/lifecycle.js';
import { isLinux, isMacintosh } from '../../../common/platform.js';
import { ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js';
import * as strings from '../../../common/strings.js';
import { AnchorAlignment, layout, LayoutAnchorPosition } from '../../../common/layout.js';
export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/;
export const MENU_ESCAPED_MNEMONIC_REGEX = /(&amp;)?(&amp;)([^\s&])/g;
@@ -859,7 +859,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
const ret = { top: 0, left: 0 };
// Start with horizontal
ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection.horizontal === HorizontalDirection.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width }).position;
ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection.horizontal === HorizontalDirection.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width });
// We don't have enough room to layout the menu fully, so we are overlapping the menu
if (ret.left >= entry.left && ret.left < entry.left + entry.width) {
@@ -872,7 +872,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
}
// Now that we have a horizontal position, try layout vertically
ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 }).position;
ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 });
// We didn't have enough room below, but we did above, so we shift down to align the menu
if (ret.top + submenu.height === entry.top && ret.top + entry.height + submenu.height <= windowDimensions.height) {
-1
View File
@@ -59,5 +59,4 @@ export interface IDefaultAccount {
readonly sessionId: string;
readonly enterprise: boolean;
readonly entitlementsData?: IEntitlementsData | null;
readonly policyData?: IPolicyData;
}
-166
View File
@@ -1,166 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Range } from './range.js';
export interface IAnchor {
x: number;
y: number;
width?: number;
height?: number;
}
export const enum AnchorAlignment {
LEFT, RIGHT
}
export const enum AnchorPosition {
BELOW, ABOVE
}
export const enum AnchorAxisAlignment {
VERTICAL, HORIZONTAL
}
interface IPosition {
readonly top: number;
readonly left: number;
}
interface ISize {
readonly width: number;
readonly height: number;
}
export interface IRect extends IPosition, ISize { }
export const enum LayoutAnchorPosition {
Before,
After
}
export enum LayoutAnchorMode {
AVOID,
ALIGN
}
export interface ILayoutAnchor {
offset: number;
size: number;
mode?: LayoutAnchorMode; // default: AVOID
position: LayoutAnchorPosition;
}
export interface ILayoutResult {
position: number;
result: 'ok' | 'flipped' | 'overlap';
}
/**
* Lays out a one dimensional view next to an anchor in a viewport.
*
* @returns The view offset within the viewport.
*/
export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): ILayoutResult {
const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size;
const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset;
if (anchor.position === LayoutAnchorPosition.Before) {
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
return { position: layoutAfterAnchorBoundary, result: 'ok' }; // happy case, lay it out after the anchor
}
if (viewSize <= layoutBeforeAnchorBoundary) {
return { position: layoutBeforeAnchorBoundary - viewSize, result: 'flipped' }; // ok case, lay it out before the anchor
}
return { position: Math.max(viewportSize - viewSize, 0), result: 'overlap' }; // sad case, lay it over the anchor
} else {
if (viewSize <= layoutBeforeAnchorBoundary) {
return { position: layoutBeforeAnchorBoundary - viewSize, result: 'ok' }; // happy case, lay it out before the anchor
}
if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) {
return { position: layoutAfterAnchorBoundary, result: 'flipped' }; // ok case, lay it out after the anchor
}
return { position: 0, result: 'overlap' }; // sad case, lay it over the anchor
}
}
interface ILayout2DOptions {
readonly anchorAlignment?: AnchorAlignment; // default: left
readonly anchorPosition?: AnchorPosition; // default: below
readonly anchorAxisAlignment?: AnchorAxisAlignment; // default: vertical
}
export interface ILayout2DResult {
top: number;
left: number;
bottom: number;
right: number;
anchorAlignment: AnchorAlignment;
anchorPosition: AnchorPosition;
}
export function layout2d(viewport: IRect, view: ISize, anchor: IRect, options?: ILayout2DOptions): ILayout2DResult {
let anchorAlignment = options?.anchorAlignment ?? AnchorAlignment.LEFT;
let anchorPosition = options?.anchorPosition ?? AnchorPosition.ABOVE;
const anchorAxisAlignment = options?.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL;
let top: number;
let left: number;
if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) {
const verticalAnchor: ILayoutAnchor = { offset: anchor.top - viewport.top, size: anchor.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.After : LayoutAnchorPosition.Before };
const horizontalAnchor: ILayoutAnchor = { offset: anchor.left, size: anchor.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
const verticalLayoutResult = layout(viewport.height, view.height, verticalAnchor);
top = verticalLayoutResult.position + viewport.top;
if (verticalLayoutResult.result === 'flipped') {
anchorPosition = anchorPosition === AnchorPosition.BELOW ? AnchorPosition.ABOVE : AnchorPosition.BELOW;
}
// if view intersects vertically with anchor, we must avoid the anchor
if (Range.intersects({ start: top, end: top + view.height }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
horizontalAnchor.mode = LayoutAnchorMode.AVOID;
}
const horizontalLayoutResult = layout(viewport.width, view.width, horizontalAnchor);
left = horizontalLayoutResult.position;
if (horizontalLayoutResult.result === 'flipped') {
anchorAlignment = anchorAlignment === AnchorAlignment.LEFT ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT;
}
} else {
const horizontalAnchor: ILayoutAnchor = { offset: anchor.left, size: anchor.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
const verticalAnchor: ILayoutAnchor = { offset: anchor.top, size: anchor.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.After : LayoutAnchorPosition.Before, mode: LayoutAnchorMode.ALIGN };
const horizontalLayoutResult = layout(viewport.width, view.width, horizontalAnchor);
left = horizontalLayoutResult.position;
if (horizontalLayoutResult.result === 'flipped') {
anchorAlignment = anchorAlignment === AnchorAlignment.LEFT ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT;
}
// if view intersects horizontally with anchor, we must avoid the anchor
if (Range.intersects({ start: left, end: left + view.width }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) {
verticalAnchor.mode = LayoutAnchorMode.AVOID;
}
const verticalLayoutResult = layout(viewport.height, view.height, verticalAnchor);
top = verticalLayoutResult.position + viewport.top;
if (verticalLayoutResult.result === 'flipped') {
anchorPosition = anchorPosition === AnchorPosition.BELOW ? AnchorPosition.ABOVE : AnchorPosition.BELOW;
}
}
const right = viewport.width - (left + view.width);
const bottom = viewport.height - (top + view.height);
return { top, left, bottom, right, anchorAlignment, anchorPosition };
}
+2 -2
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../nls.js';
import { IDefaultAccount } from './defaultAccount.js';
import { IPolicyData } from './defaultAccount.js';
/**
* System-wide policy file path for Linux systems.
@@ -96,5 +96,5 @@ export interface IPolicy {
*
* If `undefined`, the feature's setting is not locked and can be overridden by other means.
*/
readonly value?: (account: IDefaultAccount) => string | number | boolean | undefined;
readonly value?: (policyData: IPolicyData) => string | number | boolean | undefined;
}
@@ -4,26 +4,27 @@
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { layout, LayoutAnchorPosition } from '../../common/layout.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js';
import { layout, LayoutAnchorPosition } from '../../../../browser/ui/contextview/contextview.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../common/utils.js';
suite('Layout', function () {
suite('Contextview', function () {
test('layout', () => {
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }).position, 0);
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }).position, 50);
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }).position, 180);
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }), 0);
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }), 50);
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }), 180);
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }).position, 0);
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }).position, 30);
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }).position, 180);
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }).position, 50);
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }).position, 100);
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }).position, 130);
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }), 0);
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }), 30);
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }), 180);
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }).position, 50);
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }).position, 30);
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }).position, 130);
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }), 50);
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }), 100);
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }), 130);
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }), 50);
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }), 30);
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }), 130);
});
ensureNoDisposablesAreLeakedInTestSuite();
@@ -37,6 +37,7 @@ import { EditorWorkerHost } from '../../common/services/editorWorkerHost.js';
import { StringEdit } from '../../common/core/edits/stringEdit.js';
import { OffsetRange } from '../../common/core/ranges/offsetRange.js';
import { FileAccess } from '../../../base/common/network.js';
import { isCompletionsEnabledWithTextResourceConfig } from '../../common/services/completionsEnablement.js';
/**
* Stop the worker if it was not needed for 5 min.
@@ -280,7 +281,9 @@ class WordBasedCompletionItemProvider implements languages.CompletionItemProvide
return undefined;
}
if (config.wordBasedSuggestions === 'offWithInlineSuggestions' && this.languageFeaturesService.inlineCompletionsProvider.has(model)) {
if (config.wordBasedSuggestions === 'offWithInlineSuggestions'
&& this.languageFeaturesService.inlineCompletionsProvider.has(model)
&& isCompletionsEnabledWithTextResourceConfig(this._configurationService, model.uri, model.getLanguageId())) {
return undefined;
}
@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import product from '../../../platform/product/common/product.js';
import { isObject } from '../../../base/common/types.js';
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
import { ITextResourceConfigurationService } from './textResourceConfiguration.js';
import { URI } from '../../../base/common/uri.js';
/**
* Get the completions enablement setting name from product configuration.
*/
function getCompletionsEnablementSettingName(): string | undefined {
return product.defaultChatAgent?.completionsEnablementSetting;
}
/**
* Checks if completions (e.g., Copilot) are enabled for a given language ID
* using `IConfigurationService`.
*
* @param configurationService The configuration service to read settings from.
* @param modeId The language ID to check. Defaults to '*' which checks the global setting.
* @returns `true` if completions are enabled for the language, `false` otherwise.
*/
export function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean {
const settingName = getCompletionsEnablementSettingName();
if (!settingName) {
return false;
}
return isCompletionsEnabledFromObject(
configurationService.getValue<Record<string, boolean>>(settingName),
modeId
);
}
/**
* Checks if completions (e.g., Copilot) are enabled for a given language ID
* using `ITextResourceConfigurationService`.
*
* @param configurationService The text resource configuration service to read settings from.
* @param modeId The language ID to check. Defaults to '*' which checks the global setting.
* @returns `true` if completions are enabled for the language, `false` otherwise.
*/
export function isCompletionsEnabledWithTextResourceConfig(configurationService: ITextResourceConfigurationService, resource: URI, modeId: string = '*'): boolean {
const settingName = getCompletionsEnablementSettingName();
if (!settingName) {
return false;
}
// Pass undefined as resource to get the global setting
return isCompletionsEnabledFromObject(
configurationService.getValue<Record<string, boolean>>(resource, settingName),
modeId
);
}
/**
* Checks if completions are enabled for a given language ID using a pre-fetched
* completions enablement object.
*
* @param completionsEnablementObject The object containing per-language enablement settings.
* @param modeId The language ID to check. Defaults to '*' which checks the global setting.
* @returns `true` if completions are enabled for the language, `false` otherwise.
*/
export function isCompletionsEnabledFromObject(completionsEnablementObject: Record<string, boolean> | undefined, modeId: string = '*'): boolean {
if (!isObject(completionsEnablementObject)) {
return false; // default to disabled if setting is not available
}
if (typeof completionsEnablementObject[modeId] !== 'undefined') {
return Boolean(completionsEnablementObject[modeId]); // go with setting if explicitly defined
}
return Boolean(completionsEnablementObject['*']); // fallback to global setting otherwise
}
@@ -28,6 +28,7 @@ import { Command, InlineCompletionEndOfLifeReasonKind, InlineCompletionTriggerKi
import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js';
import { ITextModel } from '../../../../common/model.js';
import { offsetEditFromContentChanges } from '../../../../common/model/textModelStringEdit.js';
import { isCompletionsEnabledFromObject } from '../../../../common/services/completionsEnablement.js';
import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js';
import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js';
import { formatRecordableLogEntry, IRecordableEditorLogEntry, IRecordableLogEntry, StructuredLogger } from '../structuredLogger.js';
@@ -445,7 +446,7 @@ export class InlineCompletionsSource extends Disposable {
}
if (!isCompletionsEnabled(this._completionsEnabled, this._textModel.getLanguageId())) {
if (!isCompletionsEnabledFromObject(this._completionsEnabled, this._textModel.getLanguageId())) {
return;
}
@@ -571,18 +572,6 @@ function isSubset<T>(set1: Set<T>, set2: Set<T>): boolean {
return [...set1].every(item => set2.has(item));
}
function isCompletionsEnabled(completionsEnablementObject: Record<string, boolean> | undefined, modeId: string = '*'): boolean {
if (completionsEnablementObject === undefined) {
return false; // default to disabled if setting is not available
}
if (typeof completionsEnablementObject[modeId] !== 'undefined') {
return Boolean(completionsEnablementObject[modeId]); // go with setting if explicitly defined
}
return Boolean(completionsEnablementObject['*']); // fallback to global setting otherwise
}
class UpdateOperation implements IDisposable {
constructor(
public readonly request: UpdateRequest,
@@ -267,6 +267,8 @@ export async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(
options.serviceCollection.set(IDefaultAccountService, {
_serviceBrand: undefined,
onDidChangeDefaultAccount: Event.None,
onDidChangePolicyData: Event.None,
policyData: null,
getDefaultAccount: async () => null,
setDefaultAccountProvider: () => { },
getDefaultAccountAuthenticationProvider: () => { return { id: 'mockProvider', name: 'Mock Provider', enterprise: false }; },
@@ -100,7 +100,7 @@ import { IDataChannelService, NullDataChannelService } from '../../../platform/d
import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js';
import { StandaloneWebWorkerService } from './services/standaloneWebWorkerService.js';
import { IDefaultAccountService } from '../../../platform/defaultAccount/common/defaultAccount.js';
import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../base/common/defaultAccount.js';
import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IPolicyData } from '../../../base/common/defaultAccount.js';
class SimpleModel implements IResolvedTextEditorModel {
@@ -1115,6 +1115,8 @@ class StandaloneDefaultAccountService implements IDefaultAccountService {
declare readonly _serviceBrand: undefined;
readonly onDidChangeDefaultAccount: Event<IDefaultAccount | null> = Event.None;
readonly onDidChangePolicyData: Event<IPolicyData | null> = Event.None;
readonly policyData: IPolicyData | null = null;
async getDefaultAccount(): Promise<IDefaultAccount | null> {
return null;
@@ -5,11 +5,13 @@
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { Event } from '../../../base/common/event.js';
import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../base/common/defaultAccount.js';
import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IPolicyData } from '../../../base/common/defaultAccount.js';
export interface IDefaultAccountProvider {
readonly defaultAccount: IDefaultAccount | null;
readonly onDidChangeDefaultAccount: Event<IDefaultAccount | null>;
readonly policyData: IPolicyData | null;
readonly onDidChangePolicyData: Event<IPolicyData | null>;
getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider;
refresh(): Promise<IDefaultAccount | null>;
signIn(options?: { additionalScopes?: readonly string[];[key: string]: unknown }): Promise<IDefaultAccount | null>;
@@ -20,6 +22,8 @@ export const IDefaultAccountService = createDecorator<IDefaultAccountService>('d
export interface IDefaultAccountService {
readonly _serviceBrand: undefined;
readonly onDidChangeDefaultAccount: Event<IDefaultAccount | null>;
readonly onDidChangePolicyData: Event<IPolicyData | null>;
readonly policyData: IPolicyData | null;
getDefaultAccount(): Promise<IDefaultAccount | null>;
getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider;
setDefaultAccountProvider(provider: IDefaultAccountProvider): void;
@@ -235,6 +235,7 @@ export interface IExtensionContributions {
readonly chatPromptFiles?: ReadonlyArray<IChatFileContribution>;
readonly chatInstructions?: ReadonlyArray<IChatFileContribution>;
readonly chatAgents?: ReadonlyArray<IChatFileContribution>;
readonly chatSkills?: ReadonlyArray<IChatFileContribution>;
readonly languageModelTools?: ReadonlyArray<IToolContribution>;
readonly languageModelToolSets?: ReadonlyArray<IToolSetContribution>;
readonly mcpServerDefinitionProviders?: ReadonlyArray<IMcpCollectionContribution>;
+2 -2
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IStringDictionary } from '../../../base/common/collections.js';
import { IDefaultAccount } from '../../../base/common/defaultAccount.js';
import { IPolicyData } from '../../../base/common/defaultAccount.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Iterable } from '../../../base/common/iterator.js';
import { Disposable } from '../../../base/common/lifecycle.js';
@@ -14,7 +14,7 @@ import { createDecorator } from '../../instantiation/common/instantiation.js';
export type PolicyValue = string | number | boolean;
export type PolicyDefinition = {
type: 'string' | 'number' | 'boolean';
value?: (account: IDefaultAccount) => string | number | boolean | undefined;
value?: (policyData: IPolicyData) => string | number | boolean | undefined;
};
export const IPolicyService = createDecorator<IPolicyService>('policy');
@@ -20,11 +20,6 @@
border-top-left-radius: 5px;
}
.quick-input-widget.no-drag .quick-input-titlebar,
.quick-input-widget.no-drag .quick-input-title,
.quick-input-widget.no-drag .quick-input-header {
cursor: default;
}
.quick-input-widget .monaco-inputbox .monaco-action-bar {
top: 0;
}
@@ -37,8 +37,6 @@ 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 { AnchorAlignment, AnchorPosition, layout2d } from '../../../base/common/layout.js';
import { getAnchorRect } from '../../../base/browser/ui/contextview/contextview.js';
const $ = dom.$;
@@ -537,7 +535,6 @@ export class QuickInputController extends Disposable {
input.quickNavigate = options.quickNavigate;
input.hideInput = !!options.hideInput;
input.contextKey = options.contextKey;
input.anchor = options.anchor;
input.busy = true;
Promise.all([picks, options.activeItem])
.then(([items, _activeItem]) => {
@@ -707,7 +704,6 @@ export class QuickInputController extends Disposable {
ui.container.style.display = '';
this.updateLayout();
this.dndController?.setEnabled(!controller.anchor);
this.dndController?.layoutContainer();
ui.inputBox.setFocus();
this.quickInputTypeContext.set(controller.type);
@@ -859,52 +855,16 @@ export class QuickInputController extends Disposable {
private updateLayout() {
if (this.ui && this.isVisible()) {
const style = this.ui.container.style;
let width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH);
const width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH);
style.width = width + 'px';
let listHeight = this.dimension && this.dimension.height * 0.4;
// Position
if (this.controller?.anchor) {
const container = this.layoutService.getContainer(dom.getActiveWindow()).getBoundingClientRect();
const anchor = getAnchorRect(this.controller.anchor);
width = 380;
listHeight = this.dimension ? Math.min(this.dimension.height * 0.2, 200) : 200;
// Beware:
// We need to add some extra pixels to the height to account for the input and padding.
const containerHeight = Math.floor(listHeight) + 6 + 26 + 16;
const { top, left, right, bottom, anchorAlignment, anchorPosition } = layout2d(container, { width, height: containerHeight }, anchor);
if (anchorAlignment === AnchorAlignment.RIGHT) {
style.right = `${right}px`;
style.left = 'initial';
} else {
style.left = `${left}px`;
style.right = 'initial';
}
if (anchorPosition === AnchorPosition.BELOW) {
style.bottom = `${bottom}px`;
style.top = 'initial';
} else {
style.top = `${top}px`;
style.bottom = 'initial';
}
style.width = `${width}px`;
style.height = '';
} else {
style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`;
style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`;
style.right = '';
style.bottom = '';
style.height = '';
}
style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`;
style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`;
this.ui.inputBox.layout();
this.ui.list.layout(listHeight);
this.ui.tree.layout(listHeight);
this.ui.list.layout(this.dimension && this.dimension.height * 0.4);
this.ui.tree.layout(this.dimension && this.dimension.height * 0.4);
}
}
@@ -999,8 +959,6 @@ export interface IQuickInputControllerHost extends ILayoutService { }
class QuickInputDragAndDropController extends Disposable {
readonly dndViewState = observableValue<{ top?: number; left?: number; done: boolean } | undefined>(this, undefined);
private _enabled = true;
private readonly _snapThreshold = 20;
private readonly _snapLineHorizontalRatio = 0.25;
@@ -1036,10 +994,6 @@ class QuickInputDragAndDropController extends Disposable {
}
layoutContainer(dimension = this._layoutService.activeContainerDimension): void {
if (!this._enabled) {
return;
}
const state = this.dndViewState.get();
const dragAreaRect = this._quickInputContainer.getBoundingClientRect();
if (state?.top && state?.left) {
@@ -1051,11 +1005,6 @@ class QuickInputDragAndDropController extends Disposable {
}
}
setEnabled(enabled: boolean): void {
this._enabled = enabled;
this._quickInputContainer.classList.toggle('no-drag', !enabled);
}
setAlignment(alignment: 'top' | 'center' | { top: number; left: number }, done = true): void {
if (alignment === 'top') {
this.dndViewState.set({
@@ -1086,10 +1035,6 @@ class QuickInputDragAndDropController extends Disposable {
// Double click
this._register(dom.addDisposableGenericMouseUpListener(dragArea, (event: MouseEvent) => {
if (!this._enabled) {
return;
}
const originEvent = new StandardMouseEvent(dom.getWindow(dragArea), event);
if (originEvent.detail !== 2) {
return;
@@ -1106,10 +1051,6 @@ class QuickInputDragAndDropController extends Disposable {
// Mouse down
this._register(dom.addDisposableGenericMouseDownListener(dragArea, (e: MouseEvent) => {
if (!this._enabled) {
return;
}
const activeWindow = dom.getWindow(this._layoutService.activeContainer);
const originEvent = new StandardMouseEvent(activeWindow, e);
@@ -197,11 +197,6 @@ export interface IPickOptions<T extends IQuickPickItem> {
*/
activeItem?: Promise<T> | T;
/**
* an optional anchor for the picker
*/
anchor?: HTMLElement | { x: number; y: number };
onKeyMods?: (keyMods: IKeyMods) => void;
onDidFocus?: (entry: T) => void;
onDidTriggerItemButton?: (context: IQuickPickItemButtonContext<T>) => void;
@@ -358,11 +353,6 @@ export interface IQuickInput extends IDisposable {
*/
ignoreFocusOut: boolean;
/**
* An optional anchor for the quick input.
*/
anchor?: HTMLElement | { x: number; y: number };
/**
* Shows the quick input.
*/
@@ -547,7 +547,7 @@ export class BreadcrumbsControl {
const pickerArrowSize = 8;
let pickerArrowOffset: number;
const data = dom.getDomNodePagePosition(event.node.firstChild as HTMLElement);
const data = dom.getDomNodePagePosition(event.node);
const y = data.top + data.height + pickerArrowSize;
if (y + maxHeight >= window.innerHeight) {
maxHeight = window.innerHeight - y - 30 /* room for shadow and status bar*/;
@@ -491,7 +491,7 @@ export class OpenSessionTargetPickerAction extends Action2 {
tooltip: localize('setSessionTarget', "Set Session Target"),
category: CHAT_CATEGORY,
f1: false,
precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ChatContextKeys.chatSessionIsEmpty, ChatContextKeys.inAgentSessionsWelcome)),
precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ChatContextKeys.chatSessionIsEmpty, ChatContextKeys.inAgentSessionsWelcome), ChatContextKeys.currentlyEditingInput.negate(), ChatContextKeys.currentlyEditing.negate()),
menu: [
{
id: MenuId.ChatInput,
@@ -526,7 +526,7 @@ export class OpenDelegationPickerAction extends Action2 {
tooltip: localize('delegateSession', "Delegate Session"),
category: CHAT_CATEGORY,
f1: false,
precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.chatSessionIsEmpty.negate()),
precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.chatSessionIsEmpty.negate(), ChatContextKeys.currentlyEditingInput.negate(), ChatContextKeys.currentlyEditing.negate()),
menu: [
{
id: MenuId.ChatInput,
@@ -155,7 +155,7 @@ export class PickAgentSessionAction extends Action2 {
async run(accessor: ServicesAccessor): Promise<void> {
const instantiationService = accessor.get(IInstantiationService);
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, undefined);
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker);
await agentSessionsPicker.pickAgentSession();
}
}
@@ -67,7 +67,6 @@ export class AgentSessionsPicker {
private readonly sorter = new AgentSessionsSorter();
constructor(
private readonly anchor: HTMLElement | undefined,
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@@ -78,7 +77,6 @@ export class AgentSessionsPicker {
const disposables = new DisposableStore();
const picker = disposables.add(this.quickInputService.createQuickPick<ISessionPickItem>({ useSeparators: true }));
picker.anchor = this.anchor;
picker.items = this.createPickerItems();
picker.canAcceptInBackground = true;
picker.placeholder = localize('chatAgentPickerPlaceholder', "Search agent sessions by name");
@@ -320,7 +320,7 @@ configurationRegistry.registerConfiguration({
name: 'ChatToolsAutoApprove',
category: PolicyCategory.InteractiveSession,
minimumVersion: '1.99',
value: (account) => account.policyData?.chat_preview_features_enabled === false ? false : undefined,
value: (policyData) => policyData.chat_preview_features_enabled === false ? false : undefined,
localization: {
description: {
key: 'autoApprove2.description',
@@ -473,11 +473,11 @@ configurationRegistry.registerConfiguration({
name: 'ChatMCP',
category: PolicyCategory.InteractiveSession,
minimumVersion: '1.99',
value: (account) => {
if (account.policyData?.mcp === false) {
value: (policyData) => {
if (policyData.mcp === false) {
return McpAccessValue.None;
}
if (account.policyData?.mcpAccess === 'registry_only') {
if (policyData.mcpAccess === 'registry_only') {
return McpAccessValue.Registry;
}
return undefined;
@@ -588,7 +588,7 @@ configurationRegistry.registerConfiguration({
name: 'ChatAgentMode',
category: PolicyCategory.InteractiveSession,
minimumVersion: '1.99',
value: (account) => account.policyData?.chat_agent_enabled === false ? false : undefined,
value: (policyData) => policyData.chat_agent_enabled === false ? false : undefined,
localization: {
description: {
key: 'chat.agent.enabled.description',
@@ -665,7 +665,7 @@ configurationRegistry.registerConfiguration({
name: 'McpGalleryServiceUrl',
category: PolicyCategory.InteractiveSession,
minimumVersion: '1.101',
value: (account) => account.policyData?.mcpRegistryUrl,
value: (policyData) => policyData.mcpRegistryUrl,
localization: {
description: {
key: 'mcp.gallery.serviceUrl',
@@ -4,24 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { ChatEntitlement, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import product from '../../../../../platform/product/common/product.js';
import { isObject } from '../../../../../base/common/types.js';
export function isNewUser(chatEntitlementService: IChatEntitlementService): boolean {
return !chatEntitlementService.sentiment.installed || // chat not installed
chatEntitlementService.entitlement === ChatEntitlement.Available; // not yet signed up to chat
}
export function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean {
const result = configurationService.getValue<Record<string, boolean>>(product.defaultChatAgent.completionsEnablementSetting);
if (!isObject(result)) {
return false;
}
if (typeof result[modeId] !== 'undefined') {
return Boolean(result[modeId]); // go with setting if explicitly defined
}
return Boolean(result['*']); // fallback to global setting otherwise
}
@@ -40,13 +40,14 @@ import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/edi
import { IChatEntitlementService, ChatEntitlementService, ChatEntitlement, IQuotaSnapshot, getChatPlanName } from '../../../../services/chat/common/chatEntitlementService.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { IChatSessionsService } from '../../common/chatSessionsService.js';
import { isNewUser, isCompletionsEnabled } from './chatStatus.js';
import { isNewUser } from './chatStatus.js';
import { IChatStatusItemService, ChatStatusEntry } from './chatStatusItemService.js';
import product from '../../../../../platform/product/common/product.js';
import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js';
import { Color } from '../../../../../base/common/color.js';
import { IViewsService } from '../../../../services/views/common/viewsService.js';
import { ChatViewId } from '../chat.js';
import { isCompletionsEnabled } from '../../../../../editor/common/services/completionsEnablement.js';
const defaultChat = product.defaultChatAgent;
@@ -19,8 +19,9 @@ import { IChatSessionsService } from '../../common/chatSessionsService.js';
import { ChatStatusDashboard } from './chatStatusDashboard.js';
import { mainWindow } from '../../../../../base/browser/window.js';
import { disposableWindowInterval } from '../../../../../base/browser/dom.js';
import { isNewUser, isCompletionsEnabled } from './chatStatus.js';
import { isNewUser } from './chatStatus.js';
import product from '../../../../../platform/product/common/product.js';
import { isCompletionsEnabled } from '../../../../../editor/common/services/completionsEnablement.js';
export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution {
@@ -163,7 +163,7 @@ function getDefaultContentSnippet(promptType: PromptsType, name: string | undefi
`name: ${name ?? '${1:agent-name}'}`,
`description: \${2:Describe what this custom agent does and when to use it.}`,
`argument-hint: \${3:The inputs this agent expects, e.g., "a task to implement" or "a question to answer".}`,
`# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. if not set, all enabled tools are allowed`,
`# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. If not set, all enabled tools are allowed.`,
`---`,
`\${4:Define what this custom agent does, including its behavior, capabilities, and any specific instructions for its operation.}`,
].join('\n');
@@ -723,7 +723,7 @@ export class ChatMcpAppModel extends Disposable {
jsonrpc: '2.0',
id,
error: { code, message },
} satisfies MCP.JSONRPCError);
} satisfies MCP.JSONRPCErrorResponse);
}
private async _sendNotification(message: McpApps.HostNotification): Promise<void> {
@@ -771,7 +771,7 @@ have to be updated for changes to the rules above, or to support more deeply nes
box-sizing: border-box;
cursor: text;
background-color: var(--vscode-input-background);
border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-border, transparent));
border: 1px solid var(--vscode-input-border, transparent);
border-radius: 4px;
padding: 0 6px 6px 6px;
/* top padding is inside the editor widget */
@@ -140,46 +140,31 @@ export class ChatContextUsageWidget extends Disposable {
const store = new DisposableStore();
this._hoverDisposable.value = store;
const getOrCreateDetails = (): ChatContextUsageDetails => {
if (!this._contextUsageDetails.value) {
this._contextUsageDetails.value = this.instantiationService.createInstance(ChatContextUsageDetails);
}
if (this.currentData) {
this._contextUsageDetails.value.update(this.currentData);
const createDetails = (): ChatContextUsageDetails | undefined => {
if (!this._isVisible.get() || !this.currentData) {
return undefined;
}
this._contextUsageDetails.value = this.instantiationService.createInstance(ChatContextUsageDetails);
this._contextUsageDetails.value.update(this.currentData);
return this._contextUsageDetails.value;
};
const resolveHoverOptions = (): IDelayedHoverOptions => {
const details = getOrCreateDetails();
return {
content: details.domNode,
appearance: { showPointer: true, compact: true },
persistence: { hideOnHover: false },
trapFocus: true
};
const hoverOptions: Omit<IDelayedHoverOptions, 'content'> = {
appearance: { showPointer: true, compact: true },
persistence: { hideOnHover: false },
trapFocus: true
};
store.add(this.hoverService.setupDelayedHover(
this.domNode,
resolveHoverOptions
));
store.add(this.hoverService.setupDelayedHover(this.domNode, () => ({
...hoverOptions,
content: createDetails()?.domNode ?? ''
})));
// Helper to show sticky hover with focus
const showStickyHover = () => {
if (this.currentData) {
// Force hide any existing hover to ensure we can show our sticky one
this.hoverService.hideHover(true);
const details = getOrCreateDetails();
const details = createDetails();
if (details) {
this.hoverService.showInstantHover(
{
content: details.domNode,
target: this.domNode,
appearance: { showPointer: true, compact: true },
persistence: { hideOnHover: false, sticky: true },
trapFocus: true,
},
{ ...hoverOptions, content: details.domNode, target: this.domNode, persistence: { hideOnHover: false, sticky: true } },
true
);
}
@@ -59,8 +59,6 @@ export class ChatViewTitleControl extends Disposable {
}
private registerActions(): void {
const that = this;
this._register(registerAction2(class extends Action2 {
constructor() {
super({
@@ -78,7 +76,7 @@ export class ChatViewTitleControl extends Disposable {
async run(accessor: ServicesAccessor): Promise<void> {
const instantiationService = accessor.get(IInstantiationService);
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, that.titleLabel.value?.element);
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker);
await agentSessionsPicker.pickAgentSession();
}
}));
@@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { DisposableMap } from '../../../../../base/common/lifecycle.js';
import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js';
import { joinPath, isEqualOrParent } from '../../../../../base/common/resources.js';
import { localize } from '../../../../../nls.js';
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
import { ExtensionIdentifier, IExtensionManifest } from '../../../../../platform/extensions/common/extensions.js';
import { IWorkbenchContribution } from '../../../../common/contributions.js';
import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js';
import { IPromptsService, PromptsStorage } from './service/promptsService.js';
@@ -15,6 +15,9 @@ import { PromptsType } from './promptTypes.js';
import { UriComponents } from '../../../../../base/common/uri.js';
import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
import { Registry } from '../../../../../platform/registry/common/platform.js';
import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js';
interface IRawChatFileContribution {
readonly path: string;
@@ -162,3 +165,80 @@ CommandsRegistry.registerCommand('_listExtensionPromptFiles', async (accessor):
return result;
});
class ChatPromptFilesDataRenderer extends Disposable implements IExtensionFeatureTableRenderer {
readonly type = 'table';
constructor(private readonly contributionPoint: ChatContributionPoint) {
super();
}
shouldRender(manifest: IExtensionManifest): boolean {
return !!manifest.contributes?.[this.contributionPoint];
}
render(manifest: IExtensionManifest): IRenderedData<ITableData> {
const contributions = manifest.contributes?.[this.contributionPoint] ?? [];
if (!contributions.length) {
return { data: { headers: [], rows: [] }, dispose: () => { } };
}
const headers = [
localize('chatFilesName', "Name"),
localize('chatFilesDescription', "Description"),
localize('chatFilesPath', "Path"),
];
const rows: IRowData[][] = contributions.map(d => {
return [
d.name ?? '-',
d.description ?? '-',
d.path,
];
});
return {
data: {
headers,
rows
},
dispose: () => { }
};
}
}
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
id: ChatContributionPoint.chatPromptFiles,
label: localize('chatPromptFiles', "Chat Prompt Files"),
access: {
canToggle: false
},
renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatPromptFiles]),
});
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
id: ChatContributionPoint.chatInstructions,
label: localize('chatInstructions', "Chat Instructions"),
access: {
canToggle: false
},
renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatInstructions]),
});
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
id: ChatContributionPoint.chatAgents,
label: localize('chatAgents', "Chat Agents"),
access: {
canToggle: false
},
renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatAgents]),
});
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
id: ChatContributionPoint.chatSkills,
label: localize('chatSkills', "Chat Skills"),
access: {
canToggle: false
},
renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatSkills]),
});
@@ -52,7 +52,7 @@ export class InlineChatAffordance extends Disposable {
this._store.add(autorun(r => {
const value = debouncedSelection.read(r);
if (!value || value.isEmpty() || !explicitSelection) {
if (!value || value.isEmpty() || !explicitSelection || _editor.getModel()?.getValueInRange(value).match(/^\s+$/)) {
selectionData.set(undefined, undefined);
return;
}
@@ -133,6 +133,11 @@ export class McpServerRequestHandler extends Disposable {
elicitation: opts.elicitationRequestHandler ? { create: {} } : undefined,
},
},
extensions: {
'io.modelcontextprotocol/ui': {
mimeTypes: ['text/html;profile=mcp-app']
}
}
},
clientInfo: {
name: productService.nameLong,
@@ -321,22 +326,26 @@ export class McpServerRequestHandler extends Disposable {
/**
* Handle successful responses
*/
private handleResult(response: MCP.JSONRPCResponse): void {
const request = this._pendingRequests.get(response.id);
if (request) {
this._pendingRequests.delete(response.id);
request.promise.complete(response.result);
private handleResult(response: MCP.JSONRPCResultResponse): void {
if (response.id !== undefined) {
const request = this._pendingRequests.get(response.id);
if (request) {
this._pendingRequests.delete(response.id);
request.promise.complete(response.result);
}
}
}
/**
* Handle error responses
*/
private handleError(response: MCP.JSONRPCError): void {
const request = this._pendingRequests.get(response.id);
if (request) {
this._pendingRequests.delete(response.id);
request.promise.error(new MpcResponseError(response.error.message, response.error.code, response.error.data));
private handleError(response: MCP.JSONRPCErrorResponse): void {
if (response.id !== undefined) {
const request = this._pendingRequests.get(response.id);
if (request) {
this._pendingRequests.delete(response.id);
request.promise.error(new MpcResponseError(response.error.message, response.error.code, response.error.data));
}
}
}
@@ -394,7 +403,7 @@ export class McpServerRequestHandler extends Disposable {
e = McpError.unknown(e);
}
const errorResponse: MCP.JSONRPCError = {
const errorResponse: MCP.JSONRPCErrorResponse = {
jsonrpc: MCP.JSONRPC_VERSION,
id: request.id,
error: {
@@ -89,6 +89,7 @@ export class McpTaskManager extends Disposable {
status: 'working',
createdAt,
ttl,
lastUpdatedAt: new Date().toISOString(),
pollInterval: 1000, // Suggest 1 second polling interval
};
@@ -171,6 +172,8 @@ export class McpTaskManager extends Disposable {
}
entry.task.status = status;
entry.task.lastUpdatedAt = new Date().toISOString();
if (statusMessage !== undefined) {
entry.task.statusMessage = statusMessage;
}
File diff suppressed because it is too large Load Diff
@@ -220,7 +220,7 @@ suite('Workbench - MCP - ServerRequestHandler', () => {
const sentMessages = transport.getSentMessages();
const pingResponse = sentMessages.find(m =>
'id' in m && m.id === pingRequest.id && 'result' in m
) as MCP.JSONRPCResponse;
) as MCP.JSONRPCResultResponse;
assert.ok(pingResponse, 'No ping response was sent');
assert.deepStrictEqual(pingResponse.result, {});
@@ -246,7 +246,7 @@ suite('Workbench - MCP - ServerRequestHandler', () => {
const sentMessages = transport.getSentMessages();
const rootsResponse = sentMessages.find(m =>
'id' in m && m.id === rootsRequest.id && 'result' in m
) as MCP.JSONRPCResponse;
) as MCP.JSONRPCResultResponse;
assert.ok(rootsResponse, 'No roots/list response was sent');
assert.strictEqual((rootsResponse.result as MCP.ListRootsResult).roots.length, 2);
@@ -400,6 +400,7 @@ suite.skip('Workbench - MCP - McpTask', () => { // TODO@connor4312 https://githu
taskId: 'task1',
status: 'working',
createdAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(),
ttl: null,
...overrides
};
@@ -209,6 +209,7 @@ export class AgentSessionsWelcomePage extends EditorPane {
clearNode(sessionsSection);
this.buildSessionsOrPrompts(sessionsSection);
}
this.layoutSessionsControl();
}));
this.scrollableElement?.scanDomNode();
@@ -163,34 +163,34 @@
/*
* Transform items into 2-column layout:
* - Items 0,1 form visual row 1 (top: 0)
* - Items 2,3 form visual row 2 (top: 52)
* - Items 4,5 form visual row 3 (top: 104)
* Left column (even): items stay in place or move up
* Right column (odd): items move right and up
* - Odd items (1, 3, 5, ...) stay in left column
* - Even items (2, 4, 6, ...) move to right column
* 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.
*/
/* Item 1 (index 1): move to right column of row 1 */
.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(2) {
transform: translateX(100%) translateY(-52px);
}
/* Item 2 (index 2): move up to row 2 left column */
/* 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);
}
/* Item 3 (index 3): move to right column of row 2 */
.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(4) {
transform: translateX(100%) translateY(-104px);
}
/* Item 4 (index 4): move up to row 3 left column */
/* Item 5: move up 2 rows */
.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(5) {
transform: translateY(-104px);
}
/* Item 5 (index 5): move to right column of row 3 */
/* 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);
}
/* Item 4: move right, up 2 rows */
.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(4) {
transform: translateX(100%) translateY(-104px);
}
/* Item 6: move right, up 3 rows */
.agentSessionsWelcome-sessionsGrid .monaco-list-row:nth-child(6) {
transform: translateX(100%) translateY(-156px);
}
@@ -111,12 +111,16 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount
declare _serviceBrand: undefined;
private defaultAccount: IDefaultAccount | null = null;
get policyData(): IPolicyData | null { return this.defaultAccountProvider?.policyData ?? null; }
private readonly initBarrier = new Barrier();
private readonly _onDidChangeDefaultAccount = this._register(new Emitter<IDefaultAccount | null>());
readonly onDidChangeDefaultAccount = this._onDidChangeDefaultAccount.event;
private readonly _onDidChangePolicyData = this._register(new Emitter<IPolicyData | null>());
readonly onDidChangePolicyData = this._onDidChangePolicyData.event;
private readonly defaultAccountConfig: IDefaultAccountConfig;
private defaultAccountProvider: IDefaultAccountProvider | null = null;
@@ -148,11 +152,15 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount
}
this.defaultAccountProvider = provider;
if (this.defaultAccountProvider.policyData) {
this._onDidChangePolicyData.fire(this.defaultAccountProvider.policyData);
}
provider.refresh().then(account => {
this.defaultAccount = account;
}).finally(() => {
this.initBarrier.open();
this._register(provider.onDidChangeDefaultAccount(account => this.setDefaultAccount(account)));
this._register(provider.onDidChangePolicyData(policyData => this._onDidChangePolicyData.fire(policyData)));
});
}
@@ -178,6 +186,16 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount
}
}
interface IAccountPolicyData {
readonly accountId: string;
readonly policyData: IPolicyData;
}
interface IDefaultAccountData {
defaultAccount: IDefaultAccount;
policyData: IAccountPolicyData | null;
}
type DefaultAccountStatusTelemetry = {
status: string;
initial: boolean;
@@ -192,12 +210,18 @@ type DefaultAccountStatusTelemetryClassification = {
class DefaultAccountProvider extends Disposable implements IDefaultAccountProvider {
private _defaultAccount: IDefaultAccount | null = null;
get defaultAccount(): IDefaultAccount | null { return this._defaultAccount ?? null; }
private _defaultAccount: IDefaultAccountData | null = null;
get defaultAccount(): IDefaultAccount | null { return this._defaultAccount?.defaultAccount ?? null; }
private _policyData: IAccountPolicyData | null = null;
get policyData(): IPolicyData | null { return this._policyData?.policyData ?? null; }
private readonly _onDidChangeDefaultAccount = this._register(new Emitter<IDefaultAccount | null>());
readonly onDidChangeDefaultAccount = this._onDidChangeDefaultAccount.event;
private readonly _onDidChangePolicyData = this._register(new Emitter<IPolicyData | null>());
readonly onDidChangePolicyData = this._onDidChangePolicyData.event;
private readonly accountStatusContext: IContextKey<string>;
private initialized = false;
private readonly initPromise: Promise<void>;
@@ -220,6 +244,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
) {
super();
this.accountStatusContext = CONTEXT_DEFAULT_ACCOUNT_STATE.bindTo(contextKeyService);
this._policyData = this.getCachedPolicyData();
this.initPromise = this.init()
.finally(() => {
this.telemetryService.publicLog2<DefaultAccountStatusTelemetry, DefaultAccountStatusTelemetryClassification>('defaultaccount:status', { status: this.defaultAccount ? 'available' : 'unavailable', initial: true });
@@ -227,6 +252,22 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
});
}
private getCachedPolicyData(): IAccountPolicyData | null {
const cached = this.storageService.get(CACHED_POLICY_DATA_KEY, StorageScope.APPLICATION);
if (cached) {
try {
const { accountId, policyData } = JSON.parse(cached);
if (accountId && policyData) {
this.logService.debug('[DefaultAccount] Initializing with cached policy data');
return { accountId, policyData };
}
} catch (error) {
this.logService.error('[DefaultAccount] Failed to parse cached policy data', getErrorMessage(error));
}
}
return null;
}
private async init(): Promise<void> {
if (isWeb && !this.environmentService.remoteAuthority) {
this.logService.debug('[DefaultAccount] Running in web without remote, skipping initialization');
@@ -323,7 +364,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
}
}
private async fetchDefaultAccount(): Promise<IDefaultAccount | null> {
private async fetchDefaultAccount(): Promise<IDefaultAccountData | null> {
const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider();
this.logService.debug('[DefaultAccount] Default account provider ID:', defaultAccountProvider.id);
@@ -336,24 +377,47 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
return await this.getDefaultAccountForAuthenticationProvider(defaultAccountProvider);
}
private setDefaultAccount(account: IDefaultAccount | null): void {
private setDefaultAccount(account: IDefaultAccountData | null): void {
if (equals(this._defaultAccount, account)) {
return;
}
this.logService.trace('[DefaultAccount] Updating default account:', account);
this._defaultAccount = account;
this._onDidChangeDefaultAccount.fire(this._defaultAccount);
if (this._defaultAccount) {
if (account) {
this._defaultAccount = account;
this.setPolicyData(account.policyData);
this._onDidChangeDefaultAccount.fire(this._defaultAccount.defaultAccount);
this.accountStatusContext.set(DefaultAccountStatus.Available);
this.logService.debug('[DefaultAccount] Account status set to Available');
} else {
this._defaultAccount = null;
this.setPolicyData(null);
this._onDidChangeDefaultAccount.fire(null);
this.accountDataPollScheduler.cancel();
this.accountStatusContext.set(DefaultAccountStatus.Unavailable);
this.logService.debug('[DefaultAccount] Account status set to Unavailable');
}
}
private setPolicyData(accountPolicyData: IAccountPolicyData | null): void {
if (equals(this._policyData, accountPolicyData)) {
return;
}
this._policyData = accountPolicyData;
this.cachePolicyData(accountPolicyData);
this._onDidChangePolicyData.fire(this._policyData?.policyData ?? null);
}
private cachePolicyData(accountPolicyData: IAccountPolicyData | null): void {
if (accountPolicyData) {
this.logService.debug('[DefaultAccount] Caching policy data for account:', accountPolicyData.accountId);
this.storageService.store(CACHED_POLICY_DATA_KEY, JSON.stringify(accountPolicyData), StorageScope.APPLICATION, StorageTarget.MACHINE);
} else {
this.logService.debug('[DefaultAccount] Removing cached policy data');
this.storageService.remove(CACHED_POLICY_DATA_KEY, StorageScope.APPLICATION);
}
}
private scheduleAccountDataPoll(): void {
if (!this._defaultAccount) {
return;
@@ -373,7 +437,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
return result;
}
private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider): Promise<IDefaultAccount | null> {
private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider): Promise<IDefaultAccountData | null> {
try {
this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authenticationProvider.id);
const sessions = await this.findMatchingProviderSession(authenticationProvider.id, this.defaultAccountConfig.authenticationProvider.scopes);
@@ -390,7 +454,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
}
}
private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[]): Promise<IDefaultAccount | null> {
private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[]): Promise<IDefaultAccountData | null> {
try {
const accountId = sessions[0].account.id;
const [entitlementsData, tokenEntitlementsData] = await Promise.all([
@@ -398,7 +462,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
this.getTokenEntitlements(sessions),
]);
let policyData = this.getCachedPolicyData(accountId);
let policyData: Mutable<IPolicyData> | undefined = this._policyData?.accountId === accountId ? { ...this._policyData.policyData } : undefined;
if (tokenEntitlementsData) {
policyData = policyData ?? {};
policyData.chat_agent_enabled = tokenEntitlementsData.chat_agent_enabled;
@@ -411,18 +475,16 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
policyData.mcpAccess = mcpRegistryProvider.registry_access;
}
}
this.cachePolicyData(accountId, policyData);
}
const account: IDefaultAccount = {
const defaultAccount: IDefaultAccount = {
authenticationProvider,
sessionId: sessions[0].id,
enterprise: authenticationProvider.enterprise || sessions[0].account.label.includes('_'),
entitlementsData,
policyData,
};
this.logService.debug('[DefaultAccount] Successfully created default account for provider:', authenticationProvider.id);
return account;
return { defaultAccount, policyData: policyData ? { accountId, policyData } : null };
} catch (error) {
this.logService.error('[DefaultAccount] Failed to create default account for provider:', authenticationProvider.id, getErrorMessage(error));
return null;
@@ -515,28 +577,6 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
return undefined;
}
private cachePolicyData(accountId: string, policyData: IPolicyData): void {
this.logService.debug('[DefaultAccount] Caching policy data for account:', accountId);
this.storageService.store(CACHED_POLICY_DATA_KEY, JSON.stringify({ accountId, policyData }), StorageScope.APPLICATION, StorageTarget.MACHINE);
}
private getCachedPolicyData(accountId: string): Mutable<IPolicyData> | undefined {
const cached = this.storageService.get(CACHED_POLICY_DATA_KEY, StorageScope.APPLICATION);
if (cached) {
try {
const { accountId: cachedAccountId, policyData } = JSON.parse(cached);
if (cachedAccountId === accountId) {
this.logService.debug('[DefaultAccount] Using cached policy data for account:', accountId);
return policyData;
}
this.logService.debug('[DefaultAccount] Cached policy data is for different account, ignoring');
} catch (error) {
this.logService.error('[DefaultAccount] Failed to parse cached policy data', getErrorMessage(error));
}
}
return undefined;
}
private async getEntitlements(sessions: AuthenticationSession[]): Promise<IEntitlementsData | undefined | null> {
const entitlementUrl = this.getEntitlementUrl();
if (!entitlementUrl) {
@@ -744,7 +784,6 @@ class DefaultAccountProviderContribution extends Disposable implements IWorkbenc
@IProductService productService: IProductService,
@IInstantiationService instantiationService: IInstantiationService,
@IDefaultAccountService defaultAccountService: IDefaultAccountService,
@ILogService logService: ILogService,
) {
super();
const defaultAccountProvider = this._register(instantiationService.createInstance(DefaultAccountProvider, toDefaultAccountConfig(productService.defaultChatAgent)));
@@ -752,4 +791,4 @@ class DefaultAccountProviderContribution extends Disposable implements IWorkbenc
}
}
registerWorkbenchContribution2(DefaultAccountProviderContribution.ID, DefaultAccountProviderContribution, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(DefaultAccountProviderContribution.ID, DefaultAccountProviderContribution, WorkbenchPhase.BlockStartup);
@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { IStringDictionary } from '../../../../base/common/collections.js';
import { IDefaultAccount } from '../../../../base/common/defaultAccount.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { AbstractPolicyService, IPolicyService, PolicyDefinition } from '../../../../platform/policy/common/policy.js';
import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';
@@ -12,32 +11,26 @@ import { IDefaultAccountService } from '../../../../platform/defaultAccount/comm
export class AccountPolicyService extends AbstractPolicyService implements IPolicyService {
private account: IDefaultAccount | null = null;
constructor(
@ILogService private readonly logService: ILogService,
@IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService
) {
super();
this.defaultAccountService.getDefaultAccount()
.then(account => {
this.account = account;
this._updatePolicyDefinitions(this.policyDefinitions);
this._register(this.defaultAccountService.onDidChangeDefaultAccount(account => {
this.account = account;
this._updatePolicyDefinitions(this.policyDefinitions);
}));
});
this._updatePolicyDefinitions(this.policyDefinitions);
this._register(this.defaultAccountService.onDidChangePolicyData(() => {
this._updatePolicyDefinitions(this.policyDefinitions);
}));
}
protected async _updatePolicyDefinitions(policyDefinitions: IStringDictionary<PolicyDefinition>): Promise<void> {
this.logService.trace(`AccountPolicyService#_updatePolicyDefinitions: Got ${Object.keys(policyDefinitions).length} policy definitions`);
const updated: string[] = [];
const policyData = this.defaultAccountService.policyData;
for (const key in policyDefinitions) {
const policy = policyDefinitions[key];
const policyValue = this.account && policy.value ? policy.value(this.account) : undefined;
const policyValue = policyData && policy.value ? policy.value(policyData) : undefined;
if (policyValue !== undefined) {
if (this.policies.get(key) !== policyValue) {
this.policies.set(key, policyValue);
@@ -13,7 +13,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/tes
import { Registry } from '../../../../../platform/registry/common/platform.js';
import { Extensions, IConfigurationNode, IConfigurationRegistry } from '../../../../../platform/configuration/common/configurationRegistry.js';
import { DefaultConfiguration, PolicyConfiguration } from '../../../../../platform/configuration/common/configurations.js';
import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../../../base/common/defaultAccount.js';
import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IPolicyData } from '../../../../../base/common/defaultAccount.js';
import { PolicyCategory } from '../../../../../base/common/policy.js';
import { TestProductService } from '../../../../test/common/workbenchTestServices.js';
@@ -30,9 +30,11 @@ const BASE_DEFAULT_ACCOUNT: IDefaultAccount = {
class DefaultAccountProvider implements IDefaultAccountProvider {
readonly onDidChangeDefaultAccount = Event.None;
readonly onDidChangePolicyData = Event.None;
constructor(
readonly defaultAccount: IDefaultAccount,
readonly policyData: IPolicyData = {},
) { }
getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider {
@@ -81,7 +83,7 @@ suite('AccountPolicyService', () => {
category: PolicyCategory.Extensions,
minimumVersion: '1.0.0',
localization: { description: { key: '', value: '' } },
value: account => account.policyData?.chat_preview_features_enabled === false ? 'policyValueB' : undefined,
value: policyData => policyData.chat_preview_features_enabled === false ? 'policyValueB' : undefined,
}
},
'setting.C': {
@@ -92,7 +94,7 @@ suite('AccountPolicyService', () => {
category: PolicyCategory.Extensions,
minimumVersion: '1.0.0',
localization: { description: { key: '', value: '' } },
value: account => account.policyData?.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined,
value: policyData => policyData.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined,
}
},
'setting.D': {
@@ -103,7 +105,7 @@ suite('AccountPolicyService', () => {
category: PolicyCategory.Extensions,
minimumVersion: '1.0.0',
localization: { description: { key: '', value: '' } },
value: account => account.policyData?.chat_preview_features_enabled === false ? false : undefined,
value: policyData => policyData.chat_preview_features_enabled === false ? false : undefined,
}
},
'setting.E': {
@@ -127,8 +129,8 @@ suite('AccountPolicyService', () => {
});
async function assertDefaultBehavior(defaultAccount: IDefaultAccount) {
defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount));
async function assertDefaultBehavior(policyData: IPolicyData | undefined) {
defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(BASE_DEFAULT_ACCOUNT, policyData));
await defaultAccountService.refresh();
await policyConfiguration.initialize();
@@ -159,18 +161,16 @@ suite('AccountPolicyService', () => {
test('should initialize with default account', async () => {
const defaultAccount = { ...BASE_DEFAULT_ACCOUNT };
await assertDefaultBehavior(defaultAccount);
await assertDefaultBehavior(undefined);
});
test('should initialize with default account and preview features enabled', async () => {
const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: true } };
await assertDefaultBehavior(defaultAccount);
await assertDefaultBehavior({ chat_preview_features_enabled: true });
});
test('should initialize with default account and preview features disabled', async () => {
const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: false } };
defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount));
const policyData: IPolicyData = { chat_preview_features_enabled: false };
defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(BASE_DEFAULT_ACCOUNT, policyData));
await defaultAccountService.refresh();
await policyConfiguration.initialize();
@@ -20,7 +20,7 @@ import { IFileService } from '../../../../../platform/files/common/files.js';
import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js';
import { FileService } from '../../../../../platform/files/common/fileService.js';
import { VSBuffer } from '../../../../../base/common/buffer.js';
import { IDefaultAccount, IDefaultAccountAuthenticationProvider } from '../../../../../base/common/defaultAccount.js';
import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IPolicyData } from '../../../../../base/common/defaultAccount.js';
import { PolicyCategory } from '../../../../../base/common/policy.js';
import { TestProductService } from '../../../../test/common/workbenchTestServices.js';
@@ -37,9 +37,11 @@ const BASE_DEFAULT_ACCOUNT: IDefaultAccount = {
class DefaultAccountProvider implements IDefaultAccountProvider {
readonly onDidChangeDefaultAccount = Event.None;
readonly onDidChangePolicyData = Event.None;
constructor(
readonly defaultAccount: IDefaultAccount,
readonly policyData: IPolicyData = {},
) { }
getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider {
@@ -90,7 +92,7 @@ suite('MultiplexPolicyService', () => {
category: PolicyCategory.Extensions,
minimumVersion: '1.0.0',
localization: { description: { key: '', value: '' } },
value: account => account.policyData?.chat_preview_features_enabled === false ? 'policyValueB' : undefined,
value: policyData => policyData.chat_preview_features_enabled === false ? 'policyValueB' : undefined,
}
},
'setting.C': {
@@ -101,7 +103,7 @@ suite('MultiplexPolicyService', () => {
category: PolicyCategory.Extensions,
minimumVersion: '1.0.0',
localization: { description: { key: '', value: '' } },
value: account => account.policyData?.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined,
value: policyData => policyData.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined,
}
},
'setting.D': {
@@ -112,7 +114,7 @@ suite('MultiplexPolicyService', () => {
category: PolicyCategory.Extensions,
minimumVersion: '1.0.0',
localization: { description: { key: '', value: '' } },
value: account => account.policyData?.chat_preview_features_enabled === false ? false : undefined,
value: policyData => policyData.chat_preview_features_enabled === false ? false : undefined,
}
},
'setting.E': {
@@ -186,8 +188,7 @@ suite('MultiplexPolicyService', () => {
test('policy from file only', async () => {
await clear();
const defaultAccount = { ...BASE_DEFAULT_ACCOUNT };
defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount));
defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(BASE_DEFAULT_ACCOUNT));
await defaultAccountService.refresh();
await fileService.writeFile(policyFile,
@@ -228,8 +229,8 @@ suite('MultiplexPolicyService', () => {
test('policy from default account only', async () => {
await clear();
const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: false } };
defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount));
const policyData: IPolicyData = { chat_preview_features_enabled: false };
defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(BASE_DEFAULT_ACCOUNT, policyData));
await defaultAccountService.refresh();
await fileService.writeFile(policyFile,
@@ -269,8 +270,8 @@ suite('MultiplexPolicyService', () => {
test('policy from file and default account', async () => {
await clear();
const defaultAccount = { ...BASE_DEFAULT_ACCOUNT, policyData: { chat_preview_features_enabled: false } };
defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(defaultAccount));
const policyData: IPolicyData = { chat_preview_features_enabled: false };
defaultAccountService.setDefaultAccountProvider(new DefaultAccountProvider(BASE_DEFAULT_ACCOUNT, policyData));
await defaultAccountService.refresh();
await fileService.writeFile(policyFile,