diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index b520069a389be..1b0af58037816 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc diff --git a/.github/workflows/monaco-editor.yml b/.github/workflows/monaco-editor.yml index cedbd379a3d54..b1d462546ac93 100644 --- a/.github/workflows/monaco-editor.yml +++ b/.github/workflows/monaco-editor.yml @@ -23,7 +23,7 @@ jobs: with: persist-credentials: false - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version-file: .nvmrc diff --git a/.github/workflows/pr-darwin-test.yml b/.github/workflows/pr-darwin-test.yml index afcb9cc5bb4a4..ff671ee251c88 100644 --- a/.github/workflows/pr-darwin-test.yml +++ b/.github/workflows/pr-darwin-test.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml index ed8ff3a40ba7b..7f892be30f20d 100644 --- a/.github/workflows/pr-linux-test.yml +++ b/.github/workflows/pr-linux-test.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index 65c57f078f967..fc7497aa3f6b2 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc @@ -95,7 +95,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc @@ -167,7 +167,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc @@ -228,7 +228,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index 0a1ec3aa45b5a..b2427d0fad32b 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2361aaa31de37..26ad219d114fd 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc diff --git a/.github/workflows/telemetry.yml b/.github/workflows/telemetry.yml index 03c3f649d5847..cb7d81e551f08 100644 --- a/.github/workflows/telemetry.yml +++ b/.github/workflows/telemetry.yml @@ -11,7 +11,7 @@ jobs: with: persist-credentials: false - - uses: 'actions/setup-node@v5' + - uses: 'actions/setup-node@v6' with: node-version: 'lts/*' diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index 28f66a0f10418..18ae50d3af6a7 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -63,7 +63,7 @@ suite('vscode API - debug', function () { assert.strictEqual(functionBreakpoint.functionName, 'func'); }); - test('start debugging', async function () { + test.skip('start debugging', async function () { // Flaky: https://github.com/microsoft/vscode/issues/242033 let stoppedEvents = 0; let variablesReceived: () => void; let initializedReceived: () => void; diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 5e4cd533c9a4c..7f44d84b8c43f 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -191,6 +191,10 @@ export interface ITask { (): T; } +export interface ICancellableTask { + (token: CancellationToken): T; +} + /** * A helper to prevent accumulation of sequential async tasks. * @@ -221,18 +225,19 @@ export class Throttler implements IDisposable { private activePromise: Promise | null; private queuedPromise: Promise | null; - private queuedPromiseFactory: ITask> | null; - - private isDisposed = false; + private queuedPromiseFactory: ICancellableTask> | null; + private cancellationTokenSource: CancellationTokenSource; constructor() { this.activePromise = null; this.queuedPromise = null; this.queuedPromiseFactory = null; + + this.cancellationTokenSource = new CancellationTokenSource(); } - queue(promiseFactory: ITask>): Promise { - if (this.isDisposed) { + queue(promiseFactory: ICancellableTask>): Promise { + if (this.cancellationTokenSource.token.isCancellationRequested) { return Promise.reject(new Error('Throttler is disposed')); } @@ -243,7 +248,7 @@ export class Throttler implements IDisposable { const onComplete = () => { this.queuedPromise = null; - if (this.isDisposed) { + if (this.cancellationTokenSource.token.isCancellationRequested) { return; } @@ -263,7 +268,7 @@ export class Throttler implements IDisposable { }); } - this.activePromise = promiseFactory(); + this.activePromise = promiseFactory(this.cancellationTokenSource.token); return new Promise((resolve, reject) => { this.activePromise!.then((result: T) => { @@ -277,7 +282,7 @@ export class Throttler implements IDisposable { } dispose(): void { - this.isDisposed = true; + this.cancellationTokenSource.cancel(); } } @@ -458,7 +463,7 @@ export class ThrottledDelayer { this.throttler = new Throttler(); } - trigger(promiseFactory: ITask>, delay?: number): Promise { + trigger(promiseFactory: ICancellableTask>, delay?: number): Promise { return this.delayer.trigger(() => this.throttler.queue(promiseFactory), delay) as unknown as Promise; } diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 5d10e6e9f8ae3..b417161930f12 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -14,7 +14,7 @@ import { EditorOption } from '../../../../common/config/editorOptions.js'; import { EndOfLinePreference, IModelDeltaDecoration } from '../../../../common/model.js'; import { ViewConfigurationChangedEvent, ViewCursorStateChangedEvent, ViewDecorationsChangedEvent, ViewFlushedEvent, ViewLinesChangedEvent, ViewLinesDeletedEvent, ViewLinesInsertedEvent, ViewScrollChangedEvent, ViewZonesChangedEvent } from '../../../../common/viewEvents.js'; import { ViewContext } from '../../../../common/viewModel/viewContext.js'; -import { RestrictedRenderingContext, RenderingContext } from '../../../view/renderingContext.js'; +import { RestrictedRenderingContext, RenderingContext, HorizontalPosition } from '../../../view/renderingContext.js'; import { ViewController } from '../../../view/viewController.js'; import { ClipboardEventUtils, ClipboardStoredMetadata, getDataToCopy, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; import { AbstractEditContext } from '../editContext.js'; @@ -60,7 +60,7 @@ export class NativeEditContext extends AbstractEditContext { private _editContextPrimarySelection: Selection = new Selection(1, 1, 1, 1); // Overflow guard container - private _parent: HTMLElement | undefined; + private readonly _parent: HTMLElement; private _decorations: string[] = []; private _primarySelection: Selection = new Selection(1, 1, 1, 1); @@ -241,9 +241,13 @@ export class NativeEditContext extends AbstractEditContext { return this._primarySelection.getPosition(); } - public prepareRender(ctx: RenderingContext): void { + public override prepareRender(ctx: RenderingContext): void { this._screenReaderSupport.prepareRender(ctx); - this._updateSelectionAndControlBounds(ctx); + this._updateSelectionAndControlBoundsData(ctx); + } + + public override onDidRender(): void { + this._updateSelectionAndControlBoundsAfterRender(); } public render(ctx: RestrictedRenderingContext): void { @@ -483,26 +487,35 @@ export class NativeEditContext extends AbstractEditContext { this._decorations = this._context.viewModel.model.deltaDecorations(this._decorations, decorations); } - private _updateSelectionAndControlBounds(ctx: RenderingContext) { - if (!this._parent) { - return; + private _linesVisibleRanges: HorizontalPosition | null = null; + private _updateSelectionAndControlBoundsData(ctx: RenderingContext): void { + const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection); + if (this._primarySelection.isEmpty()) { + const linesVisibleRanges = ctx.visibleRangeForPosition(viewSelection.getStartPosition()); + this._linesVisibleRanges = linesVisibleRanges; + } else { + this._linesVisibleRanges = null; } + } + + private _updateSelectionAndControlBoundsAfterRender() { const options = this._context.configuration.options; const contentLeft = options.get(EditorOption.layoutInfo).contentLeft; - const parentBounds = this._parent.getBoundingClientRect(); + const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection); const verticalOffsetStart = this._context.viewLayout.getVerticalOffsetForLineNumber(viewSelection.startLineNumber); + const verticalOffsetEnd = this._context.viewLayout.getVerticalOffsetAfterLineNumber(viewSelection.endLineNumber); + // Make sure this doesn't force an extra layout (i.e. don't call it before rendering finished) + const parentBounds = this._parent.getBoundingClientRect(); const top = parentBounds.top + verticalOffsetStart - this._scrollTop; - const verticalOffsetEnd = this._context.viewLayout.getVerticalOffsetAfterLineNumber(viewSelection.endLineNumber); const height = verticalOffsetEnd - verticalOffsetStart; let left = parentBounds.left + contentLeft - this._scrollLeft; let width: number; if (this._primarySelection.isEmpty()) { - const linesVisibleRanges = ctx.visibleRangeForPosition(viewSelection.getStartPosition()); - if (linesVisibleRanges) { - left += linesVisibleRanges.left; + if (this._linesVisibleRanges) { + left += this._linesVisibleRanges.left; } width = 0; } else { @@ -515,9 +528,6 @@ export class NativeEditContext extends AbstractEditContext { } private _updateCharacterBounds(e: CharacterBoundsUpdateEvent): void { - if (!this._parent) { - return; - } const options = this._context.configuration.options; const typicalHalfWidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; const contentLeft = options.get(EditorOption.layoutInfo).contentLeft; diff --git a/src/vs/editor/browser/services/hoverService/hoverWidget.ts b/src/vs/editor/browser/services/hoverService/hoverWidget.ts index 3696dd6561f98..6e3936028952c 100644 --- a/src/vs/editor/browser/services/hoverService/hoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/hoverWidget.ts @@ -112,8 +112,6 @@ export class HoverWidget extends Widget implements IHoverWidget { options.appearance ??= {}; options.appearance.compact ??= true; options.appearance.showPointer ??= true; - options.position ??= {}; - options.position.hoverPosition ??= HoverPosition.BELOW; break; } case HoverStyle.Mouse: { diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.ts b/src/vs/editor/contrib/rename/browser/renameWidget.ts index f2bc9d613f2f2..8a9e22a660264 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.ts +++ b/src/vs/editor/contrib/rename/browser/renameWidget.ts @@ -45,6 +45,7 @@ import { widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; import { IColorTheme, IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { HoverStyle } from '../../../../base/browser/ui/hover/hover.js'; /** for debugging */ const _sticky = false @@ -935,10 +936,7 @@ class InputWithButton implements IDisposable { this._buttonHoverContent = this._buttonGenHoverText; this._disposables.add(getBaseLayerHoverDelegate().setupDelayedHover(this._buttonNode, () => ({ content: this._buttonHoverContent, - appearance: { - showPointer: true, - compact: true, - } + style: HoverStyle.Pointer, }))); this._domNode.appendChild(this._buttonNode); diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index f0aa6f62f1dca..337ec9a0d4e9e 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -29,6 +29,7 @@ import { Action2, IAction2Options } from '../../../platform/actions/common/actio import { ViewContainerLocation } from '../../common/views.js'; import { IPaneCompositePartService } from '../../services/panecomposite/browser/panecomposite.js'; import { createConfigureKeybindingAction } from '../../../platform/actions/common/menuService.js'; +import { HoverStyle } from '../../../base/browser/ui/hover/hover.js'; export interface ICompositeBar { @@ -260,16 +261,13 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { this._register(this.hoverService.setupDelayedHover(this.container, () => ({ content: this.computeTitle(), + style: HoverStyle.Pointer, position: { hoverPosition: this.options.hoverOptions.position(), }, persistence: { hideOnKeyDown: true, }, - appearance: { - showPointer: true, - compact: true, - } }), { groupId: 'composite-bar-actions' })); // Label diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts index 2d63ad8b8a51a..d93d47517ffdb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts @@ -8,7 +8,7 @@ import { $ } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; -import type { IHoverLifecycleOptions, IHoverOptions } from '../../../../base/browser/ui/hover/hover.js'; +import { HoverStyle, type IHoverLifecycleOptions, type IHoverOptions } from '../../../../base/browser/ui/hover/hover.js'; import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -59,10 +59,7 @@ import { ILanguageModelToolsService, ToolSet } from '../common/languageModelTool import { getCleanPromptName } from '../common/promptSyntax/config/promptFileLocations.js'; const commonHoverOptions: Partial = { - appearance: { - compact: true, - showPointer: true, - }, + style: HoverStyle.Pointer, position: { hoverPosition: HoverPosition.BELOW }, @@ -336,9 +333,15 @@ function createImageElements(resource: URI | undefined, name: string, fullName: if ((!supportsVision && currentLanguageModel) || omittedState === OmittedState.Full) { element.classList.add('warning'); hoverElement.textContent = localize('chat.imageAttachmentHover', "{0} does not support images.", currentLanguageModelName ?? 'This model'); - disposable.add(hoverService.setupDelayedHover(element, { content: hoverElement, appearance: { showPointer: true } })); + disposable.add(hoverService.setupDelayedHover(element, { + content: hoverElement, + style: HoverStyle.Pointer, + })); } else { - disposable.add(hoverService.setupDelayedHover(element, { content: hoverElement, appearance: { showPointer: true } })); + disposable.add(hoverService.setupDelayedHover(element, { + content: hoverElement, + style: HoverStyle.Pointer, + })); const blob = new Blob([buffer as Uint8Array], { type: 'image/png' }); const url = URL.createObjectURL(blob); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAgentCommandContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAgentCommandContentPart.ts index 5f3e0a44fb79a..cbc8518b7c49b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAgentCommandContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAgentCommandContentPart.ts @@ -15,6 +15,7 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { localize } from '../../../../../nls.js'; import { Button } from '../../../../../base/browser/ui/button/button.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; +import { HoverStyle } from '../../../../../base/browser/ui/hover/hover.js'; export class ChatAgentCommandContentPart extends Disposable implements IChatContentPart { @@ -36,14 +37,20 @@ export class ChatAgentCommandContentPart extends Disposable implements IChatCont const commandSpan = document.createElement('span'); this.domNode.appendChild(commandSpan); commandSpan.innerText = chatSubcommandLeader + cmd.name; - this._store.add(this._hoverService.setupDelayedHover(commandSpan, { content: cmd.description, appearance: { showPointer: true } }, { groupId })); + this._store.add(this._hoverService.setupDelayedHover(commandSpan, { + content: cmd.description, + style: HoverStyle.Pointer, + }, { groupId })); const rerun = localize('rerun', "Rerun without {0}{1}", chatSubcommandLeader, cmd.name); const btn = new Button(this.domNode, { ariaLabel: rerun }); btn.icon = Codicon.close; this._store.add(btn.onDidClick(() => onClick())); this._store.add(btn); - this._store.add(this._hoverService.setupDelayedHover(btn.element, { content: rerun, appearance: { showPointer: true } }, { groupId })); + this._store.add(this._hoverService.setupDelayedHover(btn.element, { + content: rerun, + style: HoverStyle.Pointer, + }, { groupId })); } hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 3a92acc492249..7e34977acaf96 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -59,6 +59,7 @@ import { IDisposableReference, ResourcePool } from './chatCollections.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { ChatExtensionsContentPart } from './chatExtensionsContentPart.js'; import { IOpenEditorOptions, registerOpenEditorListeners } from '../../../../../platform/editor/browser/editor.js'; +import { HoverStyle } from '../../../../../base/browser/ui/hover/hover.js'; const $ = dom.$; @@ -584,14 +585,12 @@ export class CollapsedCodeBlock extends Disposable { this.tooltip = tooltip; if (!this.hover.value) { - this.hover.value = this.hoverService.setupDelayedHover(this.element, () => ( - { - content: this.tooltip!, - appearance: { compact: true, showPointer: true }, - position: { hoverPosition: HoverPosition.BELOW }, - persistence: { hideOnKeyDown: true }, - } - )); + this.hover.value = this.hoverService.setupDelayedHover(this.element, () => ({ + content: this.tooltip!, + style: HoverStyle.Pointer, + position: { hoverPosition: HoverPosition.BELOW }, + persistence: { hideOnKeyDown: true }, + })); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts index 9baa67e232974..ef4e9b60a9454 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts @@ -42,6 +42,7 @@ import { IChatContentPartRenderContext } from '../chatContentParts.js'; import { ChatMarkdownContentPart, EditorPool } from '../chatMarkdownContentPart.js'; import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; import { openTerminalSettingsLinkCommandId } from './chatTerminalToolProgressPart.js'; +import { HoverStyle } from '../../../../../../base/browser/ui/hover/hover.js'; export const enum TerminalToolConfirmationStorageKeys { TerminalAutoApproveWarningAccepted = 'chat.tools.terminal.autoApprove.warningAccepted' @@ -184,8 +185,8 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS append(elements.editor, editor.object.element); this._register(hoverService.setupDelayedHover(elements.editor, { content: message, + style: HoverStyle.Pointer, position: { hoverPosition: HoverPosition.LEFT }, - appearance: { showPointer: true }, })); const confirmWidget = this._register(this.instantiationService.createInstance( ChatCustomConfirmationWidget, diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts index afa70133eed08..3ea4e046fd59b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts @@ -48,6 +48,7 @@ import { ChatSessionTracker } from '../chatSessionTracker.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { getLocalHistoryDateFormatter } from '../../../../localHistory/browser/localHistory.js'; import { ChatSessionUri } from '../../../common/chatUri.js'; +import { HoverStyle } from '../../../../../../base/browser/ui/hover/hover.js'; interface ISessionTemplateData { readonly container: HTMLElement; @@ -282,7 +283,7 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer ({ content: tooltipContent, - appearance: { showPointer: true }, + style: HoverStyle.Pointer, position: { hoverPosition: this.getHoverPosition() } }), { groupId: 'chat.sessions' }) ); @@ -290,7 +291,7 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer ({ content: tooltipContent.markdown, - appearance: { showPointer: true }, + style: HoverStyle.Pointer, position: { hoverPosition: this.getHoverPosition() } }), { groupId: 'chat.sessions' }) ); @@ -311,7 +312,7 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer ({ content: nls.localize('chat.sessions.lastActivity', 'Last Activity: {0}', fullDateTime), - appearance: { showPointer: true }, + style: HoverStyle.Pointer, position: { hoverPosition: this.getHoverPosition() } }), { groupId: 'chat.sessions' }) ); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index eb15198a9fa2a..d3c59cf56ce74 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1361,7 +1361,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private _getGenerateInstructionsMessage(): IMarkdownString { // Start checking for instruction files immediately if not already done if (!this._instructionFilesCheckPromise) { - this._instructionFilesCheckPromise = this._checkForInstructionFiles(); + this._instructionFilesCheckPromise = this._checkForAgentInstructionFiles(); // Use VS Code's idiomatic pattern for disposal-safe promise callbacks this._register(thenIfNotDisposed(this._instructionFilesCheckPromise, hasFiles => { this._instructionFilesExist = hasFiles; @@ -1391,10 +1391,25 @@ export class ChatWidget extends Disposable implements IChatWidget { return new MarkdownString(''); } - private async _checkForInstructionFiles(): Promise { + /** + * Checks if any agent instruction files (.github/copilot-instructions.md or AGENTS.md) exist in the workspace. + * Used to determine whether to show the "Generate Agent Instructions" hint. + * + * @returns true if instruction files exist OR if instruction features are disabled (to hide the hint) + */ + private async _checkForAgentInstructionFiles(): Promise { try { - const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, undefined); - return await computer.hasAgentInstructions(CancellationToken.None); + const useCopilotInstructionsFiles = this.configurationService.getValue(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES); + const useAgentMd = this.configurationService.getValue(PromptsConfig.USE_AGENT_MD); + if (!useCopilotInstructionsFiles && !useAgentMd) { + // If both settings are disabled, return true to hide the hint (since the features aren't enabled) + return true; + } + return ( + (await this.promptsService.listCopilotInstructionsMDs(CancellationToken.None)).length > 0 || + // Note: only checking for AGENTS.md files at the root folder, not ones in subfolders. + (await this.promptsService.listAgentMDs(CancellationToken.None, false)).length > 0 + ); } catch (error) { // On error, assume no instruction files exist to be safe this.logService.warn('[ChatWidget] Error checking for instruction files:', error); diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts index 65fae65e6bff3..07d9543ec1508 100644 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modePickerActionItem.ts @@ -89,7 +89,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem { agentMode && makeAction(agentMode, currentMode), ...customBuiltinModeActions, ...otherBuiltinModes.map(mode => mode && makeAction(mode, currentMode)), - ...customModes.custom.map(mode => makeActionFromCustomMode(mode, currentMode)) + ...customModes.custom?.map(mode => makeActionFromCustomMode(mode, currentMode)) ?? [] ]); return orderedModes; diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts index 395d477a9c4c3..f453f91f2d215 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts @@ -26,6 +26,8 @@ import { UILabelProvider } from '../../../../../../base/common/keybindingLabels. import { OS } from '../../../../../../base/common/platform.js'; import { askForPromptSourceFolder } from './askForPromptSourceFolder.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { PromptsConfig } from '../../../common/promptSyntax/config/config.js'; /** * Options for the {@link askToSelectInstructions} function. @@ -194,6 +196,7 @@ export class PromptFilePickers { @IInstantiationService private readonly _instaService: IInstantiationService, @IPromptsService private readonly _promptsService: IPromptsService, @ILabelService private readonly _labelService: ILabelService, + @IConfigurationService private readonly _configurationService: IConfigurationService, ) { } @@ -297,9 +300,37 @@ export class PromptFilePickers { } const locals = await this._promptsService.listPromptFilesForStorage(options.type, PromptsStorage.local, CancellationToken.None); if (locals.length) { - result.push({ type: 'separator', label: localize('separator.workspace', "Workspace") }); + // Note: No need to localize the label ".github/instructions', since it's the same in all languages. + result.push({ type: 'separator', label: '.github/instructions' }); result.push(...locals.map(l => this._createPromptPickItem(l, buttons))); } + + // Agent instruction files (copilot-instructions.md and AGENTS.md) are added here and not included in the output of + // listPromptFilesForStorage() because that function only handles *.instructions.md files (under `.github/instructions/`, etc.) + let agentInstructionFiles: IPromptPath[] = []; + if (options.type === PromptsType.instructions) { + const useNestedAgentMD = this._configurationService.getValue(PromptsConfig.USE_NESTED_AGENT_MD); + const agentInstructionUris = [ + ...await this._promptsService.listCopilotInstructionsMDs(CancellationToken.None), + ...await this._promptsService.listAgentMDs(CancellationToken.None, !!useNestedAgentMD) + ]; + agentInstructionFiles = agentInstructionUris.map(uri => { + const folderName = this._labelService.getUriLabel(dirname(uri), { relative: true }); + // Don't show the folder path for files under .github folder (namely, copilot-instructions.md) since that is only defined once per repo. + const shouldShowFolderPath = folderName?.toLowerCase() !== '.github'; + return { + uri, + description: shouldShowFolderPath ? folderName : undefined, + storage: PromptsStorage.local, + type: options.type + } satisfies IPromptPath; + }); + } + if (agentInstructionFiles.length) { + result.push({ type: 'separator', label: localize('separator.workspace-agent-instructions', "Agent Instructions") }); + result.push(...agentInstructionFiles.map(l => this._createPromptPickItem(l, buttons))); + } + const exts = await this._promptsService.listPromptFilesForStorage(options.type, PromptsStorage.extension, CancellationToken.None); if (exts.length) { result.push({ type: 'separator', label: localize('separator.extensions', "Extensions") }); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 1efe6567c4b54..ad05805f32ec0 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -7,7 +7,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { match, splitGlobAware } from '../../../../../base/common/glob.js'; import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { basename, dirname, joinPath } from '../../../../../base/common/resources.js'; +import { basename, dirname } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -19,7 +19,7 @@ import { IWorkspaceContextService } from '../../../../../platform/workspace/comm import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, toPromptFileVariableEntry, toPromptTextVariableEntry, PromptFileVariableKind } from '../chatVariableEntries.js'; import { IToolData } from '../languageModelToolsService.js'; import { PromptsConfig } from './config/config.js'; -import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, isPromptOrInstructionsFile } from './config/promptFileLocations.js'; +import { isPromptOrInstructionsFile } from './config/promptFileLocations.js'; import { PromptsType } from './promptTypes.js'; import { ParsedPromptFile } from './service/newPromptsParser.js'; import { IPromptPath, IPromptsService } from './service/promptsService.js'; @@ -110,48 +110,6 @@ export class ComputeAutomaticInstructions { this.sendTelemetry(telemetryEvent); } - /** - * Checks if any agent instruction files (.github/copilot-instructions.md or agents.md) exist in the workspace. - * Used to determine whether to show the "Generate Agent Instructions" hint. - * - * @returns true if instruction files exist OR if instruction features are disabled (to hide the hint) - */ - public async hasAgentInstructions(token: CancellationToken): Promise { - const useCopilotInstructionsFiles = this._configurationService.getValue(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES); - const useAgentMd = this._configurationService.getValue(PromptsConfig.USE_AGENT_MD); - - // If both settings are disabled, return true to hide the hint (since the features aren't enabled) - if (!useCopilotInstructionsFiles && !useAgentMd) { - return true; - } - const { folders } = this._workspaceService.getWorkspace(); - - // Check for copilot-instructions.md files - if (useCopilotInstructionsFiles) { - for (const folder of folders) { - const file = joinPath(folder.uri, `.github/` + COPILOT_CUSTOM_INSTRUCTIONS_FILENAME); - if (await this._fileService.exists(file)) { - return true; - } - } - } - - // Check for agents.md files - if (useAgentMd) { - const resolvedRoots = await this._fileService.resolveAll(folders.map(f => ({ resource: f.uri }))); - for (const root of resolvedRoots) { - if (root.success && root.stat?.children) { - const agentMd = root.stat.children.find(c => c.isFile && c.name.toLowerCase() === 'agents.md'); - if (agentMd) { - return true; - } - } - } - } - - return false; - } - private sendTelemetry(telemetryEvent: InstructionsCollectionEvent): void { // Emit telemetry telemetryEvent.totalInstructionsCount = telemetryEvent.agentInstructionsCount + telemetryEvent.referencedInstructionsCount + telemetryEvent.applyingInstructionsCount + telemetryEvent.listedInstructionsCount; @@ -221,33 +179,23 @@ export class ComputeAutomaticInstructions { this._logService.trace(`[InstructionsContextComputer] No agent instructions files added (settings disabled).`); return; } - const instructionFiles: string[] = []; - instructionFiles.push(`.github/` + COPILOT_CUSTOM_INSTRUCTIONS_FILENAME); - const { folders } = this._workspaceService.getWorkspace(); const entries: ChatRequestVariableSet = new ChatRequestVariableSet(); if (useCopilotInstructionsFiles) { - for (const folder of folders) { - const file = joinPath(folder.uri, `.github/` + COPILOT_CUSTOM_INSTRUCTIONS_FILENAME); - if (await this._fileService.exists(file)) { - entries.add(toPromptFileVariableEntry(file, PromptFileVariableKind.Instruction, localize('instruction.file.reason.copilot', 'Automatically attached as setting {0} is enabled', PromptsConfig.USE_COPILOT_INSTRUCTION_FILES), true)); - telemetryEvent.agentInstructionsCount++; - this._logService.trace(`[InstructionsContextComputer] copilot-instruction.md files added: ${file.toString()}`); - } + const files: URI[] = await this._promptsService.listCopilotInstructionsMDs(token); + for (const file of files) { + entries.add(toPromptFileVariableEntry(file, PromptFileVariableKind.Instruction, localize('instruction.file.reason.copilot', 'Automatically attached as setting {0} is enabled', PromptsConfig.USE_COPILOT_INSTRUCTION_FILES), true)); + telemetryEvent.agentInstructionsCount++; + this._logService.trace(`[InstructionsContextComputer] copilot-instruction.md files added: ${file.toString()}`); } await this._addReferencedInstructions(entries, telemetryEvent, token); } if (useAgentMd) { - const resolvedRoots = await this._fileService.resolveAll(folders.map(f => ({ resource: f.uri }))); - for (const root of resolvedRoots) { - if (root.success && root.stat?.children) { - const agentMd = root.stat.children.find(c => c.isFile && c.name.toLowerCase() === 'agents.md'); - if (agentMd) { - entries.add(toPromptFileVariableEntry(agentMd.resource, PromptFileVariableKind.Instruction, localize('instruction.file.reason.agentsmd', 'Automatically attached as setting {0} is enabled', PromptsConfig.USE_AGENT_MD), true)); - telemetryEvent.agentInstructionsCount++; - this._logService.trace(`[InstructionsContextComputer] AGENTS.md files added: ${agentMd.resource.toString()}`); - } - } + const files = await this._promptsService.listAgentMDs(token, false); + for (const file of files) { + entries.add(toPromptFileVariableEntry(file, PromptFileVariableKind.Instruction, localize('instruction.file.reason.agentsmd', 'Automatically attached as setting {0} is enabled', PromptsConfig.USE_AGENT_MD), true)); + telemetryEvent.agentInstructionsCount++; + this._logService.trace(`[InstructionsContextComputer] AGENTS.md files added: ${file.toString()}`); } } for (const entry of entries.asArray()) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 5e388a31aff95..b3462c6d400ab 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -414,33 +414,26 @@ export class PromptValidatorContribution extends Disposable { const trackers = new ResourceMap(); this.localDisposables.add(toDisposable(() => { trackers.forEach(tracker => tracker.dispose()); + trackers.clear(); })); + this.modelService.getModels().forEach(model => { + const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); + if (promptType) { + trackers.set(model.uri, new ModelTracker(model, promptType, this.validator, this.promptsService, this.markerService)); + } + }); - const validateAllDelayer = this._register(new Delayer(200)); - const validateAll = (): void => { - validateAllDelayer.trigger(async () => { - this.modelService.getModels().forEach(model => { - const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); - if (promptType) { - trackers.set(model.uri, new ModelTracker(model, promptType, this.validator, this.promptsService, this.markerService)); - } - }); - }); - }; this.localDisposables.add(this.modelService.onModelAdded((model) => { const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); - if (promptType) { + if (promptType && !trackers.has(model.uri)) { trackers.set(model.uri, new ModelTracker(model, promptType, this.validator, this.promptsService, this.markerService)); } })); this.localDisposables.add(this.modelService.onModelRemoved((model) => { - const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); - if (promptType) { - const tracker = trackers.get(model.uri); - if (tracker) { - tracker.dispose(); - trackers.delete(model.uri); - } + const tracker = trackers.get(model.uri); + if (tracker) { + tracker.dispose(); + trackers.delete(model.uri); } })); this.localDisposables.add(this.modelService.onModelLanguageChanged((event) => { @@ -455,10 +448,11 @@ export class PromptValidatorContribution extends Disposable { trackers.set(model.uri, new ModelTracker(model, promptType, this.validator, this.promptsService, this.markerService)); } })); + + const validateAll = (): void => trackers.forEach(tracker => tracker.validate()); this.localDisposables.add(this.languageModelToolsService.onDidChangeTools(() => validateAll())); this.localDisposables.add(this.chatModeService.onDidChangeChatModes(() => validateAll())); this.localDisposables.add(this.languageModelsService.onDidChangeLanguageModels(() => validateAll())); - validateAll(); } } @@ -479,7 +473,7 @@ class ModelTracker extends Disposable { this.validate(); } - private validate(): void { + public validate(): void { this.delayer.trigger(async () => { const markers: IMarkerData[] = []; const ast = this.promptsService.getParsedPromptFile(this.textModel); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index 6fb1db9901408..399f9941714d2 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -257,7 +257,21 @@ export interface IPromptsService extends IDisposable { getPromptLocationLabel(promptPath: IPromptPath): string; + /** + * Gets list of all AGENTS.md files in the workspace. + */ findAgentMDsInWorkspace(token: CancellationToken): Promise; + + /** + * Gets list of AGENTS.md files. + * @param includeNested Whether to include AGENTS.md files from subfolders, or only from the root. + */ + listAgentMDs(token: CancellationToken, includeNested: boolean): Promise; + + /** + * Gets list of .github/copilot-instructions.md files. + */ + listCopilotInstructionsMDs(token: CancellationToken): Promise; } export interface IChatPromptSlashCommand { 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 4b7c087ec7a22..cc2d468f76d35 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -351,6 +351,26 @@ export class PromptsService extends Disposable implements IPromptsService { findAgentMDsInWorkspace(token: CancellationToken): Promise { return this.fileLocator.findAgentMDsInWorkspace(token); } + + public async listAgentMDs(token: CancellationToken, includeNested: boolean): Promise { + const useAgentMD = this.configurationService.getValue(PromptsConfig.USE_AGENT_MD); + if (!useAgentMD) { + return []; + } + if (includeNested) { + return await this.fileLocator.findAgentMDsInWorkspace(token); + } else { + return await this.fileLocator.findAgentMDsInWorkspaceRoots(token); + } + } + + public async listCopilotInstructionsMDs(token: CancellationToken): Promise { + const useCopilotInstructionsFiles = this.configurationService.getValue(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES); + if (!useCopilotInstructionsFiles) { + return []; + } + return await this.fileLocator.findCopilotInstructionsMDsInWorkspace(token); + } } function getCommandNameFromPromptPath(promptPath: IPromptPath): string { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts index e3afeaf8f4f6c..831abd801db53 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts @@ -11,7 +11,7 @@ import { getPromptFileLocationsConfigKey, PromptsConfig } from '../config/config import { basename, dirname, joinPath } from '../../../../../../base/common/resources.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { getPromptFileExtension, getPromptFileType } from '../config/promptFileLocations.js'; +import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, getPromptFileExtension, getPromptFileType } from '../config/promptFileLocations.js'; import { PromptsType } from '../promptTypes.js'; import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; import { Schemas } from '../../../../../../base/common/network.js'; @@ -275,7 +275,21 @@ export class PromptFilesLocator extends Disposable { return []; } + public async findCopilotInstructionsMDsInWorkspace(token: CancellationToken): Promise { + const result: URI[] = []; + const { folders } = this.workspaceService.getWorkspace(); + for (const folder of folders) { + const file = joinPath(folder.uri, `.github/` + COPILOT_CUSTOM_INSTRUCTIONS_FILENAME); + if (await this.fileService.exists(file)) { + result.push(file); + } + } + return result; + } + /** + * Gets list of `AGENTS.md` files anywhere in the workspace. + */ public async findAgentMDsInWorkspace(token: CancellationToken): Promise { const result = await Promise.all(this.workspaceService.getWorkspace().folders.map(folder => this.findAgentMDsInFolder(folder.uri, token))); return result.flat(1); @@ -305,6 +319,24 @@ export class PromptFilesLocator extends Disposable { return []; } + + /** + * Gets list of `AGENTS.md` files only at the root workspace folder(s). + */ + public async findAgentMDsInWorkspaceRoots(token: CancellationToken): Promise { + const result: URI[] = []; + const { folders } = this.workspaceService.getWorkspace(); + const resolvedRoots = await this.fileService.resolveAll(folders.map(f => ({ resource: f.uri }))); + for (const root of resolvedRoots) { + if (root.success && root.stat?.children) { + const agentMd = root.stat.children.find(c => c.isFile && c.name.toLowerCase() === 'agents.md'); + if (agentMd) { + result.push(agentMd.resource); + } + } + } + return result; + } } /** diff --git a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts index a1b522e7ccddc..41b921b3e729d 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts @@ -46,6 +46,8 @@ export class MockPromptsService implements IPromptsService { registerContributedFile(type: PromptsType, name: string, description: string, uri: URI, extension: IExtensionDescription): IDisposable { throw new Error('Not implemented'); } getPromptLocationLabel(promptPath: IPromptPath): string { throw new Error('Not implemented'); } findAgentMDsInWorkspace(token: CancellationToken): Promise { throw new Error('Not implemented'); } + listAgentMDs(token: CancellationToken): Promise { throw new Error('Not implemented'); } + listCopilotInstructionsMDs(token: CancellationToken): Promise { throw new Error('Not implemented'); } dispose(): void { } } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 8f73cd9facd5e..8aa3a89b7b137 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -58,6 +58,7 @@ suite('PromptsService', () => { testConfigService.setUserConfiguration(PromptsConfig.KEY, true); testConfigService.setUserConfiguration(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES, true); testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_MD, true); + testConfigService.setUserConfiguration(PromptsConfig.USE_NESTED_AGENT_MD, false); testConfigService.setUserConfiguration(PromptsConfig.INSTRUCTIONS_LOCATION_KEY, { [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true }); testConfigService.setUserConfiguration(PromptsConfig.PROMPT_LOCATIONS_KEY, { [PROMPT_DEFAULT_SOURCE_FOLDER]: true }); testConfigService.setUserConfiguration(PromptsConfig.MODE_LOCATION_KEY, { [MODE_DEFAULT_SOURCE_FOLDER]: true }); @@ -701,7 +702,18 @@ suite('PromptsService', () => { }, ], }, - + { + name: 'folder1', + children: [ + // This will not be returned because we have PromptsConfig.USE_NESTED_AGENT_MD set to false. + { + name: 'AGENTS.md', + contents: [ + 'An AGENTS.md file in another repo' + ] + } + ] + } ], }])).mock(); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts index 3c759b6dc9c9e..10f62eb9c831e 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerActions.ts @@ -36,6 +36,7 @@ import { LocalMcpServerScope } from '../../../services/mcp/common/mcpWorkbenchMa import { ExtensionAction } from '../../extensions/browser/extensionsActions.js'; import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; import { IContextMenuProvider } from '../../../../base/browser/contextmenu.js'; +import Severity from '../../../../base/common/severity.js'; export interface IMcpServerActionChangeEvent extends IActionChangeEvent { readonly hidden?: boolean; @@ -1158,9 +1159,9 @@ export class McpServerStatusAction extends McpServerAction { } } - const runtimeState = this.mcpServer.runtimeState; - if (runtimeState?.disabled && runtimeState.reason) { - this.updateStatus({ icon: warningIcon, message: runtimeState.reason }, true); + const runtimeState = this.mcpServer.runtimeStatus; + if (runtimeState?.message) { + this.updateStatus({ icon: runtimeState.message.severity === Severity.Warning ? warningIcon : runtimeState.message.severity === Severity.Error ? errorIcon : infoIcon, message: runtimeState.message.text }, true); } } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts index 0a5e67fd61fe7..51842d678cefd 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts @@ -26,7 +26,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { getLocationBasedViewColors } from '../../../browser/parts/views/viewPane.js'; import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { IViewDescriptorService, IViewsRegistry, ViewContainerLocation, Extensions as ViewExtensions } from '../../../common/views.js'; -import { HasInstalledMcpServersContext, IMcpWorkbenchService, InstalledMcpServersViewId, IWorkbenchMcpServer, McpServerContainers, McpServersGalleryStatusContext } from '../common/mcpTypes.js'; +import { HasInstalledMcpServersContext, IMcpWorkbenchService, InstalledMcpServersViewId, IWorkbenchMcpServer, McpServerContainers, McpServerEnablementState, McpServersGalleryStatusContext } from '../common/mcpTypes.js'; import { DropDownAction, getContextMenuActions, InstallAction, InstallingLabelAction, ManageMcpServerAction, McpServerStatusAction } from './mcpServerActions.js'; import { PublisherWidget, StarredWidget, McpServerIconWidget, McpServerHoverWidget, McpServerScopeBadgeWidget } from './mcpServerWidgets.js'; import { ActionRunner, IAction, Separator } from '../../../../base/common/actions.js'; @@ -484,7 +484,7 @@ class McpServerRenderer implements IPagedRenderer data.root.classList.toggle('disabled', !!mcpServer.runtimeState?.disabled); + const updateEnablement = () => data.root.classList.toggle('disabled', !!mcpServer.runtimeStatus?.state && mcpServer.runtimeStatus.state !== McpServerEnablementState.Enabled); updateEnablement(); data.mcpServerDisposables.push(this.mcpWorkbenchService.onChange(e => { if (!e || e.id === mcpServer.id) { diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index 22d68e6d88f92..e4f66303f5140 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -36,12 +36,13 @@ import { DidUninstallWorkbenchMcpServerEvent, IWorkbenchLocalMcpServer, IWorkben import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { mcpConfigurationSection } from '../common/mcpConfiguration.js'; import { McpServerInstallData, McpServerInstallClassification } from '../common/mcpServer.js'; -import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServerEnablementState, McpServerInstallState, McpServerRuntimeState, McpServersGalleryStatusContext } from '../common/mcpTypes.js'; +import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServerEnablementState, McpServerInstallState, McpServerEnablementStatus, McpServersGalleryStatusContext } from '../common/mcpTypes.js'; import { McpServerEditorInput } from './mcpServerEditorInput.js'; import { IMcpGalleryManifestService } from '../../../../platform/mcp/common/mcpGalleryManifest.js'; import { IPager, singlePagePager } from '../../../../base/common/paging.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { runOnChange } from '../../../../base/common/observable.js'; +import Severity from '../../../../base/common/severity.js'; interface IMcpServerStateProvider { (mcpWorkbenchServer: McpWorkbenchServer): T; @@ -51,7 +52,7 @@ class McpWorkbenchServer implements IWorkbenchMcpServer { constructor( private installStateProvider: IMcpServerStateProvider, - private runtimeStateProvider: IMcpServerStateProvider, + private runtimeStateProvider: IMcpServerStateProvider, public local: IWorkbenchLocalMcpServer | undefined, public gallery: IGalleryMcpServer | undefined, public readonly installable: IInstallableMcpServer | undefined, @@ -116,7 +117,7 @@ class McpWorkbenchServer implements IWorkbenchMcpServer { return this.local?.config ?? this.installable?.config; } - get runtimeState(): McpServerRuntimeState | undefined { + get runtimeStatus(): McpServerEnablementStatus | undefined { return this.runtimeStateProvider(this); } @@ -204,7 +205,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ this._onChange.fire(undefined); } })); - this._register(mcpGalleryManifestService.onDidChangeMcpGalleryManifest(e => this.syncInstalledMcpServers(true))); + this._register(mcpGalleryManifestService.onDidChangeMcpGalleryManifest(e => this.syncInstalledMcpServers())); this._register(this.allowedMcpServersService.onDidChangeAllowedMcpServers(() => { this._local = this.sort(this._local); this._onChange.fire(undefined); @@ -251,7 +252,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ if (server) { server.local = local; } else { - server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeState(e), local, source, undefined); + server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), local, source, undefined); } if (!local.galleryUrl) { server.gallery = undefined; @@ -277,7 +278,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ this._local[serverIndex].local = result.local; server = this._local[serverIndex]; } else { - server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeState(e), result.local, result.source, undefined); + server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), result.local, result.source, undefined); this.addServer(server); } this._onChange.fire(server); @@ -294,7 +295,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ return undefined; } - private async syncInstalledMcpServers(resetGallery?: boolean): Promise { + private async syncInstalledMcpServers(): Promise { const names: string[] = []; for (const installed of this.local) { @@ -308,30 +309,30 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ if (names.length) { const galleryServers = await this.mcpGalleryService.getMcpServersFromGallery(names); - if (galleryServers.length) { - await this.syncInstalledMcpServersWithGallery(galleryServers, resetGallery); - } + await this.syncInstalledMcpServersWithGallery(galleryServers); } } - private async syncInstalledMcpServersWithGallery(gallery: IGalleryMcpServer[], resetGallery?: boolean): Promise { + private async syncInstalledMcpServersWithGallery(gallery: IGalleryMcpServer[]): Promise { const galleryMap = new Map(gallery.map(server => [server.name, server])); for (const mcpServer of this.local) { if (!mcpServer.local) { continue; } const key = mcpServer.local.name; - const galleryServer = key ? galleryMap.get(key) : undefined; - if (!galleryServer) { - if (mcpServer.gallery && resetGallery) { + const gallery = key ? galleryMap.get(key) : undefined; + + if (!gallery || gallery.galleryUrl !== mcpServer.local.galleryUrl) { + if (mcpServer.gallery) { mcpServer.gallery = undefined; this._onChange.fire(mcpServer); } continue; } - mcpServer.gallery = galleryServer; + + mcpServer.gallery = gallery; if (!mcpServer.local.manifest) { - mcpServer.local = await this.mcpManagementService.updateMetadata(mcpServer.local, galleryServer); + mcpServer.local = await this.mcpManagementService.updateMetadata(mcpServer.local, gallery); } this._onChange.fire(mcpServer); } @@ -343,12 +344,12 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ } const pager = await this.mcpGalleryService.query(options, token); return { - firstPage: pager.firstPage.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeState(e), undefined, gallery, undefined)), + firstPage: pager.firstPage.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, gallery, undefined)), total: pager.total, pageSize: pager.pageSize, getPage: async (pageIndex, token) => { const page = await pager.getPage(pageIndex, token); - return page.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeState(e), undefined, gallery, undefined)); + return page.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, gallery, undefined)); } }; } @@ -357,7 +358,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ const installed = await this.mcpManagementService.getInstalled(); this._local = this.sort(installed.map(i => { const existing = this._local.find(local => local.id === i.id); - const local = existing ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeState(e), undefined, undefined, undefined); + const local = existing ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, undefined, undefined); local.local = i; return local; })); @@ -373,10 +374,10 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ private sort(local: McpWorkbenchServer[]): McpWorkbenchServer[] { return local.sort((a, b) => { if (a.name === b.name) { - if (!a.runtimeState?.disabled) { + if (!a.runtimeStatus || a.runtimeStatus.state === McpServerEnablementState.Enabled) { return -1; } - if (!b.runtimeState?.disabled) { + if (!b.runtimeStatus || b.runtimeStatus.state === McpServerEnablementState.Enabled) { return 1; } return 0; @@ -391,7 +392,8 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ const workspace: IWorkbenchLocalMcpServer[] = []; for (const server of this.local) { - if (this.getEnablementState(server) !== McpServerEnablementState.Enabled) { + const enablementStatus = this.getEnablementStatus(server); + if (enablementStatus && enablementStatus.state !== McpServerEnablementState.Enabled) { continue; } @@ -661,7 +663,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ if (config.type === undefined) { (>config).type = (parsed).command ? McpServerType.LOCAL : McpServerType.REMOTE; } - this.open(this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeState(e), undefined, undefined, { name, config, inputs })); + this.open(this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, undefined, { name, config, inputs })); } catch (e) { // ignore } @@ -675,7 +677,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ this.logService.info(`MCP server '${url}' not found`); return true; } - const local = this.local.find(e => e.name === gallery.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeState(e), undefined, gallery, undefined); + const local = this.local.find(e => e.name === gallery.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, gallery, undefined); this.open(local); } catch (e) { // ignore @@ -691,7 +693,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ this.logService.info(`MCP server '${name}' not found`); return true; } - const local = this.local.find(e => e.name === gallery.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeState(e), undefined, gallery, undefined); + const local = this.local.find(e => e.name === gallery.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, gallery, undefined); this.open(local); } catch (e) { // ignore @@ -719,41 +721,63 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ return local ? McpServerInstallState.Installed : McpServerInstallState.Uninstalled; } - private getRuntimeState(mcpServer: McpWorkbenchServer): McpServerRuntimeState | undefined { - if (!mcpServer.local) { - return undefined; - } + private getRuntimeStatus(mcpServer: McpWorkbenchServer): McpServerEnablementStatus | undefined { + const enablementStatus = this.getEnablementStatus(mcpServer); - const accessValue = this.configurationService.getValue(mcpAccessConfig); - const settingsCommandLink = createCommandUri('workbench.action.openSettings', { query: `@id:${mcpAccessConfig}` }).toString(); - if (accessValue === McpAccessValue.None) { - return { disabled: true, reason: new MarkdownString(localize('disabled - all not allowed', "This MCP Server is disabled because MCP servers are configured to be disabled in the Editor. Please check your [settings]({0}).", settingsCommandLink)) }; - } - if (accessValue === McpAccessValue.Registry && !mcpServer.gallery) { - return { disabled: true, reason: new MarkdownString(localize('disabled - some not allowed', "This MCP Server is disabled because it is configured to be disabled in the Editor. Please check your [settings]({0}).", settingsCommandLink)) }; + if (enablementStatus) { + return enablementStatus; } if (!this.mcpService.servers.get().find(s => s.definition.id === mcpServer.id)) { - return { disabled: true }; + return { state: McpServerEnablementState.Disabled }; } return undefined; } - private getEnablementState(mcpServer: McpWorkbenchServer): McpServerEnablementState { + private getEnablementStatus(mcpServer: McpWorkbenchServer): McpServerEnablementStatus | undefined { if (!mcpServer.local) { - return McpServerEnablementState.Enabled; + return undefined; } + const settingsCommandLink = createCommandUri('workbench.action.openSettings', { query: `@id:${mcpAccessConfig}` }).toString(); const accessValue = this.configurationService.getValue(mcpAccessConfig); + if (accessValue === McpAccessValue.None) { - return McpServerEnablementState.DisabledByAccess; + return { + state: McpServerEnablementState.DisabledByAccess, + message: { + severity: Severity.Warning, + text: new MarkdownString(localize('disabled - all not allowed', "This MCP Server is disabled because MCP servers are configured to be disabled in the Editor. Please check your [settings]({0}).", settingsCommandLink)) + } + }; + } - if (accessValue === McpAccessValue.Registry && !mcpServer.gallery) { - return McpServerEnablementState.DisabledByAccess; + + if (accessValue === McpAccessValue.Registry) { + if (!mcpServer.gallery) { + return { + state: McpServerEnablementState.DisabledByAccess, + message: { + severity: Severity.Warning, + text: new MarkdownString(localize('disabled - some not allowed', "This MCP Server is disabled because it is configured to be disabled in the Editor. Please check your [settings]({0}).", settingsCommandLink)) + } + }; + } + + const remoteUrl = mcpServer.local.config.type === McpServerType.REMOTE && mcpServer.local.config.url; + if (remoteUrl && !mcpServer.gallery.configuration.remotes?.some(remote => remote.url === remoteUrl)) { + return { + state: McpServerEnablementState.DisabledByAccess, + message: { + severity: Severity.Warning, + text: new MarkdownString(localize('disabled - some not allowed', "This MCP Server is disabled because it is configured to be disabled in the Editor. Please check your [settings]({0}).", settingsCommandLink)) + } + }; + } } - return McpServerEnablementState.Enabled; + return undefined; } } diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index ca79e79d5ecfd..7bc9dadc0f43e 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -8,11 +8,12 @@ import { assertNever } from '../../../../base/common/assert.js'; import { decodeHex, encodeHex, VSBuffer } from '../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; -import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { equals as objectsEqual } from '../../../../base/common/objects.js'; import { IObservable, ObservableMap } from '../../../../base/common/observable.js'; import { IPager } from '../../../../base/common/paging.js'; +import Severity from '../../../../base/common/severity.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { Location } from '../../../../editor/common/languages.js'; import { localize } from '../../../../nls.js'; @@ -679,6 +680,7 @@ export interface IMcpServerEditorOptions extends IEditorOptions { } export const enum McpServerEnablementState { + Disabled, DisabledByAccess, Enabled, } @@ -696,14 +698,20 @@ export const enum McpServerEditorTab { Configuration = 'configuration', } -export type McpServerRuntimeState = { readonly disabled: boolean; readonly reason?: MarkdownString }; +export type McpServerEnablementStatus = { + readonly state: McpServerEnablementState; + readonly message?: { + readonly severity: Severity; + readonly text: IMarkdownString; + }; +}; export interface IWorkbenchMcpServer { readonly gallery: IGalleryMcpServer | undefined; readonly local: IWorkbenchLocalMcpServer | undefined; readonly installable: IInstallableMcpServer | undefined; readonly installState: McpServerInstallState; - readonly runtimeState: McpServerRuntimeState | undefined; + readonly runtimeStatus: McpServerEnablementStatus | undefined; readonly id: string; readonly name: string; readonly label: string; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index 639cc8f5d28f6..50ce47140b28a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -5,7 +5,7 @@ import * as DOM from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; -import type { IHoverOptions, IHoverWidget } from '../../../../base/browser/ui/hover/hover.js'; +import { HoverStyle, type IHoverOptions, type IHoverWidget } from '../../../../base/browser/ui/hover/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { SimpleIconLabel } from '../../../../base/browser/ui/iconLabel/simpleIconLabel.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; @@ -100,13 +100,10 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { private defaultHoverOptions: Partial = { trapFocus: true, + style: HoverStyle.Pointer, position: { hoverPosition: HoverPosition.BELOW, }, - appearance: { - showPointer: true, - compact: false, - } }; private addHoverDisposables(disposables: DisposableStore, element: HTMLElement, showHover: (focus: boolean) => IHoverWidget | undefined) { @@ -515,13 +512,10 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { return this.hoverService.showInstantHover({ content: new MarkdownString().appendMarkdown(defaultOverrideHoverContent), target: this.defaultOverrideIndicator.element, + style: HoverStyle.Pointer, position: { hoverPosition: HoverPosition.BELOW, }, - appearance: { - showPointer: true, - compact: false - } }, focus); }; this.addHoverDisposables(this.defaultOverrideIndicator.disposables, this.defaultOverrideIndicator.element, showHover); diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index cec76c3a1a085..8ca4b2be26635 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -75,6 +75,7 @@ import { IDragAndDropData } from '../../../../base/browser/dnd.js'; import { ElementsDragAndDropData, ListViewTargetSector } from '../../../../base/browser/ui/list/listView.js'; import { CodeDataTransfers } from '../../../../platform/dnd/browser/dnd.js'; import { SCMHistoryItemTransferData } from './scmHistoryChatContext.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; const PICK_REPOSITORY_ACTION_ID = 'workbench.scm.action.graph.pickRepository'; const PICK_HISTORY_ITEM_REFS_ACTION_ID = 'workbench.scm.action.graph.pickHistoryItemRefs'; @@ -1521,6 +1522,7 @@ export class SCMHistoryViewPane extends ViewPane { private readonly _treeOperationSequencer = new Sequencer(); private readonly _treeLoadMoreSequencer = new Sequencer(); + private readonly _refreshThrottler = new Throttler(); private readonly _updateChildrenThrottler = new Throttler(); private readonly _scmProviderCtx: IContextKey; @@ -1558,6 +1560,7 @@ export class SCMHistoryViewPane extends ViewPane { this._actionRunner = this.instantiationService.createInstance(SCMHistoryViewPaneActionRunner); this._register(this._actionRunner); + this._register(this._refreshThrottler); this._register(this._updateChildrenThrottler); } @@ -1753,9 +1756,21 @@ export class SCMHistoryViewPane extends ViewPane { } async refresh(): Promise { + return this._refreshThrottler.queue(token => this._refresh(token)); + } + + private async _refresh(token: CancellationToken): Promise { + if (token.isCancellationRequested) { + return; + } + this._treeViewModel.clearRepositoryState(); await this._updateChildren(); + if (token.isCancellationRequested) { + return; + } + this.updateActions(); this._repositoryOutdated.set(false, undefined); this._tree.scrollTop = 0; diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts index b689e9d9fbe57..4b54e88b639f7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -192,7 +192,6 @@ export class SCMRepositoriesViewPane extends ViewPane { this._register(this.treeDataSource); const compressionEnabled = observableConfigValue('scm.compactFolders', true, this.configurationService); - const selectionModeConfig = observableConfigValue<'multiple' | 'single'>('scm.repositories.selectionMode', 'single', this.configurationService); this.tree = this.instantiationService.createInstance( WorkbenchCompressibleAsyncDataTree, @@ -217,7 +216,7 @@ export class SCMRepositoriesViewPane extends ViewPane { }, compressionEnabled: compressionEnabled.get(), overrideStyles: this.getLocationBasedColors().listOverrideStyles, - multipleSelectionSupport: selectionModeConfig.get() === 'multiple', + multipleSelectionSupport: this.scmViewService.selectionModeConfig.get() === 'multiple', expandOnDoubleClick: false, expandOnlyOnTwistieClick: true, accessibilityProvider: { @@ -233,7 +232,7 @@ export class SCMRepositoriesViewPane extends ViewPane { this._register(this.tree); this._register(autorun(reader => { - const selectionMode = selectionModeConfig.read(reader); + const selectionMode = this.scmViewService.selectionModeConfig.read(reader); this.tree.updateOptions({ multipleSelectionSupport: selectionMode === 'multiple' }); })); diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts index 90bb220a5fd39..5cc2ffe1e6054 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts @@ -5,7 +5,7 @@ import './media/scm.css'; import { IDisposable, DisposableStore, combinedDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, IObservable } from '../../../../base/common/observable.js'; +import { autorun } from '../../../../base/common/observable.js'; import { append, $ } from '../../../../base/browser/dom.js'; import { ISCMProvider, ISCMRepository, ISCMViewService } from '../common/scm.js'; import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js'; @@ -25,8 +25,6 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; export class RepositoryActionRunner extends ActionRunner { constructor(private readonly getSelectedRepositories: () => ISCMRepository[]) { @@ -65,21 +63,17 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer; constructor( private readonly toolbarMenuId: MenuId, private readonly actionViewItemProvider: IActionViewItemProvider, @ICommandService private commandService: ICommandService, - @IConfigurationService private configurationService: IConfigurationService, @IContextKeyService private contextKeyService: IContextKeyService, @IContextMenuService private contextMenuService: IContextMenuService, @IKeybindingService private keybindingService: IKeybindingService, @IMenuService private menuService: IMenuService, @ISCMViewService private scmViewService: ISCMViewService, @ITelemetryService private telemetryService: ITelemetryService - ) { - this._selectionModeConfig = observableConfigValue('scm.repositories.selectionMode', 'single', this.configurationService); - } + ) { } renderTemplate(container: HTMLElement): RepositoryTemplate { // hack @@ -105,7 +99,7 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer { - const selectionMode = this._selectionModeConfig.read(undefined); + const selectionMode = this.scmViewService.selectionModeConfig.read(undefined); const activeRepository = this.scmViewService.activeRepository.read(reader); const icon = selectionMode === 'single' diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 551ab1b493dee..9ffbf6cc760fe 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -18,7 +18,7 @@ import { binarySearch } from '../../../../base/common/arrays.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { autorun, derivedObservableWithCache, derivedOpts, IObservable, ISettableObservable, latestChangedValue, observableFromEventOpts, observableValue, runOnChange } from '../../../../base/common/observable.js'; +import { autorun, derivedObservableWithCache, derivedOpts, IObservable, ISettableObservable, latestChangedValue, observableFromEventOpts, observableValue, observableValueOpts, runOnChange, transaction } from '../../../../base/common/observable.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; @@ -112,6 +112,7 @@ export class SCMViewService implements ISCMViewService { declare readonly _serviceBrand: undefined; readonly menus: ISCMMenus; + readonly selectionModeConfig: IObservable<'multiple' | 'single'>; private didFinishLoading: boolean = false; private didSelectRepository: boolean = false; @@ -222,9 +223,7 @@ export class SCMViewService implements ISCMViewService { */ private readonly _activeRepositoryObs: IObservable; private readonly _activeRepositoryPinnedObs: ISettableObservable; - private readonly _focusedRepositoryObs: IObservable; - - private readonly _selectionModeConfig: IObservable<'multiple' | 'single'>; + private readonly _focusedRepositoryObs: ISettableObservable; private _repositoriesSortKey: ISCMRepositorySortKey; private _sortKeyContextKey: IContextKey; @@ -243,14 +242,14 @@ export class SCMViewService implements ISCMViewService { ) { this.menus = instantiationService.createInstance(SCMMenus); - this._selectionModeConfig = observableConfigValue<'multiple' | 'single'>('scm.repositories.selectionMode', 'single', this.configurationService); + this.selectionModeConfig = observableConfigValue<'multiple' | 'single'>('scm.repositories.selectionMode', 'single', this.configurationService); try { this.previousState = JSON.parse(storageService.get('scm:view:visibleRepositories', StorageScope.WORKSPACE, '')); // If previously there were multiple visible repositories but the // view mode is `single`, only restore the first visible repository. - if (this.previousState && this.previousState.visible.length > 1 && this._selectionModeConfig.get() === 'single') { + if (this.previousState && this.previousState.visible.length > 1 && this.selectionModeConfig.get() === 'single') { this.previousState = { ...this.previousState, visible: [this.previousState.visible[0]] @@ -260,19 +259,15 @@ export class SCMViewService implements ISCMViewService { // noop } - this._focusedRepositoryObs = observableFromEventOpts( - { - owner: this, - equalsFn: () => false - }, this.onDidFocusRepository, - () => this.focusedRepository); + this._focusedRepositoryObs = observableValueOpts({ + owner: this, + equalsFn: () => false + }, undefined); - this._activeEditorObs = observableFromEventOpts( - { - owner: this, - equalsFn: () => false - }, this.editorService.onDidActiveEditorChange, - () => this.editorService.activeEditor); + this._activeEditorObs = observableFromEventOpts({ + owner: this, + equalsFn: () => false + }, this.editorService.onDidActiveEditorChange, () => this.editorService.activeEditor); this._activeEditorRepositoryObs = derivedObservableWithCache(this, (reader, lastValue) => { @@ -307,7 +302,7 @@ export class SCMViewService implements ISCMViewService { }); this.disposables.add(autorun(reader => { - const selectionMode = this._selectionModeConfig.read(undefined); + const selectionMode = this.selectionModeConfig.read(undefined); const activeRepository = this.activeRepository.read(reader); if (selectionMode === 'single' && activeRepository) { @@ -322,7 +317,7 @@ export class SCMViewService implements ISCMViewService { } })); - this.disposables.add(runOnChange(this._selectionModeConfig, selectionMode => { + this.disposables.add(runOnChange(this.selectionModeConfig, selectionMode => { if (selectionMode === 'single' && this.visibleRepositories.length > 1) { const repository = this.visibleRepositories[0]; this.visibleRepositories = [repository]; @@ -377,7 +372,7 @@ export class SCMViewService implements ISCMViewService { this.insertRepositoryView(this._repositories, repositoryView); - if (this._selectionModeConfig.get() === 'multiple' || !this._repositories.find(r => r.selectionIndex !== -1)) { + if (this.selectionModeConfig.get() === 'multiple' || !this._repositories.find(r => r.selectionIndex !== -1)) { // Multiple selection mode or single selection mode (select first repository) this._repositories.forEach((repositoryView, index) => { if (repositoryView.selectionIndex === -1) { @@ -414,14 +409,14 @@ export class SCMViewService implements ISCMViewService { } } - if (this._selectionModeConfig.get() === 'multiple' || !this._repositories.find(r => r.selectionIndex !== -1)) { + if (this.selectionModeConfig.get() === 'multiple' || !this._repositories.find(r => r.selectionIndex !== -1)) { // Multiple selection mode or single selection mode (select first repository) const maxSelectionIndex = this.getMaxSelectionIndex(); this.insertRepositoryView(this._repositories, { ...repositoryView, selectionIndex: maxSelectionIndex + 1 }); this._onDidChangeRepositories.fire({ added: [repositoryView.repository], removed }); // Pin repository if needed - if (this._selectionModeConfig.get() === 'single' && this.previousState?.pinned && this.didSelectRepository) { + if (this.selectionModeConfig.get() === 'single' && this.previousState?.pinned && this.didSelectRepository) { this.pinActiveRepository(repository); } } else { @@ -465,6 +460,7 @@ export class SCMViewService implements ISCMViewService { // Check if the last repository was removed if (removed.length === 1 && this._repositories.length === 0) { this._onDidFocusRepository.fire(undefined); + this._focusedRepositoryObs.set(undefined, undefined); } // Check if the pinned repository was removed @@ -485,9 +481,9 @@ export class SCMViewService implements ISCMViewService { } if (visible) { - if (this._selectionModeConfig.get() === 'single') { + if (this.selectionModeConfig.get() === 'single') { this.visibleRepositories = [repository]; - } else if (this._selectionModeConfig.get() === 'multiple') { + } else if (this.selectionModeConfig.get() === 'multiple') { this.visibleRepositories = [...this.visibleRepositories, repository]; } } else { @@ -516,9 +512,19 @@ export class SCMViewService implements ISCMViewService { } this._repositories.forEach(r => r.focused = r.repository === repository); + const focusedRepository = this._repositories.find(r => r.focused); - if (this._repositories.find(r => r.focused)) { + if (focusedRepository) { this._onDidFocusRepository.fire(repository); + + transaction(tx => { + this._focusedRepositoryObs.set(focusedRepository.repository, tx); + + // Pin the focused repository if needed + if (this._activeRepositoryPinnedObs.get() !== undefined) { + this._activeRepositoryPinnedObs.set(focusedRepository.repository, tx); + } + }); } } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 5bcc5e638f434..cb50e484dd089 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -221,6 +221,7 @@ export interface ISCMViewService { readonly _serviceBrand: undefined; readonly menus: ISCMMenus; + readonly selectionModeConfig: IObservable<'multiple' | 'single'>; repositories: ISCMRepository[]; readonly onDidChangeRepositories: Event; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 598cdaf0a0469..91334b6e4bf6d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -317,11 +317,20 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer this._cachedContainerWidth = -1); + } + return this._cachedContainerWidth; } renderElement(instance: ITerminalInstance, index: number, template: ITerminalTabEntryTemplate): void { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index 153a231f5c185..154d27f2af0f6 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -134,7 +134,9 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str if ( firstSubcommandFirstWord !== commandLine && !commandsWithSubcommands.has(commandLine) && - !commandsWithSubSubCommands.has(commandLine) + !commandsWithSubSubCommands.has(commandLine) && + autoApproveResult.commandLineResult.result !== 'denied' && + autoApproveResult.subCommandResults.every(e => e.result !== 'denied') ) { actions.push({ label: localize('autoApprove.exactCommand', 'Always Allow Exact Command Line'), diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts index 671a513798b60..4b0f8976ab494 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts @@ -826,6 +826,22 @@ suite('RunInTerminalTool', () => { 'configure', ]); }); + + test('should not show command line option when it\'s rejected', async () => { + setAutoApprove({ + echo: true, + '/\\(.+\\)/s': { approve: false, matchCommandLine: true } + }); + + const result = await executeToolTest({ + command: 'echo (abc)' + }); + + assertConfirmationRequired(result); + assertDropdownActions(result, [ + 'configure', + ]); + }); }); suite('chat session disposal cleanup', () => {