From 995204f4e435ae906214ca84b97674cdf6edb693 Mon Sep 17 00:00:00 2001 From: Brian Jenkins Date: Mon, 28 Sep 2026 01:13:48 -0400 Subject: [PATCH 1/6] refactor: stop loading chat and notebook implementations from core Search, terminal decorations, SCM, tasks, getting started and the extension host only needed a type, a lazily-used class, a command/tool id or a small helper from chat/notebook, but imported them from modules that load most of the chat implementation or the notebook editor. The same goes for the missing CustomizationHarnessService, which only needed a descriptor. Co-Authored-By: Claude Opus 5.5 --- src/missing-services.ts | 13 +- ...untime-dependencies-from-core-on-cha.patch | 383 ++++++++++++++++++ 2 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 vscode-patches/0109-refactor-avoid-runtime-dependencies-from-core-on-cha.patch diff --git a/src/missing-services.ts b/src/missing-services.ts index 2a67f876..0152774a 100644 --- a/src/missing-services.ts +++ b/src/missing-services.ts @@ -543,7 +543,7 @@ import { IChatInputNotificationService } from 'vs/workbench/contrib/chat/browser import { IChatPhoneInputPresenter } from 'vs/workbench/contrib/chat/browser/widget/input/chatPhoneInputPresenter.service' import { IChatImageCarouselService } from 'vs/workbench/contrib/chat/browser/chatImageCarouselService.service.js' import { IChatDebugService } from 'vs/workbench/contrib/chat/common/chatDebugService.service.js' -import { createVSCodeHarnessDescriptor } from 'vs/workbench/contrib/chat/common/customizationHarnessService' +import type { IHarnessDescriptor } from 'vs/workbench/contrib/chat/common/customizationHarnessService' import { ICustomizationHarnessService } from 'vs/workbench/contrib/chat/common/customizationHarnessService.service' import type { IEnablementModel } from 'vs/workbench/contrib/chat/common/enablement.js' import { IPluginGitService } from 'vs/workbench/contrib/chat/common/plugins/pluginGitService.service.js' @@ -7270,8 +7270,15 @@ class CustomizationHarnessService implements ICustomizationHarnessService { URI.parse('chat-session://local/default') ) activeHarness: ICustomizationHarnessService['activeHarness'] = constObservable(SessionType.Local) - availableHarnesses: ICustomizationHarnessService['availableHarnesses'] = constObservable([ - createVSCodeHarnessDescriptor() + // Not using `createVSCodeHarnessDescriptor` on purpose: importing it loads most of the chat implementation + availableHarnesses: ICustomizationHarnessService['availableHarnesses'] = constObservable< + IHarnessDescriptor[] + >([ + { + id: SessionType.Local, + label: 'Local', + icon: Codicon.vm + } ]) onDidChangeSlashCommands: ICustomizationHarnessService['onDidChangeSlashCommands'] = Event.None onDidChangeCustomAgents: ICustomizationHarnessService['onDidChangeCustomAgents'] = Event.None diff --git a/vscode-patches/0109-refactor-avoid-runtime-dependencies-from-core-on-cha.patch b/vscode-patches/0109-refactor-avoid-runtime-dependencies-from-core-on-cha.patch new file mode 100644 index 00000000..6e061657 --- /dev/null +++ b/vscode-patches/0109-refactor-avoid-runtime-dependencies-from-core-on-cha.patch @@ -0,0 +1,383 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Brian Jenkins +Date: Mon, 28 Sep 2026 00:29:33 -0400 +Subject: [PATCH] refactor: avoid runtime dependencies from core on chat and + notebook implementations + +Search, terminal decorations, SCM, tasks, getting started and the extension +host only needed a type, a lazily-used class, a command/tool id or a small +helper from chat/notebook, but imported them from modules that load the whole +chat implementation or notebook editor, which made them part of every +workbench, even without the chat/notebook service overrides. + +Co-Authored-By: Claude Opus 5.5 +--- + .../api/common/extHostLanguageModelTools.ts | 3 +- + .../api/common/extHostTypeConverters.ts | 2 +- + .../chat/browser/actions/chatConstants.ts | 1 + + .../contrib/chat/common/chatImageBuffer.ts | 34 +++++++++++++++++++ + .../chat/common/chatImageExtraction.ts | 31 ++--------------- + .../common/tools/builtinTools/editFileTool.ts | 5 +-- + .../chat/common/tools/builtinTools/toolIds.ts | 9 +++++ + .../chat/common/tools/builtinTools/tools.ts | 2 +- + .../contrib/scm/browser/scm.contribution.ts | 2 +- + .../workbench/contrib/scm/browser/scmInput.ts | 2 +- + .../browser/searchTreeModel/searchResult.ts | 8 ++--- + .../contrib/search/browser/searchView.ts | 9 +++-- + .../tasks/browser/abstractTaskService.ts | 2 +- + .../terminal/browser/xterm/decorationAddon.ts | 4 ++- + .../browser/agentSessionsWelcome.ts | 4 +-- + .../browser/agentSessionsWelcomeInput.ts | 1 + + .../browser/gettingStarted.contribution.ts | 4 +-- + 17 files changed, 74 insertions(+), 49 deletions(-) + create mode 100644 src/vs/workbench/contrib/chat/common/chatImageBuffer.ts + create mode 100644 src/vs/workbench/contrib/chat/common/tools/builtinTools/toolIds.ts + +diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts +index f34d76c9..782d83e6 100644 +--- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts ++++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts +@@ -13,8 +13,7 @@ import { generateUuid } from '../../../base/common/uuid.js'; + import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; + import { IPreparedToolInvocation, IStreamedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolInvocationPreparationContext, IToolInvocationStreamContext, IToolResult, ToolInvocationPresentation } from '../../contrib/chat/common/tools/languageModelToolsService.js'; + import { computeCombinationKey } from '../../contrib/chat/common/tools/languageModelToolsConfirmationService.js'; +-import { ExtensionEditToolId, InternalEditToolId } from '../../contrib/chat/common/tools/builtinTools/editFileTool.js'; +-import { InternalFetchWebPageToolId } from '../../contrib/chat/common/tools/builtinTools/tools.js'; ++import { ExtensionEditToolId, InternalEditToolId, InternalFetchWebPageToolId } from '../../contrib/chat/common/tools/builtinTools/toolIds.js'; + import { SearchExtensionsToolId } from '../../contrib/extensions/common/searchExtensionsTool.js'; + import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; + import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; +diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts +index b3e6860b..36aa3bf9 100644 +--- a/src/vs/workbench/api/common/extHostTypeConverters.ts ++++ b/src/vs/workbench/api/common/extHostTypeConverters.ts +@@ -46,7 +46,7 @@ import { IChatRequestModeInstructions } from '../../contrib/chat/common/model/ch + import { IChatAgentMarkdownContentWithVulnerability, IChatAutoModeResolutionPart, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatExternalToolInvocationUpdate, IChatFollowup, IChatHookPart, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffDataSerialized, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTerminalToolInvocationData, IChatTextEdit, IChatThinkingPart, IChatToolInvocationSerialized, IChatTreeData, IChatUserActionEvent, IChatVoiceProgressPart, IChatWarningMessage, IChatInfoMessage, IChatWorkspaceEdit } from '../../contrib/chat/common/chatService/chatService.js'; + import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js'; + import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; +-import { coerceImageBuffer } from '../../contrib/chat/common/chatImageExtraction.js'; ++import { coerceImageBuffer } from '../../contrib/chat/common/chatImageBuffer.js'; + import { ChatSessionStatus, IChatSessionItem } from '../../contrib/chat/common/chatSessionsService.js'; + import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; + import { ChatRequestHooks, resolveEffectiveCommand } from '../../contrib/chat/common/promptSyntax/hookSchema.js'; +diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatConstants.ts b/src/vs/workbench/contrib/chat/browser/actions/chatConstants.ts +index 17e137b5..71002004 100644 +--- a/src/vs/workbench/contrib/chat/browser/actions/chatConstants.ts ++++ b/src/vs/workbench/contrib/chat/browser/actions/chatConstants.ts +@@ -6,6 +6,7 @@ import { localize2 } from '../../../../../nls.js'; + + export const CHAT_CATEGORY = localize2('chat.category', 'Chat'); + export const CHAT_OPEN_ACTION_ID = 'workbench.action.chat.open'; ++export const CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID = 'workbench.action.chat.triggerSetupSupportAnonymousAction'; + export const ASK_QUICK_QUESTION_ACTION_ID = 'workbench.action.quickchat.toggle'; + export const AcceptToolConfirmationActionId = 'workbench.action.chat.acceptTool'; + export const SkipToolConfirmationActionId = 'workbench.action.chat.skipTool'; +diff --git a/src/vs/workbench/contrib/chat/common/chatImageBuffer.ts b/src/vs/workbench/contrib/chat/common/chatImageBuffer.ts +new file mode 100644 +index 00000000..4d0b845d +--- /dev/null ++++ b/src/vs/workbench/contrib/chat/common/chatImageBuffer.ts +@@ -0,0 +1,34 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright (c) Microsoft Corporation. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++// Kept in its own dependency-free module so that code outside of chat (e.g. extHostTypeConverters) ++// can use it without loading the chat model ++export function coerceImageBuffer(value: unknown): Uint8Array | undefined { ++ if (value instanceof Uint8Array) { ++ return value; ++ } ++ if (value instanceof ArrayBuffer) { ++ return new Uint8Array(value); ++ } ++ if (!value || typeof value !== 'object' || Array.isArray(value)) { ++ return undefined; ++ } ++ ++ const record = value as Record; ++ const keys = Object.keys(record).sort((a, b) => Number(a) - Number(b)); ++ if (keys.length === 0) { ++ return undefined; ++ } ++ ++ const result = new Uint8Array(keys.length); ++ for (let index = 0; index < keys.length; index++) { ++ const byte = record[keys[index]]; ++ if (keys[index] !== String(index) || typeof byte !== 'number' || !Number.isInteger(byte) || byte < 0 || byte > 255) { ++ return undefined; ++ } ++ result[index] = byte; ++ } ++ return result; ++} +diff --git a/src/vs/workbench/contrib/chat/common/chatImageExtraction.ts b/src/vs/workbench/contrib/chat/common/chatImageExtraction.ts +index cc289ef3..d0ae0f2d 100644 +--- a/src/vs/workbench/contrib/chat/common/chatImageExtraction.ts ++++ b/src/vs/workbench/contrib/chat/common/chatImageExtraction.ts +@@ -14,6 +14,9 @@ import { ChatResponseResource } from './model/chatModel.js'; + import { IChatContentInlineReference, IChatToolInvocation, IChatToolInvocationSerialized, IToolResultOutputDetailsSerialized } from './chatService/chatService.js'; + import { isToolResultInputOutputDetails, isToolResultOutputDetails, IToolResultOutputDetails } from './tools/languageModelToolsService.js'; + import { getExplicitFileOrImageAttachmentSummary, type IChatRequestVariableEntry, isImageVariableEntry } from './attachments/chatVariableEntries.js'; ++import { coerceImageBuffer } from './chatImageBuffer.js'; ++ ++export { coerceImageBuffer }; + + export interface IChatExtractedImage { + readonly id: string; +@@ -188,34 +191,6 @@ async function extractImageFromInlineReference( + }; + } + +-export function coerceImageBuffer(value: unknown): Uint8Array | undefined { +- if (value instanceof Uint8Array) { +- return value; +- } +- if (value instanceof ArrayBuffer) { +- return new Uint8Array(value); +- } +- if (!value || typeof value !== 'object' || Array.isArray(value)) { +- return undefined; +- } +- +- const record = value as Record; +- const keys = Object.keys(record).sort((a, b) => Number(a) - Number(b)); +- if (keys.length === 0) { +- return undefined; +- } +- +- const result = new Uint8Array(keys.length); +- for (let index = 0; index < keys.length; index++) { +- const byte = record[keys[index]]; +- if (keys[index] !== String(index) || typeof byte !== 'number' || !Number.isInteger(byte) || byte < 0 || byte > 255) { +- return undefined; +- } +- result[index] = byte; +- } +- return result; +-} +- + /** + * Extract images from a chat request's variable attachments (user-attached images). + */ +diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/editFileTool.ts +index addfc96f..fc33b652 100644 +--- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/editFileTool.ts ++++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/editFileTool.ts +@@ -15,9 +15,10 @@ import { ICodeMapperService } from '../../editing/chatCodeMapperService.js'; + import { ChatModel } from '../../model/chatModel.js'; + import { IChatService } from '../../chatService/chatService.js'; + import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../languageModelToolsService.js'; ++import { InternalEditToolId } from './toolIds.js'; ++ ++export { ExtensionEditToolId, InternalEditToolId } from './toolIds.js'; + +-export const ExtensionEditToolId = 'vscode_editFile'; +-export const InternalEditToolId = 'vscode_editFile_internal'; + export const EditToolData: IToolData = { + id: InternalEditToolId, + displayName: '', // not used +diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/toolIds.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/toolIds.ts +new file mode 100644 +index 00000000..91daae1f +--- /dev/null ++++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/toolIds.ts +@@ -0,0 +1,9 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright (c) Microsoft Corporation. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++// Kept in a dependency-free module so that the extension host can use them without loading the tools ++export const ExtensionEditToolId = 'vscode_editFile'; ++export const InternalEditToolId = 'vscode_editFile_internal'; ++export const InternalFetchWebPageToolId = 'vscode_fetchWebPage_internal'; +diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +index 4a711c40..bec1c893 100644 +--- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts ++++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +@@ -104,4 +104,4 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo + } + } + +-export const InternalFetchWebPageToolId = 'vscode_fetchWebPage_internal'; ++export { InternalFetchWebPageToolId } from './toolIds.js'; +diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +index 6ee2a32c..a5cba418 100644 +--- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts ++++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +@@ -40,7 +40,7 @@ import { SCMAccessibilityHelp } from './scmAccessibilityHelp.js'; + import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; + import { SCMHistoryItemContextContribution } from './scmHistoryChatContext.js'; + import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +-import { CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../../chat/browser/actions/chatActions.js'; ++import { CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../../chat/browser/actions/chatConstants.js'; + import { SCMInputContextKeys } from './scmInput.js'; + import product from '../../../../platform/product/common/product.js'; + +diff --git a/src/vs/workbench/contrib/scm/browser/scmInput.ts b/src/vs/workbench/contrib/scm/browser/scmInput.ts +index 63642985..66846d74 100644 +--- a/src/vs/workbench/contrib/scm/browser/scmInput.ts ++++ b/src/vs/workbench/contrib/scm/browser/scmInput.ts +@@ -70,7 +70,7 @@ import { IAccessibilityService } from '../../../../platform/accessibility/common + import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; + import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; + import product from '../../../../platform/product/common/product.js'; +-import { CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../../chat/browser/actions/chatActions.js'; ++import { CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../../chat/browser/actions/chatConstants.js'; + + export const SCMInputContextKeys = { + SCMInputHasValidationMessage: new RawContextKey('scmInputHasValidationMessage', false), +diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts +index c55c9aa6..7cba78cd 100644 +--- a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts ++++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts +@@ -10,7 +10,7 @@ import { ITextModel } from '../../../../../editor/common/model.js'; + import { IModelService } from '../../../../../editor/common/services/model.js'; + import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; + import { IProgress, IProgressStep } from '../../../../../platform/progress/common/progress.js'; +-import { NotebookEditorWidget } from '../../../notebook/browser/notebookEditorWidget.js'; ++import type { NotebookEditorWidget } from '../../../notebook/browser/notebookEditorWidget.js'; + import { INotebookEditorService } from '../../../notebook/browser/services/notebookEditorService.js'; + import { IAITextQuery, IFileMatch, ISearchComplete, ITextQuery, QueryType } from '../../../../services/search/common/search.js'; + import { arrayContainsElementOrParent, IChangeEvent, ISearchTreeFileMatch, ISearchTreeFolderMatch, IPlainTextSearchHeading, ISearchModel, ISearchResult, isSearchTreeFileMatch, isSearchTreeFolderMatch, isSearchTreeFolderMatchNoRoot, isSearchTreeFolderMatchWithResource, isSearchTreeMatch, isTextSearchHeading, ITextSearchHeading, mergeSearchResultEvents, RenderableMatch, SEARCH_RESULT_PREFIX } from './searchTreeCommon.js'; +@@ -48,9 +48,9 @@ export class SearchResultImpl extends Disposable implements ISearchResult { + this._register(this.modelService.onModelAdded(model => this.onModelAdded(model))); + + this._register(this.notebookEditorService.onDidAddNotebookEditor(widget => { +- if (widget instanceof NotebookEditorWidget) { +- this.onDidAddNotebookEditorWidget(widget); +- } ++ // Only NotebookEditorWidget registers itself with the notebook editor service, so a type-only cast is ++ // enough here and avoids a runtime dependency on the whole notebook editor implementation ++ this.onDidAddNotebookEditorWidget(widget); + })); + + this._id = SEARCH_RESULT_PREFIX + Date.now().toString(); +diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts +index cd44270e..9f200dd3 100644 +--- a/src/vs/workbench/contrib/search/browser/searchView.ts ++++ b/src/vs/workbench/contrib/search/browser/searchView.ts +@@ -55,7 +55,8 @@ import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPan + import { IEditorPane } from '../../../common/editor.js'; + import { Memento } from '../../../common/memento.js'; + import { IViewDescriptorService } from '../../../common/views.js'; +-import { NotebookEditor } from '../../notebook/browser/notebookEditor.js'; ++import type { NotebookEditor } from '../../notebook/browser/notebookEditor.js'; ++import { NOTEBOOK_EDITOR_ID } from '../../notebook/common/notebookCommon.js'; + import { ExcludePatternInputWidget, IncludePatternInputWidget } from './patternInputWidget.js'; + import { IFindInFilesArgs } from './searchActionsBase.js'; + import { searchDetailsIcon } from './searchIcons.js'; +@@ -2269,13 +2270,15 @@ export class SearchView extends ViewPane { + return; + } + +- if (editor instanceof NotebookEditor) { ++ // Checking the editor id rather than using `instanceof` avoids a runtime dependency on the notebook editor ++ if (editor?.getId() === NOTEBOOK_EDITOR_ID) { ++ const notebookEditor = editor as NotebookEditor; + const elemParent = element.parent() as INotebookFileInstanceMatch; + if (isSearchTreeMatch(element)) { + if (isIMatchInNotebook(element)) { + element.parent().showMatch(element); + } else { +- const editorWidget = editor.getControl(); ++ const editorWidget = notebookEditor.getControl(); + if (editorWidget) { + // Ensure that the editor widget is binded. If if is, then this should return immediately. + // Otherwise, it will bind the widget. +diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +index 129e228a..6c4ba470 100644 +--- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts ++++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +@@ -85,7 +85,7 @@ import { IPathService } from '../../../services/path/common/pathService.js'; + import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; + import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; + import { IViewsService } from '../../../services/views/common/viewsService.js'; +-import { CHAT_OPEN_ACTION_ID } from '../../chat/browser/actions/chatActions.js'; ++import { CHAT_OPEN_ACTION_ID } from '../../chat/browser/actions/chatConstants.js'; + import { IChatAgentService } from '../../chat/common/participants/chatAgents.js'; + import { IChatService } from '../../chat/common/chatService/chatService.js'; + import { configureTaskIcon, isWorkspaceFolder, ITaskQuickPickEntry, QUICKOPEN_DETAIL_CONFIG, QUICKOPEN_SKIP_CONFIG, TaskQuickPick } from './taskQuickPick.js'; +diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +index 0294c9fa..a7edf1ca 100644 +--- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts ++++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +@@ -29,7 +29,7 @@ import { MarkdownString } from '../../../../../base/common/htmlContent.js'; + import { IChatContextPickService } from '../../../chat/browser/attachments/chatContextPickService.js'; + import { IChatWidgetService } from '../../../chat/browser/chat.js'; + import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +-import { TerminalContext } from '../../../chat/browser/actions/chatContext.js'; ++import type { TerminalContext } from '../../../chat/browser/actions/chatContext.js'; + import { getTerminalUri, parseTerminalUri } from '../terminalUri.js'; + import { URI } from '../../../../../base/common/uri.js'; + import { ChatAgentLocation } from '../../../chat/common/constants.js'; +@@ -547,6 +547,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco + let terminalContext: TerminalContext | undefined; + if (this._resource) { + const parsedUri = parseTerminalUri(this._resource); ++ // Loaded lazily: the chat context implementation is only needed once a command is attached to chat ++ const { TerminalContext } = await import('../../../chat/browser/actions/chatContext.js'); + terminalContext = this._instantiationService.createInstance(TerminalContext, getTerminalUri(parsedUri.workspaceId, parsedUri.instanceId!, undefined, command.id)); + } + +diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +index e31e406c..3ef69f71 100644 +--- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts ++++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +@@ -42,7 +42,7 @@ import { ACTION_ID_NEW_CHAT } from '../../chat/browser/actions/chatActions.js'; + import { IAgentSessionsService } from '../../chat/browser/agentSessions/agentSessionsService.js'; + import { AgentSessionProviders, AgentSessionTarget } from '../../chat/browser/agentSessions/agentSessions.js'; + import { IAgentSession } from '../../chat/browser/agentSessions/agentSessionsModel.js'; +-import { AgentSessionsWelcomeEditorOptions, AgentSessionsWelcomeInput, AgentSessionsWelcomeWorkspaceKind } from './agentSessionsWelcomeInput.js'; ++import { AgentSessionsWelcomeEditorOptions, AgentSessionsWelcomeInput, AgentSessionsWelcomeWorkspaceKind, agentSessionsWelcomeCommandId } from './agentSessionsWelcomeInput.js'; + import { IChatService } from '../../chat/common/chatService/chatService.js'; + import { IChatModel } from '../../chat/common/model/chatModel.js'; + import { ChatViewId, IChatWidgetService, ISessionTypePickerDelegate, IWorkspacePickerDelegate, IWorkspacePickerItem } from '../../chat/browser/chat.js'; +@@ -129,7 +129,7 @@ type AgentSessionsWelcomeActionEvent = { + export class AgentSessionsWelcomePage extends EditorPane { + + static readonly ID = 'agentSessionsWelcomePage'; +- static readonly COMMAND_ID = 'workbench.action.openAgentSessionsWelcome'; ++ static readonly COMMAND_ID = agentSessionsWelcomeCommandId; + + private container!: HTMLElement; + private contentContainer!: HTMLElement; +diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcomeInput.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcomeInput.ts +index 4057dedf..de88c08b 100644 +--- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcomeInput.ts ++++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcomeInput.ts +@@ -11,6 +11,7 @@ import { IUntypedEditorInput } from '../../../common/editor.js'; + import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; + + export const agentSessionsWelcomeInputTypeId = 'workbench.editors.agentSessionsWelcomeInput'; ++export const agentSessionsWelcomeCommandId = 'workbench.action.openAgentSessionsWelcome'; + export type AgentSessionsWelcomeInitiator = 'startup' | 'command'; + export type AgentSessionsWelcomeWorkspaceKind = 'empty' | 'folder' | 'workspace'; + +diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +index f90937e6..0b9fb3a1 100644 +--- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts ++++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +@@ -32,7 +32,7 @@ import { Categories } from '../../../../platform/action/common/actionCommonCateg + import { DisposableStore } from '../../../../base/common/lifecycle.js'; + import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; + import { GettingStartedAccessibleView } from './gettingStartedAccessibleView.js'; +-import { AgentSessionsWelcomePage } from '../../welcomeAgentSessions/browser/agentSessionsWelcome.js'; ++import { agentSessionsWelcomeCommandId } from '../../welcomeAgentSessions/browser/agentSessionsWelcomeInput.js'; + import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; + import './gettingStartedService._contribution.js'; + +@@ -72,7 +72,7 @@ registerAction2(class extends Action2 { + + // If no specific walkthrough is requested and agent sessions welcome is preferred, open that instead + if (!walkthroughID && !chatEntitlementService.sentiment.hidden && configurationService.getValue('workbench.startupEditor') === 'agentSessionsWelcomePage') { +- commandService.executeCommand(AgentSessionsWelcomePage.COMMAND_ID); ++ commandService.executeCommand(agentSessionsWelcomeCommandId); + return; + } else { + if (walkthroughID) { +-- +2.39.5 (Apple Git-154) + From 07ec0978c76cc641e556417c6c79c6dd264af9a2 Mon Sep 17 00:00:00 2001 From: Brian Jenkins Date: Mon, 28 Sep 2026 01:13:48 -0400 Subject: [PATCH 2/6] feat: only load chat/notebook/mcp extension host participants with their service override The extension service override was loading every main thread participant, including the chat, notebook and mcp ones along with their implementation. They are now loaded by their own service override. As the extension host requires every main thread participant to be registered, an inert implementation is registered for the missing ones. Co-Authored-By: Claude Opus 5.5 --- src/service-override/chat.ts | 1 + src/service-override/extensions.ts | 3 +- src/service-override/mcp.ts | 1 + src/service-override/notebook.ts | 1 + .../tools/inertMainThreadCustomer.ts | 54 ++++ ...xtension-host-contribution-per-featu.patch | 286 ++++++++++++++++++ 6 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 src/service-override/tools/inertMainThreadCustomer.ts create mode 100644 vscode-patches/0110-refactor-split-extension-host-contribution-per-featu.patch diff --git a/src/service-override/chat.ts b/src/service-override/chat.ts index f5a22709..b046dc11 100644 --- a/src/service-override/chat.ts +++ b/src/service-override/chat.ts @@ -262,6 +262,7 @@ import 'vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSession import 'vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution' import 'vs/workbench/contrib/chat/browser/attachments/chatReferenceAttachmentWidget.contribution' import 'vs/workbench/contrib/chat/browser/attachments/transcriptContextAttachmentWidget.contribution' +import 'vs/workbench/api/browser/extensionHost.chat.contribution' class DefaultAccountService implements IDefaultAccountService { declare _serviceBrand: undefined diff --git a/src/service-override/extensions.ts b/src/service-override/extensions.ts index ad4cc155..cccc46b7 100644 --- a/src/service-override/extensions.ts +++ b/src/service-override/extensions.ts @@ -69,7 +69,8 @@ import { } from 'vs/platform/extensionManagement/common/extensionsScannerService.js' import * as platform from 'vs/base/common/platform' import { getBuiltInExtensionTranslationsUris } from '../l10n.js' -import 'vs/workbench/api/browser/extensionHost.contribution' +import 'vs/workbench/api/browser/extensionHost.base.contribution' +import './tools/inertMainThreadCustomer.js' export interface WorkerConfig { url: string diff --git a/src/service-override/mcp.ts b/src/service-override/mcp.ts index b207c864..615935b5 100644 --- a/src/service-override/mcp.ts +++ b/src/service-override/mcp.ts @@ -36,6 +36,7 @@ import { IWorkbenchMcpGatewayService } from 'vs/workbench/contrib/mcp/common/mcp import { BrowserMcpGatewayService } from 'vs/workbench/contrib/mcp/browser/mcpGatewayService' import 'vs/workbench/contrib/mcp/browser/mcp.contribution' import 'vs/workbench/contrib/mcp/browser/mcp.view.contribution' +import 'vs/workbench/api/browser/extensionHost.mcp.contribution' export default function getServiceOverride(): IEditorOverrideServices { return { diff --git a/src/service-override/notebook.ts b/src/service-override/notebook.ts index 5562b461..d797fc75 100644 --- a/src/service-override/notebook.ts +++ b/src/service-override/notebook.ts @@ -40,6 +40,7 @@ import { NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/brows import { INotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.service' import 'vs/workbench/contrib/replNotebook/browser/repl.contribution' import 'vs/workbench/contrib/notebook/browser/notebook.contribution' +import 'vs/workbench/api/browser/extensionHost.notebook.contribution' export default function getServiceOverride(): IEditorOverrideServices { return { diff --git a/src/service-override/tools/inertMainThreadCustomer.ts b/src/service-override/tools/inertMainThreadCustomer.ts new file mode 100644 index 00000000..a92687eb --- /dev/null +++ b/src/service-override/tools/inertMainThreadCustomer.ts @@ -0,0 +1,54 @@ +import { ILogService } from 'vs/platform/log/common/log.service' +import { MainContext } from 'vs/workbench/api/common/extHost.protocol' +import { + extHostCustomer, + type IExtHostContext +} from 'vs/workbench/services/extensions/common/extHostCustomers' +import type { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier' + +/** + * The extension host requires every `MainContext` proxy to have a main thread implementation, + * but the ones of optional features (chat, notebook, mcp...) are only registered by their service overrides. + * + * This customer registers an inert implementation for every proxy that nothing else registered. + * It doesn't depend on the registration order: unnamed customers are instantiated after the named ones, + * and a real implementation set later by another unnamed customer replaces the inert one. + * + * Every `$` method resolves with an empty array: some extHost implementations call the main thread + * on construction and iterate over the result (e.g. `ExtHostLanguageModelTools` with `$getTools`) + */ +class InertMainThreadCustomer { + constructor(extHostContext: IExtHostContext, @ILogService logService: ILogService) { + const ids: ProxyIdentifier[] = Object.values(MainContext) + const missing = ids.filter((id) => { + try { + extHostContext.assertRegistered([id]) + return false + } catch { + return true + } + }) + for (const id of missing) { + extHostContext.set(id, createInertMainThreadActor()) + } + if (missing.length > 0) { + logService.debug( + `Registered inert main thread implementations for: ${missing.map((id) => id.sid).join(', ')}` + ) + } + } + + dispose(): void {} +} + +function createInertMainThreadActor() { + return new Proxy( + {}, + { + get: (_target, property) => + typeof property === 'string' && property.startsWith('$') ? async () => [] : undefined + } + ) +} + +extHostCustomer(InertMainThreadCustomer) diff --git a/vscode-patches/0110-refactor-split-extension-host-contribution-per-featu.patch b/vscode-patches/0110-refactor-split-extension-host-contribution-per-featu.patch new file mode 100644 index 00000000..014efee7 --- /dev/null +++ b/vscode-patches/0110-refactor-split-extension-host-contribution-per-featu.patch @@ -0,0 +1,286 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Brian Jenkins +Date: Mon, 28 Sep 2026 00:30:27 -0400 +Subject: [PATCH] refactor: split extension host contribution per feature + +extensionHost.contribution still imports everything, but the chat, notebook +and mcp mainThread participants now live in their own files next to an +extensionHost.base.contribution, so they can be loaded only when needed. + +Co-Authored-By: Claude Opus 5.5 +--- + .../extensionHost.base.contribution.ts | 90 +++++++++++++++ + .../extensionHost.chat.contribution.ts | 16 +++ + .../api/browser/extensionHost.contribution.ts | 105 +----------------- + .../browser/extensionHost.mcp.contribution.ts | 7 ++ + .../extensionHost.notebook.contribution.ts | 11 ++ + 5 files changed, 128 insertions(+), 101 deletions(-) + create mode 100644 src/vs/workbench/api/browser/extensionHost.base.contribution.ts + create mode 100644 src/vs/workbench/api/browser/extensionHost.chat.contribution.ts + create mode 100644 src/vs/workbench/api/browser/extensionHost.mcp.contribution.ts + create mode 100644 src/vs/workbench/api/browser/extensionHost.notebook.contribution.ts + +diff --git a/src/vs/workbench/api/browser/extensionHost.base.contribution.ts b/src/vs/workbench/api/browser/extensionHost.base.contribution.ts +new file mode 100644 +index 00000000..bb2c5531 +--- /dev/null ++++ b/src/vs/workbench/api/browser/extensionHost.base.contribution.ts +@@ -0,0 +1,90 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright (c) Microsoft Corporation. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++ ++// --- other interested parties ++import '../common/jsonValidationExtensionPoint.js'; ++import '../../services/themes/common/colorExtensionPoint.js'; ++import '../../services/themes/common/iconExtensionPoint.js'; ++import '../../services/themes/common/tokenClassificationExtensionPoint.js'; ++import '../../contrib/codeEditor/common/languageConfigurationExtensionPoint.js'; ++import './statusBarExtensionPoint.js'; ++import '../../services/themes/browser/cssExtensionPoint.js'; ++ ++// --- mainThread participants ++import './mainThreadAgentEditorComments.js'; ++import './mainThreadLocalization.js'; ++import './mainThreadBulkEdits.js'; ++import './mainThreadEmbeddings.js'; ++import './mainThreadCodeInsets.js'; ++import './mainThreadCLICommands.js'; ++import './mainThreadClipboard.js'; ++import './mainThreadCommands.js'; ++import './mainThreadConfiguration.js'; ++import './mainThreadConsole.js'; ++import './mainThreadDebugService.js'; ++import './mainThreadDecorations.js'; ++import './mainThreadDiagnostics.js'; ++import './mainThreadDialogs.js'; ++import './mainThreadDocumentContentProviders.js'; ++import './mainThreadDocuments.js'; ++import './mainThreadDocumentsAndEditors.js'; ++import './mainThreadEditor.js'; ++import './mainThreadEditors.js'; ++import './mainThreadEditorTabs.js'; ++import './mainThreadErrors.js'; ++import './mainThreadExtensionService.js'; ++import './mainThreadFileSystem.js'; ++import './mainThreadFileSystemEventService.js'; ++import './mainThreadLanguageFeatures.js'; ++import './mainThreadLanguages.js'; ++import './mainThreadLogService.js'; ++import './mainThreadMessageService.js'; ++import './mainThreadManagedSockets.js'; ++import './mainThreadBrowserTunnelProxy.js'; ++import './mainThreadOutputService.js'; ++import './mainThreadProgress.js'; ++import './mainThreadQuickDiff.js'; ++import './mainThreadDocumentDiff.js'; ++import './mainThreadQuickOpen.js'; ++import './mainThreadRemoteConnectionData.js'; ++import './mainThreadSaveParticipant.js'; ++import './mainThreadSpeech.js'; ++import './mainThreadEditSessionIdentityParticipant.js'; ++import './mainThreadSCM.js'; ++import './mainThreadSearch.js'; ++import './mainThreadStatusBar.js'; ++import './mainThreadStorage.js'; ++import './mainThreadTelemetry.js'; ++import './mainThreadTerminalService.js'; ++import './mainThreadTerminalShellIntegration.js'; ++import './mainThreadTheming.js'; ++import './mainThreadTreeViews.js'; ++import './mainThreadDownloadService.js'; ++import './mainThreadUrls.js'; ++import './mainThreadUriOpeners.js'; ++import './mainThreadWindow.js'; ++import './mainThreadPower.js'; ++import './mainThreadWebviewManager.js'; ++import './mainThreadWorkspace.js'; ++import './mainThreadComments.js'; ++import './mainThreadInteractive.js'; ++import './mainThreadTask.js'; ++import './mainThreadLabelService.js'; ++import './mainThreadTunnelService.js'; ++import './mainThreadAuthentication.js'; ++import './mainThreadTimeline.js'; ++import './mainThreadTesting.js'; ++import './mainThreadSecretState.js'; ++import './mainThreadShare.js'; ++import './mainThreadProfileContentHandlers.js'; ++import './mainThreadAiRelatedInformation.js'; ++import './mainThreadAiEmbeddingVector.js'; ++import './mainThreadAiSettingsSearch.js'; ++import './mainThreadChatOutputRenderer.js'; ++import './mainThreadDataChannels.js'; ++import './mainThreadMeteredConnection.js'; ++import './mainThreadGitExtensionService.js'; ++import './mainThreadBrowsers.js'; +diff --git a/src/vs/workbench/api/browser/extensionHost.chat.contribution.ts b/src/vs/workbench/api/browser/extensionHost.chat.contribution.ts +new file mode 100644 +index 00000000..c319d415 +--- /dev/null ++++ b/src/vs/workbench/api/browser/extensionHost.chat.contribution.ts +@@ -0,0 +1,16 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright (c) Microsoft Corporation. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++// Chat and language model mainThread participants, split out of extensionHost.contribution so they can be loaded separately ++import './mainThreadLanguageModels.js'; ++import './mainThreadChatAgents2.js'; ++import './mainThreadChatCodeMapper.js'; ++import './mainThreadLanguageModelTools.js'; ++import './mainThreadChatContext.js'; ++import './mainThreadChatDebug.js'; ++import './mainThreadChatStatus.js'; ++import './mainThreadChatQuota.js'; ++import './mainThreadChatInputNotification.js'; ++import './mainThreadChatSessions.js'; +diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts +index 82aee03d..93e9b81f 100644 +--- a/src/vs/workbench/api/browser/extensionHost.contribution.ts ++++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts +@@ -3,104 +3,7 @@ + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +- +-// --- other interested parties +-import '../common/jsonValidationExtensionPoint.js'; +-import '../../services/themes/common/colorExtensionPoint.js'; +-import '../../services/themes/common/iconExtensionPoint.js'; +-import '../../services/themes/common/tokenClassificationExtensionPoint.js'; +-import '../../contrib/codeEditor/common/languageConfigurationExtensionPoint.js'; +-import './statusBarExtensionPoint.js'; +-import '../../services/themes/browser/cssExtensionPoint.js'; +- +-// --- mainThread participants +-import './mainThreadAgentEditorComments.js'; +-import './mainThreadLocalization.js'; +-import './mainThreadBulkEdits.js'; +-import './mainThreadLanguageModels.js'; +-import './mainThreadChatAgents2.js'; +-import './mainThreadChatCodeMapper.js'; +-import './mainThreadLanguageModelTools.js'; +-import './mainThreadEmbeddings.js'; +-import './mainThreadCodeInsets.js'; +-import './mainThreadCLICommands.js'; +-import './mainThreadClipboard.js'; +-import './mainThreadCommands.js'; +-import './mainThreadConfiguration.js'; +-import './mainThreadConsole.js'; +-import './mainThreadDebugService.js'; +-import './mainThreadDecorations.js'; +-import './mainThreadDiagnostics.js'; +-import './mainThreadDialogs.js'; +-import './mainThreadDocumentContentProviders.js'; +-import './mainThreadDocuments.js'; +-import './mainThreadDocumentsAndEditors.js'; +-import './mainThreadEditor.js'; +-import './mainThreadEditors.js'; +-import './mainThreadEditorTabs.js'; +-import './mainThreadErrors.js'; +-import './mainThreadExtensionService.js'; +-import './mainThreadFileSystem.js'; +-import './mainThreadFileSystemEventService.js'; +-import './mainThreadLanguageFeatures.js'; +-import './mainThreadLanguages.js'; +-import './mainThreadLogService.js'; +-import './mainThreadMessageService.js'; +-import './mainThreadManagedSockets.js'; +-import './mainThreadBrowserTunnelProxy.js'; +-import './mainThreadOutputService.js'; +-import './mainThreadProgress.js'; +-import './mainThreadQuickDiff.js'; +-import './mainThreadDocumentDiff.js'; +-import './mainThreadQuickOpen.js'; +-import './mainThreadRemoteConnectionData.js'; +-import './mainThreadSaveParticipant.js'; +-import './mainThreadSpeech.js'; +-import './mainThreadEditSessionIdentityParticipant.js'; +-import './mainThreadSCM.js'; +-import './mainThreadSearch.js'; +-import './mainThreadStatusBar.js'; +-import './mainThreadStorage.js'; +-import './mainThreadTelemetry.js'; +-import './mainThreadTerminalService.js'; +-import './mainThreadTerminalShellIntegration.js'; +-import './mainThreadTheming.js'; +-import './mainThreadTreeViews.js'; +-import './mainThreadDownloadService.js'; +-import './mainThreadUrls.js'; +-import './mainThreadUriOpeners.js'; +-import './mainThreadWindow.js'; +-import './mainThreadPower.js'; +-import './mainThreadWebviewManager.js'; +-import './mainThreadWorkspace.js'; +-import './mainThreadComments.js'; +-import './mainThreadNotebook.js'; +-import './mainThreadNotebookKernels.js'; +-import './mainThreadNotebookDocumentsAndEditors.js'; +-import './mainThreadNotebookRenderers.js'; +-import './mainThreadNotebookSaveParticipant.js'; +-import './mainThreadInteractive.js'; +-import './mainThreadTask.js'; +-import './mainThreadLabelService.js'; +-import './mainThreadTunnelService.js'; +-import './mainThreadAuthentication.js'; +-import './mainThreadTimeline.js'; +-import './mainThreadTesting.js'; +-import './mainThreadSecretState.js'; +-import './mainThreadShare.js'; +-import './mainThreadProfileContentHandlers.js'; +-import './mainThreadAiRelatedInformation.js'; +-import './mainThreadAiEmbeddingVector.js'; +-import './mainThreadAiSettingsSearch.js'; +-import './mainThreadMcp.js'; +-import './mainThreadChatContext.js'; +-import './mainThreadChatDebug.js'; +-import './mainThreadChatStatus.js'; +-import './mainThreadChatQuota.js'; +-import './mainThreadChatInputNotification.js'; +-import './mainThreadChatOutputRenderer.js'; +-import './mainThreadChatSessions.js'; +-import './mainThreadDataChannels.js'; +-import './mainThreadMeteredConnection.js'; +-import './mainThreadGitExtensionService.js'; +-import './mainThreadBrowsers.js'; ++import './extensionHost.base.contribution.js'; ++import './extensionHost.chat.contribution.js'; ++import './extensionHost.notebook.contribution.js'; ++import './extensionHost.mcp.contribution.js'; +diff --git a/src/vs/workbench/api/browser/extensionHost.mcp.contribution.ts b/src/vs/workbench/api/browser/extensionHost.mcp.contribution.ts +new file mode 100644 +index 00000000..570c524a +--- /dev/null ++++ b/src/vs/workbench/api/browser/extensionHost.mcp.contribution.ts +@@ -0,0 +1,7 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright (c) Microsoft Corporation. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++// MCP mainThread participants, split out of extensionHost.contribution so they can be loaded separately ++import './mainThreadMcp.js'; +diff --git a/src/vs/workbench/api/browser/extensionHost.notebook.contribution.ts b/src/vs/workbench/api/browser/extensionHost.notebook.contribution.ts +new file mode 100644 +index 00000000..ff2f3cc2 +--- /dev/null ++++ b/src/vs/workbench/api/browser/extensionHost.notebook.contribution.ts +@@ -0,0 +1,11 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright (c) Microsoft Corporation. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++// Notebook mainThread participants, split out of extensionHost.contribution so they can be loaded separately ++import './mainThreadNotebook.js'; ++import './mainThreadNotebookKernels.js'; ++import './mainThreadNotebookDocumentsAndEditors.js'; ++import './mainThreadNotebookRenderers.js'; ++import './mainThreadNotebookSaveParticipant.js'; +-- +2.39.5 (Apple Git-154) + From e4ded69b610e014a4539adc03a9390f84bf97864 Mon Sep 17 00:00:00 2001 From: Brian Jenkins Date: Mon, 28 Sep 2026 01:13:48 -0400 Subject: [PATCH 3/6] chore: check that optional features are only loaded by their service override Fail the build when too much of chat/notebook/mcp is loaded without their service override, or when an extension host main thread participant is not imported anymore (it would silently be replaced by an inert implementation). Co-Authored-By: Claude Opus 5.5 --- .../extension-host-bridges-check-plugin.ts | 37 ++++++++ .../plugins/feature-isolation-check-plugin.ts | 92 +++++++++++++++++++ rollup/rollup.config.ts | 12 +++ 3 files changed, 141 insertions(+) create mode 100644 rollup/plugins/extension-host-bridges-check-plugin.ts create mode 100644 rollup/plugins/feature-isolation-check-plugin.ts diff --git a/rollup/plugins/extension-host-bridges-check-plugin.ts b/rollup/plugins/extension-host-bridges-check-plugin.ts new file mode 100644 index 00000000..a3e5295c --- /dev/null +++ b/rollup/plugins/extension-host-bridges-check-plugin.ts @@ -0,0 +1,37 @@ +import type { Plugin } from 'rollup' +import * as fs from 'node:fs' +import * as nodePath from 'node:path' + +/** + * The extension host main thread participants are split between several `extensionHost.*.contribution` files, + * and `inertMainThreadCustomer` silently registers an inert implementation for any missing one. + * + * This checks that every main thread participant is still part of the build, so that one that is forgotten + * (e.g. while rebasing the VSCode patches after VSCode added one) fails the build instead of being silently inert + */ +export default ({ vscodeSrcDir }: { vscodeSrcDir: string }): Plugin => { + return { + name: 'extension-host-bridges-check', + buildEnd(error) { + if (error != null) { + return + } + const directory = nodePath.resolve(vscodeSrcDir, 'vs/workbench/api/browser') + const moduleIds = new Set(this.getModuleIds()) + const missing = fs + .readdirSync(directory) + .filter((file) => /^mainThread\w+\.js$/.test(file)) + .map((file) => nodePath.resolve(directory, file)) + .filter((file) => /\bextHost(Named)?Customer\b/.test(fs.readFileSync(file, 'utf-8'))) + .filter((file) => !moduleIds.has(file)) + + if (missing.length > 0) { + this.error( + `Extension host main thread participants not imported by any extensionHost.*.contribution file:\n${missing + .map((file) => ` ${nodePath.relative(vscodeSrcDir, file)}`) + .join('\n')}` + ) + } + } + } +} diff --git a/rollup/plugins/feature-isolation-check-plugin.ts b/rollup/plugins/feature-isolation-check-plugin.ts new file mode 100644 index 00000000..50ad3e23 --- /dev/null +++ b/rollup/plugins/feature-isolation-check-plugin.ts @@ -0,0 +1,92 @@ +import type { Plugin, PluginContext } from 'rollup' + +interface FeatureIsolationCheckOptions { + /** Matches the modules of the optional features (e.g. chat, notebook, mcp) */ + featureModules: RegExp + /** Matches the entry points that are allowed to load feature modules (their service overrides) */ + allowedEntries: RegExp + /** + * Maximum size (in bytes of transformed code) of the feature modules statically reachable from + * the other entry points. It covers the known remaining dependencies, and fails the build when a + * VSCode update (or a change here) makes more of the features load unconditionally + */ + maxSize: number +} + +/** + * Checks that optional features are only loaded by their own service overrides, so that a workbench + * without them doesn't load (and bundle) their implementation + */ +export default ({ + featureModules, + allowedEntries, + maxSize +}: FeatureIsolationCheckOptions): Plugin => { + function getSize(context: PluginContext, id: string) { + return context.getModuleInfo(id)?.code?.length ?? 0 + } + + function collectStaticImports(context: PluginContext, from: string[]) { + const seen = new Set(from) + const queue = [...from] + while (queue.length > 0) { + const id = queue.pop()! + for (const imported of context.getModuleInfo(id)?.importedIds ?? []) { + if (!seen.has(imported)) { + seen.add(imported) + queue.push(imported) + } + } + } + return seen + } + + return { + name: 'feature-isolation-check', + buildEnd(error) { + if (error != null) { + return + } + const entries = Array.from(this.getModuleIds()).filter( + (id) => this.getModuleInfo(id)!.isEntry && !allowedEntries.test(id) + ) + const reachable = collectStaticImports(this, entries) + const reachableFeatureModules = Array.from(reachable).filter((id) => featureModules.test(id)) + const totalSize = reachableFeatureModules.reduce((acc, id) => acc + getSize(this, id), 0) + + // Where the other modules start depending on the features, and how much each of them loads + const crossings = Array.from(reachable) + .filter((id) => !featureModules.test(id)) + .flatMap((importer) => + this.getModuleInfo(importer)! + .importedIds.filter((id) => featureModules.test(id)) + .map((imported) => ({ importer, imported })) + ) + .map(({ importer, imported }) => ({ + importer, + imported, + size: Array.from(collectStaticImports(this, [imported])) + .filter((id) => featureModules.test(id)) + .reduce((acc, id) => acc + getSize(this, id), 0) + })) + .sort((a, b) => b.size - a.size) + + const kb = (size: number) => `${Math.round(size / 1024)}KB` + const shorten = (id: string) => id.replace(/^.*?\/(vs|src)\//, '$1/') + const report = [ + `${reachableFeatureModules.length} feature modules (${kb(totalSize)}) are loaded without their service overrides (max ${kb(maxSize)})`, + ...crossings + .slice(0, 20) + .map( + ({ importer, imported, size }) => + ` ${kb(size).padStart(7)} ${shorten(importer)} -> ${shorten(imported)}` + ) + ].join('\n') + + if (totalSize > maxSize) { + this.error(report) + } + this.info(report) + } + } +} diff --git a/rollup/rollup.config.ts b/rollup/rollup.config.ts index f95d0b63..902910a8 100644 --- a/rollup/rollup.config.ts +++ b/rollup/rollup.config.ts @@ -20,6 +20,8 @@ import { import vscodeAssetGlobMetaUrl from './plugins/vscode-asset-glob-meta-url-plugin.js' import dynamicImportPolyfillPlugin from './plugins/dynamic-import-polyfill-plugin.js' import resolveAssetUrlPlugin from './plugins/resolve-asset-url-plugin.js' +import featureIsolationCheckPlugin from './plugins/feature-isolation-check-plugin.js' +import extensionHostBridgesCheckPlugin from './plugins/extension-host-bridges-check-plugin.js' import { typeDedupReplaceTransformer } from './tools/typeDedup.js' import { DIST_DIR_MAIN, @@ -191,6 +193,16 @@ export default (args: Record): rollup.RollupOptions => { return undefined } }, + featureIsolationCheckPlugin({ + // Service identifiers (`.service` files) are shared on purpose + featureModules: /\/vs\/workbench\/contrib\/(chat|notebook|mcp)\/(?!.*\.service\.js$)/, + // Service overrides that build on top of those features + allowedEntries: + /\/src\/service-override\/(chat|notebook|mcp|interactive|ai|speech|welcome)\.ts$/, + // ~910KB with VSCode 1.138: mostly dictation, go to symbol in chat, editor tabs and chat context keys + maxSize: 1000 * 1024 + }), + extensionHostBridgesCheckPlugin({ vscodeSrcDir: VSCODE_SRC_DIR }), configuredSubpackagePlugin(), copy({ hook: 'writeBundle', From 1832f5d446d840746bc31c11a54f9e97e044eca3 Mon Sep 17 00:00:00 2001 From: Brian Jenkins Date: Mon, 28 Sep 2026 11:43:18 -0400 Subject: [PATCH 4/6] refactor: use typed missing implementations for optional main thread participants Replace the generic proxy with implementations of the chat, language model, notebook and mcp MainThread shapes, like the missing services: calls that are only notifications do nothing, queries return nothing, and actions are unsupported. They are only registered for those ids, when nothing else did. Co-Authored-By: Claude Opus 5.5 --- src/missing-services.ts | 291 +++++++++++++++++- src/service-override/extensions.ts | 1 - .../tools/inertMainThreadCustomer.ts | 54 ---- 3 files changed, 290 insertions(+), 56 deletions(-) delete mode 100644 src/service-override/tools/inertMainThreadCustomer.ts diff --git a/src/missing-services.ts b/src/missing-services.ts index 0152774a..8bd767e1 100644 --- a/src/missing-services.ts +++ b/src/missing-services.ts @@ -511,7 +511,31 @@ import { getBuiltInExtensionTranslationsUris, getExtensionIdProvidingCurrentLocale } from './l10n.js' -import { unsupported } from './tools.js' +import { noop, unsupported } from './tools.js' +import { + MainContext, + type MainThreadChatAgentsShape2, + type MainThreadChatContextShape, + type MainThreadChatDebugShape, + type MainThreadChatInputNotificationShape, + type MainThreadChatQuotaShape, + type MainThreadChatSessionsShape, + type MainThreadChatStatusShape, + type MainThreadCodeMapperShape, + type MainThreadLanguageModelToolsShape, + type MainThreadLanguageModelsShape, + type MainThreadMcpShape, + type MainThreadNotebookDocumentsShape, + type MainThreadNotebookEditorsShape, + type MainThreadNotebookKernelsShape, + type MainThreadNotebookRenderersShape, + type MainThreadNotebookShape +} from 'vs/workbench/api/common/extHost.protocol' +import { + extHostCustomer, + type IExtHostContext +} from 'vs/workbench/services/extensions/common/extHostCustomers' +import type { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier' import { NullAgentHostService } from 'vs/platform/agentHost/browser/nullAgentHostService' import { NullSSHRemoteAgentHostService } from 'vs/platform/agentHost/browser/nullSshRemoteAgentHostService' @@ -8468,3 +8492,268 @@ class GitHubService implements IGitHubService { } registerSingleton(IGitHubService, GitHubService, InstantiationType.Delayed) + +// Main thread counterparts of the extension host API of optional features (chat, language models, notebooks, mcp) +// Their actual implementations are only registered by the corresponding service overrides, but the extension host +// requires all of them to be registered, and some of them are called when the extension host starts +// (e.g. `ExtHostLanguageModelTools` calls `$getTools`) + +class MissingMainThreadLanguageModels implements MainThreadLanguageModelsShape { + $registerLanguageModelProvider: MainThreadLanguageModelsShape['$registerLanguageModelProvider'] = + noop + $onLMProviderChange: MainThreadLanguageModelsShape['$onLMProviderChange'] = noop + $unregisterProvider: MainThreadLanguageModelsShape['$unregisterProvider'] = noop + @Unsupported + $tryStartChatRequest: MainThreadLanguageModelsShape['$tryStartChatRequest'] = unsupported + $reportResponsePart: MainThreadLanguageModelsShape['$reportResponsePart'] = async () => {} + $reportResponseDone: MainThreadLanguageModelsShape['$reportResponseDone'] = async () => {} + $selectChatModels: MainThreadLanguageModelsShape['$selectChatModels'] = async () => [] + @Unsupported + $countTokens: MainThreadLanguageModelsShape['$countTokens'] = unsupported + $cancelLanguageModelChatRequest: MainThreadLanguageModelsShape['$cancelLanguageModelChatRequest'] = + noop + $fileIsIgnored: MainThreadLanguageModelsShape['$fileIsIgnored'] = async () => false + $registerFileIgnoreProvider: MainThreadLanguageModelsShape['$registerFileIgnoreProvider'] = noop + $unregisterFileIgnoreProvider: MainThreadLanguageModelsShape['$unregisterFileIgnoreProvider'] = + noop + dispose(): void {} +} + +class MissingMainThreadChatAgents2 implements MainThreadChatAgentsShape2 { + $handleProgressChunk: MainThreadChatAgentsShape2['$handleProgressChunk'] = async () => {} + $handleAnchorResolve: MainThreadChatAgentsShape2['$handleAnchorResolve'] = noop + $registerAgent: MainThreadChatAgentsShape2['$registerAgent'] = noop + $registerChatParticipantDetectionProvider: MainThreadChatAgentsShape2['$registerChatParticipantDetectionProvider'] = + noop + $unregisterChatParticipantDetectionProvider: MainThreadChatAgentsShape2['$unregisterChatParticipantDetectionProvider'] = + noop + $registerPromptFileProvider: MainThreadChatAgentsShape2['$registerPromptFileProvider'] = noop + $unregisterPromptFileProvider: MainThreadChatAgentsShape2['$unregisterPromptFileProvider'] = noop + $onDidChangePromptFiles: MainThreadChatAgentsShape2['$onDidChangePromptFiles'] = noop + $registerChatSessionCustomizationProvider: MainThreadChatAgentsShape2['$registerChatSessionCustomizationProvider'] = + noop + $unregisterChatSessionCustomizationProvider: MainThreadChatAgentsShape2['$unregisterChatSessionCustomizationProvider'] = + noop + $onDidChangeCustomizations: MainThreadChatAgentsShape2['$onDidChangeCustomizations'] = noop + $registerAgentCompletionsProvider: MainThreadChatAgentsShape2['$registerAgentCompletionsProvider'] = + noop + $unregisterAgentCompletionsProvider: MainThreadChatAgentsShape2['$unregisterAgentCompletionsProvider'] = + noop + $updateAgent: MainThreadChatAgentsShape2['$updateAgent'] = noop + $unregisterAgent: MainThreadChatAgentsShape2['$unregisterAgent'] = noop + @Unsupported + $transferActiveChatSession: MainThreadChatAgentsShape2['$transferActiveChatSession'] = unsupported + $provideCustomAgents: MainThreadChatAgentsShape2['$provideCustomAgents'] = async () => [] + $provideInstructions: MainThreadChatAgentsShape2['$provideInstructions'] = async () => [] + $provideSkills: MainThreadChatAgentsShape2['$provideSkills'] = async () => [] + $provideSlashCommands: MainThreadChatAgentsShape2['$provideSlashCommands'] = async () => [] + $provideHooks: MainThreadChatAgentsShape2['$provideHooks'] = async () => [] + $providePlugins: MainThreadChatAgentsShape2['$providePlugins'] = async () => [] + dispose(): void {} +} + +class MissingMainThreadCodeMapper implements MainThreadCodeMapperShape { + $registerCodeMapperProvider: MainThreadCodeMapperShape['$registerCodeMapperProvider'] = noop + $unregisterCodeMapperProvider: MainThreadCodeMapperShape['$unregisterCodeMapperProvider'] = noop + $handleProgress: MainThreadCodeMapperShape['$handleProgress'] = async () => {} + dispose(): void {} +} + +class MissingMainThreadLanguageModelTools implements MainThreadLanguageModelToolsShape { + $getTools: MainThreadLanguageModelToolsShape['$getTools'] = async () => [] + $acceptToolProgress: MainThreadLanguageModelToolsShape['$acceptToolProgress'] = noop + @Unsupported + $invokeTool: MainThreadLanguageModelToolsShape['$invokeTool'] = unsupported + @Unsupported + $countTokensForInvocation: MainThreadLanguageModelToolsShape['$countTokensForInvocation'] = + unsupported + $registerTool: MainThreadLanguageModelToolsShape['$registerTool'] = noop + $registerToolWithDefinition: MainThreadLanguageModelToolsShape['$registerToolWithDefinition'] = + noop + $unregisterTool: MainThreadLanguageModelToolsShape['$unregisterTool'] = noop + dispose(): void {} +} + +class MissingMainThreadChatContext implements MainThreadChatContextShape { + $registerChatWorkspaceContextProvider: MainThreadChatContextShape['$registerChatWorkspaceContextProvider'] = + noop + $registerChatExplicitContextProvider: MainThreadChatContextShape['$registerChatExplicitContextProvider'] = + noop + $registerChatResourceContextProvider: MainThreadChatContextShape['$registerChatResourceContextProvider'] = + noop + $unregisterChatContextProvider: MainThreadChatContextShape['$unregisterChatContextProvider'] = + noop + $updateWorkspaceContextItems: MainThreadChatContextShape['$updateWorkspaceContextItems'] = noop + @Unsupported + $executeChatContextItemCommand: MainThreadChatContextShape['$executeChatContextItemCommand'] = + unsupported + dispose(): void {} +} + +class MissingMainThreadChatDebug implements MainThreadChatDebugShape { + $registerChatDebugLogProvider: MainThreadChatDebugShape['$registerChatDebugLogProvider'] = noop + $unregisterChatDebugLogProvider: MainThreadChatDebugShape['$unregisterChatDebugLogProvider'] = + noop + $acceptChatDebugEvent: MainThreadChatDebugShape['$acceptChatDebugEvent'] = noop + $subscribeToCoreDebugEvents: MainThreadChatDebugShape['$subscribeToCoreDebugEvents'] = noop + $unsubscribeFromCoreDebugEvents: MainThreadChatDebugShape['$unsubscribeFromCoreDebugEvents'] = + noop + dispose(): void {} +} + +class MissingMainThreadChatStatus implements MainThreadChatStatusShape { + $setEntry: MainThreadChatStatusShape['$setEntry'] = noop + $disposeEntry: MainThreadChatStatusShape['$disposeEntry'] = noop + dispose(): void {} +} + +class MissingMainThreadChatQuota implements MainThreadChatQuotaShape { + $updateQuotas: MainThreadChatQuotaShape['$updateQuotas'] = noop + dispose(): void {} +} + +class MissingMainThreadChatInputNotification implements MainThreadChatInputNotificationShape { + $setNotification: MainThreadChatInputNotificationShape['$setNotification'] = noop + $disposeNotification: MainThreadChatInputNotificationShape['$disposeNotification'] = noop + dispose(): void {} +} + +class MissingMainThreadChatSessions implements MainThreadChatSessionsShape { + $registerChatSessionItemController: MainThreadChatSessionsShape['$registerChatSessionItemController'] = + noop + $updateChatSessionItemControllerCapabilities: MainThreadChatSessionsShape['$updateChatSessionItemControllerCapabilities'] = + noop + $unregisterChatSessionItemController: MainThreadChatSessionsShape['$unregisterChatSessionItemController'] = + noop + $updateChatSessionItems: MainThreadChatSessionsShape['$updateChatSessionItems'] = async () => {} + $addOrUpdateChatSessionItem: MainThreadChatSessionsShape['$addOrUpdateChatSessionItem'] = + async () => {} + $onDidCommitChatSessionItem: MainThreadChatSessionsShape['$onDidCommitChatSessionItem'] = noop + $registerChatSessionContentProvider: MainThreadChatSessionsShape['$registerChatSessionContentProvider'] = + noop + $unregisterChatSessionContentProvider: MainThreadChatSessionsShape['$unregisterChatSessionContentProvider'] = + noop + $onDidChangeChatSessionOptions: MainThreadChatSessionsShape['$onDidChangeChatSessionOptions'] = + noop + $onDidChangeChatSessionProviderOptions: MainThreadChatSessionsShape['$onDidChangeChatSessionProviderOptions'] = + noop + $updateChatSessionInputState: MainThreadChatSessionsShape['$updateChatSessionInputState'] = noop + $handleProgressChunk: MainThreadChatSessionsShape['$handleProgressChunk'] = async () => {} + $handleAnchorResolve: MainThreadChatSessionsShape['$handleAnchorResolve'] = noop + $handleProgressComplete: MainThreadChatSessionsShape['$handleProgressComplete'] = noop + dispose(): void {} +} + +class MissingMainThreadNotebook implements MainThreadNotebookShape { + $registerNotebookSerializer: MainThreadNotebookShape['$registerNotebookSerializer'] = noop + $unregisterNotebookSerializer: MainThreadNotebookShape['$unregisterNotebookSerializer'] = noop + $registerNotebookCellStatusBarItemProvider: MainThreadNotebookShape['$registerNotebookCellStatusBarItemProvider'] = + async () => {} + $unregisterNotebookCellStatusBarItemProvider: MainThreadNotebookShape['$unregisterNotebookCellStatusBarItemProvider'] = + async () => {} + $emitCellStatusBarEvent: MainThreadNotebookShape['$emitCellStatusBarEvent'] = noop + dispose(): void {} +} + +class MissingMainThreadNotebookKernels implements MainThreadNotebookKernelsShape { + $postMessage: MainThreadNotebookKernelsShape['$postMessage'] = async () => false + $addKernel: MainThreadNotebookKernelsShape['$addKernel'] = async () => {} + $updateKernel: MainThreadNotebookKernelsShape['$updateKernel'] = noop + $removeKernel: MainThreadNotebookKernelsShape['$removeKernel'] = noop + $updateNotebookPriority: MainThreadNotebookKernelsShape['$updateNotebookPriority'] = noop + $createExecution: MainThreadNotebookKernelsShape['$createExecution'] = noop + $updateExecution: MainThreadNotebookKernelsShape['$updateExecution'] = noop + $completeExecution: MainThreadNotebookKernelsShape['$completeExecution'] = noop + $createNotebookExecution: MainThreadNotebookKernelsShape['$createNotebookExecution'] = noop + $beginNotebookExecution: MainThreadNotebookKernelsShape['$beginNotebookExecution'] = noop + $completeNotebookExecution: MainThreadNotebookKernelsShape['$completeNotebookExecution'] = noop + $addKernelDetectionTask: MainThreadNotebookKernelsShape['$addKernelDetectionTask'] = + async () => {} + $removeKernelDetectionTask: MainThreadNotebookKernelsShape['$removeKernelDetectionTask'] = noop + $addKernelSourceActionProvider: MainThreadNotebookKernelsShape['$addKernelSourceActionProvider'] = + async () => {} + $removeKernelSourceActionProvider: MainThreadNotebookKernelsShape['$removeKernelSourceActionProvider'] = + noop + $emitNotebookKernelSourceActionsChangeEvent: MainThreadNotebookKernelsShape['$emitNotebookKernelSourceActionsChangeEvent'] = + noop + $receiveVariable: MainThreadNotebookKernelsShape['$receiveVariable'] = noop + $variablesUpdated: MainThreadNotebookKernelsShape['$variablesUpdated'] = noop + dispose(): void {} +} + +class MissingMainThreadNotebookDocuments implements MainThreadNotebookDocumentsShape { + @Unsupported + $tryCreateNotebook: MainThreadNotebookDocumentsShape['$tryCreateNotebook'] = unsupported + @Unsupported + $tryOpenNotebook: MainThreadNotebookDocumentsShape['$tryOpenNotebook'] = unsupported + @Unsupported + $trySaveNotebook: MainThreadNotebookDocumentsShape['$trySaveNotebook'] = unsupported + dispose(): void {} +} + +class MissingMainThreadNotebookEditors implements MainThreadNotebookEditorsShape { + @Unsupported + $tryShowNotebookDocument: MainThreadNotebookEditorsShape['$tryShowNotebookDocument'] = unsupported + @Unsupported + $tryRevealRange: MainThreadNotebookEditorsShape['$tryRevealRange'] = unsupported + $trySetSelections: MainThreadNotebookEditorsShape['$trySetSelections'] = noop + dispose(): void {} +} + +class MissingMainThreadNotebookRenderers implements MainThreadNotebookRenderersShape { + $postMessage: MainThreadNotebookRenderersShape['$postMessage'] = async () => false + dispose(): void {} +} + +class MissingMainThreadMcp implements MainThreadMcpShape { + $onDidChangeState: MainThreadMcpShape['$onDidChangeState'] = noop + $onDidPublishLog: MainThreadMcpShape['$onDidPublishLog'] = noop + $onDidReceiveMessage: MainThreadMcpShape['$onDidReceiveMessage'] = noop + $upsertMcpCollection: MainThreadMcpShape['$upsertMcpCollection'] = noop + $deleteMcpCollection: MainThreadMcpShape['$deleteMcpCollection'] = noop + @Unsupported + $getTokenFromServerMetadata: MainThreadMcpShape['$getTokenFromServerMetadata'] = unsupported + @Unsupported + $getTokenForProviderId: MainThreadMcpShape['$getTokenForProviderId'] = unsupported + $logMcpAuthSetup: MainThreadMcpShape['$logMcpAuthSetup'] = noop + @Unsupported + $startMcpGateway: MainThreadMcpShape['$startMcpGateway'] = unsupported + $disposeMcpGateway: MainThreadMcpShape['$disposeMcpGateway'] = noop + dispose(): void {} +} + +const missingMainThreadCustomers: [ProxyIdentifier, new () => IDisposable][] = [ + [MainContext.MainThreadLanguageModels, MissingMainThreadLanguageModels], + [MainContext.MainThreadChatAgents2, MissingMainThreadChatAgents2], + [MainContext.MainThreadCodeMapper, MissingMainThreadCodeMapper], + [MainContext.MainThreadLanguageModelTools, MissingMainThreadLanguageModelTools], + [MainContext.MainThreadChatContext, MissingMainThreadChatContext], + [MainContext.MainThreadChatDebug, MissingMainThreadChatDebug], + [MainContext.MainThreadChatStatus, MissingMainThreadChatStatus], + [MainContext.MainThreadChatQuota, MissingMainThreadChatQuota], + [MainContext.MainThreadChatInputNotification, MissingMainThreadChatInputNotification], + [MainContext.MainThreadChatSessions, MissingMainThreadChatSessions], + [MainContext.MainThreadNotebook, MissingMainThreadNotebook], + [MainContext.MainThreadNotebookKernels, MissingMainThreadNotebookKernels], + [MainContext.MainThreadNotebookDocuments, MissingMainThreadNotebookDocuments], + [MainContext.MainThreadNotebookEditors, MissingMainThreadNotebookEditors], + [MainContext.MainThreadNotebookRenderers, MissingMainThreadNotebookRenderers], + [MainContext.MainThreadMcp, MissingMainThreadMcp] +] + +// Unnamed customers are instantiated after the named ones, and a real implementation set later replaces the missing one, +// so it only registers the missing implementations whatever the import order is +class MissingMainThreadCustomers implements IDisposable { + constructor(extHostContext: IExtHostContext) { + for (const [id, ctor] of missingMainThreadCustomers) { + try { + extHostContext.assertRegistered([id]) + } catch { + extHostContext.set(id, new ctor()) + } + } + } + + dispose(): void {} +} +extHostCustomer(MissingMainThreadCustomers) diff --git a/src/service-override/extensions.ts b/src/service-override/extensions.ts index cccc46b7..a2d31625 100644 --- a/src/service-override/extensions.ts +++ b/src/service-override/extensions.ts @@ -70,7 +70,6 @@ import { import * as platform from 'vs/base/common/platform' import { getBuiltInExtensionTranslationsUris } from '../l10n.js' import 'vs/workbench/api/browser/extensionHost.base.contribution' -import './tools/inertMainThreadCustomer.js' export interface WorkerConfig { url: string diff --git a/src/service-override/tools/inertMainThreadCustomer.ts b/src/service-override/tools/inertMainThreadCustomer.ts deleted file mode 100644 index a92687eb..00000000 --- a/src/service-override/tools/inertMainThreadCustomer.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ILogService } from 'vs/platform/log/common/log.service' -import { MainContext } from 'vs/workbench/api/common/extHost.protocol' -import { - extHostCustomer, - type IExtHostContext -} from 'vs/workbench/services/extensions/common/extHostCustomers' -import type { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier' - -/** - * The extension host requires every `MainContext` proxy to have a main thread implementation, - * but the ones of optional features (chat, notebook, mcp...) are only registered by their service overrides. - * - * This customer registers an inert implementation for every proxy that nothing else registered. - * It doesn't depend on the registration order: unnamed customers are instantiated after the named ones, - * and a real implementation set later by another unnamed customer replaces the inert one. - * - * Every `$` method resolves with an empty array: some extHost implementations call the main thread - * on construction and iterate over the result (e.g. `ExtHostLanguageModelTools` with `$getTools`) - */ -class InertMainThreadCustomer { - constructor(extHostContext: IExtHostContext, @ILogService logService: ILogService) { - const ids: ProxyIdentifier[] = Object.values(MainContext) - const missing = ids.filter((id) => { - try { - extHostContext.assertRegistered([id]) - return false - } catch { - return true - } - }) - for (const id of missing) { - extHostContext.set(id, createInertMainThreadActor()) - } - if (missing.length > 0) { - logService.debug( - `Registered inert main thread implementations for: ${missing.map((id) => id.sid).join(', ')}` - ) - } - } - - dispose(): void {} -} - -function createInertMainThreadActor() { - return new Proxy( - {}, - { - get: (_target, property) => - typeof property === 'string' && property.startsWith('$') ? async () => [] : undefined - } - ) -} - -extHostCustomer(InertMainThreadCustomer) From b734f1ea8115a653f1de6fb8f19a6cc5230bfc50 Mon Sep 17 00:00:00 2001 From: Brian Jenkins Date: Mon, 28 Sep 2026 11:43:18 -0400 Subject: [PATCH 5/6] chore: remove extension host participants check A participant that isn't imported anymore is no longer silently replaced by an inert implementation: without a missing implementation for it, the extension host fails to start. The contribution check from #831 also detects a participant left in extensionHost.contribution. Co-Authored-By: Claude Opus 5.5 --- .../extension-host-bridges-check-plugin.ts | 37 ------------------- rollup/rollup.config.ts | 2 - 2 files changed, 39 deletions(-) delete mode 100644 rollup/plugins/extension-host-bridges-check-plugin.ts diff --git a/rollup/plugins/extension-host-bridges-check-plugin.ts b/rollup/plugins/extension-host-bridges-check-plugin.ts deleted file mode 100644 index a3e5295c..00000000 --- a/rollup/plugins/extension-host-bridges-check-plugin.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Plugin } from 'rollup' -import * as fs from 'node:fs' -import * as nodePath from 'node:path' - -/** - * The extension host main thread participants are split between several `extensionHost.*.contribution` files, - * and `inertMainThreadCustomer` silently registers an inert implementation for any missing one. - * - * This checks that every main thread participant is still part of the build, so that one that is forgotten - * (e.g. while rebasing the VSCode patches after VSCode added one) fails the build instead of being silently inert - */ -export default ({ vscodeSrcDir }: { vscodeSrcDir: string }): Plugin => { - return { - name: 'extension-host-bridges-check', - buildEnd(error) { - if (error != null) { - return - } - const directory = nodePath.resolve(vscodeSrcDir, 'vs/workbench/api/browser') - const moduleIds = new Set(this.getModuleIds()) - const missing = fs - .readdirSync(directory) - .filter((file) => /^mainThread\w+\.js$/.test(file)) - .map((file) => nodePath.resolve(directory, file)) - .filter((file) => /\bextHost(Named)?Customer\b/.test(fs.readFileSync(file, 'utf-8'))) - .filter((file) => !moduleIds.has(file)) - - if (missing.length > 0) { - this.error( - `Extension host main thread participants not imported by any extensionHost.*.contribution file:\n${missing - .map((file) => ` ${nodePath.relative(vscodeSrcDir, file)}`) - .join('\n')}` - ) - } - } - } -} diff --git a/rollup/rollup.config.ts b/rollup/rollup.config.ts index 902910a8..4acf3065 100644 --- a/rollup/rollup.config.ts +++ b/rollup/rollup.config.ts @@ -21,7 +21,6 @@ import vscodeAssetGlobMetaUrl from './plugins/vscode-asset-glob-meta-url-plugin. import dynamicImportPolyfillPlugin from './plugins/dynamic-import-polyfill-plugin.js' import resolveAssetUrlPlugin from './plugins/resolve-asset-url-plugin.js' import featureIsolationCheckPlugin from './plugins/feature-isolation-check-plugin.js' -import extensionHostBridgesCheckPlugin from './plugins/extension-host-bridges-check-plugin.js' import { typeDedupReplaceTransformer } from './tools/typeDedup.js' import { DIST_DIR_MAIN, @@ -202,7 +201,6 @@ export default (args: Record): rollup.RollupOptions => { // ~910KB with VSCode 1.138: mostly dictation, go to symbol in chat, editor tabs and chat context keys maxSize: 1000 * 1024 }), - extensionHostBridgesCheckPlugin({ vscodeSrcDir: VSCODE_SRC_DIR }), configuredSubpackagePlugin(), copy({ hook: 'writeBundle', From 3e4b7bdfdc23f266ba002061010c845e495ef963 Mon Sep 17 00:00:00 2001 From: Brian Jenkins Date: Mon, 28 Sep 2026 12:32:38 -0400 Subject: [PATCH 6/6] refactor: move missing main thread participants into their own module They are only needed with the extension host, so they are now imported by the extensions service override instead of living in missing-services. The Unsupported decorator moves to tools so both can use it, and the decorator check covers the new module. Co-Authored-By: Claude Opus 5.5 --- scripts/check-unsupported-decorator.ts | 9 +- src/missing-services.ts | 314 +----------------- src/service-override/extensions.ts | 1 + .../tools/missingMainThreadCustomers.ts | 291 ++++++++++++++++ src/tools.ts | 27 ++ 5 files changed, 326 insertions(+), 316 deletions(-) create mode 100644 src/service-override/tools/missingMainThreadCustomers.ts diff --git a/scripts/check-unsupported-decorator.ts b/scripts/check-unsupported-decorator.ts index 2c435cdb..c5cd828a 100644 --- a/scripts/check-unsupported-decorator.ts +++ b/scripts/check-unsupported-decorator.ts @@ -1,11 +1,14 @@ import { Project } from 'ts-morph' const project = new Project({ tsConfigFilePath: 'tsconfig.json' }) -const sourceFile = project.getSourceFileOrThrow('src/missing-services.ts') +const sourceFiles = [ + 'src/missing-services.ts', + 'src/service-override/tools/missingMainThreadCustomers.ts' +].map((path) => project.getSourceFileOrThrow(path)) const issues: string[] = [] -for (const cls of sourceFile.getClasses()) { +for (const cls of sourceFiles.flatMap((sourceFile) => sourceFile.getClasses())) { for (const prop of cls.getProperties()) { const initText = prop.getInitializer()?.getText().trim() const usesUnsupported = initText === 'unsupported' @@ -26,4 +29,4 @@ if (issues.length) { console.error(issues.join('\n')) process.exit(1) } -console.log('✅ All consistent') \ No newline at end of file +console.log('✅ All consistent') diff --git a/src/missing-services.ts b/src/missing-services.ts index 8bd767e1..5fedfaef 100644 --- a/src/missing-services.ts +++ b/src/missing-services.ts @@ -511,31 +511,7 @@ import { getBuiltInExtensionTranslationsUris, getExtensionIdProvidingCurrentLocale } from './l10n.js' -import { noop, unsupported } from './tools.js' -import { - MainContext, - type MainThreadChatAgentsShape2, - type MainThreadChatContextShape, - type MainThreadChatDebugShape, - type MainThreadChatInputNotificationShape, - type MainThreadChatQuotaShape, - type MainThreadChatSessionsShape, - type MainThreadChatStatusShape, - type MainThreadCodeMapperShape, - type MainThreadLanguageModelToolsShape, - type MainThreadLanguageModelsShape, - type MainThreadMcpShape, - type MainThreadNotebookDocumentsShape, - type MainThreadNotebookEditorsShape, - type MainThreadNotebookKernelsShape, - type MainThreadNotebookRenderersShape, - type MainThreadNotebookShape -} from 'vs/workbench/api/common/extHost.protocol' -import { - extHostCustomer, - type IExtHostContext -} from 'vs/workbench/services/extensions/common/extHostCustomers' -import type { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier' +import { Unsupported, unsupported } from './tools.js' import { NullAgentHostService } from 'vs/platform/agentHost/browser/nullAgentHostService' import { NullSSHRemoteAgentHostService } from 'vs/platform/agentHost/browser/nullSshRemoteAgentHostService' @@ -607,29 +583,6 @@ import { IAgentHostCustomizationService } from 'vs/workbench/contrib/chat/browse import { IAgentHostActiveClientService } from 'vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostActiveClientService.service' import { IGitHubService } from 'vs/platform/github/common/githubService.service.js' -function Unsupported(target: object, propertyKey: string, descriptor?: PropertyDescriptor) { - function unsupported() { - throw new Error( - `Unsupported: ${target.constructor.name}.${propertyKey} is not supported. You are using a feature without registering the corresponding service override.` - ) - } - if (descriptor != null) { - if (descriptor.value != null) { - descriptor.value = unsupported - } else if (descriptor.get != null) { - descriptor.get = unsupported - } - } else { - Object.defineProperty(target, propertyKey, { - get() { - unsupported() - }, - set() {}, - configurable: true, - enumerable: true - }) - } -} /** * Editor services: all editor service that are not defined in vs/editor/standalone/ */ @@ -8492,268 +8445,3 @@ class GitHubService implements IGitHubService { } registerSingleton(IGitHubService, GitHubService, InstantiationType.Delayed) - -// Main thread counterparts of the extension host API of optional features (chat, language models, notebooks, mcp) -// Their actual implementations are only registered by the corresponding service overrides, but the extension host -// requires all of them to be registered, and some of them are called when the extension host starts -// (e.g. `ExtHostLanguageModelTools` calls `$getTools`) - -class MissingMainThreadLanguageModels implements MainThreadLanguageModelsShape { - $registerLanguageModelProvider: MainThreadLanguageModelsShape['$registerLanguageModelProvider'] = - noop - $onLMProviderChange: MainThreadLanguageModelsShape['$onLMProviderChange'] = noop - $unregisterProvider: MainThreadLanguageModelsShape['$unregisterProvider'] = noop - @Unsupported - $tryStartChatRequest: MainThreadLanguageModelsShape['$tryStartChatRequest'] = unsupported - $reportResponsePart: MainThreadLanguageModelsShape['$reportResponsePart'] = async () => {} - $reportResponseDone: MainThreadLanguageModelsShape['$reportResponseDone'] = async () => {} - $selectChatModels: MainThreadLanguageModelsShape['$selectChatModels'] = async () => [] - @Unsupported - $countTokens: MainThreadLanguageModelsShape['$countTokens'] = unsupported - $cancelLanguageModelChatRequest: MainThreadLanguageModelsShape['$cancelLanguageModelChatRequest'] = - noop - $fileIsIgnored: MainThreadLanguageModelsShape['$fileIsIgnored'] = async () => false - $registerFileIgnoreProvider: MainThreadLanguageModelsShape['$registerFileIgnoreProvider'] = noop - $unregisterFileIgnoreProvider: MainThreadLanguageModelsShape['$unregisterFileIgnoreProvider'] = - noop - dispose(): void {} -} - -class MissingMainThreadChatAgents2 implements MainThreadChatAgentsShape2 { - $handleProgressChunk: MainThreadChatAgentsShape2['$handleProgressChunk'] = async () => {} - $handleAnchorResolve: MainThreadChatAgentsShape2['$handleAnchorResolve'] = noop - $registerAgent: MainThreadChatAgentsShape2['$registerAgent'] = noop - $registerChatParticipantDetectionProvider: MainThreadChatAgentsShape2['$registerChatParticipantDetectionProvider'] = - noop - $unregisterChatParticipantDetectionProvider: MainThreadChatAgentsShape2['$unregisterChatParticipantDetectionProvider'] = - noop - $registerPromptFileProvider: MainThreadChatAgentsShape2['$registerPromptFileProvider'] = noop - $unregisterPromptFileProvider: MainThreadChatAgentsShape2['$unregisterPromptFileProvider'] = noop - $onDidChangePromptFiles: MainThreadChatAgentsShape2['$onDidChangePromptFiles'] = noop - $registerChatSessionCustomizationProvider: MainThreadChatAgentsShape2['$registerChatSessionCustomizationProvider'] = - noop - $unregisterChatSessionCustomizationProvider: MainThreadChatAgentsShape2['$unregisterChatSessionCustomizationProvider'] = - noop - $onDidChangeCustomizations: MainThreadChatAgentsShape2['$onDidChangeCustomizations'] = noop - $registerAgentCompletionsProvider: MainThreadChatAgentsShape2['$registerAgentCompletionsProvider'] = - noop - $unregisterAgentCompletionsProvider: MainThreadChatAgentsShape2['$unregisterAgentCompletionsProvider'] = - noop - $updateAgent: MainThreadChatAgentsShape2['$updateAgent'] = noop - $unregisterAgent: MainThreadChatAgentsShape2['$unregisterAgent'] = noop - @Unsupported - $transferActiveChatSession: MainThreadChatAgentsShape2['$transferActiveChatSession'] = unsupported - $provideCustomAgents: MainThreadChatAgentsShape2['$provideCustomAgents'] = async () => [] - $provideInstructions: MainThreadChatAgentsShape2['$provideInstructions'] = async () => [] - $provideSkills: MainThreadChatAgentsShape2['$provideSkills'] = async () => [] - $provideSlashCommands: MainThreadChatAgentsShape2['$provideSlashCommands'] = async () => [] - $provideHooks: MainThreadChatAgentsShape2['$provideHooks'] = async () => [] - $providePlugins: MainThreadChatAgentsShape2['$providePlugins'] = async () => [] - dispose(): void {} -} - -class MissingMainThreadCodeMapper implements MainThreadCodeMapperShape { - $registerCodeMapperProvider: MainThreadCodeMapperShape['$registerCodeMapperProvider'] = noop - $unregisterCodeMapperProvider: MainThreadCodeMapperShape['$unregisterCodeMapperProvider'] = noop - $handleProgress: MainThreadCodeMapperShape['$handleProgress'] = async () => {} - dispose(): void {} -} - -class MissingMainThreadLanguageModelTools implements MainThreadLanguageModelToolsShape { - $getTools: MainThreadLanguageModelToolsShape['$getTools'] = async () => [] - $acceptToolProgress: MainThreadLanguageModelToolsShape['$acceptToolProgress'] = noop - @Unsupported - $invokeTool: MainThreadLanguageModelToolsShape['$invokeTool'] = unsupported - @Unsupported - $countTokensForInvocation: MainThreadLanguageModelToolsShape['$countTokensForInvocation'] = - unsupported - $registerTool: MainThreadLanguageModelToolsShape['$registerTool'] = noop - $registerToolWithDefinition: MainThreadLanguageModelToolsShape['$registerToolWithDefinition'] = - noop - $unregisterTool: MainThreadLanguageModelToolsShape['$unregisterTool'] = noop - dispose(): void {} -} - -class MissingMainThreadChatContext implements MainThreadChatContextShape { - $registerChatWorkspaceContextProvider: MainThreadChatContextShape['$registerChatWorkspaceContextProvider'] = - noop - $registerChatExplicitContextProvider: MainThreadChatContextShape['$registerChatExplicitContextProvider'] = - noop - $registerChatResourceContextProvider: MainThreadChatContextShape['$registerChatResourceContextProvider'] = - noop - $unregisterChatContextProvider: MainThreadChatContextShape['$unregisterChatContextProvider'] = - noop - $updateWorkspaceContextItems: MainThreadChatContextShape['$updateWorkspaceContextItems'] = noop - @Unsupported - $executeChatContextItemCommand: MainThreadChatContextShape['$executeChatContextItemCommand'] = - unsupported - dispose(): void {} -} - -class MissingMainThreadChatDebug implements MainThreadChatDebugShape { - $registerChatDebugLogProvider: MainThreadChatDebugShape['$registerChatDebugLogProvider'] = noop - $unregisterChatDebugLogProvider: MainThreadChatDebugShape['$unregisterChatDebugLogProvider'] = - noop - $acceptChatDebugEvent: MainThreadChatDebugShape['$acceptChatDebugEvent'] = noop - $subscribeToCoreDebugEvents: MainThreadChatDebugShape['$subscribeToCoreDebugEvents'] = noop - $unsubscribeFromCoreDebugEvents: MainThreadChatDebugShape['$unsubscribeFromCoreDebugEvents'] = - noop - dispose(): void {} -} - -class MissingMainThreadChatStatus implements MainThreadChatStatusShape { - $setEntry: MainThreadChatStatusShape['$setEntry'] = noop - $disposeEntry: MainThreadChatStatusShape['$disposeEntry'] = noop - dispose(): void {} -} - -class MissingMainThreadChatQuota implements MainThreadChatQuotaShape { - $updateQuotas: MainThreadChatQuotaShape['$updateQuotas'] = noop - dispose(): void {} -} - -class MissingMainThreadChatInputNotification implements MainThreadChatInputNotificationShape { - $setNotification: MainThreadChatInputNotificationShape['$setNotification'] = noop - $disposeNotification: MainThreadChatInputNotificationShape['$disposeNotification'] = noop - dispose(): void {} -} - -class MissingMainThreadChatSessions implements MainThreadChatSessionsShape { - $registerChatSessionItemController: MainThreadChatSessionsShape['$registerChatSessionItemController'] = - noop - $updateChatSessionItemControllerCapabilities: MainThreadChatSessionsShape['$updateChatSessionItemControllerCapabilities'] = - noop - $unregisterChatSessionItemController: MainThreadChatSessionsShape['$unregisterChatSessionItemController'] = - noop - $updateChatSessionItems: MainThreadChatSessionsShape['$updateChatSessionItems'] = async () => {} - $addOrUpdateChatSessionItem: MainThreadChatSessionsShape['$addOrUpdateChatSessionItem'] = - async () => {} - $onDidCommitChatSessionItem: MainThreadChatSessionsShape['$onDidCommitChatSessionItem'] = noop - $registerChatSessionContentProvider: MainThreadChatSessionsShape['$registerChatSessionContentProvider'] = - noop - $unregisterChatSessionContentProvider: MainThreadChatSessionsShape['$unregisterChatSessionContentProvider'] = - noop - $onDidChangeChatSessionOptions: MainThreadChatSessionsShape['$onDidChangeChatSessionOptions'] = - noop - $onDidChangeChatSessionProviderOptions: MainThreadChatSessionsShape['$onDidChangeChatSessionProviderOptions'] = - noop - $updateChatSessionInputState: MainThreadChatSessionsShape['$updateChatSessionInputState'] = noop - $handleProgressChunk: MainThreadChatSessionsShape['$handleProgressChunk'] = async () => {} - $handleAnchorResolve: MainThreadChatSessionsShape['$handleAnchorResolve'] = noop - $handleProgressComplete: MainThreadChatSessionsShape['$handleProgressComplete'] = noop - dispose(): void {} -} - -class MissingMainThreadNotebook implements MainThreadNotebookShape { - $registerNotebookSerializer: MainThreadNotebookShape['$registerNotebookSerializer'] = noop - $unregisterNotebookSerializer: MainThreadNotebookShape['$unregisterNotebookSerializer'] = noop - $registerNotebookCellStatusBarItemProvider: MainThreadNotebookShape['$registerNotebookCellStatusBarItemProvider'] = - async () => {} - $unregisterNotebookCellStatusBarItemProvider: MainThreadNotebookShape['$unregisterNotebookCellStatusBarItemProvider'] = - async () => {} - $emitCellStatusBarEvent: MainThreadNotebookShape['$emitCellStatusBarEvent'] = noop - dispose(): void {} -} - -class MissingMainThreadNotebookKernels implements MainThreadNotebookKernelsShape { - $postMessage: MainThreadNotebookKernelsShape['$postMessage'] = async () => false - $addKernel: MainThreadNotebookKernelsShape['$addKernel'] = async () => {} - $updateKernel: MainThreadNotebookKernelsShape['$updateKernel'] = noop - $removeKernel: MainThreadNotebookKernelsShape['$removeKernel'] = noop - $updateNotebookPriority: MainThreadNotebookKernelsShape['$updateNotebookPriority'] = noop - $createExecution: MainThreadNotebookKernelsShape['$createExecution'] = noop - $updateExecution: MainThreadNotebookKernelsShape['$updateExecution'] = noop - $completeExecution: MainThreadNotebookKernelsShape['$completeExecution'] = noop - $createNotebookExecution: MainThreadNotebookKernelsShape['$createNotebookExecution'] = noop - $beginNotebookExecution: MainThreadNotebookKernelsShape['$beginNotebookExecution'] = noop - $completeNotebookExecution: MainThreadNotebookKernelsShape['$completeNotebookExecution'] = noop - $addKernelDetectionTask: MainThreadNotebookKernelsShape['$addKernelDetectionTask'] = - async () => {} - $removeKernelDetectionTask: MainThreadNotebookKernelsShape['$removeKernelDetectionTask'] = noop - $addKernelSourceActionProvider: MainThreadNotebookKernelsShape['$addKernelSourceActionProvider'] = - async () => {} - $removeKernelSourceActionProvider: MainThreadNotebookKernelsShape['$removeKernelSourceActionProvider'] = - noop - $emitNotebookKernelSourceActionsChangeEvent: MainThreadNotebookKernelsShape['$emitNotebookKernelSourceActionsChangeEvent'] = - noop - $receiveVariable: MainThreadNotebookKernelsShape['$receiveVariable'] = noop - $variablesUpdated: MainThreadNotebookKernelsShape['$variablesUpdated'] = noop - dispose(): void {} -} - -class MissingMainThreadNotebookDocuments implements MainThreadNotebookDocumentsShape { - @Unsupported - $tryCreateNotebook: MainThreadNotebookDocumentsShape['$tryCreateNotebook'] = unsupported - @Unsupported - $tryOpenNotebook: MainThreadNotebookDocumentsShape['$tryOpenNotebook'] = unsupported - @Unsupported - $trySaveNotebook: MainThreadNotebookDocumentsShape['$trySaveNotebook'] = unsupported - dispose(): void {} -} - -class MissingMainThreadNotebookEditors implements MainThreadNotebookEditorsShape { - @Unsupported - $tryShowNotebookDocument: MainThreadNotebookEditorsShape['$tryShowNotebookDocument'] = unsupported - @Unsupported - $tryRevealRange: MainThreadNotebookEditorsShape['$tryRevealRange'] = unsupported - $trySetSelections: MainThreadNotebookEditorsShape['$trySetSelections'] = noop - dispose(): void {} -} - -class MissingMainThreadNotebookRenderers implements MainThreadNotebookRenderersShape { - $postMessage: MainThreadNotebookRenderersShape['$postMessage'] = async () => false - dispose(): void {} -} - -class MissingMainThreadMcp implements MainThreadMcpShape { - $onDidChangeState: MainThreadMcpShape['$onDidChangeState'] = noop - $onDidPublishLog: MainThreadMcpShape['$onDidPublishLog'] = noop - $onDidReceiveMessage: MainThreadMcpShape['$onDidReceiveMessage'] = noop - $upsertMcpCollection: MainThreadMcpShape['$upsertMcpCollection'] = noop - $deleteMcpCollection: MainThreadMcpShape['$deleteMcpCollection'] = noop - @Unsupported - $getTokenFromServerMetadata: MainThreadMcpShape['$getTokenFromServerMetadata'] = unsupported - @Unsupported - $getTokenForProviderId: MainThreadMcpShape['$getTokenForProviderId'] = unsupported - $logMcpAuthSetup: MainThreadMcpShape['$logMcpAuthSetup'] = noop - @Unsupported - $startMcpGateway: MainThreadMcpShape['$startMcpGateway'] = unsupported - $disposeMcpGateway: MainThreadMcpShape['$disposeMcpGateway'] = noop - dispose(): void {} -} - -const missingMainThreadCustomers: [ProxyIdentifier, new () => IDisposable][] = [ - [MainContext.MainThreadLanguageModels, MissingMainThreadLanguageModels], - [MainContext.MainThreadChatAgents2, MissingMainThreadChatAgents2], - [MainContext.MainThreadCodeMapper, MissingMainThreadCodeMapper], - [MainContext.MainThreadLanguageModelTools, MissingMainThreadLanguageModelTools], - [MainContext.MainThreadChatContext, MissingMainThreadChatContext], - [MainContext.MainThreadChatDebug, MissingMainThreadChatDebug], - [MainContext.MainThreadChatStatus, MissingMainThreadChatStatus], - [MainContext.MainThreadChatQuota, MissingMainThreadChatQuota], - [MainContext.MainThreadChatInputNotification, MissingMainThreadChatInputNotification], - [MainContext.MainThreadChatSessions, MissingMainThreadChatSessions], - [MainContext.MainThreadNotebook, MissingMainThreadNotebook], - [MainContext.MainThreadNotebookKernels, MissingMainThreadNotebookKernels], - [MainContext.MainThreadNotebookDocuments, MissingMainThreadNotebookDocuments], - [MainContext.MainThreadNotebookEditors, MissingMainThreadNotebookEditors], - [MainContext.MainThreadNotebookRenderers, MissingMainThreadNotebookRenderers], - [MainContext.MainThreadMcp, MissingMainThreadMcp] -] - -// Unnamed customers are instantiated after the named ones, and a real implementation set later replaces the missing one, -// so it only registers the missing implementations whatever the import order is -class MissingMainThreadCustomers implements IDisposable { - constructor(extHostContext: IExtHostContext) { - for (const [id, ctor] of missingMainThreadCustomers) { - try { - extHostContext.assertRegistered([id]) - } catch { - extHostContext.set(id, new ctor()) - } - } - } - - dispose(): void {} -} -extHostCustomer(MissingMainThreadCustomers) diff --git a/src/service-override/extensions.ts b/src/service-override/extensions.ts index a2d31625..868bad80 100644 --- a/src/service-override/extensions.ts +++ b/src/service-override/extensions.ts @@ -70,6 +70,7 @@ import { import * as platform from 'vs/base/common/platform' import { getBuiltInExtensionTranslationsUris } from '../l10n.js' import 'vs/workbench/api/browser/extensionHost.base.contribution' +import './tools/missingMainThreadCustomers.js' export interface WorkerConfig { url: string diff --git a/src/service-override/tools/missingMainThreadCustomers.ts b/src/service-override/tools/missingMainThreadCustomers.ts new file mode 100644 index 00000000..1c195770 --- /dev/null +++ b/src/service-override/tools/missingMainThreadCustomers.ts @@ -0,0 +1,291 @@ +import type { IDisposable } from 'vs/base/common/lifecycle' +import { + MainContext, + type MainThreadChatAgentsShape2, + type MainThreadChatContextShape, + type MainThreadChatDebugShape, + type MainThreadChatInputNotificationShape, + type MainThreadChatQuotaShape, + type MainThreadChatSessionsShape, + type MainThreadChatStatusShape, + type MainThreadCodeMapperShape, + type MainThreadLanguageModelToolsShape, + type MainThreadLanguageModelsShape, + type MainThreadMcpShape, + type MainThreadNotebookDocumentsShape, + type MainThreadNotebookEditorsShape, + type MainThreadNotebookKernelsShape, + type MainThreadNotebookRenderersShape, + type MainThreadNotebookShape +} from 'vs/workbench/api/common/extHost.protocol' +import { + extHostCustomer, + type IExtHostContext +} from 'vs/workbench/services/extensions/common/extHostCustomers' +import type { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier' +import { noop, Unsupported, unsupported } from '../../tools.js' + +// Main thread counterparts of the extension host API of optional features (chat, language models, notebooks, mcp) +// Their actual implementations are only registered by the corresponding service overrides, but the extension host +// requires all of them to be registered, and some of them are called when the extension host starts +// (e.g. `ExtHostLanguageModelTools` calls `$getTools`) + +class MissingMainThreadLanguageModels implements MainThreadLanguageModelsShape { + $registerLanguageModelProvider: MainThreadLanguageModelsShape['$registerLanguageModelProvider'] = + noop + $onLMProviderChange: MainThreadLanguageModelsShape['$onLMProviderChange'] = noop + $unregisterProvider: MainThreadLanguageModelsShape['$unregisterProvider'] = noop + @Unsupported + $tryStartChatRequest: MainThreadLanguageModelsShape['$tryStartChatRequest'] = unsupported + $reportResponsePart: MainThreadLanguageModelsShape['$reportResponsePart'] = async () => {} + $reportResponseDone: MainThreadLanguageModelsShape['$reportResponseDone'] = async () => {} + $selectChatModels: MainThreadLanguageModelsShape['$selectChatModels'] = async () => [] + @Unsupported + $countTokens: MainThreadLanguageModelsShape['$countTokens'] = unsupported + $cancelLanguageModelChatRequest: MainThreadLanguageModelsShape['$cancelLanguageModelChatRequest'] = + noop + $fileIsIgnored: MainThreadLanguageModelsShape['$fileIsIgnored'] = async () => false + $registerFileIgnoreProvider: MainThreadLanguageModelsShape['$registerFileIgnoreProvider'] = noop + $unregisterFileIgnoreProvider: MainThreadLanguageModelsShape['$unregisterFileIgnoreProvider'] = + noop + dispose(): void {} +} + +class MissingMainThreadChatAgents2 implements MainThreadChatAgentsShape2 { + $handleProgressChunk: MainThreadChatAgentsShape2['$handleProgressChunk'] = async () => {} + $handleAnchorResolve: MainThreadChatAgentsShape2['$handleAnchorResolve'] = noop + $registerAgent: MainThreadChatAgentsShape2['$registerAgent'] = noop + $registerChatParticipantDetectionProvider: MainThreadChatAgentsShape2['$registerChatParticipantDetectionProvider'] = + noop + $unregisterChatParticipantDetectionProvider: MainThreadChatAgentsShape2['$unregisterChatParticipantDetectionProvider'] = + noop + $registerPromptFileProvider: MainThreadChatAgentsShape2['$registerPromptFileProvider'] = noop + $unregisterPromptFileProvider: MainThreadChatAgentsShape2['$unregisterPromptFileProvider'] = noop + $onDidChangePromptFiles: MainThreadChatAgentsShape2['$onDidChangePromptFiles'] = noop + $registerChatSessionCustomizationProvider: MainThreadChatAgentsShape2['$registerChatSessionCustomizationProvider'] = + noop + $unregisterChatSessionCustomizationProvider: MainThreadChatAgentsShape2['$unregisterChatSessionCustomizationProvider'] = + noop + $onDidChangeCustomizations: MainThreadChatAgentsShape2['$onDidChangeCustomizations'] = noop + $registerAgentCompletionsProvider: MainThreadChatAgentsShape2['$registerAgentCompletionsProvider'] = + noop + $unregisterAgentCompletionsProvider: MainThreadChatAgentsShape2['$unregisterAgentCompletionsProvider'] = + noop + $updateAgent: MainThreadChatAgentsShape2['$updateAgent'] = noop + $unregisterAgent: MainThreadChatAgentsShape2['$unregisterAgent'] = noop + @Unsupported + $transferActiveChatSession: MainThreadChatAgentsShape2['$transferActiveChatSession'] = unsupported + $provideCustomAgents: MainThreadChatAgentsShape2['$provideCustomAgents'] = async () => [] + $provideInstructions: MainThreadChatAgentsShape2['$provideInstructions'] = async () => [] + $provideSkills: MainThreadChatAgentsShape2['$provideSkills'] = async () => [] + $provideSlashCommands: MainThreadChatAgentsShape2['$provideSlashCommands'] = async () => [] + $provideHooks: MainThreadChatAgentsShape2['$provideHooks'] = async () => [] + $providePlugins: MainThreadChatAgentsShape2['$providePlugins'] = async () => [] + dispose(): void {} +} + +class MissingMainThreadCodeMapper implements MainThreadCodeMapperShape { + $registerCodeMapperProvider: MainThreadCodeMapperShape['$registerCodeMapperProvider'] = noop + $unregisterCodeMapperProvider: MainThreadCodeMapperShape['$unregisterCodeMapperProvider'] = noop + $handleProgress: MainThreadCodeMapperShape['$handleProgress'] = async () => {} + dispose(): void {} +} + +class MissingMainThreadLanguageModelTools implements MainThreadLanguageModelToolsShape { + $getTools: MainThreadLanguageModelToolsShape['$getTools'] = async () => [] + $acceptToolProgress: MainThreadLanguageModelToolsShape['$acceptToolProgress'] = noop + @Unsupported + $invokeTool: MainThreadLanguageModelToolsShape['$invokeTool'] = unsupported + @Unsupported + $countTokensForInvocation: MainThreadLanguageModelToolsShape['$countTokensForInvocation'] = + unsupported + $registerTool: MainThreadLanguageModelToolsShape['$registerTool'] = noop + $registerToolWithDefinition: MainThreadLanguageModelToolsShape['$registerToolWithDefinition'] = + noop + $unregisterTool: MainThreadLanguageModelToolsShape['$unregisterTool'] = noop + dispose(): void {} +} + +class MissingMainThreadChatContext implements MainThreadChatContextShape { + $registerChatWorkspaceContextProvider: MainThreadChatContextShape['$registerChatWorkspaceContextProvider'] = + noop + $registerChatExplicitContextProvider: MainThreadChatContextShape['$registerChatExplicitContextProvider'] = + noop + $registerChatResourceContextProvider: MainThreadChatContextShape['$registerChatResourceContextProvider'] = + noop + $unregisterChatContextProvider: MainThreadChatContextShape['$unregisterChatContextProvider'] = + noop + $updateWorkspaceContextItems: MainThreadChatContextShape['$updateWorkspaceContextItems'] = noop + @Unsupported + $executeChatContextItemCommand: MainThreadChatContextShape['$executeChatContextItemCommand'] = + unsupported + dispose(): void {} +} + +class MissingMainThreadChatDebug implements MainThreadChatDebugShape { + $registerChatDebugLogProvider: MainThreadChatDebugShape['$registerChatDebugLogProvider'] = noop + $unregisterChatDebugLogProvider: MainThreadChatDebugShape['$unregisterChatDebugLogProvider'] = + noop + $acceptChatDebugEvent: MainThreadChatDebugShape['$acceptChatDebugEvent'] = noop + $subscribeToCoreDebugEvents: MainThreadChatDebugShape['$subscribeToCoreDebugEvents'] = noop + $unsubscribeFromCoreDebugEvents: MainThreadChatDebugShape['$unsubscribeFromCoreDebugEvents'] = + noop + dispose(): void {} +} + +class MissingMainThreadChatStatus implements MainThreadChatStatusShape { + $setEntry: MainThreadChatStatusShape['$setEntry'] = noop + $disposeEntry: MainThreadChatStatusShape['$disposeEntry'] = noop + dispose(): void {} +} + +class MissingMainThreadChatQuota implements MainThreadChatQuotaShape { + $updateQuotas: MainThreadChatQuotaShape['$updateQuotas'] = noop + dispose(): void {} +} + +class MissingMainThreadChatInputNotification implements MainThreadChatInputNotificationShape { + $setNotification: MainThreadChatInputNotificationShape['$setNotification'] = noop + $disposeNotification: MainThreadChatInputNotificationShape['$disposeNotification'] = noop + dispose(): void {} +} + +class MissingMainThreadChatSessions implements MainThreadChatSessionsShape { + $registerChatSessionItemController: MainThreadChatSessionsShape['$registerChatSessionItemController'] = + noop + $updateChatSessionItemControllerCapabilities: MainThreadChatSessionsShape['$updateChatSessionItemControllerCapabilities'] = + noop + $unregisterChatSessionItemController: MainThreadChatSessionsShape['$unregisterChatSessionItemController'] = + noop + $updateChatSessionItems: MainThreadChatSessionsShape['$updateChatSessionItems'] = async () => {} + $addOrUpdateChatSessionItem: MainThreadChatSessionsShape['$addOrUpdateChatSessionItem'] = + async () => {} + $onDidCommitChatSessionItem: MainThreadChatSessionsShape['$onDidCommitChatSessionItem'] = noop + $registerChatSessionContentProvider: MainThreadChatSessionsShape['$registerChatSessionContentProvider'] = + noop + $unregisterChatSessionContentProvider: MainThreadChatSessionsShape['$unregisterChatSessionContentProvider'] = + noop + $onDidChangeChatSessionOptions: MainThreadChatSessionsShape['$onDidChangeChatSessionOptions'] = + noop + $onDidChangeChatSessionProviderOptions: MainThreadChatSessionsShape['$onDidChangeChatSessionProviderOptions'] = + noop + $updateChatSessionInputState: MainThreadChatSessionsShape['$updateChatSessionInputState'] = noop + $handleProgressChunk: MainThreadChatSessionsShape['$handleProgressChunk'] = async () => {} + $handleAnchorResolve: MainThreadChatSessionsShape['$handleAnchorResolve'] = noop + $handleProgressComplete: MainThreadChatSessionsShape['$handleProgressComplete'] = noop + dispose(): void {} +} + +class MissingMainThreadNotebook implements MainThreadNotebookShape { + $registerNotebookSerializer: MainThreadNotebookShape['$registerNotebookSerializer'] = noop + $unregisterNotebookSerializer: MainThreadNotebookShape['$unregisterNotebookSerializer'] = noop + $registerNotebookCellStatusBarItemProvider: MainThreadNotebookShape['$registerNotebookCellStatusBarItemProvider'] = + async () => {} + $unregisterNotebookCellStatusBarItemProvider: MainThreadNotebookShape['$unregisterNotebookCellStatusBarItemProvider'] = + async () => {} + $emitCellStatusBarEvent: MainThreadNotebookShape['$emitCellStatusBarEvent'] = noop + dispose(): void {} +} + +class MissingMainThreadNotebookKernels implements MainThreadNotebookKernelsShape { + $postMessage: MainThreadNotebookKernelsShape['$postMessage'] = async () => false + $addKernel: MainThreadNotebookKernelsShape['$addKernel'] = async () => {} + $updateKernel: MainThreadNotebookKernelsShape['$updateKernel'] = noop + $removeKernel: MainThreadNotebookKernelsShape['$removeKernel'] = noop + $updateNotebookPriority: MainThreadNotebookKernelsShape['$updateNotebookPriority'] = noop + $createExecution: MainThreadNotebookKernelsShape['$createExecution'] = noop + $updateExecution: MainThreadNotebookKernelsShape['$updateExecution'] = noop + $completeExecution: MainThreadNotebookKernelsShape['$completeExecution'] = noop + $createNotebookExecution: MainThreadNotebookKernelsShape['$createNotebookExecution'] = noop + $beginNotebookExecution: MainThreadNotebookKernelsShape['$beginNotebookExecution'] = noop + $completeNotebookExecution: MainThreadNotebookKernelsShape['$completeNotebookExecution'] = noop + $addKernelDetectionTask: MainThreadNotebookKernelsShape['$addKernelDetectionTask'] = + async () => {} + $removeKernelDetectionTask: MainThreadNotebookKernelsShape['$removeKernelDetectionTask'] = noop + $addKernelSourceActionProvider: MainThreadNotebookKernelsShape['$addKernelSourceActionProvider'] = + async () => {} + $removeKernelSourceActionProvider: MainThreadNotebookKernelsShape['$removeKernelSourceActionProvider'] = + noop + $emitNotebookKernelSourceActionsChangeEvent: MainThreadNotebookKernelsShape['$emitNotebookKernelSourceActionsChangeEvent'] = + noop + $receiveVariable: MainThreadNotebookKernelsShape['$receiveVariable'] = noop + $variablesUpdated: MainThreadNotebookKernelsShape['$variablesUpdated'] = noop + dispose(): void {} +} + +class MissingMainThreadNotebookDocuments implements MainThreadNotebookDocumentsShape { + @Unsupported + $tryCreateNotebook: MainThreadNotebookDocumentsShape['$tryCreateNotebook'] = unsupported + @Unsupported + $tryOpenNotebook: MainThreadNotebookDocumentsShape['$tryOpenNotebook'] = unsupported + @Unsupported + $trySaveNotebook: MainThreadNotebookDocumentsShape['$trySaveNotebook'] = unsupported + dispose(): void {} +} + +class MissingMainThreadNotebookEditors implements MainThreadNotebookEditorsShape { + @Unsupported + $tryShowNotebookDocument: MainThreadNotebookEditorsShape['$tryShowNotebookDocument'] = unsupported + @Unsupported + $tryRevealRange: MainThreadNotebookEditorsShape['$tryRevealRange'] = unsupported + $trySetSelections: MainThreadNotebookEditorsShape['$trySetSelections'] = noop + dispose(): void {} +} + +class MissingMainThreadNotebookRenderers implements MainThreadNotebookRenderersShape { + $postMessage: MainThreadNotebookRenderersShape['$postMessage'] = async () => false + dispose(): void {} +} + +class MissingMainThreadMcp implements MainThreadMcpShape { + $onDidChangeState: MainThreadMcpShape['$onDidChangeState'] = noop + $onDidPublishLog: MainThreadMcpShape['$onDidPublishLog'] = noop + $onDidReceiveMessage: MainThreadMcpShape['$onDidReceiveMessage'] = noop + $upsertMcpCollection: MainThreadMcpShape['$upsertMcpCollection'] = noop + $deleteMcpCollection: MainThreadMcpShape['$deleteMcpCollection'] = noop + @Unsupported + $getTokenFromServerMetadata: MainThreadMcpShape['$getTokenFromServerMetadata'] = unsupported + @Unsupported + $getTokenForProviderId: MainThreadMcpShape['$getTokenForProviderId'] = unsupported + $logMcpAuthSetup: MainThreadMcpShape['$logMcpAuthSetup'] = noop + @Unsupported + $startMcpGateway: MainThreadMcpShape['$startMcpGateway'] = unsupported + $disposeMcpGateway: MainThreadMcpShape['$disposeMcpGateway'] = noop + dispose(): void {} +} + +const missingMainThreadCustomers: [ProxyIdentifier, new () => IDisposable][] = [ + [MainContext.MainThreadLanguageModels, MissingMainThreadLanguageModels], + [MainContext.MainThreadChatAgents2, MissingMainThreadChatAgents2], + [MainContext.MainThreadCodeMapper, MissingMainThreadCodeMapper], + [MainContext.MainThreadLanguageModelTools, MissingMainThreadLanguageModelTools], + [MainContext.MainThreadChatContext, MissingMainThreadChatContext], + [MainContext.MainThreadChatDebug, MissingMainThreadChatDebug], + [MainContext.MainThreadChatStatus, MissingMainThreadChatStatus], + [MainContext.MainThreadChatQuota, MissingMainThreadChatQuota], + [MainContext.MainThreadChatInputNotification, MissingMainThreadChatInputNotification], + [MainContext.MainThreadChatSessions, MissingMainThreadChatSessions], + [MainContext.MainThreadNotebook, MissingMainThreadNotebook], + [MainContext.MainThreadNotebookKernels, MissingMainThreadNotebookKernels], + [MainContext.MainThreadNotebookDocuments, MissingMainThreadNotebookDocuments], + [MainContext.MainThreadNotebookEditors, MissingMainThreadNotebookEditors], + [MainContext.MainThreadNotebookRenderers, MissingMainThreadNotebookRenderers], + [MainContext.MainThreadMcp, MissingMainThreadMcp] +] + +// Unnamed customers are instantiated after the named ones, and a real implementation set later replaces the missing one, +// so it only registers the missing implementations whatever the import order is +class MissingMainThreadCustomers implements IDisposable { + constructor(extHostContext: IExtHostContext) { + for (const [id, ctor] of missingMainThreadCustomers) { + try { + extHostContext.assertRegistered([id]) + } catch { + extHostContext.set(id, new ctor()) + } + } + } + + dispose(): void {} +} +extHostCustomer(MissingMainThreadCustomers) diff --git a/src/tools.ts b/src/tools.ts index 4a84e67a..04f1072f 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -4,6 +4,33 @@ export function unsupported(): never { export const noop = (): void => {} +/** + * Makes a property, method or getter of a missing implementation throw an explicit error + */ +export function Unsupported(target: object, propertyKey: string, descriptor?: PropertyDescriptor) { + function unsupported() { + throw new Error( + `Unsupported: ${target.constructor.name}.${propertyKey} is not supported. You are using a feature without registering the corresponding service override.` + ) + } + if (descriptor != null) { + if (descriptor.value != null) { + descriptor.value = unsupported + } else if (descriptor.get != null) { + descriptor.get = unsupported + } + } else { + Object.defineProperty(target, propertyKey, { + get() { + unsupported() + }, + set() {}, + configurable: true, + enumerable: true + }) + } +} + export function memoized(fct: (...args: A) => T): (...args: A) => T { let v: T | null = null return (...args) => {