From a480db68ab638632e2bb1d648e8c92ddd428aa47 Mon Sep 17 00:00:00 2001 From: cwebster-99 Date: Wed, 25 Feb 2026 17:08:20 -0600 Subject: [PATCH 01/28] refactor: improve premium requests quota UI in copilot status menu --- .../browser/chatManagement/chatUsageWidget.ts | 16 ++++++++-------- .../browser/chatStatus/chatStatusDashboard.ts | 14 +++++++++----- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts index d33b5a1524c9f..a325f02f86c82 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts @@ -68,12 +68,12 @@ export class ChatUsageWidget extends Disposable { // Premium requests if (premiumChatQuota) { - this.renderQuotaItem(this.usageSection, localize('plan.premiumRequests', 'Premium requests'), premiumChatQuota); + const premiumLabel = premiumChatQuota.overageEnabled ? localize('plan.includedPremiumRequests', 'Included premium requests') : localize('plan.premiumRequests', 'Premium requests'); + this.renderQuotaItem(this.usageSection, premiumLabel, premiumChatQuota, premiumChatQuota.overageEnabled); - // Additional overage message - if (premiumChatQuota.overageEnabled) { + if (premiumChatQuota.overageEnabled && !premiumChatQuota.unlimited) { const overageMessage = DOM.append(this.usageSection, $('.overage-message')); - overageMessage.textContent = localize('plan.additionalPaidEnabled', 'Additional paid premium requests enabled.'); + overageMessage.textContent = localize('plan.overageApproved', 'Additional premium requests approved after 100%.'); } } @@ -89,7 +89,7 @@ export class ChatUsageWidget extends Disposable { this._onDidChangeContentHeight.fire(height); } - private renderQuotaItem(container: HTMLElement, label: string, quota: IQuotaSnapshot): void { + private renderQuotaItem(container: HTMLElement, label: string, quota: IQuotaSnapshot, overageEnabled: boolean = false): void { const quotaItem = DOM.append(container, $('.quota-item')); const quotaItemHeader = DOM.append(quotaItem, $('.quota-item-header')); @@ -109,10 +109,10 @@ export class ChatUsageWidget extends Disposable { const percentageUsed = this.getQuotaPercentageUsed(quota); progressBar.style.width = percentageUsed + '%'; - // Apply warning/error classes based on usage - if (percentageUsed >= 90) { + // Apply warning/error classes based on usage (don't show error/warning if overage is enabled) + if (percentageUsed >= 90 && !overageEnabled) { quotaItem.classList.add('error'); - } else if (percentageUsed >= 75) { + } else if (percentageUsed >= 75 && !overageEnabled) { quotaItem.classList.add('warning'); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index 4cc80cb99f8a5..a800e0c05cb03 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -179,7 +179,8 @@ export class ChatStatusDashboard extends DomWidget { const completionsQuotaIndicator = completionsQuota && (completionsQuota.total > 0 || completionsQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, completionsQuota, localize('completionsLabel', "Inline Suggestions"), false) : undefined; const chatQuotaIndicator = chatQuota && (chatQuota.total > 0 || chatQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, chatQuota, localize('chatsLabel', "Chat messages"), false) : undefined; - const premiumChatQuotaIndicator = premiumChatQuota && (premiumChatQuota.total > 0 || premiumChatQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, premiumChatQuota, localize('premiumChatsLabel', "Premium requests"), true) : undefined; + const premiumChatLabel = premiumChatQuota?.overageEnabled && !premiumChatQuota?.unlimited ? localize('includedPremiumChatsLabel', "Included premium requests") : localize('premiumChatsLabel', "Premium requests"); + const premiumChatQuotaIndicator = premiumChatQuota && (premiumChatQuota.total > 0 || premiumChatQuota.unlimited) ? this.createQuotaIndicator(this.element, this._store, premiumChatQuota, premiumChatLabel, true) : undefined; if (resetDate) { this.element.appendChild($('div.description', undefined, localize('limitQuota', "Allowance resets {0}.", resetDateHasTime ? this.dateTimeFormatter.value.format(new Date(resetDate)) : this.dateFormatter.value.format(new Date(resetDate))))); @@ -528,15 +529,18 @@ export class ChatStatusDashboard extends DomWidget { quotaBit.style.width = `${usedPercentage}%`; - if (usedPercentage >= 90) { + const overageEnabled = supportsOverage && typeof quota !== 'string' && quota?.overageEnabled; + if (usedPercentage >= 90 && !overageEnabled) { quotaIndicator.classList.add('error'); - } else if (usedPercentage >= 75) { + } else if (usedPercentage >= 75 && !overageEnabled) { quotaIndicator.classList.add('warning'); } if (supportsOverage) { - if (typeof quota !== 'string' && quota?.overageEnabled) { - overageLabel.textContent = localize('additionalUsageEnabled', "Additional paid premium requests enabled."); + if (typeof quota !== 'string' && quota.unlimited) { + overageLabel.textContent = ''; + } else if (typeof quota !== 'string' && quota?.overageEnabled) { + overageLabel.textContent = localize('additionalUsageApproved', "Additional premium requests approved after 100%."); } else { overageLabel.textContent = localize('additionalUsageDisabled', "Additional paid premium requests disabled."); } From 64e0d66e738c28260287f8f465c01ba06992b91c Mon Sep 17 00:00:00 2001 From: cwebster-99 Date: Wed, 25 Feb 2026 17:36:45 -0600 Subject: [PATCH 02/28] Updating string --- .../contrib/chat/browser/chatManagement/chatUsageWidget.ts | 4 +++- .../contrib/chat/browser/chatStatus/chatStatusDashboard.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts index a325f02f86c82..c49f770db150b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts @@ -73,7 +73,9 @@ export class ChatUsageWidget extends Disposable { if (premiumChatQuota.overageEnabled && !premiumChatQuota.unlimited) { const overageMessage = DOM.append(this.usageSection, $('.overage-message')); - overageMessage.textContent = localize('plan.overageApproved', 'Additional premium requests approved after 100%.'); + overageMessage.append(localize('plan.overageApprovedLine1', "Additional premium requests approved.")); + DOM.append(overageMessage, $('br')); + overageMessage.append(localize('plan.overageApprovedLine2', "You can continue after included premium requests limit reaches 100%.")); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index a800e0c05cb03..ad2cb70765e27 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -540,7 +540,11 @@ export class ChatStatusDashboard extends DomWidget { if (typeof quota !== 'string' && quota.unlimited) { overageLabel.textContent = ''; } else if (typeof quota !== 'string' && quota?.overageEnabled) { - overageLabel.textContent = localize('additionalUsageApproved', "Additional premium requests approved after 100%."); + overageLabel.replaceChildren( + localize('additionalUsageApprovedLine1', "Additional premium requests approved."), + $('br'), + localize('additionalUsageApprovedLine2', "You can continue after the included premium requests limit reaches 100%.") + ); } else { overageLabel.textContent = localize('additionalUsageDisabled', "Additional paid premium requests disabled."); } From c159d7ad9bbfeb7d8cec08e88b0f1bbbc2f0ad8c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:30:52 +0100 Subject: [PATCH 03/28] feat: add sync indicator for branch changes in chat widget (#297933) --- .../chat/browser/media/chatWelcomePart.css | 9 + .../contrib/chat/browser/newChatViewPane.ts | 12 +- .../contrib/chat/browser/syncIndicator.ts | 163 ++++++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/vs/sessions/contrib/chat/browser/syncIndicator.ts diff --git a/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css b/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css index c12322771a874..6df45c10a07d0 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css @@ -361,3 +361,12 @@ .sessions-chat-picker-slot .action-label span + .chat-session-option-label { margin-left: 2px; } + +/* Sync indicator: a slim non-interactive-looking separator before the button */ +.sessions-chat-sync-indicator { + margin-left: 4px; +} + +.sessions-chat-sync-indicator .action-label .sessions-chat-dropdown-label { + margin-left: 3px; +} diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 9d9c6c67b25b6..a997bfd0351af 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -59,6 +59,7 @@ import { FolderPicker } from './folderPicker.js'; import { IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; import { IsolationModePicker, SessionTargetPicker } from './sessionTargetPicker.js'; import { BranchPicker } from './branchPicker.js'; +import { SyncIndicator } from './syncIndicator.js'; import { INewSession, ISessionOptionGroup, RemoteNewSession } from './newSession.js'; import { RepoPicker } from './repoPicker.js'; import { CloudModelPicker } from './modelPicker.js'; @@ -92,6 +93,7 @@ class NewChatWidget extends Disposable { private readonly _targetPicker: SessionTargetPicker; private readonly _isolationModePicker: IsolationModePicker; private readonly _branchPicker: BranchPicker; + private readonly _syncIndicator: SyncIndicator; private readonly _options: INewChatWidgetOptions; // Input @@ -163,6 +165,7 @@ class NewChatWidget extends Disposable { this._targetPicker = this._register(new SessionTargetPicker(options.allowedTargets, options.defaultTarget)); this._isolationModePicker = this._register(this.instantiationService.createInstance(IsolationModePicker)); this._branchPicker = this._register(this.instantiationService.createInstance(BranchPicker)); + this._syncIndicator = this._register(this.instantiationService.createInstance(SyncIndicator)); this._options = options; // When target changes, create new session @@ -171,6 +174,7 @@ class NewChatWidget extends Disposable { const isLocal = target === AgentSessionProviders.Background; this._isolationModePicker.setVisible(isLocal); this._branchPicker.setVisible(isLocal); + this._syncIndicator.setVisible(isLocal); this._focusEditor(); })); @@ -179,7 +183,8 @@ class NewChatWidget extends Disposable { this._updateInputLoadingState(); })); - this._register(this._branchPicker.onDidChange(() => { + this._register(this._branchPicker.onDidChange((branch) => { + this._syncIndicator.setBranch(branch); this._focusEditor(); })); @@ -239,11 +244,13 @@ class NewChatWidget extends Disposable { dom.append(isolationContainer, dom.$('.sessions-chat-local-mode-spacer')); const branchContainer = dom.append(isolationContainer, dom.$('.sessions-chat-local-mode-right')); this._branchPicker.render(branchContainer); + this._syncIndicator.render(branchContainer); // Set initial visibility based on default target const isLocal = this._targetPicker.selectedTarget === AgentSessionProviders.Background; this._isolationModePicker.setVisible(isLocal); this._branchPicker.setVisible(isLocal); + this._syncIndicator.setVisible(isLocal); // Render target buttons & extension pickers this._renderOptionGroupPickers(); @@ -335,6 +342,7 @@ class NewChatWidget extends Disposable { this._updateInputLoadingState(); this._branchPicker.setRepository(undefined); this._isolationModePicker.setRepository(undefined); + this._syncIndicator.setRepository(undefined); this.gitService.openRepository(folderUri).then(repository => { if (cts.token.isCancellationRequested) { @@ -344,6 +352,7 @@ class NewChatWidget extends Disposable { this._updateInputLoadingState(); this._isolationModePicker.setRepository(repository); this._branchPicker.setRepository(repository); + this._syncIndicator.setRepository(repository); }).catch(e => { if (cts.token.isCancellationRequested) { return; @@ -353,6 +362,7 @@ class NewChatWidget extends Disposable { this._updateInputLoadingState(); this._isolationModePicker.setRepository(undefined); this._branchPicker.setRepository(undefined); + this._syncIndicator.setRepository(undefined); }); } diff --git a/src/vs/sessions/contrib/chat/browser/syncIndicator.ts b/src/vs/sessions/contrib/chat/browser/syncIndicator.ts new file mode 100644 index 0000000000000..3480f68166b2a --- /dev/null +++ b/src/vs/sessions/contrib/chat/browser/syncIndicator.ts @@ -0,0 +1,163 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { localize } from '../../../../nls.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IGitRepository } from '../../../../workbench/contrib/git/common/gitService.js'; + +const GIT_SYNC_COMMAND = 'git.sync'; + +/** + * Renders a compact "Synchronize Changes" button next to the branch picker. + * Shows ahead/behind counts (e.g. "3↓ 2↑") and is only visible when + * the selected branch matches the repository HEAD and has changes to sync. + */ +export class SyncIndicator extends Disposable { + + private _repository: IGitRepository | undefined; + private _selectedBranch: string | undefined; + private _visible = true; + + private readonly _renderDisposables = this._register(new DisposableStore()); + private readonly _stateDisposables = this._register(new DisposableStore()); + + private _slotElement: HTMLElement | undefined; + private _buttonElement: HTMLElement | undefined; + + constructor( + @ICommandService private readonly commandService: ICommandService, + ) { + super(); + } + + /** + * Sets the git repository. Subscribes to its state observable to react to + * ahead/behind changes. + */ + setRepository(repository: IGitRepository | undefined): void { + this._stateDisposables.clear(); + this._repository = repository; + + if (repository) { + this._stateDisposables.add(autorun(reader => { + repository.state.read(reader); + this._update(); + })); + } else { + this._update(); + } + } + + /** + * Sets the currently selected branch name (from the branch picker). + * The sync indicator is only shown when the selected branch is the HEAD branch. + */ + setBranch(branch: string | undefined): void { + this._selectedBranch = branch; + this._update(); + } + + /** + * Renders the sync indicator button into the given container. + */ + render(container: HTMLElement): void { + this._renderDisposables.clear(); + + const slot = dom.append(container, dom.$('.sessions-chat-picker-slot.sessions-chat-sync-indicator')); + this._slotElement = slot; + this._renderDisposables.add({ dispose: () => slot.remove() }); + + const button = dom.append(slot, dom.$('a.action-label')); + button.tabIndex = 0; + button.role = 'button'; + this._buttonElement = button; + + this._renderDisposables.add(dom.addDisposableListener(button, dom.EventType.CLICK, (e) => { + dom.EventHelper.stop(e, true); + this.commandService.executeCommand(GIT_SYNC_COMMAND); + })); + + this._renderDisposables.add(dom.addDisposableListener(button, dom.EventType.KEY_DOWN, (e) => { + if (e.key === 'Enter' || e.key === ' ') { + dom.EventHelper.stop(e, true); + this.commandService.executeCommand(GIT_SYNC_COMMAND); + } + })); + + this._update(); + } + + /** + * Shows or hides the sync indicator slot. + */ + setVisible(visible: boolean): void { + this._visible = visible; + this._update(); + } + + private _getAheadBehind(): { ahead: number; behind: number } | undefined { + if (!this._repository) { + return undefined; + } + + const head = this._repository.state.get().HEAD; + if (!head?.upstream) { + return undefined; + } + + // Only show sync for the HEAD branch (i.e. the selected branch must match the actual HEAD) + if (head.name !== this._selectedBranch) { + return undefined; + } + + const ahead = head.ahead ?? 0; + const behind = head.behind ?? 0; + if (ahead === 0 && behind === 0) { + return undefined; + } + + return { ahead, behind }; + } + + private _update(): void { + if (!this._slotElement || !this._buttonElement) { + return; + } + + const counts = this._getAheadBehind(); + if (!counts || !this._visible) { + this._slotElement.style.display = 'none'; + return; + } + + this._slotElement.style.display = ''; + + dom.clearNode(this._buttonElement); + dom.append(this._buttonElement, renderIcon(Codicon.sync)); + + const parts: string[] = []; + if (counts.behind > 0) { + parts.push(`${counts.behind}↓`); + } + if (counts.ahead > 0) { + parts.push(`${counts.ahead}↑`); + } + + const label = dom.append(this._buttonElement, dom.$('span.sessions-chat-dropdown-label')); + label.textContent = parts.join('\u00a0'); + + this._buttonElement.title = localize( + 'syncIndicator.tooltip', + "Synchronize Changes ({0} to pull, {1} to push)", + counts.behind, + counts.ahead, + ); + } +} From 4fcfd36271ba48f4f28da8e25873c7dbb324010e Mon Sep 17 00:00:00 2001 From: Isidor Date: Thu, 26 Feb 2026 11:39:44 +0100 Subject: [PATCH 04/28] fixes #288433 --- .../browser/actions/chatAccessibilityHelp.ts | 1 + .../browser/chatEditing/chatEditingActions.ts | 50 +++++++++++++++++++ .../contrib/chat/browser/chatTipService.ts | 2 +- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index a2a71e17376aa..b21a1e21048d5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -115,6 +115,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'age } content.push(localize('chatEditing.helpfulCommands', 'Some helpful commands include:')); content.push(localize('workbench.action.chat.undoEdits', '- Undo Edits{0}.', '')); + content.push(localize('workbench.action.chat.restoreLastCheckpoint', '- Restore to Last Checkpoint{0}.', '')); content.push(localize('workbench.action.chat.editing.attachFiles', '- Attach Files{0}.', '')); content.push(localize('chatEditing.removeFileFromWorkingSet', '- Remove File from Working Set{0}.', '')); content.push(localize('chatEditing.acceptFile', '- Keep{0} and Undo File{1}.', '', '')); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 488358e0c902e..460a79c2b5716 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { alert } from '../../../../../base/browser/ui/aria/aria.js'; import { basename } from '../../../../../base/common/resources.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; @@ -598,6 +599,55 @@ registerAction2(class RestoreCheckpointAction extends Action2 { } }); +registerAction2(class RestoreLastCheckpoint extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.restoreLastCheckpoint', + title: localize2('chat.restoreLastCheckpoint.label', "Restore to Last Checkpoint"), + f1: true, + category: CHAT_CATEGORY, + icon: Codicon.discard, + precondition: ContextKeyExpr.and( + ChatContextKeys.inChatSession, + ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, true), + ChatContextKeys.lockedToCodingAgent.negate() + ) + }); + } + + async run(accessor: ServicesAccessor, ...args: unknown[]) { + let item = args[0] as ChatTreeItem | undefined; + const chatWidgetService = accessor.get(IChatWidgetService); + const chatService = accessor.get(IChatService); + const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget; + if (!isResponseVM(item) && !isRequestVM(item)) { + item = widget?.getFocus(); + } + + const sessionResource = widget?.viewModel?.sessionResource ?? (isChatTreeItem(item) ? item.sessionResource : undefined); + if (!sessionResource) { + return; + } + + const chatModel = chatService.getSession(sessionResource); + if (!chatModel?.editingSession) { + return; + } + + const checkpointRequest = chatModel.checkpoint; + if (!checkpointRequest) { + alert(localize('chat.restoreCheckpoint.none', 'There is no checkpoint to restore.')); + return; + } + + widget?.viewModel?.model.setCheckpoint(checkpointRequest.id); + widget?.focusInput(); + widget?.input.setValue(checkpointRequest.message.text, false); + + await restoreSnapshotWithConfirmationByRequestId(accessor, sessionResource, checkpointRequest.id); + } +}); + registerAction2(class EditAction extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index c830a665e7a22..e4c5387b9c5a3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -285,7 +285,7 @@ const TIP_CATALOG: ITipDefinition[] = [ ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Edit), ), ), - excludeWhenCommandsExecuted: ['workbench.action.chat.restoreCheckpoint'], + excludeWhenCommandsExecuted: ['workbench.action.chat.restoreCheckpoint', 'workbench.action.chat.restoreLastCheckpoint'], }, { id: 'tip.messageQueueing', From 248b17d1e8c850f71e503cca2a628eb18cf69e7c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 26 Feb 2026 11:58:14 +0100 Subject: [PATCH 05/28] sessions - allow `--sessions` even for existing windows (#297955) * feat - add product service to launch main service * refactor - rearrange sessions window logic in `startOpenWindow` --- src/vs/platform/launch/electron-main/launchMainService.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index db2fd75d13495..274600742e45a 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -18,6 +18,7 @@ import { ICodeWindow } from '../../window/electron-main/window.js'; import { IWindowSettings } from '../../window/common/window.js'; import { IOpenConfiguration, IWindowsMainService, OpenContext } from '../../windows/electron-main/windows.js'; import { IProtocolUrl } from '../../url/electron-main/url.js'; +import { IProductService } from '../../product/common/productService.js'; export const ID = 'launchMainService'; export const ILaunchMainService = createDecorator(ID); @@ -45,6 +46,7 @@ export class LaunchMainService implements ILaunchMainService { @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IURLService private readonly urlService: IURLService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IProductService private readonly productService: IProductService, ) { } async start(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { @@ -111,6 +113,7 @@ export class LaunchMainService implements ILaunchMainService { private async startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { const context = isLaunchedFromCli(userEnv) ? OpenContext.CLI : OpenContext.DESKTOP; + let usedWindows: ICodeWindow[] = []; const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined; @@ -142,6 +145,11 @@ export class LaunchMainService implements ILaunchMainService { await this.windowsMainService.openExtensionDevelopmentHostWindow(args.extensionDevelopmentPath, baseConfig); } + // Sessions window + else if (args['sessions'] && this.productService.quality !== 'stable') { + usedWindows = await this.windowsMainService.openSessionsWindow({ context, contextWindowId: undefined }); + } + // Start without file/folder arguments else if (!args._.length && !args['folder-uri'] && !args['file-uri']) { let openNewWindow = false; From b75d9899e6eee69636ff454b562d05361206e336 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 26 Feb 2026 11:41:35 +0000 Subject: [PATCH 06/28] feat: enhance dropdown and sticky widget styles with improved shadows and backgrounds --- extensions/theme-2026/themes/styles.css | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 14e1bb6cb1020..c8bce9798b2fd 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -289,6 +289,10 @@ border-radius: var(--radius-lg); } +.monaco-workbench .monaco-select-box-dropdown-container { + box-shadow: var(--shadow-lg); +} + .monaco-workbench .monaco-menu-container > .monaco-scrollable-element { border-radius: var(--radius-lg) !important; box-shadow: var(--shadow-lg) !important; @@ -588,11 +592,16 @@ border-bottom: none !important; } -.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-line-numbers, +.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-lines-scrollable { + background: var(--vscode-editor-background) !important +} + .monaco-editor .sticky-widget .sticky-line-content { - backdrop-filter: var(--backdrop-blur-md) !important; - -webkit-backdrop-filter: var(--backdrop-blur-md) !important; - background: color-mix(in srgb, var(--vscode-editor-background) 40%, transparent) !important; + background: var(--vscode-editor-background) !important +} + +.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-line-numbers { + background: var(--vscode-editor-background) !important; } .monaco-workbench .monaco-editor .sticky-widget .sticky-line-content:hover { From 4aff97add0f297ad97cb6b7de1097e91ac12e419 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 26 Feb 2026 03:43:17 -0800 Subject: [PATCH 07/28] Move status entry to the right and make it more subtle (#297865) --- .../update/browser/media/updateStatusBarEntry.css | 5 +++++ .../update/browser/updateStatusBarEntry.ts | 15 ++++----------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/update/browser/media/updateStatusBarEntry.css b/src/vs/workbench/contrib/update/browser/media/updateStatusBarEntry.css index a35bad5f4696a..ee233981af0b6 100644 --- a/src/vs/workbench/contrib/update/browser/media/updateStatusBarEntry.css +++ b/src/vs/workbench/contrib/update/browser/media/updateStatusBarEntry.css @@ -3,6 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-workbench .part.statusbar > .items-container > #status\.update > a > span { + color: var(--vscode-button-background); + font-size: 16px; +} + .update-status-tooltip { display: flex; flex-direction: column; diff --git a/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts b/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts index 3d2d6d1c8e0bd..baf84977f90a6 100644 --- a/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts +++ b/src/vs/workbench/contrib/update/browser/updateStatusBarEntry.ts @@ -77,7 +77,6 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor return; } - const productIcon = this.productService.quality === 'insider' ? '$(vscode-insiders)' : '$(vscode)'; switch (state.type) { case StateType.Uninitialized: case StateType.Idle: @@ -97,9 +96,8 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor case StateType.AvailableForDownload: this.updateStatusBarEntry({ - kind: 'prominent', name: UpdateStatusBarEntryContribution.NAME, - text: nls.localize('updateStatus.updateAvailableStatus', "{0} Update available, click to download.", productIcon), + text: nls.localize('updateStatus.updateAvailableStatus', "$(circle-filled) Update available, click to download."), ariaLabel: nls.localize('updateStatus.updateAvailableAria', "Update available, click to download."), tooltip: this.getAvailableTooltip(state.update), command: 'update.downloadNow' @@ -118,9 +116,8 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor case StateType.Downloaded: this.updateStatusBarEntry({ - kind: 'prominent', name: UpdateStatusBarEntryContribution.NAME, - text: nls.localize('updateStatus.updateReadyStatus', "{0} Update downloaded, click to install.", productIcon), + text: nls.localize('updateStatus.updateReadyStatus', "$(circle-filled) Update downloaded, click to install."), ariaLabel: nls.localize('updateStatus.updateReadyAria', "Update downloaded, click to install."), tooltip: this.getReadyToInstallTooltip(state.update), command: 'update.install' @@ -140,9 +137,8 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor case StateType.Ready: { this.updateStatusBarEntry({ - kind: 'prominent', name: UpdateStatusBarEntryContribution.NAME, - text: nls.localize('updateStatus.restartToUpdateStatus', "{0} Update is ready, click to restart.", productIcon), + text: nls.localize('updateStatus.restartToUpdateStatus', "$(circle-filled) Update is ready, click to restart."), ariaLabel: nls.localize('updateStatus.restartToUpdateAria', "Update is ready, click to restart."), tooltip: this.getRestartToUpdateTooltip(state.update), command: 'update.restart' @@ -170,10 +166,7 @@ export class UpdateStatusBarEntryContribution extends Disposable implements IWor entry, 'status.update', StatusbarAlignment.LEFT, - { - location: { id: 'status.host', priority: Number.MAX_VALUE }, - alignment: StatusbarAlignment.LEFT - } + -Number.MAX_VALUE ); } } From 9c4cc0ea63030fa327cdc1f42f6172de1c7f3640 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 26 Feb 2026 12:43:37 +0100 Subject: [PATCH 08/28] sessions - hide title bar actions when welcome overlay shows (#297958) feat - add `SessionsWelcomeVisibleContext` for overlays --- src/vs/sessions/browser/layoutActions.ts | 6 ++++-- src/vs/sessions/common/contextkeys.ts | 6 ++++++ src/vs/sessions/contrib/chat/browser/chat.contribution.ts | 4 +++- src/vs/sessions/contrib/chat/browser/runScriptAction.ts | 3 ++- .../contrib/sessions/browser/sessionsTitleBarWidget.ts | 4 +++- .../terminal/browser/sessionsTerminalContribution.ts | 4 +++- .../contrib/welcome/browser/welcome.contribution.ts | 8 ++++++++ 7 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/vs/sessions/browser/layoutActions.ts b/src/vs/sessions/browser/layoutActions.ts index 39f7697ed7280..cd1f588050490 100644 --- a/src/vs/sessions/browser/layoutActions.ts +++ b/src/vs/sessions/browser/layoutActions.ts @@ -9,12 +9,14 @@ import { KeyCode, KeyMod } from '../../base/common/keyCodes.js'; import { localize, localize2 } from '../../nls.js'; import { Categories } from '../../platform/action/common/actionCommonCategories.js'; import { Action2, MenuRegistry, registerAction2 } from '../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../platform/contextkey/common/contextkey.js'; import { Menus } from './menus.js'; import { ServicesAccessor } from '../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../platform/keybinding/common/keybindingsRegistry.js'; import { registerIcon } from '../../platform/theme/common/iconRegistry.js'; import { AuxiliaryBarVisibleContext, IsAuxiliaryWindowContext, IsWindowAlwaysOnTopContext, SideBarVisibleContext } from '../../workbench/common/contextkeys.js'; import { IWorkbenchLayoutService, Parts } from '../../workbench/services/layout/browser/layoutService.js'; +import { SessionsWelcomeVisibleContext } from '../common/contextkeys.js'; // Register Icons const panelLeftIcon = registerIcon('agent-panel-left', Codicon.layoutSidebarLeft, localize('panelLeft', "Represents a side bar in the left position")); @@ -53,7 +55,7 @@ class ToggleSidebarVisibilityAction extends Action2 { id: Menus.TitleBarLeft, group: 'navigation', order: 0, - when: IsAuxiliaryWindowContext.toNegated() + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) }, { id: Menus.TitleBarContext, @@ -105,7 +107,7 @@ class ToggleSecondarySidebarVisibilityAction extends Action2 { id: Menus.TitleBarRight, group: 'navigation', order: 10, - when: IsAuxiliaryWindowContext.toNegated() + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) }, { id: Menus.TitleBarContext, diff --git a/src/vs/sessions/common/contextkeys.ts b/src/vs/sessions/common/contextkeys.ts index f07b24f2ff607..76d7136d14c5a 100644 --- a/src/vs/sessions/common/contextkeys.ts +++ b/src/vs/sessions/common/contextkeys.ts @@ -13,3 +13,9 @@ export const ChatBarFocusContext = new RawContextKey('chatBarFocus', fa export const ChatBarVisibleContext = new RawContextKey('chatBarVisible', false, localize('chatBarVisible', "Whether the chat bar is visible")); //#endregion + +//#region < --- Welcome --- > + +export const SessionsWelcomeVisibleContext = new RawContextKey('sessionsWelcomeVisible', false, localize('sessionsWelcomeVisible', "Whether the sessions welcome overlay is visible")); + +//#endregion diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index b7879423464a0..165307b8a3e98 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -37,6 +37,8 @@ import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/vie import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { ChatViewPane } from '../../../../workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; export class OpenSessionWorktreeInVSCodeAction extends Action2 { static readonly ID = 'chat.openSessionWorktreeInVSCode'; @@ -50,7 +52,7 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 { id: Menus.TitleBarRight, group: 'navigation', order: 10, - when: IsAuxiliaryWindowContext.toNegated() + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) }] }); } diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index 8bfb9f7bc6d01..bacdbac4330b8 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -17,6 +17,7 @@ import { Menus } from '../../../browser/menus.js'; import { ISessionsConfigurationService, ITaskEntry, TaskStorageTarget } from './sessionsConfigurationService.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; +import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; @@ -291,5 +292,5 @@ MenuRegistry.appendMenuItem(Menus.TitleBarRight, { icon: Codicon.play, group: 'navigation', order: 8, - when: IsAuxiliaryWindowContext.toNegated() + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) }); diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index ec1d688fa414b..8f57869ab1c12 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -13,6 +13,7 @@ import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hover import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { MenuRegistry, SubmenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { Menus } from '../../../browser/menus.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; @@ -31,6 +32,7 @@ import { basename } from '../../../../base/common/resources.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ViewAllSessionChangesAction } from '../../../../workbench/contrib/chat/browser/chatEditing/chatEditingActions.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; +import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; /** * Sessions Title Bar Widget - renders the active chat session title @@ -375,7 +377,7 @@ export class SessionsTitleBarContribution extends Disposable implements IWorkben submenu: Menus.TitleBarControlMenu, title: localize('agentSessionsControl', "Agent Sessions"), order: 101, - when: IsAuxiliaryWindowContext.negate() + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.negate(), SessionsWelcomeVisibleContext.negate()) })); // Register a placeholder action so the submenu appears diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index ff97b6ae70c44..de2276a85a95a 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -20,6 +20,8 @@ import { IPathService } from '../../../../workbench/services/path/common/pathSer import { Menus } from '../../../browser/menus.js'; import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; /** * Returns the cwd URI for the given session: worktree for non-cloud agent @@ -147,7 +149,7 @@ class OpenSessionInTerminalAction extends Action2 { id: Menus.TitleBarRight, group: 'navigation', order: 9, - when: IsAuxiliaryWindowContext.toNegated() + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) }] }); } diff --git a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts index 2addf1c60a961..6a9a7d29a481e 100644 --- a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts +++ b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts @@ -5,6 +5,7 @@ import './media/welcomeOverlay.css'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; import { $, append } from '../../../../base/browser/dom.js'; import { autorun } from '../../../../base/common/observable.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -24,6 +25,7 @@ import { IInstantiationService, ServicesAccessor } from '../../../../platform/in import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; const WELCOME_COMPLETE_KEY = 'workbench.agentsession.welcomeComplete'; @@ -134,6 +136,7 @@ class SessionsWelcomeContribution extends Disposable implements IWorkbenchContri @IInstantiationService private readonly instantiationService: IInstantiationService, @IProductService private readonly productService: IProductService, @IStorageService private readonly storageService: IStorageService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); @@ -196,6 +199,11 @@ class SessionsWelcomeContribution extends Disposable implements IWorkbenchContri this.watcherRef.clear(); this.overlayRef.value = new DisposableStore(); + // Mark the welcome overlay as visible for titlebar disabling + const welcomeVisibleKey = SessionsWelcomeVisibleContext.bindTo(this.contextKeyService); + welcomeVisibleKey.set(true); + this.overlayRef.value.add(toDisposable(() => welcomeVisibleKey.reset())); + const overlay = this.overlayRef.value.add(this.instantiationService.createInstance( SessionsWelcomeOverlay, this.layoutService.mainContainer, From 664081c20a9bdb09ee5f610968e80721ab7bbb21 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 26 Feb 2026 12:47:49 +0100 Subject: [PATCH 09/28] Revert "sessions - show file changes summary in changes window" (#297966) Revert "sessions - show file changes summary in changes window (#297656)" This reverts commit b9a94fe6d8aa25b92023b3ae926179897a372f93. --- .../contrib/chat/browser/widget/chatListRenderer.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 87681d808cee1..1cf8b39f364e6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1406,15 +1406,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('chat.checkpoints.showFileChanges'); + return element.isComplete && isLocalSession && this.configService.getValue('chat.checkpoints.showFileChanges'); } private getDataForProgressiveRender(element: IChatResponseViewModel) { From edd359cb75461c31b5940889d3f67c07b4671d4e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 26 Feb 2026 12:48:59 +0100 Subject: [PATCH 10/28] stop the log spam, I cannot read the console anymore (#297964) --- src/vs/platform/hover/browser/hoverService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/hover/browser/hoverService.ts b/src/vs/platform/hover/browser/hoverService.ts index 116bfe0824cc4..6779e7e1c5e79 100644 --- a/src/vs/platform/hover/browser/hoverService.ts +++ b/src/vs/platform/hover/browser/hoverService.ts @@ -556,7 +556,7 @@ export class HoverService extends Disposable implements IHoverService { if (targetElement.title !== '') { console.warn('HTML element already has a title attribute, which will conflict with the custom hover. Please remove the title attribute.'); - console.trace('Stack trace:', targetElement.title); + // console.trace('Stack trace:', targetElement.title); targetElement.title = ''; } From 494df680c8e9433efc1dc937f33eb36be439f373 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 26 Feb 2026 11:54:27 +0000 Subject: [PATCH 11/28] fix: add missing semicolon in sticky widget background style --- extensions/theme-2026/themes/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index c8bce9798b2fd..a91a2a3919119 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -593,7 +593,7 @@ } .monaco-workbench .monaco-editor .sticky-widget .sticky-widget-lines-scrollable { - background: var(--vscode-editor-background) !important + background: var(--vscode-editor-background) !important; } .monaco-editor .sticky-widget .sticky-line-content { From 0d6dd5d5d5271b1a6cadddc051c689c4fb293b3c Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 26 Feb 2026 11:54:43 +0000 Subject: [PATCH 12/28] fix: add missing semicolon in sticky widget background style --- extensions/theme-2026/themes/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index a91a2a3919119..11bd6ee590bf1 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -597,7 +597,7 @@ } .monaco-editor .sticky-widget .sticky-line-content { - background: var(--vscode-editor-background) !important + background: var(--vscode-editor-background) !important; } .monaco-workbench .monaco-editor .sticky-widget .sticky-widget-line-numbers { From 7a56e5b8b0d8cea50a9dd7fd3b834be98a5efab2 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 26 Feb 2026 11:59:21 +0000 Subject: [PATCH 13/28] Add border radius to debug view title for improved aesthetics --- src/vs/workbench/contrib/debug/browser/media/debugViewlet.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index f6ca5b3900c8d..4a627af1f786c 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -57,6 +57,7 @@ line-height: inherit; padding-top: 0; padding-bottom: 0; + border-radius: 0 var(--vscode-cornerRadius-small) var(--vscode-cornerRadius-small) 0; /* The debug view title is crowded, let this one get narrower than others */ min-width: 90px; From 9057f9274f4a8f5afcf4eb7e0ff633044f6996b9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 26 Feb 2026 13:02:46 +0100 Subject: [PATCH 14/28] sessions - fix icon alignment vertically (#297970) --- .../chat/browser/agentSessions/media/agentsessionsviewer.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 84a06b2877e8e..3a0329d0f3ce0 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -99,8 +99,7 @@ .agent-session-icon { flex-shrink: 0; font-size: 12px; - width: 14px; - height: 14px; + height: 16px; display: flex; align-items: center; justify-content: center; From dd2fdf50c9ef56468c63f6967e6f6a42897ea40f Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 26 Feb 2026 13:08:26 +0100 Subject: [PATCH 15/28] fix active session when changing from untitled to committed session (#297969) --- .../browser/sessionsManagementService.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index e8e21d75fed71..7ba297981dd98 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -39,6 +39,7 @@ const repositoryOptionId = 'repository'; */ export interface IActiveSessionItem { readonly resource: URI; + readonly isUntitled: boolean; readonly label: string | undefined; readonly repository: URI | undefined; readonly worktree: URI | undefined; @@ -151,10 +152,19 @@ export class SessionsManagementService extends Disposable implements ISessionsMa const agentSession = this.agentSessionsService.model.getSession(currentActive.resource); if (!agentSession) { - // Only switch sessions if the active session was a known agent session - // that got deleted. New session resources that aren't yet in the model - // should not trigger a switch. - if (isAgentSession(currentActive)) { + if (currentActive.isUntitled) { + // The untitled session was committed by the extension via + // onDidCommitChatSessionItem, which replaces the untitled + // resource with a new committed resource. The commit handler + // already swapped the ChatViewPane widget to the new resource, + // so find it by checking the widget's current session resource. + const chatViewWidgets = this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat); + const committedResource = chatViewWidgets[0]?.viewModel?.sessionResource; + const committedSession = committedResource ? this.agentSessionsService.model.getSession(committedResource) : undefined; + if (committedSession) { + this.setActiveSession(committedSession); + } + } else { this.showNextSession(); } return; @@ -397,6 +407,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this.lastSelectedSession = session.resource; const [repository, worktree] = this.getRepositoryFromMetadata(session.metadata); activeSessionItem = { + isUntitled: this.chatService.getSession(session.resource)?.contributedChatSession?.isUntitled ?? true, label: session.label, resource: session.resource, repository, @@ -404,6 +415,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa }; } else { activeSessionItem = { + isUntitled: true, label: undefined, resource: session.resource, repository: session.repoUri, @@ -412,6 +424,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this._activeSessionDisposables.add(session.onDidChange(e => { if (e === 'repoUri') { this._activeSession.set({ + isUntitled: true, label: undefined, resource: session.resource, repository: session.repoUri, From bd9ab3032d96618368a2f26c231dc8ee632210bb Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:59:54 +0100 Subject: [PATCH 16/28] Enhance feedback and comment renderer with deletion and markdown support (#297984) * feat: enhance agent feedback hover and chat attachment widgets with deletion support * fix: set title for truncated text in read-only mode in feedback comment renderer * feat: enhance comment renderer with rich markdown hover support in read-only mode * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/agentFeedbackAttachmentWidget.ts | 2 +- .../browser/agentFeedbackHover.ts | 127 +++++++++++++----- .../chatAttachmentWidgetRegistry.ts | 2 + .../chatAttachmentsContentPart.ts | 7 +- 4 files changed, 101 insertions(+), 37 deletions(-) diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachmentWidget.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachmentWidget.ts index fb2b68188e315..33a887638e83f 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachmentWidget.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachmentWidget.ts @@ -73,6 +73,6 @@ export class AgentFeedbackAttachmentWidget extends Disposable { this.element.ariaLabel = localize('chat.agentFeedback', "Attached agent feedback, {0}", this._attachment.name); // Custom interactive hover - this._store.add(this._instantiationService.createInstance(AgentFeedbackHover, this.element, this._attachment)); + this._store.add(this._instantiationService.createInstance(AgentFeedbackHover, this.element, this._attachment, options.supportsDeletion)); } } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts index d02c9725cd1b8..9b38ad3c63bb8 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts @@ -11,11 +11,14 @@ import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { IObjectTreeElement, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js'; import { Action } from '../../../../base/common/actions.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { basename } from '../../../../base/common/path.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { URI } from '../../../../base/common/uri.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; import { localize } from '../../../../nls.js'; import { FileKind } from '../../../../platform/files/common/files.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; @@ -78,7 +81,7 @@ class FeedbackFileRenderer implements ITreeRenderer { - for (const item of element.items) { - this._agentFeedbackService.removeFeedback(this._sessionResource, item.id); + if (this._agentFeedbackService) { + const service = this._agentFeedbackService; + const sessionResource = this._sessionResource; + templateData.actionBar.push(new Action( + 'agentFeedback.removeFileComments', + localize('agentFeedbackHover.removeAll', "Remove All"), + ThemeIcon.asClassName(Codicon.close), + true, + () => { + for (const item of element.items) { + service.removeFeedback(sessionResource, item.id); + } } - } - ), { icon: true, label: false }); + ), { icon: true, label: false }); + } } disposeTemplate(templateData: IFeedbackFileTemplate): void { @@ -129,8 +136,10 @@ class FeedbackFileRenderer implements ITreeRenderer; element: IFeedbackCommentElement | undefined; } @@ -139,8 +148,11 @@ class FeedbackCommentRenderer implements ITreeRenderer { - const data = templateData.element; - if (data) { - e.preventDefault(); - e.stopPropagation(); - this._agentFeedbackService.revealFeedback(this._sessionResource, data.id); - } - })); + const templateData: IFeedbackCommentTemplate = { textElement, row, actionBar, templateDisposables, hoverDisposable, element: undefined }; + + if (this._agentFeedbackService) { + const service = this._agentFeedbackService; + const sessionResource = this._sessionResource; + templateDisposables.add(dom.addDisposableListener(row, dom.EventType.CLICK, (e) => { + const data = templateData.element; + if (data) { + e.preventDefault(); + e.stopPropagation(); + service.revealFeedback(sessionResource, data.id); + } + })); + } return templateData; } @@ -173,21 +191,58 @@ class FeedbackCommentRenderer implements ITreeRenderer this._buildCommentHover(element), + { groupId: 'agent-feedback-comment' } + ); + } + templateData.actionBar.clear(); - templateData.actionBar.push(new Action( - 'agentFeedback.removeComment', - localize('agentFeedbackHover.remove', "Remove"), - ThemeIcon.asClassName(Codicon.close), - true, - () => { - this._agentFeedbackService.removeFeedback(this._sessionResource, element.id); - } - ), { icon: true, label: false }); + if (this._agentFeedbackService) { + const service = this._agentFeedbackService; + const sessionResource = this._sessionResource; + templateData.actionBar.push(new Action( + 'agentFeedback.removeComment', + localize('agentFeedbackHover.remove', "Remove"), + ThemeIcon.asClassName(Codicon.close), + true, + () => { + service.removeFeedback(sessionResource, element.id); + } + ), { icon: true, label: false }); + } } disposeTemplate(templateData: IFeedbackCommentTemplate): void { templateData.templateDisposables.dispose(); } + + private _buildCommentHover(element: IFeedbackCommentElement): IDelayedHoverOptions { + const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); + markdown.appendText(element.text); + + // Try to get the code snippet synchronously from already-loaded models + const model = this._modelService.getModel(element.resourceUri); + if (model) { + const snippet = model.getValueInRange(element.range); + if (snippet) { + const languageId = this._languageService.guessLanguageIdByFilepathOrFirstLine(element.resourceUri); + markdown.appendMarkdown('\n\n'); + markdown.appendCodeblock(languageId ?? '', snippet); + } + } + + return { + content: markdown, + style: HoverStyle.Pointer, + position: { + hoverPosition: HoverPosition.RIGHT, + }, + }; + } } // --- Hover --- @@ -202,16 +257,19 @@ export class AgentFeedbackHover extends Disposable { constructor( private readonly _element: HTMLElement, private readonly _attachment: IAgentFeedbackVariableEntry, + private readonly _canDelete: boolean, @IHoverService private readonly _hoverService: IHoverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, + @IModelService private readonly _modelService: IModelService, + @ILanguageService private readonly _languageService: ILanguageService, ) { super(); // Show on hover (delayed) this._store.add(this._hoverService.setupDelayedHover( this._element, - () => this._store.add(this._buildHoverContent()), // needs a better disposable story + () => this._store.add(this._buildHoverContent()), { groupId: 'chat-attachments' } )); @@ -252,8 +310,8 @@ export class AgentFeedbackHover extends Disposable { treeContainer, new FeedbackTreeDelegate(), [ - new FeedbackFileRenderer(resourceLabels, this._agentFeedbackService, this._attachment.sessionResource), - new FeedbackCommentRenderer(this._agentFeedbackService, this._attachment.sessionResource), + new FeedbackFileRenderer(resourceLabels, this._canDelete ? this._agentFeedbackService : undefined, this._attachment.sessionResource), + new FeedbackCommentRenderer(this._canDelete ? this._agentFeedbackService : undefined, this._attachment.sessionResource, this._hoverService, this._modelService, this._languageService), ], { defaultIndent: 0, @@ -313,6 +371,7 @@ export class AgentFeedbackHover extends Disposable { private _buildTreeData(): { children: IObjectTreeElement[]; commentElements: IFeedbackCommentElement[] } { // Group feedback items by file const byFile = new Map(); + for (const item of this._attachment.feedbackItems) { const key = item.resourceUri.toString(); let group = byFile.get(key); diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts index e4cb7c0fc57e6..404a3ae6bc877 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.ts @@ -15,6 +15,8 @@ export interface IChatAttachmentWidgetInstance extends IDisposable { readonly element: HTMLElement; readonly onDidDelete: event.Event; readonly onDidOpen: event.Event; + /** Optional label element, used for applying warning styles on omitted attachments. */ + readonly label?: { readonly element: HTMLElement }; } /** diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts index 92a5a84d9af2a..6cff3c02973ae 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts @@ -14,6 +14,7 @@ import { ResourceLabels } from '../../../../../browser/labels.js'; import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isTerminalVariableEntry, isWorkspaceVariableEntry, OmittedState } from '../../../common/attachments/chatVariableEntries.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../../common/chatService/chatService.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../../attachments/chatAttachmentWidgets.js'; +import { IChatAttachmentWidgetRegistry } from '../../attachments/chatAttachmentWidgetRegistry.js'; export interface IChatAttachmentsContentPartOptions { readonly variables: readonly IChatRequestVariableEntry[]; @@ -39,6 +40,7 @@ export class ChatAttachmentsContentPart extends Disposable { constructor( options: IChatAttachmentsContentPartOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatAttachmentWidgetRegistry private readonly chatAttachmentWidgetRegistry: IChatAttachmentWidgetRegistry, ) { super(); this._variables = options.variables; @@ -167,7 +169,8 @@ export class ChatAttachmentsContentPart extends Disposable { // skip workspace attachments return; } else { - widget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels); + widget = this.chatAttachmentWidgetRegistry.createWidget(attachment, { shouldFocusClearButton: false, supportsDeletion: false }, container) + ?? this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels); } let ariaLabel: string | null = null; @@ -180,7 +183,7 @@ export class ChatAttachmentsContentPart extends Disposable { ariaLabel = `${ariaLabel}${description ? ` ${description}` : ''}`; for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { // eslint-disable-next-line no-restricted-syntax - const element = widget.label.element.querySelector(selector); + const element = widget.label?.element.querySelector(selector); if (element) { element.classList.add('warning'); } From e30f28558f3f78c15ec5da40e3e149050ea3bf3c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:59:13 +0100 Subject: [PATCH 17/28] feat: add clone repository option to folder picker (#297995) --- extensions/git/src/commands.ts | 4 ++-- .../contrib/chat/browser/folderPicker.ts | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index f1456675f2e61..5c93d8f7a1ffb 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1028,8 +1028,8 @@ export class CommandCenter { } @command('git.clone') - async clone(url?: string, parentPath?: string, options?: { ref?: string }): Promise { - await this.cloneManager.clone(url, { parentPath, ...options }); + async clone(url?: string, parentPath?: string, options?: { ref?: string; postCloneAction?: 'none' }): Promise { + return this.cloneManager.clone(url, { parentPath, ...options }); } @command('git.cloneRecursive') diff --git a/src/vs/sessions/contrib/chat/browser/folderPicker.ts b/src/vs/sessions/contrib/chat/browser/folderPicker.ts index a83b40962a9f1..22e53aa45654f 100644 --- a/src/vs/sessions/contrib/chat/browser/folderPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/folderPicker.ts @@ -12,6 +12,7 @@ import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; @@ -63,6 +64,7 @@ export class FolderPicker extends Disposable { @IStorageService private readonly storageService: IStorageService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IFileDialogService private readonly fileDialogService: IFileDialogService, + @ICommandService private readonly commandService: ICommandService, ) { super(); @@ -131,6 +133,8 @@ export class FolderPicker extends Disposable { this.actionWidgetService.hide(); if (item.uri.scheme === 'command' && item.uri.path === 'browse') { this._browseForFolder(); + } else if (item.uri.scheme === 'command' && item.uri.path === 'clone') { + this._cloneRepository(); } else { this._selectFolder(item.uri); } @@ -196,6 +200,17 @@ export class FolderPicker extends Disposable { } } + private async _cloneRepository(): Promise { + try { + const clonedPath: string | undefined = await this.commandService.executeCommand('git.clone', undefined, undefined, { postCloneAction: 'none' }); + if (clonedPath) { + this._selectFolder(URI.file(clonedPath)); + } + } catch { + // clone was cancelled or failed — nothing to do + } + } + private _addToRecentlyPickedFolders(folderUri: URI): void { this._recentlyPickedFolders = [folderUri, ...this._recentlyPickedFolders.filter(f => !isEqual(f, folderUri))].slice(0, MAX_RECENT_FOLDERS); this.storageService.store(STORAGE_KEY_RECENT_FOLDERS, JSON.stringify(this._recentlyPickedFolders.map(f => f.toString())), StorageScope.PROFILE, StorageTarget.MACHINE); @@ -253,6 +268,12 @@ export class FolderPicker extends Disposable { group: { title: '', icon: Codicon.search }, item: { uri: URI.from({ scheme: 'command', path: 'browse' }), label: localize('browseFolder', "Browse...") }, }); + items.push({ + kind: ActionListItemKind.Action, + label: localize('cloneRepository', "Clone..."), + group: { title: '', icon: Codicon.repoClone }, + item: { uri: URI.from({ scheme: 'command', path: 'clone' }), label: localize('cloneRepository', "Clone...") }, + }); return items; } From fa6faf3c1f9234bcbe0d5bb94d6595256659b64c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 26 Feb 2026 15:02:07 +0100 Subject: [PATCH 18/28] sessions - add hideSkipButton option and refactor dialog options (#297976) * fix - add hideSkipButton option to dialog options * refactor - update dialog options to use dialogHideSkip --- .../contrib/chat/browser/newChatViewPane.ts | 39 +------------------ .../welcome/browser/welcome.contribution.ts | 3 +- .../chatSetup/chatSetupContributions.ts | 4 +- .../chat/browser/chatSetup/chatSetupRunner.ts | 8 ++-- 4 files changed, 9 insertions(+), 45 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index a997bfd0351af..489cceb9cce5a 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -36,9 +36,6 @@ import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js' import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { localize } from '../../../../nls.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService } from '../../../../workbench/services/chat/common/chatEntitlementService.js'; -import { CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { ChatSessionPosition, getResourceForNewChatSession } from '../../../../workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.js'; import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../../../workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.js'; @@ -154,8 +151,6 @@ class NewChatWidget extends Disposable { @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, @IGitService private readonly gitService: IGitService, @IStorageService private readonly storageService: IStorageService, - @IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService, - @ICommandService private readonly commandService: ICommandService, ) { super(); this._contextAttachments = this._register(this.instantiationService.createInstance(NewChatContextAttachments)); @@ -807,28 +802,13 @@ class NewChatWidget extends Disposable { this._sendButton.enabled = !this._sending && hasText && !(this._newSession.value?.disabled ?? true); } - private async _send(options?: { skipSetup?: boolean; openNewAfterSend?: boolean }): Promise { + private async _send(options?: { openNewAfterSend?: boolean }): Promise { const query = this._editor.getModel()?.getValue().trim(); const session = this._newSession.value; if (!query || !session || this._sending) { return; } - // If chat is not set up (extension not installed or user not signed in), - // trigger the standard chat setup flow first, then re-submit. - if (!options?.skipSetup && this._needsChatSetup()) { - const success = await this.commandService.executeCommand(CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID, { - dialogIcon: Codicon.agent, - dialogTitle: this.chatEntitlementService.anonymous ? - localize('sessions.startUsingSessions', "Start using Sessions") : - localize('sessions.signinRequired', "Sign in to use Sessions") - }); - if (success) { - return await this._send({ ...options, skipSetup: true }); - } - return; - } - // If the session is disabled due to missing folder/repo, open the picker if (session.disabled) { if (!this._hasRequiredRepoOrFolderSelection(session.target)) { @@ -891,23 +871,6 @@ class NewChatWidget extends Disposable { } } - private _needsChatSetup(): boolean { - const { sentiment, entitlement } = this.chatEntitlementService; - if ( - !sentiment?.installed || // Extension not installed: run setup to install - sentiment?.disabled || // Extension disabled: run setup to enable - sentiment?.untrusted || // Workspace untrusted: run setup to ask for trust - entitlement === ChatEntitlement.Available || // Entitlement available: run setup to sign up - ( - entitlement === ChatEntitlement.Unknown && // Entitlement unknown: run setup to sign in / sign up - !this.chatEntitlementService.anonymous // unless anonymous access is enabled - ) - ) { - return true; - } - - return false; - } // --- Layout --- diff --git a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts index 6a9a7d29a481e..209161e31c749 100644 --- a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts +++ b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts @@ -88,7 +88,8 @@ class SessionsWelcomeOverlay extends Disposable { dialogIcon: Codicon.agent, dialogTitle: this.chatEntitlementService.anonymous ? localize('sessions.startUsingSessions', "Start using Sessions") : - localize('sessions.signinRequired', "Sign in to use Sessions") + localize('sessions.signinRequired', "Sign in to use Sessions"), + dialogHideSkip: true }); if (success) { diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts index 044303008fc30..d110a659c915f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts @@ -232,7 +232,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr }); } - override async run(accessor: ServicesAccessor, mode?: ChatModeKind | string, options?: { forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous; inputValue?: string; dialogIcon?: ThemeIcon; dialogTitle?: string }): Promise { + override async run(accessor: ServicesAccessor, mode?: ChatModeKind | string, options?: { forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous; inputValue?: string; dialogIcon?: ThemeIcon; dialogTitle?: string; dialogHideSkip?: boolean }): Promise { const widgetService = accessor.get(IChatWidgetService); const instantiationService = accessor.get(IInstantiationService); const dialogService = accessor.get(IDialogService); @@ -280,7 +280,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr }); } - override async run(accessor: ServicesAccessor, options?: { dialogIcon?: ThemeIcon; dialogTitle?: string }): Promise { + override async run(accessor: ServicesAccessor, options?: { dialogIcon?: ThemeIcon; dialogTitle?: string; dialogHideSkip?: boolean }): Promise { const commandService = accessor.get(ICommandService); const telemetryService = accessor.get(ITelemetryService); const chatEntitlementService = accessor.get(IChatEntitlementService); diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts index 4cc090ef3dab4..7e77a64f55ee3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupRunner.ts @@ -82,7 +82,7 @@ export class ChatSetup { this.skipDialogOnce = true; } - async run(options?: { disableChatViewReveal?: boolean; forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous; dialogIcon?: ThemeIcon; dialogTitle?: string }): Promise { + async run(options?: { disableChatViewReveal?: boolean; forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous; dialogIcon?: ThemeIcon; dialogTitle?: string; dialogHideSkip?: boolean }): Promise { if (this.pendingRun) { return this.pendingRun; } @@ -96,7 +96,7 @@ export class ChatSetup { } } - private async doRun(options?: { disableChatViewReveal?: boolean; forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous; dialogIcon?: ThemeIcon; dialogTitle?: string }): Promise { + private async doRun(options?: { disableChatViewReveal?: boolean; forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: ChatSetupAnonymous; dialogIcon?: ThemeIcon; dialogTitle?: string; dialogHideSkip?: boolean }): Promise { this.context.update({ later: false }); const dialogSkipped = this.skipDialogOnce; @@ -162,10 +162,10 @@ export class ChatSetup { return { success, dialogSkipped }; } - private async showDialog(options?: { forceSignInDialog?: boolean; forceAnonymous?: ChatSetupAnonymous; dialogIcon?: ThemeIcon; dialogTitle?: string }): Promise { + private async showDialog(options?: { forceSignInDialog?: boolean; forceAnonymous?: ChatSetupAnonymous; dialogIcon?: ThemeIcon; dialogTitle?: string; dialogHideSkip?: boolean }): Promise { const disposables = new DisposableStore(); - const useCloseButton = await this.experimentService.getTreatment('chatSetupDialogCloseButton'); + const useCloseButton = options?.dialogHideSkip || await this.experimentService.getTreatment('chatSetupDialogCloseButton'); const buttons = this.getButtons(options, useCloseButton); const dialog = disposables.add(new Dialog( From 1eef0115c639f1431904899c76f3502cbf02c911 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 26 Feb 2026 15:02:29 +0100 Subject: [PATCH 19/28] sessions - stop moving them up for in progress, go by date (#297980) --- .../browser/agentSessions/agentSessionsViewer.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index be63965253746..eb1748803f038 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -839,17 +839,6 @@ export class AgentSessionsSorter implements ITreeSorter { return 1; // a (other) comes after b (needs input) } - // In Progress - const aInProgress = sessionA.status === AgentSessionStatus.InProgress; - const bInProgress = sessionB.status === AgentSessionStatus.InProgress; - - if (aInProgress && !bInProgress) { - return -1; // a (in-progress) comes before b (finished) - } - if (!aInProgress && bInProgress) { - return 1; // a (finished) comes after b (in-progress) - } - // Archived const aArchived = sessionA.isArchived(); const bArchived = sessionB.isArchived(); @@ -867,7 +856,7 @@ export class AgentSessionsSorter implements ITreeSorter { return override; } - //Sort by end or start time (most recent first) + // Sort by end or start time (most recent first) const timeA = getAgentSessionTime(sessionA.timing); const timeB = getAgentSessionTime(sessionB.timing); return timeB - timeA; From 32ddcadf105c1a458eeae02d7464db451659bc58 Mon Sep 17 00:00:00 2001 From: Isidor Date: Thu, 26 Feb 2026 15:06:36 +0100 Subject: [PATCH 20/28] fix: update default max requests for chat agent configuration to 50 --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 6bc7c2d60dfd4..fd4ff0ac68a96 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1433,7 +1433,6 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr 'chatAgentMaxRequestsFree' : 'chatAgentMaxRequestsPro'; this.experimentService.getTreatment(treatmentId).then((value) => { - const defaultValue = value ?? (this.entitlementService.entitlement === ChatEntitlement.Free ? 25 : 25); const node: IConfigurationNode = { id: 'chatSidebar', title: nls.localize('interactiveSessionConfigurationTitle', "Chat"), @@ -1442,7 +1441,7 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr 'chat.agent.maxRequests': { type: 'number', markdownDescription: nls.localize('chat.agent.maxRequests', "The maximum number of requests to allow per-turn when using an agent. When the limit is reached, will ask to confirm to continue."), - default: defaultValue, + default: 50, order: 2, }, } From 1da3d7005d0a3663040586e4afd1ddd387d759fa Mon Sep 17 00:00:00 2001 From: Isidor Date: Thu, 26 Feb 2026 15:14:06 +0100 Subject: [PATCH 21/28] Thanks Copilot for nice review --- src/vs/workbench/contrib/chat/browser/chat.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index fd4ff0ac68a96..aae88d01f5e86 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -1441,7 +1441,7 @@ class ChatAgentSettingContribution extends Disposable implements IWorkbenchContr 'chat.agent.maxRequests': { type: 'number', markdownDescription: nls.localize('chat.agent.maxRequests', "The maximum number of requests to allow per-turn when using an agent. When the limit is reached, will ask to confirm to continue."), - default: 50, + default: value ?? 50, order: 2, }, } From 96baba2cc4d3cf49b48492b5be4c67cc588241a8 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 26 Feb 2026 14:46:52 +0100 Subject: [PATCH 22/28] feat: update component explorer dependencies and add new test fixtures - Updated @vscode/component-explorer to version 0.1.1-16 and @vscode/component-explorer-cli to version 0.1.1-12 in package.json and package-lock.json. - Added new test fixtures for chat question carousel, code action list, find widget, inline completions extras, rename widget, and suggest widget. - Implemented rendering logic for new fixtures to enhance testing capabilities for various components. --- .github/skills/component-fixtures/SKILL.md | 343 ++++++++++++++++++ .vscode/launch.json | 14 +- .vscode/tasks.json | 13 +- build/vite/package-lock.json | 18 +- build/vite/package.json | 4 +- package-lock.json | 18 +- package.json | 5 +- .../chatQuestionCarousel.fixture.ts | 172 +++++++++ .../codeActionList.fixture.ts | 102 ++++++ .../componentFixtures/findWidget.fixture.ts | 129 +++++++ .../inlineCompletionsExtras.fixture.ts | 306 ++++++++++++++++ .../componentFixtures/renameWidget.fixture.ts | 117 ++++++ .../suggestWidget.fixture.ts | 170 +++++++++ 13 files changed, 1387 insertions(+), 24 deletions(-) create mode 100644 .github/skills/component-fixtures/SKILL.md create mode 100644 src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts create mode 100644 src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts create mode 100644 src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts create mode 100644 src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts create mode 100644 src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts create mode 100644 src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts diff --git a/.github/skills/component-fixtures/SKILL.md b/.github/skills/component-fixtures/SKILL.md new file mode 100644 index 0000000000000..ec2df9d4e923d --- /dev/null +++ b/.github/skills/component-fixtures/SKILL.md @@ -0,0 +1,343 @@ +--- +name: component-fixtures +description: Use when creating or updating component fixtures for screenshot testing, or when designing UI components to be fixture-friendly. Covers fixture file structure, theming, service setup, CSS scoping, async rendering, and common pitfalls. +--- + +# Component Fixtures + +Component fixtures render isolated UI components for visual screenshot testing via the component explorer. Fixtures live in `src/vs/workbench/test/browser/componentFixtures/` and are auto-discovered by the Vite dev server using the glob `src/**/*.fixture.ts`. + +Use tools `mcp_component-exp_`* to list and screenshot fixtures. If you cannot see these tools, inform the user to them on. + +## Running Fixtures Locally + +1. Start the component explorer daemon: run the **Launch Component Explorer** task +2. Use the `mcp_component-exp_list_fixtures` tool to see all available fixtures and their URLs +3. Use the `mcp_component-exp_screenshot` tool to capture screenshots programmatically + +## File Structure + +Each fixture file exports a default `defineThemedFixtureGroup(...)`. The file must end with `.fixture.ts`. + +``` +src/vs/workbench/test/browser/componentFixtures/ + fixtureUtils.ts # Shared helpers (DO NOT import @vscode/component-explorer elsewhere) + myComponent.fixture.ts # Your fixture file +``` + +## Basic Pattern + +```typescript +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; + +export default defineThemedFixtureGroup({ + Default: defineComponentFixture({ render: renderMyComponent }), + AnotherVariant: defineComponentFixture({ render: renderMyComponent }), +}); + +function renderMyComponent({ container, disposableStore, theme }: ComponentFixtureContext): void { + container.style.width = '400px'; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: theme, + additionalServices: (reg) => { + // Register additional services the component needs + reg.define(IMyService, MyServiceImpl); + reg.defineInstance(IMockService, mockInstance); + }, + }); + + const widget = disposableStore.add( + instantiationService.createInstance(MyWidget, /* constructor args */) + ); + container.appendChild(widget.domNode); +} +``` + +Key points: +- **`defineThemedFixtureGroup`** automatically creates Dark and Light variants for each fixture +- **`defineComponentFixture`** wraps your render function with theme setup and shadow DOM isolation +- **`createEditorServices`** provides a `TestInstantiationService` with base editor services pre-registered +- Always register created widgets with `disposableStore.add(...)` to prevent leaks +- Pass `colorTheme: theme` to `createEditorServices` so theme colors render correctly + +## Utilities from fixtureUtils.ts + +| Export | Purpose | +|---|---| +| `defineComponentFixture` | Creates Dark/Light themed fixture variants from a render function | +| `defineThemedFixtureGroup` | Groups multiple themed fixtures into a named fixture group | +| `createEditorServices` | Creates `TestInstantiationService` with all base editor services | +| `registerWorkbenchServices` | Registers additional workbench services (context menu, label, etc.) | +| `createTextModel` | Creates a text model via `ModelService` for editor fixtures | +| `setupTheme` | Applies theme CSS to a container (called automatically by `defineComponentFixture`) | +| `darkTheme` / `lightTheme` | Pre-loaded `ColorThemeData` instances | + +**Important:** Only `fixtureUtils.ts` may import from `@vscode/component-explorer`. All fixture files must go through the helpers in `fixtureUtils.ts`. + +## CSS Scoping + +Fixtures render inside shadow DOM. The component-explorer automatically adopts the global VS Code stylesheets and theme CSS. + +### Matching production CSS selectors + +Many VS Code components have CSS rules scoped to deep ancestor selectors (e.g., `.interactive-session .interactive-input-part > .widget-container .my-element`). In fixtures, you must recreate the required ancestor DOM structure for these selectors to match: + +```typescript +function render({ container }: ComponentFixtureContext): void { + container.classList.add('interactive-session'); + + // Recreate ancestor structure that CSS selectors expect + const inputPart = dom.$('.interactive-input-part'); + const widgetContainer = dom.$('.widget-container'); + inputPart.appendChild(widgetContainer); + container.appendChild(inputPart); + + widgetContainer.appendChild(myWidget.domNode); +} +``` + +**Design recommendation for new components:** Avoid deeply nested CSS selectors that require specific ancestor elements. Use self-contained class names (e.g., `.my-widget .my-element` rather than `.parent-view .parent-part > .wrapper .my-element`). This makes components easier to fixture and reuse. + +## Services + +### Using createEditorServices + +`createEditorServices` pre-registers these services: `IAccessibilityService`, `IKeybindingService`, `IClipboardService`, `IOpenerService`, `INotificationService`, `IDialogService`, `IUndoRedoService`, `ILanguageService`, `IConfigurationService`, `IStorageService`, `IThemeService`, `IModelService`, `ICodeEditorService`, `IContextKeyService`, `ICommandService`, `ITelemetryService`, `IHoverService`, `IUserInteractionService`, and more. + +### Additional services + +Register extra services via `additionalServices`: + +```typescript +createEditorServices(disposableStore, { + additionalServices: (reg) => { + // Class-based (instantiated by DI): + reg.define(IMyService, MyServiceImpl); + // Instance-based (pre-constructed): + reg.defineInstance(IMyService, myMockInstance); + }, +}); +``` + +### Mocking services + +Use the `mock()` helper from `base/test/common/mock.js` to create mock service instances: + +```typescript +import { mock } from '../../../../base/test/common/mock.js'; + +const myService = new class extends mock() { + override someMethod(): string { return 'test'; } + override onSomeEvent = Event.None; +}; +reg.defineInstance(IMyService, myService); +``` + +For mock view models or data objects: +```typescript +const element = new class extends mock() { }(); +``` + +## Async Rendering + +The component explorer waits **2 animation frames** after the synchronous render function returns. For most components, this is sufficient. + +If your render function returns a `Promise`, the component explorer waits for the promise to resolve. + +### Pitfall: DOM reparenting causes flickering + +Avoid moving rendered widgets between DOM parents after initial render. This causes: +- Layout recalculation (the widget jumps as `position: absolute` coordinates become invalid) +- Focus loss (blur events can trigger hide logic in widgets like QuickInput) +- Screenshot instability (the component explorer may capture an intermediate layout state) + +**Bad pattern — reparenting a widget after async wait:** +```typescript +async function render({ container }: ComponentFixtureContext): Promise { + const host = document.createElement('div'); + container.appendChild(host); + // ... create widget inside host ... + await waitForWidget(); + container.appendChild(widget); // BAD: reparenting causes flicker + host.remove(); +} +``` + +**Better pattern — render in-place with the correct DOM structure from the start:** +```typescript +function render({ container }: ComponentFixtureContext): void { + // Set up the correct DOM structure first, then create the widget inside it + const widget = createWidget(container); + container.appendChild(widget.domNode); +} +``` + +If the component absolutely requires async setup (e.g., QuickInput which renders internally), minimize DOM manipulation after the widget appears by structuring the host container to match the final layout from the beginning. + +## Adapting Existing Components for Fixtures + +Existing components often need small changes to become fixturable. When writing a fixture reveals friction, fix the component — don't work around it in the fixture. Common adaptations: + +### Decouple CSS from ancestor context + +If a component's CSS only works inside a deeply nested selector like `.workbench .sidebar .my-view .my-widget`, refactor the CSS to be self-contained. Move the styles so they're scoped to the component's own root class: + +```css +/* Before: requires specific ancestors */ +.workbench .sidebar .my-view .my-widget .header { font-weight: bold; } + +/* After: self-contained */ +.my-widget .header { font-weight: bold; } +``` + +If the component shares styles with its parent (e.g., inheriting background color), use CSS custom properties rather than relying on ancestor selectors. + +### Extract hard-coded service dependencies + +If a component reaches into singletons or global state instead of using DI, refactor it to accept services through the constructor: + +```typescript +// Before: hard to mock in fixtures +class MyWidget { + private readonly config = getSomeGlobalConfig(); +} + +// After: injectable and testable +class MyWidget { + constructor(@IConfigurationService private readonly configService: IConfigurationService) { } +} +``` + +### Add options to control auto-focus and animation + +Components that auto-focus on creation or run animations cause flaky screenshots. Add an options parameter: + +```typescript +interface IMyWidgetOptions { + shouldAutoFocus?: boolean; +} +``` + +The fixture passes `shouldAutoFocus: false`. The production call site keeps the default behavior. + +### Expose internal state for "already completed" rendering + +Many components have lifecycle states (loading → active → completed). If the component can only reach the "completed" state through user interaction, add support for initializing directly into that state via constructor data: + +```typescript +// The fixture can pass pre-filled data to render the summary/completed state +// without simulating the full user interaction flow. +const carousel: IChatQuestionCarousel = { + questions, + allowSkip: true, + kind: 'questionCarousel', + isUsed: true, // Already completed + data: { 'q1': 'answer' }, // Pre-filled answers +}; +``` + +### Make DOM node accessible + +If a component builds its DOM internally and doesn't expose the root element, add a public `readonly domNode: HTMLElement` property so fixtures can append it to the container. + +## Writing Fixture-Friendly Components + +When designing new UI components, follow these practices to make them easy to fixture: + +### 1. Accept a container element in the constructor + +```typescript +// Good: container is passed in +class MyWidget { + constructor(container: HTMLElement, @IFoo foo: IFoo) { + this.domNode = dom.append(container, dom.$('.my-widget')); + } +} + +// Also good: widget creates its own domNode for the caller to place +class MyWidget { + readonly domNode: HTMLElement; + constructor(@IFoo foo: IFoo) { + this.domNode = dom.$('.my-widget'); + } +} +``` + +### 2. Use dependency injection for all services + +All external dependencies should come through DI so fixtures can provide test implementations: + +```typescript +// Good: services injected +constructor(@IThemeService private readonly themeService: IThemeService) { } + +// Bad: reaching into globals +constructor() { this.theme = getGlobalTheme(); } +``` + +### 3. Keep CSS selectors shallow + +```css +/* Good: self-contained, easy to fixture */ +.my-widget .my-header { ... } +.my-widget .my-list-item { ... } + +/* Bad: requires deep ancestor chain */ +.workbench .sidebar .my-view .my-widget .my-header { ... } +``` + +### 4. Avoid reading from layout/window services during construction + +Components that measure the window or read layout dimensions during construction are hard to fixture because the shadow DOM container has different dimensions than the workbench: + +```typescript +// Prefer: use CSS for sizing, or accept dimensions as parameters +container.style.width = '400px'; +container.style.height = '300px'; + +// Avoid: reading from layoutService during construction +const width = this.layoutService.mainContainerDimension.width; +``` + +### 5. Support disabling auto-focus in fixtures + +Auto-focus can interfere with screenshot stability. Provide options to disable it: + +```typescript +interface IMyWidgetOptions { + shouldAutoFocus?: boolean; // Fixtures pass false +} +``` + +### 6. Expose the DOM node + +The fixture needs to append the widget's DOM to the container. Expose it as a public `readonly domNode: HTMLElement`. + +## Multiple Fixture Variants + +Create variants to show different states of the same component: + +```typescript +export default defineThemedFixtureGroup({ + // Different data states + Empty: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { items: [] }) }), + WithItems: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { items: sampleItems }) }), + + // Different configurations + ReadOnly: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { readonly: true }) }), + Editable: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { readonly: false }) }), + + // Lifecycle states + Loading: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { state: 'loading' }) }), + Completed: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { state: 'done' }) }), +}); +``` + +## Learnings + +Update this section with insights from your fixture development experience! + +* Do not copy the component to the fixture and modify it there. Always adapt the original component to be fixture-friendly, then render it in the fixture. This ensures the fixture tests the real component code and lifecycle, rather than a modified version that may hide bugs. + +* **Don't recompose child widgets in fixtures.** Never manually instantiate and add a sub-widget (e.g., a toolbar content widget) that the parent component is supposed to create. Instead, configure the parent correctly (e.g., set the right editor option, register the right provider) so the child appears through the normal code path. Manually recomposing hides integration bugs and doesn't test the real widget lifecycle. diff --git a/.vscode/launch.json b/.vscode/launch.json index 04fc39061884c..47d901042e3a8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -648,9 +648,19 @@ } }, { - "name": "Component Explorer", + "name": "Component Explorer (Edge)", "type": "msedge", - "port": 9230, + "request": "launch", + "url": "http://localhost:5337/___explorer", + "preLaunchTask": "Launch Component Explorer", + "presentation": { + "group": "1_component_explorer", + "order": 4 + } + }, + { + "name": "Component Explorer (Chrome)", + "type": "chrome", "request": "launch", "url": "http://localhost:5337/___explorer", "preLaunchTask": "Launch Component Explorer", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c330df2edecc9..8353bc02c75b8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -375,9 +375,18 @@ { "label": "Launch Component Explorer", "type": "shell", - "command": "npx component-explorer serve -c ./test/componentFixtures/component-explorer.json", + "command": "npx component-explorer serve -c ./test/componentFixtures/component-explorer.json -vv", "isBackground": true, - "problemMatcher": [] + "problemMatcher": { + "owner": "component-explorer", + "fileLocation": "absolute", + "pattern": { + "regexp": "^\\s*at\\s+(.+?):(\\d+):(\\d+)\\s*$", + "file": 1, + "line": 2, + "column": 3 + } + } } ] } diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index 4179138e714c7..8d3f50df39b52 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -8,8 +8,8 @@ "name": "@vscode/sample-source", "version": "0.0.0", "devDependencies": { - "@vscode/component-explorer": "^0.1.1-12", - "@vscode/component-explorer-vite-plugin": "^0.1.1-12", + "@vscode/component-explorer": "^0.1.1-16", + "@vscode/component-explorer-vite-plugin": "^0.1.1-16", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", "vite": "npm:rolldown-vite@latest" @@ -683,20 +683,22 @@ "license": "MIT" }, "node_modules/@vscode/component-explorer": { - "version": "0.1.1-12", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-12.tgz", - "integrity": "sha512-qqbxbu3BvqWtwFdVsROLUSd1BiScCiUPP5n0sk0yV1WDATlAl6wQMX1QlmsZy3hag8iP/MXUEj5tSBjA1T7tFw==", + "version": "0.1.1-16", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-16.tgz", + "integrity": "sha512-is1RxdlNO5K1RSqWd5z8BN6gPrqEBZfjgUi3ZJbQj8Z4VqmqoJsNLIzBXOIlQJX+5mWgeNdOq3vxe0u15ZkAlA==", "dev": true, + "license": "MIT", "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@vscode/component-explorer-vite-plugin": { - "version": "0.1.1-12", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-12.tgz", - "integrity": "sha512-MG5ndoooX2X9PYto1WkNSwWKKmR5OJx3cBnUf7JHm8ERw+8RsZbLe+WS+hVOqnCVPxHy7t+0IYRFl7IC5cuwOQ==", + "version": "0.1.1-16", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-16.tgz", + "integrity": "sha512-z2EqusWl49dUF3vNDgmJJJQXkv4ejeBH9AdFZUWOiGaMvjjFX6UV7oQ733b+vo5YFE8my9WaK7D691i2wZ47Fg==", "dev": true, + "license": "MIT", "dependencies": { "tinyglobby": "^0.2.0" }, diff --git a/build/vite/package.json b/build/vite/package.json index 245bf4fc8001a..5e5d59d1a1696 100644 --- a/build/vite/package.json +++ b/build/vite/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "devDependencies": { - "@vscode/component-explorer": "^0.1.1-12", - "@vscode/component-explorer-vite-plugin": "^0.1.1-12", + "@vscode/component-explorer": "^0.1.1-16", + "@vscode/component-explorer-vite-plugin": "^0.1.1-16", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", "vite": "npm:rolldown-vite@latest" diff --git a/package-lock.json b/package-lock.json index 587596d6666fd..d0ebb1204c211 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,8 +84,8 @@ "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", "@typescript/native-preview": "^7.0.0-dev.20260130", - "@vscode/component-explorer": "^0.1.1-12", - "@vscode/component-explorer-cli": "^0.1.1-8", + "@vscode/component-explorer": "^0.1.1-16", + "@vscode/component-explorer-cli": "^0.1.1-12", "@vscode/gulp-electron": "1.40.0", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", @@ -3065,20 +3065,22 @@ "license": "CC-BY-4.0" }, "node_modules/@vscode/component-explorer": { - "version": "0.1.1-12", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-12.tgz", - "integrity": "sha512-qqbxbu3BvqWtwFdVsROLUSd1BiScCiUPP5n0sk0yV1WDATlAl6wQMX1QlmsZy3hag8iP/MXUEj5tSBjA1T7tFw==", + "version": "0.1.1-16", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-16.tgz", + "integrity": "sha512-is1RxdlNO5K1RSqWd5z8BN6gPrqEBZfjgUi3ZJbQj8Z4VqmqoJsNLIzBXOIlQJX+5mWgeNdOq3vxe0u15ZkAlA==", "dev": true, + "license": "MIT", "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@vscode/component-explorer-cli": { - "version": "0.1.1-8", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer-cli/-/component-explorer-cli-0.1.1-8.tgz", - "integrity": "sha512-Sze4SdE6zlr5Mkd/RFLmLqSmEjsjq1f2pBp4a/S5u0TDS4matrkklb3LHym8dMbYC6UjWQuFOkYFZwXnGFxZqw==", + "version": "0.1.1-12", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-cli/-/component-explorer-cli-0.1.1-12.tgz", + "integrity": "sha512-SaChUP94wkf1RaaJ/MnpQsxsr7pUpqQJq5Z9QLbrZuUqRil2TZEHwYLSqpQPqLgybNxZtrlMDivTjcCWXFTttg==", "dev": true, + "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", "clipanion": "^4.0.0-rc.4", diff --git a/package.json b/package.json index cb5b8b6a963e4..64ad8d3954fa9 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "perf": "node scripts/code-perf.js", "update-build-ts-version": "npm install -D typescript@next && npm install -D @typescript/native-preview && (cd build && npm run typecheck)", "install-local-component-explorer": "npm install ../vscode-packages/js-component-explorer/dist/vscode-component-explorer-0.1.0.tgz ../vscode-packages/js-component-explorer/dist/vscode-component-explorer-cli-0.1.0.tgz --no-save && cd build/vite && npm install ../../../vscode-packages/js-component-explorer/dist/vscode-component-explorer-vite-plugin-0.1.0.tgz --no-save", + "symlink-local-component-explorer": "npm install ../vscode-packages/js-component-explorer/packages/explorer ../vscode-packages/js-component-explorer/packages/cli --no-save && cd build/vite && npm install ../../../vscode-packages/js-component-explorer/packages/vite-plugin ../../../vscode-packages/js-component-explorer/packages/explorer --no-save", "install-latest-component-explorer": "npm install @vscode/component-explorer@next @vscode/component-explorer-cli@next && cd build/vite && npm install @vscode/component-explorer-vite-plugin@next && npm install @vscode/component-explorer@next" }, "dependencies": { @@ -152,8 +153,8 @@ "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", "@typescript/native-preview": "^7.0.0-dev.20260130", - "@vscode/component-explorer": "^0.1.1-12", - "@vscode/component-explorer-cli": "^0.1.1-8", + "@vscode/component-explorer": "^0.1.1-16", + "@vscode/component-explorer-cli": "^0.1.1-12", "@vscode/gulp-electron": "1.40.0", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", diff --git a/src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts new file mode 100644 index 0000000000000..0cef5d23bd7ac --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { IMarkdownRendererService, MarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { IChatQuestion, IChatQuestionCarousel } from '../../../contrib/chat/common/chatService/chatService.js'; +import { ChatQuestionCarouselPart, IChatQuestionCarouselOptions } from '../../../contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.js'; +import { IChatContentPartRenderContext } from '../../../contrib/chat/browser/widget/chatContentParts/chatContentParts.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { Event } from '../../../../base/common/event.js'; +import { observableValue } from '../../../../base/common/observable.js'; +import { IChatRequestViewModel } from '../../../contrib/chat/common/model/chatViewModel.js'; +import '../../../contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css'; + +function createCarousel(questions: IChatQuestion[], allowSkip: boolean = true): IChatQuestionCarousel { + return { + questions, + allowSkip, + kind: 'questionCarousel', + }; +} + +function createMockContext(): IChatContentPartRenderContext { + return { + element: new class extends mock() { }(), + elementIndex: 0, + container: document.createElement('div'), + content: [], + contentIndex: 0, + editorPool: undefined!, + codeBlockStartIndex: 0, + treeStartIndex: 0, + diffEditorPool: undefined!, + codeBlockModelCollection: undefined!, + currentWidth: observableValue('currentWidth', 400), + onDidChangeVisibility: Event.None, + }; +} + +function createOptions(): IChatQuestionCarouselOptions { + return { + onSubmit: () => { }, + shouldAutoFocus: false, + }; +} + +function renderCarousel(context: ComponentFixtureContext, carousel: IChatQuestionCarousel): void { + const { container, disposableStore } = context; + + const instantiationService = createEditorServices(disposableStore, { + additionalServices: (reg) => { + reg.define(IMarkdownRendererService, MarkdownRendererService); + }, + }); + + const part = disposableStore.add( + instantiationService.createInstance( + ChatQuestionCarouselPart, + carousel, + createMockContext(), + createOptions(), + ) + ); + + container.style.width = '400px'; + container.style.padding = '8px'; + container.classList.add('interactive-session'); + + // The CSS uses `.interactive-session .interactive-input-part > .chat-question-carousel-widget-container` + // for most layout rules, so we need those wrapper elements. + const inputPart = dom.$('.interactive-input-part'); + const widgetContainer = dom.$('.chat-question-carousel-widget-container'); + inputPart.appendChild(widgetContainer); + container.appendChild(inputPart); + + widgetContainer.appendChild(part.domNode); +} + +// ============================================================================ +// Sample questions +// ============================================================================ + +const textQuestion: IChatQuestion = { + id: 'project-name', + type: 'text', + title: 'Project name', + message: 'What is the name of your project?', + defaultValue: 'my-project', +}; + +const singleSelectQuestion: IChatQuestion = { + id: 'language', + type: 'singleSelect', + title: 'Language', + message: 'Which language do you want to use?', + options: [ + { id: 'ts', label: 'TypeScript - Strongly typed JavaScript', value: 'typescript' }, + { id: 'js', label: 'JavaScript - Dynamic scripting language', value: 'javascript' }, + { id: 'py', label: 'Python - General purpose language', value: 'python' }, + { id: 'rs', label: 'Rust - Systems programming', value: 'rust' }, + ], + defaultValue: 'ts', +}; + +const multiSelectQuestion: IChatQuestion = { + id: 'features', + type: 'multiSelect', + title: 'Features', + message: 'Which features should be enabled?', + options: [ + { id: 'lint', label: 'Linting', value: 'linting' }, + { id: 'fmt', label: 'Formatting', value: 'formatting' }, + { id: 'test', label: 'Testing', value: 'testing' }, + { id: 'ci', label: 'CI/CD Pipeline', value: 'ci' }, + ], + defaultValue: ['lint', 'fmt'], +}; + +// ============================================================================ +// Fixtures +// ============================================================================ + +export default defineThemedFixtureGroup({ + SingleTextQuestion: defineComponentFixture({ + render: (context) => renderCarousel(context, createCarousel([textQuestion])), + }), + + SingleSelectQuestion: defineComponentFixture({ + render: (context) => renderCarousel(context, createCarousel([singleSelectQuestion])), + }), + + MultiSelectQuestion: defineComponentFixture({ + render: (context) => renderCarousel(context, createCarousel([multiSelectQuestion])), + }), + + MultipleQuestions: defineComponentFixture({ + render: (context) => renderCarousel(context, createCarousel([ + textQuestion, + singleSelectQuestion, + multiSelectQuestion, + ])), + }), + + NoSkip: defineComponentFixture({ + render: (context) => renderCarousel(context, createCarousel([singleSelectQuestion], false)), + }), + + SubmittedSummary: defineComponentFixture({ + render: (context) => { + const carousel = createCarousel([textQuestion, singleSelectQuestion, multiSelectQuestion]); + carousel.isUsed = true; + carousel.data = { + 'project-name': 'my-app', + 'language': { selectedValue: 'typescript', freeformValue: undefined }, + 'features': { selectedValues: ['linting', 'formatting'], freeformValue: undefined }, + }; + renderCarousel(context, carousel); + }, + }), + + SkippedSummary: defineComponentFixture({ + render: (context) => { + const carousel = createCarousel([textQuestion, singleSelectQuestion]); + carousel.isUsed = true; + carousel.data = {}; + renderCarousel(context, carousel); + }, + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts new file mode 100644 index 0000000000000..76be7d9d68248 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../base/common/codicons.js'; +import { Event } from '../../../../base/common/event.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from './fixtureUtils.js'; +import { ActionList, ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; +import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; + +import '../../../../platform/actionWidget/browser/actionWidget.css'; +import '../../../../base/browser/ui/codicons/codiconStyles.js'; +import '../../../../editor/contrib/symbolIcons/browser/symbolIcons.js'; + +interface CodeActionFixtureOptions extends ComponentFixtureContext { + items: IActionListItem[]; + width?: string; +} + +function renderCodeActionList(options: CodeActionFixtureOptions): void { + const { container, disposableStore, theme } = options; + container.style.width = options.width ?? '300px'; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: theme, + additionalServices: (reg) => { + registerWorkbenchServices(reg); + reg.defineInstance(ILayoutService, new class extends mock() { + declare readonly _serviceBrand: undefined; + override get mainContainer() { return container; } + override get activeContainer() { return container; } + override get mainContainerDimension() { return { width: 300, height: 600 }; } + override get activeContainerDimension() { return { width: 300, height: 600 }; } + override readonly mainContainerOffset = { top: 0, quickPickTop: 0 }; + override readonly onDidLayoutMainContainer = Event.None; + override readonly onDidLayoutActiveContainer = Event.None; + override readonly onDidLayoutContainer = Event.None; + override readonly onDidChangeActiveContainer = Event.None; + override readonly onDidAddContainer = Event.None; + override get containers() { return [container]; } + override getContainer() { return container; } + override whenContainerStylesLoaded() { return undefined; } + }); + }, + }); + + const delegate: IActionListDelegate = { + onHide: () => { }, + onSelect: () => { }, + }; + + const anchor = container; + + const list = disposableStore.add(instantiationService.createInstance( + ActionList, + 'codeActionWidget', + false, + options.items, + delegate, + undefined, + undefined, + anchor, + )); + + // Render the list directly into the container instead of using context view + const wrapper = document.createElement('div'); + wrapper.classList.add('action-widget'); + wrapper.appendChild(list.domNode); + container.appendChild(wrapper); + + list.layout(0); + list.focus(); +} + +const quickFixItems: IActionListItem[] = [ + { kind: ActionListItemKind.Header, group: { title: 'Quick Fix' } }, + { kind: ActionListItemKind.Action, item: 'fix-import', label: 'Add missing import for \'useState\'', group: { title: 'Quick Fix', icon: Codicon.lightBulb } }, + { kind: ActionListItemKind.Action, item: 'fix-typo', label: 'Change spelling to \'initialCount\'', group: { title: 'Quick Fix', icon: Codicon.lightBulb } }, + { kind: ActionListItemKind.Action, item: 'fix-type', label: 'Add explicit type annotation', group: { title: 'Quick Fix', icon: Codicon.lightBulb } }, + { kind: ActionListItemKind.Header, group: { title: 'Extract', icon: Codicon.wrench } }, + { kind: ActionListItemKind.Action, item: 'extract-const', label: 'Extract to constant in enclosing scope', group: { title: 'Extract', icon: Codicon.wrench } }, + { kind: ActionListItemKind.Action, item: 'extract-fn', label: 'Extract to function in module scope', group: { title: 'Extract', icon: Codicon.wrench } }, + { kind: ActionListItemKind.Header, group: { title: 'Source Action', icon: Codicon.symbolFile } }, + { kind: ActionListItemKind.Action, item: 'organize-imports', label: 'Organize Imports', group: { title: 'Source Action', icon: Codicon.symbolFile } }, +]; + +const simpleFixes: IActionListItem[] = [ + { kind: ActionListItemKind.Action, item: 'fix-1', label: 'Convert to arrow function', group: { title: 'Quick Fix', icon: Codicon.lightBulb } }, + { kind: ActionListItemKind.Action, item: 'fix-2', label: 'Remove unused variable', group: { title: 'Quick Fix', icon: Codicon.lightBulb } }, + { kind: ActionListItemKind.Action, item: 'fix-3', label: 'Add \'await\' to async call', group: { title: 'Quick Fix', icon: Codicon.lightBulb } }, +]; + +export default defineThemedFixtureGroup({ + GroupedCodeActions: defineComponentFixture({ + render: (context) => renderCodeActionList({ ...context, items: quickFixItems }), + }), + SimpleQuickFixes: defineComponentFixture({ + render: (context) => renderCodeActionList({ ...context, items: simpleFixes }), + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts new file mode 100644 index 0000000000000..4ed036840b666 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../base/common/uri.js'; +import { IContextViewProvider } from '../../../../base/browser/ui/contextview/contextview.js'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { FindReplaceState } from '../../../../editor/contrib/find/browser/findState.js'; +import { FindWidget, IFindController } from '../../../../editor/contrib/find/browser/findWidget.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; + +import '../../../../editor/contrib/find/browser/findWidget.css'; +import '../../../../base/browser/ui/codicons/codiconStyles.js'; + +const SAMPLE_CODE = `import { useState } from 'react'; + +function Counter({ initialCount }: { initialCount: number }) { + const [count, setCount] = useState(initialCount); + + return ( +
+

Count: {count}

+ + +
+ ); +} + +export default Counter; +`; + +interface FindFixtureOptions extends ComponentFixtureContext { + searchString?: string; + replaceString?: string; + showReplace?: boolean; + matchesCount?: number; + matchesPosition?: number; +} + +async function renderFindWidget(options: FindFixtureOptions): Promise { + const { container, disposableStore, theme } = options; + container.style.width = '600px'; + container.style.height = '350px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { colorTheme: theme }); + + const textModel = disposableStore.add(createTextModel( + instantiationService, + SAMPLE_CODE, + URI.parse('inmemory://find-fixture.tsx'), + 'typescript' + )); + + const editorWidgetOptions: ICodeEditorWidgetOptions = { + contributions: [] + }; + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 14, + cursorBlinking: 'solid', + find: { addExtraSpaceOnTop: false }, + }, + editorWidgetOptions + )); + + editor.setModel(textModel); + editor.focus(); + + const state = disposableStore.add(new FindReplaceState()); + + const mockController: IFindController = { + replace: () => { }, + replaceAll: () => { }, + getGlobalBufferTerm: async () => '', + }; + + const mockContextViewProvider: IContextViewProvider = { + showContextView: () => { }, + hideContextView: () => { }, + layout: () => { }, + }; + + disposableStore.add(new FindWidget( + editor, + mockController, + state, + mockContextViewProvider, + instantiationService.get(IKeybindingService), + instantiationService.get(IContextKeyService), + instantiationService.get(IHoverService), + undefined, + undefined, + instantiationService.get(IConfigurationService), + instantiationService.get(IAccessibilityService), + )); + + state.change({ + searchString: options.searchString ?? 'count', + isRevealed: true, + isReplaceRevealed: options.showReplace ?? false, + replaceString: options.replaceString ?? '', + }, false); + + // Wait for the CSS transition (top: -64px → 0, 200ms linear) + await new Promise(resolve => setTimeout(resolve, 300)); +} + +export default defineThemedFixtureGroup({ + Find: defineComponentFixture({ + render: (context) => renderFindWidget({ ...context, searchString: 'count' }), + }), + FindAndReplace: defineComponentFixture({ + render: (context) => renderFindWidget({ ...context, searchString: 'count', replaceString: 'value', showReplace: true }), + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts new file mode 100644 index 0000000000000..bbd8420665cdb --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts @@ -0,0 +1,306 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { constObservable, IObservableWithChange } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from './fixtureUtils.js'; +import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { IEditorOptions } from '../../../../editor/common/config/editorOptions.js'; +import { Position } from '../../../../editor/common/core/position.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; +import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; +import '../../../../editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.js'; +import { InlineCompletionsSource, InlineCompletionsState } from '../../../../editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.js'; +import { InlineEditItem } from '../../../../editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.js'; +import { TextModelValueReference } from '../../../../editor/contrib/inlineCompletions/browser/model/textModelValueReference.js'; +import { JumpToView } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/jumpToView.js'; +import { IUserInteractionService, MockUserInteractionService } from '../../../../platform/userInteraction/browser/userInteractionService.js'; + +import '../../../../editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.css'; +import '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css'; +import '../../../../base/browser/ui/codicons/codiconStyles.js'; + +const SAMPLE_CODE = `function fibonacci(n: number): number { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +} + +const result = fibonacci(10); +console.log(result); +`; + +const LONG_DISTANCE_CODE = `import { readFile, writeFile } from 'fs'; +import { join } from 'path'; + +interface Config { + inputDir: string; + outputDir: string; + verbose: boolean; +} + +function loadConfig(): Config { + return { + inputDir: './input', + outputDir: './output', + verbose: false, + }; +} + +function processLine(line: string): string { + return line.trim().toUpperCase(); +} + +function validateInput(data: string): boolean { + return data.length > 0 && data.length < 10000; +} + +async function processFile(config: Config, filename: string): Promise { + const inputPath = join(config.inputDir, filename); + const data = await readFile(inputPath, 'utf8'); + if (!validateInput(data)) { + throw new Error('Invalid input'); + } + const lines = data.split('\\n'); + const processed = lines.map(processLine); + const outputPath = join(config.outputDir, filename); + await writeFile(outputPath, processed.join('\\n')); + if (config.verbose) { + console.log(\`Processed \${filename}\`); + } +} + +async function main() { + const config = loadConfig(); + const files = ['a.txt', 'b.txt', 'c.txt']; + for (const file of files) { + await processFile(config, file); + } +} + +main(); +`; + +interface HintsToolbarOptions extends ComponentFixtureContext { + simulateHover?: boolean; +} + +const HINTS_CODE = `function greet(name: string): string { + return "Hello, " + name +} + +greet("World"); +`; + +async function renderHintsToolbar(options: HintsToolbarOptions): Promise { + const { container, disposableStore, theme } = options; + container.style.width = '500px'; + container.style.height = '180px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: theme, + additionalServices: (reg) => { + registerWorkbenchServices(reg); + if (options.simulateHover) { + reg.defineInstance(IUserInteractionService, new MockUserInteractionService(true, true)); + } + }, + }); + + const textModel = disposableStore.add(createTextModel( + instantiationService, + HINTS_CODE, + URI.parse('inmemory://hints-toolbar.ts'), + 'typescript' + )); + + // Register an inline completion provider (not an inline edit) so the result is ghost text + const languageFeaturesService = instantiationService.get(ILanguageFeaturesService); + disposableStore.add(languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, { + provideInlineCompletions: () => ({ + items: [{ + insertText: ' + "!";', + range: new Range(2, 28, 2, 28), + }], + }), + disposeInlineCompletions: () => { }, + })); + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 14, + cursorBlinking: 'solid', + inlineSuggest: { showToolbar: 'always' }, + }, + { contributions: EditorExtensionsRegistry.getEditorContributions() } satisfies ICodeEditorWidgetOptions + )); + + editor.setModel(textModel); + editor.setPosition({ lineNumber: 2, column: 28 }); + editor.focus(); + + const controller = InlineCompletionsController.get(editor); + controller?.model?.get()?.triggerExplicitly(); + + await new Promise(resolve => setTimeout(resolve, 100)); +} + +function renderJumpToHint({ container, disposableStore, theme }: ComponentFixtureContext): void { + container.style.width = '500px'; + container.style.height = '200px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { colorTheme: theme }); + + const textModel = disposableStore.add(createTextModel( + instantiationService, + SAMPLE_CODE, + URI.parse('inmemory://jump-to-hint.ts'), + 'typescript' + )); + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 14, + cursorBlinking: 'solid', + }, + { contributions: [] } satisfies ICodeEditorWidgetOptions + )); + + editor.setModel(textModel); + editor.setPosition({ lineNumber: 1, column: 1 }); + editor.focus(); + + const editorObs = observableCodeEditor(editor); + disposableStore.add(instantiationService.createInstance( + JumpToView, + editorObs, + { style: 'label' }, + constObservable({ jumpToPosition: new Position(6, 18) }), + )); +} + +function createLongDistanceEditor(options: { + container: HTMLElement; + disposableStore: import('../../../../base/common/lifecycle.js').DisposableStore; + theme: import('./fixtureUtils.js').ComponentFixtureContext['theme']; + code: string; + cursorLine: number; + editRange: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number }; + newText: string; + editorOptions?: IEditorOptions; +}): void { + const { container, disposableStore, theme } = options; + container.style.width = '600px'; + container.style.height = '500px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { colorTheme: theme }); + + const textModel = disposableStore.add(createTextModel( + instantiationService, + options.code, + URI.parse('inmemory://long-distance.ts'), + 'typescript' + )); + + instantiationService.stubInstance(InlineCompletionsSource, { + cancelUpdate: () => { }, + clear: () => { }, + clearOperationOnTextModelChange: constObservable(undefined) as IObservableWithChange, + clearSuggestWidgetInlineCompletions: () => { }, + dispose: () => { }, + fetch: async () => true, + inlineCompletions: constObservable(new InlineCompletionsState([ + InlineEditItem.createForTest( + TextModelValueReference.snapshot(textModel), + new Range( + options.editRange.startLineNumber, + options.editRange.startColumn, + options.editRange.endLineNumber, + options.editRange.endColumn + ), + options.newText + ) + ], undefined)), + loading: constObservable(false), + seedInlineCompletionsWithSuggestWidget: () => { }, + seedWithCompletion: () => { }, + suggestWidgetInlineCompletions: constObservable(InlineCompletionsState.createEmpty()), + }); + + const editorWidgetOptions: ICodeEditorWidgetOptions = { + contributions: EditorExtensionsRegistry.getEditorContributions() + }; + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 14, + cursorBlinking: 'solid', + inlineSuggest: { + edits: { showLongDistanceHint: true }, + }, + ...options.editorOptions, + }, + editorWidgetOptions + )); + + editor.setModel(textModel); + editor.setPosition({ lineNumber: options.cursorLine, column: 1 }); + editor.focus(); + + const controller = InlineCompletionsController.get(editor); + controller?.model?.get(); +} + +export default defineThemedFixtureGroup({ + HintsToolbar: defineComponentFixture({ + render: (context) => renderHintsToolbar(context), + }), + HintsToolbarHovered: defineComponentFixture({ + render: (context) => renderHintsToolbar({ ...context, simulateHover: true }), + }), + JumpToHint: defineComponentFixture({ + render: renderJumpToHint, + }), + LongDistanceHint: defineComponentFixture({ + render: (context) => createLongDistanceEditor({ + ...context, + code: LONG_DISTANCE_CODE, + cursorLine: 1, + editRange: { startLineNumber: 28, startColumn: 1, endLineNumber: 35, endColumn: 100 }, + newText: `async function processFile(config: Config, filename: string): Promise { + const inputPath = join(config.inputDir, filename); + const outputPath = join(config.outputDir, filename); + const data = await readFile(inputPath, 'utf8'); + if (!validateInput(data)) { + throw new Error(\`Invalid input in \${filename}\`); + } + const processed = data.split('\\n').map(processLine).join('\\n'); + await writeFile(outputPath, processed);`, + }), + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts new file mode 100644 index 0000000000000..82416f4872ccd --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { RenameWidget } from '../../../../editor/contrib/rename/browser/renameWidget.js'; + +import '../../../../editor/contrib/rename/browser/renameWidget.css'; +import '../../../../base/browser/ui/codicons/codiconStyles.js'; + +const SAMPLE_CODE = `class UserService { + private _users: Map = new Map(); + + getUser(userId: string): User | undefined { + return this._users.get(userId); + } + + addUser(user: User): void { + this._users.set(user.id, user); + } +} +`; + +interface RenameFixtureOptions extends ComponentFixtureContext { + cursorLine: number; + cursorColumn: number; + currentName: string; + rangeStartColumn: number; + rangeEndColumn: number; +} + +function renderRenameWidget(options: RenameFixtureOptions): void { + const { container, disposableStore, theme } = options; + container.style.width = '500px'; + container.style.height = '280px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { colorTheme: theme }); + + const textModel = disposableStore.add(createTextModel( + instantiationService, + SAMPLE_CODE, + URI.parse('inmemory://rename-fixture.ts'), + 'typescript' + )); + + const editorWidgetOptions: ICodeEditorWidgetOptions = { + contributions: [] + }; + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 14, + cursorBlinking: 'solid', + }, + editorWidgetOptions + )); + + editor.setModel(textModel); + editor.setPosition({ lineNumber: options.cursorLine, column: options.cursorColumn }); + + const renameWidget = instantiationService.createInstance( + RenameWidget, + editor, + ['editor.action.rename', 'editor.action.rename'], + ); + disposableStore.add(renameWidget); + + const cts = new CancellationTokenSource(); + disposableStore.add(cts); + + renameWidget.getInput( + { + startLineNumber: options.cursorLine, + startColumn: options.rangeStartColumn, + endLineNumber: options.cursorLine, + endColumn: options.rangeEndColumn, + }, + options.currentName, + false, + undefined, + cts + ); +} + +export default defineThemedFixtureGroup({ + RenameVariable: defineComponentFixture({ + render: (context) => renderRenameWidget({ + ...context, + cursorLine: 4, + cursorColumn: 2, + currentName: 'getUser', + rangeStartColumn: 2, + rangeEndColumn: 9, + }), + }), + RenameClass: defineComponentFixture({ + render: (context) => renderRenameWidget({ + ...context, + cursorLine: 1, + cursorColumn: 7, + currentName: 'UserService', + rangeStartColumn: 7, + rangeEndColumn: 18, + }), + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts new file mode 100644 index 0000000000000..623650a7a8d64 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../base/common/uri.js'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; +import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { EditorOption, IEditorOptions } from '../../../../editor/common/config/editorOptions.js'; +import { CompletionItemKind, CompletionList } from '../../../../editor/common/languages.js'; +import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js'; +import { ISuggestMemoryService } from '../../../../editor/contrib/suggest/browser/suggestMemory.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { IMenuChangeEvent, IMenuService } from '../../../../platform/actions/common/actions.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { CompletionModel, LineContext } from '../../../../editor/contrib/suggest/browser/completionModel.js'; +import { CompletionItem } from '../../../../editor/contrib/suggest/browser/suggest.js'; +import { WordDistance } from '../../../../editor/contrib/suggest/browser/wordDistance.js'; +import { FuzzyScoreOptions } from '../../../../base/common/filters.js'; + +// CSS imports for the suggest widget +import '../../../../editor/contrib/suggest/browser/media/suggest.css'; +import '../../../../editor/contrib/symbolIcons/browser/symbolIcons.js'; +import '../../../../base/browser/ui/codicons/codiconStyles.js'; + +interface SuggestFixtureOptions extends ComponentFixtureContext { + code: string; + cursorLine: number; + cursorColumn: number; + completions: CompletionList; + width?: string; + height?: string; + editorOptions?: IEditorOptions; +} + +async function renderSuggestWidget(options: SuggestFixtureOptions): Promise { + const { container, disposableStore, theme } = options; + container.style.width = options.width ?? '500px'; + container.style.height = options.height ?? '300px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: theme, + additionalServices: (reg) => { + reg.defineInstance(ISuggestMemoryService, new class extends mock() { + override memorize(): void { } + override select(): number { return 0; } + }); + reg.defineInstance(IMenuService, new class extends mock() { + override createMenu() { + return { onDidChange: new Emitter().event, getActions: () => [], dispose: () => { } }; + } + }); + }, + }); + + const textModel = disposableStore.add(createTextModel( + instantiationService, + options.code, + URI.parse('inmemory://suggest-fixture.ts'), + 'typescript' + )); + + const editorWidgetOptions: ICodeEditorWidgetOptions = { + contributions: EditorExtensionsRegistry.getEditorContributions() + }; + + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + fontSize: 14, + cursorBlinking: 'solid', + suggest: { + showIcons: true, + showStatusBar: true, + }, + ...options.editorOptions, + }, + editorWidgetOptions + )); + + editor.setModel(textModel); + const position = { lineNumber: options.cursorLine, column: options.cursorColumn }; + editor.setPosition(position); + editor.focus(); + + const controller = SuggestController.get(editor)!; + const widget = controller.widget.value; + + const completionList = options.completions; + const provider = { _debugDisplayName: 'suggestFixture', provideCompletionItems: () => completionList }; + const items = completionList.suggestions.map(s => new CompletionItem(position, s, completionList, provider)); + + const lineContent = textModel.getLineContent(position.lineNumber); + const leadingLineContent = lineContent.substring(0, position.column - 1); + + const completionModel = new CompletionModel( + items, + position.column, + new LineContext(leadingLineContent, 0), + WordDistance.None, + editor.getOption(EditorOption.suggest), + 'inline', + FuzzyScoreOptions.default, + undefined + ); + + widget.showSuggestions(completionModel, 0, false, false, false); +} + +const typescriptCompletions: CompletionList = { + suggestions: [ + { label: 'addEventListener', kind: CompletionItemKind.Method, insertText: 'addEventListener', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 }, detail: '(method) addEventListener(type: string, listener: EventListener): void' }, + { label: 'appendChild', kind: CompletionItemKind.Method, insertText: 'appendChild', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 }, detail: '(method) appendChild(node: Node): Node' }, + { label: 'attributes', kind: CompletionItemKind.Property, insertText: 'attributes', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 } }, + { label: 'blur', kind: CompletionItemKind.Method, insertText: 'blur', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 }, detail: '(method) blur(): void' }, + { label: 'childElementCount', kind: CompletionItemKind.Property, insertText: 'childElementCount', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 } }, + { label: 'children', kind: CompletionItemKind.Property, insertText: 'children', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 } }, + { label: 'classList', kind: CompletionItemKind.Property, insertText: 'classList', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 } }, + { label: 'className', kind: CompletionItemKind.Property, insertText: 'className', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 } }, + { label: 'click', kind: CompletionItemKind.Method, insertText: 'click', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 }, detail: '(method) click(): void' }, + { label: 'cloneNode', kind: CompletionItemKind.Method, insertText: 'cloneNode', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 }, detail: '(method) cloneNode(deep?: boolean): Node' }, + { label: 'closest', kind: CompletionItemKind.Method, insertText: 'closest', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 }, detail: '(method) closest(selectors: string): Element | null' }, + { label: 'contains', kind: CompletionItemKind.Method, insertText: 'contains', range: { startLineNumber: 3, startColumn: 10, endLineNumber: 3, endColumn: 10 }, detail: '(method) contains(other: Node | null): boolean' }, + ], +}; + +const mixedKindCompletions: CompletionList = { + suggestions: [ + { label: 'MyClass', kind: CompletionItemKind.Class, insertText: 'MyClass', range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, detail: 'class MyClass' }, + { label: 'myFunction', kind: CompletionItemKind.Function, insertText: 'myFunction', range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, detail: 'function myFunction(): void' }, + { label: 'myVariable', kind: CompletionItemKind.Variable, insertText: 'myVariable', range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, detail: 'const myVariable: string' }, + { label: 'IMyInterface', kind: CompletionItemKind.Interface, insertText: 'IMyInterface', range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, detail: 'interface IMyInterface' }, + { label: 'MyEnum', kind: CompletionItemKind.Enum, insertText: 'MyEnum', range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, detail: 'enum MyEnum' }, + { label: 'MY_CONSTANT', kind: CompletionItemKind.Constant, insertText: 'MY_CONSTANT', range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, detail: 'const MY_CONSTANT = 42' }, + { label: 'myKeyword', kind: CompletionItemKind.Keyword, insertText: 'myKeyword', range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 } }, + { label: 'mySnippet', kind: CompletionItemKind.Snippet, insertText: 'mySnippet', range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, detail: 'snippet' }, + ], +}; + +export default defineThemedFixtureGroup({ + MethodCompletions: defineComponentFixture({ + render: (context) => renderSuggestWidget({ + ...context, + code: `const element = document.getElementById('app'); +if (element) { + element. +}`, + cursorLine: 3, + cursorColumn: 10, + completions: typescriptCompletions, + }), + }), + + MixedKinds: defineComponentFixture({ + render: (context) => renderSuggestWidget({ + ...context, + code: '', + cursorLine: 1, + cursorColumn: 1, + completions: mixedKindCompletions, + }), + }), +}); From 21ec368773df9cef955e1ca645ca89db0c0d24f0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 26 Feb 2026 16:21:50 +0100 Subject: [PATCH 23/28] sessions - fix error on startup around duplicate view registration (#298015) --- src/vs/sessions/contrib/chat/browser/chat.contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index 165307b8a3e98..6568bdfdd234f 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -139,11 +139,11 @@ class RegisterChatViewContainerContribution implements IWorkbenchContribution { const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); let chatViewContainer = viewContainerRegistry.get(ChatViewContainerId); if (chatViewContainer) { - viewContainerRegistry.deregisterViewContainer(chatViewContainer); const view = viewsRegistry.getView(ChatViewId); if (view) { viewsRegistry.deregisterViews([view], chatViewContainer); } + viewContainerRegistry.deregisterViewContainer(chatViewContainer); } chatViewContainer = viewContainerRegistry.registerViewContainer({ From a6e7ca8ceebe1b1c07e497002ccf960562bca043 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Thu, 26 Feb 2026 16:22:34 +0100 Subject: [PATCH 24/28] sync changes action for git synchronization in agent sessions --- .../changesView/browser/changesView.ts | 3 + .../gitSync/browser/gitSync.contribution.ts | 117 ++++++++++++++++++ src/vs/sessions/sessions.desktop.main.ts | 1 + 3 files changed, 121 insertions(+) create mode 100644 src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 973e8e2ba1b4b..d437e6e19a032 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -565,6 +565,9 @@ export class ChangesViewPane extends ViewPane { if (action.id === 'github.createPullRequest') { return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow' }; } + if (action.id === 'chatEditing.synchronizeChanges') { + return { showIcon: true, showLabel: true, isSecondary: true }; + } return undefined; } } diff --git a/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts b/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts new file mode 100644 index 0000000000000..45e2dcf0af3b3 --- /dev/null +++ b/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { localize } from '../../../../nls.js'; +import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; +import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; +import { IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; + +const hasGitSyncChangesContextKey = new RawContextKey('agentSessionHasGitSyncChanges', false, { + type: 'boolean', + description: localize('agentSessionHasGitSyncChanges', "True when the active agent session worktree has ahead or behind commits relative to its upstream.") +}); + +class GitSyncContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'sessions.contrib.gitSync'; + + private readonly _syncActionDisposable = this._register(new MutableDisposable()); + private readonly _gitRepoDisposables = this._register(new DisposableStore()); + + constructor( + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService, + @IGitService private readonly gitService: IGitService, + ) { + super(); + + const contextKey = hasGitSyncChangesContextKey.bindTo(this.contextKeyService); + + this._register(autorun(reader => { + const activeSession = this.sessionManagementService.activeSession.read(reader); + this._gitRepoDisposables.clear(); + + const worktreeUri = activeSession ? this.sessionManagementService.getActiveSession()?.worktree : undefined; + if (!worktreeUri) { + this._syncActionDisposable.clear(); + contextKey.set(false); + return; + } + + this.gitService.openRepository(worktreeUri).then(repository => { + if (!repository) { + this._syncActionDisposable.clear(); + contextKey.set(false); + return; + } + this._gitRepoDisposables.add(autorun(innerReader => { + const state = repository.state.read(innerReader); + const head = state.HEAD; + if (!head?.upstream) { + this._syncActionDisposable.clear(); + contextKey.set(false); + return; + } + const ahead = head.ahead ?? 0; + const behind = head.behind ?? 0; + const hasSyncChanges = ahead > 0 || behind > 0; + contextKey.set(hasSyncChanges); + this._syncActionDisposable.value = registerSyncAction(ahead, behind); + })); + }); + })); + } +} + +function registerSyncAction(behind: number, ahead: number): IDisposable { + if (behind === 0 && ahead === 0) { + return Disposable.None; + } + let title = ''; + if (behind > 0) { + title += `${behind}↓ `; + } + if (ahead > 0) { + title += `${ahead}↑`; + } + + class SynchronizeChangesAction extends Action2 { + static readonly ID = 'chatEditing.synchronizeChanges'; + + constructor() { + super({ + id: SynchronizeChangesAction.ID, + title, + tooltip: localize('synchronizeChanges', "Synchronize Changes with Git (Behind {0}, Ahead {1})", behind, ahead), + icon: Codicon.sync, + category: CHAT_CATEGORY, + menu: [ + { + id: MenuId.ChatEditingSessionChangesToolbar, + group: 'navigation', + order: 5, + when: hasGitSyncChangesContextKey, + }, + ], + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand('git.sync'); + } + } + return registerAction2(SynchronizeChangesAction); +} + +registerWorkbenchContribution2(GitSyncContribution.ID, GitSyncContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 34cffa0159b21..4570cb5ff6812 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -207,6 +207,7 @@ import './contrib/chat/browser/chat.contribution.js'; import './contrib/sessions/browser/sessions.contribution.js'; import './contrib/sessions/browser/customizationsToolbar.contribution.js'; import './contrib/changesView/browser/changesView.contribution.js'; +import './contrib/gitSync/browser/gitSync.contribution.js'; import './contrib/fileTreeView/browser/fileTreeView.contribution.js'; // view registration disabled; filesystem provider still needed import './contrib/configuration/browser/configuration.contribution.js'; From 0371658b576012159ca56009ff202054c6ff2b3e Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 26 Feb 2026 15:33:54 +0000 Subject: [PATCH 25/28] fix: improve notification styles and adjust shadow properties --- extensions/theme-2026/themes/styles.css | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 11bd6ee590bf1..090a5d7954ad6 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -236,26 +236,35 @@ /* Notifications */ -.monaco-workbench .notifications-toasts { - box-shadow: var(--shadow-lg); - border-radius: var(--radius-sm); +.monaco-workbench .notifications-toasts, +.monaco-workbench > .notifications-toasts .notification-toast-container { + overflow: visible; } .monaco-workbench .notification-toast { box-shadow: none !important; - margin: 0 !important; } -.monaco-workbench .notification-toast-container { +.monaco-workbench .notifications-list-container { backdrop-filter: var(--backdrop-blur-md); -webkit-backdrop-filter: var(--backdrop-blur-md); background: color-mix(in srgb, var(--vscode-notifications-background) 60%, transparent) !important; + box-shadow: var(--shadow-lg) !important; } -.monaco-workbench.vs-dark .notification-toast-container { +.monaco-workbench .notifications-center .notifications-list-container { + box-shadow: none !important; +} + +.monaco-workbench .notification-list-item, +.monaco-workbench .notifications-center-header { background: color-mix(in srgb, var(--vscode-notifications-background) 60%, transparent) !important; } +.monaco-workbench > .notifications-toasts .notification-toast-container > .notification-toast { + opacity: 1; +} + .monaco-workbench .notification-toast-container .notification-toast { background-color: transparent !important; } @@ -272,8 +281,8 @@ background: color-mix(in srgb, var(--vscode-notifications-background) 60%, transparent) !important; } -.monaco-workbench .notifications-list-container, -.monaco-workbench > .notifications-center > .notifications-center-header, +/* .monaco-workbench .notifications-list-container, +.monaco-workbench > .notifications-center > .notifications-center-header, */ .monaco-workbench .notifications-list-container .monaco-list-rows { background: transparent !important; } From d38271c0505c2783c9f5d2ec2072ff8c5c954760 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 26 Feb 2026 15:55:51 +0000 Subject: [PATCH 26/28] feat: update @vscode/codicons to version 0.0.45-11 and add new 'new-session' icon --- package-lock.json | 8 ++++---- package.json | 2 +- remote/web/package-lock.json | 8 ++++---- remote/web/package.json | 2 +- src/vs/base/common/codiconsLibrary.ts | 1 + 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index d0ebb1204c211..bb626ac34e2b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-10", + "@vscode/codicons": "^0.0.45-11", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", @@ -3059,9 +3059,9 @@ ] }, "node_modules/@vscode/codicons": { - "version": "0.0.45-10", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-10.tgz", - "integrity": "sha512-05CYIpwSYKEQN0qBnfmLC/5VcasOwmeLsl3SGj944UyJ1/vJQpqL153A+0xh4geYEeqcOtIc42emmCizsCzf0Q==", + "version": "0.0.45-11", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-11.tgz", + "integrity": "sha512-fLjx4i7pfSYJJzzmQ6tZnshWWSLYUfg8Ru6xNRBWRSFj8yZkuuXEZGMxju4mt/tuu8Y/gjhEGmIVmVC16fg+yQ==", "license": "CC-BY-4.0" }, "node_modules/@vscode/component-explorer": { diff --git a/package.json b/package.json index 64ad8d3954fa9..fd8a39f10a865 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", - "@vscode/codicons": "^0.0.45-10", + "@vscode/codicons": "^0.0.45-11", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 1b80d4aedeac4..6767fc211f02b 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-10", + "@vscode/codicons": "^0.0.45-11", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", @@ -73,9 +73,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@vscode/codicons": { - "version": "0.0.45-10", - "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-10.tgz", - "integrity": "sha512-05CYIpwSYKEQN0qBnfmLC/5VcasOwmeLsl3SGj944UyJ1/vJQpqL153A+0xh4geYEeqcOtIc42emmCizsCzf0Q==", + "version": "0.0.45-11", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-11.tgz", + "integrity": "sha512-fLjx4i7pfSYJJzzmQ6tZnshWWSLYUfg8Ru6xNRBWRSFj8yZkuuXEZGMxju4mt/tuu8Y/gjhEGmIVmVC16fg+yQ==", "license": "CC-BY-4.0" }, "node_modules/@vscode/iconv-lite-umd": { diff --git a/remote/web/package.json b/remote/web/package.json index 69493c7779ade..29fb739234012 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@vscode/codicons": "^0.0.45-10", + "@vscode/codicons": "^0.0.45-11", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index f541d4face8e6..1a2d78bcd70ef 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -655,4 +655,5 @@ export const codiconsLibrary = { openai: register('openai', 0xec81), claude: register('claude', 0xec82), openInWindow: register('open-in-window', 0xec83), + newSession: register('new-session', 0xec84), } as const; From cac4044503d2fda1deb338a1e9c283535978a9ad Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 26 Feb 2026 17:04:12 +0100 Subject: [PATCH 27/28] - fix listening to active session (#298012) * - fix listening to active session - new empty workbench workspace service for sessions - add active workbench folder to workspace * fix tests * feedback * fix compilation * fix compilation --- eslint.config.js | 27 ++- src/vs/sessions/browser/workbench.ts | 9 - .../contrib/chat/browser/chat.contribution.ts | 3 +- .../browser/configuration.contribution.ts | 1 - .../browser/sessionsManagementService.ts | 32 +++- .../browser/sessionsTerminalContribution.ts | 7 +- .../sessionsTerminalContribution.test.ts | 11 +- .../electron-browser/sessions.main.ts | 65 +++----- .../browser/configurationService.ts | 27 +++ .../browser/workspaceContextService.ts | 154 ++++++++++++++++++ src/vs/sessions/sessions.desktop.main.ts | 1 - 11 files changed, 265 insertions(+), 72 deletions(-) create mode 100644 src/vs/sessions/services/configuration/browser/configurationService.ts create mode 100644 src/vs/sessions/services/workspace/browser/workspaceContextService.ts diff --git a/eslint.config.js b/eslint.config.js index 93a8a1b7b4396..29ffd569c6085 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1955,7 +1955,8 @@ export default tseslint.config( 'vs/workbench/browser/**', 'vs/workbench/contrib/**', 'vs/workbench/services/*/~', - 'vs/sessions/~' + 'vs/sessions/~', + 'vs/sessions/services/*/~' ] }, { @@ -1974,6 +1975,30 @@ export default tseslint.config( 'vs/sessions/contrib/*/~' ] }, + { + 'target': 'src/vs/sessions/services/*/~', + 'restrictions': [ + 'vs/base/~', + 'vs/base/parts/*/~', + 'vs/platform/*/~', + 'vs/editor/~', + 'vs/editor/contrib/*/~', + 'vs/workbench/~', + 'vs/workbench/services/*/~', + { + 'when': 'test', + 'pattern': 'vs/workbench/contrib/*/~' + }, // TODO@layers + 'tas-client', // node module allowed even in /common/ + 'vscode-textmate', // node module allowed even in /common/ + '@vscode/vscode-languagedetection', // node module allowed even in /common/ + '@vscode/tree-sitter-wasm', // type import + { + 'when': 'hasBrowser', + 'pattern': '@xterm/xterm' + } // node module allowed even in /browser/ + ] + }, ] } }, diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index 66e41f3a74276..24b0d19b0d475 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -398,15 +398,6 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService { // Wrap up instantiationService.invokeFunction(accessor => { const lifecycleService = accessor.get(ILifecycleService); - - // TODO@Sandeep debt around cyclic dependencies - const configurationService = accessor.get(IConfigurationService); - // eslint-disable-next-line local/code-no-in-operator - if (configurationService && 'acquireInstantiationService' in configurationService) { - (configurationService as { acquireInstantiationService: (instantiationService: unknown) => void }).acquireInstantiationService(instantiationService); - } - - // Signal to lifecycle that services are set lifecycleService.phase = LifecyclePhase.Ready; }); diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index 6568bdfdd234f..0ace6677cb836 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -17,7 +17,6 @@ import { IViewContainersRegistry, IViewsRegistry, ViewContainerLocation, Extensi import { Registry } from '../../../../platform/registry/common/platform.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; -import { isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { ISessionsManagementService, IsNewChatSessionContext } from '../../sessions/browser/sessionsManagementService.js'; import { Menus } from '../../../browser/menus.js'; import { BranchChatSessionAction } from './branchChatSessionAction.js'; @@ -67,7 +66,7 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 { return; } - const folderUri = isAgentSession(activeSession) && activeSession.providerType !== AgentSessionProviders.Cloud ? activeSession.worktree : undefined; + const folderUri = activeSession.providerType === AgentSessionProviders.Background ? activeSession?.worktree ?? activeSession?.repository : undefined; if (!folderUri) { return; diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index bbd83462075ee..b1730dd1897e3 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -41,7 +41,6 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'workbench.tips.enabled': false, 'workbench.layoutControl.type': 'toggles', 'workbench.editor.useModal': 'all', - 'workbench.editor.labelFormat': 'short', 'workbench.panel.showLabels': false, 'window.menuStyle': 'custom', 'window.dialogStyle': 'custom', diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index 7ba297981dd98..1100d36cc0086 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -24,6 +24,8 @@ import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browse import { INewSession, LocalNewSession, RemoteNewSession } from '../../chat/browser/newSession.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; +import { IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; export const IsNewChatSessionContext = new RawContextKey('isNewChatSession', true); @@ -43,6 +45,7 @@ export interface IActiveSessionItem { readonly label: string | undefined; readonly repository: URI | undefined; readonly worktree: URI | undefined; + readonly providerType: string; } export interface ISessionsManagementService { @@ -117,6 +120,8 @@ export class SessionsManagementService extends Disposable implements ISessionsMa @IContextKeyService contextKeyService: IContextKeyService, @ICommandService private readonly commandService: ICommandService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, ) { super(); @@ -396,7 +401,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa return; } this.isNewChatSessionContext.set(true); - this._activeSession.set(undefined, undefined); + this.setActiveSession(undefined); } private setActiveSession(session: IAgentSession | INewSession | undefined): void { @@ -412,6 +417,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa resource: session.resource, repository, worktree, + providerType: session.providerType, }; } else { activeSessionItem = { @@ -420,21 +426,27 @@ export class SessionsManagementService extends Disposable implements ISessionsMa resource: session.resource, repository: session.repoUri, worktree: undefined, + providerType: session.target, }; this._activeSessionDisposables.add(session.onDidChange(e => { if (e === 'repoUri') { - this._activeSession.set({ + this.doSetActiveSession({ isUntitled: true, label: undefined, resource: session.resource, repository: session.repoUri, worktree: undefined, - }, undefined); + providerType: session.target, + }); } })); } } + this.doSetActiveSession(activeSessionItem); + } + + private doSetActiveSession(activeSessionItem: IActiveSessionItem | undefined): void { if (equals(this._activeSession.get(), activeSessionItem)) { return; } @@ -444,6 +456,20 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } else { this.logService.trace('[ActiveSessionService] Active session cleared'); } + + const currentRepo = this.workspaceContextService.getWorkspace().folders[0]?.uri; + const activeSessionRepo = activeSessionItem?.providerType === AgentSessionProviders.Background ? activeSessionItem?.worktree ?? activeSessionItem?.repository : undefined; + if (activeSessionRepo) { + if (currentRepo) { + if (!this.uriIdentityService.extUri.isEqual(currentRepo, activeSessionRepo)) { + this.workspaceEditingService.updateFolders(0, 1, [{ uri: activeSessionRepo }], true); + } + } else { + this.workspaceEditingService.addFolders([{ uri: activeSessionRepo }], true); + } + } else { + this.workspaceEditingService.removeFolders([currentRepo], true); + } this._activeSession.set(activeSessionItem, undefined); } diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index de2276a85a95a..38c8e785ebb74 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -12,8 +12,6 @@ import { localize2 } from '../../../../nls.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IWorkbenchContribution, getWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; -import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; -import { isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; @@ -28,10 +26,7 @@ import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; * sessions, repository otherwise, or `undefined` when neither is available. */ function getSessionCwd(session: IActiveSessionItem | undefined): URI | undefined { - if (isAgentSession(session) && session.providerType !== AgentSessionProviders.Cloud) { - return session.worktree ?? session.repository; - } - return session?.repository; + return session?.worktree ?? session?.repository; } /** diff --git a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts index aba8ca51fce95..5df597398ff60 100644 --- a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts +++ b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts @@ -132,14 +132,15 @@ suite('SessionsTerminalContribution', () => { assert.strictEqual(createdTerminals[0].cwd.fsPath, worktreeUri.fsPath); }); - test('creates a terminal with repository for cloud agent sessions', async () => { + test('reate a terminal with repository for cloud agent sessions', async () => { const repoUri = URI.file('/repo'); - const session = makeAgentSession({ worktree: URI.file('/worktree'), repository: repoUri, providerType: AgentSessionProviders.Cloud }); + const workTree = URI.file('/worktree'); + const session = makeAgentSession({ worktree: workTree, repository: repoUri, providerType: AgentSessionProviders.Cloud }); activeSessionObs.set(session, undefined); await tick(); assert.strictEqual(createdTerminals.length, 1); - assert.strictEqual(createdTerminals[0].cwd.fsPath, repoUri.fsPath); + assert.strictEqual(createdTerminals[0].cwd.fsPath, workTree.fsPath); }); test('creates a terminal with repository for non-agent sessions', async () => { @@ -317,7 +318,7 @@ suite('SessionsTerminalContribution', () => { assert.strictEqual(createdTerminals[0].cwd.fsPath, repoUri.fsPath); }); - test('uses repository for cloud agent session even when worktree exists', async () => { + test('does not use repository for cloud agent session when worktree exists', async () => { const worktreeUri = URI.file('/worktree'); const repoUri = URI.file('/repo'); const session = makeAgentSession({ @@ -328,7 +329,7 @@ suite('SessionsTerminalContribution', () => { activeSessionObs.set(session, undefined); await tick(); - assert.strictEqual(createdTerminals[0].cwd.fsPath, repoUri.fsPath); + assert.strictEqual(createdTerminals[0].cwd.fsPath, worktreeUri.fsPath); }); // --- switching back to previously used path reuses terminal --- diff --git a/src/vs/sessions/electron-browser/sessions.main.ts b/src/vs/sessions/electron-browser/sessions.main.ts index 87669d73043a2..23f71e9d66a26 100644 --- a/src/vs/sessions/electron-browser/sessions.main.ts +++ b/src/vs/sessions/electron-browser/sessions.main.ts @@ -11,12 +11,11 @@ import { setFullscreen } from '../../base/browser/browser.js'; import { domContentLoaded } from '../../base/browser/dom.js'; import { onUnexpectedError } from '../../base/common/errors.js'; import { URI } from '../../base/common/uri.js'; -import { WorkspaceService } from '../../workbench/services/configuration/browser/configurationService.js'; import { INativeWorkbenchEnvironmentService, NativeWorkbenchEnvironmentService } from '../../workbench/services/environment/electron-browser/environmentService.js'; import { ServiceCollection } from '../../platform/instantiation/common/serviceCollection.js'; import { ILoggerService, ILogService, LogLevel } from '../../platform/log/common/log.js'; import { NativeWorkbenchStorageService } from '../../workbench/services/storage/electron-browser/storageService.js'; -import { IWorkspaceContextService, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IAnyWorkspaceIdentifier, reviveIdentifier, toWorkspaceIdentifier } from '../../platform/workspace/common/workspace.js'; +import { IWorkspaceContextService, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IAnyWorkspaceIdentifier, reviveIdentifier } from '../../platform/workspace/common/workspace.js'; import { IWorkbenchConfigurationService } from '../../workbench/services/configuration/common/configuration.js'; import { IStorageService } from '../../platform/storage/common/storage.js'; import { Disposable } from '../../base/common/lifecycle.js'; @@ -30,7 +29,6 @@ import { IRemoteAgentService } from '../../workbench/services/remote/common/remo import { FileService } from '../../platform/files/common/fileService.js'; import { IFileService } from '../../platform/files/common/files.js'; import { RemoteFileSystemProviderClient } from '../../workbench/services/remote/common/remoteFileSystemProviderClient.js'; -import { ConfigurationCache } from '../../workbench/services/configuration/common/configurationCache.js'; import { ISignService } from '../../platform/sign/common/sign.js'; import { IProductService } from '../../platform/product/common/productService.js'; import { IUriIdentityService } from '../../platform/uriIdentity/common/uriIdentity.js'; @@ -66,8 +64,11 @@ import { AccountPolicyService } from '../../workbench/services/policies/common/a import { MultiplexPolicyService } from '../../workbench/services/policies/common/multiplexPolicyService.js'; import { Workbench as AgenticWorkbench } from '../browser/workbench.js'; import { NativeMenubarControl } from '../../workbench/electron-browser/parts/titlebar/menubarControl.js'; +import { IWorkspaceEditingService } from '../../workbench/services/workspaces/common/workspaceEditing.js'; +import { ConfigurationService } from '../services/configuration/browser/configurationService.js'; +import { SessionsWorkspaceContextService } from '../services/workspace/browser/workspaceContextService.js'; -export class AgenticMain extends Disposable { +export class SessionsMain extends Disposable { constructor( private readonly configuration: INativeWindowConfiguration @@ -167,7 +168,7 @@ export class AgenticMain extends Disposable { this._register(workbench.onDidShutdown(() => this.dispose())); } - private async initServices(): Promise<{ serviceCollection: ServiceCollection; logService: ILogService; storageService: NativeWorkbenchStorageService; configurationService: WorkspaceService }> { + private async initServices(): Promise<{ serviceCollection: ServiceCollection; logService: ILogService; storageService: NativeWorkbenchStorageService; configurationService: ConfigurationService }> { const serviceCollection = new ServiceCollection(); @@ -290,13 +291,13 @@ export class AgenticMain extends Disposable { // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // Create services that require resolving in parallel - const workspace = this.resolveWorkspaceIdentifier(environmentService); - const [configurationService, storageService] = await Promise.all([ - this.createWorkspaceService(workspace, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService, policyService).then(service => { + // Workspace + const workspaceContextService = new SessionsWorkspaceContextService(uriIdentityService.extUri.joinPath(uriIdentityService.extUri.dirname(userDataProfilesService.profilesHome), 'agent-sessions.code-workspace'), uriIdentityService); + serviceCollection.set(IWorkspaceContextService, workspaceContextService); + serviceCollection.set(IWorkspaceEditingService, workspaceContextService); - // Workspace - serviceCollection.set(IWorkspaceContextService, service); + const [configurationService, storageService] = await Promise.all([ + this.createConfigurationService(userDataProfileService, fileService, logService, policyService).then(service => { // Configuration serviceCollection.set(IWorkbenchConfigurationService, service); @@ -304,7 +305,7 @@ export class AgenticMain extends Disposable { return service; }), - this.createStorageService(workspace, environmentService, userDataProfileService, userDataProfilesService, mainProcessService).then(service => { + this.createStorageService(workspaceContextService.getWorkspace(), environmentService, userDataProfileService, userDataProfilesService, mainProcessService).then(service => { // Storage serviceCollection.set(IStorageService, service); @@ -325,13 +326,9 @@ export class AgenticMain extends Disposable { const workspaceTrustEnablementService = new WorkspaceTrustEnablementService(configurationService, environmentService); serviceCollection.set(IWorkspaceTrustEnablementService, workspaceTrustEnablementService); - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService, fileService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, workspaceContextService, workspaceTrustEnablementService, fileService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); - // Update workspace trust so that configuration is updated accordingly - configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted()); - this._register(workspaceTrustManagementService.onDidChangeTrust(() => configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted()))); - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // @@ -346,39 +343,19 @@ export class AgenticMain extends Disposable { return { serviceCollection, logService, storageService, configurationService }; } - private resolveWorkspaceIdentifier(environmentService: INativeWorkbenchEnvironmentService): IAnyWorkspaceIdentifier { - - // Return early for when a folder or multi-root is opened - if (this.configuration.workspace) { - return this.configuration.workspace; - } - - // Otherwise, workspace is empty, so we derive an identifier - return toWorkspaceIdentifier(this.configuration.backupPath, environmentService.isExtensionDevelopment); - } - - private async createWorkspaceService( - workspace: IAnyWorkspaceIdentifier, - environmentService: INativeWorkbenchEnvironmentService, + private async createConfigurationService( userDataProfileService: IUserDataProfileService, - userDataProfilesService: IUserDataProfilesService, fileService: FileService, - remoteAgentService: IRemoteAgentService, - uriIdentityService: IUriIdentityService, logService: ILogService, policyService: IPolicyService - ): Promise { - const configurationCache = new ConfigurationCache([Schemas.file, Schemas.vscodeUserData] /* Cache all non native resources */, environmentService, fileService); - const workspaceService = new WorkspaceService({ remoteAuthority: environmentService.remoteAuthority, configurationCache }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService, policyService); - + ): Promise { + const configurationService = new ConfigurationService(userDataProfileService.currentProfile.settingsResource, fileService, policyService, logService); try { - await workspaceService.initialize(workspace); - - return workspaceService; + await configurationService.initialize(); + return configurationService; } catch (error) { onUnexpectedError(error); - - return workspaceService; + return configurationService; } } @@ -416,7 +393,7 @@ export interface IDesktopMain { } export function main(configuration: INativeWindowConfiguration): Promise { - const workbench = new AgenticMain(configuration); + const workbench = new SessionsMain(configuration); return workbench.open(); } diff --git a/src/vs/sessions/services/configuration/browser/configurationService.ts b/src/vs/sessions/services/configuration/browser/configurationService.ts new file mode 100644 index 0000000000000..3c145277fa4c6 --- /dev/null +++ b/src/vs/sessions/services/configuration/browser/configurationService.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { ConfigurationService as BaseConfigurationService } from '../../../../platform/configuration/common/configurationService.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService, RestrictedSettings } from '../../../../workbench/services/configuration/common/configuration.js'; + +// Import to register contributions +import '../../../../workbench/services/configuration/browser/configurationService.js'; + +export class ConfigurationService extends BaseConfigurationService implements IWorkbenchConfigurationService { + readonly restrictedSettings: RestrictedSettings = { default: [] }; + readonly onDidChangeRestrictedSettings = Event.None; + async whenRemoteConfigurationLoaded(): Promise { } + isSettingAppliedForAllProfiles(key: string): boolean { + const scope = Registry.as(Extensions.Configuration).getConfigurationProperties()[key]?.scope; + if (scope && APPLICATION_SCOPES.includes(scope)) { + return true; + } + const allProfilesSettings = this.getValue(APPLY_ALL_PROFILES_SETTING) ?? []; + return Array.isArray(allProfilesSettings) && allProfilesSettings.includes(key); + } +} diff --git a/src/vs/sessions/services/workspace/browser/workspaceContextService.ts b/src/vs/sessions/services/workspace/browser/workspaceContextService.ts new file mode 100644 index 0000000000000..f54aa74c0656c --- /dev/null +++ b/src/vs/sessions/services/workspace/browser/workspaceContextService.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../base/common/event.js'; +import { removeTrailingPathSeparator } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { Workspace, WorkspaceFolder, IWorkspace, IWorkspaceContextService, IWorkspaceFoldersChangeEvent, IWorkspaceFoldersWillChangeEvent, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceFolderCreationData } from '../../../../platform/workspaces/common/workspaces.js'; +import { getWorkspaceIdentifier } from '../../../../workbench/services/workspaces/browser/workspaces.js'; +import { IDidEnterWorkspaceEvent, IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; + +export class SessionsWorkspaceContextService implements IWorkspaceContextService, IWorkspaceEditingService { + + declare readonly _serviceBrand: undefined; + + readonly onDidChangeWorkbenchState = Event.None; + readonly onDidChangeWorkspaceName = Event.None; + readonly onDidEnterWorkspace = Event.None as Event; + + private readonly _onWillChangeWorkspaceFolders = new Emitter(); + readonly onWillChangeWorkspaceFolders = this._onWillChangeWorkspaceFolders.event; + + private readonly _onDidChangeWorkspaceFolders = new Emitter(); + readonly onDidChangeWorkspaceFolders = this._onDidChangeWorkspaceFolders.event; + + private workspace: Workspace; + + constructor( + sessionsWorkspaceUri: URI, + private readonly uriIdentityService: IUriIdentityService + ) { + const workspaceIdentifier = getWorkspaceIdentifier(sessionsWorkspaceUri); + this.workspace = new Workspace(workspaceIdentifier.id, [], false, workspaceIdentifier.configPath, uri => uriIdentityService.extUri.ignorePathCasing(uri)); + } + + getCompleteWorkspace(): Promise { + return Promise.resolve(this.workspace); + } + + getWorkspace(): IWorkspace { + return this.workspace; + } + + getWorkbenchState(): WorkbenchState { + return WorkbenchState.EMPTY; + } + + getWorkspaceFolder(resource: URI): IWorkspaceFolder | null { + return this.workspace.getFolder(resource); + } + + public isInsideWorkspace(resource: URI): boolean { + return !!this.getWorkspaceFolder(resource); + } + + public isCurrentWorkspace(workspaceIdOrFolder: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI): boolean { + return false; + } + + public addFolders(foldersToAdd: IWorkspaceFolderCreationData[]): Promise { + return this.doUpdateFolders(foldersToAdd, []); + } + + public removeFolders(foldersToRemove: URI[]): Promise { + return this.doUpdateFolders([], foldersToRemove); + } + + public async updateFolders(index: number, deleteCount?: number, foldersToAddCandidates?: IWorkspaceFolderCreationData[]): Promise { + const folders = this.workspace.folders; + + let foldersToDelete: URI[] = []; + if (typeof deleteCount === 'number') { + foldersToDelete = folders.slice(index, index + deleteCount).map(folder => folder.uri); + } + + let foldersToAdd: IWorkspaceFolderCreationData[] = []; + if (Array.isArray(foldersToAddCandidates)) { + foldersToAdd = foldersToAddCandidates.map(folderToAdd => ({ uri: removeTrailingPathSeparator(folderToAdd.uri), name: folderToAdd.name })); + } + + return this.doUpdateFolders(foldersToAdd, foldersToDelete, index); + } + + async enterWorkspace(_path: URI): Promise { } + + async createAndEnterWorkspace(_folders: IWorkspaceFolderCreationData[], _path?: URI): Promise { } + + async saveAndEnterWorkspace(_path: URI): Promise { } + + async copyWorkspaceSettings(_toWorkspace: IWorkspaceIdentifier): Promise { } + + async pickNewWorkspacePath(): Promise { return undefined; } + + private async doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToRemove: URI[], index?: number): Promise { + if (foldersToAdd.length === 0 && foldersToRemove.length === 0) { + return; + } + + const currentFolders = this.workspace.folders; + + // Remove folders + let newFolders = currentFolders.filter(folder => + !foldersToRemove.some(toRemove => this.uriIdentityService.extUri.isEqual(folder.uri, toRemove)) + ); + + // Add folders + const foldersToAddWorkspaceFolders = foldersToAdd + .filter(folderToAdd => !newFolders.some(existing => this.uriIdentityService.extUri.isEqual(existing.uri, folderToAdd.uri))) + .map(folderToAdd => new WorkspaceFolder( + { uri: folderToAdd.uri, name: folderToAdd.name || this.uriIdentityService.extUri.basenameOrAuthority(folderToAdd.uri), index: 0 }, + { uri: folderToAdd.uri.toString() } + )); + + if (foldersToAddWorkspaceFolders.length > 0) { + if (typeof index === 'number' && index >= 0 && index < newFolders.length) { + newFolders = [...newFolders.slice(0, index), ...foldersToAddWorkspaceFolders, ...newFolders.slice(index)]; + } else { + newFolders = [...newFolders, ...foldersToAddWorkspaceFolders]; + } + } + + // Recompute indices + newFolders = newFolders.map((f, i) => new WorkspaceFolder({ uri: f.uri, name: f.name, index: i }, f.raw)); + + // Compute change event + const added = newFolders.filter(folder => !currentFolders.some(existing => this.uriIdentityService.extUri.isEqual(existing.uri, folder.uri))); + const removed = currentFolders.filter(folder => !newFolders.some(existing => this.uriIdentityService.extUri.isEqual(existing.uri, folder.uri))); + const changed: IWorkspaceFolder[] = []; + const changes: IWorkspaceFoldersChangeEvent = { added, removed, changed }; + + if (added.length === 0 && removed.length === 0) { + return; + } + + // Fire will change event + const joinPromises: Promise[] = []; + this._onWillChangeWorkspaceFolders.fire({ + changes, + fromCache: false, + join(promise: Promise) { joinPromises.push(promise); } + }); + await Promise.allSettled(joinPromises); + + // Update workspace + const workspaceIdentifier = getWorkspaceIdentifier(this.workspace.configuration!); + this.workspace = new Workspace(workspaceIdentifier.id, newFolders, false, workspaceIdentifier.configPath, uri => this.uriIdentityService.extUri.ignorePathCasing(uri)); + + // Fire did change event + this._onDidChangeWorkspaceFolders.fire(changes); + } +} diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 34cffa0159b21..f7ec8b17c08c6 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -47,7 +47,6 @@ import '../platform/meteredConnection/electron-browser/meteredConnectionService. import '../workbench/services/request/electron-browser/requestService.js'; import '../workbench/services/clipboard/electron-browser/clipboardService.js'; import '../workbench/services/contextmenu/electron-browser/contextmenuService.js'; -import '../workbench/services/workspaces/electron-browser/workspaceEditingService.js'; import '../workbench/services/configurationResolver/electron-browser/configurationResolverService.js'; import '../workbench/services/accessibility/electron-browser/accessibilityService.js'; import '../workbench/services/keybinding/electron-browser/nativeKeyboardLayout.js'; From a6bb8ac865bf341e184616182aae9aaacf29b50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?= <1588988+jruales@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:06:28 -0800 Subject: [PATCH 28/28] Add browserView folders to CODENOTIFY (#297858) * Add Integrated Browser section to CODENOTIFY * Integrate entries --- .github/CODENOTIFY | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODENOTIFY b/.github/CODENOTIFY index 7aba51a470b27..eaf90f0dd1afd 100644 --- a/.github/CODENOTIFY +++ b/.github/CODENOTIFY @@ -26,6 +26,7 @@ src/vs/base/browser/ui/tree/** @joaomoreno @benibenj # Platform src/vs/platform/auxiliaryWindow/** @bpasero src/vs/platform/backup/** @bpasero +src/vs/platform/browserView/** @kycutler @jruales src/vs/platform/dialogs/** @bpasero src/vs/platform/editor/** @bpasero src/vs/platform/environment/** @bpasero @@ -65,6 +66,7 @@ src/vs/code/** @bpasero @deepak1556 src/vs/workbench/services/activity/** @bpasero src/vs/workbench/services/authentication/** @TylerLeonhardt src/vs/workbench/services/auxiliaryWindow/** @bpasero +src/vs/workbench/services/browserView/** @kycutler @jruales src/vs/workbench/services/contextmenu/** @bpasero src/vs/workbench/services/dialogs/** @alexr00 @bpasero src/vs/workbench/services/editor/** @bpasero @@ -97,6 +99,7 @@ src/vs/workbench/electron-browser/** @bpasero # Workbench Contributions src/vs/workbench/contrib/authentication/** @TylerLeonhardt +src/vs/workbench/contrib/browserView/** @kycutler @jruales src/vs/workbench/contrib/files/** @bpasero src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @roblourens src/vs/workbench/contrib/localization/** @TylerLeonhardt