From 8412c237a3fb8087fa8cdc087dd562fbd3aee77d Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" <122617954+vs-code-engineering[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:28:33 -0700 Subject: [PATCH 01/32] Update endgame notebook milestones to 1.118.0 (#311302) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .vscode/notebooks/endgame.github-issues | 2 +- .vscode/notebooks/my-endgame.github-issues | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 4e49bcac12e40..e6a3c2dcdd5ac 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"1.117.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified" + "value": "$MILESTONE=milestone:\"1.118.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 40cee05f835aa..64b20f52dbb6c 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"1.117.0\"\n\n$MINE=assignee:@me" + "value": "$MILESTONE=milestone:\"1.118.0\"\n\n$MINE=assignee:@me" }, { "kind": 2, From f2576e277ed47f372a2dfce3d0f5acefd2cb9c96 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Mon, 20 Apr 2026 12:45:14 -0400 Subject: [PATCH 02/32] Fix model list hover for upgrade scenario (#311399) --- src/vs/platform/actionWidget/browser/actionList.ts | 9 +++++++-- src/vs/platform/actionWidget/browser/actionWidget.css | 9 +++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 05aae838ca03c..337851c052634 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -1505,8 +1505,13 @@ export class ActionListWidget extends Disposable { } } else if (element && element.hover?.content && typeof e.index === 'number') { // Show hover for disabled items that have hover content (with delay) - this._hideSubmenu(); - this._scheduleSubmenuShow(element, e.index); + if (this._currentSubmenuElement === element) { + this._cancelSubmenuHide(); + this._cancelSubmenuShow(); + } else { + this._hideSubmenu(); + this._scheduleSubmenuShow(element, e.index); + } } } diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index ccf83901b21b7..357a57f67bbd2 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -357,6 +357,15 @@ word-wrap: break-word; } +.action-list-submenu-hover-header a { + color: var(--vscode-textLink-foreground); +} + +.action-list-submenu-hover-header a:hover, +.action-list-submenu-hover-header a:active { + color: var(--vscode-textLink-activeForeground); +} + .action-list-submenu-hover-header.has-submenu { border-bottom: 1px solid var(--vscode-menu-separatorBackground, var(--vscode-editorWidget-border)); } From 62fb0f06ae60adf775fc354d48cddb7d912599ab Mon Sep 17 00:00:00 2001 From: Vijay Upadya <41652029+vijayupadya@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:46:14 -0700 Subject: [PATCH 03/32] Revert "Add menu toggle for cloud sync in chat panel" (#311392) Revert "Add menu toggle for cloud sync in chat panel (#311130)" This reverts commit eafba12c12552b4b6dde5ac5dffd3817def704a4. --- extensions/copilot/package.json | 9 ------ extensions/copilot/package.nls.json | 2 +- .../common/sessionIndexingPreference.ts | 12 +------- .../chronicle/common/standupPrompt.ts | 2 ++ .../test/sessionIndexingPreference.spec.ts | 24 --------------- .../vscode-node/remoteSessionExporter.ts | 11 ++----- .../common/configurationService.ts | 2 -- .../chat/browser/actions/chatActions.ts | 30 ------------------- 8 files changed, 7 insertions(+), 85 deletions(-) diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 2f5d24135b8b5..d54d644260abc 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -3968,15 +3968,6 @@ { "id": "advanced", "properties": { - "github.copilot.chat.sessionSearch.cloudSync.enabled": { - "type": "boolean", - "default": false, - "markdownDescription": "%github.copilot.config.sessionSearch.cloudSync.enabled%", - "tags": [ - "advanced", - "onExp" - ] - }, "github.copilot.chat.reasoningEffortOverride": { "type": [ "string", diff --git a/extensions/copilot/package.nls.json b/extensions/copilot/package.nls.json index 48568dfeee76e..0b4674414975e 100644 --- a/extensions/copilot/package.nls.json +++ b/extensions/copilot/package.nls.json @@ -173,7 +173,7 @@ "copilot.chronicle.tips.description": "Get personalized tips based on your Copilot usage patterns", "github.copilot.config.sessionSearch.enabled": "Enable session search and /chronicle commands. This is a team-internal setting.", "github.copilot.config.sessionSearch.localIndex.enabled": "Enable local session tracking. When enabled, Copilot tracks session data locally for /chronicle commands.", - "github.copilot.config.sessionSearch.cloudSync.enabled": "Enable cloud sync for session data. When enabled, chat session data is synced to the cloud for cross-device querying.", + "github.copilot.config.sessionSearch.cloudSync.enabled": "Enable cloud sync for session data. When enabled, session data is synced to your Copilot account for cross-device access.", "github.copilot.config.sessionSearch.cloudSync.excludeRepositories": "Repository patterns to exclude from cloud sync. Use exact `owner/repo` names or glob patterns like `my-org/*`. Sessions from matching repos will only be stored locally.", "copilot.workspace.explain.description": "Explain how the code in your active editor works", "copilot.workspace.edit.description": "Edit files in your workspace", diff --git a/extensions/copilot/src/extension/chronicle/common/sessionIndexingPreference.ts b/extensions/copilot/src/extension/chronicle/common/sessionIndexingPreference.ts index f5816ce96404f..404adb2c5cb10 100644 --- a/extensions/copilot/src/extension/chronicle/common/sessionIndexingPreference.ts +++ b/extensions/copilot/src/extension/chronicle/common/sessionIndexingPreference.ts @@ -46,19 +46,9 @@ export class SessionIndexingPreference { /** * Check if cloud sync is enabled for a given repo. * Returns true if cloudSync.enabled is true AND the repo is not excluded. - * Check both new and old setting for backward compatibility. */ hasCloudConsent(repoNwo?: string): boolean { - let cloudEnabled: boolean; - if (this._configService.isConfigured(ConfigKey.Advanced.SessionSearchCloudSync)) { - // New key explicitly set by user — authoritative - cloudEnabled = this._configService.getConfig(ConfigKey.Advanced.SessionSearchCloudSync); - } else { - // Fall back to old internal key for existing users who haven't migrated yet - cloudEnabled = this._configService.getConfig(ConfigKey.TeamInternal.SessionSearchCloudSyncEnabled); - } - - if (!cloudEnabled) { + if (!this._configService.getConfig(ConfigKey.TeamInternal.SessionSearchCloudSyncEnabled)) { return false; } diff --git a/extensions/copilot/src/extension/chronicle/common/standupPrompt.ts b/extensions/copilot/src/extension/chronicle/common/standupPrompt.ts index f1cd693471d47..eaf68d3a42284 100644 --- a/extensions/copilot/src/extension/chronicle/common/standupPrompt.ts +++ b/extensions/copilot/src/extension/chronicle/common/standupPrompt.ts @@ -132,6 +132,7 @@ Standup for : - Key files: list 2-3 most important files changed - Tools used: mention key tools if visible (e.g., apply_patch, run_in_terminal, search) - PR: [#123](link) — merged/closed (if applicable) + - Sessions: \`session-id-1\`, \`session-id-2\` **🚧 In Progress** @@ -139,6 +140,7 @@ Standup for : - Summary of current work (1-2 sentences based on turn content) - Key files: list 2-3 most important files being worked on - PR: [#789](link) — draft/open (if applicable) + - Sessions: \`session-id\` Formatting rules: - Use the turn data (user messages AND assistant responses) to understand WHAT was done, not just that something happened diff --git a/extensions/copilot/src/extension/chronicle/common/test/sessionIndexingPreference.spec.ts b/extensions/copilot/src/extension/chronicle/common/test/sessionIndexingPreference.spec.ts index a3788ec5897d3..e5b07ca575163 100644 --- a/extensions/copilot/src/extension/chronicle/common/test/sessionIndexingPreference.spec.ts +++ b/extensions/copilot/src/extension/chronicle/common/test/sessionIndexingPreference.spec.ts @@ -9,25 +9,16 @@ import { SessionIndexingPreference } from '../sessionIndexingPreference'; function createMockConfigService(opts: { localIndexEnabled?: boolean; cloudSyncEnabled?: boolean; - cloudSyncPublicEnabled?: boolean; excludeRepositories?: string[]; } = {}) { const configs: Record = {}; // Map by fullyQualifiedId configs['github.copilot.chat.advanced.sessionSearch.localIndex.enabled'] = opts.localIndexEnabled ?? false; configs['github.copilot.chat.advanced.sessionSearch.cloudSync.enabled'] = opts.cloudSyncEnabled ?? false; - configs['github.copilot.chat.sessionSearch.cloudSync.enabled'] = opts.cloudSyncPublicEnabled ?? false; configs['github.copilot.chat.advanced.sessionSearch.cloudSync.excludeRepositories'] = opts.excludeRepositories ?? []; - // Track which keys are explicitly configured (set by the user) - const configuredKeys = new Set(); - if (opts.cloudSyncPublicEnabled !== undefined) { - configuredKeys.add('github.copilot.chat.sessionSearch.cloudSync.enabled'); - } - return { getConfig: (key: { fullyQualifiedId: string }) => configs[key.fullyQualifiedId], - isConfigured: (key: { fullyQualifiedId: string }) => configuredKeys.has(key.fullyQualifiedId), } as unknown as import('../../../../platform/configuration/common/configurationService').IConfigurationService; } @@ -90,19 +81,4 @@ describe('SessionIndexingPreference', () => { expect(pref.hasCloudConsent('private-org/repo-b')).toBe(false); expect(pref.hasCloudConsent('public-org/repo-a')).toBe(true); }); - - it('hasCloudConsent uses new public key when explicitly configured', () => { - const pref = new SessionIndexingPreference(createMockConfigService({ - cloudSyncPublicEnabled: true, - })); - expect(pref.hasCloudConsent()).toBe(true); - }); - - it('hasCloudConsent new public key overrides old internal key', () => { - const pref = new SessionIndexingPreference(createMockConfigService({ - cloudSyncEnabled: true, - cloudSyncPublicEnabled: false, - })); - expect(pref.hasCloudConsent()).toBe(false); - }); }); diff --git a/extensions/copilot/src/extension/chronicle/vscode-node/remoteSessionExporter.ts b/extensions/copilot/src/extension/chronicle/vscode-node/remoteSessionExporter.ts index 1779504dae207..c469e4e8d0fcd 100644 --- a/extensions/copilot/src/extension/chronicle/vscode-node/remoteSessionExporter.ts +++ b/extensions/copilot/src/extension/chronicle/vscode-node/remoteSessionExporter.ts @@ -120,16 +120,12 @@ export class RemoteSessionExporter extends Disposable implements IExtensionContr // Only set up span listener when both local index and cloud sync are enabled. // Uses autorun to react if settings change at runtime. - // Both new and old settings taken into account for backward compatibility const localEnabled = this._configService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.SessionSearchLocalIndexEnabled, this._expService); - const cloudEnabledInternal = this._configService.getConfigObservable(ConfigKey.TeamInternal.SessionSearchCloudSyncEnabled); - const cloudEnabledPublic = this._configService.getConfigObservable(ConfigKey.Advanced.SessionSearchCloudSync); + const cloudEnabled = this._configService.getConfigObservable(ConfigKey.TeamInternal.SessionSearchCloudSyncEnabled); const spanListenerStore = this._register(new DisposableStore()); this._register(autorun(reader => { spanListenerStore.clear(); - const publicValue = cloudEnabledPublic.read(reader); - const cloudEnabled = this._configService.isConfigured(ConfigKey.Advanced.SessionSearchCloudSync) ? publicValue : cloudEnabledInternal.read(reader); - if (!localEnabled.read(reader) || !cloudEnabled) { + if (!localEnabled.read(reader) || !cloudEnabled.read(reader)) { return; } @@ -268,10 +264,9 @@ export class RemoteSessionExporter extends Disposable implements IExtensionContr } // Only export remotely if the user has cloud consent for this repo - // Also require localIndex to be enabled (team-internal gate) as defense-in-depth const repoNwo = `${repo.owner}/${repo.repo}`; - if (!this._configService.getExperimentBasedConfig(ConfigKey.TeamInternal.SessionSearchLocalIndexEnabled, this._expService) || !this._indexingPreference.hasCloudConsent(repoNwo)) { + if (!this._indexingPreference.hasCloudConsent(repoNwo)) { this._disabledSessions.add(sessionId); return; } diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index e0bf525b7ef24..9f1a1737e02ca 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -713,8 +713,6 @@ export namespace ConfigKey { /** Internal: override reasoning/thinking effort sent to model APIs (e.g. Responses API, Messages API). Used by evals. */ export const ReasoningEffortOverride = defineSetting('chat.reasoningEffortOverride', ConfigType.Simple, null); - - export const SessionSearchCloudSync = defineAndMigrateSetting('chat.advanced.sessionSearch.cloudSync.enabled', 'chat.sessionSearch.cloudSync.enabled', false); } /** diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index d285ce397abaa..f7d4917de6895 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1085,36 +1085,6 @@ export function registerChatActions() { } }); - registerAction2(class ToggleSessionCloudSyncAction extends Action2 { - private static readonly _settingKey = 'github.copilot.chat.sessionSearch.cloudSync.enabled'; - constructor() { - super({ - id: 'workbench.action.chat.toggleSessionCloudSync', - title: localize2('chat.toggleSessionCloudSync', "Sync Chat Sessions to Cloud"), - category: CHAT_CATEGORY, - f1: false, - toggled: ContextKeyExpr.equals(`config.${ToggleSessionCloudSyncAction._settingKey}`, true), - menu: [{ - id: CHAT_CONFIG_MENU_ID, - when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId), ContextKeyExpr.equals('github.copilot.sessionSearch.enabled', true)), - order: 2, - group: '4_logs' - }, { - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId), ContextKeyExpr.equals('github.copilot.sessionSearch.enabled', true)), - order: 2, - group: '4_logs' - }] - }); - } - - async run(accessor: ServicesAccessor): Promise { - const configurationService = accessor.get(IConfigurationService); - const currentValue = configurationService.getValue(ToggleSessionCloudSyncAction._settingKey); - await configurationService.updateValue(ToggleSessionCloudSyncAction._settingKey, !currentValue); - } - }); - const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.completionsAdvancedSetting}.authProvider`, defaultChat.provider.enterprise.id)); registerAction2(class extends Action2 { constructor() { From ef2d27ce596c9b6385a8361943f5c3045f6a97c3 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:48:09 -0700 Subject: [PATCH 04/32] Merge pull request #311309 from microsoft/anthonykim1/whyAgentLocalShowsUpWithoutSetting Gate AgentHostTerminalContribution on chat.agentHost.enabled --- .../agentHostTerminalContribution.ts | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts index f178541476d8d..514f6bb6ec053 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostTerminalContribution.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { localize } from '../../../../../../nls.js'; -import { IAgentHostService } from '../../../../../../platform/agentHost/common/agentService.js'; +import { AgentHostEnabledSettingId, IAgentHostService } from '../../../../../../platform/agentHost/common/agentService.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution } from '../../../../../../workbench/common/contributions.js'; import { LoggingAgentConnection } from '../../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.js'; @@ -14,22 +15,45 @@ import { IAgentHostTerminalService } from '../../../../../../workbench/contrib/t /** * Registers local agent host terminal entries with * {@link IAgentHostTerminalService} so they appear in the terminal dropdown. + * + * Gated on the `chat.agentHost.enabled` setting. */ export class AgentHostTerminalContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.agentHostTerminal'; private readonly _localEntry = this._register(new MutableDisposable()); + private readonly _conditionalListeners = this._register(new MutableDisposable()); constructor( @IAgentHostService private readonly _agentHostService: IAgentHostService, @IAgentHostTerminalService private readonly _agentHostTerminalService: IAgentHostTerminalService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(); - this._register(this._agentHostService.onAgentHostStart(() => this._reconcile())); - this._register(this._agentHostService.onAgentHostExit(() => this._reconcile())); - this._reconcile(); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AgentHostEnabledSettingId)) { + this._updateEnabled(); + } + })); + + this._updateEnabled(); + } + + private _updateEnabled(): void { + if (this._configurationService.getValue(AgentHostEnabledSettingId)) { + if (!this._conditionalListeners.value) { + const store = new DisposableStore(); + store.add(this._agentHostService.onAgentHostStart(() => this._reconcile())); + store.add(this._agentHostService.onAgentHostExit(() => this._reconcile())); + this._conditionalListeners.value = store; + this._reconcile(); + } + } else { + this._conditionalListeners.value = undefined; + this._localEntry.value = undefined; + } } private _reconcile(): void { From df017eb04616f7256a98561a1399daddbd28460d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:48:30 +0000 Subject: [PATCH 05/32] Editor - exploration for layout controls in the editor title (#311007) --- build/lib/i18n.resources.json | 12 +- .../lib/stylelint/vscode-known-variables.json | 1 + src/vs/platform/actions/common/actions.ts | 1 + .../browser/parts/media/editorPart.css | 44 +++++++ .../changes/browser/changesViewActions.ts | 58 ---------- .../editor/browser/editor.contribution.ts | 96 +++++++++++++++ src/vs/sessions/sessions.common.main.ts | 1 + .../browser/parts/editor/editorTabsControl.ts | 109 +++++++++++++++++- .../editor/media/multieditortabscontrol.css | 22 ++-- .../parts/editor/multiEditorTabsControl.ts | 12 +- .../parts/editor/noEditorTabsControl.ts | 7 ++ .../parts/editor/singleEditorTabsControl.ts | 7 ++ 12 files changed, 297 insertions(+), 73 deletions(-) create mode 100644 src/vs/sessions/contrib/editor/browser/editor.contribution.ts diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index e04a38c9fbe30..d8d040444942d 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -676,10 +676,10 @@ "name": "vs/sessions/contrib/files", "project": "vscode-sessions" }, - { - "name": "vs/sessions/contrib/policyBlocked", - "project": "vscode-sessions" - }, + { + "name": "vs/sessions/contrib/policyBlocked", + "project": "vscode-sessions" + }, { "name": "vs/sessions/contrib/git", "project": "vscode-sessions" @@ -711,6 +711,10 @@ { "name": "vs/sessions/contrib/tunnelHost", "project": "vscode-sessions" + }, + { + "name": "vs/sessions/contrib/editor", + "project": "vscode-sessions" } ] } diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 59dfc331f3501..f0be1a9ea91d7 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -941,6 +941,7 @@ "--inline-chat-frame-progress", "--insert-border-color", "--last-tab-margin-right", + "--last-tab-layout-actions-width", "--list-scroll-right-offset", "--monaco-monospace-font", "--monaco-monospace-font", diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 910c40db359ce..f4ac668759d06 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -88,6 +88,7 @@ export class MenuId { static readonly EditorContextPeek = new MenuId('EditorContextPeek'); static readonly EditorContextShare = new MenuId('EditorContextShare'); static readonly EditorTitle = new MenuId('EditorTitle'); + static readonly EditorTitleLayout = new MenuId('EditorTitleLayout'); static readonly ModalEditorTitle = new MenuId('ModalEditorTitle'); static readonly ModalEditorEditorTitle = new MenuId('ModalEditorEditorTitle'); static readonly CompactWindowEditorTitle = new MenuId('CompactWindowEditorTitle'); diff --git a/src/vs/sessions/browser/parts/media/editorPart.css b/src/vs/sessions/browser/parts/media/editorPart.css index b75b69460c996..841b364322f49 100644 --- a/src/vs/sessions/browser/parts/media/editorPart.css +++ b/src/vs/sessions/browser/parts/media/editorPart.css @@ -19,3 +19,47 @@ .agent-sessions-workbench.nosidebar.nochatbar .part.editor:not(.modal-editor-part) { margin-left: 10px; } + +/* Editor Layout Actions Toolbar */ + +.agent-sessions-workbench .part.editor > .content .editor-group-container > .title .editor-actions { + padding-right: 0; +} + +.agent-sessions-workbench .part.editor > .content .editor-group-container > .title .editor-actions-separator { + display: block; + align-self: center; + width: 1px; + height: 16px; + margin: 0 4px; + background-color: var(--vscode-titleBar-activeForeground); + opacity: 0.3; +} + +.agent-sessions-workbench .part.editor > .content .editor-group-container > .title .editor-layout-actions { + display: block; + cursor: default; + flex: initial; + padding: 0 8px 0 4px; + height: var(--editor-group-tab-height); +} + +.agent-sessions-workbench .part.editor > .content .editor-group-container > .title .editor-layout-actions.hidden { + display: none; +} + +.agent-sessions-workbench .part.editor > .content .editor-group-container > .title .editor-layout-actions .action-item { + margin-right: 4px; +} + +.agent-sessions-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .editor-layout-actions { + /* When tabs are wrapped, position the trailing editor actions at the end of the very last row */ + position: absolute; + bottom: 0; + right: 0; +} + +.agent-sessions-workbench .part.editor > .content .editor-group-container > .title.two-tab-bars > .tabs-and-actions-container:first-child .editor-layout-actions { + /* When multiple tab bars are visible, only show editor actions for the last tab bar */ + display: none; +} diff --git a/src/vs/sessions/contrib/changes/browser/changesViewActions.ts b/src/vs/sessions/contrib/changes/browser/changesViewActions.ts index b185328525a2c..86ec33bab13fc 100644 --- a/src/vs/sessions/contrib/changes/browser/changesViewActions.ts +++ b/src/vs/sessions/contrib/changes/browser/changesViewActions.ts @@ -26,8 +26,6 @@ import { ViewContainerLocation } from '../../../../workbench/common/views.js'; import { ChangesViewPane } from './changesView.js'; import { SESSIONS_FILES_CONTAINER_ID } from '../../files/browser/files.contribution.js'; import { SESSIONS_FILES_VIEW_ID } from '../../files/browser/filesView.js'; -import { IAgentWorkbenchLayoutService } from '../../../browser/workbench.js'; -import { EditorMaximizedContext } from '../../../common/contextkeys.js'; const openChangesViewActionOptions: IAction2Options = { id: 'workbench.action.agentSessions.openChangesView', @@ -199,59 +197,3 @@ class OpenPullRequestAction extends Action2 { } registerAction2(OpenPullRequestAction); - -class MaximizeMainEditorPartAction extends Action2 { - static readonly ID = 'workbench.action.agentSessions.maximizeMainEditorPart'; - - constructor() { - super({ - id: MaximizeMainEditorPartAction.ID, - title: localize2('maximizeMainEditorPart', "Maximize Editor"), - icon: Codicon.screenFull, - f1: false, - menu: { - id: MenuId.EditorTitle, - group: 'navigation', - order: 100001, - when: ContextKeyExpr.and( - IsSessionsWindowContext, - EditorMaximizedContext.negate()) - } - }); - } - - async run(accessor: ServicesAccessor): Promise { - const layoutService = accessor.get(IAgentWorkbenchLayoutService); - layoutService.setEditorMaximized(true); - } -} - -registerAction2(MaximizeMainEditorPartAction); - -class RestoreMainEditorPartAction extends Action2 { - static readonly ID = 'workbench.action.agentSessions.restoreMainEditorPart'; - - constructor() { - super({ - id: RestoreMainEditorPartAction.ID, - title: localize2('restoreMainEditorPart', "Restore Editor"), - icon: Codicon.screenNormal, - f1: false, - menu: { - id: MenuId.EditorTitle, - group: 'navigation', - order: 100001, - when: ContextKeyExpr.and( - IsSessionsWindowContext, - EditorMaximizedContext) - } - }); - } - - async run(accessor: ServicesAccessor): Promise { - const layoutService = accessor.get(IAgentWorkbenchLayoutService); - layoutService.setEditorMaximized(false); - } -} - -registerAction2(RestoreMainEditorPartAction); diff --git a/src/vs/sessions/contrib/editor/browser/editor.contribution.ts b/src/vs/sessions/contrib/editor/browser/editor.contribution.ts new file mode 100644 index 0000000000000..f8f9928377b6c --- /dev/null +++ b/src/vs/sessions/contrib/editor/browser/editor.contribution.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize2 } from '../../../../nls.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; +import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; +import { IAgentWorkbenchLayoutService } from '../../../browser/workbench.js'; +import { EditorMaximizedContext } from '../../../common/contextkeys.js'; + +class MaximizeMainEditorPartAction extends Action2 { + static readonly ID = 'workbench.action.agentSessions.maximizeMainEditorPart'; + + constructor() { + super({ + id: MaximizeMainEditorPartAction.ID, + title: localize2('maximizeMainEditorPart', "Maximize Editor"), + icon: Codicon.screenFull, + f1: false, + menu: { + id: MenuId.EditorTitleLayout, + group: 'navigation', + order: 1, + when: ContextKeyExpr.and( + IsSessionsWindowContext, + EditorMaximizedContext.negate()) + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const layoutService = accessor.get(IAgentWorkbenchLayoutService); + layoutService.setEditorMaximized(true); + } +} + +registerAction2(MaximizeMainEditorPartAction); + +class RestoreMainEditorPartAction extends Action2 { + static readonly ID = 'workbench.action.agentSessions.restoreMainEditorPart'; + + constructor() { + super({ + id: RestoreMainEditorPartAction.ID, + title: localize2('restoreMainEditorPart', "Restore Editor"), + icon: Codicon.screenNormal, + f1: false, + menu: { + id: MenuId.EditorTitleLayout, + group: 'navigation', + order: 1, + when: ContextKeyExpr.and( + IsSessionsWindowContext, + EditorMaximizedContext) + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const layoutService = accessor.get(IAgentWorkbenchLayoutService); + layoutService.setEditorMaximized(false); + } +} + +registerAction2(RestoreMainEditorPartAction); + +class CloseMainEditorPartAction extends Action2 { + static readonly ID = 'workbench.action.agentSessions.closeMainEditorPart'; + + constructor() { + super({ + id: CloseMainEditorPartAction.ID, + title: localize2('closeMainEditorPart', "Close Editor"), + icon: Codicon.close, + f1: false, + menu: { + id: MenuId.EditorTitleLayout, + group: 'navigation', + order: 100, + when: IsSessionsWindowContext + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand('workbench.action.closeAllGroups'); + } +} + +registerAction2(CloseMainEditorPartAction); diff --git a/src/vs/sessions/sessions.common.main.ts b/src/vs/sessions/sessions.common.main.ts index a279fc1fdaf33..901dcb40f6f08 100644 --- a/src/vs/sessions/sessions.common.main.ts +++ b/src/vs/sessions/sessions.common.main.ts @@ -475,6 +475,7 @@ import './contrib/applyCommitsToParentRepo/browser/applyChangesToParentRepo.js'; import './contrib/fileTreeView/browser/fileTreeView.contribution.js'; // view registration disabled; filesystem provider still needed import './contrib/configuration/browser/configuration.contribution.js'; import './contrib/workingSet/browser/workingSet.contribution.js'; +import './contrib/editor/browser/editor.contribution.js'; import './contrib/terminal/browser/sessionsTerminalContribution.js'; import './contrib/logs/browser/logs.contribution.js'; diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index b0a44e2c23af4..2ba6b32bccf35 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -6,7 +6,7 @@ import './media/editortabscontrol.css'; import { localize } from '../../../../nls.js'; import { DataTransfers } from '../../../../base/browser/dnd.js'; -import { $, Dimension, getActiveWindow, getWindow, isMouseEvent } from '../../../../base/browser/dom.js'; +import { $, Dimension, getActiveWindow, getWindow, isMouseEvent, setVisibility } from '../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { ActionsOrientation, IActionViewItem, prepareActions } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IAction, ActionRunner } from '../../../../base/common/actions.js'; @@ -32,7 +32,7 @@ import { assertReturnsDefined } from '../../../../base/common/types.js'; import { isFirefox } from '../../../../base/browser/browser.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { SideBySideEditorInput } from '../../../common/editor/sideBySideEditorInput.js'; -import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { WorkbenchToolBar, HiddenItemStrategy } from '../../../../platform/actions/browser/toolbar.js'; import { LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js'; import { DraggedTreeItemsIdentifier } from '../../../../editor/common/services/treeViewsDnd.js'; import { IEditorResolverService } from '../../../services/editor/common/editorResolverService.js'; @@ -109,6 +109,12 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC private readonly editorActionsToolbarDisposables = this._register(new DisposableStore()); private readonly editorActionsDisposables = this._register(new DisposableStore()); + private editorLayoutActionsSeparator: HTMLElement | undefined; + protected editorLayoutActionsToolbarContainer: HTMLElement | undefined; + private editorLayoutActionsToolbar: WorkbenchToolBar | undefined; + private readonly editorLayoutActionsToolbarDisposables = this._register(new DisposableStore()); + private readonly editorLayoutActionsDisposables = this._register(new DisposableStore()); + private readonly contextMenuContextKeyService: IContextKeyService; private resourceContext: ResourceContextKey; @@ -182,6 +188,14 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC parent.appendChild(this.editorActionsToolbarContainer); this.handleEditorActionToolBarVisibility(this.editorActionsToolbarContainer); + + this.editorLayoutActionsSeparator = $('div.editor-actions-separator'); + parent.appendChild(this.editorLayoutActionsSeparator); + + this.editorLayoutActionsToolbarContainer = $('div.editor-layout-actions'); + parent.appendChild(this.editorLayoutActionsToolbarContainer); + + this.handleEditorLayoutActionsToolBarVisibility(this.editorLayoutActionsToolbarContainer); } private handleEditorActionToolBarVisibility(container: HTMLElement): void { @@ -203,6 +217,32 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC container.classList.toggle('hidden', !editorActionsEnabled); } + private handleEditorLayoutActionsToolBarVisibility(container: HTMLElement): void { + const editorActionsEnabled = this.editorActionsEnabled; + const editorActionsVisible = !!this.editorLayoutActionsToolbar; + + // Create toolbar if it is enabled (and not yet created) + if (editorActionsEnabled && !editorActionsVisible) { + this.doCreateEditorLayoutActionsToolBar(container); + } + // Remove toolbar if it is not enabled (and is visible) + else if (!editorActionsEnabled && editorActionsVisible) { + this.editorLayoutActionsToolbar?.getElement().remove(); + this.editorLayoutActionsToolbar = undefined; + this.editorLayoutActionsToolbarDisposables.clear(); + this.editorLayoutActionsDisposables.clear(); + } + + container.classList.toggle('hidden', !editorActionsEnabled); + + // Keep the sibling separator in sync with the toolbar. The separator lives outside + // the hidden containers so it must be explicitly hidden whenever the layout toolbar + // is disabled/removed; otherwise it would remain visible as an orphan line. + if (this.editorLayoutActionsSeparator && !editorActionsEnabled) { + setVisibility(false, this.editorLayoutActionsSeparator); + } + } + private doCreateEditorActionsToolBar(container: HTMLElement): void { const context: IEditorCommandsContext = { groupId: this.groupView.id }; @@ -234,6 +274,38 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC })); } + private doCreateEditorLayoutActionsToolBar(container: HTMLElement): void { + const context: IEditorCommandsContext = { groupId: this.groupView.id }; + + // Toolbar Widget (no overflow, no hidden-item "..." button so layout actions + // are always rendered inline after the primary toolbar's own overflow). + this.editorLayoutActionsToolbar = this.editorLayoutActionsToolbarDisposables.add(this.instantiationService.createInstance(WorkbenchToolBar, container, { + actionViewItemProvider: (action, options) => this.actionViewItemProvider(action, options), + orientation: ActionsOrientation.HORIZONTAL, + ariaLabel: localize('ariaLabelEditorActionsLayout', "Editor layout actions"), + getKeyBinding: action => this.getKeybinding(action), + actionRunner: this.editorLayoutActionsToolbarDisposables.add(new EditorCommandsContextActionRunner(context)), + anchorAlignmentProvider: () => AnchorAlignment.RIGHT, + renderDropdownAsChildElement: this.renderDropdownAsChildElement, + telemetrySource: 'editorPartTrailing', + resetMenu: MenuId.EditorTitleLayout, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + highlightToggledItems: true + })); + + // Context + this.editorLayoutActionsToolbar.context = context; + + // Action Run Handling + this.editorLayoutActionsToolbarDisposables.add(this.editorLayoutActionsToolbar.actionRunner.onDidRun(e => { + + // Notify for Error + if (e.error && !isCancellationError(e.error)) { + this.notificationService.error(e.error); + } + })); + } + private actionViewItemProvider(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined { const activeEditorPane = this.groupView.activeEditorPane; @@ -263,9 +335,33 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC const editorActionsToolbar = assertReturnsDefined(this.editorActionsToolbar); const { primary, secondary } = this.prepareEditorActions(editorActions.actions); editorActionsToolbar.setActions(prepareActions(primary), prepareActions(secondary)); + + this.updateEditorLayoutActionsToolbar(); + } + + private updateEditorLayoutActionsToolbar(): void { + if (!this.editorActionsEnabled || !this.editorLayoutActionsToolbar) { + return; + } + + this.editorLayoutActionsDisposables.clear(); + + const editorActions = this.groupView.createEditorActions(this.editorLayoutActionsDisposables, MenuId.EditorTitleLayout); + this.editorLayoutActionsDisposables.add(editorActions.onDidChange(() => this.updateEditorLayoutActionsToolbar())); + + const { primary, secondary } = this.prepareEditorLayoutActions(editorActions.actions); + this.editorLayoutActionsToolbar.setActions(prepareActions(primary), prepareActions(secondary)); + + // Only show the separator when the layout toolbar actually has actions. + if (this.editorLayoutActionsSeparator) { + setVisibility(primary.length > 0 || secondary.length > 0, this.editorLayoutActionsSeparator); + } } protected abstract prepareEditorActions(editorActions: IToolbarActions): IToolbarActions; + + protected abstract prepareEditorLayoutActions(editorActions: IToolbarActions): IToolbarActions; + private getEditorPaneAwareContextKeyService(): IContextKeyService { return this.groupView.activeEditorPane?.scopedContextKeyService ?? this.contextKeyService; } @@ -277,6 +373,11 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC const editorActionsToolbar = assertReturnsDefined(this.editorActionsToolbar); editorActionsToolbar.setActions([], []); + + this.editorLayoutActionsToolbar?.setActions([], []); + if (this.editorLayoutActionsSeparator) { + setVisibility(false, this.editorLayoutActionsSeparator); + } } protected onGroupDragStart(e: DragEvent, element: HTMLElement): boolean { @@ -483,6 +584,10 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC this.handleEditorActionToolBarVisibility(this.editorActionsToolbarContainer); this.updateEditorActionsToolbar(); } + if (this.editorLayoutActionsToolbarContainer) { + this.handleEditorLayoutActionsToolBarVisibility(this.editorLayoutActionsToolbarContainer); + this.updateEditorLayoutActionsToolbar(); + } } } diff --git a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index 4f9477d08657a..d57abaaf41293 100644 --- a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -285,7 +285,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected.tab-border-top > .tab-border-top-container, .monaco-workbench .part.editor > .content .editor-group-container > .title:not(.two-tab-bars) .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, -.monaco-workbench .part.editor > .content .editor-group-container > .title.two-tab-bars .tabs-and-actions-container:not(:first-child) .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, +.monaco-workbench .part.editor > .content .editor-group-container > .title.two-tab-bars .tabs-and-actions-container:not(:first-child) .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { display: block; position: absolute; @@ -329,8 +329,8 @@ position: relative; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .tab-label > .monaco-icon-label-container::after, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fixed > .tab-label > .monaco-icon-label-container::after { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .tab-label > .monaco-icon-label-container::after, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fixed > .tab-label > .monaco-icon-label-container::after { content: ''; /* enables a linear gradient to overlay the end of the label when tabs overflow */ position: absolute; right: 0; @@ -343,13 +343,13 @@ height: calc(100% - 2px); } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink:focus > .tab-label > .monaco-icon-label-container::after, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fixed:focus > .tab-label > .monaco-icon-label-container::after { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink:focus > .tab-label > .monaco-icon-label-container::after, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fixed:focus > .tab-label > .monaco-icon-label-container::after { opacity: 0; /* when tab has the focus this shade breaks the tab border (fixes https://github.com/microsoft/vscode/issues/57819) */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .tab-label.tab-label-has-badge::after, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fixed > .tab-label.tab-label-has-badge::after { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .tab-label.tab-label-has-badge::after, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fixed > .tab-label.tab-label-has-badge::after { margin-right: 5px; /* with tab sizing shrink/fixed and badges, we want a right-margin because the close button is hidden. Use margin instead of padding to support animating the badge (https://github.com/microsoft/vscode/issues/242661) */ } @@ -526,6 +526,14 @@ right: 0; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions-separator { + display: none; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-layout-actions { + display: none; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title.two-tab-bars > .tabs-and-actions-container:first-child .editor-actions { /* When multiple tab bars are visible, only show editor actions for the last tab bar */ diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index b56047d66e1f8..1fe80afb1db18 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -1775,6 +1775,10 @@ export class MultiEditorTabsControl extends EditorTabsControl { } } + protected override prepareEditorLayoutActions(editorActions: IToolbarActions): IToolbarActions { + return editorActions; + } + getHeight(): number { // Return quickly if our used dimensions are known @@ -1880,6 +1884,9 @@ export class MultiEditorTabsControl extends EditorTabsControl { private doLayoutTabsWrapping(dimensions: IEditorTitleControlDimensions): boolean { const [tabsAndActionsContainer, tabsContainer, editorToolbarContainer, tabsScrollbar] = assertReturnsAllDefined(this.tabsAndActionsContainer, this.tabsContainer, this.editorActionsToolbarContainer, this.tabsScrollbar); + const layoutActionsContainer = this.editorLayoutActionsToolbarContainer; + const editorToolbarWidth = () => editorToolbarContainer.offsetWidth + (layoutActionsContainer?.offsetWidth ?? 0); + // Handle wrapping tabs according to setting: // - enabled: only add class if tabs wrap and don't exceed available dimensions // - disabled: remove class and margin-right variable @@ -1896,7 +1903,8 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Update `last-tab-margin-right` CSS variable to account for the absolute // positioned editor actions container when tabs wrap. The margin needs to // be the width of the editor actions container to avoid screen cheese. - tabsContainer.style.setProperty('--last-tab-margin-right', tabsWrapMultiLine ? `${editorToolbarContainer.offsetWidth}px` : '0'); + tabsContainer.style.setProperty('--last-tab-margin-right', tabsWrapMultiLine ? `${editorToolbarWidth()}px` : '0'); + tabsAndActionsContainer.style.setProperty('--last-tab-layout-actions-width', `${layoutActionsContainer?.offsetWidth ?? 0}px`); // Remove old css classes that are not needed anymore for (const tab of tabsContainer.children) { @@ -1914,7 +1922,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { return true; // no tab always fits } - const lastTabOverlapWithToolbarWidth = lastTab.offsetWidth + editorToolbarContainer.offsetWidth - dimensions.available.width; + const lastTabOverlapWithToolbarWidth = lastTab.offsetWidth + editorToolbarWidth() - dimensions.available.width; if (lastTabOverlapWithToolbarWidth > 1) { // Allow for slight rounding errors related to zooming here // https://github.com/microsoft/vscode/issues/116385 diff --git a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts index af16587fdb03e..8d1c807d833f6 100644 --- a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts @@ -20,6 +20,13 @@ export class NoEditorTabsControl extends EditorTabsControl { }; } + protected override prepareEditorLayoutActions(): IToolbarActions { + return { + primary: [], + secondary: [] + }; + } + openEditor(editor: EditorInput): boolean { return this.handleOpenedEditors(); } diff --git a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts index 0ef439569e1e5..6b1732c7850f4 100644 --- a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts @@ -359,6 +359,13 @@ export class SingleEditorTabsControl extends EditorTabsControl { } } + protected override prepareEditorLayoutActions(): IToolbarActions { + return { + primary: [], + secondary: [] + }; + } + getHeight(): number { return this.tabHeight; } From 2f08b1e9a94b9593f7c88fdc6ea19ec1443248b5 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:55:45 -0700 Subject: [PATCH 06/32] add hint for chat.permissions.default and agents input fix (#311273) --- .../browser/agentHostSessionConfigPicker.ts | 14 ++++++++++--- .../browser/permissionPicker.ts | 21 ++++++++++++++++--- .../input/permissionPickerActionItem.ts | 10 +++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/agentHostSessionConfigPicker.ts b/src/vs/sessions/contrib/chat/browser/agentHostSessionConfigPicker.ts index 1c57397e18aea..f9528deea19f1 100644 --- a/src/vs/sessions/contrib/chat/browser/agentHostSessionConfigPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/agentHostSessionConfigPicker.ts @@ -188,9 +188,17 @@ async function confirmAutoApproveLevel(value: string, dialogService: IDialogServ custom: { icon: isAutopilot ? Codicon.rocket : Codicon.warning, markdownDetails: [{ - markdown: new MarkdownString(isAutopilot - ? localize('agentHostAutoApprove.autopilot.warning.detail', "Autopilot will auto-approve all tool calls and continue working autonomously until the task is complete. This includes terminal commands, file edits, and external tool calls. The agent will make decisions on your behalf without asking for confirmation.\n\nYou can stop the agent at any time by clicking the stop button. This applies to the current session only.") - : localize('agentHostAutoApprove.bypass.warning.detail', "Bypass Approvals will auto-approve all tool calls without asking for confirmation. This includes file edits, terminal commands, and external tool calls.")), + markdown: new MarkdownString( + localize( + 'agentHostAutoApprove.warning.detailWithDefaultSetting', + "{0}\n\nTo make this the starting permission level for new chat sessions, change the [{1}](command:workbench.action.openSettings?%5B%22{1}%22%5D) setting.", + isAutopilot + ? localize('agentHostAutoApprove.autopilot.warning.detail', "Autopilot will auto-approve all tool calls and continue working autonomously until the task is complete. This includes terminal commands, file edits, and external tool calls. The agent will make decisions on your behalf without asking for confirmation.\n\nYou can stop the agent at any time by clicking the stop button. This applies to the current session only.") + : localize('agentHostAutoApprove.bypass.warning.detail', "Bypass Approvals will auto-approve all tool calls without asking for confirmation. This includes file edits, terminal commands, and external tool calls."), + ChatConfiguration.DefaultPermissionLevel, + ), + { isTrusted: { enabledCommands: ['workbench.action.openSettings'] } }, + ), }], }, }); diff --git a/src/vs/sessions/contrib/copilotChatSessions/browser/permissionPicker.ts b/src/vs/sessions/contrib/copilotChatSessions/browser/permissionPicker.ts index 8c20dcdefd8a1..9507116fa346e 100644 --- a/src/vs/sessions/contrib/copilotChatSessions/browser/permissionPicker.ts +++ b/src/vs/sessions/contrib/copilotChatSessions/browser/permissionPicker.ts @@ -16,7 +16,7 @@ import { ISessionsManagementService } from '../../../services/sessions/common/se import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { ChatConfiguration, ChatPermissionLevel } from '../../../../workbench/contrib/chat/common/constants.js'; +import { ChatConfiguration, ChatPermissionLevel, isChatPermissionLevel } from '../../../../workbench/contrib/chat/common/constants.js'; import Severity from '../../../../base/common/severity.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; @@ -77,6 +77,15 @@ export class PermissionPicker extends Disposable { render(container: HTMLElement): HTMLElement { this._renderDisposables.clear(); + // Initialize the picker to reflect the configured default permission level + // (`chat.permissions.default`) whenever it is (re-)rendered. If enterprise + // policy disables global auto-approval, clamp to Default regardless of the + // configured default so we never show an elevated level the user can't pick. + const policyRestricted = this.configurationService.inspect(ChatConfiguration.GlobalAutoApprove).policyValue === false; + const configuredDefault = this.configurationService.getValue(ChatConfiguration.DefaultPermissionLevel); + const initialLevel = isChatPermissionLevel(configuredDefault) ? configuredDefault : ChatPermissionLevel.Default; + this._currentLevel = policyRestricted ? ChatPermissionLevel.Default : initialLevel; + const slot = dom.append(container, dom.$('.sessions-chat-picker-slot')); this._renderDisposables.add({ dispose: () => slot.remove() }); @@ -220,7 +229,10 @@ export class PermissionPicker extends Disposable { custom: { icon: Codicon.warning, markdownDetails: [{ - markdown: new MarkdownString(localize('permissions.autoApprove.warning.detail', "Bypass Approvals will auto-approve all tool calls without asking for confirmation. This includes file edits, terminal commands, and external tool calls.")), + markdown: new MarkdownString( + localize('permissions.autoApprove.warning.detail', "Bypass Approvals will auto-approve all tool calls without asking for confirmation. This includes file edits, terminal commands, and external tool calls.\n\nTo make this the starting permission level for new chat sessions, change the [{0}](command:workbench.action.openSettings?%5B%22{0}%22%5D) setting.", ChatConfiguration.DefaultPermissionLevel), + { isTrusted: { enabledCommands: ['workbench.action.openSettings'] } }, + ), }], }, }); @@ -247,7 +259,10 @@ export class PermissionPicker extends Disposable { custom: { icon: Codicon.rocket, markdownDetails: [{ - markdown: new MarkdownString(localize('permissions.autopilot.warning.detail', "Autopilot will auto-approve all tool calls and continue working autonomously until the task is complete. The agent will make decisions on your behalf without asking for confirmation.\n\nYou can stop the agent at any time by clicking the stop button. This applies to the current session only.")), + markdown: new MarkdownString( + localize('permissions.autopilot.warning.detail', "Autopilot will auto-approve all tool calls and continue working autonomously until the task is complete. The agent will make decisions on your behalf without asking for confirmation.\n\nYou can stop the agent at any time by clicking the stop button. This applies to the current session only.\n\nTo make this the starting permission level for new chat sessions, change the [{0}](command:workbench.action.openSettings?%5B%22{0}%22%5D) setting.", ChatConfiguration.DefaultPermissionLevel), + { isTrusted: { enabledCommands: ['workbench.action.openSettings'] } }, + ), }], }, }); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index 0e2fdbf924ca1..e5dd82824fa3e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -132,7 +132,10 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { custom: { icon: Codicon.warning, markdownDetails: [{ - markdown: new MarkdownString(localize('permissions.autoApprove.warning.detail', "Bypass Approvals will auto-approve all tool calls without asking for confirmation. This includes file edits, terminal commands, and external tool calls.")), + markdown: new MarkdownString( + localize('permissions.autoApprove.warning.detail', "Bypass Approvals will auto-approve all tool calls without asking for confirmation. This includes file edits, terminal commands, and external tool calls.\n\nTo make this the starting permission level for new chat sessions, change the [{0}](command:workbench.action.openSettings?%5B%22{0}%22%5D) setting.", ChatConfiguration.DefaultPermissionLevel), + { isTrusted: { enabledCommands: ['workbench.action.openSettings'] } }, + ), }], }, }); @@ -188,7 +191,10 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { custom: { icon: Codicon.rocket, markdownDetails: [{ - markdown: new MarkdownString(localize('permissions.autopilot.warning.detail', "Autopilot will auto-approve all tool calls and continue working autonomously until the task is complete. This includes terminal commands, file edits, and external tool calls. The agent will make decisions on your behalf without asking for confirmation.\n\nYou can stop the agent at any time by clicking the stop button. This applies to the current session only.")), + markdown: new MarkdownString( + localize('permissions.autopilot.warning.detail', "Autopilot will auto-approve all tool calls and continue working autonomously until the task is complete. This includes terminal commands, file edits, and external tool calls. The agent will make decisions on your behalf without asking for confirmation.\n\nYou can stop the agent at any time by clicking the stop button. This applies to the current session only.\n\nTo make this the starting permission level for new chat sessions, change the [{0}](command:workbench.action.openSettings?%5B%22{0}%22%5D) setting.", ChatConfiguration.DefaultPermissionLevel), + { isTrusted: { enabledCommands: ['workbench.action.openSettings'] } }, + ), }], }, }); From 3aa6d1dc9774c2e66739af17393963bef917aaca Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Mon, 20 Apr 2026 10:10:53 -0700 Subject: [PATCH 07/32] Revert "Update endgame notebook milestones to 1.118.0" (#311409) Revert "Update endgame notebook milestones to 1.118.0 (#311302)" This reverts commit 8412c237a3fb8087fa8cdc087dd562fbd3aee77d. --- .vscode/notebooks/endgame.github-issues | 2 +- .vscode/notebooks/my-endgame.github-issues | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index e6a3c2dcdd5ac..4e49bcac12e40 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"1.118.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified" + "value": "$MILESTONE=milestone:\"1.117.0\"\n\n$TPI_CREATION=2026-03-23 // Used to find fixes that need to be verified" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 64b20f52dbb6c..40cee05f835aa 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"1.118.0\"\n\n$MINE=assignee:@me" + "value": "$MILESTONE=milestone:\"1.117.0\"\n\n$MINE=assignee:@me" }, { "kind": 2, From 3aeae51bd21097902fe7cfb413a00397fb3a3e82 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:17:34 -0700 Subject: [PATCH 08/32] chore: add copilot tag prefix (#311416) Co-authored-by: Copilot --- build/azure-pipelines/product-copilot-recovery.yml | 1 + extensions/copilot/build/pre-release.yml | 1 + extensions/copilot/build/release.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/build/azure-pipelines/product-copilot-recovery.yml b/build/azure-pipelines/product-copilot-recovery.yml index 92078b5154d53..056bdedaed7b3 100644 --- a/build/azure-pipelines/product-copilot-recovery.yml +++ b/build/azure-pipelines/product-copilot-recovery.yml @@ -71,3 +71,4 @@ extends: publishExtension: ${{ parameters.publishExtension }} ghReleasePublishVSIX: true + ghTagPrefix: 'copilot/' diff --git a/extensions/copilot/build/pre-release.yml b/extensions/copilot/build/pre-release.yml index 00c83bbbafdf2..06fb599900283 100644 --- a/extensions/copilot/build/pre-release.yml +++ b/extensions/copilot/build/pre-release.yml @@ -257,3 +257,4 @@ extends: publishExtension: ${{ parameters.publishExtension }} ghReleasePublishVSIX: true + ghTagPrefix: 'copilot/' diff --git a/extensions/copilot/build/release.yml b/extensions/copilot/build/release.yml index 7e3022928edd8..7bfcd30b6c705 100644 --- a/extensions/copilot/build/release.yml +++ b/extensions/copilot/build/release.yml @@ -243,3 +243,4 @@ extends: publishExtension: ${{ parameters.publishExtension }} ghReleasePublishVSIX: true + ghTagPrefix: 'copilot/' From 110f6f955953d3bbe1e0b0df267ac011f8011f18 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:48:22 -0700 Subject: [PATCH 09/32] Fix some basic eslint errors in copilot Starts working through some basic repo wide eslint rules for the copilot extension. Stuff like missing semicolons and missing readonly modifiers for disposables --- .../copilotcli/node/permissionHelpers.ts | 4 +- .../claudeChatSessionContentProvider.spec.ts | 1 + .../extension/src/codeReferencing/index.ts | 2 +- .../vscode-node/extension/src/statusBar.ts | 4 +- .../lib/src/experiments/featuresService.ts | 2 +- .../src/prompt/similarFiles/openTabFiles.ts | 4 +- .../vscode-node/conversationFeature.ts | 4 +- .../vscode-node/languageModelAccess.ts | 1 - .../conversation/vscode-node/remoteAgents.ts | 2 +- .../test/languageModelAccess.test.ts | 4 +- .../importDiagnosticsCompletionProvider.ts | 2 +- .../src/extension/log/node/chatLogExport.ts | 2 +- .../src/extension/mcp/vscode-node/commands.ts | 8 ++-- .../extension/search/vscode-node/commands.ts | 2 +- .../preComputedToolEmbeddingsCache.ts | 2 +- .../common/serverProtocol.ts | 40 +++++++++--------- .../src/common/contextProvider.ts | 10 ++--- .../serverPlugin/src/common/protocol.ts | 42 +++++++++---------- .../serverPlugin/src/common/typescripts.ts | 2 +- .../serverPlugin/src/node/create.ts | 2 +- .../serverPlugin/src/node/test/nes.spec.ts | 4 +- .../vscode-node/languageContextService.ts | 10 ++--- .../vscode-node/nesRenameService.ts | 10 ++--- .../typescriptContext/vscode-node/types.ts | 4 +- .../vscode-node/workspaceListenerService.ts | 2 +- .../copilot/src/lib/node/chatLibMain.ts | 2 +- .../authentication/common/authentication.ts | 2 +- .../src/platform/chat/common/commonTypes.ts | 2 +- .../endpoint/common/endpointProvider.ts | 2 +- .../test/node/openaiCompatibleEndpoint.ts | 2 +- .../git/vscode-node/gitServiceImpl.ts | 2 +- .../platform/inlineCompletions/common/api.ts | 4 +- .../common/dataTypes/languageContext.ts | 14 +++---- .../common/dataTypes/xtabPromptOptions.ts | 12 +++--- .../common/statelessNextEditProvider.ts | 8 ++-- .../nesXtabHistoryTracker.ts | 6 +-- .../workspaceDocumentEditTracker.ts | 2 +- .../node/inlineEditsModelService.ts | 2 +- .../src/platform/networking/common/fetch.ts | 4 +- .../platform/networking/common/networking.ts | 2 +- .../common/alternativeContentEditGenerator.ts | 2 +- .../review/vscode/reviewServiceImpl.ts | 4 +- .../telemetry/common/msftTelemetrySender.ts | 2 +- .../src/platform/thinking/common/thinking.ts | 2 +- extensions/copilot/src/util/node/worker.ts | 2 +- .../copilot/test/base/simulationOptions.ts | 2 +- .../test/pipeline/alternativeAction/types.ts | 6 +-- extensions/copilot/test/pipeline/pipeline.ts | 2 +- .../copilot/test/testVisualizationRunner.ts | 2 +- .../test/testVisualizationRunnerSTest.ts | 2 +- 50 files changed, 132 insertions(+), 130 deletions(-) diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/permissionHelpers.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/permissionHelpers.ts index db8bea59cc5c9..0d92735543381 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/permissionHelpers.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/permissionHelpers.ts @@ -28,7 +28,7 @@ type CoreTerminalConfirmationToolParams = { command: string | undefined; isBackground: boolean; }; -} +}; type CoreConfirmationToolParams = { tool: ToolName.CoreConfirmationTool; @@ -37,7 +37,7 @@ type CoreConfirmationToolParams = { message: string; confirmationType: 'basic'; }; -} +}; /** * The result of requesting permissions — the full union accepted by `Session.respondToPermission`. diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts index 6b24f11ea52be..ea01bc986cca6 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/test/claudeChatSessionContentProvider.spec.ts @@ -1202,6 +1202,7 @@ class FakeGitService extends mock() { } override dispose(): void { + super.dispose(); this._onDidOpenRepository.dispose(); this._onDidCloseRepository.dispose(); } diff --git a/extensions/copilot/src/extension/completions-core/vscode-node/extension/src/codeReferencing/index.ts b/extensions/copilot/src/extension/completions-core/vscode-node/extension/src/codeReferencing/index.ts index 18464815c0744..fc61f1fffd6ca 100644 --- a/extensions/copilot/src/extension/completions-core/vscode-node/extension/src/codeReferencing/index.ts +++ b/extensions/copilot/src/extension/completions-core/vscode-node/extension/src/codeReferencing/index.ts @@ -21,7 +21,7 @@ export class CodeReference implements IDisposable { constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ICompletionsRuntimeModeService readonly _runtimeMode: ICompletionsRuntimeModeService, + @ICompletionsRuntimeModeService private readonly _runtimeMode: ICompletionsRuntimeModeService, @ICompletionsLogTargetService private readonly _logTarget: ICompletionsLogTargetService, @IAuthenticationService private readonly _authenticationService: IAuthenticationService, ) { } diff --git a/extensions/copilot/src/extension/completions-core/vscode-node/extension/src/statusBar.ts b/extensions/copilot/src/extension/completions-core/vscode-node/extension/src/statusBar.ts index b936070cf03b3..aaf849637bcc1 100644 --- a/extensions/copilot/src/extension/completions-core/vscode-node/extension/src/statusBar.ts +++ b/extensions/copilot/src/extension/completions-core/vscode-node/extension/src/statusBar.ts @@ -21,8 +21,8 @@ export class CopilotStatusBar extends StatusReporter implements IDisposable { constructor( id: string, - @ICompletionsExtensionStatus readonly extensionStatusService: ICompletionsExtensionStatus, - @IInstantiationService readonly instantiationService: IInstantiationService, + @ICompletionsExtensionStatus private readonly extensionStatusService: ICompletionsExtensionStatus, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); diff --git a/extensions/copilot/src/extension/completions-core/vscode-node/lib/src/experiments/featuresService.ts b/extensions/copilot/src/extension/completions-core/vscode-node/lib/src/experiments/featuresService.ts index 62dc7c95b3813..f15af99a66c91 100644 --- a/extensions/copilot/src/extension/completions-core/vscode-node/lib/src/experiments/featuresService.ts +++ b/extensions/copilot/src/extension/completions-core/vscode-node/lib/src/experiments/featuresService.ts @@ -17,7 +17,7 @@ export type ContextProviderExpSettings = { excludeRelatedFiles: boolean; timeBudget: number; params?: Record; -} +}; export const ICompletionsFeaturesService = createServiceIdentifier('ICompletionsFeaturesService'); export interface ICompletionsFeaturesService { diff --git a/extensions/copilot/src/extension/completions-core/vscode-node/lib/src/prompt/similarFiles/openTabFiles.ts b/extensions/copilot/src/extension/completions-core/vscode-node/lib/src/prompt/similarFiles/openTabFiles.ts index a9dc51ffc7e44..b81b6b6239207 100644 --- a/extensions/copilot/src/extension/completions-core/vscode-node/lib/src/prompt/similarFiles/openTabFiles.ts +++ b/extensions/copilot/src/extension/completions-core/vscode-node/lib/src/prompt/similarFiles/openTabFiles.ts @@ -15,7 +15,9 @@ import { } from './neighborFiles'; export class OpenTabFiles implements INeighborSource { - constructor(@ICompletionsTextDocumentManagerService readonly docManager: ICompletionsTextDocumentManagerService) { } + constructor( + @ICompletionsTextDocumentManagerService private readonly docManager: ICompletionsTextDocumentManagerService + ) { } private truncateDocs( docs: readonly TextDocumentContents[], diff --git a/extensions/copilot/src/extension/conversation/vscode-node/conversationFeature.ts b/extensions/copilot/src/extension/conversation/vscode-node/conversationFeature.ts index 098cb35b01dac..41f331a5522de 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/conversationFeature.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/conversationFeature.ts @@ -49,9 +49,9 @@ import { generateTerminalFixes, setLastCommandMatchResult } from './terminalFixG */ export class ConversationFeature implements IExtensionContribution { /** Disposables that exist for the lifetime of this object */ - private _disposables = new DisposableStore(); + private readonly _disposables = new DisposableStore(); /** Disposables that are cleared whenever feature enablement is toggled */ - private _activatedDisposables = new DisposableStore(); + private readonly _activatedDisposables = new DisposableStore(); /** For the conversation features to be enabled, the proxy needs to return a token with k/v pair: chat=1 */ public _enabled; /** The feature is marked as active the first time it is enabled. */ diff --git a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts index 10ca8aa3c2f8e..addf7d792d568 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/languageModelAccess.ts @@ -486,7 +486,6 @@ class LanguageModelAccessPromptBaseCountCache { export class CopilotLanguageModelWrapper extends Disposable { constructor( - @IExperimentationService readonly _expService: IExperimentationService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IBlockedExtensionService private readonly _blockedExtensionService: IBlockedExtensionService, @IInstantiationService private readonly _instantiationService: IInstantiationService, diff --git a/extensions/copilot/src/extension/conversation/vscode-node/remoteAgents.ts b/extensions/copilot/src/extension/conversation/vscode-node/remoteAgents.ts index 24c3e41dbd18d..8b8539f7163d0 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/remoteAgents.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/remoteAgents.ts @@ -88,7 +88,7 @@ interface IGitHubRepositoryReference { } export class RemoteAgentContribution implements IDisposable { - private disposables = new DisposableStore(); + private readonly disposables = new DisposableStore(); private refreshRemoteAgentsP: Promise | undefined; private enabledSkillsPromise: Promise> | undefined; diff --git a/extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccess.test.ts b/extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccess.test.ts index bcb109a7dd545..290c04e472df7 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccess.test.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccess.test.ts @@ -29,7 +29,7 @@ suite('CopilotLanguageModelWrapper', () => { instaService = accessor.get(IInstantiationService); } - suite('validateRequest - invalid', async () => { + suite('validateRequest - invalid', () => { let wrapper: CopilotLanguageModelWrapper; let endpoint: IChatEndpoint; setup(async () => { @@ -59,7 +59,7 @@ suite('CopilotLanguageModelWrapper', () => { }); }); - suite('validateRequest - valid', async () => { + suite('validateRequest - valid', () => { let wrapper: CopilotLanguageModelWrapper; let endpoint: IChatEndpoint; setup(async () => { diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsBasedCompletions/importDiagnosticsCompletionProvider.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsBasedCompletions/importDiagnosticsCompletionProvider.ts index 9c6c314649788..5b4e1d2cc3db9 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsBasedCompletions/importDiagnosticsCompletionProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsBasedCompletions/importDiagnosticsCompletionProvider.ts @@ -361,7 +361,7 @@ export type ImportDetails = { labelShort: string; labelDeduped: string; importSource: ImportSource; -} +}; export interface ILanguageImportHandler { isImportDiagnostic(diagnostic: Diagnostic): boolean; diff --git a/extensions/copilot/src/extension/log/node/chatLogExport.ts b/extensions/copilot/src/extension/log/node/chatLogExport.ts index c61a044b72f26..b4f97d9d0e1d6 100644 --- a/extensions/copilot/src/extension/log/node/chatLogExport.ts +++ b/extensions/copilot/src/extension/log/node/chatLogExport.ts @@ -139,7 +139,7 @@ export async function createExportedPrompt( kind: 'error', error: error?.toString() || 'Unknown error', timestamp: new Date().toISOString() - } as unknown as ExportedLogEntry); + }); } } diff --git a/extensions/copilot/src/extension/mcp/vscode-node/commands.ts b/extensions/copilot/src/extension/mcp/vscode-node/commands.ts index 9cfa94dcd6b9c..eb8bd3b80b9bc 100644 --- a/extensions/copilot/src/extension/mcp/vscode-node/commands.ts +++ b/extensions/copilot/src/extension/mcp/vscode-node/commands.ts @@ -115,10 +115,10 @@ export class McpSetupCommands extends Disposable { }; constructor( - @ITelemetryService readonly telemetryService: ITelemetryService, - @ILogService readonly logService: ILogService, - @IFetcherService readonly fetcherService: IFetcherService, - @IInstantiationService readonly instantiationService: IInstantiationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @ILogService private readonly logService: ILogService, + @IFetcherService private readonly fetcherService: IFetcherService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this._register(toDisposable(() => this.pendingSetup?.cts.dispose(true))); diff --git a/extensions/copilot/src/extension/search/vscode-node/commands.ts b/extensions/copilot/src/extension/search/vscode-node/commands.ts index 4a81d810ed8cb..f7241fe3789c9 100644 --- a/extensions/copilot/src/extension/search/vscode-node/commands.ts +++ b/extensions/copilot/src/extension/search/vscode-node/commands.ts @@ -12,7 +12,7 @@ import { SearchFeedbackKind, SemanticSearchTextSearchProvider } from '../../work export class SearchPanelCommands extends Disposable { constructor( - @ITelemetryService readonly telemetryService: ITelemetryService, + @ITelemetryService telemetryService: ITelemetryService, @IFeedbackReporter private readonly feedbackReporter: IFeedbackReporter, ) { super(); diff --git a/extensions/copilot/src/extension/tools/common/virtualTools/preComputedToolEmbeddingsCache.ts b/extensions/copilot/src/extension/tools/common/virtualTools/preComputedToolEmbeddingsCache.ts index 58a306f2347f2..910d543251434 100644 --- a/extensions/copilot/src/extension/tools/common/virtualTools/preComputedToolEmbeddingsCache.ts +++ b/extensions/copilot/src/extension/tools/common/virtualTools/preComputedToolEmbeddingsCache.ts @@ -18,7 +18,7 @@ export class PreComputedToolEmbeddingsCache implements IToolEmbeddingsCache { private embeddingsMap: Map | undefined; constructor( - @ILogService readonly _logService: ILogService, + @ILogService private readonly _logService: ILogService, @IInstantiationService instantiationService: IInstantiationService, @IEnvService envService: IEnvService ) { diff --git a/extensions/copilot/src/extension/typescriptContext/common/serverProtocol.ts b/extensions/copilot/src/extension/typescriptContext/common/serverProtocol.ts index 3f24bcfefa91e..86ba81a2439fd 100644 --- a/extensions/copilot/src/extension/typescriptContext/common/serverProtocol.ts +++ b/extensions/copilot/src/extension/typescriptContext/common/serverProtocol.ts @@ -30,30 +30,30 @@ export enum CacheScopeKind { export type FileCacheScope = { kind: CacheScopeKind.File; -} +}; export type NeighborFilesCacheScope = { kind: CacheScopeKind.NeighborFiles; -} +}; export type Position = { line: number; character: number; -} +}; export type Range = { start: Position; end: Position; -} +}; export type WithinRangeCacheScope = { kind: CacheScopeKind.WithinRange; range: Range; -} +}; export type OutsideRangeCacheScope = { kind: CacheScopeKind.OutsideRange; ranges: Range[]; -} +}; export type CacheScope = FileCacheScope | NeighborFilesCacheScope | WithinRangeCacheScope | OutsideRangeCacheScope; @@ -66,7 +66,7 @@ export enum EmitMode { export type CacheInfo = { emitMode: EmitMode; scope: CacheScope; -} +}; export namespace CacheInfo { export type has = { cache: CacheInfo }; export function has(item: unknown): item is has { @@ -76,7 +76,7 @@ export namespace CacheInfo { export type CachedContextItem = { key: ContextItemKey; sizeInChars?: number; -} +}; export namespace CachedContextItem { export function create(key: ContextItemKey, sizeInChars?: number): CachedContextItem { return { key, sizeInChars }; @@ -236,7 +236,7 @@ export namespace ContextItem { export type PriorityTag = { priority: number; -} +}; export enum ContextRunnableState { Created = 'created', @@ -291,7 +291,7 @@ export type ContextRunnableResult = { * A human readable path to the signature to ease debugging. */ debugPath?: ContextRunnableResultId | undefined; -} +}; export type CachedContextRunnableResult = { @@ -317,7 +317,7 @@ export type CachedContextRunnableResult = { * The cache information of the runnable. */ cache?: CacheInfo; -} +}; export type ContextRunnableResultReference = { @@ -328,7 +328,7 @@ export type ContextRunnableResultReference = { * this state. */ id: ContextRunnableResultId; -} +}; export type ContextRunnableResultTypes = ContextRunnableResult | ContextRunnableResultReference; @@ -345,7 +345,7 @@ export namespace ErrorData { export type Timings = { totalTime: number; computeTime: number; -} +}; export namespace Timings { export function create(totalTime: number, computeTime: number): Timings { return { totalTime, computeTime }; @@ -397,7 +397,7 @@ export type ContextRequestResult = { * New server side context items that were computed. */ contextItems?: ContextItem[]; -} +}; export interface ComputeContextRequestArgs extends tt.server.protocol.FileLocationRequestArgs { startTime: number; @@ -504,17 +504,17 @@ export namespace PrepareNesRenameResult { canRename: RenameKind.yes; oldName: string; onOldState: boolean; - } + }; export type Maybe = { canRename: RenameKind.maybe; oldName: string; onOldState: boolean; - } + }; export type No = { canRename: RenameKind.no; timedOut: boolean; reason?: string; - } + }; } export type PrepareNesRenameResult = PrepareNesRenameResult.Yes | PrepareNesRenameResult.Maybe | PrepareNesRenameResult.No; @@ -566,17 +566,17 @@ export interface NesRenameRequestArgs extends tt.server.protocol.FileLocationReq export type TextChange = { range: Range; newText?: string; -} +}; export type RenameGroup = { file: FilePath; changes: TextChange[]; -} +}; export namespace NesRenameResult { export type OK = { groups: RenameGroup[]; - } + }; export type Failed = CustomResponse.Failed; } diff --git a/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts b/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts index d2ebb44a4216d..d40c2b7ee2fa2 100644 --- a/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts +++ b/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts @@ -374,7 +374,7 @@ export class RunnableResult { public addSnippet(code: SnippetProvider, location: SnippetLocation, key: string | undefined): void; public addSnippet(code: SnippetProvider, location: SnippetLocation, key: string | undefined, ifRoom: false): void; public addSnippet(code: SnippetProvider, location: SnippetLocation, key: string | undefined, ifRoom: true): boolean; - public addSnippet(code: SnippetProvider, location: SnippetLocation, key: string | undefined, ifRoom: boolean): boolean + public addSnippet(code: SnippetProvider, location: SnippetLocation, key: string | undefined, ifRoom: boolean): boolean; public addSnippet(code: SnippetProvider, location: SnippetLocation, key: string | undefined, ifRoom: boolean = false): boolean { const budget = location === SnippetLocation.Primary ? this.primaryBudget : this.secondaryBudget; if (code.isEmpty()) { @@ -684,7 +684,7 @@ class CacheBasedContextRunnable implements ContextRunnable { export type SymbolData = { symbol: tt.Symbol; name?: string; -} +}; enum SymbolEmitDataKind { symbol = 'symbol', @@ -695,12 +695,12 @@ type SymbolEmitData = { kind: SymbolEmitDataKind.symbol; symbol: tt.Symbol; name?: string; -} +}; type TypeAliasEmitData = { kind: SymbolEmitDataKind.typeAlias; node: tt.TypeAliasDeclaration; -} +}; type EmitData = SymbolEmitData | TypeAliasEmitData; @@ -1158,4 +1158,4 @@ export class CharacterBudget { this.spent(chars); this.throwIfExhausted(); } -} \ No newline at end of file +} diff --git a/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts b/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts index 3f24bcfefa91e..df8032f437f79 100644 --- a/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts +++ b/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/protocol.ts @@ -30,30 +30,30 @@ export enum CacheScopeKind { export type FileCacheScope = { kind: CacheScopeKind.File; -} +}; export type NeighborFilesCacheScope = { kind: CacheScopeKind.NeighborFiles; -} +}; export type Position = { line: number; character: number; -} +}; export type Range = { start: Position; end: Position; -} +}; export type WithinRangeCacheScope = { kind: CacheScopeKind.WithinRange; range: Range; -} +}; export type OutsideRangeCacheScope = { kind: CacheScopeKind.OutsideRange; ranges: Range[]; -} +}; export type CacheScope = FileCacheScope | NeighborFilesCacheScope | WithinRangeCacheScope | OutsideRangeCacheScope; @@ -66,7 +66,7 @@ export enum EmitMode { export type CacheInfo = { emitMode: EmitMode; scope: CacheScope; -} +}; export namespace CacheInfo { export type has = { cache: CacheInfo }; export function has(item: unknown): item is has { @@ -76,7 +76,7 @@ export namespace CacheInfo { export type CachedContextItem = { key: ContextItemKey; sizeInChars?: number; -} +}; export namespace CachedContextItem { export function create(key: ContextItemKey, sizeInChars?: number): CachedContextItem { return { key, sizeInChars }; @@ -236,7 +236,7 @@ export namespace ContextItem { export type PriorityTag = { priority: number; -} +}; export enum ContextRunnableState { Created = 'created', @@ -291,7 +291,7 @@ export type ContextRunnableResult = { * A human readable path to the signature to ease debugging. */ debugPath?: ContextRunnableResultId | undefined; -} +}; export type CachedContextRunnableResult = { @@ -317,7 +317,7 @@ export type CachedContextRunnableResult = { * The cache information of the runnable. */ cache?: CacheInfo; -} +}; export type ContextRunnableResultReference = { @@ -328,7 +328,7 @@ export type ContextRunnableResultReference = { * this state. */ id: ContextRunnableResultId; -} +}; export type ContextRunnableResultTypes = ContextRunnableResult | ContextRunnableResultReference; @@ -345,7 +345,7 @@ export namespace ErrorData { export type Timings = { totalTime: number; computeTime: number; -} +}; export namespace Timings { export function create(totalTime: number, computeTime: number): Timings { return { totalTime, computeTime }; @@ -397,7 +397,7 @@ export type ContextRequestResult = { * New server side context items that were computed. */ contextItems?: ContextItem[]; -} +}; export interface ComputeContextRequestArgs extends tt.server.protocol.FileLocationRequestArgs { startTime: number; @@ -504,17 +504,17 @@ export namespace PrepareNesRenameResult { canRename: RenameKind.yes; oldName: string; onOldState: boolean; - } + }; export type Maybe = { canRename: RenameKind.maybe; oldName: string; onOldState: boolean; - } + }; export type No = { canRename: RenameKind.no; timedOut: boolean; reason?: string; - } + }; } export type PrepareNesRenameResult = PrepareNesRenameResult.Yes | PrepareNesRenameResult.Maybe | PrepareNesRenameResult.No; @@ -566,17 +566,17 @@ export interface NesRenameRequestArgs extends tt.server.protocol.FileLocationReq export type TextChange = { range: Range; newText?: string; -} +}; export type RenameGroup = { file: FilePath; changes: TextChange[]; -} +}; export namespace NesRenameResult { export type OK = { groups: RenameGroup[]; - } + }; export type Failed = CustomResponse.Failed; } @@ -600,4 +600,4 @@ export namespace NesRenameResponse { export type NesRenameResponse = (tt.server.protocol.Response & { body: NesRenameResponse.OK | NesRenameResponse.Failed; -}) | { type: 'cancelled' }; \ No newline at end of file +}) | { type: 'cancelled' }; diff --git a/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/typescripts.ts b/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/typescripts.ts index e0bb6fb713dac..61136b3596a42 100644 --- a/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/typescripts.ts +++ b/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/common/typescripts.ts @@ -451,7 +451,7 @@ namespace tss { } } - export type DirectSuperSymbolInfo = { extends?: { symbol: tt.Symbol; name: string } | undefined; implements?: { symbol: tt.Symbol; name: string }[] } + export type DirectSuperSymbolInfo = { extends?: { symbol: tt.Symbol; name: string } | undefined; implements?: { symbol: tt.Symbol; name: string }[] }; export class Symbols { diff --git a/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/node/create.ts b/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/node/create.ts index 58c78e314b54f..44799003358f6 100644 --- a/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/node/create.ts +++ b/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/node/create.ts @@ -114,7 +114,7 @@ type ResolvedInput = { startTime: number; timeBudget: number; requestStartTime: number; -} +}; const resolveInput = (args: T | undefined, defaultTimeBudget: number): ResolvedInput | FailedHandlerResponse => { const requestStartTime = Date.now(); if (args === undefined) { diff --git a/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/node/test/nes.spec.ts b/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/node/test/nes.spec.ts index 7f1ec232603a7..a445ccdf7ac38 100644 --- a/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/node/test/nes.spec.ts +++ b/extensions/copilot/src/extension/typescriptContext/serverPlugin/src/node/test/nes.spec.ts @@ -80,12 +80,12 @@ type TrackedRenameInfo = { oldName: string; newName: string; range: Range; -} +}; type PostRenameTestCase = { trackedRename: TrackedRenameInfo; testCase: NesRenameTestCase; -} +}; function computeNesRenameTestCases(filePath: string): NesRenameTestCase[] { const text = fs.readFileSync(filePath, 'utf8'); diff --git a/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts b/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts index c735d3a8c78a7..7f48633ba47c0 100644 --- a/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts +++ b/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts @@ -477,7 +477,7 @@ type ContextRequestState = { type CacheInfo = { version: number; state: CacheState; -} +}; enum CacheState { NotPopulated = 'NotPopulated', @@ -1244,9 +1244,9 @@ export class LanguageContextServiceImpl implements ILanguageContextService, vsco public readonly onContextComputedOnTimeout: vscode.Event; constructor( + @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExperimentationService private readonly experimentationService: IExperimentationService, - @ITelemetryService readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService ) { this.isDebugging = process.execArgv.some((arg) => /^--(?:inspect|debug)(?:-brk)?(?:=\d+)?$/i.test(arg)); @@ -1791,7 +1791,7 @@ async function* mapAsyncIterable( const showContextInspectorViewContextKey = `github.copilot.chat.showContextInspectorView`; export class InlineCompletionContribution implements vscode.Disposable, TokenBudgetProvider { - private disposables: DisposableStore; + private readonly disposables: DisposableStore; private registrations: DisposableStore | undefined; private readonly registrationQueue: Queue; @@ -1799,10 +1799,10 @@ export class InlineCompletionContribution implements vscode.Disposable, TokenBud private readonly telemetrySender: TelemetrySender; constructor( + @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExperimentationService private readonly experimentationService: IExperimentationService, - @ILogService readonly logService: ILogService, - @ITelemetryService readonly telemetryService: ITelemetryService, + @ILogService private readonly logService: ILogService, @ILanguageContextService private readonly languageContextService: ILanguageContextService, @ILanguageContextProviderService private readonly languageContextProviderService: ILanguageContextProviderService, ) { diff --git a/extensions/copilot/src/extension/typescriptContext/vscode-node/nesRenameService.ts b/extensions/copilot/src/extension/typescriptContext/vscode-node/nesRenameService.ts index fa6fa590995a8..6201e9bfacc0c 100644 --- a/extensions/copilot/src/extension/typescriptContext/vscode-node/nesRenameService.ts +++ b/extensions/copilot/src/extension/typescriptContext/vscode-node/nesRenameService.ts @@ -70,7 +70,7 @@ namespace NesRenameRequestArgs { type TextChange = { range: protocol.Range; newText?: string; -} +}; type RenameGroup = { file: vscode.Uri; changes: TextChange[]; @@ -138,14 +138,14 @@ class TelemetrySender { export class NesRenameContribution implements vscode.Disposable { private _isActivated: Promise | undefined; - private disposables: DisposableStore; + private readonly disposables: DisposableStore; private readonly telemetrySender: TelemetrySender; private static readonly ExecConfig: ExecConfig = { executionTarget: ExecutionTarget.Semantic }; constructor( - @ITelemetryService readonly telemetryService: ITelemetryService, - @ILogService readonly logService: ILogService, + @ITelemetryService telemetryService: ITelemetryService, + @ILogService private readonly logService: ILogService, ) { this.telemetrySender = new TelemetrySender(telemetryService, logService); this.disposables = new DisposableStore(); @@ -345,4 +345,4 @@ export class NesRenameContribution implements vscode.Disposable { } return { document, position, oldName, newName }; } -} \ No newline at end of file +} diff --git a/extensions/copilot/src/extension/typescriptContext/vscode-node/types.ts b/extensions/copilot/src/extension/typescriptContext/vscode-node/types.ts index fc2037916e74c..0f4bcdc3904eb 100644 --- a/extensions/copilot/src/extension/typescriptContext/vscode-node/types.ts +++ b/extensions/copilot/src/extension/typescriptContext/vscode-node/types.ts @@ -15,7 +15,7 @@ export type ResolvedRunnableResult = { items: protocol.FullContextItem[]; cache?: protocol.CacheInfo; debugPath?: protocol.ContextRunnableResultId | undefined; -} +}; export namespace ResolvedRunnableResult { export function from(result: protocol.ContextRunnableResult, items: protocol.FullContextItem[]): ResolvedRunnableResult { return { @@ -34,7 +34,7 @@ export type ContextComputedEvent = { position: vscode.Position; source?: string; summary: ContextItemSummary; -} +}; export type OnCachePopulatedEvent = ContextComputedEvent & { items: ReadonlyArray }; export type OnContextComputedEvent = ContextComputedEvent & { items: ReadonlyArray }; diff --git a/extensions/copilot/src/extension/workspaceRecorder/vscode-node/workspaceListenerService.ts b/extensions/copilot/src/extension/workspaceRecorder/vscode-node/workspaceListenerService.ts index 140751fe2cc54..96ee0dcc18745 100644 --- a/extensions/copilot/src/extension/workspaceRecorder/vscode-node/workspaceListenerService.ts +++ b/extensions/copilot/src/extension/workspaceRecorder/vscode-node/workspaceListenerService.ts @@ -10,7 +10,7 @@ import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { IRecordableEditorLogEntry, IRecordableLogEntry, ITextModelEditReasonMetadata, IWorkspaceListenerService } from '../common/workspaceListenerService'; export class WorkspacListenerService extends Disposable implements IWorkspaceListenerService { - readonly _serviceBrand = undefined; + declare _serviceBrand: undefined; private readonly _onStructuredData = this._register(new Emitter()); readonly onStructuredData = this._onStructuredData.event; diff --git a/extensions/copilot/src/lib/node/chatLibMain.ts b/extensions/copilot/src/lib/node/chatLibMain.ts index 2f399d4095983..4dd64f67f70b0 100644 --- a/extensions/copilot/src/lib/node/chatLibMain.ts +++ b/extensions/copilot/src/lib/node/chatLibMain.ts @@ -716,7 +716,7 @@ export interface IEditorSession { readonly uiKind?: string; } -export type IActionItem = ActionItem +export type IActionItem = ActionItem; export interface INotificationSender { showWarningMessage(message: string, ...actions: IActionItem[]): Promise; } diff --git a/extensions/copilot/src/platform/authentication/common/authentication.ts b/extensions/copilot/src/platform/authentication/common/authentication.ts index 99a0338673656..d0adc1d09a1f8 100644 --- a/extensions/copilot/src/platform/authentication/common/authentication.ts +++ b/extensions/copilot/src/platform/authentication/common/authentication.ts @@ -277,7 +277,7 @@ export abstract class BaseAuthenticationService extends Disposable implements IA // #endregion //#region ADO Token - abstract getAdoAccessTokenBase64(options?: AuthenticationGetSessionOptions): Promise + abstract getAdoAccessTokenBase64(options?: AuthenticationGetSessionOptions): Promise; //#endregion protected async _handleAuthChangeEvent(): Promise { diff --git a/extensions/copilot/src/platform/chat/common/commonTypes.ts b/extensions/copilot/src/platform/chat/common/commonTypes.ts index 73fe349229dd5..6fb7bae58f98b 100644 --- a/extensions/copilot/src/platform/chat/common/commonTypes.ts +++ b/extensions/copilot/src/platform/chat/common/commonTypes.ts @@ -187,7 +187,7 @@ export type ChatFetchRetriableError = /** * We requested conversation, the response was filtered by RAI, but we want to retry. */ - { type: ChatFetchResponseType.FilteredRetry; reason: string; category: FilterReason; value: T; requestId: string; serverRequestId: string | undefined } + { type: ChatFetchResponseType.FilteredRetry; reason: string; category: FilterReason; value: T; requestId: string; serverRequestId: string | undefined }; export type FetchSuccess = { type: ChatFetchResponseType.Success; value: T; requestId: string; serverRequestId: string | undefined; usage: APIUsage | undefined; resolvedModel: string }; diff --git a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts index 64de72693150b..32b19748b143e 100644 --- a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts +++ b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts @@ -70,7 +70,7 @@ type ICompletionModelCapabilities = { type: 'completion'; family: string; tokenizer: TokenizerType; -} +}; export enum ModelSupportedEndpoint { ChatCompletions = '/chat/completions', diff --git a/extensions/copilot/src/platform/endpoint/test/node/openaiCompatibleEndpoint.ts b/extensions/copilot/src/platform/endpoint/test/node/openaiCompatibleEndpoint.ts index b76279558497d..cae82f015dc0a 100644 --- a/extensions/copilot/src/platform/endpoint/test/node/openaiCompatibleEndpoint.ts +++ b/extensions/copilot/src/platform/endpoint/test/node/openaiCompatibleEndpoint.ts @@ -73,7 +73,7 @@ export type IModelConfig = { max_completion_tokens?: number | null; intent?: boolean | null; }; -} +}; export class OpenAICompatibleTestEndpoint extends ChatEndpoint { constructor( diff --git a/extensions/copilot/src/platform/git/vscode-node/gitServiceImpl.ts b/extensions/copilot/src/platform/git/vscode-node/gitServiceImpl.ts index 8d4949173dbb3..ccee88711f1e8 100644 --- a/extensions/copilot/src/platform/git/vscode-node/gitServiceImpl.ts +++ b/extensions/copilot/src/platform/git/vscode-node/gitServiceImpl.ts @@ -483,7 +483,7 @@ export class GitServiceImpl extends Disposable implements IGitService { } private static repoToRepoContext(repo: Repository): RepoContext; - private static repoToRepoContext(repo: Repository | undefined | null): RepoContext | undefined + private static repoToRepoContext(repo: Repository | undefined | null): RepoContext | undefined; private static repoToRepoContext(repo: Repository | undefined | null): RepoContext | undefined { if (!repo) { return undefined; diff --git a/extensions/copilot/src/platform/inlineCompletions/common/api.ts b/extensions/copilot/src/platform/inlineCompletions/common/api.ts index 60a820a3bd9f7..8634278dbf792 100644 --- a/extensions/copilot/src/platform/inlineCompletions/common/api.ts +++ b/extensions/copilot/src/platform/inlineCompletions/common/api.ts @@ -11,12 +11,12 @@ export namespace Copilot { export type Position = { line: number; character: number; - } + }; export type Range = { start: Position; end: Position; - } + }; /** * The ContextProvider API allows extensions to provide additional context items that diff --git a/extensions/copilot/src/platform/inlineEdits/common/dataTypes/languageContext.ts b/extensions/copilot/src/platform/inlineEdits/common/dataTypes/languageContext.ts index a9b0725f0cf76..16e23a2535ee2 100644 --- a/extensions/copilot/src/platform/inlineEdits/common/dataTypes/languageContext.ts +++ b/extensions/copilot/src/platform/inlineEdits/common/dataTypes/languageContext.ts @@ -11,13 +11,13 @@ export type LanguageContextEntry = { context: ContextItem; timeStamp: number; onTimeout: boolean; -} +}; export type LanguageContextResponse = { start: number; end: number; items: LanguageContextEntry[]; -} +}; type SerializedSnippetContext = { kind: ContextKind.Snippet; @@ -25,21 +25,21 @@ type SerializedSnippetContext = { uri: string; additionalUris?: string[]; value: string; -} +}; type SerializedTraitContext = { kind: ContextKind.Trait; priority: number; name: string; value: string; -} +}; type SerializedDiagnosticBagContext = { kind: ContextKind.DiagnosticBag; priority: number; uri: string; values: Omit[]; -} +}; type SerializedContextItem = SerializedSnippetContext | SerializedTraitContext | SerializedDiagnosticBagContext; @@ -50,7 +50,7 @@ export type SerializedContextResponse = { context: SerializedContextItem; timeStamp: number; }[]; -} +}; export function serializeLanguageContext(response: LanguageContextResponse): SerializedContextResponse { return { @@ -113,7 +113,7 @@ export type SerializedDiagnostic = { source: string; code: string | number | undefined; range: string; -} +}; function serializeDiagnostic(diagnostic: Diagnostic): Omit; function serializeDiagnostic(diagnostic: Diagnostic, resource: Uri): SerializedDiagnostic; diff --git a/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts b/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts index 0c9220928a0fc..0dee4e8cf4d98 100644 --- a/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts +++ b/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts @@ -31,7 +31,7 @@ export type RecentlyViewedDocumentsOptions = { readonly includeViewedFiles: boolean; readonly includeLineNumbers: IncludeLineNumbersOption; readonly clippingStrategy: RecentFileClippingStrategy; -} +}; export namespace RecentlyViewedDocumentsOptions { export const VALIDATOR: IValidator> = vObj({ @@ -49,14 +49,14 @@ export type LanguageContextOptions = { readonly enabled: boolean; readonly maxTokens: number; readonly traitPosition: 'before' | 'after'; -} +}; export type DiffHistoryOptions = { readonly nEntries: number; readonly maxTokens: number; readonly onlyForDocsInPrompt: boolean; readonly useRelativePaths: boolean; -} +}; export type PagedClipping = { pageSize: number }; @@ -66,7 +66,7 @@ export type CurrentFileOptions = { readonly includeLineNumbers: IncludeLineNumbersOption; readonly includeCursorTag: boolean; readonly prioritizeAboveCursor: boolean; -} +}; export namespace CurrentFileOptions { export const VALIDATOR: IValidator> = vObj({ @@ -96,7 +96,7 @@ export type LintOptions = { maxLineDistance: number; /** When set to a value > 0, also include linter diagnostics from the N most recently edited/viewed files. */ nRecentFiles: number; -} +}; /** * The raw user-facing aggressiveness setting. Includes `Default` to distinguish @@ -241,7 +241,7 @@ export type PromptOptions = { readonly diffHistory: DiffHistoryOptions; readonly includePostScript: boolean; readonly lintOptions: LintOptions | undefined; -} +}; /** * Prompt strategies that tweak prompt in a way that's different from current prod prompting strategy. diff --git a/extensions/copilot/src/platform/inlineEdits/common/statelessNextEditProvider.ts b/extensions/copilot/src/platform/inlineEdits/common/statelessNextEditProvider.ts index 6a3c77f6edacc..3e27f7701fae3 100644 --- a/extensions/copilot/src/platform/inlineEdits/common/statelessNextEditProvider.ts +++ b/extensions/copilot/src/platform/inlineEdits/common/statelessNextEditProvider.ts @@ -26,7 +26,7 @@ import { InlineEditRequestLogContext } from './inlineEditLogContext'; import { stringifyChatMessages } from './utils/stringifyChatMessages'; import { IXtabHistoryEntry } from './workspaceEditTracker/nesXtabHistoryTracker'; -export type EditStreaming = AsyncGenerator +export type EditStreaming = AsyncGenerator; export class WithStatelessProviderTelemetry { constructor( @@ -36,7 +36,7 @@ export class WithStatelessProviderTelemetry { } } -export type EditStreamingWithTelemetry = AsyncGenerator, WithStatelessProviderTelemetry, void> +export type EditStreamingWithTelemetry = AsyncGenerator, WithStatelessProviderTelemetry, void>; export type StreamedEdit = { readonly targetDocument: DocumentId; @@ -49,7 +49,7 @@ export type StreamedEdit = { * in either the original location or the jump target location. */ readonly originalWindow?: OffsetRange; -} +}; export type PushEdit = (edit: Result) => void; @@ -432,7 +432,7 @@ export type FetchResultWithStats = { readonly response: FetchResponse; readonly fetchTime: number; readonly fetchResult: ChatFetchResponseType; -} +}; export class StatelessNextEditTelemetryBuilder { diff --git a/extensions/copilot/src/platform/inlineEdits/common/workspaceEditTracker/nesXtabHistoryTracker.ts b/extensions/copilot/src/platform/inlineEdits/common/workspaceEditTracker/nesXtabHistoryTracker.ts index c755f5bd9862b..ed7eaa8c671ed 100644 --- a/extensions/copilot/src/platform/inlineEdits/common/workspaceEditTracker/nesXtabHistoryTracker.ts +++ b/extensions/copilot/src/platform/inlineEdits/common/workspaceEditTracker/nesXtabHistoryTracker.ts @@ -38,19 +38,19 @@ export interface IXtabHistoryVisibleRangesEntry extends IXtabHistoryDocumentEntr export type IXtabHistoryEntry = | IXtabHistoryEditEntry - | IXtabHistoryVisibleRangesEntry + | IXtabHistoryVisibleRangesEntry; type DocumentChangedEvent = { value: StringText; changes: StringEdit[]; previous: StringText | undefined; -} +}; type DocumentSelectionChangedEvent = { value: readonly OffsetRange[]; changes: unknown[]; previous: readonly OffsetRange[] | undefined; -} +}; /** * Controls how consecutive edits to the same document are merged in history. diff --git a/extensions/copilot/src/platform/inlineEdits/common/workspaceEditTracker/workspaceDocumentEditTracker.ts b/extensions/copilot/src/platform/inlineEdits/common/workspaceEditTracker/workspaceDocumentEditTracker.ts index a764232212ed8..64a029afd5380 100644 --- a/extensions/copilot/src/platform/inlineEdits/common/workspaceEditTracker/workspaceDocumentEditTracker.ts +++ b/extensions/copilot/src/platform/inlineEdits/common/workspaceEditTracker/workspaceDocumentEditTracker.ts @@ -17,7 +17,7 @@ export type DocumentHistoryDifference = { before: StringText; after: StringText; edits: StringEdit; -} +}; export class WorkspaceDocumentEditHistory extends Disposable { private readonly _documentState = new Map(); diff --git a/extensions/copilot/src/platform/inlineEdits/node/inlineEditsModelService.ts b/extensions/copilot/src/platform/inlineEdits/node/inlineEditsModelService.ts index 84a0dde583b62..0066d0d6e10b5 100644 --- a/extensions/copilot/src/platform/inlineEdits/node/inlineEditsModelService.ts +++ b/extensions/copilot/src/platform/inlineEdits/node/inlineEditsModelService.ts @@ -39,7 +39,7 @@ interface ModelConfigurationWithSource extends ModelConfiguration { type ModelInfo = { models: ModelConfigurationWithSource[]; currentModelId: string; -} +}; export class InlineEditsModelService extends Disposable implements IInlineEditsModelService { diff --git a/extensions/copilot/src/platform/networking/common/fetch.ts b/extensions/copilot/src/platform/networking/common/fetch.ts index 5bda09c2f668e..02a3f985c1417 100644 --- a/extensions/copilot/src/platform/networking/common/fetch.ts +++ b/extensions/copilot/src/platform/networking/common/fetch.ts @@ -304,12 +304,12 @@ export type StreamOptions = { * All other chunks will also include a usage field, but with a null value. NOTE: If the stream is interrupted, you may not receive the final usage chunk which contains the total token usage for the request. */ include_usage?: boolean; -} +}; export type Prediction = { type: 'content'; content: string | { type: string; text: string }[]; -} +}; /** based on https://platform.openai.com/docs/api-reference/chat/create * diff --git a/extensions/copilot/src/platform/networking/common/networking.ts b/extensions/copilot/src/platform/networking/common/networking.ts index 927855b3548ac..8c70a781c1e53 100644 --- a/extensions/copilot/src/platform/networking/common/networking.ts +++ b/extensions/copilot/src/platform/networking/common/networking.ts @@ -231,7 +231,7 @@ export type IChatRequestTelemetryProperties = { parentRequestId?: string; /** For a subagent: The tool_call_id from the parent agent's LLM response that triggered this subagent invocation. */ parentToolCallId?: string; -} +}; export interface ICreateEndpointBodyOptions extends IMakeChatRequestOptions { requestId: string; diff --git a/extensions/copilot/src/platform/notebook/common/alternativeContentEditGenerator.ts b/extensions/copilot/src/platform/notebook/common/alternativeContentEditGenerator.ts index 02bc8e99af0cd..2ab7f8dd581dd 100644 --- a/extensions/copilot/src/platform/notebook/common/alternativeContentEditGenerator.ts +++ b/extensions/copilot/src/platform/notebook/common/alternativeContentEditGenerator.ts @@ -23,7 +23,7 @@ export type NotebookEditGenerationTelemtryOptions = { model: Promise | string | undefined; requestId: string | undefined; source: NotebookEditGenrationSource; -} +}; export enum NotebookEditGenrationSource { codeMapperEditNotebook = 'codeMapperEditNotebook', diff --git a/extensions/copilot/src/platform/review/vscode/reviewServiceImpl.ts b/extensions/copilot/src/platform/review/vscode/reviewServiceImpl.ts index 8148929bb4dc9..8861a96de252c 100644 --- a/extensions/copilot/src/platform/review/vscode/reviewServiceImpl.ts +++ b/extensions/copilot/src/platform/review/vscode/reviewServiceImpl.ts @@ -24,8 +24,8 @@ const numberOfReviewCommentsKey = 'github.copilot.chat.review.numberOfComments'; export class ReviewServiceImpl implements IReviewService { declare _serviceBrand: undefined; - private _disposables = new DisposableStore(); - private _repositoryDisposables = new DisposableStore(); + private readonly _disposables = new DisposableStore(); + private readonly _repositoryDisposables = new DisposableStore(); private _reviewDiffReposString: string | undefined; private _diagnosticCollection: vscode.DiagnosticCollection | undefined; private _commentController = vscode.comments.createCommentController('github-copilot-review', 'Code Review'); diff --git a/extensions/copilot/src/platform/telemetry/common/msftTelemetrySender.ts b/extensions/copilot/src/platform/telemetry/common/msftTelemetrySender.ts index 2074272384dd8..0bbc00c2509dc 100644 --- a/extensions/copilot/src/platform/telemetry/common/msftTelemetrySender.ts +++ b/extensions/copilot/src/platform/telemetry/common/msftTelemetrySender.ts @@ -20,7 +20,7 @@ export class BaseMsftTelemetrySender implements IMSFTTelemetrySender { protected _internalLargeEventTelemetryReporter: ITelemetryReporter | undefined; private _externalTelemetryReporter: ITelemetryReporter; - protected _disposables: DisposableStore = new DisposableStore(); + protected readonly _disposables: DisposableStore = new DisposableStore(); private _username: string | undefined; private _vscodeTeamMember: boolean = false; private _sku: string | undefined; diff --git a/extensions/copilot/src/platform/thinking/common/thinking.ts b/extensions/copilot/src/platform/thinking/common/thinking.ts index 60d71b85ed713..f61ef2af8ddf0 100644 --- a/extensions/copilot/src/platform/thinking/common/thinking.ts +++ b/extensions/copilot/src/platform/thinking/common/thinking.ts @@ -46,7 +46,7 @@ export type EncryptedThinkingDelta = { id: string; text?: string; encrypted: string; -} +}; export function isEncryptedThinkingDelta(delta: ThinkingDelta | EncryptedThinkingDelta): delta is EncryptedThinkingDelta { return (delta as EncryptedThinkingDelta).encrypted !== undefined; diff --git a/extensions/copilot/src/util/node/worker.ts b/extensions/copilot/src/util/node/worker.ts index 425387c2bb511..0fface63bf6fe 100644 --- a/extensions/copilot/src/util/node/worker.ts +++ b/extensions/copilot/src/util/node/worker.ts @@ -60,7 +60,7 @@ export class RcpResponseHandler { export type RpcProxy = { [K in keyof ProxyType]: ProxyType[K] extends ((...args: infer Args) => infer R) ? (...args: Args) => Promise> : never; -} +}; export function createRpcProxy(remoteCall: (name: string, args: any[]) => Promise): RpcProxy { const handler = { diff --git a/extensions/copilot/test/base/simulationOptions.ts b/extensions/copilot/test/base/simulationOptions.ts index ad45fd201867f..84f5d40fcc8a3 100644 --- a/extensions/copilot/test/base/simulationOptions.ts +++ b/extensions/copilot/test/base/simulationOptions.ts @@ -14,7 +14,7 @@ export type NesDatagen = { readonly output: string | undefined; readonly rowOffset: number; readonly workerMode: boolean; -} +}; export class SimulationOptions { public static fromProcessArgs(): SimulationOptions { diff --git a/extensions/copilot/test/pipeline/alternativeAction/types.ts b/extensions/copilot/test/pipeline/alternativeAction/types.ts index 40e8733bc6bf8..51e8bce74729e 100644 --- a/extensions/copilot/test/pipeline/alternativeAction/types.ts +++ b/extensions/copilot/test/pipeline/alternativeAction/types.ts @@ -18,7 +18,7 @@ export type IData = { isInlineCompletion: boolean; }; suggestionStatus: NextEditTelemetryStatus; -} +}; export namespace NextUserEdit { export type t = { @@ -36,7 +36,7 @@ export namespace Recording { relativePath: string; originalOpIdx: number; }; - } + }; } export namespace SuggestedEdit { @@ -45,7 +45,7 @@ export namespace SuggestedEdit { edit: ISerializedEdit; scoreCategory: 'nextEdit'; score: number; - } + }; } export namespace Scoring { diff --git a/extensions/copilot/test/pipeline/pipeline.ts b/extensions/copilot/test/pipeline/pipeline.ts index 0a17601a79c08..2e05f9e33e118 100644 --- a/extensions/copilot/test/pipeline/pipeline.ts +++ b/extensions/copilot/test/pipeline/pipeline.ts @@ -40,7 +40,7 @@ export type RunPipelineOptions = { readonly configFile: string | undefined; readonly verbose: number | boolean | undefined; readonly parallelism: number; -} +}; export async function runInputPipeline(opts: RunPipelineOptions, log = console.log.bind(console)): Promise { const nesDatagenOpts = opts.nesDatagen!; diff --git a/extensions/copilot/test/testVisualizationRunner.ts b/extensions/copilot/test/testVisualizationRunner.ts index 2b53cb723d2b8..38fa828ca828b 100644 --- a/extensions/copilot/test/testVisualizationRunner.ts +++ b/extensions/copilot/test/testVisualizationRunner.ts @@ -25,7 +25,7 @@ function run(args: { fileName: string; path: string[] }) { runCurrentTest(); } -const g = globalThis as any as IDebugValueEditorGlobals & IPlaygroundRunnerGlobals; +const g = globalThis as unknown as IDebugValueEditorGlobals & IPlaygroundRunnerGlobals; g.$$playgroundRunner_data = { currentPath: [] }; // The timeout seems to fix a deadlock-issue of tsx, when the run function is called from the debugger. diff --git a/extensions/copilot/test/testVisualizationRunnerSTest.ts b/extensions/copilot/test/testVisualizationRunnerSTest.ts index bdf1bd8195405..8f711a37c5b8c 100644 --- a/extensions/copilot/test/testVisualizationRunnerSTest.ts +++ b/extensions/copilot/test/testVisualizationRunnerSTest.ts @@ -18,7 +18,7 @@ function run(args: { fileName: string; path: string }) { runCurrentTest(); } -const g = globalThis as any as IDebugValueEditorGlobals & IPlaygroundRunnerGlobals; +const g = globalThis as unknown as IDebugValueEditorGlobals & IPlaygroundRunnerGlobals; g.$$playgroundRunner_data = { currentPath: [] }; From 1b82a19e2b421789db70613da200dc2235a12791 Mon Sep 17 00:00:00 2001 From: Hawk Ticehurst <39639992+hawkticehurst@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:59:57 -0400 Subject: [PATCH 10/32] sessions: animate active sidebar chat spinner (#311440) sessions: animate active sidebar chat icons Render the existing spinning loading icon for in-progress sessions in the sidebar list so active chats match the rest of the sessions app. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts index c6f81b34beacb..2bf8add8446ee 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts @@ -457,7 +457,7 @@ class SessionItemRenderer implements ITreeRenderer Date: Mon, 20 Apr 2026 19:05:47 +0000 Subject: [PATCH 11/32] [cherry-pick] Hide review button when chat plan is collapsed (#311445) Co-authored-by: vs-code-engineering[bot] --- .../browser/widget/chatContentParts/chatPlanReviewPart.ts | 1 + .../browser/widget/chatContentParts/media/chatPlanReview.css | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPlanReviewPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPlanReviewPart.ts index b75e9091c593d..5825d0f7dcbe0 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPlanReviewPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatPlanReviewPart.ts @@ -337,6 +337,7 @@ export class ChatPlanReviewPart extends Disposable implements IChatContentPart { private updateCollapsedPresentation(): void { this.domNode.classList.toggle('chat-plan-review-collapsed', this._isCollapsed); + this._restoreButton.element.classList.toggle('chat-plan-review-hidden', this._isCollapsed); this._collapseButton.label = this._isCollapsed ? `$(${Codicon.chevronDown.id})` : `$(${Codicon.chevronUp.id})`; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatPlanReview.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatPlanReview.css index ed4307e8dc185..aaf8596568a8c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatPlanReview.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatPlanReview.css @@ -234,6 +234,11 @@ width: auto; } +/* ---------- Hidden helper ---------- */ +.interactive-session .chat-plan-review-container .monaco-button.chat-plan-review-hidden { + display: none; +} + /* ---------- Collapsed state ---------- */ .interactive-session .chat-plan-review-container.chat-plan-review-collapsed > .chat-confirmation-widget2.chat-plan-review > .chat-confirmation-widget-message-scrollable, .interactive-session .chat-plan-review-container.chat-plan-review-collapsed > .chat-confirmation-widget2.chat-plan-review > .chat-plan-review-feedback, From 11d49abcbf1fc58e40ba9fb4782abed1f7ce311b Mon Sep 17 00:00:00 2001 From: Ulugbek Abdullaev Date: Tue, 21 Apr 2026 00:31:30 +0500 Subject: [PATCH 12/32] nes: fix: do not insert spurious trailing newline after suggestion (#311441) * nes: show how insertion at cursor leaves an unnecessary newline after the suggestion Co-authored-by: Copilot * nes: fix: do not insert spurious trailing newline after suggestion Co-authored-by: Copilot * account for CRLF Co-authored-by: Copilot --------- Co-authored-by: Copilot --- .../vscode-node/isInlineSuggestion.spec.ts | 107 +++++++++++++++++- .../vscode-node/isInlineSuggestion.ts | 8 +- 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/extensions/copilot/src/extension/inlineEdits/test/vscode-node/isInlineSuggestion.spec.ts b/extensions/copilot/src/extension/inlineEdits/test/vscode-node/isInlineSuggestion.spec.ts index cc9a7b0226b9e..6ed0e69d3cb40 100644 --- a/extensions/copilot/src/extension/inlineEdits/test/vscode-node/isInlineSuggestion.spec.ts +++ b/extensions/copilot/src/extension/inlineEdits/test/vscode-node/isInlineSuggestion.spec.ts @@ -101,8 +101,9 @@ suite('toInlineSuggestion', () => { assert.isDefined(result); // Range is an empty range at the cursor for a pure insertion assert.deepStrictEqual(result!.range, new Range(1, 15, 1, 15)); - // Text is prepended with the newline between cursor and original range - assert.strictEqual(result!.newText, '\n' + replaceText); + // Text is prepended with the newline between cursor and original range, + // and the trailing newline is dropped so we don't introduce a blank line. + assert.strictEqual(result!.newText, '\n' + replaceText.replace(/\r?\n$/, '')); }); test('should not use ghost text when inserting on next line when none empty', () => { @@ -149,7 +150,8 @@ suite('toInlineSuggestion', () => { const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText); assert.isDefined(result); assert.deepStrictEqual(result!.range, new Range(0, 13, 0, 13)); - assert.strictEqual(result!.newText, '\n' + replaceText); + // Trailing '\n' is dropped to avoid a spurious blank line. + assert.strictEqual(result!.newText, '\n' + replaceText.replace(/\r?\n$/, '')); }); test('multi-line insertion without trailing newline rejected when target line has content', () => { @@ -318,7 +320,8 @@ function createDocumentSymbol( const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText); assert.isDefined(result); assert.deepStrictEqual(result!.range, new Range(0, 6, 0, 6)); - assert.strictEqual(result!.newText, '\n\n'); + // Trailing '\n' is dropped — only the prepended newline remains. + assert.strictEqual(result!.newText, '\n'); }); test('next-line: cursor at end of an empty line', () => { @@ -330,7 +333,8 @@ function createDocumentSymbol( const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText); assert.isDefined(result); assert.deepStrictEqual(result!.range, new Range(0, 0, 0, 0)); - assert.strictEqual(result!.newText, '\nnew line\n'); + // Trailing '\n' is dropped to avoid a spurious blank line. + assert.strictEqual(result!.newText, '\nnew line'); }); test('next-line: range on line before cursor is rejected', () => { @@ -547,4 +551,97 @@ function createDocumentSymbol( assert.deepStrictEqual(result!.range, new Range(1, 0, 1, 0)); assert.strictEqual(result!.newText, ''); }); + + test('insertion on next line in fieldLabels object', () => { + const doc = `import React, { useState } from "react"; + +interface FormData { + firstName: string; + lastName: string; + password: string; + email: string; + age: string; + city: string; +} + +const initialFormData: FormData = { + firstName: "", + lastName: "", + password: "", + email: "", + age: "", + city: "", +}; + +const fieldLabels: Record = { + firstName: "First Name", + lastName: "Last Name", + email: "Email Address", + age: "Age", + city: "City", +}; +`; + const document = createTextDocumentData(Uri.from({ scheme: 'test', path: '/test/file.tsx' }), doc, 'typescriptreact').document; + const cursorPosition = new Position(22, 26); // end of ` lastName: "Last Name",` + const replaceRange = new Range(23, 0, 23, 0); + const replaceText = ' password: "Password",\n'; + + const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText, true); + assert.isDefined(result); + assert.deepStrictEqual(result!.range, new Range(22, 26, 22, 26)); + // Trailing '\n' is dropped because the original line terminator after + // the cursor is preserved. + assert.strictEqual(result!.newText, '\n password: "Password",'); + }); + + suite('CRLF', () => { + + function createCRLFDocument(lines: string[], languageId: string = 'typescript') { + return createTextDocumentData( + Uri.from({ scheme: 'test', path: '/test/file.ts' }), + lines.join('\r\n'), + languageId, + '\r\n', + ).document; + } + + test('next-line insertion: trailing CRLF is dropped (no dangling \\r)', () => { + const document = createCRLFDocument(['function foo(', '', 'other']); + const cursorPosition = new Position(0, 13); // end of "function foo(" + const replaceRange = new Range(1, 0, 1, 0); // empty line + const replaceText = ' a: string,\r\n b: number\r\n'; + + const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText); + assert.isDefined(result); + assert.deepStrictEqual(result!.range, new Range(0, 13, 0, 13)); + // The trailing CRLF must be stripped entirely; no dangling '\r' + // should leak into the suggestion text. + assert.strictEqual(result!.newText, '\r\n a: string,\r\n b: number'); + }); + + test('next-line insertion: trailing CRLF on non-empty target line', () => { + const document = createCRLFDocument(['function foo(', ')', 'other']); + const cursorPosition = new Position(0, 13); + const replaceRange = new Range(1, 0, 1, 0); + const replaceText = ' a: string,\r\n b: number\r\n'; + + const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText); + assert.isDefined(result); + assert.deepStrictEqual(result!.range, new Range(0, 13, 0, 13)); + assert.strictEqual(result!.newText, '\r\n a: string,\r\n b: number'); + }); + + test('next-line insertion: CRLF-only newText is fully stripped', () => { + const document = createCRLFDocument(['line 0', '', 'line 2']); + const cursorPosition = new Position(0, 6); + const replaceRange = new Range(1, 0, 1, 0); + const replaceText = '\r\n'; + + const result = toInlineSuggestion(cursorPosition, document, replaceRange, replaceText); + assert.isDefined(result); + assert.deepStrictEqual(result!.range, new Range(0, 6, 0, 6)); + // Only the prepended CRLF between cursor and original range remains. + assert.strictEqual(result!.newText, '\r\n'); + }); + }); }); diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts index 3add1ecc00e94..12913941227ab 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts @@ -26,7 +26,13 @@ export function toInlineSuggestion(cursorPos: Position, doc: TextDocument, range // Use an empty range at the cursor so the suggestion is a pure insertion const adjustedRange = new Range(cursorPos, cursorPos); const textBetweenCursorAndRange = doc.getText(new Range(cursorPos, range.start)); - return { range: adjustedRange, newText: textBetweenCursorAndRange + newText }; + // The original range is on the next line, so the line terminator that + // already separates the cursor's line from range.start is preserved. + // Drop a single trailing line ending from newText (if present) to avoid + // inserting an extra blank line after the suggestion. Handle CRLF as + // well as LF so we don't leave a dangling '\r'. + const adjustedNewText = newText.replace(/\r?\n$/, ''); + return { range: adjustedRange, newText: textBetweenCursorAndRange + adjustedNewText }; } if (advanced) { From a557dbcf64be1920a81b22e26a34a15343bad8ab Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 20 Apr 2026 12:51:16 -0700 Subject: [PATCH 13/32] Await pending session DB writes on agent host shutdown (#311432) * Await pending session DB writes on agent host shutdown The agent host server's SIGTERM/SIGINT handler called process.exit(0) synchronously, abandoning any fire-and-forget SQLite writes that were in flight (configValues, customTitle, isRead/isDone, diffs). Under CI load this caused the 'Session Config persistence across restarts' integration test to the most recent SessionConfigChanged writeflake could lose the race against shutdown, leaving the previous value persisted instead. Track in-flight writes inside SessionDatabase via a _pendingWrites set populated by every public mutating method (the outermost wrap is required so the await this._ensureDb() window is also covered). SessionDataService aggregates whenIdle() across all live per-session DBs. The server's shutdown handler now awaits this with a 3s raceTimeout before disposing. Removes the await timeout(500) hack the test previously needed. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review: close ws server first, simplify whenIdle loop - Close the WebSocket server before awaiting whenIdle() so no further actions can be dispatched during the flush window. - Simplify SessionDataService.whenIdle(): per-DB whenIdle() already drains writes against existing DBs, so the outer loop only needs to re-pass when a NEW DB was opened during the await. Comment now matches the code. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Windows: trigger graceful shutdown via stdin close in test On Windows, child.kill() (SIGTERM) terminates the process unconditionally without invoking the SIGTERM so the in-flight setMetadata writehandler never reaches SQLite and the second phase sees no persisted config at all. Closing the child's stdin fires process.stdin.on('end', shutdown) on every platform, exercising the same graceful flush path. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentHost/common/sessionDataService.ts | 15 ++ .../agentHost/node/agentHostServerMain.ts | 30 ++- .../agentHost/node/sessionDataService.ts | 31 ++- .../agentHost/node/sessionDatabase.ts | 185 ++++++++++++------ .../test/common/sessionTestHelpers.ts | 4 + .../agentHost/test/node/agentService.test.ts | 2 + .../agentHost/test/node/copilotAgent.test.ts | 1 + .../protocol/sessionConfig.integrationTest.ts | 15 +- 8 files changed, 205 insertions(+), 78 deletions(-) diff --git a/src/vs/platform/agentHost/common/sessionDataService.ts b/src/vs/platform/agentHost/common/sessionDataService.ts index e398c8a13ee76..3f8ff83584a15 100644 --- a/src/vs/platform/agentHost/common/sessionDataService.ts +++ b/src/vs/platform/agentHost/common/sessionDataService.ts @@ -173,6 +173,13 @@ export interface ISessionDatabase extends IDisposable { */ remapTurnIds(mapping: ReadonlyMap): Promise; + /** + * Resolves once all in-flight write operations on this database have + * settled. Used by graceful shutdown to flush fire-and-forget writes + * before the process exits. + */ + whenIdle(): Promise; + /** * Close the database connection. After calling this method, the object is * considered disposed and all other methods will reject with an error. @@ -235,4 +242,12 @@ export interface ISessionDataService { * Called at startup; safe to call multiple times. */ cleanupOrphanedData(knownSessionIds: Set): Promise; + + /** + * Resolves once all in-flight write operations across every currently + * open per-session database have settled. Intended for graceful + * shutdown — fire-and-forget writes (e.g. metadata persistence) would + * otherwise be lost when the process exits. + */ + whenIdle(): Promise; } diff --git a/src/vs/platform/agentHost/node/agentHostServerMain.ts b/src/vs/platform/agentHost/node/agentHostServerMain.ts index 7f6112007237b..9b09da0f7ea26 100644 --- a/src/vs/platform/agentHost/node/agentHostServerMain.ts +++ b/src/vs/platform/agentHost/node/agentHostServerMain.ts @@ -16,6 +16,7 @@ globalThis._VSCODE_FILE_ROOT = fileURLToPath(new URL('../../../..', import.meta. import * as fs from 'fs'; import * as os from 'os'; import { DisposableStore } from '../../../base/common/lifecycle.js'; +import { raceTimeout } from '../../../base/common/async.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { localize } from '../../../nls.js'; @@ -253,12 +254,31 @@ async function main(): Promise { // Keep alive until stdin closes or signal process.stdin.resume(); - process.stdin.on('end', shutdown); - process.on('SIGTERM', shutdown); - process.on('SIGINT', shutdown); - - function shutdown(): void { + process.stdin.on('end', () => { void shutdown(); }); + process.on('SIGTERM', () => { void shutdown(); }); + process.on('SIGINT', () => { void shutdown(); }); + + let shuttingDown = false; + async function shutdown(): Promise { + if (shuttingDown) { + return; + } + shuttingDown = true; logService.info('[AgentHostServer] Shutting down...'); + // Close the WebSocket server first so no further actions can be + // dispatched while we wait for in-flight writes to flush — otherwise + // a late-arriving action could keep queuing DB writes and either + // undermine the flush or push us past the timeout. + wsServer.dispose(); + // Wait for in-flight persistence writes to flush to the per-session + // SQLite databases. Without this, a SIGTERM arriving while a + // `setMetadata` write (configValues, customTitle, isRead, isDone, + // diffs) is in flight can drop the latest value — see the + // "Session Config persistence across restarts" integration test. + // Capped so a stuck write cannot hang shutdown indefinitely. + await raceTimeout(sessionDataService.whenIdle(), 3000, () => { + logService.warn('[AgentHostServer] Timed out waiting for session database writes to flush; exiting anyway.'); + }); disposables.dispose(); loggerService?.dispose(); process.exit(0); diff --git a/src/vs/platform/agentHost/node/sessionDataService.ts b/src/vs/platform/agentHost/node/sessionDataService.ts index 0fa6c86a5caf9..96c17fc745dee 100644 --- a/src/vs/platform/agentHost/node/sessionDataService.ts +++ b/src/vs/platform/agentHost/node/sessionDataService.ts @@ -12,6 +12,14 @@ import { ISessionDatabase, ISessionDataService, SESSION_DB_FILENAME } from '../c import { SessionDatabase } from './sessionDatabase.js'; class SessionDatabaseCollection extends ReferenceCollection { + + /** + * The set of currently-open databases. Mirrors what's held by the + * underlying ref-counted map, but exposed so {@link SessionDataService.whenIdle} + * can iterate without reaching into private state. + */ + readonly liveDatabases = new Set(); + constructor( private readonly _getDbPath: (key: string) => string, private readonly _logService: ILogService, @@ -22,10 +30,13 @@ class SessionDatabaseCollection extends ReferenceCollection { protected createReferencedObject(key: string): ISessionDatabase { const dbPath = this._getDbPath(key); this._logService.trace(`[SessionDataService] Opening database: ${dbPath}`); - return new SessionDatabase(dbPath); + const db = new SessionDatabase(dbPath); + this.liveDatabases.add(db); + return db; } protected destroyReferencedObject(_key: string, object: ISessionDatabase): void { + this.liveDatabases.delete(object); object.dispose(); } } @@ -124,4 +135,22 @@ export class SessionDataService implements ISessionDataService { this._logService.warn('[SessionDataService] Failed to run orphan cleanup', err); } } + + async whenIdle(): Promise { + // Each `SessionDatabase.whenIdle()` already loops internally until + // that DB is quiescent, so the outer loop only needs to handle the + // case where a new DB was opened (and writes queued against it) + // while we were awaiting an earlier pass. + while (true) { + const dbs = [...this._databases.liveDatabases]; + if (dbs.length === 0) { + return; + } + await Promise.all(dbs.map(db => db.whenIdle())); + const newOnes = [...this._databases.liveDatabases].filter(db => !dbs.includes(db)); + if (newOnes.length === 0) { + return; + } + } + } } diff --git a/src/vs/platform/agentHost/node/sessionDatabase.ts b/src/vs/platform/agentHost/node/sessionDatabase.ts index b1618942fc4d4..12e2a63b769a1 100644 --- a/src/vs/platform/agentHost/node/sessionDatabase.ts +++ b/src/vs/platform/agentHost/node/sessionDatabase.ts @@ -195,6 +195,17 @@ export class SessionDatabase implements ISessionDatabase { protected _closed: Promise | true | undefined; private readonly _fileEditSequencer = new SequencerByKey(); + /** + * In-flight write operations. Tracked so {@link whenIdle} can await them + * before the process exits — without this, a `SIGTERM` arriving between + * a fire-and-forget mutating call (e.g. `setMetadata`) being invoked and + * its underlying SQLite query completing would silently drop the write. + * Every public mutating method routes its returned promise through + * {@link _track}; reads (`getMetadata`, `getFileEdits`, ...) skip + * tracking since shutdown does not need to wait for them. + */ + private readonly _pendingWrites = new Set>(); + constructor( private readonly _path: string, private readonly _migrations: readonly ISessionDatabaseMigration[] = sessionDatabaseMigrations, @@ -251,23 +262,29 @@ export class SessionDatabase implements ISessionDatabase { // ---- Turns ---------------------------------------------------------- - async createTurn(turnId: string): Promise { - const db = await this._ensureDb(); - await dbRun(db, 'INSERT OR IGNORE INTO turns (id) VALUES (?)', [turnId]); + createTurn(turnId: string): Promise { + return this._track(async () => { + const db = await this._ensureDb(); + await dbRun(db, 'INSERT OR IGNORE INTO turns (id) VALUES (?)', [turnId]); + }); } - async deleteTurn(turnId: string): Promise { - const db = await this._ensureDb(); - await dbRun(db, 'DELETE FROM turns WHERE id = ?', [turnId]); + deleteTurn(turnId: string): Promise { + return this._track(async () => { + const db = await this._ensureDb(); + await dbRun(db, 'DELETE FROM turns WHERE id = ?', [turnId]); + }); } - async setTurnEventId(turnId: string, eventId: string): Promise { - const db = await this._ensureDb(); - await dbRun(db, 'INSERT OR IGNORE INTO turns (id) VALUES (?)', [turnId]); - // Only set the event ID if not already set — steering messages - // trigger additional user.message events within the same turn, - // and we must preserve the first (boundary) event ID. - await dbRun(db, 'UPDATE turns SET event_id = ? WHERE id = ? AND event_id IS NULL', [eventId, turnId]); + setTurnEventId(turnId: string, eventId: string): Promise { + return this._track(async () => { + const db = await this._ensureDb(); + await dbRun(db, 'INSERT OR IGNORE INTO turns (id) VALUES (?)', [turnId]); + // Only set the event ID if not already set — steering messages + // trigger additional user.message events within the same turn, + // and we must preserve the first (boundary) event ID. + await dbRun(db, 'UPDATE turns SET event_id = ? WHERE id = ? AND event_id IS NULL', [eventId, turnId]); + }); } async getTurnEventId(turnId: string): Promise { @@ -292,36 +309,42 @@ export class SessionDatabase implements ISessionDatabase { return row?.event_id as string | undefined ?? undefined; } - async truncateFromTurn(turnId: string): Promise { - const db = await this._ensureDb(); - // Delete the target turn and all turns inserted after it (by rowid order). - // File edits cascade-delete via the foreign key constraint. - await dbRun(db, - `DELETE FROM turns WHERE rowid >= (SELECT rowid FROM turns WHERE id = ?)`, - [turnId], - ); + truncateFromTurn(turnId: string): Promise { + return this._track(async () => { + const db = await this._ensureDb(); + // Delete the target turn and all turns inserted after it (by rowid order). + // File edits cascade-delete via the foreign key constraint. + await dbRun(db, + `DELETE FROM turns WHERE rowid >= (SELECT rowid FROM turns WHERE id = ?)`, + [turnId], + ); + }); } - async deleteTurnsAfter(turnId: string): Promise { - const db = await this._ensureDb(); - // Delete all turns inserted after the given turn (by rowid order), - // keeping the given turn itself. - // File edits cascade-delete via the foreign key constraint. - await dbRun(db, - `DELETE FROM turns WHERE rowid > (SELECT rowid FROM turns WHERE id = ?)`, - [turnId], - ); + deleteTurnsAfter(turnId: string): Promise { + return this._track(async () => { + const db = await this._ensureDb(); + // Delete all turns inserted after the given turn (by rowid order), + // keeping the given turn itself. + // File edits cascade-delete via the foreign key constraint. + await dbRun(db, + `DELETE FROM turns WHERE rowid > (SELECT rowid FROM turns WHERE id = ?)`, + [turnId], + ); + }); } - async deleteAllTurns(): Promise { - const db = await this._ensureDb(); - await dbExec(db, 'DELETE FROM turns'); + deleteAllTurns(): Promise { + return this._track(async () => { + const db = await this._ensureDb(); + await dbExec(db, 'DELETE FROM turns'); + }); } // ---- File edits ----------------------------------------------------- - async storeFileEdit(edit: IFileEditRecord & IFileEditContent): Promise { - return this._fileEditSequencer.queue(edit.filePath, async () => { + storeFileEdit(edit: IFileEditRecord & IFileEditContent): Promise { + return this._track(() => this._fileEditSequencer.queue(edit.filePath, async () => { const db = await this._ensureDb(); // Ensure the turn exists — lazily insert since the turn record // may not have been created by an explicit createTurn() call. @@ -343,7 +366,7 @@ export class SessionDatabase implements ISessionDatabase { edit.removedLines ?? null, ], ); - }); + })); } async getFileEdits(toolCallIds: string[]): Promise { @@ -459,42 +482,74 @@ export class SessionDatabase implements ISessionDatabase { return result; } - async setMetadata(key: string, value: string): Promise { - const db = await this._ensureDb(); - await dbRun(db, 'INSERT OR REPLACE INTO session_metadata (key, value) VALUES (?, ?)', [key, value]); + setMetadata(key: string, value: string): Promise { + return this._track(async () => { + const db = await this._ensureDb(); + await dbRun(db, 'INSERT OR REPLACE INTO session_metadata (key, value) VALUES (?, ?)', [key, value]); + }); } - async remapTurnIds(mapping: ReadonlyMap): Promise { - const db = await this._ensureDb(); - // Defer FK checks to commit time so we can update turns.id and - // file_edits.turn_id in any order without mid-statement violations. - // This pragma auto-resets after the transaction ends. - await dbExec(db, 'PRAGMA defer_foreign_keys = ON'); - await dbExec(db, 'BEGIN TRANSACTION'); - try { - // Delete turns not present in the mapping (e.g. turns beyond - // the fork point). File edits cascade-delete via FK. - const oldIds = [...mapping.keys()]; - if (oldIds.length > 0) { - const placeholders = oldIds.map(() => '?').join(','); - await dbRun(db, - `DELETE FROM turns WHERE id NOT IN (${placeholders})`, - oldIds, - ); - } + remapTurnIds(mapping: ReadonlyMap): Promise { + return this._track(async () => { + const db = await this._ensureDb(); + // Defer FK checks to commit time so we can update turns.id and + // file_edits.turn_id in any order without mid-statement violations. + // This pragma auto-resets after the transaction ends. + await dbExec(db, 'PRAGMA defer_foreign_keys = ON'); + await dbExec(db, 'BEGIN TRANSACTION'); + try { + // Delete turns not present in the mapping (e.g. turns beyond + // the fork point). File edits cascade-delete via FK. + const oldIds = [...mapping.keys()]; + if (oldIds.length > 0) { + const placeholders = oldIds.map(() => '?').join(','); + await dbRun(db, + `DELETE FROM turns WHERE id NOT IN (${placeholders})`, + oldIds, + ); + } - // Remap the remaining turn IDs to their new values - for (const [oldId, newId] of mapping) { - await dbRun(db, 'UPDATE turns SET id = ? WHERE id = ?', [newId, oldId]); - await dbRun(db, 'UPDATE file_edits SET turn_id = ? WHERE turn_id = ?', [newId, oldId]); + // Remap the remaining turn IDs to their new values + for (const [oldId, newId] of mapping) { + await dbRun(db, 'UPDATE turns SET id = ? WHERE id = ?', [newId, oldId]); + await dbRun(db, 'UPDATE file_edits SET turn_id = ? WHERE turn_id = ?', [newId, oldId]); + } + await dbExec(db, 'COMMIT'); + } catch (err) { + await dbExec(db, 'ROLLBACK'); + throw err; } - await dbExec(db, 'COMMIT'); - } catch (err) { - await dbExec(db, 'ROLLBACK'); - throw err; + }); + } + + /** + * Resolves once all currently in-flight write operations have settled. + * Used by graceful shutdown to flush pending fire-and-forget writes + * before the process exits. Should be called from a path where no + * further writes are expected; loops until idle to also drain any + * writes that get queued while we're awaiting. + */ + async whenIdle(): Promise { + while (this._pendingWrites.size > 0) { + await Promise.allSettled([...this._pendingWrites]); } } + /** + * Wrap a mutating operation's promise so {@link whenIdle} can await it. + * Invoke at the **outermost** layer of every public mutating method so + * that any internal awaits (notably `_ensureDb()`) are covered too — + * tracking only the leaf `dbRun`/`dbExec` would miss the window + * between the method being called and the query actually being queued. + */ + private _track(fn: () => Promise): Promise { + const p = fn(); + this._pendingWrites.add(p); + const untrack = () => { this._pendingWrites.delete(p); }; + p.then(untrack, untrack); + return p; + } + async close() { await (this._closed ??= this._dbPromise?.then(db => db.close()).catch(() => { }) || true); } diff --git a/src/vs/platform/agentHost/test/common/sessionTestHelpers.ts b/src/vs/platform/agentHost/test/common/sessionTestHelpers.ts index 7b9089a7fa09e..51887b7f65064 100644 --- a/src/vs/platform/agentHost/test/common/sessionTestHelpers.ts +++ b/src/vs/platform/agentHost/test/common/sessionTestHelpers.ts @@ -90,6 +90,8 @@ export class TestSessionDatabase implements ISessionDatabase { async remapTurnIds(_mapping: ReadonlyMap): Promise { } + async whenIdle(): Promise { } + private _toEditRecords(edits: (IFileEditRecord & IFileEditContent)[]): IFileEditRecord[] { return edits.map(({ beforeContent: _, afterContent: _2, ...metadata }) => metadata); } @@ -130,6 +132,7 @@ export function createSessionDataService(database: ISessionDatabase = new TestSe tryOpenDatabase: async () => createReference(database), deleteSessionData: async () => { }, cleanupOrphanedData: async () => { }, + whenIdle: async () => { }, }; } @@ -142,6 +145,7 @@ export function createNullSessionDataService(): ISessionDataService { tryOpenDatabase: async () => undefined, deleteSessionData: async () => { }, cleanupOrphanedData: async () => { }, + whenIdle: async () => { }, }; } diff --git a/src/vs/platform/agentHost/test/node/agentService.test.ts b/src/vs/platform/agentHost/test/node/agentService.test.ts index 83d962767abc5..6d4c6d43689a6 100644 --- a/src/vs/platform/agentHost/test/node/agentService.test.ts +++ b/src/vs/platform/agentHost/test/node/agentService.test.ts @@ -65,6 +65,7 @@ suite('AgentService (node dispatcher)', () => { tryOpenDatabase: async () => undefined, deleteSessionData: async () => { }, cleanupOrphanedData: async () => { }, + whenIdle: async () => { }, }; fileService = disposables.add(new FileService(new NullLogService())); disposables.add(fileService.registerProvider(Schemas.inMemory, disposables.add(new InMemoryFileSystemProvider()))); @@ -195,6 +196,7 @@ suite('AgentService (node dispatcher)', () => { }), deleteSessionData: async () => { }, cleanupOrphanedData: async () => { }, + whenIdle: async () => { }, }; // Create a mock that returns a session with that ID diff --git a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts index 909524517da33..4c9a9a1e1aad6 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgent.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgent.test.ts @@ -103,6 +103,7 @@ class TestSessionDataService extends Disposable implements ISessionDataService { deleteSessionData(): Promise { return Promise.resolve(); } cleanupOrphanedData(): Promise { return Promise.resolve(); } + whenIdle(): Promise { return Promise.resolve(); } } class TestCopilotClient implements ICopilotClient { diff --git a/src/vs/platform/agentHost/test/node/protocol/sessionConfig.integrationTest.ts b/src/vs/platform/agentHost/test/node/protocol/sessionConfig.integrationTest.ts index 5025cccfd3b7d..66fc211103fe1 100644 --- a/src/vs/platform/agentHost/test/node/protocol/sessionConfig.integrationTest.ts +++ b/src/vs/platform/agentHost/test/node/protocol/sessionConfig.integrationTest.ts @@ -6,7 +6,6 @@ import assert from 'assert'; import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; -import { timeout } from '../../../../../base/common/async.js'; import { URI } from '../../../../../base/common/uri.js'; import type { IResolveSessionConfigResult, ISessionConfigCompletionsResult, ISubscribeResult } from '../../../common/state/protocol/commands.js'; import { ActionType, type ISessionAddedNotification } from '../../../common/state/sessionActions.js'; @@ -202,13 +201,15 @@ suite('Protocol WebSocket - Session Config persistence across restarts', functio const configChanged = await client1.waitForNotification(n => isActionNotification(n, ActionType.SessionConfigChanged)); assert.strictEqual(getActionEnvelope(configChanged).action.type, ActionType.SessionConfigChanged); - // `_persistConfigValues` is fire-and-forget; give the SQLite write - // a moment to flush before tearing down the server. - await timeout(500); - client1.close(); } finally { - server1.process.kill(); + // Trigger graceful shutdown by closing stdin rather than sending + // SIGTERM — on Windows, `child.kill()` (SIGTERM) unconditionally + // terminates the process without invoking the shutdown handler, + // so in-flight `setMetadata` writes never reach SQLite. Closing + // stdin fires `process.stdin.on('end', shutdown)` in the server + // on every platform. + server1.process.stdin!.end(); await new Promise(resolve => server1.process.once('exit', () => resolve())); } @@ -239,7 +240,7 @@ suite('Protocol WebSocket - Session Config persistence across restarts', functio client2.close(); } finally { - server2.process.kill(); + server2.process.stdin!.end(); await new Promise(resolve => server2.process.once('exit', () => resolve())); } }); From f7bd15603cd3ad410b6d87d02f72cd3cf5e85f62 Mon Sep 17 00:00:00 2001 From: Hawk Ticehurst <39639992+hawkticehurst@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:20:42 -0400 Subject: [PATCH 14/32] chat: tighten generated title prompt (#311459) Refine the title-generation prompt to prefer sentence case and more compact 3-6 word summaries for chat sessions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/extension/prompts/node/panel/title.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/extensions/copilot/src/extension/prompts/node/panel/title.tsx b/extensions/copilot/src/extension/prompts/node/panel/title.tsx index 1af4bad430b62..5bd6434b11138 100644 --- a/extensions/copilot/src/extension/prompts/node/panel/title.tsx +++ b/extensions/copilot/src/extension/prompts/node/panel/title.tsx @@ -16,16 +16,21 @@ export class TitlePrompt extends PromptElement { return ( <> - You are an expert in crafting pithy titles for chatbot conversations. You are presented with a chat request, and you reply with a brief title that captures the main topic of that request.
+ You are an expert in crafting ultra-compact titles for chatbot conversations. You are presented with a chat request, and you reply with only a brief title that captures the main topic of that request.
- The title should not be wrapped in quotes. It should be about 8 words or fewer.
+ Write the title in sentence case, not title case. Preserve product names, abbreviations, code symbols, and proper nouns.
+ Aim for 3-6 words. Prefer the shortest accurate title.
+ Drop articles like "a", "an", and "the" unless needed for clarity.
+ Drop filler and generic framing like "help with", "question about", "request for", or "issue with".
+ Prefer short, concrete synonyms and omit unnecessary words.
+ Do not wrap the title in quotes or add trailing punctuation.
Here are some examples of good titles:
- Git rebase question
- - Installing Python packages
- - Location of LinkedList implementation in codebase
- - Adding a tree view to a VS Code extension
- - React useState hook usage + - Install Python packages
+ - LinkedList implementation location
+ - Add VS Code tree view
+ - React useState usage
Please write a brief title for the following request:
From 38b3e89910a877e9bd0beb1ff9131194b08a07eb Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:23:14 -0700 Subject: [PATCH 15/32] Remove old `MSGestureEvent` conditional I think this only existed in ie/edge? --- src/vs/base/browser/mouseEvent.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/vs/base/browser/mouseEvent.ts b/src/vs/base/browser/mouseEvent.ts index 2f8c99ff327d5..2873a56e1e40c 100644 --- a/src/vs/base/browser/mouseEvent.ts +++ b/src/vs/base/browser/mouseEvent.ts @@ -67,14 +67,8 @@ export class StandardMouseEvent implements IMouseEvent { this.altKey = e.altKey; this.metaKey = e.metaKey; - if (typeof e.pageX === 'number') { - this.posx = e.pageX; - this.posy = e.pageY; - } else { - // Probably hit by MSGestureEvent - this.posx = e.clientX + this.target.ownerDocument.body.scrollLeft + this.target.ownerDocument.documentElement.scrollLeft; - this.posy = e.clientY + this.target.ownerDocument.body.scrollTop + this.target.ownerDocument.documentElement.scrollTop; - } + this.posx = e.pageX; + this.posy = e.pageY; // Find the position of the iframe this code is executing in relative to the iframe where the event was captured. const iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(targetWindow, e.view); From fd15d12cead93690a7271dbd51f808fe46b92974 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:30:57 -0700 Subject: [PATCH 16/32] Remove redundant back-to-installed links from MCP/Plugin widgets (#311456) Leverage the global back arrow button (left of search) to handle browse-mode exit instead of showing separate 'Back to installed servers/plugins' links inside each widget. - Parameterize createBackArrowButton with optional click handler - For MCP/Plugin sections, exit browse mode on click when active - Remove backLink DOM element, listeners, and layout accounting - Remove .mcp-back-link CSS styles - Add public isInBrowseMode()/exitBrowseMode() to both widgets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../aiCustomizationManagementEditor.ts | 28 ++++++++++--- .../browser/aiCustomization/mcpListWidget.ts | 42 ++++++++----------- .../media/aiCustomizationManagement.css | 24 ----------- .../aiCustomization/pluginListWidget.ts | 41 ++++++++---------- 4 files changed, 56 insertions(+), 79 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index a016b959f84f2..87d1743b42f76 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -780,7 +780,7 @@ export class AICustomizationManagementEditor extends EditorPane { this.welcomePage.rebuildCards(new Set(this.sections.map(s => s.id))); } - private createBackArrowButton(): HTMLButtonElement { + private createBackArrowButton(onClick?: () => void): HTMLButtonElement { const button = $('button.section-back-arrow-button') as HTMLButtonElement; button.type = 'button'; button.setAttribute('aria-label', localize('backToOverview', "Back to overview")); @@ -789,13 +789,17 @@ export class AICustomizationManagementEditor extends EditorPane { icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.arrowLeft)); icon.setAttribute('aria-hidden', 'true'); this.editorDisposables.add(DOM.addDisposableListener(button, 'click', () => { - this.showWelcomePage(); + if (onClick) { + onClick(); + } else { + this.showWelcomePage(); + } })); return button; } - private injectBackArrowIntoSearchRow(widget: { prependToSearchRow(el: HTMLElement): void }): void { - widget.prependToSearchRow(this.createBackArrowButton()); + private injectBackArrowIntoSearchRow(widget: { prependToSearchRow(el: HTMLElement): void }, onClick?: () => void): void { + widget.prependToSearchRow(this.createBackArrowButton(onClick)); } private createContent(): void { @@ -859,7 +863,13 @@ export class AICustomizationManagementEditor extends EditorPane { this.mcpContentContainer = DOM.append(contentInner, $('.mcp-content-container')); this.mcpListWidget = this.editorDisposables.add(this.instantiationService.createInstance(McpListWidget)); this.mcpContentContainer.appendChild(this.mcpListWidget.element); - this.injectBackArrowIntoSearchRow(this.mcpListWidget); + this.injectBackArrowIntoSearchRow(this.mcpListWidget, () => { + if (this.mcpListWidget!.isInBrowseMode()) { + this.mcpListWidget!.exitBrowseMode(); + } else { + this.showWelcomePage(); + } + }); // Embedded MCP server detail view this.mcpDetailContainer = DOM.append(contentInner, $('.mcp-detail-container')); @@ -879,7 +889,13 @@ export class AICustomizationManagementEditor extends EditorPane { this.pluginContentContainer = DOM.append(contentInner, $('.plugin-content-container')); this.pluginListWidget = this.editorDisposables.add(this.instantiationService.createInstance(PluginListWidget)); this.pluginContentContainer.appendChild(this.pluginListWidget.element); - this.injectBackArrowIntoSearchRow(this.pluginListWidget); + this.injectBackArrowIntoSearchRow(this.pluginListWidget, () => { + if (this.pluginListWidget!.isInBrowseMode()) { + this.pluginListWidget!.exitBrowseMode(); + } else { + this.showWelcomePage(); + } + }); // Embedded plugin detail view this.pluginDetailContainer = DOM.append(contentInner, $('.plugin-detail-container')); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts index 33a229e24a21b..10d6b3a41e9c4 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/mcpListWidget.ts @@ -379,7 +379,6 @@ export class McpListWidget extends Disposable { private readonly disabledLinkListener = this._register(new MutableDisposable()); private browseButton!: Button; private addButton!: Button; - private backLink!: HTMLElement; private filteredServers: IWorkbenchMcpServer[] = []; private filteredBuiltinCount = 0; @@ -470,26 +469,6 @@ export class McpListWidget extends Disposable { this.commandService.executeCommand(McpCommandIds.AddConfiguration); })); - // Back to installed link (shown only in browse mode) - this.backLink = DOM.append(this.element, $('.mcp-back-link')); - this.backLink.setAttribute('role', 'button'); - this.backLink.tabIndex = 0; - this.backLink.setAttribute('aria-label', localize('backToInstalledAriaLabel', "Back to installed servers")); - const backIcon = DOM.append(this.backLink, $('span')); - backIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.arrowLeft)); - const backText = DOM.append(this.backLink, $('span')); - backText.textContent = localize('backToInstalled', "Back to installed servers"); - this._register(DOM.addDisposableListener(this.backLink, 'click', () => { - this.toggleBrowseMode(false); - })); - this._register(DOM.addDisposableListener(this.backLink, 'keydown', (e: KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - this.toggleBrowseMode(false); - } - })); - this.backLink.style.display = 'none'; - // Empty state this.emptyContainer = DOM.append(this.element, $('.mcp-empty-state')); const emptyHeader = DOM.append(this.emptyContainer, $('.empty-state-header')); @@ -651,7 +630,6 @@ export class McpListWidget extends Disposable { this.searchQuery = ''; // Update UI for browse vs installed mode - this.backLink.style.display = browse ? '' : 'none'; this.addButton.element.style.display = browse ? 'none' : ''; this.browseButton.element.parentElement!.style.display = browse ? 'none' : ''; @@ -942,7 +920,6 @@ export class McpListWidget extends Disposable { this.filterServers(); } - /** /** * Prepends an element to the search row (left of the search input). */ @@ -950,6 +927,22 @@ export class McpListWidget extends Disposable { this.searchAndButtonContainer.insertBefore(element, this.searchAndButtonContainer.firstChild); } + /** + * Whether the widget is currently in marketplace browse mode. + */ + isInBrowseMode(): boolean { + return this.browseMode; + } + + /** + * Exits marketplace browse mode and returns to the installed servers list. + */ + exitBrowseMode(): void { + if (this.browseMode) { + this.toggleBrowseMode(false); + } + } + /** * Layouts the widget. */ @@ -969,8 +962,7 @@ export class McpListWidget extends Disposable { return; } const footerHeight = this.sectionHeader.offsetHeight; - const backLinkHeight = this.browseMode ? this.backLink.offsetHeight : 0; - const listHeight = Math.max(0, height - searchBarHeight - footerHeight - backLinkHeight); + const listHeight = Math.max(0, height - searchBarHeight - footerHeight); this.listContainer.style.height = `${listHeight}px`; this.list.layout(listHeight, width); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index f878d5f25d377..b1943a20d8661 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -1008,30 +1008,6 @@ outline-offset: -1px; } -/* Back to installed link */ -.mcp-list-widget .mcp-back-link { - display: flex; - align-items: center; - gap: 4px; - padding: 4px 6px; - cursor: pointer; - font-size: 12px; - color: var(--vscode-descriptionForeground); - flex-shrink: 0; - border-radius: 4px; - margin: 4px 0; -} - -.mcp-list-widget .mcp-back-link:hover { - background-color: var(--vscode-toolbar-hoverBackground); - color: var(--vscode-foreground); -} - -.mcp-list-widget .mcp-back-link:focus-visible { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; -} - /* Gallery item specifics */ .mcp-gallery-item .mcp-gallery-name-row { display: flex; diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts index 25b106d85a0a5..1825b7ea72de8 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts @@ -345,7 +345,6 @@ export class PluginListWidget extends Disposable { private disabledMessage!: HTMLElement; private readonly disabledLinkListener = this._register(new MutableDisposable()); private browseButton!: Button; - private backLink!: HTMLElement; private installedItems: IInstalledPluginItem[] = []; private displayEntries: IPluginListEntry[] = []; @@ -438,26 +437,6 @@ export class PluginListWidget extends Disposable { this.commandService.executeCommand('workbench.action.chat.createPlugin'); })); - // Back to installed link (shown only in browse mode) - this.backLink = DOM.append(this.element, $('.mcp-back-link')); - this.backLink.setAttribute('role', 'button'); - this.backLink.tabIndex = 0; - this.backLink.setAttribute('aria-label', localize('backToInstalledPluginsAriaLabel', "Back to installed plugins")); - const backIcon = DOM.append(this.backLink, $('span')); - backIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.arrowLeft)); - const backText = DOM.append(this.backLink, $('span')); - backText.textContent = localize('backToInstalledPlugins', "Back to installed plugins"); - this._register(DOM.addDisposableListener(this.backLink, 'click', () => { - this.toggleBrowseMode(false); - })); - this._register(DOM.addDisposableListener(this.backLink, 'keydown', (e: KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - this.toggleBrowseMode(false); - } - })); - this.backLink.style.display = 'none'; - // Empty state this.emptyContainer = DOM.append(this.element, $('.mcp-empty-state')); const emptyHeader = DOM.append(this.emptyContainer, $('.empty-state-header')); @@ -646,7 +625,6 @@ export class PluginListWidget extends Disposable { this.searchInput.value = ''; this.searchQuery = ''; - this.backLink.style.display = browse ? '' : 'none'; this.browseButton.element.parentElement!.style.display = browse ? 'none' : ''; this.searchInput.setPlaceHolder(browse @@ -845,6 +823,22 @@ export class PluginListWidget extends Disposable { this.searchAndButtonContainer.insertBefore(element, this.searchAndButtonContainer.firstChild); } + /** + * Whether the widget is currently in marketplace browse mode. + */ + isInBrowseMode(): boolean { + return this.browseMode; + } + + /** + * Exits marketplace browse mode and returns to the installed plugins list. + */ + exitBrowseMode(): void { + if (this.browseMode) { + this.toggleBrowseMode(false); + } + } + layout(height: number, width: number): void { this.lastHeight = height; this.lastWidth = width; @@ -861,8 +855,7 @@ export class PluginListWidget extends Disposable { return; } const footerHeight = this.sectionHeader.offsetHeight; - const backLinkHeight = this.browseMode ? this.backLink.offsetHeight : 0; - const listHeight = Math.max(0, height - searchBarHeight - footerHeight - backLinkHeight); + const listHeight = Math.max(0, height - searchBarHeight - footerHeight); this.listContainer.style.height = `${listHeight}px`; this.list.layout(listHeight, width); From 751ee75ed06d919144155030d279e11337ffd563 Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" <122617954+vs-code-engineering[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:31:32 -0700 Subject: [PATCH 17/32] [cherry-pick] Add expand action for question part (#311464) [cherry-pick] Add expand action for question part Co-authored-by: vs-code-engineering[bot] --- .../chatContentParts/chatQuestionCarouselPart.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index 588704e3c3676..305eda8bcc98a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -687,13 +687,14 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const title = dom.$('.chat-question-title'); const messageContent = this.getQuestionText(questionText); title.setAttribute('aria-label', messageContent); - questionRenderStore.add(this._hoverService.setupDelayedHover(title, { content: messageContent })); - const titleText = question.required - ? new MarkdownString(`${isMarkdownString(questionText) ? questionText.value : questionText} *`) - : (isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText)); - const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(titleText)); - title.appendChild(renderedTitle.element); + const rawValue = isMarkdownString(questionText) ? questionText.value : questionText; + const suffixed = question.required ? `${rawValue} *` : rawValue; + const md = isMarkdownString(questionText) + ? MarkdownString.lift({ ...questionText, value: suffixed }) + : new MarkdownString(suffixed); + const rendered = questionRenderStore.add(this._markdownRendererService.render(md)); + title.appendChild(rendered.element); titleRow.appendChild(title); } From 8974e74593602890cd85b4bbcddeeee0c232a698 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:32:26 -0700 Subject: [PATCH 18/32] chore: bump SDK (#311451) * chore: bump SDK * Lower version due to breaking changes --- extensions/copilot/chat-lib/package-lock.json | 512 +++++++++++++++++- extensions/copilot/package-lock.json | 18 +- extensions/copilot/package.json | 2 +- 3 files changed, 504 insertions(+), 28 deletions(-) diff --git a/extensions/copilot/chat-lib/package-lock.json b/extensions/copilot/chat-lib/package-lock.json index cc1de521b7e13..7e8ac762088c6 100644 --- a/extensions/copilot/chat-lib/package-lock.json +++ b/extensions/copilot/chat-lib/package-lock.json @@ -30,7 +30,6 @@ "@anthropic-ai/sdk": "^0.82.0", "@octokit/types": "^14.1.0", "@types/node": "^22.16.3", - "@types/vscode": "^1.109.0", "copyfiles": "^2.4.1", "dotenv": "^17.2.0", "npm-run-all": "^4.1.5", @@ -1221,13 +1220,6 @@ "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.3.tgz", "integrity": "sha512-F/IjUGnV6pIN7R4ZV4npHJVoNtaLZWvb+2/9gctxjb99wkpI7Ozg8VPogwDiTRyjLwZXAYxjvdg1KS8LTHKdDA==" }, - "node_modules/@types/vscode": { - "version": "1.109.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", - "integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -1566,9 +1558,9 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -3234,9 +3226,9 @@ } }, "node_modules/minimatch/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4830,13 +4822,13 @@ } }, "node_modules/vite": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", - "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -4927,6 +4919,490 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/vitest": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", diff --git a/extensions/copilot/package-lock.json b/extensions/copilot/package-lock.json index 42ffd2a42a190..394b8dbcd09c1 100644 --- a/extensions/copilot/package-lock.json +++ b/extensions/copilot/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.2.98", + "@anthropic-ai/claude-agent-sdk": "0.2.112", "@anthropic-ai/sdk": "^0.82.0", "@github/blackbird-external-ingest-utils": "^0.3.0", "@github/copilot": "^1.0.28", @@ -180,13 +180,13 @@ } }, "node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.2.98", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.98.tgz", - "integrity": "sha512-pWUx+xY21rKy5wvX0eBZja7p8J5ykOYaHsykvdj9nkTbAVXmP1WusI1mP6jbBByJ8uBJeBc4beAPSZIFcdIpTA==", + "version": "0.2.112", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.112.tgz", + "integrity": "sha512-vMFoiDKlOive8p3tphpV1gQaaytOipwGJ+uw9mvvaLQUODSC2+fCdRDAY25i2Tsv+lOtxzXBKctmaDuWqZY7ig==", "license": "SEE LICENSE IN README.md", "dependencies": { - "@anthropic-ai/sdk": "^0.80.0", - "@modelcontextprotocol/sdk": "^1.27.1" + "@anthropic-ai/sdk": "^0.81.0", + "@modelcontextprotocol/sdk": "^1.29.0" }, "engines": { "node": ">=18.0.0" @@ -207,9 +207,9 @@ } }, "node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/sdk": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.80.0.tgz", - "integrity": "sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g==", + "version": "0.81.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.81.0.tgz", + "integrity": "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw==", "license": "MIT", "dependencies": { "json-schema-to-ts": "^3.1.1" diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index d54d644260abc..9028db141f0ff 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -6408,7 +6408,7 @@ "zod": "3.25.76" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.2.98", + "@anthropic-ai/claude-agent-sdk": "0.2.112", "@anthropic-ai/sdk": "^0.82.0", "@github/blackbird-external-ingest-utils": "^0.3.0", "@github/copilot": "^1.0.28", From 442a0515b8a527ff30fac7cde5ef2f1a834174b7 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:41:49 -0700 Subject: [PATCH 19/32] Remove empty dispose overrides --- src/vs/base/browser/ui/findinput/replaceInput.ts | 4 ---- src/vs/editor/browser/controller/mouseHandler.ts | 4 ---- src/vs/editor/browser/services/editorWorkerService.ts | 3 --- .../browser/viewParts/blockDecorations/blockDecorations.ts | 3 --- .../browser/viewParts/editorScrollbar/editorScrollbar.ts | 3 --- src/vs/editor/browser/viewParts/margin/margin.ts | 3 --- src/vs/editor/browser/viewParts/rulers/rulers.ts | 3 --- .../browser/viewParts/scrollDecoration/scrollDecoration.ts | 3 --- src/vs/editor/common/viewLayout/viewLayout.ts | 3 --- src/vs/editor/standalone/browser/standaloneCodeEditor.ts | 6 ------ .../platform/keybinding/common/abstractKeybindingService.ts | 3 --- .../codeEditor/electron-browser/selectionClipboard.ts | 3 --- .../workbench/services/dialogs/browser/simpleFileDialog.ts | 3 --- 13 files changed, 44 deletions(-) diff --git a/src/vs/base/browser/ui/findinput/replaceInput.ts b/src/vs/base/browser/ui/findinput/replaceInput.ts index 10c5b47b6530e..a9ae899d424b5 100644 --- a/src/vs/base/browser/ui/findinput/replaceInput.ts +++ b/src/vs/base/browser/ui/findinput/replaceInput.ts @@ -280,8 +280,4 @@ export class ReplaceInput extends Widget { this.inputBox.paddingRight = this.cachedOptionsWidth; this.domNode.style.width = newWidth + 'px'; } - - public override dispose(): void { - super.dispose(); - } } diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 4ad4ca9d817e5..52291332b2ad2 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -403,10 +403,6 @@ class MouseDownOperation extends Disposable { this._lastMouseEvent = null; } - public override dispose(): void { - super.dispose(); - } - public isActive(): boolean { return this._isActive; } diff --git a/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index d5c878af3b386..ec09cf29853e2 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/src/vs/editor/browser/services/editorWorkerService.ts @@ -97,9 +97,6 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ this._register(languageFeaturesService.completionProvider.register('*', new WordBasedCompletionItemProvider(this._workerManager, configurationService, this._modelService, this._languageConfigurationService, this._logService, languageFeaturesService))); } - public override dispose(): void { - super.dispose(); - } public canComputeUnicodeHighlights(uri: URI): boolean { return canSyncModel(this._modelService, uri); diff --git a/src/vs/editor/browser/viewParts/blockDecorations/blockDecorations.ts b/src/vs/editor/browser/viewParts/blockDecorations/blockDecorations.ts index 70fe366c06c9f..7cd7ea0be4cd8 100644 --- a/src/vs/editor/browser/viewParts/blockDecorations/blockDecorations.ts +++ b/src/vs/editor/browser/viewParts/blockDecorations/blockDecorations.ts @@ -51,9 +51,6 @@ export class BlockDecorations extends ViewPart { return didChange; } - public override dispose(): void { - super.dispose(); - } // --- begin event handlers diff --git a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts index aa633a1cadfa6..816dc313885dc 100644 --- a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts +++ b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts @@ -105,9 +105,6 @@ export class EditorScrollbar extends ViewPart { this._register(dom.addDisposableListener(this.scrollbarDomNode.domNode, 'scroll', (e: Event) => onBrowserDesperateReveal(this.scrollbarDomNode.domNode, true, false))); } - public override dispose(): void { - super.dispose(); - } private _setLayout(): void { const options = this._context.configuration.options; diff --git a/src/vs/editor/browser/viewParts/margin/margin.ts b/src/vs/editor/browser/viewParts/margin/margin.ts index cd4660c834d13..2edbe484f7077 100644 --- a/src/vs/editor/browser/viewParts/margin/margin.ts +++ b/src/vs/editor/browser/viewParts/margin/margin.ts @@ -50,9 +50,6 @@ export class Margin extends ViewPart { this._domNode.appendChild(this._glyphMarginBackgroundDomNode); } - public override dispose(): void { - super.dispose(); - } public getDomNode(): FastDomNode { return this._domNode; diff --git a/src/vs/editor/browser/viewParts/rulers/rulers.ts b/src/vs/editor/browser/viewParts/rulers/rulers.ts index ec1a5042e9168..cb67b702704eb 100644 --- a/src/vs/editor/browser/viewParts/rulers/rulers.ts +++ b/src/vs/editor/browser/viewParts/rulers/rulers.ts @@ -34,9 +34,6 @@ export class Rulers extends ViewPart { this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; } - public override dispose(): void { - super.dispose(); - } // --- begin event handlers diff --git a/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts b/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts index dc5dc3007091b..a703acd26c2c0 100644 --- a/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts +++ b/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts @@ -35,9 +35,6 @@ export class ScrollDecorationViewPart extends ViewPart { this._domNode.setAttribute('aria-hidden', 'true'); } - public override dispose(): void { - super.dispose(); - } private _updateShouldShow(): boolean { const newShouldShow = (this._useShadows && this._scrollTop > 0); diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 202187a4aadb2..e64482e192c35 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -191,9 +191,6 @@ export class ViewLayout extends Disposable implements IViewLayout { this._updateHeight(); } - public override dispose(): void { - super.dispose(); - } public getScrollable(): Scrollable { return this._scrollable.getScrollable(); diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index b55bf1c003802..db742fc9ba8a8 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -474,9 +474,6 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon } } - public override dispose(): void { - super.dispose(); - } public override updateOptions(newOptions: Readonly): void { updateConfigurationService(this._configurationService, newOptions, false); @@ -544,9 +541,6 @@ export class StandaloneDiffEditor2 extends DiffEditorWidget implements IStandalo this._register(themeDomRegistration); } - public override dispose(): void { - super.dispose(); - } public override updateOptions(newOptions: Readonly): void { updateConfigurationService(this._configurationService, newOptions, true); diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index 787e20858a203..19ec19eaf5031 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -80,9 +80,6 @@ export abstract class AbstractKeybindingService extends Disposable implements IK this._logging = false; } - public override dispose(): void { - super.dispose(); - } protected abstract _getResolver(): KeybindingResolver; protected abstract _documentHasFocus(): boolean; diff --git a/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts b/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts index ba585c282453c..3ccaafb41286e 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts @@ -85,9 +85,6 @@ export class SelectionClipboard extends Disposable implements IEditorContributio } } - public override dispose(): void { - super.dispose(); - } } class LinuxSelectionClipboardPastePreventer extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 3a269eae76188..1a4bcc43b36ce 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -531,9 +531,6 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog { }); } - public override dispose(): void { - super.dispose(); - } private async handleValueChange(value: string) { try { From dadeb7c4167bdb76f10a3bab9d4b4f20201d7b1d Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Mon, 20 Apr 2026 13:45:25 -0700 Subject: [PATCH 20/32] no nps survey for agents app (#311478) --- .../workbench/contrib/surveys/browser/nps.contribution.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts b/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts index 5c0e330d87aea..f98dbed3b483b 100644 --- a/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts @@ -16,6 +16,7 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { URI } from '../../../../base/common/uri.js'; import { platform } from '../../../../base/common/process.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; const PROBABILITY = 0.15; const SESSION_COUNT_KEY = 'nps/sessionCount'; @@ -31,9 +32,10 @@ class NPSContribution implements IWorkbenchContribution { @ITelemetryService telemetryService: ITelemetryService, @IOpenerService openerService: IOpenerService, @IProductService productService: IProductService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - if (!productService.npsSurveyUrl || !configurationService.getValue('telemetry.feedback.enabled')) { + if (!productService.npsSurveyUrl || !configurationService.getValue('telemetry.feedback.enabled') || environmentService.isSessionsWindow) { return; } From 30701dbfcd7a9ff4bb770d5baf25413788b389d2 Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" Date: Mon, 20 Apr 2026 20:45:47 +0000 Subject: [PATCH 21/32] [cherry-pick] Fix bad modal clipping --- .../workbench/contrib/notebook/browser/notebookEditorWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 4cb3e8b225818..7c8cc185ff60e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -1946,7 +1946,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const modalEditorContainer = this.editorGroupsService.activeModalEditorPart?.modalElement; let clippingContainer: HTMLElement | undefined; - if (DOM.isHTMLElement(modalEditorContainer)) { + if (DOM.isHTMLElement(modalEditorContainer) && modalEditorContainer.contains(shadowElement)) { clippingContainer = modalEditorContainer; } else { clippingContainer = this.layoutService.getContainer(DOM.getWindow(this.getDomNode()), Parts.EDITOR_PART); From dd50d09337e4b2902e58f4cdb7a646f65eb3071e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 20 Apr 2026 14:17:05 -0700 Subject: [PATCH 22/32] Merge pull request #311473 from microsoft/connor4312/terminal-nointeract agentHost: set non-interactive env vars for tool-triggered terminals --- .../agentHost/node/agentHostTerminalManager.ts | 16 ++++++++++++++-- .../agentHost/node/copilot/copilotShellTools.ts | 2 +- .../test/node/copilotShellTools.test.ts | 7 ++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/agentHost/node/agentHostTerminalManager.ts b/src/vs/platform/agentHost/node/agentHostTerminalManager.ts index 3193302c67255..f0a77734e4029 100644 --- a/src/vs/platform/agentHost/node/agentHostTerminalManager.ts +++ b/src/vs/platform/agentHost/node/agentHostTerminalManager.ts @@ -38,7 +38,7 @@ export interface ICommandFinishedEvent { */ export interface IAgentHostTerminalManager { readonly _serviceBrand: undefined; - createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean }): Promise; + createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise; writeInput(uri: string, data: string): void; onData(uri: string, cb: (data: string) => void): IDisposable; onExit(uri: string, cb: (exitCode: number) => void): IDisposable; @@ -172,7 +172,7 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe * Create a new terminal backed by node-pty. * Spawns the user's default shell. */ - async createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean }): Promise { + async createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise { const uri = params.terminal; if (this._terminals.has(uri)) { throw new Error(`Terminal already exists: ${uri}`); @@ -199,6 +199,18 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe // prevents agent-executed commands from polluting the user's shell history. env['VSCODE_PREVENT_SHELL_HISTORY'] = '1'; } + if (options?.nonInteractive) { + // Suppress paging and interactive prompts so that tool-spawned + // terminals produce clean, machine-friendly output. An empty + // string disables paging in git, less, and most CLI tools and + // is safe on all platforms (unlike 'cat' which isn't on Windows PATH). + env['LC_ALL'] = 'C.UTF-8'; + env['PAGER'] = ''; + env['GIT_PAGER'] = ''; + env['GH_PAGER'] = ''; + env['GIT_TERMINAL_PROMPT'] = '0'; + env['DEBIAN_FRONTEND'] = 'noninteractive'; + } let shellArgs: string[] = []; const injection = await getShellIntegrationInjection( diff --git a/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts b/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts index c944e862b4441..9282fbd2ef529 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts @@ -109,7 +109,7 @@ export class ShellManager { claim, name: shellDisplayName, cwd: cwd ?? this._workingDirectory?.fsPath, - }, { shell: getShellExecutable(shellType), preventShellHistory: true }); + }, { shell: getShellExecutable(shellType), preventShellHistory: true, nonInteractive: true }); const shell: IManagedShell = { id, terminalUri, shellType }; this._shells.set(id, shell); diff --git a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts b/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts index 59386ce3b3fa6..ef56bf3e74f22 100644 --- a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts @@ -19,9 +19,9 @@ import { ShellManager, prefixForHistorySuppression } from '../../node/copilot/co class TestAgentHostTerminalManager implements IAgentHostTerminalManager { declare readonly _serviceBrand: undefined; - readonly created: { params: ICreateTerminalParams; options?: { shell?: string; preventShellHistory?: boolean } }[] = []; + readonly created: { params: ICreateTerminalParams; options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean } }[] = []; - async createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean }): Promise { + async createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise { this.created.push({ params, options }); } writeInput(): void { } @@ -66,7 +66,7 @@ suite('CopilotShellTools', () => { ]); }); - test('opts every managed shell into shell-history suppression', async () => { + test('opts every managed shell into shell-history suppression and non-interactive mode', async () => { const terminalManager = new TestAgentHostTerminalManager(); const services = new ServiceCollection(); services.set(ILogService, new NullLogService()); @@ -79,6 +79,7 @@ suite('CopilotShellTools', () => { assert.strictEqual(terminalManager.created.length, 1); assert.strictEqual(terminalManager.created[0].options?.preventShellHistory, true); + assert.strictEqual(terminalManager.created[0].options?.nonInteractive, true); }); test('prefixForHistorySuppression prepends a space for POSIX shells, no-op for PowerShell', () => { From 7a0f366d3fc8bb78e2159fe0335c84b10ac4b53e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 20 Apr 2026 14:17:22 -0700 Subject: [PATCH 23/32] agentHost: use vacuum into for safer sqlite migration during forking (#311477) * agentHost: use vacuum into for safer sqlite migration during forking * comments --- .../agentHost/common/sessionDataService.ts | 6 ++++ .../agentHost/node/copilot/copilotAgent.ts | 18 ++++++---- .../agentHost/node/sessionDatabase.ts | 5 +++ .../test/common/sessionTestHelpers.ts | 2 ++ .../test/node/sessionDatabase.test.ts | 35 +++++++++++++++++++ 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/agentHost/common/sessionDataService.ts b/src/vs/platform/agentHost/common/sessionDataService.ts index 3f8ff83584a15..d78d718861bd7 100644 --- a/src/vs/platform/agentHost/common/sessionDataService.ts +++ b/src/vs/platform/agentHost/common/sessionDataService.ts @@ -173,6 +173,12 @@ export interface ISessionDatabase extends IDisposable { */ remapTurnIds(mapping: ReadonlyMap): Promise; + /** + * Creates a safe, consistent copy of the database at the given path + * using SQLite's `VACUUM INTO` command. + */ + vacuumInto(targetPath: string): Promise; + /** * Resolves once all in-flight write operations on this database have * settled. Used by graceful shutdown to flush fire-and-forget writes diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts index bdb525dc0f87d..66c0b60fd2151 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgent.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgent.ts @@ -464,15 +464,21 @@ export class CopilotAgent extends Disposable implements IAgent { }); const newSessionId = forkResult.sessionId; - // Copy the source session's database file so the forked session - // inherits turn event IDs and file-edit snapshots. - const sourceDbDir = this._sessionDataService.getSessionDataDir(config.fork!.session); + // Copy the source session's database using VACUUM INTO so the + // forked session inherits turn event IDs and file-edit snapshots. + // VACUUM INTO is safe even while the source DB is open. const targetDbDir = this._sessionDataService.getSessionDataDirById(newSessionId); - const sourceDbPath = URI.joinPath(sourceDbDir, SESSION_DB_FILENAME); const targetDbPath = URI.joinPath(targetDbDir, SESSION_DB_FILENAME); try { - await fs.mkdir(targetDbDir.fsPath, { recursive: true }); - await fs.copyFile(sourceDbPath.fsPath, targetDbPath.fsPath); + const sourceDbRef = await this._sessionDataService.tryOpenDatabase(config.fork!.session); + if (sourceDbRef) { + try { + await fs.mkdir(targetDbDir.fsPath, { recursive: true }); + await sourceDbRef.object.vacuumInto(targetDbPath.fsPath); + } finally { + sourceDbRef.dispose(); + } + } } catch (err) { this._logService.warn(`[Copilot] Failed to copy session database for fork: ${err instanceof Error ? err.message : String(err)}`); } diff --git a/src/vs/platform/agentHost/node/sessionDatabase.ts b/src/vs/platform/agentHost/node/sessionDatabase.ts index 12e2a63b769a1..5c80593ddc8b8 100644 --- a/src/vs/platform/agentHost/node/sessionDatabase.ts +++ b/src/vs/platform/agentHost/node/sessionDatabase.ts @@ -535,6 +535,11 @@ export class SessionDatabase implements ISessionDatabase { } } + async vacuumInto(targetPath: string) { + const db = await this._ensureDb(); + await dbRun(db, 'VACUUM INTO ?', [targetPath]); + } + /** * Wrap a mutating operation's promise so {@link whenIdle} can await it. * Invoke at the **outermost** layer of every public mutating method so diff --git a/src/vs/platform/agentHost/test/common/sessionTestHelpers.ts b/src/vs/platform/agentHost/test/common/sessionTestHelpers.ts index 51887b7f65064..467b5427c7646 100644 --- a/src/vs/platform/agentHost/test/common/sessionTestHelpers.ts +++ b/src/vs/platform/agentHost/test/common/sessionTestHelpers.ts @@ -72,6 +72,8 @@ export class TestSessionDatabase implements ISessionDatabase { async close(): Promise { } + async vacuumInto(_targetPath: string): Promise { } + dispose(): void { } async setTurnEventId(_turnId: string, _eventId: string): Promise { } diff --git a/src/vs/platform/agentHost/test/node/sessionDatabase.test.ts b/src/vs/platform/agentHost/test/node/sessionDatabase.test.ts index 23d26104eb73a..9253836f420f7 100644 --- a/src/vs/platform/agentHost/test/node/sessionDatabase.test.ts +++ b/src/vs/platform/agentHost/test/node/sessionDatabase.test.ts @@ -4,11 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { tmpdir } from 'os'; +import * as fs from 'fs/promises'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { SessionDatabase, runMigrations, sessionDatabaseMigrations, type ISessionDatabaseMigration } from '../../node/sessionDatabase.js'; import { FileEditKind } from '../../common/state/sessionState.js'; import type { Database } from '@vscode/sqlite3'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { join } from '../../../../base/common/path.js'; suite('SessionDatabase', () => { @@ -489,4 +493,35 @@ suite('SessionDatabase', () => { assert.ok(tables.includes('session_metadata')); }); }); + + // ---- vacuumInto ----------------------------------------------------- + + suite('vacuumInto', () => { + + let tmpDir: string; + + setup(async () => { + tmpDir = await fs.mkdtemp(join(tmpdir(), 'session-db-test-' + generateUuid())); + }); + + teardown(async () => { + await Promise.all([db?.close(), db2?.close()]); + db = db2 = undefined; + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + + test('produces a copy with the same data', async () => { + db = disposables.add(await SessionDatabase.open(':memory:')); + await db.createTurn('turn-1'); + await db.setTurnEventId('turn-1', 'evt-1'); + await db.setMetadata('key', 'value'); + + const targetPath = join(tmpDir, 'copy.db'); + await db.vacuumInto(targetPath); + + db2 = disposables.add(await SessionDatabase.open(targetPath)); + assert.strictEqual(await db2.getTurnEventId('turn-1'), 'evt-1'); + assert.strictEqual(await db2.getMetadata('key'), 'value'); + }); + }); }); From 4fd4618c7e19a2cdc537e1336d371cb644fcb87d Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 20 Apr 2026 14:17:25 -0700 Subject: [PATCH 24/32] Merge pull request #311472 from microsoft/connor4312/ah-auto-connect agentHost: auto-connect for tunnels and cached sessions --- .../common/remoteAgentHostService.ts | 6 + .../browser/baseAgentHostSessionsProvider.ts | 8 +- .../chat/browser/sessionWorkspacePicker.ts | 33 ++- .../browser/remoteAgentHost.contribution.ts | 19 +- .../remoteAgentHostSessionsProvider.ts | 205 +++++++++++++++++- .../browser/tunnelAgentHost.contribution.ts | 58 +++-- .../remoteAgentHostSessionsProvider.test.ts | 63 +++++- 7 files changed, 353 insertions(+), 39 deletions(-) diff --git a/src/vs/platform/agentHost/common/remoteAgentHostService.ts b/src/vs/platform/agentHost/common/remoteAgentHostService.ts index 5214fe52cf6fc..73cf6beb37cfd 100644 --- a/src/vs/platform/agentHost/common/remoteAgentHostService.ts +++ b/src/vs/platform/agentHost/common/remoteAgentHostService.ts @@ -22,6 +22,12 @@ export const RemoteAgentHostsSettingId = 'chat.remoteAgentHosts'; /** Configuration key to enable remote agent host connections. */ export const RemoteAgentHostsEnabledSettingId = 'chat.remoteAgentHostsEnabled'; +/** + * Configuration key that controls whether online dev tunnels and + * configured SSH remote agent hosts are auto-connected at startup. + */ +export const RemoteAgentHostAutoConnectSettingId = 'chat.remoteAgentHostsAutoConnect'; + export const enum RemoteAgentHostEntryType { WebSocket = 'websocket', SSH = 'ssh', diff --git a/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts index 77b2797d5c871..a8bbc82367d44 100644 --- a/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/agentHost/browser/baseAgentHostSessionsProvider.ts @@ -14,8 +14,8 @@ import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { localize } from '../../../../nls.js'; import { AgentSession, IAgentConnection, IAgentSessionMetadata } from '../../../../platform/agentHost/common/agentService.js'; -import { NotificationType } from '../../../../platform/agentHost/common/state/protocol/notifications.js'; import { IResolveSessionConfigResult } from '../../../../platform/agentHost/common/state/protocol/commands.js'; +import { NotificationType } from '../../../../platform/agentHost/common/state/protocol/notifications.js'; import type { IFileEdit, IModelSelection, ISessionConfigPropertySchema, ISessionState, ISessionSummary } from '../../../../platform/agentHost/common/state/protocol/state.js'; import { ActionType, isSessionAction } from '../../../../platform/agentHost/common/state/sessionActions.js'; import { StateComponents } from '../../../../platform/agentHost/common/state/sessionState.js'; @@ -24,12 +24,12 @@ import { IChatSendRequestOptions, IChatService } from '../../../../workbench/con import { IChatSessionFileChange, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation, ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js'; import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; -import { agentHostSessionWorkspaceKey } from '../../../common/agentHostSessionWorkspace.js'; -import { diffsToChanges, diffsEqual, mapProtocolStatus } from '../../../common/agentHostDiffs.js'; +import { diffsEqual, diffsToChanges, mapProtocolStatus } from '../../../common/agentHostDiffs.js'; import { buildMutableConfigSchema, IAgentHostSessionsProvider, resolvedConfigsEqual } from '../../../common/agentHostSessionsProvider.js'; +import { agentHostSessionWorkspaceKey } from '../../../common/agentHostSessionWorkspace.js'; import { isSessionConfigComplete } from '../../../common/sessionConfig.js'; -import { ISendRequestOptions, ISessionChangeEvent } from '../../../services/sessions/common/sessionsProvider.js'; import { IChat, IGitHubInfo, ISession, ISessionType, ISessionWorkspace, ISessionWorkspaceBrowseAction, SessionStatus } from '../../../services/sessions/common/session.js'; +import { ISendRequestOptions, ISessionChangeEvent } from '../../../services/sessions/common/sessionsProvider.js'; // ============================================================================ // AgentHostSessionAdapter — shared adapter for local and remote sessions diff --git a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts index 5d05396bfb118..ba888405c0894 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts @@ -197,11 +197,10 @@ export class WorkspacePicker extends Disposable { return; } if (item.remoteProvider && item.browseActionIndex === undefined) { - if (item.remoteProvider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX)) { - // Disconnected tunnel — trigger connection flow - this.commandService.executeCommand('workbench.action.sessions.connectViaTunnel'); - } else { - // Disconnected SSH host — show options menu after widget hides + if (!item.remoteProvider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX)) { + // Disconnected SSH host — show options menu after widget hides. + // (Disconnected tunnels are rendered as disabled with a + // refresh toolbar action, so onSelect doesn't fire for them.) this._showRemoteHostOptionsDelayed(item.remoteProvider); } } else if (item.browseActionIndex !== undefined) { @@ -437,11 +436,27 @@ export class WorkspacePicker extends Disposable { const status = provider.connectionStatus!.get(); const isConnected = status === RemoteAgentHostConnectionStatus.Connected; const providerBrowseIndex = allBrowseActions.findIndex(a => a.providerId === provider.id); + const isTunnel = provider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX); const toolbarActions: IAction[] = []; - // Gear menu only for SSH hosts, not tunnel providers - if (!provider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX)) { + if (isTunnel) { + // Offline/connecting tunnels: surface a refresh button that + // attempts to (re)connect in case the cached status is stale. + if (!isConnected && providerBrowseIndex >= 0) { + const browseIndex = providerBrowseIndex; + toolbarActions.push(toAction({ + id: `workspacePicker.remote.refresh.${provider.id}`, + label: localize('workspacePicker.refreshTunnel', "Attempt to Connect"), + class: ThemeIcon.asClassName(Codicon.refresh), + run: () => { + this.actionWidgetService.hide(); + this._executeBrowseAction(browseIndex); + }, + })); + } + } else { + // Gear menu only for SSH hosts, not tunnel providers toolbarActions.push(toAction({ id: `workspacePicker.remote.gear.${provider.id}`, label: localize('workspacePicker.remoteOptions', "Options"), @@ -453,15 +468,13 @@ export class WorkspacePicker extends Disposable { })); } - const isTunnel = provider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX); - items.push({ kind: ActionListItemKind.Action, label: provider.label, description: this._getStatusDescription(status), hover: { content: this._getStatusHover(status, provider.remoteAddress) }, group: { title: '', icon: isTunnel ? Codicon.cloud : Codicon.remote }, - disabled: isTunnel ? false : !isConnected, + disabled: !isConnected, item: { browseActionIndex: isConnected && providerBrowseIndex >= 0 ? providerBrowseIndex : undefined, remoteProvider: provider, diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts index 2349a0a4699d6..0f12071dbdc3f 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts @@ -10,7 +10,7 @@ import { URI } from '../../../../base/common/uri.js'; import * as nls from '../../../../nls.js'; import { agentHostAuthority } from '../../../../platform/agentHost/common/agentHostUri.js'; import { type AgentProvider, type IAgentConnection } from '../../../../platform/agentHost/common/agentService.js'; -import { IRemoteAgentHostConnectionInfo, IRemoteAgentHostEntry, IRemoteAgentHostService, RemoteAgentHostConnectionStatus, RemoteAgentHostEntryType, RemoteAgentHostsEnabledSettingId, RemoteAgentHostsSettingId, getEntryAddress } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; +import { IRemoteAgentHostConnectionInfo, IRemoteAgentHostEntry, IRemoteAgentHostService, RemoteAgentHostAutoConnectSettingId, RemoteAgentHostConnectionStatus, RemoteAgentHostEntryType, RemoteAgentHostsEnabledSettingId, RemoteAgentHostsSettingId, getEntryAddress } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; import { TunnelAgentHostsSettingId } from '../../../../platform/agentHost/common/tunnelAgentHost.js'; import { type IProtectedResourceMetadata, type URI as ProtocolURI } from '../../../../platform/agentHost/common/state/protocol/state.js'; import { type IAgentInfo, type ICustomizationRef, type IRootState } from '../../../../platform/agentHost/common/state/sessionState.js'; @@ -102,7 +102,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc // Reconcile providers when configured entries change this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(RemoteAgentHostsSettingId) || e.affectsConfiguration(RemoteAgentHostsEnabledSettingId)) { + if (e.affectsConfiguration(RemoteAgentHostsSettingId) || e.affectsConfiguration(RemoteAgentHostsEnabledSettingId) || e.affectsConfiguration(RemoteAgentHostAutoConnectSettingId)) { this._reconcile(); } })); @@ -194,6 +194,7 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc * sshConfigHost but no active connection. */ private _reconnectSSHEntries(): void { + const autoConnect = this._configurationService.getValue(RemoteAgentHostAutoConnectSettingId); const entries = this._remoteAgentHostService.configuredEntries; for (const entry of entries) { if (entry.connection.type !== RemoteAgentHostEntryType.SSH || !entry.connection.sshConfigHost) { @@ -208,6 +209,9 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc if (hasConnection || this._pendingSSHReconnects.has(sshConfigHost)) { continue; } + if (!autoConnect) { + continue; + } this._pendingSSHReconnects.add(sshConfigHost); this._logService.info(`[RemoteAgentHost] Re-establishing SSH tunnel for ${sshConfigHost}`); this._sshService.reconnect(sshConfigHost, entry.name).then(() => { @@ -216,6 +220,10 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc }).catch(err => { this._pendingSSHReconnects.delete(sshConfigHost); this._logService.error(`[RemoteAgentHost] SSH reconnect failed for ${sshConfigHost}`, err); + // Host is unreachable — unpublish any cached sessions we + // were showing so the UI doesn't list stale entries for a + // host we cannot currently reach. + this._providerInstances.get(address)?.unpublishCachedSessions(); }); } } @@ -590,6 +598,13 @@ Registry.as(ConfigurationExtensions.Configuration).regis scope: ConfigurationScope.APPLICATION, tags: ['experimental', 'advanced'], }, + [RemoteAgentHostAutoConnectSettingId]: { + type: 'boolean', + description: nls.localize('chat.remoteAgentHosts.autoConnect', "Automatically connect to online dev tunnel and SSH-configured remote agent hosts on startup. When disabled, cached sessions are still shown but connections are established only on demand."), + default: true, + scope: ConfigurationScope.APPLICATION, + tags: ['experimental', 'advanced'], + }, 'chat.sshRemoteAgentHostCommand': { type: 'string', description: nls.localize('chat.sshRemoteAgentHostCommand', "For development: Override the command used to start the remote agent host over SSH. When set, skips automatic CLI installation and runs this command instead. The command must print a WebSocket URL matching ws://127.0.0.1:PORT (optionally with ?tkn=TOKEN) to stdout or stderr./"), diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts index 17786c2c4c9f0..c4c81f5a9c7e4 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts @@ -19,6 +19,7 @@ import { AgentSession, type IAgentConnection, type IAgentSessionMetadata } from import { RemoteAgentHostConnectionStatus } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { IChatService } from '../../../../workbench/contrib/chat/common/chatService/chatService.js'; import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; @@ -53,6 +54,62 @@ function wellKnownAgentProvider(sessionType: string): string | undefined { return undefined; } +/** Storage key prefix for cached session summaries, per remote address. */ +const CACHED_SESSIONS_STORAGE_PREFIX = 'remoteAgentHost.cachedSessions.'; + +/** Maximum number of cached session summaries persisted per host. */ +const CACHED_SESSIONS_MAX_PER_HOST = 100; + +/** + * Serialized shape of an {@link IAgentSessionMetadata} suitable for + * persisting via {@link IStorageService}. URIs are stored as strings + * and diffs are intentionally omitted (they are re-populated when the + * connection refreshes sessions). + */ +interface ISerializedSessionMetadata { + readonly session: string; + readonly startTime: number; + readonly modifiedTime: number; + readonly summary?: string; + readonly model?: IAgentSessionMetadata['model']; + readonly workingDirectory?: string; + readonly isRead?: boolean; + readonly isDone?: boolean; + readonly project?: { readonly uri: string; readonly displayName: string }; +} + +function serializeMetadata(meta: IAgentSessionMetadata): ISerializedSessionMetadata { + return { + session: meta.session.toString(), + startTime: meta.startTime, + modifiedTime: meta.modifiedTime, + summary: meta.summary, + model: meta.model, + workingDirectory: meta.workingDirectory?.toString(), + isRead: meta.isRead, + isDone: meta.isDone, + project: meta.project ? { uri: meta.project.uri.toString(), displayName: meta.project.displayName } : undefined, + }; +} + +function deserializeMetadata(raw: ISerializedSessionMetadata): IAgentSessionMetadata | undefined { + try { + return { + session: URI.parse(raw.session), + startTime: raw.startTime, + modifiedTime: raw.modifiedTime, + summary: raw.summary, + model: raw.model, + workingDirectory: raw.workingDirectory ? URI.parse(raw.workingDirectory) : undefined, + isRead: raw.isRead, + isDone: raw.isDone, + project: raw.project ? { uri: URI.parse(raw.project.uri), displayName: raw.project.displayName } : undefined, + }; + } catch { + return undefined; + } +} + function toLocalProjectUri(uri: URI, connectionAuthority: string): URI { return uri.scheme === Schemas.file ? toAgentHostUri(uri, connectionAuthority) : uri; } @@ -120,11 +177,36 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid private readonly _connectionListeners = this._register(new DisposableStore()); private readonly _connectionAuthority: string; private readonly _connectOnDemand: (() => Promise) | undefined; + /** Storage key used for persisting {@link _sessionCache} snapshots. */ + private readonly _storageKey: string; + /** + * Set when {@link _sessionCache} has changed since the last persist. + * The actual write happens on the next `onWillSaveState` signal from + * {@link IStorageService} so that bursts of notifications do not + * repeatedly re-serialize the whole cache. + */ + private _cacheDirty = false; + /** + * Snapshot of the source metadata for each adapter in {@link _sessionCache}, + * keyed by raw session ID. Captured in {@link createAdapter} and re-used by + * {@link _persistCache} to serialize sessions without having to reconstruct + * every `IAgentSessionMetadata` field from observables. + */ + private readonly _metaByRawId = new Map(); + /** + * When `true`, the provider has been marked unreachable and sessions are + * hidden from {@link getSessions}, even though {@link _sessionCache} and + * persistent storage are retained. Cleared when a new connection is wired + * up in {@link setConnection}, at which point the cached entries are + * re-announced so the UI can repopulate. + */ + private _unpublished = false; constructor( config: IRemoteAgentHostSessionsProviderConfig, @IFileDialogService private readonly _fileDialogService: IFileDialogService, @INotificationService private readonly _notificationService: INotificationService, + @IStorageService private readonly _storageService: IStorageService, @IChatSessionsService chatSessionsService: IChatSessionsService, @IChatService chatService: IChatService, @IChatWidgetService chatWidgetService: IChatWidgetService, @@ -139,6 +221,7 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid this.id = `agenthost-${this._connectionAuthority}`; this.label = displayName; this.remoteAddress = config.address; + this._storageKey = `${CACHED_SESSIONS_STORAGE_PREFIX}${this._connectionAuthority}`; this.browseActions = [{ label: localize('folders', "Folders"), @@ -146,6 +229,30 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid providerId: this.id, run: () => this._browseForFolder(), }]; + + this._loadCachedSessions(); + + this._register(this._onDidChangeSessions.event(e => { + if (this._unpublished) { + return; + } + if (e.added.length > 0 || e.removed.length > 0 || e.changed.length > 0) { + this._cacheDirty = true; + } + for (const removed of e.removed) { + const rawId = this._rawIdFromChatId(removed.sessionId); + if (rawId) { + this._metaByRawId.delete(rawId); + } + } + })); + + this._register(this._storageService.onWillSaveState(() => { + if (this._cacheDirty) { + this._persistCache(); + this._cacheDirty = false; + } + })); } // -- BaseAgentHostSessionsProvider hooks --------------------------------- @@ -158,6 +265,7 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid const provider = AgentSession.provider(meta.session) ?? DEFAULT_AGENT_HOST_PROVIDER; const resourceScheme = remoteAgentHostSessionTypeId(this._connectionAuthority, provider); const logicalType = this._logicalSessionTypeForProvider(provider); + this._metaByRawId.set(AgentSession.id(meta.session), meta); return new AgentHostSessionAdapter(meta, this.id, resourceScheme, logicalType, { icon: this.icon, description: new MarkdownString().appendText(this.label), @@ -175,6 +283,10 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid return wellKnownAgentProvider(sessionType) ?? sessionType.substring(`remote-${this._connectionAuthority}-`.length); } + override getSessions(): ISession[] { + return this._unpublished ? [] : super.getSessions(); + } + protected override mapWorkingDirectoryUri(uri: URI): URI { return toAgentHostUri(uri, this._connectionAuthority); } @@ -237,6 +349,7 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid this._sessionStateSubscriptions.clearAndDisposeAll(); this._connection = connection; this._defaultDirectory = defaultDirectory; + this._unpublished = false; // Dynamically discover session types from the host's advertised agents. const rootStateValue = connection.rootState.value; @@ -256,7 +369,10 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid /** * Clear the connection, e.g. when the remote host disconnects. - * Retains the provider registration so it remains visible in the UI. + * Retains the provider registration so it remains visible in the UI, + * and **preserves** the cached session list so previously loaded + * sessions stay visible while we're offline. Callers that know the + * host is unreachable should follow up with {@link unpublishCachedSessions}. */ clearConnection(): void { this._connectionListeners.clear(); @@ -279,19 +395,91 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid this._onDidChangeSessionTypes.fire(); } - const removed: ISession[] = Array.from(this._sessionCache.values()); + // Drop only the transient pending/draft session; keep the persisted + // cache so the workspace picker keeps showing offline sessions. if (this._pendingSession) { - removed.push(this._pendingSession); + const pending = this._pendingSession; this._pendingSession = undefined; + this._onDidChangeSessions.fire({ added: [], removed: [pending], changed: [] }); } - this._sessionCache.clear(); - this._runningSessionConfigs.clear(); + + // Reset the in-memory cache-initialized flag so a fresh connection + // triggers a full list refresh (which will reconcile against the + // persisted entries we keep on disk). this._cacheInitialized = false; + } + + /** + * Hide cached sessions from the UI without discarding them. Called by the + * host-tracking contributions when they determine the remote host is + * unreachable (tunnel offline or SSH reconnect failed). The in-memory + * cache and persisted storage are left intact so the sessions can be + * restored if the host comes back online in this session, or on the next + * launch. The next {@link setConnection} call re-announces the cached + * entries. + */ + unpublishCachedSessions(): void { + if (this._unpublished) { + return; + } + this._unpublished = true; + const removed: ISession[] = Array.from(this._sessionCache.values()); if (removed.length > 0) { this._onDidChangeSessions.fire({ added: [], removed, changed: [] }); } } + /** Load persisted session summaries into {@link _sessionCache}. */ + private _loadCachedSessions(): void { + const parsed = this._storageService.getObject(this._storageKey, StorageScope.APPLICATION); + if (!Array.isArray(parsed)) { + return; + } + for (const entry of parsed as readonly ISerializedSessionMetadata[]) { + const meta = deserializeMetadata(entry); + if (!meta) { + continue; + } + const rawId = AgentSession.id(meta.session); + if (this._sessionCache.has(rawId)) { + continue; + } + const cached = this.createAdapter(meta); + this._sessionCache.set(rawId, cached); + } + } + + /** + * Persist the current {@link _sessionCache} to storage, capping at + * {@link CACHED_SESSIONS_MAX_PER_HOST} most-recently-modified entries. + * Mutable fields are read from each adapter's observables and overlaid on + * top of the original metadata snapshot captured in {@link _metaByRawId}. + */ + private _persistCache(): void { + const entries: ISerializedSessionMetadata[] = []; + for (const [rawId, adapter] of this._sessionCache) { + const base = this._metaByRawId.get(rawId); + if (!base) { + continue; + } + entries.push(serializeMetadata({ + ...base, + summary: adapter.title.get() || base.summary, + modifiedTime: adapter.updatedAt.get().getTime(), + model: adapter.modelSelection ?? base.model, + isRead: adapter.isRead.get(), + isDone: adapter.isArchived.get(), + })); + } + if (entries.length === 0) { + this._storageService.remove(this._storageKey, StorageScope.APPLICATION); + return; + } + entries.sort((a, b) => b.modifiedTime - a.modifiedTime); + const limited = entries.slice(0, CACHED_SESSIONS_MAX_PER_HOST); + this._storageService.store(this._storageKey, JSON.stringify(limited), StorageScope.APPLICATION, StorageTarget.USER); + } + // -- Session-type sync --------------------------------------------------- /** @@ -365,7 +553,12 @@ export class RemoteAgentHostSessionsProvider extends BaseAgentHostSessionsProvid private async _browseForFolder(): Promise { // Establish connection on demand if a hook is provided (e.g. tunnel relay) if (!this._connection && this._connectOnDemand) { - await this._connectOnDemand(); + try { + await this._connectOnDemand(); + } catch (err) { + this._notificationService.error(localize('connectFailed', "Failed to connect to remote agent host '{0}': {1}", this.label, err instanceof Error ? err.message : String(err))); + return undefined; + } } if (!this._connection) { diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/tunnelAgentHost.contribution.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/tunnelAgentHost.contribution.ts index 4f8e3fd342f4f..2bd9cf5652544 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/tunnelAgentHost.contribution.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/tunnelAgentHost.contribution.ts @@ -7,7 +7,7 @@ import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../ import { isWeb } from '../../../../base/common/platform.js'; import { mainWindow } from '../../../../base/browser/window.js'; import * as nls from '../../../../nls.js'; -import { IRemoteAgentHostService, RemoteAgentHostConnectionStatus, RemoteAgentHostsEnabledSettingId } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; +import { IRemoteAgentHostService, RemoteAgentHostAutoConnectSettingId, RemoteAgentHostConnectionStatus, RemoteAgentHostsEnabledSettingId } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; import { ITunnelAgentHostService, TUNNEL_ADDRESS_PREFIX, type ITunnelInfo } from '../../../../platform/agentHost/common/tunnelAgentHost.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -42,6 +42,12 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc private readonly _providerInstances = new Map(); private readonly _pendingConnects = new Map>(); private _lastStatusCheck = 0; + /** + * `false` until the first {@link _silentStatusCheck} resolves. Until then + * we keep newly-created providers in the `Connecting` state so the picker + * doesn't briefly show every cached tunnel as "Offline" on startup. + */ + private _initialStatusChecked = false; /** Previous connection status per address — used to detect Connected→Disconnected transitions. */ private readonly _previousStatuses = new Map(); @@ -171,9 +177,13 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc RemoteAgentHostSessionsProvider, { address, name, - connectOnDemand: () => this._connectTunnel(address), + connectOnDemand: () => this._connectTunnel(address, { userInitiated: true }), }, ); + // Surface as "Connecting" until the first silent status check or an + // auto-connect attempt determines the real state; otherwise the picker + // flashes "Offline" for every cached tunnel on startup. + provider.setConnectionStatus(RemoteAgentHostConnectionStatus.Connecting); store.add(provider); store.add(this._sessionsProvidersService.registerProvider(provider)); this._providerInstances.set(address, provider); @@ -190,6 +200,10 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc provider.setConnectionStatus(connectionInfo.status); } else if (this._pendingConnects.has(address)) { provider.setConnectionStatus(RemoteAgentHostConnectionStatus.Connecting); + } else if (!this._initialStatusChecked) { + // Keep the initial "Connecting" state so the picker doesn't + // flash "Offline" before the first silent status check runs. + provider.setConnectionStatus(RemoteAgentHostConnectionStatus.Connecting); } else { provider.setConnectionStatus(RemoteAgentHostConnectionStatus.Disconnected); } @@ -219,7 +233,7 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc * Establish a relay connection to a cached tunnel. Called on demand * when the user invokes the browse action on an online-but-not-connected tunnel. */ - private _connectTunnel(address: string): Promise { + private _connectTunnel(address: string, options: { readonly userInitiated: boolean }): Promise { const existing = this._pendingConnects.get(address); if (existing) { return existing; @@ -239,15 +253,16 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc const promise = (async () => { // Show a progress notification after a short delay so quick - // connects don't flash a notification. + // connects don't flash a notification. Only show for user-initiated + // connects; background auto-connects and reconnects stay silent. let handle: { close(): void } | undefined; - const timer = setTimeout(() => { + const timer = options.userInitiated ? setTimeout(() => { handle = this._notificationService.notify({ severity: Severity.Info, message: nls.localize('tunnelConnecting', "Connecting to tunnel '{0}'...", cached.name), progress: { infinite: true }, }); - }, 1000); + }, 1000) : undefined; this._updateConnectionStatuses(); try { @@ -279,7 +294,9 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc } throw err; } finally { - clearTimeout(timer); + if (timer !== undefined) { + clearTimeout(timer); + } handle?.close(); this._pendingConnects.delete(address); this._updateConnectionStatuses(); @@ -412,7 +429,7 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc } this._reconnectAttempts.set(address, attempt + 1); - this._connectTunnel(address).catch(() => { /* _connectTunnel already re-schedules on failure */ }); + this._connectTunnel(address, { userInitiated: false }).catch(() => { /* _connectTunnel already re-schedules on failure */ }); }, delay); this._reconnectTimeouts.set(address, timer); } @@ -607,6 +624,8 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc private async _silentStatusCheck(): Promise { const enabled = this._configurationService.getValue(RemoteAgentHostsEnabledSettingId); if (!enabled) { + this._initialStatusChecked = true; + this._updateConnectionStatuses(); return; } @@ -618,6 +637,8 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc onlineTunnels = await this._tunnelService.listTunnels({ silent: true }); } catch { // No cached token or network error — leave statuses as-is + this._initialStatusChecked = true; + this._updateConnectionStatuses(); return; } @@ -643,7 +664,8 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc // Update online/offline status based on hostConnectionCount. // For tunnels, Connected means "host is online" (clickable to connect), // Disconnected means "host is offline". Actual relay connection - // establishment happens when the user clicks the tunnel. + // establishment happens when the user clicks the tunnel (or via + // auto-connect below when enabled). const onlineTunnelMap = new Map(onlineTunnels.map(t => [t.tunnelId, t])); for (const [address, provider] of this._providerInstances) { // Skip tunnels that already have an active relay connection @@ -660,13 +682,18 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc provider.setConnectionStatus(RemoteAgentHostConnectionStatus.Connected); } else { provider.setConnectionStatus(RemoteAgentHostConnectionStatus.Disconnected); + // Host is not online — drop any cached sessions we were + // showing for it so the UI doesn't list stale entries. + provider.unpublishCachedSessions(); } } - // Auto-connect online tunnels that aren't connected yet. - // On web there is no workspace picker to trigger manual connection, - // so we connect eagerly when a tunnel is discovered and online. - if (isWeb) { + // Auto-connect online tunnels that aren't connected yet when the + // user has opted into auto-connect (default on). This mirrors the + // web embedder behaviour where no workspace picker is available + // to trigger manual connection. + const autoConnect = this._configurationService.getValue(RemoteAgentHostAutoConnectSettingId); + if (autoConnect) { for (const tunnel of onlineTunnels) { if (tunnel.hostConnectionCount > 0) { const address = `${TUNNEL_ADDRESS_PREFIX}${tunnel.tunnelId}`; @@ -674,12 +701,15 @@ export class TunnelAgentHostContribution extends Disposable implements IWorkbenc c => c.address === address && c.status === RemoteAgentHostConnectionStatus.Connected ); if (!alreadyConnected) { - this._connectTunnel(address); + this._connectTunnel(address, { userInitiated: false }); } } } } } + + this._initialStatusChecked = true; + this._updateConnectionStatuses(); } } diff --git a/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts b/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts index 5109b3ebad754..04d2ee6d5d65f 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts @@ -177,7 +177,7 @@ function createSession(id: string, opts?: { provider?: string; summary?: string; }; } -function createProvider(disposables: DisposableStore, connection: MockAgentConnection, overrides?: { address?: string; connectionName?: string | undefined; sendRequest?: (resource: URI, message: string, options?: IChatSendRequestOptions) => Promise; openSession?: boolean }): RemoteAgentHostSessionsProvider { +function createProvider(disposables: DisposableStore, connection: MockAgentConnection, overrides?: { address?: string; connectionName?: string | undefined; sendRequest?: (resource: URI, message: string, options?: IChatSendRequestOptions) => Promise; openSession?: boolean; storageService?: IStorageService; noConnection?: boolean }): RemoteAgentHostSessionsProvider { const instantiationService = disposables.add(new TestInstantiationService()); instantiationService.stub(IFileDialogService, {}); @@ -196,7 +196,7 @@ function createProvider(disposables: DisposableStore, connection: MockAgentConne instantiationService.stub(ILanguageModelsService, { lookupLanguageModel: () => undefined, }); - instantiationService.stub(IStorageService, disposables.add(new InMemoryStorageService())); + instantiationService.stub(IStorageService, overrides?.storageService ?? disposables.add(new InMemoryStorageService())); const config: IRemoteAgentHostSessionsProviderConfig = { address: overrides?.address ?? 'localhost:4321', @@ -204,7 +204,9 @@ function createProvider(disposables: DisposableStore, connection: MockAgentConne }; const provider = disposables.add(instantiationService.createInstance(RemoteAgentHostSessionsProvider, config)); - provider.setConnection(connection); + if (!overrides?.noConnection) { + provider.setConnection(connection); + } return provider; } @@ -761,6 +763,61 @@ suite('RemoteAgentHostSessionsProvider', () => { assert.strictEqual(session!.loading.get(), false); })); + test('unpublishCachedSessions hides sessions but retains persisted cache', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + const storageService = disposables.add(new InMemoryStorageService()); + connection.addSession(createSession('keep-me', { summary: 'Keep Me' })); + const provider = createProvider(disposables, connection, { storageService }); + await timeout(0); + assert.strictEqual(provider.getSessions().length, 1); + + const events: ISessionChangeEvent[] = []; + disposables.add(provider.onDidChangeSessions(e => events.push(e))); + + provider.unpublishCachedSessions(); + + // Sessions are hidden from the listing immediately. + assert.deepStrictEqual( + { + sessionCount: provider.getSessions().length, + eventRemovedTitles: events.flatMap(e => e.removed.map(s => s.title.get())), + }, + { sessionCount: 0, eventRemovedTitles: ['Keep Me'] }, + ); + + // Flush triggers onWillSaveState; the metadata must survive so the + // session re-serializes instead of being dropped from storage. + await storageService.flush(); + + const provider2 = createProvider(disposables, new MockAgentConnection(), { storageService, noConnection: true }); + assert.deepStrictEqual( + provider2.getSessions().map(s => s.title.get()), + ['Keep Me'], + ); + })); + + test('setConnection after unpublishCachedSessions restores cached sessions', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + connection.addSession(createSession('restore-me', { summary: 'Restore Me' })); + const provider = createProvider(disposables, connection); + await timeout(0); + assert.strictEqual(provider.getSessions().length, 1); + + provider.unpublishCachedSessions(); + assert.strictEqual(provider.getSessions().length, 0); + + // Simulate the host coming back online with a fresh connection that + // still reports the same session. + const reconnected = new MockAgentConnection(); + disposables.add(toDisposable(() => reconnected.dispose())); + reconnected.addSession(createSession('restore-me', { summary: 'Restore Me' })); + provider.setConnection(reconnected); + await timeout(0); + + assert.deepStrictEqual( + provider.getSessions().map(s => s.title.get()), + ['Restore Me'], + ); + })); + test('sendAndCreateChat throws for unknown session', async () => { const provider = createProvider(disposables, connection); await assert.rejects( From 78548e184dad5167d5ceeb8a385444b202587855 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Mon, 20 Apr 2026 14:18:10 -0700 Subject: [PATCH 25/32] Make tool_search visible in the tools picker (#311485) - Add userDescription and toolReferenceName to tool_search registration so it passes the canBeReferencedInPrompt check - Localize displayName and userDescription with l10n.t() - Group tool under the 'vscode' toolset - Remove redundant allowTools gate in agentIntent (already gated by models filter on registration and endpoint.supportsToolSearch) --- .../copilot/src/extension/intents/node/agentIntent.ts | 4 +--- .../copilot/src/extension/tools/node/toolSearchTool.ts | 6 +++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/extensions/copilot/src/extension/intents/node/agentIntent.ts b/extensions/copilot/src/extension/intents/node/agentIntent.ts index 8b85a7f39c44e..bbf7d45a66f26 100644 --- a/extensions/copilot/src/extension/intents/node/agentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/agentIntent.ts @@ -17,7 +17,7 @@ import { IAutomodeService } from '../../../platform/endpoint/node/automodeServic import { IEnvService } from '../../../platform/env/common/envService'; import { ILogService } from '../../../platform/log/common/logService'; import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogService'; -import { CUSTOM_TOOL_SEARCH_NAME, isAnthropicContextEditingEnabled } from '../../../platform/networking/common/anthropic'; +import { isAnthropicContextEditingEnabled } from '../../../platform/networking/common/anthropic'; import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { modelsWithoutResponsesContextManagement } from '../../../platform/networking/common/openai'; import { INotebookService } from '../../../platform/notebook/common/notebookService'; @@ -141,8 +141,6 @@ export const getAgentTools = async (accessor: ServicesAccessor, request: vscode. allowTools[ToolName.MultiReplaceString] = true; } - allowTools[CUSTOM_TOOL_SEARCH_NAME] = !!model.supportsToolSearch; - const tools = toolsService.getEnabledTools(request, model, tool => { if (typeof allowTools[tool.name] === 'boolean') { return allowTools[tool.name]; diff --git a/extensions/copilot/src/extension/tools/node/toolSearchTool.ts b/extensions/copilot/src/extension/tools/node/toolSearchTool.ts index ceca12f5a8b9a..a0f83013ee292 100644 --- a/extensions/copilot/src/extension/tools/node/toolSearchTool.ts +++ b/extensions/copilot/src/extension/tools/node/toolSearchTool.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; +import * as l10n from '@vscode/l10n'; import { ILogService } from '../../../platform/log/common/logService'; import { CUSTOM_TOOL_SEARCH_NAME } from '../../../platform/networking/common/anthropic'; import { LanguageModelTextPart, LanguageModelToolResult } from '../../../vscodeTypes'; @@ -56,10 +57,13 @@ export class ToolSearchTool implements ICopilotModelSpecificTool Date: Tue, 21 Apr 2026 07:22:21 +1000 Subject: [PATCH 26/32] feat: add 'kind' property to parent session metadata (#311469) Co-authored-by: Copilot --- .../chatSessions/vscode-node/chatSessionMetadataStoreImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionMetadataStoreImpl.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionMetadataStoreImpl.ts index 2213eca09fa7a..8a9f81751d994 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionMetadataStoreImpl.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionMetadataStoreImpl.ts @@ -373,7 +373,7 @@ export class ChatSessionMetadataStore extends Disposable implements IChatSession public async setSessionParentId(sessionId: string, parentSessionId: string): Promise { await this._intialize.value; - await this.updateMetadataFields(sessionId, { parentSessionId }); + await this.updateMetadataFields(sessionId, { parentSessionId, kind: 'sub-session' }); } public async getSessionParentId(sessionId: string): Promise { From 79e5111feb7a277bfbe2bb06fb06dd71c877c284 Mon Sep 17 00:00:00 2001 From: Ben Villalobos Date: Mon, 20 Apr 2026 14:23:32 -0700 Subject: [PATCH 27/32] Allow cherry-pick bot PRs in engineering system changes check (#311475) * Allow cherry-pick bot PRs in engineering system changes check Add an exception for PRs created by vs-code-engineering[bot] whose title starts with [cherry-pick] and that carry the cherry-pick-artifact label. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fetch cherry-pick-artifact label via API at runtime The label is applied ~2s after PR creation, so the webhook payload may not include it. Fetch current labels from the API instead, gated behind cheap event-payload checks to avoid extra API calls on unrelated PRs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add label retry loop and consolidate guard expressions Retry the cherry-pick-artifact label check up to 3 times (2s apart) to handle the ~2s delay between PR creation and label application. Consolidate the repeated exception guards into a single 'allowed' step with a 'blocked' output, simplifying downstream conditions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../no-engineering-system-changes.yml | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/workflows/no-engineering-system-changes.yml b/.github/workflows/no-engineering-system-changes.yml index b567d1258844d..77c04be07181d 100644 --- a/.github/workflows/no-engineering-system-changes.yml +++ b/.github/workflows/no-engineering-system-changes.yml @@ -88,22 +88,54 @@ jobs: fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Allow cherry-pick bot PRs + id: cherry_pick_exception + if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && steps.bot_field_exception.outputs.allowed != 'true' && github.event.pull_request.user.login == 'vs-code-engineering[bot]' && startsWith(github.event.pull_request.title, '[cherry-pick]') }} + run: | + # The label is applied ~2s after PR creation, so the webhook payload + # may not include it yet. Fetch current labels from the API with retries. + for attempt in 1 2 3; do + if gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels --jq '.[].name' | grep -qx 'cherry-pick-artifact'; then + echo "Cherry-pick PR by vs-code-engineering bot with cherry-pick-artifact label — allowing" + echo "allowed=true" >> $GITHUB_OUTPUT + exit 0 + fi + if [ "$attempt" -lt 3 ]; then + echo "cherry-pick-artifact label not present yet (attempt $attempt/3); retrying in 2s" + sleep 2 + fi + done + echo "Cherry-pick PR by bot but missing cherry-pick-artifact label after retries — not allowed" + echo "allowed=false" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Determine if engineering system changes are allowed + id: allowed + if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' }} + run: | + if [[ "${{ steps.bot_field_exception.outputs.allowed }}" == "true" || "${{ steps.cherry_pick_exception.outputs.allowed }}" == "true" ]]; then + echo "Engineering system changes are allowed by an exception" + echo "blocked=false" >> $GITHUB_OUTPUT + else + echo "No exception applies — enforcing restrictions" + echo "blocked=true" >> $GITHUB_OUTPUT + fi - name: Prevent Copilot from modifying engineering systems - if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && steps.bot_field_exception.outputs.allowed != 'true' && github.event.pull_request.user.login == 'Copilot' }} + if: ${{ steps.allowed.outputs.blocked == 'true' && github.event.pull_request.user.login == 'Copilot' }} run: | echo "Copilot is not allowed to modify .github/workflows, build folder files, or package.json files." echo "If you need to update engineering systems, please do so manually or through authorized means." exit 1 - uses: octokit/request-action@b91aabaa861c777dcdb14e2387e30eddf04619ae # v3.0.0 id: get_permissions - if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && steps.bot_field_exception.outputs.allowed != 'true' && github.event.pull_request.user.login != 'Copilot' }} + if: ${{ steps.allowed.outputs.blocked == 'true' && github.event.pull_request.user.login != 'Copilot' }} with: route: GET /repos/microsoft/vscode/collaborators/${{ github.event.pull_request.user.login }}/permission env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set control output variable id: control - if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && steps.bot_field_exception.outputs.allowed != 'true' && github.event.pull_request.user.login != 'Copilot' }} + if: ${{ steps.allowed.outputs.blocked == 'true' && github.event.pull_request.user.login != 'Copilot' }} run: | echo "user: ${{ github.event.pull_request.user.login }}" echo "role: ${{ fromJson(steps.get_permissions.outputs.data).permission }}" @@ -111,7 +143,7 @@ jobs: echo "should_run: ${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) }}" echo "should_run=${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) && github.event.pull_request.user.login != 'dependabot[bot]' }}" >> $GITHUB_OUTPUT - name: Check for engineering system changes - if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && steps.bot_field_exception.outputs.allowed != 'true' && steps.control.outputs.should_run == 'true' }} + if: ${{ steps.allowed.outputs.blocked == 'true' && steps.control.outputs.should_run == 'true' }} run: | echo "Changes to .github/workflows/, build/ folder files, or package.json files aren't allowed in PRs." exit 1 From bef799f103500d8592796bc4f717f41fbbf86e77 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:37:45 -0700 Subject: [PATCH 28/32] Don't expose public mutable properties from actionbar --- src/vs/base/browser/ui/actionbar/actionbar.ts | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index da20e27e04010..f2b116fb7d2a5 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -79,7 +79,9 @@ export class ActionBar extends Disposable implements IActionRunner { }; // View Items - viewItems: IActionViewItem[]; + private _viewItems: IActionViewItem[]; + get viewItems(): readonly IActionViewItem[] { return this._viewItems; } + private readonly viewItemDisposables = this._register(new DisposableMap()); private previouslyFocusedItem?: number; protected focusedItem?: number; @@ -91,7 +93,7 @@ export class ActionBar extends Disposable implements IActionRunner { private focusable: boolean = true; // Elements - domNode: HTMLElement; + readonly domNode: HTMLElement; protected readonly actionsList: HTMLElement; private readonly _onDidBlur = this._register(new Emitter()); @@ -130,7 +132,7 @@ export class ActionBar extends Disposable implements IActionRunner { this._actionRunnerDisposables.add(this._actionRunner.onDidRun(e => this._onDidRun.fire(e))); this._actionRunnerDisposables.add(this._actionRunner.onWillRun(e => this._onWillRun.fire(e))); - this.viewItems = []; + this._viewItems = []; this.focusedItem = undefined; this.domNode = document.createElement('div'); @@ -380,10 +382,10 @@ export class ActionBar extends Disposable implements IActionRunner { if (index === null || index < 0 || index >= this.actionsList.children.length) { this.actionsList.appendChild(actionViewItemElement); - this.viewItems.push(item); + this._viewItems.push(item); } else { this.actionsList.insertBefore(actionViewItemElement, this.actionsList.children[index]); - this.viewItems.splice(index, 0, item); + this._viewItems.splice(index, 0, item); index++; } }); @@ -424,32 +426,18 @@ export class ActionBar extends Disposable implements IActionRunner { } getWidth(index: number): number { - if (index >= 0 && index < this.actionsList.children.length) { - const item = this.actionsList.children.item(index); - if (item) { - return item.clientWidth; - } - } - - return 0; + return this.actionsList.children.item(index)?.clientWidth ?? 0; } getHeight(index: number): number { - if (index >= 0 && index < this.actionsList.children.length) { - const item = this.actionsList.children.item(index); - if (item) { - return item.clientHeight; - } - } - - return 0; + return this.actionsList.children.item(index)?.clientHeight ?? 0; } pull(index: number): void { if (index >= 0 && index < this.viewItems.length) { this.actionsList.childNodes[index].remove(); this.viewItemDisposables.deleteAndDispose(this.viewItems[index]); - dispose(this.viewItems.splice(index, 1)); + dispose(this._viewItems.splice(index, 1)); this.refreshRole(); } } @@ -459,7 +447,7 @@ export class ActionBar extends Disposable implements IActionRunner { return; } - this.viewItems = dispose(this.viewItems); + this._viewItems = dispose(this._viewItems); this.viewItemDisposables.clearAndDisposeAll(); DOM.clearNode(this.actionsList); this.refreshRole(); @@ -623,7 +611,7 @@ export class ActionBar extends Disposable implements IActionRunner { override dispose(): void { this._context = undefined; - this.viewItems = dispose(this.viewItems); + this._viewItems = dispose(this._viewItems); this.getContainer().remove(); super.dispose(); } From 4aaee5198e750deb5e91e3d2634d1d379cc765b8 Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" <122617954+vs-code-engineering[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 07:46:28 +1000 Subject: [PATCH 29/32] [cherry-pick] fix: update presentation logic for ManageTodoListTool invocation (#311494) Co-authored-by: vs-code-engineering[bot] --- .../tools/builtinTools/manageTodoListTool.ts | 4 +- .../builtinTools/manageTodoListTool.test.ts | 90 ++++++++++++++++++- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts index 193755bde2345..8188530e13dfd 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts @@ -15,7 +15,8 @@ import { IToolResult, ToolDataSource, IToolInvocationPreparationContext, - IPreparedToolInvocation + IPreparedToolInvocation, + ToolInvocationPresentation } from '../languageModelToolsService.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; @@ -162,6 +163,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { return { invocationMessage, + presentation: items.length ? undefined : ToolInvocationPresentation.Hidden, pastTenseMessage: new MarkdownString(message ?? localize('todo.updatedList', "Updated todo list")), toolSpecificData: { kind: 'todoList', diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.ts index 5b8ad6895d5bf..cec8af2a4b187 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.ts @@ -4,9 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../../../base/common/event.js'; +import { URI } from '../../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; -import { createManageTodoListToolData } from '../../../../common/tools/builtinTools/manageTodoListTool.js'; -import { IToolData } from '../../../../common/tools/languageModelToolsService.js'; +import { NullLogService } from '../../../../../../../platform/log/common/log.js'; +import { NullTelemetryService } from '../../../../../../../platform/telemetry/common/telemetryUtils.js'; +import { createManageTodoListToolData, ManageTodoListTool } from '../../../../common/tools/builtinTools/manageTodoListTool.js'; +import { IChatTodo, IChatTodoListService } from '../../../../common/tools/chatTodoListService.js'; +import { IToolData, ToolInvocationPresentation } from '../../../../common/tools/languageModelToolsService.js'; import { IJSONSchema } from '../../../../../../../base/common/jsonSchema.js'; suite('ManageTodoListTool Schema', () => { @@ -59,3 +65,83 @@ suite('ManageTodoListTool Schema', () => { assert.deepStrictEqual(statusProperty.enum, ['not-started', 'in-progress', 'completed'], 'Status should have correct enum values'); }); }); + +suite('ManageTodoListTool prepareToolInvocation', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + const sessionResource = URI.parse('vscode-chat://session/1'); + + function createMockTodoListService(todos: IChatTodo[] = []): IChatTodoListService { + return { + _serviceBrand: undefined, + onDidUpdateTodos: Event.None, + getTodos: () => todos, + setTodos: () => { }, + migrateTodos: () => { }, + }; + } + + function createTool(todos: IChatTodo[] = []): ManageTodoListTool { + return store.add(new ManageTodoListTool( + createMockTodoListService(todos), + new NullLogService(), + NullTelemetryService, + )); + } + + test('presentation is Hidden when todoList param is empty', async () => { + const tool = createTool(); + const result = await tool.prepareToolInvocation({ + parameters: { todoList: [] }, + toolCallId: 'call-1', + chatSessionResource: sessionResource, + }, CancellationToken.None); + + assert.strictEqual(result?.presentation, ToolInvocationPresentation.Hidden); + }); + + test('presentation is undefined when todoList param has items', async () => { + const tool = createTool(); + const result = await tool.prepareToolInvocation({ + parameters: { + todoList: [{ id: 1, title: 'Task 1', status: 'not-started' }], + }, + toolCallId: 'call-1', + chatSessionResource: sessionResource, + }, CancellationToken.None); + + assert.strictEqual(result?.presentation, undefined); + }); + + test('presentation is Hidden for read operation with no existing todos', async () => { + const tool = createTool([]); + const result = await tool.prepareToolInvocation({ + parameters: { operation: 'read' }, + toolCallId: 'call-1', + chatSessionResource: sessionResource, + }, CancellationToken.None); + + assert.strictEqual(result?.presentation, ToolInvocationPresentation.Hidden); + }); + + test('presentation is undefined for read operation with existing todos', async () => { + const tool = createTool([{ id: 1, title: 'Existing', status: 'in-progress' }]); + const result = await tool.prepareToolInvocation({ + parameters: { operation: 'read' }, + toolCallId: 'call-1', + chatSessionResource: sessionResource, + }, CancellationToken.None); + + assert.strictEqual(result?.presentation, undefined); + }); + + test('returns undefined when no chatSessionResource is provided', async () => { + const tool = createTool(); + const result = await tool.prepareToolInvocation({ + parameters: { todoList: [{ id: 1, title: 'Task', status: 'not-started' }] }, + toolCallId: 'call-1', + chatSessionResource: undefined, + }, CancellationToken.None); + + assert.strictEqual(result, undefined); + }); +}); From 7382ed6438adb01d49205f001f8b458711bfb853 Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Mon, 20 Apr 2026 21:06:56 +0200 Subject: [PATCH 30/32] nes: refactor: simplify toInlineSuggestion Co-authored-by: Copilot --- .../vscode-node/isInlineSuggestion.spec.ts | 21 +++ .../vscode-node/isInlineSuggestion.ts | 124 +++++++++++------- 2 files changed, 100 insertions(+), 45 deletions(-) diff --git a/extensions/copilot/src/extension/inlineEdits/test/vscode-node/isInlineSuggestion.spec.ts b/extensions/copilot/src/extension/inlineEdits/test/vscode-node/isInlineSuggestion.spec.ts index 6ed0e69d3cb40..5bb09b87c47dd 100644 --- a/extensions/copilot/src/extension/inlineEdits/test/vscode-node/isInlineSuggestion.spec.ts +++ b/extensions/copilot/src/extension/inlineEdits/test/vscode-node/isInlineSuggestion.spec.ts @@ -644,4 +644,25 @@ const fieldLabels: Record = { assert.strictEqual(result!.newText, '\r\n'); }); }); + + suite('multi-line range, no common prefix', () => { + + // Regression: when commonLen === 0 and the replaced text starts with '\n', + // `lastIndexOf('\n', -1)` would (incorrectly) clamp to 0 and report a + // match, causing the leading newline to be stripped — which can collapse + // the multi-line range into a same-line "suggestion" that the function + // then accepts. With the original substring-based check, no strip occurs + // and the result is `undefined`. + test('does not strip leading newline when nothing is in common', () => { + const document = createMockDocument(['abc', 'x', 'rest']); + // replacedText = '\nx', newText[0]='Y' differs from '\n', commonLen=0. + const replaceRange = new Range(0, 3, 1, 1); + const cursorPosition = new Position(1, 1); + const replaceText = 'Yx'; + + // The range cannot legitimately be collapsed to a single line, so + // the function must not synthesize a ghost-text suggestion. + assert.isUndefined(toInlineSuggestion(cursorPosition, document, replaceRange, replaceText)); + }); + }); }); diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts index 12913941227ab..ad774a69994f5 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Position, Range, TextDocument } from 'vscode'; -import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange'; export interface InlineSuggestionEdit { readonly range: Range; @@ -17,73 +16,108 @@ export interface InlineSuggestionEdit { * which is required for VS Code to render ghost text. */ export function toInlineSuggestion(cursorPos: Position, doc: TextDocument, range: Range, newText: string, advanced: boolean = true): InlineSuggestionEdit | undefined { - // If multi line insertion starts on the next line - // All new lines have to be newly created lines - if (range.isEmpty && cursorPos.line + 1 === range.start.line && range.start.character === 0 - && doc.lineAt(cursorPos.line).text.length === cursorPos.character // cursor is at the end of the line - && (newText.endsWith('\n') || (newText.includes('\n') && doc.lineAt(range.end.line).text.length === range.end.character)) // no remaining content after insertion - ) { - // Use an empty range at the cursor so the suggestion is a pure insertion - const adjustedRange = new Range(cursorPos, cursorPos); - const textBetweenCursorAndRange = doc.getText(new Range(cursorPos, range.start)); - // The original range is on the next line, so the line terminator that - // already separates the cursor's line from range.start is preserved. - // Drop a single trailing line ending from newText (if present) to avoid - // inserting an extra blank line after the suggestion. Handle CRLF as - // well as LF so we don't leave a dangling '\r'. - const adjustedNewText = newText.replace(/\r?\n$/, ''); - return { range: adjustedRange, newText: textBetweenCursorAndRange + adjustedNewText }; + // Special case: a multi-line insertion that starts on the line *after* the cursor + // can be re-expressed as a pure insertion at the cursor. + const nextLineInsertion = tryAdjustNextLineInsertion(cursorPos, doc, range, newText); + if (nextLineInsertion) { + return nextLineInsertion; } - if (advanced) { - // If the range spans multiple lines, try to reduce it by stripping a common - // prefix (up to a newline boundary) from the replaced text and newText. - if (range.start.line !== range.end.line) { - const fullReplacedText = doc.getText(range); - let commonLen = 0; - const maxLen = Math.min(fullReplacedText.length, newText.length); - while (commonLen < maxLen && fullReplacedText[commonLen] === newText[commonLen]) { - commonLen++; - } - const lastNewline = fullReplacedText.substring(0, commonLen).lastIndexOf('\n'); - if (lastNewline >= 0) { - const strippedLen = lastNewline + 1; - newText = newText.substring(strippedLen); - const newStart = doc.positionAt(doc.offsetAt(range.start) + strippedLen); - range = new Range(newStart, range.end); - } - } + // If the range spans multiple lines, try to collapse it to a single line by + // trimming a shared prefix up to a newline boundary. + if (advanced && range.start.line !== range.end.line) { + ({ range, newText } = stripCommonLinePrefix(doc, range, newText)); } + // Ghost text requires the edit to be on the cursor's line. if (range.start.line !== range.end.line || range.start.line !== cursorPos.line) { return undefined; } - const cursorOffset = doc.offsetAt(cursorPos); - const offsetRange = new OffsetRange(doc.offsetAt(range.start), doc.offsetAt(range.end)); - - const replacedText = offsetRange.substring(doc.getText()); + return validateSameLineGhostText(cursorPos, doc, range, newText); +} - const cursorOffsetInReplacedText = cursorOffset - offsetRange.start; - if (cursorOffsetInReplacedText < 0) { +/** + * If the cursor is at the end of a line and the edit is an empty-range insertion + * at column 0 of the next line (with no leftover content after the insertion), + * rewrite it as a pure insertion at the cursor position. + */ +function tryAdjustNextLineInsertion(cursorPos: Position, doc: TextDocument, range: Range, newText: string): InlineSuggestionEdit | undefined { + if (!range.isEmpty) { + return undefined; + } + if (cursorPos.line + 1 !== range.start.line || range.start.character !== 0) { return undefined; } + if (doc.lineAt(cursorPos.line).text.length !== cursorPos.character) { + return undefined; // cursor is not at the end of the line + } - const textBeforeCursorIsEqual = replacedText.substring(0, cursorOffsetInReplacedText) === newText.substring(0, cursorOffsetInReplacedText); - if (!textBeforeCursorIsEqual) { + const targetLineFullyConsumed = doc.lineAt(range.end.line).text.length === range.end.character; + const noLeftoverAfterInsertion = newText.endsWith('\n') || (newText.includes('\n') && targetLineFullyConsumed); + if (!noLeftoverAfterInsertion) { return undefined; } + // Use an empty range at the cursor so the suggestion is a pure insertion. + // The original line terminator between the cursor and `range.start` is preserved + // in the document, so: + // - prepend that terminator to `newText` (it lives in the doc, not in the edit), and + // - drop a single trailing line ending from `newText` to avoid an extra blank line. + // CRLF-safe so we don't leak a dangling '\r' into the suggestion. + const lineBreak = doc.getText(new Range(cursorPos, range.start)); + const trimmedNewText = newText.replace(/\r?\n$/, ''); + return { range: new Range(cursorPos, cursorPos), newText: lineBreak + trimmedNewText }; +} + +/** + * Strip the longest shared prefix that ends on a newline boundary from both sides + * of a multi-line edit. This often shrinks the range so it fits on a single line, + * which is required for ghost text rendering. + */ +function stripCommonLinePrefix(doc: TextDocument, range: Range, newText: string): { range: Range; newText: string } { + const replacedText = doc.getText(range); + const maxLen = Math.min(replacedText.length, newText.length); + let commonLen = 0; + while (commonLen < maxLen && replacedText[commonLen] === newText[commonLen]) { + commonLen++; + } + if (commonLen === 0) { + return { range, newText }; + } + const lastNewline = replacedText.lastIndexOf('\n', commonLen - 1); + if (lastNewline < 0) { + return { range, newText }; + } + const strippedLen = lastNewline + 1; + const newStart = doc.positionAt(doc.offsetAt(range.start) + strippedLen); + return { range: new Range(newStart, range.end), newText: newText.substring(strippedLen) }; +} + +/** + * Validate that a single-line edit can be rendered as ghost text at the cursor: + * - the cursor is at or after `range.start` + * - everything before the cursor in the replaced text matches `newText` + * - the replaced text is a subword of `newText` (i.e. only insertions are needed) + */ +function validateSameLineGhostText(cursorPos: Position, doc: TextDocument, range: Range, newText: string): InlineSuggestionEdit | undefined { + const replacedText = doc.getText(range); + const cursorOffsetInReplacedText = cursorPos.character - range.start.character; + if (cursorOffsetInReplacedText < 0) { + return undefined; + } + if (replacedText.substring(0, cursorOffsetInReplacedText) !== newText.substring(0, cursorOffsetInReplacedText)) { + return undefined; + } if (!isSubword(replacedText, newText)) { return undefined; } - return { range, newText }; } + /** * a is subword of b if a can be obtained by removing characters from b */ - export function isSubword(a: string, b: string): boolean { for (let aIdx = 0, bIdx = 0; aIdx < a.length; bIdx++) { if (bIdx >= b.length) { From bc3fee1c7c9a7ac0bb910c2f55f8baa5cb5b9727 Mon Sep 17 00:00:00 2001 From: ulugbekna Date: Mon, 20 Apr 2026 23:44:36 +0200 Subject: [PATCH 31/32] update jsdoc Co-authored-by: Copilot --- .../inlineEdits/vscode-node/isInlineSuggestion.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts index ad774a69994f5..b1876d17e0352 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts @@ -39,8 +39,12 @@ export function toInlineSuggestion(cursorPos: Position, doc: TextDocument, range /** * If the cursor is at the end of a line and the edit is an empty-range insertion - * at column 0 of the next line (with no leftover content after the insertion), - * rewrite it as a pure insertion at the cursor position. + * at column 0 of the next line, rewrite it as a pure insertion at the cursor + * position. This is allowed when either: + * - `newText` ends with a newline (any existing content on the target line is + * pushed onto the following line), or + * - `newText` contains a newline and the target line is fully consumed by the + * insertion (no leftover content after the insertion). */ function tryAdjustNextLineInsertion(cursorPos: Position, doc: TextDocument, range: Range, newText: string): InlineSuggestionEdit | undefined { if (!range.isEmpty) { From d52c531dd37a84b06c82921c1fdd714d6a9fd222 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 21 Apr 2026 08:11:51 +1000 Subject: [PATCH 32/32] Enhance draft input persistence for remote chat sessions (#311312) * Enhance draft input persistence for remote chat sessions * Strip attachments from external session draft input state in metadata Agent-Logs-Url: https://github.com/microsoft/vscode/sessions/fdcea905-d6d1-42f5-8993-0ead68edb74c Co-authored-by: DonJayamanne <1948812+DonJayamanne@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../contrib/chat/browser/widget/chatWidget.ts | 3 ++ .../browser/widget/input/chatInputPart.ts | 11 +++++++ .../common/chatService/chatServiceImpl.ts | 33 ++++++++++++++++--- .../chat/common/model/chatSessionStore.ts | 21 ++++++++++-- .../common/chatService/chatService.test.ts | 27 +++++++++++++++ 5 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 719d50a08e8d0..20951ea08e619 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -1973,6 +1973,9 @@ export class ChatWidget extends Disposable implements IChatWidget { } if (!model) { + // Flush any unsent draft to the outgoing input model before we drop our + // reference to it, so the host's `willDisposeModel` persistence sees it. + this.inputPart.flushInputStateToModel(); if (this.viewModel?.editing) { this.finishedEditing(); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index c9b5c092c328b..7ece11c1e336b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -1076,6 +1076,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge queueMicrotask(() => this.inputActionsToolbar?.relayout()); } + /** + * Flush the current input state to the bound input model. Use this before + * the host releases its model reference (e.g. on session switch) to ensure + * an unsent draft is captured by `willDisposeModel` persistence. + */ + public flushInputStateToModel(): void { + if (this._inputModel) { + this._syncInputStateToModel(); + } + } + public setCurrentLanguageModel(model: ILanguageModelChatMetadataAndIdentifier) { this._currentLanguageModel.set(model, undefined); diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 9c5c22961fb2a..2f93372484952 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -62,6 +62,22 @@ import { ChatMode } from '../chatModes.js'; const serializedChatKey = 'interactive.sessions'; +/** + * True when the user has typed text or attached non-trivial context to the input + * but not yet sent it. Used to decide whether an external session needs metadata + * persisted on dispose so the draft survives switching sessions. + */ +function hasDraftInput(model: ChatModel): boolean { + const state = model.inputModel.state.get(); + if (!state) { + return false; + } + if (state.inputText.trim().length > 0) { + return true; + } + return state.attachments.length > 0; +} + class CancellableRequest implements IDisposable { private readonly _yieldRequested: ISettableObservable = observableValue(this, false); @@ -187,7 +203,9 @@ export class ChatService extends Disposable implements IChatService { } else if (this._saveModelsEnabled) { await this._chatSessionStore.storeSessions([model]); } - } else if (!localSessionId && model.getRequests().length > 0) { + } else if (!localSessionId && (model.getRequests().length > 0 || hasDraftInput(model))) { + // External sessions: persist metadata when there are requests, OR when the + // user has typed/attached unsent input we need to restore on next open. await this._chatSessionStore.storeSessionsMetadataOnly([model]); } } @@ -575,7 +593,9 @@ export class ChatService extends Disposable implements IChatService { const chatSessionType = getChatSessionType(sessionResource); const modelId = findLast(providedSession.history.filter(m => m.type === 'request'), req => req.modelId)?.modelId; const agentUri = findLast(providedSession.history.filter(m => m.type === 'request'), req => req.modeInstructions?.uri)?.modeInstructions?.uri; - const storedPermissionLevel = this._chatSessionStore.getMetadataForSessionSync(sessionResource)?.permissionLevel; + const storedMetadata = this._chatSessionStore.getMetadataForSessionSync(sessionResource); + const storedPermissionLevel = storedMetadata?.permissionLevel; + const storedInputState = storedMetadata?.inputState; let initialData: ISerializedChatDataReference | undefined = undefined; if ((modelId || agentUri)) { const mode: ISerializableChatModelInputState['mode'] = agentUri ? { kind: ChatModeKind.Agent, id: agentUri.toString() } : { kind: ChatModeKind.Agent, id: ChatMode.Agent.id }; @@ -607,18 +627,21 @@ export class ChatService extends Disposable implements IChatService { }; } - // Contributed sessions do not use UI tools + // Contributed sessions do not use UI tools. + // Prefer (in order): a transferred draft, a persisted draft from metadata, + // otherwise let the constructor fall back to initialData.value.inputState. const modelRef = this._sessionModels.acquireOrCreate({ initialData, location, sessionResource: sessionResource, canUseTools: false, transferEditingSession: providedSession.transferredState?.editingSession, - inputState: providedSession.transferredState?.inputState, + inputState: providedSession.transferredState?.inputState ?? storedInputState, }, debugOwner ?? 'ChatService#loadRemoteSession'); // Restore permission level from metadata even when initialData was not constructed - if (storedPermissionLevel && !initialData) { + // and no inputState carried it through. + if (storedPermissionLevel && !initialData && !storedInputState) { modelRef.object.inputModel.setState({ permissionLevel: storedPermissionLevel }); } diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts index be5e704e3feaa..4f52965588018 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts @@ -29,7 +29,7 @@ import { awaitStatsForSession } from '../chat.js'; import { IChatSessionStats, IChatSessionTiming, ResponseModelState } from '../chatService/chatService.js'; import { ChatAgentLocation, ChatPermissionLevel } from '../constants.js'; import { ModifiedFileEntryState } from '../editing/chatEditingService.js'; -import { ChatModel, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, ISerializedChatDataReference, normalizeSerializableChatData } from './chatModel.js'; +import { ChatModel, ISerializableChatData, ISerializableChatDataIn, ISerializableChatModelInputState, ISerializableChatsData, ISerializedChatDataReference, normalizeSerializableChatData } from './chatModel.js'; import { ChatSessionOperationLog } from './chatSessionOperationLog.js'; import { LocalChatSessionUri } from './chatUri.js'; @@ -758,6 +758,13 @@ export interface IChatSessionEntryMetadata { * The permission level for tool auto-approval, if not default. */ permissionLevel?: ChatPermissionLevel; + + /** + * Serialized draft input state (text, attachments, mode, selected model, ...) for + * external sessions, so that unsent input is preserved when switching away and + * back. Local sessions instead persist their full state via storeSessions. + */ + inputState?: ISerializableChatModelInputState; } function isChatSessionEntryMetadata(obj: unknown): obj is IChatSessionEntryMetadata { @@ -831,6 +838,15 @@ async function getSessionMetadata(session: ChatModel | ISerializableChatData): P lastResponseState = ResponseModelState.Cancelled; } + const isExternal = session instanceof ChatModel && !LocalChatSessionUri.parseLocalSessionId(session.sessionResource); + // Persist draft input state only for external sessions; local sessions already + // have their full state serialized via storeSessions, so duplicating here would + // be wasteful and risk drift between the two locations. + // Attachments are excluded because they can contain large binary payloads + // (e.g. base64-encoded images) that would bloat the session index entry. + const rawInputState = isExternal ? (session as ChatModel).inputModel.toJSON() : undefined; + const inputState = rawInputState ? { ...rawInputState, attachments: [] } : undefined; + return { sessionId: session.sessionId, title: title || localize('newChat', "New Chat"), @@ -840,9 +856,10 @@ async function getSessionMetadata(session: ChatModel | ISerializableChatData): P hasPendingEdits: session instanceof ChatModel ? (session.editingSession?.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified)) : false, isEmpty: session instanceof ChatModel ? session.getRequests().length === 0 : session.requests.length === 0, stats, - isExternal: session instanceof ChatModel && !LocalChatSessionUri.parseLocalSessionId(session.sessionResource), + isExternal, lastResponseState, permissionLevel: session instanceof ChatModel ? session.inputModel.state.get()?.permissionLevel : undefined, + inputState, }; } diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts index 8302030f1340a..f8fc406fd5cf5 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts @@ -1487,6 +1487,33 @@ suite('ChatService', () => { const model = ref.object as ChatModel; assert.strictEqual(model.lastRequest?.response?.isComplete, true, 'Non-streaming session should complete response at load time'); }); + + test('draft input is restored after disposing and reloading a remote session', async () => { + const { resource } = setupRemoteProvider({ history: [] }); + + const testService = createChatService(); + + // Load the session and seed an unsent draft on its inputModel. + const ref1 = await testService.acquireOrLoadSession(resource, ChatAgentLocation.Chat, CancellationToken.None); + assert.ok(ref1, 'Should load remote session'); + const model1 = ref1.object as ChatModel; + model1.inputModel.setState({ + inputText: 'unsent draft', + selections: [{ selectionStartLineNumber: 1, selectionStartColumn: 1, positionLineNumber: 1, positionColumn: 12 }], + }); + + // Release the only reference -> willDisposeModel runs and persists metadata. + ref1.dispose(); + await testService.waitForModelDisposals(); + + // Reload the same session. The draft must be restored from metadata. + const ref2 = await testService.acquireOrLoadSession(resource, ChatAgentLocation.Chat, CancellationToken.None); + assert.ok(ref2, 'Should re-load remote session'); + testDisposables.add(ref2); + const model2 = ref2.object as ChatModel; + const restored = model2.inputModel.state.get(); + assert.strictEqual(restored?.inputText, 'unsent draft', 'Input text should be restored'); + }); }); });